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, details) = self._simple()
379 (protocol, algo, challenge) = _split(details)
381 raise communicationError(self.who,
382 "unknown protocol version %s" % protocol)
384 if self.user is None:
385 user = self.config['username']
388 if self.password is None:
389 password = self.config['password']
391 password = self.password
392 # TODO support algorithms other than SHA-1
395 h.update(binascii.unhexlify(challenge))
396 self._simple("user", user, h.hexdigest())
398 self._simple("cookie", cookie)
399 self.state = 'connected'
400 except socket.error, e:
402 raise communicationError(self.who, e)
407 def _disconnect(self):
408 # disconnect from the server, whatever state we are in
414 self.state = 'disconnected'
416 ########################################################################
419 def play(self, track):
423 track -- the path of the track to play.
425 Returns the ID of the new queue entry.
427 Note that queue IDs are unicode strings (because all track information
428 values are unicode strings).
430 res, details = self._simple("play", track)
431 return unicode(details) # because it's unicode in queue() output
433 def remove(self, track):
434 """Remove a track from the queue.
437 track -- the path or ID of the track to remove.
439 self._simple("remove", track)
442 """Enable playing."""
443 self._simple("enable")
445 def disable(self, *now):
449 now -- if present (with any value), the current track is stopped
453 self._simple("disable", "now")
455 self._simple("disable")
457 def scratch(self, *id):
458 """Scratch the currently playing track.
461 id -- if present, the ID of the track to scratch.
464 self._simple("scratch", id[0])
466 self._simple("scratch")
469 """Shut down the server.
471 Only trusted users can perform this operation.
473 self._simple("shutdown")
475 def reconfigure(self):
476 """Make the server reload its configuration.
478 Only trusted users can perform this operation.
480 self._simple("reconfigure")
482 def rescan(self, pattern):
483 """Rescan one or more collections.
486 pattern -- glob pattern matching collections to rescan.
488 Only trusted users can perform this operation.
490 self._simple("rescan", pattern)
493 """Return the server's version number."""
494 return _split(self._simple("version")[1])[0]
497 """Return the currently playing track.
499 If a track is playing then it is returned as a dictionary. See
500 disorder_protocol(5) for the meanings of the keys. All keys are
501 plain strings but the values will be unicode strings.
503 If no track is playing then None is returned."""
504 res, details = self._simple("playing")
507 return _queueEntry(details)
508 except _splitError, s:
509 raise protocolError(self.who, s.str())
513 def _somequeue(self, command):
514 self._simple(command)
516 return map(lambda s: _queueEntry(s), self._body())
517 except _splitError, s:
518 raise protocolError(self.who, s.str())
521 """Return a list of recently played tracks.
523 The return value is a list of dictionaries corresponding to
524 recently played tracks. The oldest track comes first.
526 See disorder_protocol(5) for the meanings of the keys. All keys are
527 plain strings but the values will be unicode strings."""
528 return self._somequeue("recent")
531 """Return the current queue.
533 The return value is a list of dictionaries corresponding to
534 recently played tracks. The next track to be played comes first.
536 See disorder_protocol(5) for the meanings of the keys. All keys are
537 plain strings but the values will be unicode strings."""
538 return self._somequeue("queue")
540 def _somedir(self, command, dir, re):
542 self._simple(command, dir, re[0])
544 self._simple(command, dir)
547 def directories(self, dir, *re):
548 """List subdirectories of a directory.
551 dir -- directory to list, or '' for the whole root.
552 re -- regexp that results must match. Optional.
554 The return value is a list of the (nonempty) subdirectories of dir.
555 If dir is '' then a list of top-level directories is returned.
557 If a regexp is specified then the basename of each result must
558 match. Matching is case-independent. See pcrepattern(3).
560 return self._somedir("dirs", dir, re)
562 def files(self, dir, *re):
563 """List files within a directory.
566 dir -- directory to list, or '' for the whole root.
567 re -- regexp that results must match. Optional.
569 The return value is a list of playable files in dir. If dir is ''
570 then a list of top-level files is returned.
572 If a regexp is specified then the basename of each result must
573 match. Matching is case-independent. See pcrepattern(3).
575 return self._somedir("files", dir, re)
577 def allfiles(self, dir, *re):
578 """List subdirectories and files within a directory.
581 dir -- directory to list, or '' for the whole root.
582 re -- regexp that results must match. Optional.
584 The return value is a list of all (nonempty) subdirectories and
585 files within dir. If dir is '' then a list of top-level files and
586 directories is returned.
588 If a regexp is specified then the basename of each result must
589 match. Matching is case-independent. See pcrepattern(3).
591 return self._somedir("allfiles", dir, re)
593 def set(self, track, key, value):
594 """Set a preference value.
597 track -- the track to modify
598 key -- the preference name
599 value -- the new preference value
601 self._simple("set", track, key, value)
603 def unset(self, track, key):
604 """Unset a preference value.
607 track -- the track to modify
608 key -- the preference to remove
610 self._simple("set", track, key, value)
612 def get(self, track, key):
613 """Get a preference value.
616 track -- the track to query
617 key -- the preference to remove
619 The return value is the preference.
621 ret, details = self._simple("get", track, key)
625 return _split(details)[0]
627 def prefs(self, track):
628 """Get all the preferences for a track.
631 track -- the track to query
633 The return value is a dictionary of all the track's preferences.
634 Note that even nominally numeric values remain encoded as strings.
636 self._simple("prefs", track)
638 for line in self._body():
641 except _splitError, s:
642 raise protocolError(self.who, s.str())
644 raise protocolError(self.who, "invalid prefs body line")
648 def _boolean(self, s):
651 def exists(self, track):
652 """Return true if a track exists
655 track -- the track to check for"""
656 return self._boolean(self._simple("exists", track))
659 """Return true if playing is enabled"""
660 return self._boolean(self._simple("enabled"))
662 def random_enabled(self):
663 """Return true if random play is enabled"""
664 return self._boolean(self._simple("random-enabled"))
666 def random_enable(self):
667 """Enable random play."""
668 self._simple("random-enable")
670 def random_disable(self):
671 """Disable random play."""
672 self._simple("random-disable")
674 def length(self, track):
675 """Return the length of a track in seconds.
678 track -- the track to query.
680 ret, details = self._simple("length", track)
683 def search(self, words):
684 """Search for tracks.
687 words -- the set of words to search for.
689 The return value is a list of track path names, all of which contain
690 all of the required words (in their path name, trackname
693 self._simple("search", _quote(words))
699 The return value is a list of all tags which apply to at least one
705 """Get server statistics.
707 The return value is list of statistics.
709 self._simple("stats")
713 """Get all preferences.
715 The return value is an encoded dump of the preferences database.
720 def set_volume(self, left, right):
724 left -- volume for the left speaker.
725 right -- volume for the right speaker.
727 self._simple("volume", left, right)
729 def get_volume(self):
732 The return value a tuple consisting of the left and right volumes.
734 ret, details = self._simple("volume")
735 return map(int,string.split(details))
737 def move(self, track, delta):
738 """Move a track in the queue.
741 track -- the name or ID of the track to move
742 delta -- the number of steps towards the head of the queue to move
744 ret, details = self._simple("move", track, str(delta))
747 def moveafter(self, target, tracks):
748 """Move a track in the queue
751 target -- target ID or None
752 tracks -- a list of IDs to move
754 If target is '' or is not in the queue then the tracks are moved to
755 the head of the queue.
757 Otherwise the tracks are moved to just after the target."""
760 self._simple("moveafter", target, *tracks)
762 def log(self, callback):
763 """Read event log entries as they happen.
765 Each event log entry is handled by passing it to callback.
767 The callback takes two arguments, the first is the client and the
768 second the line from the event log.
770 The callback should return True to continue or False to stop (don't
771 forget this, or your program will mysteriously misbehave).
773 It is suggested that you use the disorder.monitor class instead of
774 calling this method directly, but this is not mandatory.
776 See disorder_protocol(5) for the event log syntax.
779 callback -- function to call with log entry
781 ret, details = self._simple("log")
784 self._debug(client.debug_body, "<<< %s" % l)
785 if l != '' and l[0] == '.':
789 if not callback(self, l):
791 # tell the server to stop sending, eat the remains of the body,
793 self._send("version")
798 """Pause the current track."""
799 self._simple("pause")
802 """Resume after a pause."""
803 self._simple("resume")
805 def part(self, track, context, part):
806 """Get a track name part
809 track -- the track to query
810 context -- the context ('sort' or 'display')
811 part -- the desired part (usually 'artist', 'album' or 'title')
813 The return value is the preference
815 ret, details = self._simple("part", track, context, part)
816 return _split(details)[0]
818 def setglobal(self, key, value):
819 """Set a global preference value.
822 key -- the preference name
823 value -- the new preference value
825 self._simple("set-global", key, value)
827 def unsetglobal(self, key):
828 """Unset a global preference value.
831 key -- the preference to remove
833 self._simple("set-global", key, value)
835 def getglobal(self, key):
836 """Get a global preference value.
839 key -- the preference to look up
841 The return value is the preference
843 ret, details = self._simple("get-global", key)
847 return _split(details)[0]
849 def make_cookie(self):
850 """Create a login cookie"""
851 ret, details = self._simple("make-cookie")
852 return _split(details)[0]
855 """Revoke a login cookie"""
856 self._simple("revoke")
858 def adduser(self, user, password):
860 self._simple("adduser", user, password)
862 def deluser(self, user):
864 self._simple("deluser", user)
866 def userinfo(self, user, key):
867 """Get user information"""
868 res, details = self._simple("userinfo", user, key)
871 return _split(details)[0]
873 def edituser(self, user, key, value):
874 """Set user information"""
875 self._simple("edituser", user, key, value)
880 The return value is a list of all users."""
881 self._simple("users")
884 def register(self, username, password, email):
885 """Register a user"""
886 res, details = self._simple("register", username, password, email)
887 return _split(details)[0]
889 def confirm(self, confirmation):
890 """Confirm a user registration"""
891 res, details = self._simple("confirm", confirmation)
893 ########################################################################
897 # read one response line and return as some suitable string object
899 # If an I/O error occurs, disconnect from the server.
901 # XXX does readline() DTRT regarding character encodings?
903 l = self.r.readline()
904 if not re.search("\n", l):
905 raise communicationError(self.who, "peer disconnected")
910 return unicode(l, "UTF-8")
913 # read a response as a (code, details) tuple
915 self._debug(client.debug_proto, "<== %s" % l)
916 m = _response.match(l)
918 return int(m.group(1)), m.group(2)
920 raise protocolError(self.who, "invalid response %s")
922 def _send(self, *command):
923 # Quote and send a command
925 # Returns the encoded command.
926 quoted = _quote(command)
927 self._debug(client.debug_proto, "==> %s" % quoted)
928 encoded = quoted.encode("UTF-8")
930 self.w.write(encoded)
937 raise communicationError(self.who, e)
942 def _simple(self, *command):
943 # Issue a simple command, throw an exception on error
945 # If an I/O error occurs, disconnect from the server.
947 # On success or 'normal' errors returns response as a (code, details) tuple
949 # On error raise operationError
950 if self.state == 'disconnected':
953 cmd = self._send(*command)
956 res, details = self._response()
957 if res / 100 == 2 or res == 555:
959 raise operationError(res, details, cmd)
962 # Fetch a dot-stuffed body
966 self._debug(client.debug_body, "<<< %s" % l)
967 if l != '' and l[0] == '.':
973 ########################################################################
974 # Configuration file parsing
976 def _readfile(self, path):
977 # Read a configuration file
981 # path -- path of file to read
983 # handlers for various commands
984 def _collection(self, command, args):
986 return "'%s' takes three args" % command
987 self.config["collections"].append(args)
989 def _unary(self, command, args):
991 return "'%s' takes only one arg" % command
992 self.config[command] = args[0]
994 def _include(self, command, args):
996 return "'%s' takes only one arg" % command
997 self._readfile(args[0])
999 def _any(self, command, args):
1000 self.config[command] = args
1002 # mapping of options to handlers
1003 _options = { "collection": _collection,
1008 "include": _include }
1011 for lno, line in enumerate(file(path, "r")):
1013 fields = _split(line, 'comments')
1014 except _splitError, s:
1015 raise parseError(path, lno + 1, str(s))
1018 # we just ignore options we don't know about, so as to cope gracefully
1019 # with version skew (and nothing to do with implementor laziness)
1020 if command in _options:
1021 e = _options[command](self, command, fields[1:])
1023 self._parseError(path, lno + 1, e)
1025 def _parseError(self, path, lno, s):
1026 raise parseError(path, lno, s)
1028 ########################################################################
1032 """DisOrder event log monitor class
1034 Intended to be subclassed with methods corresponding to event log messages
1035 the implementor cares about over-ridden."""
1037 def __init__(self, c=None):
1038 """Constructor for the monitor class
1040 Can be passed a client to use. If none is specified then one
1041 will be created specially for the purpose.
1050 """Start monitoring logs. Continues monitoring until one of the
1051 message-specific methods returns False. Can be called more than once
1052 (but not recursively!)"""
1053 self.c.log(self._callback)
1056 """Return the timestamp of the current (or most recent) event log entry"""
1057 return self.timestamp
1059 def _callback(self, c, line):
1063 return self.invalid(line)
1065 return self.invalid(line)
1066 self.timestamp = int(bits[0], 16)
1069 if keyword == 'completed':
1071 return self.completed(bits[0])
1072 elif keyword == 'failed':
1074 return self.failed(bits[0], bits[1])
1075 elif keyword == 'moved':
1080 return self.invalid(line)
1081 return self.moved(bits[0], n, bits[2])
1082 elif keyword == 'playing':
1084 return self.playing(bits[0], None)
1085 elif len(bits) == 2:
1086 return self.playing(bits[0], bits[1])
1087 elif keyword == 'queue' or keyword == 'recent-added':
1089 q = _list2dict(bits)
1091 return self.invalid(line)
1092 if keyword == 'queue':
1093 return self.queue(q)
1094 if keyword == 'recent-added':
1095 return self.recent_added(q)
1096 elif keyword == 'recent-removed':
1098 return self.recent_removed(bits[0])
1099 elif keyword == 'removed':
1101 return self.removed(bits[0], None)
1102 elif len(bits) == 2:
1103 return self.removed(bits[0], bits[1])
1104 elif keyword == 'scratched':
1106 return self.scratched(bits[0], bits[1])
1107 return self.invalid(line)
1109 def completed(self, track):
1110 """Called when a track completes.
1113 track -- track that completed"""
1116 def failed(self, track, error):
1117 """Called when a player suffers an error.
1120 track -- track that failed
1121 error -- error indicator"""
1124 def moved(self, id, offset, user):
1125 """Called when a track is moved in the queue.
1128 id -- queue entry ID
1129 offset -- distance moved
1130 user -- user responsible"""
1133 def playing(self, track, user):
1134 """Called when a track starts playing.
1137 track -- track that has started
1138 user -- user that submitted track, or None"""
1142 """Called when a track is added to the queue.
1145 q -- dictionary of new queue entry"""
1148 def recent_added(self, q):
1149 """Called when a track is added to the recently played list
1152 q -- dictionary of new queue entry"""
1155 def recent_removed(self, id):
1156 """Called when a track is removed from the recently played list
1159 id -- ID of removed entry (always the oldest)"""
1162 def removed(self, id, user):
1163 """Called when a track is removed from the queue, either manually
1164 or in order to play it.
1167 id -- ID of removed entry
1168 user -- user responsible (or None if we're playing this track)"""
1171 def scratched(self, track, user):
1172 """Called when a track is scratched
1175 track -- track that was scratched
1176 user -- user responsible"""
1179 def invalid(self, line):
1180 """Called when an event log line cannot be interpreted
1183 line -- line that could not be understood"""
1188 # py-indent-offset:2