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):
110 self.details_ = details
112 """Return the complete response string from the server.
114 Excludes the final newline.
116 return "%d %s" % (self.res_, self.details_)
118 """Return the response code from the server."""
121 """Returns the detail string from the server."""
124 class communicationError(Error):
125 """DisOrder control protocol communication error.
127 Indicates that communication with the server went wrong, perhaps
128 because the server was restarted. The caller could report an error to
129 the user and wait for further user instructions, or even automatically
132 def __init__(self, who, error):
136 return "%s: %s" % (self.who, str(self.error))
138 ########################################################################
139 # DisOrder-specific text processing
142 # Unescape the contents of a string
146 # s -- string to unescape
148 s = re.sub("\\\\n", "\n", s)
149 s = re.sub("\\\\(.)", "\\1", s)
152 def _split(s, *comments):
153 # Split a string into fields according to the usual Disorder string splitting
158 # s -- string to parse
159 # comments -- if present, parse comments
163 # On success, a list of fields is returned.
165 # On error, disorder.parseError is thrown.
170 if comments and s[0] == '#':
177 # pick of quoted fields of both kinds
182 fields.append(_unescape(m.group(1)))
185 # and unquoted fields
186 m = _unquoted.match(s)
188 fields.append(m.group(0))
191 # anything left must be in error
192 if s[0] == '"' or s[0] == '\'':
193 raise _splitError("invalid quoted string")
195 raise _splitError("syntax error")
199 # Escape the contents of a string
203 # s -- string to escape
205 if re.search("[\\\\\"'\n \t\r]", s) or s == '':
206 s = re.sub(r'[\\"]', r'\\\g<0>', s)
207 s = re.sub("\n", r"\\n", s)
213 # Quote a list of values
214 return ' '.join(map(_escape, list))
217 # Return the value of s in a form suitable for writing to stderr
218 return s.encode(locale.nl_langinfo(locale.CODESET), 'replace')
221 # Convert a list of the form [k1, v1, k2, v2, ..., kN, vN]
222 # to a dictionary {k1:v1, k2:v2, ..., kN:vN}
230 except StopIteration:
235 # parse a queue entry
236 return _list2dict(_split(s))
238 ########################################################################
242 """DisOrder client class.
244 This class provides access to the DisOrder server either on this
245 machine or across the internet.
247 The server to connect to, and the username and password to use, are
248 determined from the configuration files as described in 'man
251 All methods will connect if necessary, as soon as you have a
252 disorder.client object you can start calling operational methods on
255 However if the server is restarted then the next method called on a
256 connection will throw an exception. This may be considered a bug.
258 All methods block until they complete.
260 Operation methods raise communicationError if the connection breaks,
261 protocolError if the response from the server is malformed, or
262 operationError if the response is valid but indicates that the
270 """Constructor for DisOrder client class.
272 The constructor reads the configuration file, but does not connect
275 If the environment variable DISORDER_PYTHON_DEBUG is set then the
276 debug flags are initialised to that value. This can be overridden
277 with the debug() method below.
279 The constructor Raises parseError() if the configuration file is not
282 pw = pwd.getpwuid(os.getuid())
283 self.debugging = int(os.getenv("DISORDER_PYTHON_DEBUG", 0))
284 self.config = { 'collections': [],
285 'username': pw.pw_name,
287 home = os.getenv("HOME")
290 privconf = _configfile + "." + pw.pw_name
291 passfile = home + os.sep + ".disorder" + os.sep + "passwd"
292 self._readfile(_configfile)
293 if os.path.exists(privconf):
294 self._readfile(privconf)
295 if os.path.exists(passfile) and _userconf:
296 self._readfile(passfile)
297 self.state = 'disconnected'
299 def debug(self, bits):
300 """Enable or disable protocol debugging. Debug messages are written
304 bits -- bitmap of operations that should generate debug information
307 debug_proto -- dump control protocol messages (excluding bodies)
308 debug_body -- dump control protocol message bodies
310 self.debugging = bits
312 def _debug(self, bit, s):
314 if self.debugging & bit:
315 sys.stderr.write(_sanitize(s))
316 sys.stderr.write("\n")
320 """Connect to the DisOrder server and authenticate.
322 Raises communicationError if connection fails and operationError if
323 authentication fails (in which case disconnection is automatic).
325 May be called more than once to retry connections (e.g. when the
326 server is down). If we are already connected and authenticated,
329 Other operations automatically connect if we're not already
330 connected, so it is not strictly necessary to call this method.
332 if self.state == 'disconnected':
334 self.state = 'connecting'
335 if 'connect' in self.config and len(self.config['connect']) > 0:
336 c = self.config['connect']
337 self.who = repr(c) # temporarily
339 a = socket.getaddrinfo(None, c[0],
345 a = socket.getaddrinfo(c[0], c[1],
351 s = socket.socket(a[0], a[1], a[2]);
353 self.who = "%s" % a[3]
355 s = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM);
356 self.who = self.config['home'] + os.sep + "socket"
358 self.w = s.makefile("wb")
359 self.r = s.makefile("rb")
360 (res, challenge) = self._simple()
362 h.update(self.config['password'])
363 h.update(binascii.unhexlify(challenge))
364 self._simple("user", self.config['username'], h.hexdigest())
365 self.state = 'connected'
366 except socket.error, e:
368 raise communicationError(self.who, e)
373 def _disconnect(self):
374 # disconnect from the server, whatever state we are in
380 self.state = 'disconnected'
382 ########################################################################
385 def become(self, who):
386 """Become another user.
389 who -- the user to become.
391 Only trusted users can perform this operation.
393 self._simple("become", who)
395 def play(self, track):
399 track -- the path of the track to play.
401 self._simple("play", track)
403 def remove(self, track):
404 """Remove a track from the queue.
407 track -- the path or ID of the track to remove.
409 self._simple("remove", track)
412 """Enable playing."""
413 self._simple("enable")
415 def disable(self, *now):
419 now -- if present (with any value), the current track is stopped
423 self._simple("disable", "now")
425 self._simple("disable")
427 def scratch(self, *id):
428 """Scratch the currently playing track.
431 id -- if present, the ID of the track to scratch.
434 self._simple("scratch", id[0])
436 self._simple("scratch")
439 """Shut down the server.
441 Only trusted users can perform this operation.
443 self._simple("shutdown")
445 def reconfigure(self):
446 """Make the server reload its configuration.
448 Only trusted users can perform this operation.
450 self._simple("reconfigure")
452 def rescan(self, pattern):
453 """Rescan one or more collections.
456 pattern -- glob pattern matching collections to rescan.
458 Only trusted users can perform this operation.
460 self._simple("rescan", pattern)
463 """Return the server's version number."""
464 return self._simple("version")[1]
467 """Return the currently playing track.
469 If a track is playing then it is returned as a dictionary.
470 If no track is playing then None is returned."""
471 res, details = self._simple("playing")
474 return _queueEntry(details)
475 except _splitError, s:
476 raise protocolError(self.who, s.str())
480 def _somequeue(self, command):
481 self._simple(command)
483 return map(lambda s: _queueEntry(s), self._body())
484 except _splitError, s:
485 raise protocolError(self.who, s.str())
488 """Return a list of recently played tracks.
490 The return value is a list of dictionaries corresponding to
491 recently played tracks. The oldest track comes first."""
492 return self._somequeue("recent")
495 """Return the current queue.
497 The return value is a list of dictionaries corresponding to
498 recently played tracks. The next track to be played comes first."""
499 return self._somequeue("queue")
501 def _somedir(self, command, dir, re):
503 self._simple(command, dir, re[0])
505 self._simple(command, dir)
508 def directories(self, dir, *re):
509 """List subdirectories of a directory.
512 dir -- directory to list, or '' for the whole root.
513 re -- regexp that results must match. Optional.
515 The return value is a list of the (nonempty) subdirectories of dir.
516 If dir is '' then a list of top-level directories is returned.
518 If a regexp is specified then the basename of each result must
519 match. Matching is case-independent. See pcrepattern(3).
521 return self._somedir("dirs", dir, re)
523 def files(self, dir, *re):
524 """List files within a directory.
527 dir -- directory to list, or '' for the whole root.
528 re -- regexp that results must match. Optional.
530 The return value is a list of playable files in dir. If dir is ''
531 then a list of top-level files is returned.
533 If a regexp is specified then the basename of each result must
534 match. Matching is case-independent. See pcrepattern(3).
536 return self._somedir("files", dir, re)
538 def allfiles(self, dir, *re):
539 """List subdirectories and files within a directory.
542 dir -- directory to list, or '' for the whole root.
543 re -- regexp that results must match. Optional.
545 The return value is a list of all (nonempty) subdirectories and
546 files within dir. If dir is '' then a list of top-level files and
547 directories is returned.
549 If a regexp is specified then the basename of each result must
550 match. Matching is case-independent. See pcrepattern(3).
552 return self._somedir("allfiles", dir, re)
554 def set(self, track, key, value):
555 """Set a preference value.
558 track -- the track to modify
559 key -- the preference name
560 value -- the new preference value
562 self._simple("set", track, key, value)
564 def unset(self, track, key):
565 """Unset a preference value.
568 track -- the track to modify
569 key -- the preference to remove
571 self._simple("set", track, key, value)
573 def get(self, track, key):
574 """Get a preference value.
577 track -- the track to query
578 key -- the preference to remove
580 The return value is the preference
582 ret, details = self._simple("get", track, key)
585 def prefs(self, track):
586 """Get all the preferences for a track.
589 track -- the track to query
591 The return value is a dictionary of all the track's preferences.
592 Note that even nominally numeric values remain encoded as strings.
594 self._simple("prefs", track)
596 for line in self._body():
599 except _splitError, s:
600 raise protocolError(self.who, s.str())
602 raise protocolError(self.who, "invalid prefs body line")
606 def _boolean(self, s):
609 def exists(self, track):
610 """Return true if a track exists
613 track -- the track to check for"""
614 return self._boolean(self._simple("exists", track))
617 """Return true if playing is enabled"""
618 return self._boolean(self._simple("enabled"))
620 def random_enabled(self):
621 """Return true if random play is enabled"""
622 return self._boolean(self._simple("random-enabled"))
624 def random_enable(self):
625 """Enable random play."""
626 self._simple("random-enable")
628 def random_disable(self):
629 """Disable random play."""
630 self._simple("random-disable")
632 def length(self, track):
633 """Return the length of a track in seconds.
636 track -- the track to query.
638 ret, details = self._simple("length", track)
641 def search(self, words):
642 """Search for tracks.
645 words -- the set of words to search for.
647 The return value is a list of track path names, all of which contain
648 all of the required words (in their path name, trackname
651 self._simple("search", *words)
655 """Get server statistics.
657 The return value is list of statistics.
659 self._simple("stats")
663 """Get all preferences.
665 The return value is an encoded dump of the preferences database.
670 def set_volume(self, left, right):
674 left -- volume for the left speaker.
675 right -- volume for the right speaker.
677 self._simple("volume", left, right)
679 def get_volume(self):
682 The return value a tuple consisting of the left and right volumes.
684 ret, details = self._simple("volume")
685 return map(int,string.split(details))
687 def move(self, track, delta):
688 """Move a track in the queue.
691 track -- the name or ID of the track to move
692 delta -- the number of steps towards the head of the queue to move
694 ret, details = self._simple("move", track, str(delta))
697 def log(self, callback):
698 """Read event log entries as they happen.
700 Each event log entry is handled by passing it to callback.
702 The callback takes two arguments, the first is the client and the
703 second the line from the event log.
705 The callback should return True to continue or False to stop (don't
706 forget this, or your program will mysteriously misbehave).
708 It is suggested that you use the disorder.monitor class instead of
709 calling this method directly, but this is not mandatory.
711 See disorder_protocol(5) for the event log syntax.
714 callback -- function to call with log entry
716 ret, details = self._simple("log")
719 self._debug(client.debug_body, "<<< %s" % l)
720 if l != '' and l[0] == '.':
724 if not callback(self, l):
726 # tell the server to stop sending, eat the remains of the body,
728 self._send("version")
733 """Pause the current track."""
734 self._simple("pause")
737 """Resume after a pause."""
738 self._simple("resume")
740 def part(self, track, context, part):
741 """Get a track name part
744 track -- the track to query
745 context -- the context ('sort' or 'display')
746 part -- the desired part (usually 'artist', 'album' or 'title')
748 The return value is the preference
750 ret, details = self._simple("part", track, context, part)
753 ########################################################################
757 # read one response line and return as some suitable string object
759 # If an I/O error occurs, disconnect from the server.
761 # XXX does readline() DTRT regarding character encodings?
763 l = self.r.readline()
764 if not re.search("\n", l):
765 raise communicationError(self.who, "peer disconnected")
770 return unicode(l, "UTF-8")
773 # read a response as a (code, details) tuple
775 self._debug(client.debug_proto, "<== %s" % l)
776 m = _response.match(l)
778 return int(m.group(1)), m.group(2)
780 raise protocolError(self.who, "invalid response %s")
782 def _send(self, *command):
783 quoted = _quote(command)
784 self._debug(client.debug_proto, "==> %s" % quoted)
785 encoded = quoted.encode("UTF-8")
787 self.w.write(encoded)
793 raise communicationError(self.who, e)
798 def _simple(self, *command):
799 # Issue a simple command, throw an exception on error
801 # If an I/O error occurs, disconnect from the server.
803 # On success returns response as a (code, details) tuple
805 # On error raise operationError
806 if self.state == 'disconnected':
810 res, details = self._response()
813 raise operationError(res, details)
816 # Fetch a dot-stuffed body
820 self._debug(client.debug_body, "<<< %s" % l)
821 if l != '' and l[0] == '.':
827 ########################################################################
828 # Configuration file parsing
830 def _readfile(self, path):
831 # Read a configuration file
835 # path -- path of file to read
837 # handlers for various commands
838 def _collection(self, command, args):
840 return "'%s' takes three args" % command
841 self.config["collections"].append(args)
843 def _unary(self, command, args):
845 return "'%s' takes only one arg" % command
846 self.config[command] = args[0]
848 def _include(self, command, args):
850 return "'%s' takes only one arg" % command
851 self._readfile(args[0])
853 def _any(self, command, args):
854 self.config[command] = args
856 # mapping of options to handlers
857 _options = { "collection": _collection,
862 "include": _include }
865 for lno, line in enumerate(file(path, "r")):
867 fields = _split(line, 'comments')
868 except _splitError, s:
869 raise parseError(path, lno + 1, str(s))
872 # we just ignore options we don't know about, so as to cope gracefully
873 # with version skew (and nothing to do with implementor laziness)
874 if command in _options:
875 e = _options[command](self, command, fields[1:])
877 self._parseError(path, lno + 1, e)
879 def _parseError(self, path, lno, s):
880 raise parseError(path, lno, s)
882 ########################################################################
886 """DisOrder event log monitor class
888 Intended to be subclassed with methods corresponding to event log messages
889 the implementor cares about over-ridden."""
891 def __init__(self, c=None):
892 """Constructor for the monitor class
894 Can be passed a client to use. If none is specified then one
895 will be created specially for the purpose.
904 """Start monitoring logs. Continues monitoring until one of the
905 message-specific methods returns False. Can be called more than once
906 (but not recursively!)"""
907 self.c.log(self._callback)
910 """Return the timestamp of the current (or most recent) event log entry"""
911 return self.timestamp
913 def _callback(self, c, line):
917 return self.invalid(line)
919 return self.invalid(line)
920 self.timestamp = int(bits[0], 16)
923 if keyword == 'completed':
925 return self.completed(bits[0])
926 elif keyword == 'failed':
928 return self.failed(bits[0], bits[1])
929 elif keyword == 'moved':
934 return self.invalid(line)
935 return self.moved(bits[0], n, bits[2])
936 elif keyword == 'playing':
938 return self.playing(bits[0], None)
940 return self.playing(bits[0], bits[1])
941 elif keyword == 'queue' or keyword == 'recent-added':
945 return self.invalid(line)
946 if keyword == 'queue':
948 if keyword == 'recent-added':
949 return self.recent_added(q)
950 elif keyword == 'recent-removed':
952 return self.recent_removed(bits[0])
953 elif keyword == 'removed':
955 return self.removed(bits[0], None)
957 return self.removed(bits[0], bits[1])
958 elif keyword == 'scratched':
960 return self.scratched(bits[0], bits[1])
961 return self.invalid(line)
963 def completed(self, track):
964 """Called when a track completes.
967 track -- track that completed"""
970 def failed(self, track, error):
971 """Called when a player suffers an error.
974 track -- track that failed
975 error -- error indicator"""
978 def moved(self, id, offset, user):
979 """Called when a track is moved in the queue.
983 offset -- distance moved
984 user -- user responsible"""
987 def playing(self, track, user):
988 """Called when a track starts playing.
991 track -- track that has started
992 user -- user that submitted track, or None"""
996 """Called when a track is added to the queue.
999 q -- dictionary of new queue entry"""
1002 def recent_added(self, q):
1003 """Called when a track is added to the recently played list
1006 q -- dictionary of new queue entry"""
1009 def recent_removed(self, id):
1010 """Called when a track is removed from the recently played list
1013 id -- ID of removed entry (always the oldest)"""
1016 def removed(self, id, user):
1017 """Called when a track is removed from the queue, either manually
1018 or in order to play it.
1021 id -- ID of removed entry
1022 user -- user responsible (or None if we're playing this track)"""
1025 def scratched(self, track, user):
1026 """Called when a track is scratched
1029 track -- track that was scratched
1030 user -- user responsible"""
1033 def invalid(self, line):
1034 """Called when an event log line cannot be interpreted
1037 line -- line that could not be understood"""
1042 # py-indent-offset:2