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