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