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
279 def __init__(self, user=None, password=None):
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,
298 self.password = password
299 home = os.getenv("HOME")
302 privconf = _configfile + "." + pw.pw_name
303 passfile = home + os.sep + ".disorder" + os.sep + "passwd"
304 if os.path.exists(_configfile):
305 self._readfile(_configfile)
306 if os.path.exists(privconf):
307 self._readfile(privconf)
308 if os.path.exists(passfile) and _userconf:
309 self._readfile(passfile)
310 self.state = 'disconnected'
312 def debug(self, bits):
313 """Enable or disable protocol debugging. Debug messages are written
317 bits -- bitmap of operations that should generate debug information
320 debug_proto -- dump control protocol messages (excluding bodies)
321 debug_body -- dump control protocol message bodies
323 self.debugging = bits
325 def _debug(self, bit, s):
327 if self.debugging & bit:
328 sys.stderr.write(_sanitize(s))
329 sys.stderr.write("\n")
332 def connect(self, cookie=None):
333 """c.connect(cookie=None)
335 Connect to the DisOrder server and authenticate.
337 Raises communicationError if connection fails and operationError if
338 authentication fails (in which case disconnection is automatic).
340 May be called more than once to retry connections (e.g. when the
341 server is down). If we are already connected and authenticated,
344 Other operations automatically connect if we're not already
345 connected, so it is not strictly necessary to call this method.
347 If COOKIE is specified then that is used to log in instead of
348 the username/password.
350 if self.state == 'disconnected':
352 self.state = 'connecting'
353 if 'connect' in self.config and len(self.config['connect']) > 0:
354 c = self.config['connect']
355 self.who = repr(c) # temporarily
357 a = socket.getaddrinfo(None, c[0],
363 a = socket.getaddrinfo(c[0], c[1],
369 s = socket.socket(a[0], a[1], a[2]);
371 self.who = "%s" % a[3]
373 s = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM);
374 self.who = self.config['home'] + os.sep + "socket"
376 self.w = s.makefile("wb")
377 self.r = s.makefile("rb")
378 (res, challenge_and_algo) = self._simple()
379 (algo, challenge) = _split(challenge_and_algo)
381 if self.user is None:
382 user = self.config['username']
385 if self.password is None:
386 password = self.config['password']
388 password = self.password
389 # TODO support algorithms other than SHA-1
392 h.update(binascii.unhexlify(challenge))
393 self._simple("user", user, h.hexdigest())
395 self._simple("cookie", cookie)
396 self.state = 'connected'
397 except socket.error, e:
399 raise communicationError(self.who, e)
404 def _disconnect(self):
405 # disconnect from the server, whatever state we are in
411 self.state = 'disconnected'
413 ########################################################################
416 def become(self, who):
417 """Become another user.
420 who -- the user to become.
422 Only trusted users can perform this operation.
424 self._simple("become", who)
426 def play(self, track):
430 track -- the path of the track to play.
432 Returns the ID of the new queue entry.
434 Note that queue IDs are unicode strings (because all track information
435 values are unicode strings).
437 res, details = self._simple("play", track)
438 return unicode(details) # because it's unicode in queue() output
440 def remove(self, track):
441 """Remove a track from the queue.
444 track -- the path or ID of the track to remove.
446 self._simple("remove", track)
449 """Enable playing."""
450 self._simple("enable")
452 def disable(self, *now):
456 now -- if present (with any value), the current track is stopped
460 self._simple("disable", "now")
462 self._simple("disable")
464 def scratch(self, *id):
465 """Scratch the currently playing track.
468 id -- if present, the ID of the track to scratch.
471 self._simple("scratch", id[0])
473 self._simple("scratch")
476 """Shut down the server.
478 Only trusted users can perform this operation.
480 self._simple("shutdown")
482 def reconfigure(self):
483 """Make the server reload its configuration.
485 Only trusted users can perform this operation.
487 self._simple("reconfigure")
489 def rescan(self, pattern):
490 """Rescan one or more collections.
493 pattern -- glob pattern matching collections to rescan.
495 Only trusted users can perform this operation.
497 self._simple("rescan", pattern)
500 """Return the server's version number."""
501 return self._simple("version")[1]
504 """Return the currently playing track.
506 If a track is playing then it is returned as a dictionary. See
507 disorder_protocol(5) for the meanings of the keys. All keys are
508 plain strings but the values will be unicode strings.
510 If no track is playing then None is returned."""
511 res, details = self._simple("playing")
514 return _queueEntry(details)
515 except _splitError, s:
516 raise protocolError(self.who, s.str())
520 def _somequeue(self, command):
521 self._simple(command)
523 return map(lambda s: _queueEntry(s), self._body())
524 except _splitError, s:
525 raise protocolError(self.who, s.str())
528 """Return a list of recently played tracks.
530 The return value is a list of dictionaries corresponding to
531 recently played tracks. The oldest track comes first.
533 See disorder_protocol(5) for the meanings of the keys. All keys are
534 plain strings but the values will be unicode strings."""
535 return self._somequeue("recent")
538 """Return the current queue.
540 The return value is a list of dictionaries corresponding to
541 recently played tracks. The next track to be played comes first.
543 See disorder_protocol(5) for the meanings of the keys. All keys are
544 plain strings but the values will be unicode strings."""
545 return self._somequeue("queue")
547 def _somedir(self, command, dir, re):
549 self._simple(command, dir, re[0])
551 self._simple(command, dir)
554 def directories(self, dir, *re):
555 """List subdirectories of a directory.
558 dir -- directory to list, or '' for the whole root.
559 re -- regexp that results must match. Optional.
561 The return value is a list of the (nonempty) subdirectories of dir.
562 If dir is '' then a list of top-level directories is returned.
564 If a regexp is specified then the basename of each result must
565 match. Matching is case-independent. See pcrepattern(3).
567 return self._somedir("dirs", dir, re)
569 def files(self, dir, *re):
570 """List files within a directory.
573 dir -- directory to list, or '' for the whole root.
574 re -- regexp that results must match. Optional.
576 The return value is a list of playable files in dir. If dir is ''
577 then a list of top-level files is returned.
579 If a regexp is specified then the basename of each result must
580 match. Matching is case-independent. See pcrepattern(3).
582 return self._somedir("files", dir, re)
584 def allfiles(self, dir, *re):
585 """List subdirectories and files within a directory.
588 dir -- directory to list, or '' for the whole root.
589 re -- regexp that results must match. Optional.
591 The return value is a list of all (nonempty) subdirectories and
592 files within dir. If dir is '' then a list of top-level files and
593 directories is returned.
595 If a regexp is specified then the basename of each result must
596 match. Matching is case-independent. See pcrepattern(3).
598 return self._somedir("allfiles", dir, re)
600 def set(self, track, key, value):
601 """Set a preference value.
604 track -- the track to modify
605 key -- the preference name
606 value -- the new preference value
608 self._simple("set", track, key, value)
610 def unset(self, track, key):
611 """Unset a preference value.
614 track -- the track to modify
615 key -- the preference to remove
617 self._simple("set", track, key, value)
619 def get(self, track, key):
620 """Get a preference value.
623 track -- the track to query
624 key -- the preference to remove
626 The return value is the preference.
628 ret, details = self._simple("get", track, key)
634 def prefs(self, track):
635 """Get all the preferences for a track.
638 track -- the track to query
640 The return value is a dictionary of all the track's preferences.
641 Note that even nominally numeric values remain encoded as strings.
643 self._simple("prefs", track)
645 for line in self._body():
648 except _splitError, s:
649 raise protocolError(self.who, s.str())
651 raise protocolError(self.who, "invalid prefs body line")
655 def _boolean(self, s):
658 def exists(self, track):
659 """Return true if a track exists
662 track -- the track to check for"""
663 return self._boolean(self._simple("exists", track))
666 """Return true if playing is enabled"""
667 return self._boolean(self._simple("enabled"))
669 def random_enabled(self):
670 """Return true if random play is enabled"""
671 return self._boolean(self._simple("random-enabled"))
673 def random_enable(self):
674 """Enable random play."""
675 self._simple("random-enable")
677 def random_disable(self):
678 """Disable random play."""
679 self._simple("random-disable")
681 def length(self, track):
682 """Return the length of a track in seconds.
685 track -- the track to query.
687 ret, details = self._simple("length", track)
690 def search(self, words):
691 """Search for tracks.
694 words -- the set of words to search for.
696 The return value is a list of track path names, all of which contain
697 all of the required words (in their path name, trackname
700 self._simple("search", _quote(words))
706 The return value is a list of all tags which apply to at least one
712 """Get server statistics.
714 The return value is list of statistics.
716 self._simple("stats")
720 """Get all preferences.
722 The return value is an encoded dump of the preferences database.
727 def set_volume(self, left, right):
731 left -- volume for the left speaker.
732 right -- volume for the right speaker.
734 self._simple("volume", left, right)
736 def get_volume(self):
739 The return value a tuple consisting of the left and right volumes.
741 ret, details = self._simple("volume")
742 return map(int,string.split(details))
744 def move(self, track, delta):
745 """Move a track in the queue.
748 track -- the name or ID of the track to move
749 delta -- the number of steps towards the head of the queue to move
751 ret, details = self._simple("move", track, str(delta))
754 def moveafter(self, target, tracks):
755 """Move a track in the queue
758 target -- target ID or None
759 tracks -- a list of IDs to move
761 If target is '' or is not in the queue then the tracks are moved to
762 the head of the queue.
764 Otherwise the tracks are moved to just after the target."""
767 self._simple("moveafter", target, *tracks)
769 def log(self, callback):
770 """Read event log entries as they happen.
772 Each event log entry is handled by passing it to callback.
774 The callback takes two arguments, the first is the client and the
775 second the line from the event log.
777 The callback should return True to continue or False to stop (don't
778 forget this, or your program will mysteriously misbehave).
780 It is suggested that you use the disorder.monitor class instead of
781 calling this method directly, but this is not mandatory.
783 See disorder_protocol(5) for the event log syntax.
786 callback -- function to call with log entry
788 ret, details = self._simple("log")
791 self._debug(client.debug_body, "<<< %s" % l)
792 if l != '' and l[0] == '.':
796 if not callback(self, l):
798 # tell the server to stop sending, eat the remains of the body,
800 self._send("version")
805 """Pause the current track."""
806 self._simple("pause")
809 """Resume after a pause."""
810 self._simple("resume")
812 def part(self, track, context, part):
813 """Get a track name part
816 track -- the track to query
817 context -- the context ('sort' or 'display')
818 part -- the desired part (usually 'artist', 'album' or 'title')
820 The return value is the preference
822 ret, details = self._simple("part", track, context, part)
825 def setglobal(self, key, value):
826 """Set a global preference value.
829 key -- the preference name
830 value -- the new preference value
832 self._simple("set-global", key, value)
834 def unsetglobal(self, key):
835 """Unset a global preference value.
838 key -- the preference to remove
840 self._simple("set-global", key, value)
842 def getglobal(self, key):
843 """Get a global preference value.
846 key -- the preference to look up
848 The return value is the preference
850 ret, details = self._simple("get-global", key)
856 def make_cookie(self):
857 """Create a login cookie"""
858 ret, details = self._simple("make-cookie")
859 return _split(details)[0]
862 """Revoke a login cookie"""
863 self._simple("revoke")
865 def adduser(self, user, password):
867 self._simple("adduser", user, password)
869 def deluser(self, user):
871 self._simple("deluser", user)
873 def userinfo(self, user, key):
874 """Get user information"""
875 res, details = self._simple("userinfo", user, key)
878 return _split(details)[0]
880 def edituser(self, user, key, value):
881 """Set user information"""
882 self._simple("edituser", user, key, value)
887 The return value is a list of all users."""
888 self._simple("users")
891 ########################################################################
895 # read one response line and return as some suitable string object
897 # If an I/O error occurs, disconnect from the server.
899 # XXX does readline() DTRT regarding character encodings?
901 l = self.r.readline()
902 if not re.search("\n", l):
903 raise communicationError(self.who, "peer disconnected")
908 return unicode(l, "UTF-8")
911 # read a response as a (code, details) tuple
913 self._debug(client.debug_proto, "<== %s" % l)
914 m = _response.match(l)
916 return int(m.group(1)), m.group(2)
918 raise protocolError(self.who, "invalid response %s")
920 def _send(self, *command):
921 # Quote and send a command
923 # Returns the encoded command.
924 quoted = _quote(command)
925 self._debug(client.debug_proto, "==> %s" % quoted)
926 encoded = quoted.encode("UTF-8")
928 self.w.write(encoded)
935 raise communicationError(self.who, e)
940 def _simple(self, *command):
941 # Issue a simple command, throw an exception on error
943 # If an I/O error occurs, disconnect from the server.
945 # On success or 'normal' errors returns response as a (code, details) tuple
947 # On error raise operationError
948 if self.state == 'disconnected':
951 cmd = self._send(*command)
954 res, details = self._response()
955 if res / 100 == 2 or res == 555:
957 raise operationError(res, details, cmd)
960 # Fetch a dot-stuffed body
964 self._debug(client.debug_body, "<<< %s" % l)
965 if l != '' and l[0] == '.':
971 ########################################################################
972 # Configuration file parsing
974 def _readfile(self, path):
975 # Read a configuration file
979 # path -- path of file to read
981 # handlers for various commands
982 def _collection(self, command, args):
984 return "'%s' takes three args" % command
985 self.config["collections"].append(args)
987 def _unary(self, command, args):
989 return "'%s' takes only one arg" % command
990 self.config[command] = args[0]
992 def _include(self, command, args):
994 return "'%s' takes only one arg" % command
995 self._readfile(args[0])
997 def _any(self, command, args):
998 self.config[command] = args
1000 # mapping of options to handlers
1001 _options = { "collection": _collection,
1006 "include": _include }
1009 for lno, line in enumerate(file(path, "r")):
1011 fields = _split(line, 'comments')
1012 except _splitError, s:
1013 raise parseError(path, lno + 1, str(s))
1016 # we just ignore options we don't know about, so as to cope gracefully
1017 # with version skew (and nothing to do with implementor laziness)
1018 if command in _options:
1019 e = _options[command](self, command, fields[1:])
1021 self._parseError(path, lno + 1, e)
1023 def _parseError(self, path, lno, s):
1024 raise parseError(path, lno, s)
1026 ########################################################################
1030 """DisOrder event log monitor class
1032 Intended to be subclassed with methods corresponding to event log messages
1033 the implementor cares about over-ridden."""
1035 def __init__(self, c=None):
1036 """Constructor for the monitor class
1038 Can be passed a client to use. If none is specified then one
1039 will be created specially for the purpose.
1048 """Start monitoring logs. Continues monitoring until one of the
1049 message-specific methods returns False. Can be called more than once
1050 (but not recursively!)"""
1051 self.c.log(self._callback)
1054 """Return the timestamp of the current (or most recent) event log entry"""
1055 return self.timestamp
1057 def _callback(self, c, line):
1061 return self.invalid(line)
1063 return self.invalid(line)
1064 self.timestamp = int(bits[0], 16)
1067 if keyword == 'completed':
1069 return self.completed(bits[0])
1070 elif keyword == 'failed':
1072 return self.failed(bits[0], bits[1])
1073 elif keyword == 'moved':
1078 return self.invalid(line)
1079 return self.moved(bits[0], n, bits[2])
1080 elif keyword == 'playing':
1082 return self.playing(bits[0], None)
1083 elif len(bits) == 2:
1084 return self.playing(bits[0], bits[1])
1085 elif keyword == 'queue' or keyword == 'recent-added':
1087 q = _list2dict(bits)
1089 return self.invalid(line)
1090 if keyword == 'queue':
1091 return self.queue(q)
1092 if keyword == 'recent-added':
1093 return self.recent_added(q)
1094 elif keyword == 'recent-removed':
1096 return self.recent_removed(bits[0])
1097 elif keyword == 'removed':
1099 return self.removed(bits[0], None)
1100 elif len(bits) == 2:
1101 return self.removed(bits[0], bits[1])
1102 elif keyword == 'scratched':
1104 return self.scratched(bits[0], bits[1])
1105 return self.invalid(line)
1107 def completed(self, track):
1108 """Called when a track completes.
1111 track -- track that completed"""
1114 def failed(self, track, error):
1115 """Called when a player suffers an error.
1118 track -- track that failed
1119 error -- error indicator"""
1122 def moved(self, id, offset, user):
1123 """Called when a track is moved in the queue.
1126 id -- queue entry ID
1127 offset -- distance moved
1128 user -- user responsible"""
1131 def playing(self, track, user):
1132 """Called when a track starts playing.
1135 track -- track that has started
1136 user -- user that submitted track, or None"""
1140 """Called when a track is added to the queue.
1143 q -- dictionary of new queue entry"""
1146 def recent_added(self, q):
1147 """Called when a track is added to the recently played list
1150 q -- dictionary of new queue entry"""
1153 def recent_removed(self, id):
1154 """Called when a track is removed from the recently played list
1157 id -- ID of removed entry (always the oldest)"""
1160 def removed(self, id, user):
1161 """Called when a track is removed from the queue, either manually
1162 or in order to play it.
1165 id -- ID of removed entry
1166 user -- user responsible (or None if we're playing this track)"""
1169 def scratched(self, track, user):
1170 """Called when a track is scratched
1173 track -- track that was scratched
1174 user -- user responsible"""
1177 def invalid(self, line):
1178 """Called when an event log line cannot be interpreted
1181 line -- line that could not be understood"""
1186 # py-indent-offset:2