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"
57 # various regexps we'll use
58 _ws = re.compile(r"^[ \t\n\r]+")
59 _squote = re.compile("'(([^\\\\']|\\\\[\\\\\"'n])+)'")
60 _dquote = re.compile("\"(([^\\\\\"]|\\\\[\\\\\"'n])+)\"")
61 _unquoted = re.compile("[^\"' \\t\\n\\r][^ \t\n\r]*")
63 _response = re.compile("([0-9]{3}) ?(.*)")
67 ########################################################################
70 class Error(Exception):
71 """Base class for DisOrder exceptions."""
73 class _splitError(Error):
75 def __init__(self, value):
78 return str(self.value)
80 class parseError(Error):
81 """Error parsing the configuration file."""
82 def __init__(self, path, line, details):
85 self.details = details
87 return "%s:%d: %s" % (self.path, self.line, self.details)
89 class protocolError(Error):
90 """DisOrder control protocol error.
92 Indicates a mismatch between the client and server's understanding of
95 def __init__(self, who, error):
99 return "%s: %s" % (self.who, str(self.error))
101 class operationError(Error):
102 """DisOrder control protocol error response.
104 Indicates that an operation failed (e.g. an attempt to play a
105 nonexistent track). The connection should still be usable.
107 def __init__(self, res, details):
109 self.details_ = details
111 """Return the complete response string from the server.
113 Excludes the final newline.
115 return "%d %s" % (self.res_, self.details_)
117 """Return the response code from the server."""
120 """Returns the detail string from the server."""
123 class communicationError(Error):
124 """DisOrder control protocol communication error.
126 Indicates that communication with the server went wrong, perhaps
127 because the server was restarted. The caller could report an error to
128 the user and wait for further user instructions, or even automatically
131 def __init__(self, who, error):
135 return "%s: %s" % (self.who, str(self.error))
137 ########################################################################
138 # DisOrder-specific text processing
141 # Unescape the contents of a string
145 # s -- string to unescape
147 s = re.sub("\\\\n", "\n", s)
148 s = re.sub("\\\\(.)", "\\1", s)
151 def _split(s, *comments):
152 # Split a string into fields according to the usual Disorder string splitting
157 # s -- string to parse
158 # comments -- if present, parse comments
162 # On success, a list of fields is returned.
164 # On error, disorder.parseError is thrown.
169 if comments and s[0] == '#':
176 # pick of quoted fields of both kinds
181 fields.append(_unescape(m.group(1)))
184 # and unquoted fields
185 m = _unquoted.match(s)
187 fields.append(m.group(0))
190 # anything left must be in error
191 if s[0] == '"' or s[0] == '\'':
192 raise _splitError("invalid quoted string")
194 raise _splitError("syntax error")
198 # Escape the contents of a string
202 # s -- string to escape
204 if re.search("[\\\\\"'\n \t\r]", s) or s == '':
205 s = re.sub(r'[\\"]', r'\\\g<0>', s)
206 s = re.sub("\n", r"\\n", s)
212 # Quote a list of values
213 return ' '.join(map(_escape, list))
216 # Return the value of s in a form suitable for writing to stderr
217 return s.encode(locale.nl_langinfo(locale.CODESET), 'replace')
220 # Convert a list of the form [k1, v1, k2, v2, ..., kN, vN]
221 # to a dictionary {k1:v1, k2:v2, ..., kN:vN}
229 except StopIteration:
234 # parse a queue entry
235 return _list2dict(_split(s))
237 ########################################################################
241 """DisOrder client class.
243 This class provides access to the DisOrder server either on this
244 machine or across the internet.
246 The server to connect to, and the username and password to use, are
247 determined from the configuration files as described in 'man
250 All methods will connect if necessary, as soon as you have a
251 disorder.client object you can start calling operational methods on
254 However if the server is restarted then the next method called on a
255 connection will throw an exception. This may be considered a bug.
257 All methods block until they complete.
259 Operation methods raise communicationError if the connection breaks,
260 protocolError if the response from the server is malformed, or
261 operationError if the response is valid but indicates that the
269 """Constructor for DisOrder client class.
271 The constructor reads the configuration file, but does not connect
274 If the environment variable DISORDER_PYTHON_DEBUG is set then the
275 debug flags are initialised to that value. This can be overridden
276 with the debug() method below.
278 The constructor Raises parseError() if the configuration file is not
281 pw = pwd.getpwuid(os.getuid())
282 self.debugging = int(os.getenv("DISORDER_PYTHON_DEBUG", 0))
283 self.config = { 'collections': [],
284 'username': pw.pw_name,
286 home = os.getenv("HOME")
289 privconf = _configfile + "." + pw.pw_name
290 passfile = home + os.sep + ".disorder" + os.sep + "passwd"
291 self._readfile(_configfile)
292 if os.path.exists(privconf):
293 self._readfile(privconf)
294 if os.path.exists(passfile):
295 self._readfile(passfile)
296 self.state = 'disconnected'
298 def debug(self, bits):
299 """Enable or disable protocol debugging. Debug messages are written
303 bits -- bitmap of operations that should generate debug information
306 debug_proto -- dump control protocol messages (excluding bodies)
307 debug_body -- dump control protocol message bodies
309 self.debugging = bits
311 def _debug(self, bit, s):
313 if self.debugging & bit:
314 sys.stderr.write(_sanitize(s))
315 sys.stderr.write("\n")
319 """Connect to the DisOrder server and authenticate.
321 Raises communicationError if connection fails and operationError if
322 authentication fails (in which case disconnection is automatic).
324 May be called more than once to retry connections (e.g. when the
325 server is down). If we are already connected and authenticated,
328 Other operations automatically connect if we're not already
329 connected, so it is not strictly necessary to call this method.
331 if self.state == 'disconnected':
333 self.state = 'connecting'
334 if 'connect' in self.config and len(self.config['connect']) > 0:
335 c = self.config['connect']
336 self.who = repr(c) # temporarily
338 a = socket.getaddrinfo(None, c[0],
344 a = socket.getaddrinfo(c[0], c[1],
350 s = socket.socket(a[0], a[1], a[2]);
352 self.who = "%s" % a[3]
354 s = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM);
355 self.who = self.config['home'] + os.sep + "socket"
357 self.w = s.makefile("wb")
358 self.r = s.makefile("rb")
359 (res, challenge) = self._simple()
361 h.update(self.config['password'])
362 h.update(binascii.unhexlify(challenge))
363 self._simple("user", self.config['username'], h.hexdigest())
364 self.state = 'connected'
365 except socket.error, e:
367 raise communicationError(self.who, e)
372 def _disconnect(self):
373 # disconnect from the server, whatever state we are in
379 self.state = 'disconnected'
381 ########################################################################
384 def become(self, who):
385 """Become another user.
388 who -- the user to become.
390 Only trusted users can perform this operation.
392 self._simple("become", who)
394 def play(self, track):
398 track -- the path of the track to play.
400 self._simple("play", track)
402 def remove(self, track):
403 """Remove a track from the queue.
406 track -- the path or ID of the track to remove.
408 self._simple("remove", track)
411 """Enable playing."""
412 self._simple("enable")
414 def disable(self, *now):
418 now -- if present (with any value), the current track is stopped
422 self._simple("disable", "now")
424 self._simple("disable")
426 def scratch(self, *id):
427 """Scratch the currently playing track.
430 id -- if present, the ID of the track to scratch.
433 self._simple("scratch", id[0])
435 self._simple("scratch")
438 """Shut down the server.
440 Only trusted users can perform this operation.
442 self._simple("shutdown")
444 def reconfigure(self):
445 """Make the server reload its configuration.
447 Only trusted users can perform this operation.
449 self._simple("reconfigure")
451 def rescan(self, pattern):
452 """Rescan one or more collections.
455 pattern -- glob pattern matching collections to rescan.
457 Only trusted users can perform this operation.
459 self._simple("rescan", pattern)
462 """Return the server's version number."""
463 return self._simple("version")[1]
466 """Return the currently playing track.
468 If a track is playing then it is returned as a dictionary.
469 If no track is playing then None is returned."""
470 res, details = self._simple("playing")
473 return _queueEntry(details)
474 except _splitError, s:
475 raise protocolError(self.who, s.str())
479 def _somequeue(self, command):
480 self._simple(command)
482 return map(lambda s: _queueEntry(s), self._body())
483 except _splitError, s:
484 raise protocolError(self.who, s.str())
487 """Return a list of recently played tracks.
489 The return value is a list of dictionaries corresponding to
490 recently played tracks. The oldest track comes first."""
491 return self._somequeue("recent")
494 """Return the current queue.
496 The return value is a list of dictionaries corresponding to
497 recently played tracks. The next track to be played comes first."""
498 return self._somequeue("queue")
500 def _somedir(self, command, dir, re):
502 self._simple(command, dir, re[0])
504 self._simple(command, dir)
507 def directories(self, dir, *re):
508 """List subdirectories of a directory.
511 dir -- directory to list, or '' for the whole root.
512 re -- regexp that results must match. Optional.
514 The return value is a list of the (nonempty) subdirectories of dir.
515 If dir is '' then a list of top-level directories is returned.
517 If a regexp is specified then the basename of each result must
518 match. Matching is case-independent. See pcrepattern(3).
520 return self._somedir("dirs", dir, re)
522 def files(self, dir, *re):
523 """List files within a directory.
526 dir -- directory to list, or '' for the whole root.
527 re -- regexp that results must match. Optional.
529 The return value is a list of playable files in dir. If dir is ''
530 then a list of top-level files is returned.
532 If a regexp is specified then the basename of each result must
533 match. Matching is case-independent. See pcrepattern(3).
535 return self._somedir("files", dir, re)
537 def allfiles(self, dir, *re):
538 """List subdirectories and files within a directory.
541 dir -- directory to list, or '' for the whole root.
542 re -- regexp that results must match. Optional.
544 The return value is a list of all (nonempty) subdirectories and
545 files within dir. If dir is '' then a list of top-level files and
546 directories is returned.
548 If a regexp is specified then the basename of each result must
549 match. Matching is case-independent. See pcrepattern(3).
551 return self._somedir("allfiles", dir, re)
553 def set(self, track, key, value):
554 """Set a preference value.
557 track -- the track to modify
558 key -- the preference name
559 value -- the new preference value
561 self._simple("set", track, key, value)
563 def unset(self, track, key):
564 """Unset a preference value.
567 track -- the track to modify
568 key -- the preference to remove
570 self._simple("set", track, key, value)
572 def get(self, track, key):
573 """Get a preference value.
576 track -- the track to query
577 key -- the preference to remove
579 The return value is the preference
581 ret, details = self._simple("get", track, key)
584 def prefs(self, track):
585 """Get all the preferences for a track.
588 track -- the track to query
590 The return value is a dictionary of all the track's preferences.
591 Note that even nominally numeric values remain encoded as strings.
593 self._simple("prefs", track)
595 for line in self._body():
598 except _splitError, s:
599 raise protocolError(self.who, s.str())
601 raise protocolError(self.who, "invalid prefs body line")
605 def _boolean(self, s):
608 def exists(self, track):
609 """Return true if a track exists
612 track -- the track to check for"""
613 return self._boolean(self._simple("exists", track))
616 """Return true if playing is enabled"""
617 return self._boolean(self._simple("enabled"))
619 def random_enabled(self):
620 """Return true if random play is enabled"""
621 return self._boolean(self._simple("random-enabled"))
623 def random_enable(self):
624 """Enable random play."""
625 self._simple("random-enable")
627 def random_disable(self):
628 """Disable random play."""
629 self._simple("random-disable")
631 def length(self, track):
632 """Return the length of a track in seconds.
635 track -- the track to query.
637 ret, details = self._simple("length", track)
640 def search(self, words):
641 """Search for tracks.
644 words -- the set of words to search for.
646 The return value is a list of track path names, all of which contain
647 all of the required words (in their path name, trackname
650 self._simple("search", *words)
654 """Get server statistics.
656 The return value is list of statistics.
658 self._simple("stats")
662 """Get all preferences.
664 The return value is an encoded dump of the preferences database.
669 def set_volume(self, left, right):
673 left -- volume for the left speaker.
674 right -- volume for the right speaker.
676 self._simple("volume", left, right)
678 def get_volume(self):
681 The return value a tuple consisting of the left and right volumes.
683 ret, details = self._simple("volume")
684 return map(int,string.split(details))
686 def move(self, track, delta):
687 """Move a track in the queue.
690 track -- the name or ID of the track to move
691 delta -- the number of steps towards the head of the queue to move
693 ret, details = self._simple("move", track, str(delta))
696 def log(self, callback):
697 """Read event log entries as they happen.
699 Each event log entry is handled by passing it to callback.
701 The callback takes two arguments, the first is the client and the
702 second the line from the event log.
704 The callback should return True to continue or False to stop (don't
705 forget this, or your program will mysteriously misbehave).
707 It is suggested that you use the disorder.monitor class instead of
708 calling this method directly, but this is not mandatory.
710 See disorder_protocol(5) for the event log syntax.
713 callback -- function to call with log entry
715 ret, details = self._simple("log")
718 self._debug(client.debug_body, "<<< %s" % l)
719 if l != '' and l[0] == '.':
723 if not callback(self, l):
725 # tell the server to stop sending, eat the remains of the body,
727 self._send("version")
732 """Pause the current track."""
733 self._simple("pause")
736 """Resume after a pause."""
737 self._simple("resume")
739 def part(self, track, context, part):
740 """Get a track name part
743 track -- the track to query
744 context -- the context ('sort' or 'display')
745 part -- the desired part (usually 'artist', 'album' or 'title')
747 The return value is the preference
749 ret, details = self._simple("part", track, context, part)
752 ########################################################################
756 # read one response line and return as some suitable string object
758 # If an I/O error occurs, disconnect from the server.
760 # XXX does readline() DTRT regarding character encodings?
762 l = self.r.readline()
763 if not re.search("\n", l):
764 raise communicationError(self.who, "peer disconnected")
769 return unicode(l, "UTF-8")
772 # read a response as a (code, details) tuple
774 self._debug(client.debug_proto, "<== %s" % l)
775 m = _response.match(l)
777 return int(m.group(1)), m.group(2)
779 raise protocolError(self.who, "invalid response %s")
781 def _send(self, *command):
782 quoted = _quote(command)
783 self._debug(client.debug_proto, "==> %s" % quoted)
784 encoded = quoted.encode("UTF-8")
786 self.w.write(encoded)
792 raise communicationError(self.who, e)
797 def _simple(self, *command):
798 # Issue a simple command, throw an exception on error
800 # If an I/O error occurs, disconnect from the server.
802 # On success returns response as a (code, details) tuple
804 # On error raise operationError
805 if self.state == 'disconnected':
809 res, details = self._response()
812 raise operationError(res, details)
815 # Fetch a dot-stuffed body
819 self._debug(client.debug_body, "<<< %s" % l)
820 if l != '' and l[0] == '.':
826 ########################################################################
827 # Configuration file parsing
829 def _readfile(self, path):
830 # Read a configuration file
834 # path -- path of file to read
836 # handlers for various commands
837 def _collection(self, command, args):
839 return "'%s' takes three args" % command
840 self.config["collections"].append(args)
842 def _unary(self, command, args):
844 return "'%s' takes only one arg" % command
845 self.config[command] = args[0]
847 def _include(self, command, args):
849 return "'%s' takes only one arg" % command
850 self._readfile(args[0])
852 def _any(self, command, args):
853 self.config[command] = args
855 # mapping of options to handlers
856 _options = { "collection": _collection,
861 "include": _include }
864 for lno, line in enumerate(file(path, "r")):
866 fields = _split(line, 'comments')
867 except _splitError, s:
868 raise parseError(path, lno + 1, str(s))
871 # we just ignore options we don't know about, so as to cope gracefully
872 # with version skew (and nothing to do with implementor laziness)
873 if command in _options:
874 e = _options[command](self, command, fields[1:])
876 self._parseError(path, lno + 1, e)
878 def _parseError(self, path, lno, s):
879 raise parseError(path, lno, s)
881 ########################################################################
885 """DisOrder event log monitor class
887 Intended to be subclassed with methods corresponding to event log messages
888 the implementor cares about over-ridden."""
890 def __init__(self, c=None):
891 """Constructor for the monitor class
893 Can be passed a client to use. If none is specified then one
894 will be created specially for the purpose.
903 """Start monitoring logs. Continues monitoring until one of the
904 message-specific methods returns False. Can be called more than once
905 (but not recursively!)"""
906 self.c.log(self._callback)
909 """Return the timestamp of the current (or most recent) event log entry"""
910 return self.timestamp
912 def _callback(self, c, line):
916 return self.invalid(line)
918 return self.invalid(line)
919 self.timestamp = int(bits[0], 16)
922 if keyword == 'completed':
924 return self.completed(bits[0])
925 elif keyword == 'failed':
927 return self.failed(bits[0], bits[1])
928 elif keyword == 'moved':
933 return self.invalid(line)
934 return self.moved(bits[0], n, bits[2])
935 elif keyword == 'playing':
937 return self.playing(bits[0], None)
939 return self.playing(bits[0], bits[1])
940 elif keyword == 'queue' or keyword == 'recent-added':
944 return self.invalid(line)
945 if keyword == 'queue':
947 if keyword == 'recent-added':
948 return self.recent_added(q)
949 elif keyword == 'recent-removed':
951 return self.recent_removed(bits[0])
952 elif keyword == 'removed':
954 return self.removed(bits[0], None)
956 return self.removed(bits[0], bits[1])
957 elif keyword == 'scratched':
959 return self.scratched(bits[0], bits[1])
960 return self.invalid(line)
962 def completed(self, track):
963 """Called when a track completes.
966 track -- track that completed"""
969 def failed(self, track, error):
970 """Called when a player suffers an error.
973 track -- track that failed
974 error -- error indicator"""
977 def moved(self, id, offset, user):
978 """Called when a track is moved in the queue.
982 offset -- distance moved
983 user -- user responsible"""
986 def playing(self, track, user):
987 """Called when a track starts playing.
990 track -- track that has started
991 user -- user that submitted track, or None"""
995 """Called when a track is added to the queue.
998 q -- dictionary of new queue entry"""
1001 def recent_added(self, q):
1002 """Called when a track is added to the recently played list
1005 q -- dictionary of new queue entry"""
1008 def recent_removed(self, id):
1009 """Called when a track is removed from the recently played list
1012 id -- ID of removed entry (always the oldest)"""
1015 def removed(self, id, user):
1016 """Called when a track is removed from the queue, either manually
1017 or in order to play it.
1020 id -- ID of removed entry
1021 user -- user responsible (or None if we're playing this track)"""
1024 def scratched(self, track, user):
1025 """Called when a track is scratched
1028 track -- track that was scratched
1029 user -- user responsible"""
1032 def invalid(self, line):
1033 """Called when an event log line cannot be interpreted
1036 line -- line that could not be understood"""
1041 # py-indent-offset:2