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 self._readfile(_configfile)
298 if os.path.exists(privconf):
299 self._readfile(privconf)
300 if os.path.exists(passfile) and _userconf:
301 self._readfile(passfile)
302 self.state = 'disconnected'
304 def debug(self, bits):
305 """Enable or disable protocol debugging. Debug messages are written
309 bits -- bitmap of operations that should generate debug information
312 debug_proto -- dump control protocol messages (excluding bodies)
313 debug_body -- dump control protocol message bodies
315 self.debugging = bits
317 def _debug(self, bit, s):
319 if self.debugging & bit:
320 sys.stderr.write(_sanitize(s))
321 sys.stderr.write("\n")
325 """Connect to the DisOrder server and authenticate.
327 Raises communicationError if connection fails and operationError if
328 authentication fails (in which case disconnection is automatic).
330 May be called more than once to retry connections (e.g. when the
331 server is down). If we are already connected and authenticated,
334 Other operations automatically connect if we're not already
335 connected, so it is not strictly necessary to call this method.
337 if self.state == 'disconnected':
339 self.state = 'connecting'
340 if 'connect' in self.config and len(self.config['connect']) > 0:
341 c = self.config['connect']
342 self.who = repr(c) # temporarily
344 a = socket.getaddrinfo(None, c[0],
350 a = socket.getaddrinfo(c[0], c[1],
356 s = socket.socket(a[0], a[1], a[2]);
358 self.who = "%s" % a[3]
360 s = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM);
361 self.who = self.config['home'] + os.sep + "socket"
363 self.w = s.makefile("wb")
364 self.r = s.makefile("rb")
365 (res, challenge) = self._simple()
367 h.update(self.config['password'])
368 h.update(binascii.unhexlify(challenge))
369 self._simple("user", self.config['username'], h.hexdigest())
370 self.state = 'connected'
371 except socket.error, e:
373 raise communicationError(self.who, e)
378 def _disconnect(self):
379 # disconnect from the server, whatever state we are in
385 self.state = 'disconnected'
387 ########################################################################
390 def become(self, who):
391 """Become another user.
394 who -- the user to become.
396 Only trusted users can perform this operation.
398 self._simple("become", who)
400 def play(self, track):
404 track -- the path of the track to play.
406 self._simple("play", track)
408 def remove(self, track):
409 """Remove a track from the queue.
412 track -- the path or ID of the track to remove.
414 self._simple("remove", track)
417 """Enable playing."""
418 self._simple("enable")
420 def disable(self, *now):
424 now -- if present (with any value), the current track is stopped
428 self._simple("disable", "now")
430 self._simple("disable")
432 def scratch(self, *id):
433 """Scratch the currently playing track.
436 id -- if present, the ID of the track to scratch.
439 self._simple("scratch", id[0])
441 self._simple("scratch")
444 """Shut down the server.
446 Only trusted users can perform this operation.
448 self._simple("shutdown")
450 def reconfigure(self):
451 """Make the server reload its configuration.
453 Only trusted users can perform this operation.
455 self._simple("reconfigure")
457 def rescan(self, pattern):
458 """Rescan one or more collections.
461 pattern -- glob pattern matching collections to rescan.
463 Only trusted users can perform this operation.
465 self._simple("rescan", pattern)
468 """Return the server's version number."""
469 return self._simple("version")[1]
472 """Return the currently playing track.
474 If a track is playing then it is returned as a dictionary.
475 If no track is playing then None is returned."""
476 res, details = self._simple("playing")
479 return _queueEntry(details)
480 except _splitError, s:
481 raise protocolError(self.who, s.str())
485 def _somequeue(self, command):
486 self._simple(command)
488 return map(lambda s: _queueEntry(s), self._body())
489 except _splitError, s:
490 raise protocolError(self.who, s.str())
493 """Return a list of recently played tracks.
495 The return value is a list of dictionaries corresponding to
496 recently played tracks. The oldest track comes first."""
497 return self._somequeue("recent")
500 """Return the current queue.
502 The return value is a list of dictionaries corresponding to
503 recently played tracks. The next track to be played comes first."""
504 return self._somequeue("queue")
506 def _somedir(self, command, dir, re):
508 self._simple(command, dir, re[0])
510 self._simple(command, dir)
513 def directories(self, dir, *re):
514 """List subdirectories of a directory.
517 dir -- directory to list, or '' for the whole root.
518 re -- regexp that results must match. Optional.
520 The return value is a list of the (nonempty) subdirectories of dir.
521 If dir is '' then a list of top-level directories is returned.
523 If a regexp is specified then the basename of each result must
524 match. Matching is case-independent. See pcrepattern(3).
526 return self._somedir("dirs", dir, re)
528 def files(self, dir, *re):
529 """List files within a directory.
532 dir -- directory to list, or '' for the whole root.
533 re -- regexp that results must match. Optional.
535 The return value is a list of playable files in dir. If dir is ''
536 then a list of top-level files is returned.
538 If a regexp is specified then the basename of each result must
539 match. Matching is case-independent. See pcrepattern(3).
541 return self._somedir("files", dir, re)
543 def allfiles(self, dir, *re):
544 """List subdirectories and files within a directory.
547 dir -- directory to list, or '' for the whole root.
548 re -- regexp that results must match. Optional.
550 The return value is a list of all (nonempty) subdirectories and
551 files within dir. If dir is '' then a list of top-level files and
552 directories is returned.
554 If a regexp is specified then the basename of each result must
555 match. Matching is case-independent. See pcrepattern(3).
557 return self._somedir("allfiles", dir, re)
559 def set(self, track, key, value):
560 """Set a preference value.
563 track -- the track to modify
564 key -- the preference name
565 value -- the new preference value
567 self._simple("set", track, key, value)
569 def unset(self, track, key):
570 """Unset a preference value.
573 track -- the track to modify
574 key -- the preference to remove
576 self._simple("set", track, key, value)
578 def get(self, track, key):
579 """Get a preference value.
582 track -- the track to query
583 key -- the preference to remove
585 The return value is the preference
587 ret, details = self._simple("get", track, key)
593 def prefs(self, track):
594 """Get all the preferences for a track.
597 track -- the track to query
599 The return value is a dictionary of all the track's preferences.
600 Note that even nominally numeric values remain encoded as strings.
602 self._simple("prefs", track)
604 for line in self._body():
607 except _splitError, s:
608 raise protocolError(self.who, s.str())
610 raise protocolError(self.who, "invalid prefs body line")
614 def _boolean(self, s):
617 def exists(self, track):
618 """Return true if a track exists
621 track -- the track to check for"""
622 return self._boolean(self._simple("exists", track))
625 """Return true if playing is enabled"""
626 return self._boolean(self._simple("enabled"))
628 def random_enabled(self):
629 """Return true if random play is enabled"""
630 return self._boolean(self._simple("random-enabled"))
632 def random_enable(self):
633 """Enable random play."""
634 self._simple("random-enable")
636 def random_disable(self):
637 """Disable random play."""
638 self._simple("random-disable")
640 def length(self, track):
641 """Return the length of a track in seconds.
644 track -- the track to query.
646 ret, details = self._simple("length", track)
649 def search(self, words):
650 """Search for tracks.
653 words -- the set of words to search for.
655 The return value is a list of track path names, all of which contain
656 all of the required words (in their path name, trackname
659 self._simple("search", _quote(words))
665 The return value is a list of all tags which apply to at least one
671 """Get server statistics.
673 The return value is list of statistics.
675 self._simple("stats")
679 """Get all preferences.
681 The return value is an encoded dump of the preferences database.
686 def set_volume(self, left, right):
690 left -- volume for the left speaker.
691 right -- volume for the right speaker.
693 self._simple("volume", left, right)
695 def get_volume(self):
698 The return value a tuple consisting of the left and right volumes.
700 ret, details = self._simple("volume")
701 return map(int,string.split(details))
703 def move(self, track, delta):
704 """Move a track in the queue.
707 track -- the name or ID of the track to move
708 delta -- the number of steps towards the head of the queue to move
710 ret, details = self._simple("move", track, str(delta))
713 def log(self, callback):
714 """Read event log entries as they happen.
716 Each event log entry is handled by passing it to callback.
718 The callback takes two arguments, the first is the client and the
719 second the line from the event log.
721 The callback should return True to continue or False to stop (don't
722 forget this, or your program will mysteriously misbehave).
724 It is suggested that you use the disorder.monitor class instead of
725 calling this method directly, but this is not mandatory.
727 See disorder_protocol(5) for the event log syntax.
730 callback -- function to call with log entry
732 ret, details = self._simple("log")
735 self._debug(client.debug_body, "<<< %s" % l)
736 if l != '' and l[0] == '.':
740 if not callback(self, l):
742 # tell the server to stop sending, eat the remains of the body,
744 self._send("version")
749 """Pause the current track."""
750 self._simple("pause")
753 """Resume after a pause."""
754 self._simple("resume")
756 def part(self, track, context, part):
757 """Get a track name part
760 track -- the track to query
761 context -- the context ('sort' or 'display')
762 part -- the desired part (usually 'artist', 'album' or 'title')
764 The return value is the preference
766 ret, details = self._simple("part", track, context, part)
769 def setglobal(self, key, value):
770 """Set a global preference value.
773 key -- the preference name
774 value -- the new preference value
776 self._simple("set-global", key, value)
778 def unsetglobal(self, key):
779 """Unset a global preference value.
782 key -- the preference to remove
784 self._simple("set-global", key, value)
786 def getglobal(self, key):
787 """Get a global preference value.
790 key -- the preference to look up
792 The return value is the preference
794 ret, details = self._simple("get-global", key)
800 ########################################################################
804 # read one response line and return as some suitable string object
806 # If an I/O error occurs, disconnect from the server.
808 # XXX does readline() DTRT regarding character encodings?
810 l = self.r.readline()
811 if not re.search("\n", l):
812 raise communicationError(self.who, "peer disconnected")
817 return unicode(l, "UTF-8")
820 # read a response as a (code, details) tuple
822 self._debug(client.debug_proto, "<== %s" % l)
823 m = _response.match(l)
825 return int(m.group(1)), m.group(2)
827 raise protocolError(self.who, "invalid response %s")
829 def _send(self, *command):
830 # Quote and send a command
832 # Returns the encoded command.
833 quoted = _quote(command)
834 self._debug(client.debug_proto, "==> %s" % quoted)
835 encoded = quoted.encode("UTF-8")
837 self.w.write(encoded)
844 raise communicationError(self.who, e)
849 def _simple(self, *command):
850 # Issue a simple command, throw an exception on error
852 # If an I/O error occurs, disconnect from the server.
854 # On success or 'normal' errors returns response as a (code, details) tuple
856 # On error raise operationError
857 if self.state == 'disconnected':
860 cmd = self._send(*command)
863 res, details = self._response()
864 if res / 100 == 2 or res == 555:
866 raise operationError(res, details, cmd)
869 # Fetch a dot-stuffed body
873 self._debug(client.debug_body, "<<< %s" % l)
874 if l != '' and l[0] == '.':
880 ########################################################################
881 # Configuration file parsing
883 def _readfile(self, path):
884 # Read a configuration file
888 # path -- path of file to read
890 # handlers for various commands
891 def _collection(self, command, args):
893 return "'%s' takes three args" % command
894 self.config["collections"].append(args)
896 def _unary(self, command, args):
898 return "'%s' takes only one arg" % command
899 self.config[command] = args[0]
901 def _include(self, command, args):
903 return "'%s' takes only one arg" % command
904 self._readfile(args[0])
906 def _any(self, command, args):
907 self.config[command] = args
909 # mapping of options to handlers
910 _options = { "collection": _collection,
915 "include": _include }
918 for lno, line in enumerate(file(path, "r")):
920 fields = _split(line, 'comments')
921 except _splitError, s:
922 raise parseError(path, lno + 1, str(s))
925 # we just ignore options we don't know about, so as to cope gracefully
926 # with version skew (and nothing to do with implementor laziness)
927 if command in _options:
928 e = _options[command](self, command, fields[1:])
930 self._parseError(path, lno + 1, e)
932 def _parseError(self, path, lno, s):
933 raise parseError(path, lno, s)
935 ########################################################################
939 """DisOrder event log monitor class
941 Intended to be subclassed with methods corresponding to event log messages
942 the implementor cares about over-ridden."""
944 def __init__(self, c=None):
945 """Constructor for the monitor class
947 Can be passed a client to use. If none is specified then one
948 will be created specially for the purpose.
957 """Start monitoring logs. Continues monitoring until one of the
958 message-specific methods returns False. Can be called more than once
959 (but not recursively!)"""
960 self.c.log(self._callback)
963 """Return the timestamp of the current (or most recent) event log entry"""
964 return self.timestamp
966 def _callback(self, c, line):
970 return self.invalid(line)
972 return self.invalid(line)
973 self.timestamp = int(bits[0], 16)
976 if keyword == 'completed':
978 return self.completed(bits[0])
979 elif keyword == 'failed':
981 return self.failed(bits[0], bits[1])
982 elif keyword == 'moved':
987 return self.invalid(line)
988 return self.moved(bits[0], n, bits[2])
989 elif keyword == 'playing':
991 return self.playing(bits[0], None)
993 return self.playing(bits[0], bits[1])
994 elif keyword == 'queue' or keyword == 'recent-added':
998 return self.invalid(line)
999 if keyword == 'queue':
1000 return self.queue(q)
1001 if keyword == 'recent-added':
1002 return self.recent_added(q)
1003 elif keyword == 'recent-removed':
1005 return self.recent_removed(bits[0])
1006 elif keyword == 'removed':
1008 return self.removed(bits[0], None)
1009 elif len(bits) == 2:
1010 return self.removed(bits[0], bits[1])
1011 elif keyword == 'scratched':
1013 return self.scratched(bits[0], bits[1])
1014 return self.invalid(line)
1016 def completed(self, track):
1017 """Called when a track completes.
1020 track -- track that completed"""
1023 def failed(self, track, error):
1024 """Called when a player suffers an error.
1027 track -- track that failed
1028 error -- error indicator"""
1031 def moved(self, id, offset, user):
1032 """Called when a track is moved in the queue.
1035 id -- queue entry ID
1036 offset -- distance moved
1037 user -- user responsible"""
1040 def playing(self, track, user):
1041 """Called when a track starts playing.
1044 track -- track that has started
1045 user -- user that submitted track, or None"""
1049 """Called when a track is added to the queue.
1052 q -- dictionary of new queue entry"""
1055 def recent_added(self, q):
1056 """Called when a track is added to the recently played list
1059 q -- dictionary of new queue entry"""
1062 def recent_removed(self, id):
1063 """Called when a track is removed from the recently played list
1066 id -- ID of removed entry (always the oldest)"""
1069 def removed(self, id, user):
1070 """Called when a track is removed from the queue, either manually
1071 or in order to play it.
1074 id -- ID of removed entry
1075 user -- user responsible (or None if we're playing this track)"""
1078 def scratched(self, track, user):
1079 """Called when a track is scratched
1082 track -- track that was scratched
1083 user -- user responsible"""
1086 def invalid(self, line):
1087 """Called when an event log line cannot be interpreted
1090 line -- line that could not be understood"""
1095 # py-indent-offset:2