chiark / gitweb /
Import from Arch revision:
[disorder] / python / disorder.py.in
1 #
2 # Copyright (C) 2004, 2005 Richard Kettlewell
3 #
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.
8 #
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.
13 #
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
17 # USA
18 #
19
20 """Python support for DisOrder
21
22 Provides disorder.client, a class for accessing a DisOrder server.
23
24 Example 1:
25
26   #! /usr/bin/env python
27   import disorder
28   d = disorder.client()
29   p = d.playing()
30   if p:
31     print p['track']
32
33 Example 2:
34
35   #! /usr/bin/env python
36   import disorder
37   import sys
38   d = disorder.client()
39   for path in sys.argv[1:]:
40     d.play(path)
41
42 """
43
44 import re
45 import string
46 import os
47 import pwd
48 import socket
49 import binascii
50 import sha
51 import sys
52 import locale
53
54 _configfile = "pkgconfdir/config"
55 _dbhome = "pkgstatedir"
56
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]*")
62
63 _response = re.compile("([0-9]{3}) ?(.*)")
64
65 version = "_version_"
66
67 ########################################################################
68 # exception classes
69
70 class Error(Exception):
71   """Base class for DisOrder exceptions."""
72
73 class _splitError(Error):
74   # _split failed
75   def __init__(self, value):
76     self.value = value
77   def __str__(self):
78     return str(self.value)
79
80 class parseError(Error):
81   """Error parsing the configuration file."""
82   def __init__(self, path, line, details):
83     self.path = path
84     self.line = line
85     self.details = details
86   def __str__(self):
87     return "%s:%d: %s" % (self.path, self.line, self.details)
88
89 class protocolError(Error):
90   """DisOrder control protocol error.
91
92   Indicates a mismatch between the client and server's understanding of
93   the control protocol.
94   """
95   def __init__(self, who, error):
96     self.who = who
97     self.error = error
98   def __str__(self):
99     return "%s: %s" % (self.who, str(self.error))
100
101 class operationError(Error):
102   """DisOrder control protocol error response.
103
104   Indicates that an operation failed (e.g. an attempt to play a
105   nonexistent track).  The connection should still be usable.
106   """
107   def __init__(self, res, details):
108     self.res_ = int(res)
109     self.details_ = details
110   def __str__(self):
111     """Return the complete response string from the server.
112
113     Excludes the final newline.
114     """
115     return "%d %s" % (self.res_, self.details_)
116   def response(self):
117     """Return the response code from the server."""
118     return self.res_
119   def details(self):
120     """Returns the detail string from the server."""
121     return self.details_
122
123 class communicationError(Error):
124   """DisOrder control protocol communication error.
125
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
129   retry the operation.
130   """
131   def __init__(self, who, error):
132     self.who = who
133     self.error = error
134   def __str__(self):
135     return "%s: %s" % (self.who, str(self.error))
136
137 ########################################################################
138 # DisOrder-specific text processing
139
140 def _unescape(s):
141   # Unescape the contents of a string
142   #
143   # Arguments:
144   #
145   # s -- string to unescape
146   #
147   s = re.sub("\\\\n", "\n", s)
148   s = re.sub("\\\\(.)", "\\1", s)
149   return s
150
151 def _split(s, *comments):
152   # Split a string into fields according to the usual Disorder string splitting
153   # conventions.
154   #
155   # Arguments:
156   #
157   # s        -- string to parse
158   # comments -- if present, parse comments
159   #
160   # Return values:
161   #
162   # On success, a list of fields is returned.
163   #
164   # On error, disorder.parseError is thrown.
165   #
166   fields = []
167   while s != "":
168     # discard comments
169     if comments and s[0] == '#':
170       break
171     # strip spaces
172     m = _ws.match(s)
173     if m:
174       s = s[m.end():]
175       continue
176     # pick of quoted fields of both kinds
177     m = _squote.match(s)
178     if not m:
179       m = _dquote.match(s)
180     if m:
181       fields.append(_unescape(m.group(1)))
182       s = s[m.end():]
183       continue
184     # and unquoted fields
185     m = _unquoted.match(s)
186     if m:
187       fields.append(m.group(0))
188       s = s[m.end():]
189       continue
190     # anything left must be in error
191     if s[0] == '"' or s[0] == '\'':
192       raise _splitError("invalid quoted string")
193     else:
194       raise _splitError("syntax error")
195   return fields
196
197 def _escape(s):
198   # Escape the contents of a string
199   #
200   # Arguments:
201   #
202   # s -- string to escape
203   #
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)
207     return '"' + s + '"'
208   else:
209     return s
210
211 def _quote(list):
212   # Quote a list of values
213   return ' '.join(map(_escape, list))
214
215 def _sanitize(s):
216   # Return the value of s in a form suitable for writing to stderr
217   return s.encode(locale.nl_langinfo(locale.CODESET), 'replace')
218
219 def _list2dict(l):
220   # Convert a list of the form [k1, v1, k2, v2, ..., kN, vN]
221   # to a dictionary {k1:v1, k2:v2, ..., kN:vN}
222   d = {}
223   i = iter(l)
224   try:
225     while True:
226       k = i.next()
227       v = i.next()
228       d[k] = v
229   except StopIteration:
230     pass
231   return d
232
233 def _queueEntry(s):
234   # parse a queue entry
235   return _list2dict(_split(s))
236
237 ########################################################################
238 # The client class
239
240 class client:
241   """DisOrder client class.
242
243   This class provides access to the DisOrder server either on this
244   machine or across the internet.
245
246   The server to connect to, and the username and password to use, are
247   determined from the configuration files as described in 'man
248   disorder_config'.
249
250   All methods will connect if necessary, as soon as you have a
251   disorder.client object you can start calling operational methods on
252   it.
253
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.
256
257   All methods block until they complete.
258
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
262   operation failed.
263   """
264
265   debug_proto = 0x0001
266   debug_body = 0x0002
267
268   def __init__(self):
269     """Constructor for DisOrder client class.
270
271     The constructor reads the configuration file, but does not connect
272     to the server.
273
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.
277
278     The constructor Raises parseError() if the configuration file is not
279     valid.
280     """
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,
285                     'home': _dbhome }
286     home = os.getenv("HOME")
287     if not home:
288       home = pw.pw_dir
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'
297
298   def debug(self, bits):
299     """Enable or disable protocol debugging.  Debug messages are written
300     to sys.stderr.
301
302     Arguments:
303     bits -- bitmap of operations that should generate debug information
304
305     Bitmap values:
306     debug_proto -- dump control protocol messages (excluding bodies)
307     debug_body -- dump control protocol message bodies
308     """
309     self.debugging = bits
310
311   def _debug(self, bit, s):
312     # debug output
313     if self.debugging & bit:
314       sys.stderr.write(_sanitize(s))
315       sys.stderr.write("\n")
316       sys.stderr.flush()
317
318   def connect(self):
319     """Connect to the DisOrder server and authenticate.
320
321     Raises communicationError if connection fails and operationError if
322     authentication fails (in which case disconnection is automatic).
323
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,
326     this is a no-op.
327
328     Other operations automatically connect if we're not already
329     connected, so it is not strictly necessary to call this method.
330     """
331     if self.state == 'disconnected':
332       try:
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
337           if len(c) == 1:
338             a = socket.getaddrinfo(None, c[0],
339                                    socket.AF_INET,
340                                    socket.SOCK_STREAM,
341                                    0,
342                                    0)
343           else:
344             a = socket.getaddrinfo(c[0], c[1],
345                                    socket.AF_INET,
346                                    socket.SOCK_STREAM,
347                                    0,
348                                    0)
349           a = a[0]
350           s = socket.socket(a[0], a[1], a[2]);
351           s.connect(a[4])
352           self.who = "%s" % a[3]
353         else:
354           s = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM);
355           self.who = self.config['home'] + os.sep + "socket"
356           s.connect(self.who)
357         self.w = s.makefile("wb")
358         self.r = s.makefile("rb")
359         (res, challenge) = self._simple()
360         h = sha.sha()
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:
366         self._disconnect()
367         raise communicationError(self.who, e)
368       except:
369         self._disconnect()
370         raise
371
372   def _disconnect(self):
373     # disconnect from the server, whatever state we are in
374     try:
375       del self.w
376       del self.r
377     except:
378       pass
379     self.state = 'disconnected'
380     
381   ########################################################################
382   # Operations
383
384   def become(self, who):
385     """Become another user.
386
387     Arguments:
388     who -- the user to become.
389
390     Only trusted users can perform this operation.
391     """
392     self._simple("become", who)
393
394   def play(self, track):
395     """Play a track.
396
397     Arguments:
398     track -- the path of the track to play.
399     """
400     self._simple("play", track)
401
402   def remove(self, track):
403     """Remove a track from the queue.
404
405     Arguments:
406     track -- the path or ID of the track to remove.
407     """
408     self._simple("remove", track)
409
410   def enable(self):
411     """Enable playing."""
412     self._simple("enable")
413
414   def disable(self, *now):
415     """Disable playing.
416
417     Arguments:
418     now -- if present (with any value), the current track is stopped
419            too.
420     """
421     if now:
422       self._simple("disable", "now")
423     else:
424       self._simple("disable")
425
426   def scratch(self, *id):
427     """Scratch the currently playing track.
428
429     Arguments:
430     id -- if present, the ID of the track to scratch.
431     """
432     if id:
433       self._simple("scratch", id[0])
434     else:
435       self._simple("scratch")
436
437   def shutdown(self):
438     """Shut down the server.
439
440     Only trusted users can perform this operation.
441     """
442     self._simple("shutdown")
443
444   def reconfigure(self):
445     """Make the server reload its configuration.
446
447     Only trusted users can perform this operation.
448     """
449     self._simple("reconfigure")
450
451   def rescan(self, pattern):
452     """Rescan one or more collections.
453
454     Arguments:
455     pattern -- glob pattern matching collections to rescan.
456
457     Only trusted users can perform this operation.
458     """
459     self._simple("rescan", pattern)
460
461   def version(self):
462     """Return the server's version number."""
463     return self._simple("version")[1]
464
465   def playing(self):
466     """Return the currently playing track.
467
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")
471     if res % 10 != 9:
472       try:
473         return _queueEntry(details)
474       except _splitError, s:
475         raise protocolError(self.who, s.str())
476     else:
477       return None
478
479   def _somequeue(self, command):
480     self._simple(command)
481     try:
482       return map(lambda s: _queueEntry(s), self._body())
483     except _splitError, s:
484       raise protocolError(self.who, s.str())
485
486   def recent(self):
487     """Return a list of recently played tracks.
488
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")
492
493   def queue(self):
494     """Return the current queue.
495
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")
499
500   def _somedir(self, command, dir, re):
501     if re:
502       self._simple(command, dir, re[0])
503     else:
504       self._simple(command, dir)
505     return self._body()
506
507   def directories(self, dir, *re):
508     """List subdirectories of a directory.
509
510     Arguments:
511     dir -- directory to list, or '' for the whole root.
512     re -- regexp that results must match.  Optional.
513
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.
516
517     If a regexp is specified then the basename of each result must
518     match.  Matching is case-independent.  See pcrepattern(3).
519     """
520     return self._somedir("dirs", dir, re)
521   
522   def files(self, dir, *re):
523     """List files within a directory.
524
525     Arguments:
526     dir -- directory to list, or '' for the whole root.
527     re -- regexp that results must match.  Optional.
528
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.
531
532     If a regexp is specified then the basename of each result must
533     match.  Matching is case-independent.  See pcrepattern(3).
534     """
535     return self._somedir("files", dir, re)
536
537   def allfiles(self, dir, *re):
538     """List subdirectories and files within a directory.
539
540     Arguments:
541     dir -- directory to list, or '' for the whole root.
542     re -- regexp that results must match.  Optional.
543
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.
547     
548     If a regexp is specified then the basename of each result must
549     match.  Matching is case-independent.  See pcrepattern(3).
550     """
551     return self._somedir("allfiles", dir, re)
552
553   def set(self, track, key, value):
554     """Set a preference value.
555
556     Arguments:
557     track -- the track to modify
558     key -- the preference name
559     value -- the new preference value
560     """
561     self._simple("set", track, key, value)
562
563   def unset(self, track, key):
564     """Unset a preference value.
565
566     Arguments:
567     track -- the track to modify
568     key -- the preference to remove
569     """
570     self._simple("set", track, key, value)
571
572   def get(self, track, key):
573     """Get a preference value.
574
575     Arguments:
576     track -- the track to query
577     key -- the preference to remove
578
579     The return value is the preference 
580     """
581     ret, details = self._simple("get", track, key)
582     return details
583
584   def prefs(self, track):
585     """Get all the preferences for a track.
586
587     Arguments:
588     track -- the track to query
589
590     The return value is a dictionary of all the track's preferences.
591     Note that even nominally numeric values remain encoded as strings.
592     """
593     self._simple("prefs", track)
594     r = {}
595     for line in self._body():
596       try:
597         kv = _split(line)
598       except _splitError, s:
599         raise protocolError(self.who, s.str())
600       if len(kv) != 2:
601         raise protocolError(self.who, "invalid prefs body line")
602       r[kv[0]] = kv[1]
603     return r
604
605   def _boolean(self, s):
606     return s[1] == 'yes'
607
608   def exists(self, track):
609     """Return true if a track exists
610
611     Arguments:
612     track -- the track to check for"""
613     return self._boolean(self._simple("exists", track))
614
615   def enabled(self):
616     """Return true if playing is enabled"""
617     return self._boolean(self._simple("enabled"))
618
619   def random_enabled(self):
620     """Return true if random play is enabled"""
621     return self._boolean(self._simple("random-enabled"))
622
623   def random_enable(self):
624     """Enable random play."""
625     self._simple("random-enable")
626
627   def random_disable(self):
628     """Disable random play."""
629     self._simple("random-disable")
630
631   def length(self, track):
632     """Return the length of a track in seconds.
633
634     Arguments:
635     track -- the track to query.
636     """
637     ret, details = self._simple("length", track)
638     return int(details)
639
640   def search(self, words):
641     """Search for tracks.
642
643     Arguments:
644     words -- the set of words to search for.
645
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
648     preferences, etc.)
649     """
650     self._simple("search", *words)
651     return self._body()
652
653   def stats(self):
654     """Get server statistics.
655
656     The return value is list of statistics.
657     """
658     self._simple("stats")
659     return self._body()
660
661   def dump(self):
662     """Get all preferences.
663
664     The return value is an encoded dump of the preferences database.
665     """
666     self._simple("dump")
667     return self._body()
668
669   def set_volume(self, left, right):
670     """Set volume.
671
672     Arguments:
673     left -- volume for the left speaker.
674     right --  volume for the right speaker.
675     """
676     self._simple("volume", left, right)
677
678   def get_volume(self):
679     """Get volume.
680
681     The return value a tuple consisting of the left and right volumes.
682     """
683     ret, details = self._simple("volume")
684     return map(int,string.split(details))
685
686   def move(self, track, delta):
687     """Move a track in the queue.
688
689     Arguments:
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
692     """
693     ret, details = self._simple("move", track, str(delta))
694     return int(details)
695
696   def log(self, callback):
697     """Read event log entries as they happen.
698
699     Each event log entry is handled by passing it to callback.
700
701     The callback takes two arguments, the first is the client and the
702     second the line from the event log.
703     
704     The callback should return True to continue or False to stop (don't
705     forget this, or your program will mysteriously misbehave).
706
707     It is suggested that you use the disorder.monitor class instead of
708     calling this method directly, but this is not mandatory.
709
710     See disorder_protocol(5) for the event log syntax.
711
712     Arguments:
713     callback -- function to call with log entry
714     """
715     ret, details = self._simple("log")
716     while True:
717       l = self._line()
718       self._debug(client.debug_body, "<<< %s" % l)
719       if l != '' and l[0] == '.':
720         if l == '.':
721           return
722         l = l[1:]
723       if not callback(self, l):
724         break
725     # tell the server to stop sending, eat the remains of the body,
726     # eat the response
727     self._send("version")
728     self._body()
729     self._response()
730
731   def pause(self):
732     """Pause the current track."""
733     self._simple("pause")
734
735   def resume(self):
736     """Resume after a pause."""
737     self._simple("resume")
738
739   def part(self, track, context, part):
740     """Get a track name part
741
742     Arguments:
743     track -- the track to query
744     context -- the context ('sort' or 'display')
745     part -- the desired part (usually 'artist', 'album' or 'title')
746
747     The return value is the preference 
748     """
749     ret, details = self._simple("part", track, context, part)
750     return details
751
752   ########################################################################
753   # I/O infrastructure
754
755   def _line(self):
756     # read one response line and return as some suitable string object
757     #
758     # If an I/O error occurs, disconnect from the server.
759     #
760     # XXX does readline() DTRT regarding character encodings?
761     try:
762       l = self.r.readline()
763       if not re.search("\n", l):
764         raise communicationError(self.who, "peer disconnected")
765       l = l[:-1]
766     except:
767       self._disconnect()
768       raise
769     return unicode(l, "UTF-8")
770
771   def _response(self):
772     # read a response as a (code, details) tuple
773     l = self._line()
774     self._debug(client.debug_proto, "<== %s" % l)
775     m = _response.match(l)
776     if m:
777       return int(m.group(1)), m.group(2)
778     else:
779       raise protocolError(self.who, "invalid response %s")
780
781   def _send(self, *command):
782     quoted = _quote(command)
783     self._debug(client.debug_proto, "==> %s" % quoted)
784     encoded = quoted.encode("UTF-8")
785     try:
786       self.w.write(encoded)
787       self.w.write("\n")
788       self.w.flush()
789     except IOError, e:
790       # e.g. EPIPE
791       self._disconnect()
792       raise communicationError(self.who, e)
793     except:
794       self._disconnect()
795       raise
796
797   def _simple(self, *command):
798     # Issue a simple command, throw an exception on error
799     #
800     # If an I/O error occurs, disconnect from the server.
801     #
802     # On success returns response as a (code, details) tuple
803     #
804     # On error raise operationError
805     if self.state == 'disconnected':
806       self.connect()
807     if command:
808       self._send(*command)
809     res, details = self._response()
810     if res / 100 == 2:
811       return res, details
812     raise operationError(res, details)
813
814   def _body(self):
815     # Fetch a dot-stuffed body
816     result = []
817     while True:
818       l = self._line()
819       self._debug(client.debug_body, "<<< %s" % l)
820       if l != '' and l[0] == '.':
821         if l == '.':
822           return result
823         l = l[1:]
824       result.append(l)
825
826   ########################################################################
827   # Configuration file parsing
828
829   def _readfile(self, path):
830     # Read a configuration file
831     #
832     # Arguments:
833     #
834     # path -- path of file to read
835
836     # handlers for various commands
837     def _collection(self, command, args):
838       if len(args) != 3:
839         return "'%s' takes three args" % command
840       self.config["collections"].append(args)
841       
842     def _unary(self, command, args):
843       if len(args) != 1:
844         return "'%s' takes only one arg" % command
845       self.config[command] = args[0]
846
847     def _include(self, command, args):
848       if len(args) != 1:
849         return "'%s' takes only one arg" % command
850       self._readfile(args[0])
851
852     def _any(self, command, args):
853       self.config[command] = args
854
855     # mapping of options to handlers
856     _options = { "collection": _collection,
857                  "username": _unary,
858                  "password": _unary,
859                  "home": _unary,
860                  "connect": _any,
861                  "include": _include }
862
863     # the parser
864     for lno, line in enumerate(file(path, "r")):
865       try:
866         fields = _split(line, 'comments')
867       except _splitError, s:
868         raise parseError(path, lno + 1, str(s))
869       if fields:
870         command = fields[0]
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:])
875           if e:
876             self._parseError(path, lno + 1, e)
877
878   def _parseError(self, path, lno, s):
879     raise parseError(path, lno, s)
880
881 ########################################################################
882 # monitor class
883
884 class monitor:
885   """DisOrder event log monitor class
886
887   Intended to be subclassed with methods corresponding to event log messages
888   the implementor cares about over-ridden."""
889
890   def __init__(self, c=None):
891     """Constructor for the monitor class
892
893     Can be passed a client to use.  If none is specified then one
894     will be created specially for the purpose.
895
896     Arguments:
897     c -- client"""
898     if c == None:
899       c = client();
900     self.c = c
901
902   def run(self):
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)
907
908   def when(self):
909     """Return the timestamp of the current (or most recent) event log entry"""
910     return self.timestamp
911
912   def _callback(self, c, line):
913     try:
914       bits = _split(line)
915     except:
916       return self.invalid(line)
917     if(len(bits) < 2):
918       return self.invalid(line)
919     self.timestamp = int(bits[0], 16)
920     keyword = bits[1]
921     bits = bits[2:]
922     if keyword == 'completed':
923       if len(bits) == 1:
924         return self.completed(bits[0])
925     elif keyword == 'failed':
926       if len(bits) == 2:
927         return self.failed(bits[0], bits[1])
928     elif keyword == 'moved':
929       if len(bits) == 3:
930         try:
931           n = int(bits[1])
932         except:
933           return self.invalid(line)
934         return self.moved(bits[0], n, bits[2])
935     elif keyword == 'playing':
936       if len(bits) == 1:
937         return self.playing(bits[0], None)
938       elif len(bits) == 2:
939         return self.playing(bits[0], bits[1])
940     elif keyword == 'queue' or keyword == 'recent-added':
941       try:
942         q = _list2dict(bits)
943       except:
944         return self.invalid(line)
945       if keyword == 'queue':
946         return self.queue(q)
947       if keyword == 'recent-added':
948         return self.recent_added(q)
949     elif keyword == 'recent-removed':
950       if len(bits) == 1:
951         return self.recent_removed(bits[0])
952     elif keyword == 'removed':
953       if len(bits) == 1:
954         return self.removed(bits[0], None)
955       elif len(bits) == 2:
956         return self.removed(bits[0], bits[1])
957     elif keyword == 'scratched':
958       if len(bits) == 2:
959         return self.scratched(bits[0], bits[1])
960     return self.invalid(line)
961
962   def completed(self, track):
963     """Called when a track completes.
964
965     Arguments:
966     track -- track that completed"""
967     return True
968
969   def failed(self, track, error):
970     """Called when a player suffers an error.
971
972     Arguments:
973     track -- track that failed
974     error -- error indicator"""
975     return True
976
977   def moved(self, id, offset, user):
978     """Called when a track is moved in the queue.
979
980     Arguments:
981     id -- queue entry ID
982     offset -- distance moved
983     user -- user responsible"""
984     return True
985
986   def playing(self, track, user):
987     """Called when a track starts playing.
988
989     Arguments:
990     track -- track that has started
991     user -- user that submitted track, or None"""
992     return True
993
994   def queue(self, q):
995     """Called when a track is added to the queue.
996
997     Arguments:
998     q -- dictionary of new queue entry"""
999     return True
1000
1001   def recent_added(self, q):
1002     """Called when a track is added to the recently played list
1003
1004     Arguments:
1005     q -- dictionary of new queue entry"""
1006     return True
1007
1008   def recent_removed(self, id):
1009     """Called when a track is removed from the recently played list
1010
1011     Arguments:
1012     id -- ID of removed entry (always the oldest)"""
1013     return True
1014
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.
1018
1019     Arguments:
1020     id -- ID of removed entry
1021     user -- user responsible (or None if we're playing this track)"""
1022     return True
1023
1024   def scratched(self, track, user):
1025     """Called when a track is scratched
1026
1027     Arguments:
1028     track -- track that was scratched
1029     user -- user responsible"""
1030     return True
1031
1032   def invalid(self, line):
1033     """Called when an event log line cannot be interpreted
1034
1035     Arguments:
1036     line -- line that could not be understood"""
1037     return True
1038
1039 # Local Variables:
1040 # mode:python
1041 # py-indent-offset:2
1042 # comment-column:40
1043 # fill-column:72
1044 # End:
1045 # arch-tag:eea975737c837febf73a000630b5ecc4