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