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