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