chiark / gitweb /
b18c7243ed0181a21f12584ee5b58cf3a07bc75f
[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, user=None, password=None):
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     self.user = user
298     self.password = password
299     home = os.getenv("HOME")
300     if not home:
301       home = pw.pw_dir
302     privconf = _configfile + "." + pw.pw_name
303     passfile = home + os.sep + ".disorder" + os.sep + "passwd"
304     if os.path.exists(_configfile):
305       self._readfile(_configfile)
306     if os.path.exists(privconf):
307       self._readfile(privconf)
308     if os.path.exists(passfile) and _userconf:
309       self._readfile(passfile)
310     self.state = 'disconnected'
311
312   def debug(self, bits):
313     """Enable or disable protocol debugging.  Debug messages are written
314     to sys.stderr.
315
316     Arguments:
317     bits -- bitmap of operations that should generate debug information
318
319     Bitmap values:
320     debug_proto -- dump control protocol messages (excluding bodies)
321     debug_body -- dump control protocol message bodies
322     """
323     self.debugging = bits
324
325   def _debug(self, bit, s):
326     # debug output
327     if self.debugging & bit:
328       sys.stderr.write(_sanitize(s))
329       sys.stderr.write("\n")
330       sys.stderr.flush()
331
332   def connect(self, cookie=None):
333     """c.connect(cookie=None)
334
335     Connect to the DisOrder server and authenticate.
336
337     Raises communicationError if connection fails and operationError if
338     authentication fails (in which case disconnection is automatic).
339
340     May be called more than once to retry connections (e.g. when the
341     server is down).  If we are already connected and authenticated,
342     this is a no-op.
343
344     Other operations automatically connect if we're not already
345     connected, so it is not strictly necessary to call this method.
346
347     If COOKIE is specified then that is used to log in instead of
348     the username/password.
349     """
350     if self.state == 'disconnected':
351       try:
352         self.state = 'connecting'
353         if 'connect' in self.config and len(self.config['connect']) > 0:
354           c = self.config['connect']
355           self.who = repr(c)            # temporarily
356           if len(c) == 1:
357             a = socket.getaddrinfo(None, c[0],
358                                    socket.AF_INET,
359                                    socket.SOCK_STREAM,
360                                    0,
361                                    0)
362           else:
363             a = socket.getaddrinfo(c[0], c[1],
364                                    socket.AF_INET,
365                                    socket.SOCK_STREAM,
366                                    0,
367                                    0)
368           a = a[0]
369           s = socket.socket(a[0], a[1], a[2]);
370           s.connect(a[4])
371           self.who = "%s" % a[3]
372         else:
373           s = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM);
374           self.who = self.config['home'] + os.sep + "socket"
375           s.connect(self.who)
376         self.w = s.makefile("wb")
377         self.r = s.makefile("rb")
378         (res, challenge_and_algo) = self._simple()
379         (algo, challenge) = _split(challenge_and_algo)
380         if cookie is None:
381           if self.user is None:
382             user = self.config['username']
383           else:
384             user = self.user
385           if self.password is None:
386             password = self.config['password']
387           else:
388             password = self.password
389           # TODO support algorithms other than SHA-1
390           h = sha.sha()
391           h.update(password)
392           h.update(binascii.unhexlify(challenge))
393           self._simple("user", user, h.hexdigest())
394         else:
395           self._simple("cookie", cookie)
396         self.state = 'connected'
397       except socket.error, e:
398         self._disconnect()
399         raise communicationError(self.who, e)
400       except:
401         self._disconnect()
402         raise
403
404   def _disconnect(self):
405     # disconnect from the server, whatever state we are in
406     try:
407       del self.w
408       del self.r
409     except:
410       pass
411     self.state = 'disconnected'
412     
413   ########################################################################
414   # Operations
415
416   def become(self, who):
417     """Become another user.
418
419     Arguments:
420     who -- the user to become.
421
422     Only trusted users can perform this operation.
423     """
424     self._simple("become", who)
425
426   def play(self, track):
427     """Play a track.
428
429     Arguments:
430     track -- the path of the track to play.
431
432     Returns the ID of the new queue entry.
433
434     Note that queue IDs are unicode strings (because all track information
435     values are unicode strings).
436     """
437     res, details = self._simple("play", track)
438     return unicode(details)             # because it's unicode in queue() output
439
440   def remove(self, track):
441     """Remove a track from the queue.
442
443     Arguments:
444     track -- the path or ID of the track to remove.
445     """
446     self._simple("remove", track)
447
448   def enable(self):
449     """Enable playing."""
450     self._simple("enable")
451
452   def disable(self, *now):
453     """Disable playing.
454
455     Arguments:
456     now -- if present (with any value), the current track is stopped
457            too.
458     """
459     if now:
460       self._simple("disable", "now")
461     else:
462       self._simple("disable")
463
464   def scratch(self, *id):
465     """Scratch the currently playing track.
466
467     Arguments:
468     id -- if present, the ID of the track to scratch.
469     """
470     if id:
471       self._simple("scratch", id[0])
472     else:
473       self._simple("scratch")
474
475   def shutdown(self):
476     """Shut down the server.
477
478     Only trusted users can perform this operation.
479     """
480     self._simple("shutdown")
481
482   def reconfigure(self):
483     """Make the server reload its configuration.
484
485     Only trusted users can perform this operation.
486     """
487     self._simple("reconfigure")
488
489   def rescan(self, pattern):
490     """Rescan one or more collections.
491
492     Arguments:
493     pattern -- glob pattern matching collections to rescan.
494
495     Only trusted users can perform this operation.
496     """
497     self._simple("rescan", pattern)
498
499   def version(self):
500     """Return the server's version number."""
501     return self._simple("version")[1]
502
503   def playing(self):
504     """Return the currently playing track.
505
506     If a track is playing then it is returned as a dictionary.  See
507     disorder_protocol(5) for the meanings of the keys.  All keys are
508     plain strings but the values will be unicode strings.
509     
510     If no track is playing then None is returned."""
511     res, details = self._simple("playing")
512     if res % 10 != 9:
513       try:
514         return _queueEntry(details)
515       except _splitError, s:
516         raise protocolError(self.who, s.str())
517     else:
518       return None
519
520   def _somequeue(self, command):
521     self._simple(command)
522     try:
523       return map(lambda s: _queueEntry(s), self._body())
524     except _splitError, s:
525       raise protocolError(self.who, s.str())
526
527   def recent(self):
528     """Return a list of recently played tracks.
529
530     The return value is a list of dictionaries corresponding to
531     recently played tracks.  The oldest track comes first.
532
533     See disorder_protocol(5) for the meanings of the keys.  All keys are
534     plain strings but the values will be unicode strings."""
535     return self._somequeue("recent")
536
537   def queue(self):
538     """Return the current queue.
539
540     The return value is a list of dictionaries corresponding to
541     recently played tracks.  The next track to be played comes first.
542
543     See disorder_protocol(5) for the meanings of the keys.  All keys are
544     plain strings but the values will be unicode strings."""
545     return self._somequeue("queue")
546
547   def _somedir(self, command, dir, re):
548     if re:
549       self._simple(command, dir, re[0])
550     else:
551       self._simple(command, dir)
552     return self._body()
553
554   def directories(self, dir, *re):
555     """List subdirectories of a directory.
556
557     Arguments:
558     dir -- directory to list, or '' for the whole root.
559     re -- regexp that results must match.  Optional.
560
561     The return value is a list of the (nonempty) subdirectories of dir.
562     If dir is '' then a list of top-level directories is returned.
563
564     If a regexp is specified then the basename of each result must
565     match.  Matching is case-independent.  See pcrepattern(3).
566     """
567     return self._somedir("dirs", dir, re)
568   
569   def files(self, dir, *re):
570     """List files within a directory.
571
572     Arguments:
573     dir -- directory to list, or '' for the whole root.
574     re -- regexp that results must match.  Optional.
575
576     The return value is a list of playable files in dir.  If dir is ''
577     then a list of top-level files is returned.
578
579     If a regexp is specified then the basename of each result must
580     match.  Matching is case-independent.  See pcrepattern(3).
581     """
582     return self._somedir("files", dir, re)
583
584   def allfiles(self, dir, *re):
585     """List subdirectories and files within a directory.
586
587     Arguments:
588     dir -- directory to list, or '' for the whole root.
589     re -- regexp that results must match.  Optional.
590
591     The return value is a list of all (nonempty) subdirectories and
592     files within dir.  If dir is '' then a list of top-level files and
593     directories is returned.
594     
595     If a regexp is specified then the basename of each result must
596     match.  Matching is case-independent.  See pcrepattern(3).
597     """
598     return self._somedir("allfiles", dir, re)
599
600   def set(self, track, key, value):
601     """Set a preference value.
602
603     Arguments:
604     track -- the track to modify
605     key -- the preference name
606     value -- the new preference value
607     """
608     self._simple("set", track, key, value)
609
610   def unset(self, track, key):
611     """Unset a preference value.
612
613     Arguments:
614     track -- the track to modify
615     key -- the preference to remove
616     """
617     self._simple("set", track, key, value)
618
619   def get(self, track, key):
620     """Get a preference value.
621
622     Arguments:
623     track -- the track to query
624     key -- the preference to remove
625
626     The return value is the preference.
627     """
628     ret, details = self._simple("get", track, key)
629     if ret == 555:
630       return None
631     else:
632       return details
633
634   def prefs(self, track):
635     """Get all the preferences for a track.
636
637     Arguments:
638     track -- the track to query
639
640     The return value is a dictionary of all the track's preferences.
641     Note that even nominally numeric values remain encoded as strings.
642     """
643     self._simple("prefs", track)
644     r = {}
645     for line in self._body():
646       try:
647         kv = _split(line)
648       except _splitError, s:
649         raise protocolError(self.who, s.str())
650       if len(kv) != 2:
651         raise protocolError(self.who, "invalid prefs body line")
652       r[kv[0]] = kv[1]
653     return r
654
655   def _boolean(self, s):
656     return s[1] == 'yes'
657
658   def exists(self, track):
659     """Return true if a track exists
660
661     Arguments:
662     track -- the track to check for"""
663     return self._boolean(self._simple("exists", track))
664
665   def enabled(self):
666     """Return true if playing is enabled"""
667     return self._boolean(self._simple("enabled"))
668
669   def random_enabled(self):
670     """Return true if random play is enabled"""
671     return self._boolean(self._simple("random-enabled"))
672
673   def random_enable(self):
674     """Enable random play."""
675     self._simple("random-enable")
676
677   def random_disable(self):
678     """Disable random play."""
679     self._simple("random-disable")
680
681   def length(self, track):
682     """Return the length of a track in seconds.
683
684     Arguments:
685     track -- the track to query.
686     """
687     ret, details = self._simple("length", track)
688     return int(details)
689
690   def search(self, words):
691     """Search for tracks.
692
693     Arguments:
694     words -- the set of words to search for.
695
696     The return value is a list of track path names, all of which contain
697     all of the required words (in their path name, trackname
698     preferences, etc.)
699     """
700     self._simple("search", _quote(words))
701     return self._body()
702
703   def tags(self):
704     """List all tags
705
706     The return value is a list of all tags which apply to at least one
707     track."""
708     self._simple("tags")
709     return self._body()
710
711   def stats(self):
712     """Get server statistics.
713
714     The return value is list of statistics.
715     """
716     self._simple("stats")
717     return self._body()
718
719   def dump(self):
720     """Get all preferences.
721
722     The return value is an encoded dump of the preferences database.
723     """
724     self._simple("dump")
725     return self._body()
726
727   def set_volume(self, left, right):
728     """Set volume.
729
730     Arguments:
731     left -- volume for the left speaker.
732     right --  volume for the right speaker.
733     """
734     self._simple("volume", left, right)
735
736   def get_volume(self):
737     """Get volume.
738
739     The return value a tuple consisting of the left and right volumes.
740     """
741     ret, details = self._simple("volume")
742     return map(int,string.split(details))
743
744   def move(self, track, delta):
745     """Move a track in the queue.
746
747     Arguments:
748     track -- the name or ID of the track to move
749     delta -- the number of steps towards the head of the queue to move
750     """
751     ret, details = self._simple("move", track, str(delta))
752     return int(details)
753
754   def moveafter(self, target, tracks):
755     """Move a track in the queue
756
757     Arguments:
758     target -- target ID or None
759     tracks -- a list of IDs to move
760
761     If target is '' or is not in the queue then the tracks are moved to
762     the head of the queue.
763
764     Otherwise the tracks are moved to just after the target."""
765     if target is None:
766       target = ''
767     self._simple("moveafter", target, *tracks)
768
769   def log(self, callback):
770     """Read event log entries as they happen.
771
772     Each event log entry is handled by passing it to callback.
773
774     The callback takes two arguments, the first is the client and the
775     second the line from the event log.
776     
777     The callback should return True to continue or False to stop (don't
778     forget this, or your program will mysteriously misbehave).
779
780     It is suggested that you use the disorder.monitor class instead of
781     calling this method directly, but this is not mandatory.
782
783     See disorder_protocol(5) for the event log syntax.
784
785     Arguments:
786     callback -- function to call with log entry
787     """
788     ret, details = self._simple("log")
789     while True:
790       l = self._line()
791       self._debug(client.debug_body, "<<< %s" % l)
792       if l != '' and l[0] == '.':
793         if l == '.':
794           return
795         l = l[1:]
796       if not callback(self, l):
797         break
798     # tell the server to stop sending, eat the remains of the body,
799     # eat the response
800     self._send("version")
801     self._body()
802     self._response()
803
804   def pause(self):
805     """Pause the current track."""
806     self._simple("pause")
807
808   def resume(self):
809     """Resume after a pause."""
810     self._simple("resume")
811
812   def part(self, track, context, part):
813     """Get a track name part
814
815     Arguments:
816     track -- the track to query
817     context -- the context ('sort' or 'display')
818     part -- the desired part (usually 'artist', 'album' or 'title')
819
820     The return value is the preference 
821     """
822     ret, details = self._simple("part", track, context, part)
823     return details
824
825   def setglobal(self, key, value):
826     """Set a global preference value.
827
828     Arguments:
829     key -- the preference name
830     value -- the new preference value
831     """
832     self._simple("set-global", key, value)
833
834   def unsetglobal(self, key):
835     """Unset a global preference value.
836
837     Arguments:
838     key -- the preference to remove
839     """
840     self._simple("set-global", key, value)
841
842   def getglobal(self, key):
843     """Get a global preference value.
844
845     Arguments:
846     key -- the preference to look up
847
848     The return value is the preference 
849     """
850     ret, details = self._simple("get-global", key)
851     if ret == 555:
852       return None
853     else:
854       return details
855
856   def make_cookie(self):
857     """Create a login cookie"""
858     ret, details = self._simple("make-cookie")
859     return _split(details)[0]
860   
861   def revoke(self):
862     """Revoke a login cookie"""
863     self._simple("revoke")
864
865   def adduser(self, user, password):
866     """Create a user"""
867     self._simple("adduser", user, password)
868
869   def deluser(self, user):
870     """Delete a user"""
871     self._simple("deluser", user)
872
873   def userinfo(self, user, key):
874     """Get user information"""
875     res, details = self._simple("userinfo", user, key)
876     if res == 555:
877       return None
878     return _split(details)[0]
879
880   def edituser(self, user, key, value):
881     """Set user information"""
882     self._simple("edituser", user, key, value)
883
884   def users(self):
885     """List all users
886
887     The return value is a list of all users."""
888     self._simple("users")
889     return self._body()
890
891   ########################################################################
892   # I/O infrastructure
893
894   def _line(self):
895     # read one response line and return as some suitable string object
896     #
897     # If an I/O error occurs, disconnect from the server.
898     #
899     # XXX does readline() DTRT regarding character encodings?
900     try:
901       l = self.r.readline()
902       if not re.search("\n", l):
903         raise communicationError(self.who, "peer disconnected")
904       l = l[:-1]
905     except:
906       self._disconnect()
907       raise
908     return unicode(l, "UTF-8")
909
910   def _response(self):
911     # read a response as a (code, details) tuple
912     l = self._line()
913     self._debug(client.debug_proto, "<== %s" % l)
914     m = _response.match(l)
915     if m:
916       return int(m.group(1)), m.group(2)
917     else:
918       raise protocolError(self.who, "invalid response %s")
919
920   def _send(self, *command):
921     # Quote and send a command
922     #
923     # Returns the encoded command.
924     quoted = _quote(command)
925     self._debug(client.debug_proto, "==> %s" % quoted)
926     encoded = quoted.encode("UTF-8")
927     try:
928       self.w.write(encoded)
929       self.w.write("\n")
930       self.w.flush()
931       return encoded
932     except IOError, e:
933       # e.g. EPIPE
934       self._disconnect()
935       raise communicationError(self.who, e)
936     except:
937       self._disconnect()
938       raise
939
940   def _simple(self, *command):
941     # Issue a simple command, throw an exception on error
942     #
943     # If an I/O error occurs, disconnect from the server.
944     #
945     # On success or 'normal' errors returns response as a (code, details) tuple
946     #
947     # On error raise operationError
948     if self.state == 'disconnected':
949       self.connect()
950     if command:
951       cmd = self._send(*command)
952     else:
953       cmd = None
954     res, details = self._response()
955     if res / 100 == 2 or res == 555:
956       return res, details
957     raise operationError(res, details, cmd)
958
959   def _body(self):
960     # Fetch a dot-stuffed body
961     result = []
962     while True:
963       l = self._line()
964       self._debug(client.debug_body, "<<< %s" % l)
965       if l != '' and l[0] == '.':
966         if l == '.':
967           return result
968         l = l[1:]
969       result.append(l)
970
971   ########################################################################
972   # Configuration file parsing
973
974   def _readfile(self, path):
975     # Read a configuration file
976     #
977     # Arguments:
978     #
979     # path -- path of file to read
980
981     # handlers for various commands
982     def _collection(self, command, args):
983       if len(args) != 3:
984         return "'%s' takes three args" % command
985       self.config["collections"].append(args)
986       
987     def _unary(self, command, args):
988       if len(args) != 1:
989         return "'%s' takes only one arg" % command
990       self.config[command] = args[0]
991
992     def _include(self, command, args):
993       if len(args) != 1:
994         return "'%s' takes only one arg" % command
995       self._readfile(args[0])
996
997     def _any(self, command, args):
998       self.config[command] = args
999
1000     # mapping of options to handlers
1001     _options = { "collection": _collection,
1002                  "username": _unary,
1003                  "password": _unary,
1004                  "home": _unary,
1005                  "connect": _any,
1006                  "include": _include }
1007
1008     # the parser
1009     for lno, line in enumerate(file(path, "r")):
1010       try:
1011         fields = _split(line, 'comments')
1012       except _splitError, s:
1013         raise parseError(path, lno + 1, str(s))
1014       if fields:
1015         command = fields[0]
1016         # we just ignore options we don't know about, so as to cope gracefully
1017         # with version skew (and nothing to do with implementor laziness)
1018         if command in _options:
1019           e = _options[command](self, command, fields[1:])
1020           if e:
1021             self._parseError(path, lno + 1, e)
1022
1023   def _parseError(self, path, lno, s):
1024     raise parseError(path, lno, s)
1025
1026 ########################################################################
1027 # monitor class
1028
1029 class monitor:
1030   """DisOrder event log monitor class
1031
1032   Intended to be subclassed with methods corresponding to event log messages
1033   the implementor cares about over-ridden."""
1034
1035   def __init__(self, c=None):
1036     """Constructor for the monitor class
1037
1038     Can be passed a client to use.  If none is specified then one
1039     will be created specially for the purpose.
1040
1041     Arguments:
1042     c -- client"""
1043     if c == None:
1044       c = client();
1045     self.c = c
1046
1047   def run(self):
1048     """Start monitoring logs.  Continues monitoring until one of the
1049     message-specific methods returns False.  Can be called more than once
1050     (but not recursively!)"""
1051     self.c.log(self._callback)
1052
1053   def when(self):
1054     """Return the timestamp of the current (or most recent) event log entry"""
1055     return self.timestamp
1056
1057   def _callback(self, c, line):
1058     try:
1059       bits = _split(line)
1060     except:
1061       return self.invalid(line)
1062     if(len(bits) < 2):
1063       return self.invalid(line)
1064     self.timestamp = int(bits[0], 16)
1065     keyword = bits[1]
1066     bits = bits[2:]
1067     if keyword == 'completed':
1068       if len(bits) == 1:
1069         return self.completed(bits[0])
1070     elif keyword == 'failed':
1071       if len(bits) == 2:
1072         return self.failed(bits[0], bits[1])
1073     elif keyword == 'moved':
1074       if len(bits) == 3:
1075         try:
1076           n = int(bits[1])
1077         except:
1078           return self.invalid(line)
1079         return self.moved(bits[0], n, bits[2])
1080     elif keyword == 'playing':
1081       if len(bits) == 1:
1082         return self.playing(bits[0], None)
1083       elif len(bits) == 2:
1084         return self.playing(bits[0], bits[1])
1085     elif keyword == 'queue' or keyword == 'recent-added':
1086       try:
1087         q = _list2dict(bits)
1088       except:
1089         return self.invalid(line)
1090       if keyword == 'queue':
1091         return self.queue(q)
1092       if keyword == 'recent-added':
1093         return self.recent_added(q)
1094     elif keyword == 'recent-removed':
1095       if len(bits) == 1:
1096         return self.recent_removed(bits[0])
1097     elif keyword == 'removed':
1098       if len(bits) == 1:
1099         return self.removed(bits[0], None)
1100       elif len(bits) == 2:
1101         return self.removed(bits[0], bits[1])
1102     elif keyword == 'scratched':
1103       if len(bits) == 2:
1104         return self.scratched(bits[0], bits[1])
1105     return self.invalid(line)
1106
1107   def completed(self, track):
1108     """Called when a track completes.
1109
1110     Arguments:
1111     track -- track that completed"""
1112     return True
1113
1114   def failed(self, track, error):
1115     """Called when a player suffers an error.
1116
1117     Arguments:
1118     track -- track that failed
1119     error -- error indicator"""
1120     return True
1121
1122   def moved(self, id, offset, user):
1123     """Called when a track is moved in the queue.
1124
1125     Arguments:
1126     id -- queue entry ID
1127     offset -- distance moved
1128     user -- user responsible"""
1129     return True
1130
1131   def playing(self, track, user):
1132     """Called when a track starts playing.
1133
1134     Arguments:
1135     track -- track that has started
1136     user -- user that submitted track, or None"""
1137     return True
1138
1139   def queue(self, q):
1140     """Called when a track is added to the queue.
1141
1142     Arguments:
1143     q -- dictionary of new queue entry"""
1144     return True
1145
1146   def recent_added(self, q):
1147     """Called when a track is added to the recently played list
1148
1149     Arguments:
1150     q -- dictionary of new queue entry"""
1151     return True
1152
1153   def recent_removed(self, id):
1154     """Called when a track is removed from the recently played list
1155
1156     Arguments:
1157     id -- ID of removed entry (always the oldest)"""
1158     return True
1159
1160   def removed(self, id, user):
1161     """Called when a track is removed from the queue, either manually
1162     or in order to play it.
1163
1164     Arguments:
1165     id -- ID of removed entry
1166     user -- user responsible (or None if we're playing this track)"""
1167     return True
1168
1169   def scratched(self, track, user):
1170     """Called when a track is scratched
1171
1172     Arguments:
1173     track -- track that was scratched
1174     user -- user responsible"""
1175     return True
1176
1177   def invalid(self, line):
1178     """Called when an event log line cannot be interpreted
1179
1180     Arguments:
1181     line -- line that could not be understood"""
1182     return True
1183
1184 # Local Variables:
1185 # mode:python
1186 # py-indent-offset:2
1187 # comment-column:40
1188 # fill-column:72
1189 # End: