chiark / gitweb /
fixes and test for disorder-dump
[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   def setglobal(self, key, value):
759     """Set a global preference value.
760
761     Arguments:
762     key -- the preference name
763     value -- the new preference value
764     """
765     self._simple("set-global", key, value)
766
767   def unsetglobal(self, key):
768     """Unset a global preference value.
769
770     Arguments:
771     key -- the preference to remove
772     """
773     self._simple("set-global", key, value)
774
775   def getglobal(self, key):
776     """Get a global preference value.
777
778     Arguments:
779     key -- the preference to look up
780
781     The return value is the preference 
782     """
783     ret, details = self._simple("get-global", key)
784     return details
785
786   ########################################################################
787   # I/O infrastructure
788
789   def _line(self):
790     # read one response line and return as some suitable string object
791     #
792     # If an I/O error occurs, disconnect from the server.
793     #
794     # XXX does readline() DTRT regarding character encodings?
795     try:
796       l = self.r.readline()
797       if not re.search("\n", l):
798         raise communicationError(self.who, "peer disconnected")
799       l = l[:-1]
800     except:
801       self._disconnect()
802       raise
803     return unicode(l, "UTF-8")
804
805   def _response(self):
806     # read a response as a (code, details) tuple
807     l = self._line()
808     self._debug(client.debug_proto, "<== %s" % l)
809     m = _response.match(l)
810     if m:
811       return int(m.group(1)), m.group(2)
812     else:
813       raise protocolError(self.who, "invalid response %s")
814
815   def _send(self, *command):
816     # Quote and send a command
817     #
818     # Returns the encoded command.
819     quoted = _quote(command)
820     self._debug(client.debug_proto, "==> %s" % quoted)
821     encoded = quoted.encode("UTF-8")
822     try:
823       self.w.write(encoded)
824       self.w.write("\n")
825       self.w.flush()
826       return encoded
827     except IOError, e:
828       # e.g. EPIPE
829       self._disconnect()
830       raise communicationError(self.who, e)
831     except:
832       self._disconnect()
833       raise
834
835   def _simple(self, *command):
836     # Issue a simple command, throw an exception on error
837     #
838     # If an I/O error occurs, disconnect from the server.
839     #
840     # On success returns response as a (code, details) tuple
841     #
842     # On error raise operationError
843     if self.state == 'disconnected':
844       self.connect()
845     if command:
846       cmd = self._send(*command)
847     else:
848       cmd = None
849     res, details = self._response()
850     if res / 100 == 2:
851       return res, details
852     raise operationError(res, details, cmd)
853
854   def _body(self):
855     # Fetch a dot-stuffed body
856     result = []
857     while True:
858       l = self._line()
859       self._debug(client.debug_body, "<<< %s" % l)
860       if l != '' and l[0] == '.':
861         if l == '.':
862           return result
863         l = l[1:]
864       result.append(l)
865
866   ########################################################################
867   # Configuration file parsing
868
869   def _readfile(self, path):
870     # Read a configuration file
871     #
872     # Arguments:
873     #
874     # path -- path of file to read
875
876     # handlers for various commands
877     def _collection(self, command, args):
878       if len(args) != 3:
879         return "'%s' takes three args" % command
880       self.config["collections"].append(args)
881       
882     def _unary(self, command, args):
883       if len(args) != 1:
884         return "'%s' takes only one arg" % command
885       self.config[command] = args[0]
886
887     def _include(self, command, args):
888       if len(args) != 1:
889         return "'%s' takes only one arg" % command
890       self._readfile(args[0])
891
892     def _any(self, command, args):
893       self.config[command] = args
894
895     # mapping of options to handlers
896     _options = { "collection": _collection,
897                  "username": _unary,
898                  "password": _unary,
899                  "home": _unary,
900                  "connect": _any,
901                  "include": _include }
902
903     # the parser
904     for lno, line in enumerate(file(path, "r")):
905       try:
906         fields = _split(line, 'comments')
907       except _splitError, s:
908         raise parseError(path, lno + 1, str(s))
909       if fields:
910         command = fields[0]
911         # we just ignore options we don't know about, so as to cope gracefully
912         # with version skew (and nothing to do with implementor laziness)
913         if command in _options:
914           e = _options[command](self, command, fields[1:])
915           if e:
916             self._parseError(path, lno + 1, e)
917
918   def _parseError(self, path, lno, s):
919     raise parseError(path, lno, s)
920
921 ########################################################################
922 # monitor class
923
924 class monitor:
925   """DisOrder event log monitor class
926
927   Intended to be subclassed with methods corresponding to event log messages
928   the implementor cares about over-ridden."""
929
930   def __init__(self, c=None):
931     """Constructor for the monitor class
932
933     Can be passed a client to use.  If none is specified then one
934     will be created specially for the purpose.
935
936     Arguments:
937     c -- client"""
938     if c == None:
939       c = client();
940     self.c = c
941
942   def run(self):
943     """Start monitoring logs.  Continues monitoring until one of the
944     message-specific methods returns False.  Can be called more than once
945     (but not recursively!)"""
946     self.c.log(self._callback)
947
948   def when(self):
949     """Return the timestamp of the current (or most recent) event log entry"""
950     return self.timestamp
951
952   def _callback(self, c, line):
953     try:
954       bits = _split(line)
955     except:
956       return self.invalid(line)
957     if(len(bits) < 2):
958       return self.invalid(line)
959     self.timestamp = int(bits[0], 16)
960     keyword = bits[1]
961     bits = bits[2:]
962     if keyword == 'completed':
963       if len(bits) == 1:
964         return self.completed(bits[0])
965     elif keyword == 'failed':
966       if len(bits) == 2:
967         return self.failed(bits[0], bits[1])
968     elif keyword == 'moved':
969       if len(bits) == 3:
970         try:
971           n = int(bits[1])
972         except:
973           return self.invalid(line)
974         return self.moved(bits[0], n, bits[2])
975     elif keyword == 'playing':
976       if len(bits) == 1:
977         return self.playing(bits[0], None)
978       elif len(bits) == 2:
979         return self.playing(bits[0], bits[1])
980     elif keyword == 'queue' or keyword == 'recent-added':
981       try:
982         q = _list2dict(bits)
983       except:
984         return self.invalid(line)
985       if keyword == 'queue':
986         return self.queue(q)
987       if keyword == 'recent-added':
988         return self.recent_added(q)
989     elif keyword == 'recent-removed':
990       if len(bits) == 1:
991         return self.recent_removed(bits[0])
992     elif keyword == 'removed':
993       if len(bits) == 1:
994         return self.removed(bits[0], None)
995       elif len(bits) == 2:
996         return self.removed(bits[0], bits[1])
997     elif keyword == 'scratched':
998       if len(bits) == 2:
999         return self.scratched(bits[0], bits[1])
1000     return self.invalid(line)
1001
1002   def completed(self, track):
1003     """Called when a track completes.
1004
1005     Arguments:
1006     track -- track that completed"""
1007     return True
1008
1009   def failed(self, track, error):
1010     """Called when a player suffers an error.
1011
1012     Arguments:
1013     track -- track that failed
1014     error -- error indicator"""
1015     return True
1016
1017   def moved(self, id, offset, user):
1018     """Called when a track is moved in the queue.
1019
1020     Arguments:
1021     id -- queue entry ID
1022     offset -- distance moved
1023     user -- user responsible"""
1024     return True
1025
1026   def playing(self, track, user):
1027     """Called when a track starts playing.
1028
1029     Arguments:
1030     track -- track that has started
1031     user -- user that submitted track, or None"""
1032     return True
1033
1034   def queue(self, q):
1035     """Called when a track is added to the queue.
1036
1037     Arguments:
1038     q -- dictionary of new queue entry"""
1039     return True
1040
1041   def recent_added(self, q):
1042     """Called when a track is added to the recently played list
1043
1044     Arguments:
1045     q -- dictionary of new queue entry"""
1046     return True
1047
1048   def recent_removed(self, id):
1049     """Called when a track is removed from the recently played list
1050
1051     Arguments:
1052     id -- ID of removed entry (always the oldest)"""
1053     return True
1054
1055   def removed(self, id, user):
1056     """Called when a track is removed from the queue, either manually
1057     or in order to play it.
1058
1059     Arguments:
1060     id -- ID of removed entry
1061     user -- user responsible (or None if we're playing this track)"""
1062     return True
1063
1064   def scratched(self, track, user):
1065     """Called when a track is scratched
1066
1067     Arguments:
1068     track -- track that was scratched
1069     user -- user responsible"""
1070     return True
1071
1072   def invalid(self, line):
1073     """Called when an event log line cannot be interpreted
1074
1075     Arguments:
1076     line -- line that could not be understood"""
1077     return True
1078
1079 # Local Variables:
1080 # mode:python
1081 # py-indent-offset:2
1082 # comment-column:40
1083 # fill-column:72
1084 # End: