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