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