2 # Copyright (C) 2004, 2005, 2007 Richard Kettlewell
4 # This program is free software; you can redistribute it and/or modify
5 # it under the terms of the GNU General Public License as published by
6 # the Free Software Foundation; either version 2 of the License, or
7 # (at your option) any later version.
9 # This program is distributed in the hope that it will be useful, but
10 # WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
12 # General Public License for more details.
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
20 """Python support for DisOrder
22 Provides disorder.client, a class for accessing a DisOrder server.
26 #! /usr/bin/env python
35 #! /usr/bin/env python
39 for path in sys.argv[1:]:
42 See disorder_protocol(5) for details of the communication protocol.
44 NB that this code only supports servers configured to use SHA1-based
45 authentication. If the server demands another hash then it will not be
46 possible to use this module.
59 _configfile = "pkgconfdir/config"
60 _dbhome = "pkgstatedir"
63 # various regexps we'll use
64 _ws = re.compile(r"^[ \t\n\r]+")
65 _squote = re.compile("'(([^\\\\']|\\\\[\\\\\"'n])+)'")
66 _dquote = re.compile("\"(([^\\\\\"]|\\\\[\\\\\"'n])+)\"")
67 _unquoted = re.compile("[^\"' \\t\\n\\r][^ \t\n\r]*")
69 _response = re.compile("([0-9]{3}) ?(.*)")
73 ########################################################################
76 class Error(Exception):
77 """Base class for DisOrder exceptions."""
79 class _splitError(Error):
81 def __init__(self, value):
84 return str(self.value)
86 class parseError(Error):
87 """Error parsing the configuration file."""
88 def __init__(self, path, line, details):
91 self.details = details
93 return "%s:%d: %s" % (self.path, self.line, self.details)
95 class protocolError(Error):
96 """DisOrder control protocol error.
98 Indicates a mismatch between the client and server's understanding of
101 def __init__(self, who, error):
105 return "%s: %s" % (self.who, str(self.error))
107 class operationError(Error):
108 """DisOrder control protocol error response.
110 Indicates that an operation failed (e.g. an attempt to play a
111 nonexistent track). The connection should still be usable.
113 def __init__(self, res, details, cmd=None):
116 self.details_ = details
118 """Return the complete response string from the server, with the command
121 Excludes the final newline.
123 if self.cmd_ is None:
124 return "%d %s" % (self.res_, self.details_)
126 return "%d %s [%s]" % (self.res_, self.details_, self.cmd_)
128 """Return the response code from the server."""
131 """Returns the detail string from the server."""
134 class communicationError(Error):
135 """DisOrder control protocol communication error.
137 Indicates that communication with the server went wrong, perhaps
138 because the server was restarted. The caller could report an error to
139 the user and wait for further user instructions, or even automatically
142 def __init__(self, who, error):
146 return "%s: %s" % (self.who, str(self.error))
148 ########################################################################
149 # DisOrder-specific text processing
152 # Unescape the contents of a string
156 # s -- string to unescape
158 s = re.sub("\\\\n", "\n", s)
159 s = re.sub("\\\\(.)", "\\1", s)
162 def _split(s, *comments):
163 # Split a string into fields according to the usual Disorder string splitting
168 # s -- string to parse
169 # comments -- if present, parse comments
173 # On success, a list of fields is returned.
175 # On error, disorder.parseError is thrown.
180 if comments and s[0] == '#':
187 # pick of quoted fields of both kinds
192 fields.append(_unescape(m.group(1)))
195 # and unquoted fields
196 m = _unquoted.match(s)
198 fields.append(m.group(0))
201 # anything left must be in error
202 if s[0] == '"' or s[0] == '\'':
203 raise _splitError("invalid quoted string")
205 raise _splitError("syntax error")
209 # Escape the contents of a string
213 # s -- string to escape
215 if re.search("[\\\\\"'\n \t\r]", s) or s == '':
216 s = re.sub(r'[\\"]', r'\\\g<0>', s)
217 s = re.sub("\n", r"\\n", s)
223 # Quote a list of values
224 return ' '.join(map(_escape, list))
227 # Return the value of s in a form suitable for writing to stderr
228 return s.encode(locale.nl_langinfo(locale.CODESET), 'replace')
231 # Convert a list of the form [k1, v1, k2, v2, ..., kN, vN]
232 # to a dictionary {k1:v1, k2:v2, ..., kN:vN}
240 except StopIteration:
245 # parse a queue entry
246 return _list2dict(_split(s))
248 ########################################################################
252 """DisOrder client class.
254 This class provides access to the DisOrder server either on this
255 machine or across the internet.
257 The server to connect to, and the username and password to use, are
258 determined from the configuration files as described in 'man
261 All methods will connect if necessary, as soon as you have a
262 disorder.client object you can start calling operational methods on
265 However if the server is restarted then the next method called on a
266 connection will throw an exception. This may be considered a bug.
268 All methods block until they complete.
270 Operation methods raise communicationError if the connection breaks,
271 protocolError if the response from the server is malformed, or
272 operationError if the response is valid but indicates that the
280 """Constructor for DisOrder client class.
282 The constructor reads the configuration file, but does not connect
285 If the environment variable DISORDER_PYTHON_DEBUG is set then the
286 debug flags are initialised to that value. This can be overridden
287 with the debug() method below.
289 The constructor Raises parseError() if the configuration file is not
292 pw = pwd.getpwuid(os.getuid())
293 self.debugging = int(os.getenv("DISORDER_PYTHON_DEBUG", 0))
294 self.config = { 'collections': [],
295 'username': pw.pw_name,
297 home = os.getenv("HOME")
300 privconf = _configfile + "." + pw.pw_name
301 passfile = home + os.sep + ".disorder" + os.sep + "passwd"
302 if os.path.exists(_configfile):
303 self._readfile(_configfile)
304 if os.path.exists(privconf):
305 self._readfile(privconf)
306 if os.path.exists(passfile) and _userconf:
307 self._readfile(passfile)
308 self.state = 'disconnected'
310 def debug(self, bits):
311 """Enable or disable protocol debugging. Debug messages are written
315 bits -- bitmap of operations that should generate debug information
318 debug_proto -- dump control protocol messages (excluding bodies)
319 debug_body -- dump control protocol message bodies
321 self.debugging = bits
323 def _debug(self, bit, s):
325 if self.debugging & bit:
326 sys.stderr.write(_sanitize(s))
327 sys.stderr.write("\n")
331 """Connect to the DisOrder server and authenticate.
333 Raises communicationError if connection fails and operationError if
334 authentication fails (in which case disconnection is automatic).
336 May be called more than once to retry connections (e.g. when the
337 server is down). If we are already connected and authenticated,
340 Other operations automatically connect if we're not already
341 connected, so it is not strictly necessary to call this method.
343 if self.state == 'disconnected':
345 self.state = 'connecting'
346 if 'connect' in self.config and len(self.config['connect']) > 0:
347 c = self.config['connect']
348 self.who = repr(c) # temporarily
350 a = socket.getaddrinfo(None, c[0],
356 a = socket.getaddrinfo(c[0], c[1],
362 s = socket.socket(a[0], a[1], a[2]);
364 self.who = "%s" % a[3]
366 s = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM);
367 self.who = self.config['home'] + os.sep + "socket"
369 self.w = s.makefile("wb")
370 self.r = s.makefile("rb")
371 (res, challenge) = self._simple()
373 h.update(self.config['password'])
374 h.update(binascii.unhexlify(challenge))
375 self._simple("user", self.config['username'], h.hexdigest())
376 self.state = 'connected'
377 except socket.error, e:
379 raise communicationError(self.who, e)
384 def _disconnect(self):
385 # disconnect from the server, whatever state we are in
391 self.state = 'disconnected'
393 ########################################################################
396 def become(self, who):
397 """Become another user.
400 who -- the user to become.
402 Only trusted users can perform this operation.
404 self._simple("become", who)
406 def play(self, track):
410 track -- the path of the track to play.
412 Returns the ID of the new queue entry.
414 Note that queue IDs are unicode strings (because all track information
415 values are unicode strings).
417 res, details = self._simple("play", track)
418 return unicode(details) # because it's unicode in queue() output
420 def remove(self, track):
421 """Remove a track from the queue.
424 track -- the path or ID of the track to remove.
426 self._simple("remove", track)
429 """Enable playing."""
430 self._simple("enable")
432 def disable(self, *now):
436 now -- if present (with any value), the current track is stopped
440 self._simple("disable", "now")
442 self._simple("disable")
444 def scratch(self, *id):
445 """Scratch the currently playing track.
448 id -- if present, the ID of the track to scratch.
451 self._simple("scratch", id[0])
453 self._simple("scratch")
456 """Shut down the server.
458 Only trusted users can perform this operation.
460 self._simple("shutdown")
462 def reconfigure(self):
463 """Make the server reload its configuration.
465 Only trusted users can perform this operation.
467 self._simple("reconfigure")
469 def rescan(self, pattern):
470 """Rescan one or more collections.
473 pattern -- glob pattern matching collections to rescan.
475 Only trusted users can perform this operation.
477 self._simple("rescan", pattern)
480 """Return the server's version number."""
481 return self._simple("version")[1]
484 """Return the currently playing track.
486 If a track is playing then it is returned as a dictionary. See
487 disorder_protocol(5) for the meanings of the keys. All keys are
488 plain strings but the values will be unicode strings.
490 If no track is playing then None is returned."""
491 res, details = self._simple("playing")
494 return _queueEntry(details)
495 except _splitError, s:
496 raise protocolError(self.who, s.str())
500 def _somequeue(self, command):
501 self._simple(command)
503 return map(lambda s: _queueEntry(s), self._body())
504 except _splitError, s:
505 raise protocolError(self.who, s.str())
508 """Return a list of recently played tracks.
510 The return value is a list of dictionaries corresponding to
511 recently played tracks. The oldest track comes first.
513 See disorder_protocol(5) for the meanings of the keys. All keys are
514 plain strings but the values will be unicode strings."""
515 return self._somequeue("recent")
518 """Return the current queue.
520 The return value is a list of dictionaries corresponding to
521 recently played tracks. The next track to be played comes first.
523 See disorder_protocol(5) for the meanings of the keys. All keys are
524 plain strings but the values will be unicode strings."""
525 return self._somequeue("queue")
527 def _somedir(self, command, dir, re):
529 self._simple(command, dir, re[0])
531 self._simple(command, dir)
534 def directories(self, dir, *re):
535 """List subdirectories of a directory.
538 dir -- directory to list, or '' for the whole root.
539 re -- regexp that results must match. Optional.
541 The return value is a list of the (nonempty) subdirectories of dir.
542 If dir is '' then a list of top-level directories is returned.
544 If a regexp is specified then the basename of each result must
545 match. Matching is case-independent. See pcrepattern(3).
547 return self._somedir("dirs", dir, re)
549 def files(self, dir, *re):
550 """List files within a directory.
553 dir -- directory to list, or '' for the whole root.
554 re -- regexp that results must match. Optional.
556 The return value is a list of playable files in dir. If dir is ''
557 then a list of top-level files is returned.
559 If a regexp is specified then the basename of each result must
560 match. Matching is case-independent. See pcrepattern(3).
562 return self._somedir("files", dir, re)
564 def allfiles(self, dir, *re):
565 """List subdirectories and files within a directory.
568 dir -- directory to list, or '' for the whole root.
569 re -- regexp that results must match. Optional.
571 The return value is a list of all (nonempty) subdirectories and
572 files within dir. If dir is '' then a list of top-level files and
573 directories is returned.
575 If a regexp is specified then the basename of each result must
576 match. Matching is case-independent. See pcrepattern(3).
578 return self._somedir("allfiles", dir, re)
580 def set(self, track, key, value):
581 """Set a preference value.
584 track -- the track to modify
585 key -- the preference name
586 value -- the new preference value
588 self._simple("set", track, key, value)
590 def unset(self, track, key):
591 """Unset a preference value.
594 track -- the track to modify
595 key -- the preference to remove
597 self._simple("set", track, key, value)
599 def get(self, track, key):
600 """Get a preference value.
603 track -- the track to query
604 key -- the preference to remove
606 The return value is the preference.
608 ret, details = self._simple("get", track, key)
614 def prefs(self, track):
615 """Get all the preferences for a track.
618 track -- the track to query
620 The return value is a dictionary of all the track's preferences.
621 Note that even nominally numeric values remain encoded as strings.
623 self._simple("prefs", track)
625 for line in self._body():
628 except _splitError, s:
629 raise protocolError(self.who, s.str())
631 raise protocolError(self.who, "invalid prefs body line")
635 def _boolean(self, s):
638 def exists(self, track):
639 """Return true if a track exists
642 track -- the track to check for"""
643 return self._boolean(self._simple("exists", track))
646 """Return true if playing is enabled"""
647 return self._boolean(self._simple("enabled"))
649 def random_enabled(self):
650 """Return true if random play is enabled"""
651 return self._boolean(self._simple("random-enabled"))
653 def random_enable(self):
654 """Enable random play."""
655 self._simple("random-enable")
657 def random_disable(self):
658 """Disable random play."""
659 self._simple("random-disable")
661 def length(self, track):
662 """Return the length of a track in seconds.
665 track -- the track to query.
667 ret, details = self._simple("length", track)
670 def search(self, words):
671 """Search for tracks.
674 words -- the set of words to search for.
676 The return value is a list of track path names, all of which contain
677 all of the required words (in their path name, trackname
680 self._simple("search", _quote(words))
686 The return value is a list of all tags which apply to at least one
692 """Get server statistics.
694 The return value is list of statistics.
696 self._simple("stats")
700 """Get all preferences.
702 The return value is an encoded dump of the preferences database.
707 def set_volume(self, left, right):
711 left -- volume for the left speaker.
712 right -- volume for the right speaker.
714 self._simple("volume", left, right)
716 def get_volume(self):
719 The return value a tuple consisting of the left and right volumes.
721 ret, details = self._simple("volume")
722 return map(int,string.split(details))
724 def move(self, track, delta):
725 """Move a track in the queue.
728 track -- the name or ID of the track to move
729 delta -- the number of steps towards the head of the queue to move
731 ret, details = self._simple("move", track, str(delta))
734 def moveafter(self, target, tracks):
735 """Move a track in the queue
738 target -- target ID or None
739 tracks -- a list of IDs to move
741 If target is '' or is not in the queue then the tracks are moved to
742 the head of the queue.
744 Otherwise the tracks are moved to just after the target."""
747 self._simple("moveafter", target, *tracks)
749 def log(self, callback):
750 """Read event log entries as they happen.
752 Each event log entry is handled by passing it to callback.
754 The callback takes two arguments, the first is the client and the
755 second the line from the event log.
757 The callback should return True to continue or False to stop (don't
758 forget this, or your program will mysteriously misbehave).
760 It is suggested that you use the disorder.monitor class instead of
761 calling this method directly, but this is not mandatory.
763 See disorder_protocol(5) for the event log syntax.
766 callback -- function to call with log entry
768 ret, details = self._simple("log")
771 self._debug(client.debug_body, "<<< %s" % l)
772 if l != '' and l[0] == '.':
776 if not callback(self, l):
778 # tell the server to stop sending, eat the remains of the body,
780 self._send("version")
785 """Pause the current track."""
786 self._simple("pause")
789 """Resume after a pause."""
790 self._simple("resume")
792 def part(self, track, context, part):
793 """Get a track name part
796 track -- the track to query
797 context -- the context ('sort' or 'display')
798 part -- the desired part (usually 'artist', 'album' or 'title')
800 The return value is the preference
802 ret, details = self._simple("part", track, context, part)
805 def setglobal(self, key, value):
806 """Set a global preference value.
809 key -- the preference name
810 value -- the new preference value
812 self._simple("set-global", key, value)
814 def unsetglobal(self, key):
815 """Unset a global preference value.
818 key -- the preference to remove
820 self._simple("set-global", key, value)
822 def getglobal(self, key):
823 """Get a global preference value.
826 key -- the preference to look up
828 The return value is the preference
830 ret, details = self._simple("get-global", key)
836 ########################################################################
840 # read one response line and return as some suitable string object
842 # If an I/O error occurs, disconnect from the server.
844 # XXX does readline() DTRT regarding character encodings?
846 l = self.r.readline()
847 if not re.search("\n", l):
848 raise communicationError(self.who, "peer disconnected")
853 return unicode(l, "UTF-8")
856 # read a response as a (code, details) tuple
858 self._debug(client.debug_proto, "<== %s" % l)
859 m = _response.match(l)
861 return int(m.group(1)), m.group(2)
863 raise protocolError(self.who, "invalid response %s")
865 def _send(self, *command):
866 # Quote and send a command
868 # Returns the encoded command.
869 quoted = _quote(command)
870 self._debug(client.debug_proto, "==> %s" % quoted)
871 encoded = quoted.encode("UTF-8")
873 self.w.write(encoded)
880 raise communicationError(self.who, e)
885 def _simple(self, *command):
886 # Issue a simple command, throw an exception on error
888 # If an I/O error occurs, disconnect from the server.
890 # On success or 'normal' errors returns response as a (code, details) tuple
892 # On error raise operationError
893 if self.state == 'disconnected':
896 cmd = self._send(*command)
899 res, details = self._response()
900 if res / 100 == 2 or res == 555:
902 raise operationError(res, details, cmd)
905 # Fetch a dot-stuffed body
909 self._debug(client.debug_body, "<<< %s" % l)
910 if l != '' and l[0] == '.':
916 ########################################################################
917 # Configuration file parsing
919 def _readfile(self, path):
920 # Read a configuration file
924 # path -- path of file to read
926 # handlers for various commands
927 def _collection(self, command, args):
929 return "'%s' takes three args" % command
930 self.config["collections"].append(args)
932 def _unary(self, command, args):
934 return "'%s' takes only one arg" % command
935 self.config[command] = args[0]
937 def _include(self, command, args):
939 return "'%s' takes only one arg" % command
940 self._readfile(args[0])
942 def _any(self, command, args):
943 self.config[command] = args
945 # mapping of options to handlers
946 _options = { "collection": _collection,
951 "include": _include }
954 for lno, line in enumerate(file(path, "r")):
956 fields = _split(line, 'comments')
957 except _splitError, s:
958 raise parseError(path, lno + 1, str(s))
961 # we just ignore options we don't know about, so as to cope gracefully
962 # with version skew (and nothing to do with implementor laziness)
963 if command in _options:
964 e = _options[command](self, command, fields[1:])
966 self._parseError(path, lno + 1, e)
968 def _parseError(self, path, lno, s):
969 raise parseError(path, lno, s)
971 ########################################################################
975 """DisOrder event log monitor class
977 Intended to be subclassed with methods corresponding to event log messages
978 the implementor cares about over-ridden."""
980 def __init__(self, c=None):
981 """Constructor for the monitor class
983 Can be passed a client to use. If none is specified then one
984 will be created specially for the purpose.
993 """Start monitoring logs. Continues monitoring until one of the
994 message-specific methods returns False. Can be called more than once
995 (but not recursively!)"""
996 self.c.log(self._callback)
999 """Return the timestamp of the current (or most recent) event log entry"""
1000 return self.timestamp
1002 def _callback(self, c, line):
1006 return self.invalid(line)
1008 return self.invalid(line)
1009 self.timestamp = int(bits[0], 16)
1012 if keyword == 'completed':
1014 return self.completed(bits[0])
1015 elif keyword == 'failed':
1017 return self.failed(bits[0], bits[1])
1018 elif keyword == 'moved':
1023 return self.invalid(line)
1024 return self.moved(bits[0], n, bits[2])
1025 elif keyword == 'playing':
1027 return self.playing(bits[0], None)
1028 elif len(bits) == 2:
1029 return self.playing(bits[0], bits[1])
1030 elif keyword == 'queue' or keyword == 'recent-added':
1032 q = _list2dict(bits)
1034 return self.invalid(line)
1035 if keyword == 'queue':
1036 return self.queue(q)
1037 if keyword == 'recent-added':
1038 return self.recent_added(q)
1039 elif keyword == 'recent-removed':
1041 return self.recent_removed(bits[0])
1042 elif keyword == 'removed':
1044 return self.removed(bits[0], None)
1045 elif len(bits) == 2:
1046 return self.removed(bits[0], bits[1])
1047 elif keyword == 'scratched':
1049 return self.scratched(bits[0], bits[1])
1050 return self.invalid(line)
1052 def completed(self, track):
1053 """Called when a track completes.
1056 track -- track that completed"""
1059 def failed(self, track, error):
1060 """Called when a player suffers an error.
1063 track -- track that failed
1064 error -- error indicator"""
1067 def moved(self, id, offset, user):
1068 """Called when a track is moved in the queue.
1071 id -- queue entry ID
1072 offset -- distance moved
1073 user -- user responsible"""
1076 def playing(self, track, user):
1077 """Called when a track starts playing.
1080 track -- track that has started
1081 user -- user that submitted track, or None"""
1085 """Called when a track is added to the queue.
1088 q -- dictionary of new queue entry"""
1091 def recent_added(self, q):
1092 """Called when a track is added to the recently played list
1095 q -- dictionary of new queue entry"""
1098 def recent_removed(self, id):
1099 """Called when a track is removed from the recently played list
1102 id -- ID of removed entry (always the oldest)"""
1105 def removed(self, id, user):
1106 """Called when a track is removed from the queue, either manually
1107 or in order to play it.
1110 id -- ID of removed entry
1111 user -- user responsible (or None if we're playing this track)"""
1114 def scratched(self, track, user):
1115 """Called when a track is scratched
1118 track -- track that was scratched
1119 user -- user responsible"""
1122 def invalid(self, line):
1123 """Called when an event log line cannot be interpreted
1126 line -- line that could not be understood"""
1131 # py-indent-offset:2