2 # Copyright (C) 2004, 2005 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:]:
54 _configfile = "pkgconfdir/config"
55 _dbhome = "pkgstatedir"
58 # various regexps we'll use
59 _ws = re.compile(r"^[ \t\n\r]+")
60 _squote = re.compile("'(([^\\\\']|\\\\[\\\\\"'n])+)'")
61 _dquote = re.compile("\"(([^\\\\\"]|\\\\[\\\\\"'n])+)\"")
62 _unquoted = re.compile("[^\"' \\t\\n\\r][^ \t\n\r]*")
64 _response = re.compile("([0-9]{3}) ?(.*)")
68 ########################################################################
71 class Error(Exception):
72 """Base class for DisOrder exceptions."""
74 class _splitError(Error):
76 def __init__(self, value):
79 return str(self.value)
81 class parseError(Error):
82 """Error parsing the configuration file."""
83 def __init__(self, path, line, details):
86 self.details = details
88 return "%s:%d: %s" % (self.path, self.line, self.details)
90 class protocolError(Error):
91 """DisOrder control protocol error.
93 Indicates a mismatch between the client and server's understanding of
96 def __init__(self, who, error):
100 return "%s: %s" % (self.who, str(self.error))
102 class operationError(Error):
103 """DisOrder control protocol error response.
105 Indicates that an operation failed (e.g. an attempt to play a
106 nonexistent track). The connection should still be usable.
108 def __init__(self, res, details, cmd=None):
111 self.details_ = details
113 """Return the complete response string from the server, with the command
116 Excludes the final newline.
118 if self.cmd_ is None:
119 return "%d %s" % (self.res_, self.details_)
121 return "%d %s [%s]" % (self.res_, self.details_, self.cmd_)
123 """Return the response code from the server."""
126 """Returns the detail string from the server."""
129 class communicationError(Error):
130 """DisOrder control protocol communication error.
132 Indicates that communication with the server went wrong, perhaps
133 because the server was restarted. The caller could report an error to
134 the user and wait for further user instructions, or even automatically
137 def __init__(self, who, error):
141 return "%s: %s" % (self.who, str(self.error))
143 ########################################################################
144 # DisOrder-specific text processing
147 # Unescape the contents of a string
151 # s -- string to unescape
153 s = re.sub("\\\\n", "\n", s)
154 s = re.sub("\\\\(.)", "\\1", s)
157 def _split(s, *comments):
158 # Split a string into fields according to the usual Disorder string splitting
163 # s -- string to parse
164 # comments -- if present, parse comments
168 # On success, a list of fields is returned.
170 # On error, disorder.parseError is thrown.
175 if comments and s[0] == '#':
182 # pick of quoted fields of both kinds
187 fields.append(_unescape(m.group(1)))
190 # and unquoted fields
191 m = _unquoted.match(s)
193 fields.append(m.group(0))
196 # anything left must be in error
197 if s[0] == '"' or s[0] == '\'':
198 raise _splitError("invalid quoted string")
200 raise _splitError("syntax error")
204 # Escape the contents of a string
208 # s -- string to escape
210 if re.search("[\\\\\"'\n \t\r]", s) or s == '':
211 s = re.sub(r'[\\"]', r'\\\g<0>', s)
212 s = re.sub("\n", r"\\n", s)
218 # Quote a list of values
219 return ' '.join(map(_escape, list))
222 # Return the value of s in a form suitable for writing to stderr
223 return s.encode(locale.nl_langinfo(locale.CODESET), 'replace')
226 # Convert a list of the form [k1, v1, k2, v2, ..., kN, vN]
227 # to a dictionary {k1:v1, k2:v2, ..., kN:vN}
235 except StopIteration:
240 # parse a queue entry
241 return _list2dict(_split(s))
243 ########################################################################
247 """DisOrder client class.
249 This class provides access to the DisOrder server either on this
250 machine or across the internet.
252 The server to connect to, and the username and password to use, are
253 determined from the configuration files as described in 'man
256 All methods will connect if necessary, as soon as you have a
257 disorder.client object you can start calling operational methods on
260 However if the server is restarted then the next method called on a
261 connection will throw an exception. This may be considered a bug.
263 All methods block until they complete.
265 Operation methods raise communicationError if the connection breaks,
266 protocolError if the response from the server is malformed, or
267 operationError if the response is valid but indicates that the
275 """Constructor for DisOrder client class.
277 The constructor reads the configuration file, but does not connect
280 If the environment variable DISORDER_PYTHON_DEBUG is set then the
281 debug flags are initialised to that value. This can be overridden
282 with the debug() method below.
284 The constructor Raises parseError() if the configuration file is not
287 pw = pwd.getpwuid(os.getuid())
288 self.debugging = int(os.getenv("DISORDER_PYTHON_DEBUG", 0))
289 self.config = { 'collections': [],
290 'username': pw.pw_name,
292 home = os.getenv("HOME")
295 privconf = _configfile + "." + pw.pw_name
296 passfile = home + os.sep + ".disorder" + os.sep + "passwd"
297 if os.path.exists(_configfile):
298 self._readfile(_configfile)
299 if os.path.exists(privconf):
300 self._readfile(privconf)
301 if os.path.exists(passfile) and _userconf:
302 self._readfile(passfile)
303 self.state = 'disconnected'
305 def debug(self, bits):
306 """Enable or disable protocol debugging. Debug messages are written
310 bits -- bitmap of operations that should generate debug information
313 debug_proto -- dump control protocol messages (excluding bodies)
314 debug_body -- dump control protocol message bodies
316 self.debugging = bits
318 def _debug(self, bit, s):
320 if self.debugging & bit:
321 sys.stderr.write(_sanitize(s))
322 sys.stderr.write("\n")
326 """Connect to the DisOrder server and authenticate.
328 Raises communicationError if connection fails and operationError if
329 authentication fails (in which case disconnection is automatic).
331 May be called more than once to retry connections (e.g. when the
332 server is down). If we are already connected and authenticated,
335 Other operations automatically connect if we're not already
336 connected, so it is not strictly necessary to call this method.
338 if self.state == 'disconnected':
340 self.state = 'connecting'
341 if 'connect' in self.config and len(self.config['connect']) > 0:
342 c = self.config['connect']
343 self.who = repr(c) # temporarily
345 a = socket.getaddrinfo(None, c[0],
351 a = socket.getaddrinfo(c[0], c[1],
357 s = socket.socket(a[0], a[1], a[2]);
359 self.who = "%s" % a[3]
361 s = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM);
362 self.who = self.config['home'] + os.sep + "socket"
364 self.w = s.makefile("wb")
365 self.r = s.makefile("rb")
366 (res, challenge) = self._simple()
368 h.update(self.config['password'])
369 h.update(binascii.unhexlify(challenge))
370 self._simple("user", self.config['username'], h.hexdigest())
371 self.state = 'connected'
372 except socket.error, e:
374 raise communicationError(self.who, e)
379 def _disconnect(self):
380 # disconnect from the server, whatever state we are in
386 self.state = 'disconnected'
388 ########################################################################
391 def become(self, who):
392 """Become another user.
395 who -- the user to become.
397 Only trusted users can perform this operation.
399 self._simple("become", who)
401 def play(self, track):
405 track -- the path of the track to play.
407 Returns the ID of the new queue entry.
409 res, details = self._simple("play", track)
410 return unicode(details) # because it's unicode in queue() output
412 def remove(self, track):
413 """Remove a track from the queue.
416 track -- the path or ID of the track to remove.
418 self._simple("remove", track)
421 """Enable playing."""
422 self._simple("enable")
424 def disable(self, *now):
428 now -- if present (with any value), the current track is stopped
432 self._simple("disable", "now")
434 self._simple("disable")
436 def scratch(self, *id):
437 """Scratch the currently playing track.
440 id -- if present, the ID of the track to scratch.
443 self._simple("scratch", id[0])
445 self._simple("scratch")
448 """Shut down the server.
450 Only trusted users can perform this operation.
452 self._simple("shutdown")
454 def reconfigure(self):
455 """Make the server reload its configuration.
457 Only trusted users can perform this operation.
459 self._simple("reconfigure")
461 def rescan(self, pattern):
462 """Rescan one or more collections.
465 pattern -- glob pattern matching collections to rescan.
467 Only trusted users can perform this operation.
469 self._simple("rescan", pattern)
472 """Return the server's version number."""
473 return self._simple("version")[1]
476 """Return the currently playing track.
478 If a track is playing then it is returned as a dictionary.
479 If no track is playing then None is returned."""
480 res, details = self._simple("playing")
483 return _queueEntry(details)
484 except _splitError, s:
485 raise protocolError(self.who, s.str())
489 def _somequeue(self, command):
490 self._simple(command)
492 return map(lambda s: _queueEntry(s), self._body())
493 except _splitError, s:
494 raise protocolError(self.who, s.str())
497 """Return a list of recently played tracks.
499 The return value is a list of dictionaries corresponding to
500 recently played tracks. The oldest track comes first."""
501 return self._somequeue("recent")
504 """Return the current queue.
506 The return value is a list of dictionaries corresponding to
507 recently played tracks. The next track to be played comes first."""
508 return self._somequeue("queue")
510 def _somedir(self, command, dir, re):
512 self._simple(command, dir, re[0])
514 self._simple(command, dir)
517 def directories(self, dir, *re):
518 """List subdirectories of a directory.
521 dir -- directory to list, or '' for the whole root.
522 re -- regexp that results must match. Optional.
524 The return value is a list of the (nonempty) subdirectories of dir.
525 If dir is '' then a list of top-level directories is returned.
527 If a regexp is specified then the basename of each result must
528 match. Matching is case-independent. See pcrepattern(3).
530 return self._somedir("dirs", dir, re)
532 def files(self, dir, *re):
533 """List files within a directory.
536 dir -- directory to list, or '' for the whole root.
537 re -- regexp that results must match. Optional.
539 The return value is a list of playable files in dir. If dir is ''
540 then a list of top-level files is returned.
542 If a regexp is specified then the basename of each result must
543 match. Matching is case-independent. See pcrepattern(3).
545 return self._somedir("files", dir, re)
547 def allfiles(self, dir, *re):
548 """List subdirectories and files within 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 all (nonempty) subdirectories and
555 files within dir. If dir is '' then a list of top-level files and
556 directories is returned.
558 If a regexp is specified then the basename of each result must
559 match. Matching is case-independent. See pcrepattern(3).
561 return self._somedir("allfiles", dir, re)
563 def set(self, track, key, value):
564 """Set a preference value.
567 track -- the track to modify
568 key -- the preference name
569 value -- the new preference value
571 self._simple("set", track, key, value)
573 def unset(self, track, key):
574 """Unset a preference value.
577 track -- the track to modify
578 key -- the preference to remove
580 self._simple("set", track, key, value)
582 def get(self, track, key):
583 """Get a preference value.
586 track -- the track to query
587 key -- the preference to remove
589 The return value is the preference
591 ret, details = self._simple("get", track, key)
597 def prefs(self, track):
598 """Get all the preferences for a track.
601 track -- the track to query
603 The return value is a dictionary of all the track's preferences.
604 Note that even nominally numeric values remain encoded as strings.
606 self._simple("prefs", track)
608 for line in self._body():
611 except _splitError, s:
612 raise protocolError(self.who, s.str())
614 raise protocolError(self.who, "invalid prefs body line")
618 def _boolean(self, s):
621 def exists(self, track):
622 """Return true if a track exists
625 track -- the track to check for"""
626 return self._boolean(self._simple("exists", track))
629 """Return true if playing is enabled"""
630 return self._boolean(self._simple("enabled"))
632 def random_enabled(self):
633 """Return true if random play is enabled"""
634 return self._boolean(self._simple("random-enabled"))
636 def random_enable(self):
637 """Enable random play."""
638 self._simple("random-enable")
640 def random_disable(self):
641 """Disable random play."""
642 self._simple("random-disable")
644 def length(self, track):
645 """Return the length of a track in seconds.
648 track -- the track to query.
650 ret, details = self._simple("length", track)
653 def search(self, words):
654 """Search for tracks.
657 words -- the set of words to search for.
659 The return value is a list of track path names, all of which contain
660 all of the required words (in their path name, trackname
663 self._simple("search", _quote(words))
669 The return value is a list of all tags which apply to at least one
675 """Get server statistics.
677 The return value is list of statistics.
679 self._simple("stats")
683 """Get all preferences.
685 The return value is an encoded dump of the preferences database.
690 def set_volume(self, left, right):
694 left -- volume for the left speaker.
695 right -- volume for the right speaker.
697 self._simple("volume", left, right)
699 def get_volume(self):
702 The return value a tuple consisting of the left and right volumes.
704 ret, details = self._simple("volume")
705 return map(int,string.split(details))
707 def move(self, track, delta):
708 """Move a track in the queue.
711 track -- the name or ID of the track to move
712 delta -- the number of steps towards the head of the queue to move
714 ret, details = self._simple("move", track, str(delta))
717 def moveafter(self, target, tracks):
718 """Move a track in the queue
721 target -- target ID or None
722 tracks -- a list of IDs to move
724 If target is '' or is not in the queue then the tracks are moved to
725 the head of the queue.
727 Otherwise the tracks are moved to just after the target."""
730 self._simple("moveafter", target, *tracks)
732 def log(self, callback):
733 """Read event log entries as they happen.
735 Each event log entry is handled by passing it to callback.
737 The callback takes two arguments, the first is the client and the
738 second the line from the event log.
740 The callback should return True to continue or False to stop (don't
741 forget this, or your program will mysteriously misbehave).
743 It is suggested that you use the disorder.monitor class instead of
744 calling this method directly, but this is not mandatory.
746 See disorder_protocol(5) for the event log syntax.
749 callback -- function to call with log entry
751 ret, details = self._simple("log")
754 self._debug(client.debug_body, "<<< %s" % l)
755 if l != '' and l[0] == '.':
759 if not callback(self, l):
761 # tell the server to stop sending, eat the remains of the body,
763 self._send("version")
768 """Pause the current track."""
769 self._simple("pause")
772 """Resume after a pause."""
773 self._simple("resume")
775 def part(self, track, context, part):
776 """Get a track name part
779 track -- the track to query
780 context -- the context ('sort' or 'display')
781 part -- the desired part (usually 'artist', 'album' or 'title')
783 The return value is the preference
785 ret, details = self._simple("part", track, context, part)
788 def setglobal(self, key, value):
789 """Set a global preference value.
792 key -- the preference name
793 value -- the new preference value
795 self._simple("set-global", key, value)
797 def unsetglobal(self, key):
798 """Unset a global preference value.
801 key -- the preference to remove
803 self._simple("set-global", key, value)
805 def getglobal(self, key):
806 """Get a global preference value.
809 key -- the preference to look up
811 The return value is the preference
813 ret, details = self._simple("get-global", key)
819 ########################################################################
823 # read one response line and return as some suitable string object
825 # If an I/O error occurs, disconnect from the server.
827 # XXX does readline() DTRT regarding character encodings?
829 l = self.r.readline()
830 if not re.search("\n", l):
831 raise communicationError(self.who, "peer disconnected")
836 return unicode(l, "UTF-8")
839 # read a response as a (code, details) tuple
841 self._debug(client.debug_proto, "<== %s" % l)
842 m = _response.match(l)
844 return int(m.group(1)), m.group(2)
846 raise protocolError(self.who, "invalid response %s")
848 def _send(self, *command):
849 # Quote and send a command
851 # Returns the encoded command.
852 quoted = _quote(command)
853 self._debug(client.debug_proto, "==> %s" % quoted)
854 encoded = quoted.encode("UTF-8")
856 self.w.write(encoded)
863 raise communicationError(self.who, e)
868 def _simple(self, *command):
869 # Issue a simple command, throw an exception on error
871 # If an I/O error occurs, disconnect from the server.
873 # On success or 'normal' errors returns response as a (code, details) tuple
875 # On error raise operationError
876 if self.state == 'disconnected':
879 cmd = self._send(*command)
882 res, details = self._response()
883 if res / 100 == 2 or res == 555:
885 raise operationError(res, details, cmd)
888 # Fetch a dot-stuffed body
892 self._debug(client.debug_body, "<<< %s" % l)
893 if l != '' and l[0] == '.':
899 ########################################################################
900 # Configuration file parsing
902 def _readfile(self, path):
903 # Read a configuration file
907 # path -- path of file to read
909 # handlers for various commands
910 def _collection(self, command, args):
912 return "'%s' takes three args" % command
913 self.config["collections"].append(args)
915 def _unary(self, command, args):
917 return "'%s' takes only one arg" % command
918 self.config[command] = args[0]
920 def _include(self, command, args):
922 return "'%s' takes only one arg" % command
923 self._readfile(args[0])
925 def _any(self, command, args):
926 self.config[command] = args
928 # mapping of options to handlers
929 _options = { "collection": _collection,
934 "include": _include }
937 for lno, line in enumerate(file(path, "r")):
939 fields = _split(line, 'comments')
940 except _splitError, s:
941 raise parseError(path, lno + 1, str(s))
944 # we just ignore options we don't know about, so as to cope gracefully
945 # with version skew (and nothing to do with implementor laziness)
946 if command in _options:
947 e = _options[command](self, command, fields[1:])
949 self._parseError(path, lno + 1, e)
951 def _parseError(self, path, lno, s):
952 raise parseError(path, lno, s)
954 ########################################################################
958 """DisOrder event log monitor class
960 Intended to be subclassed with methods corresponding to event log messages
961 the implementor cares about over-ridden."""
963 def __init__(self, c=None):
964 """Constructor for the monitor class
966 Can be passed a client to use. If none is specified then one
967 will be created specially for the purpose.
976 """Start monitoring logs. Continues monitoring until one of the
977 message-specific methods returns False. Can be called more than once
978 (but not recursively!)"""
979 self.c.log(self._callback)
982 """Return the timestamp of the current (or most recent) event log entry"""
983 return self.timestamp
985 def _callback(self, c, line):
989 return self.invalid(line)
991 return self.invalid(line)
992 self.timestamp = int(bits[0], 16)
995 if keyword == 'completed':
997 return self.completed(bits[0])
998 elif keyword == 'failed':
1000 return self.failed(bits[0], bits[1])
1001 elif keyword == 'moved':
1006 return self.invalid(line)
1007 return self.moved(bits[0], n, bits[2])
1008 elif keyword == 'playing':
1010 return self.playing(bits[0], None)
1011 elif len(bits) == 2:
1012 return self.playing(bits[0], bits[1])
1013 elif keyword == 'queue' or keyword == 'recent-added':
1015 q = _list2dict(bits)
1017 return self.invalid(line)
1018 if keyword == 'queue':
1019 return self.queue(q)
1020 if keyword == 'recent-added':
1021 return self.recent_added(q)
1022 elif keyword == 'recent-removed':
1024 return self.recent_removed(bits[0])
1025 elif keyword == 'removed':
1027 return self.removed(bits[0], None)
1028 elif len(bits) == 2:
1029 return self.removed(bits[0], bits[1])
1030 elif keyword == 'scratched':
1032 return self.scratched(bits[0], bits[1])
1033 return self.invalid(line)
1035 def completed(self, track):
1036 """Called when a track completes.
1039 track -- track that completed"""
1042 def failed(self, track, error):
1043 """Called when a player suffers an error.
1046 track -- track that failed
1047 error -- error indicator"""
1050 def moved(self, id, offset, user):
1051 """Called when a track is moved in the queue.
1054 id -- queue entry ID
1055 offset -- distance moved
1056 user -- user responsible"""
1059 def playing(self, track, user):
1060 """Called when a track starts playing.
1063 track -- track that has started
1064 user -- user that submitted track, or None"""
1068 """Called when a track is added to the queue.
1071 q -- dictionary of new queue entry"""
1074 def recent_added(self, q):
1075 """Called when a track is added to the recently played list
1078 q -- dictionary of new queue entry"""
1081 def recent_removed(self, id):
1082 """Called when a track is removed from the recently played list
1085 id -- ID of removed entry (always the oldest)"""
1088 def removed(self, id, user):
1089 """Called when a track is removed from the queue, either manually
1090 or in order to play it.
1093 id -- ID of removed entry
1094 user -- user responsible (or None if we're playing this track)"""
1097 def scratched(self, track, user):
1098 """Called when a track is scratched
1101 track -- track that was scratched
1102 user -- user responsible"""
1105 def invalid(self, line):
1106 """Called when an event log line cannot be interpreted
1109 line -- line that could not be understood"""
1114 # py-indent-offset:2