chiark / gitweb /
Merge playlist 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 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
129     command 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
437     information 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.
543     All keys are 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
779     deleted.
780
781     It is suggested that you use the disorder.monitor class instead of
782     calling this method directly, but this is not mandatory.
783
784     See disorder_protocol(5) for the event log syntax.
785
786     Arguments:
787     callback -- function to call with log entry
788     """
789     ret, details = self._simple("log")
790     while True:
791       l = self._line()
792       self._debug(client.debug_body, "<<< %s" % l)
793       if l != '' and l[0] == '.':
794         if l == '.':
795           return
796         l = l[1:]
797       if not callback(self, l):
798         break
799
800   def pause(self):
801     """Pause the current track."""
802     self._simple("pause")
803
804   def resume(self):
805     """Resume after a pause."""
806     self._simple("resume")
807
808   def part(self, track, context, part):
809     """Get a track name part
810
811     Arguments:
812     track -- the track to query
813     context -- the context ('sort' or 'display')
814     part -- the desired part (usually 'artist', 'album' or 'title')
815
816     The return value is the preference 
817     """
818     ret, details = self._simple("part", track, context, part)
819     return _split(details)[0]
820
821   def setglobal(self, key, value):
822     """Set a global preference value.
823
824     Arguments:
825     key -- the preference name
826     value -- the new preference value
827     """
828     self._simple("set-global", key, value)
829
830   def unsetglobal(self, key):
831     """Unset a global preference value.
832
833     Arguments:
834     key -- the preference to remove
835     """
836     self._simple("set-global", key, value)
837
838   def getglobal(self, key):
839     """Get a global preference value.
840
841     Arguments:
842     key -- the preference to look up
843
844     The return value is the preference 
845     """
846     ret, details = self._simple("get-global", key)
847     if ret == 555:
848       return None
849     else:
850       return _split(details)[0]
851
852   def make_cookie(self):
853     """Create a login cookie"""
854     ret, details = self._simple("make-cookie")
855     return _split(details)[0]
856   
857   def revoke(self):
858     """Revoke a login cookie"""
859     self._simple("revoke")
860
861   def adduser(self, user, password):
862     """Create a user"""
863     self._simple("adduser", user, password)
864
865   def deluser(self, user):
866     """Delete a user"""
867     self._simple("deluser", user)
868
869   def userinfo(self, user, key):
870     """Get user information"""
871     res, details = self._simple("userinfo", user, key)
872     if res == 555:
873       return None
874     return _split(details)[0]
875
876   def edituser(self, user, key, value):
877     """Set user information"""
878     self._simple("edituser", user, key, value)
879
880   def users(self):
881     """List all users
882
883     The return value is a list of all users."""
884     self._simple("users")
885     return self._body()
886
887   def register(self, username, password, email):
888     """Register a user"""
889     res, details = self._simple("register", username, password, email)
890     return _split(details)[0]
891
892   def confirm(self, confirmation):
893     """Confirm a user registration"""
894     res, details = self._simple("confirm", confirmation)
895
896   def schedule_list(self):
897     """Get a list of scheduled events """
898     self._simple("schedule-list")
899     return self._body()
900
901   def schedule_del(self, event):
902     """Delete a scheduled event"""
903     self._simple("schedule-del", event)
904
905   def schedule_get(self, event):
906     """Get the details for an event as a dict (returns None if
907     event not found)"""
908     res, details = self._simple("schedule-get", event)
909     if res == 555:
910       return None
911     d = {}
912     for line in self._body():
913       bits = _split(line)
914       d[bits[0]] = bits[1]
915     return d
916
917   def schedule_add(self, when, priority, action, *rest):
918     """Add a scheduled event"""
919     self._simple("schedule-add", str(when), priority, action, *rest)
920
921   def adopt(self, id):
922     """Adopt a randomly picked track"""
923     self._simple("adopt", id)
924
925   def playlist_delete(self, playlist):
926     """Delete a playlist"""
927     res, details = self._simple("playlist-delete", playlist)
928     if res == 555:
929       raise operationError(res, details, "playlist-delete")
930
931   def playlist_get(self, playlist):
932     """Get the contents of a playlist
933
934     The return value is an array of track names, or None if there is no
935     such playlist."""
936     res, details = self._simple("playlist-get", playlist)
937     if res == 555:
938       return None
939     return self._body()
940
941   def playlist_lock(self, playlist):
942     """Lock a playlist.  Playlists can only be modified when locked."""
943     self._simple("playlist-lock", playlist)
944
945   def playlist_unlock(self):
946     """Unlock the locked playlist."""
947     self._simple("playlist-unlock")
948
949   def playlist_set(self, playlist, tracks):
950     """Set the contents of a playlist.  The playlist must be locked.
951
952     Arguments:
953     playlist -- Playlist to set
954     tracks -- Array of tracks"""
955     self._simple_body(tracks, "playlist-set", playlist)
956
957   def playlist_set_share(self, playlist, share):
958     """Set the sharing status of a playlist"""
959     self._simple("playlist-set-share", playlist, share)
960
961   def playlist_get_share(self, playlist):
962     """Returns the sharing status of a playlist"""
963     res, details = self._simple("playlist-get-share", playlist)
964     if res == 555:
965       return None
966     return _split(details)[0]
967
968   def playlists(self):
969     """Returns the list of visible playlists"""
970     self._simple("playlists")
971     return self._body()
972
973   ########################################################################
974   # I/O infrastructure
975
976   def _line(self):
977     # read one response line and return as some suitable string object
978     #
979     # If an I/O error occurs, disconnect from the server.
980     #
981     # XXX does readline() DTRT regarding character encodings?
982     try:
983       l = self.r.readline()
984       if not re.search("\n", l):
985         raise communicationError(self.who, "peer disconnected")
986       l = l[:-1]
987     except:
988       self._disconnect()
989       raise
990     return unicode(l, "UTF-8")
991
992   def _response(self):
993     # read a response as a (code, details) tuple
994     l = self._line()
995     self._debug(client.debug_proto, "<== %s" % l)
996     m = _response.match(l)
997     if m:
998       return int(m.group(1)), m.group(2)
999     else:
1000       raise protocolError(self.who, "invalid response %s")
1001
1002   def _send(self, body, *command):
1003     # Quote and send a command and optional body
1004     #
1005     # Returns the encoded command.
1006     quoted = _quote(command)
1007     self._debug(client.debug_proto, "==> %s" % quoted)
1008     encoded = quoted.encode("UTF-8")
1009     try:
1010       self.w.write(encoded)
1011       self.w.write("\n")
1012       if body != None:
1013         for l in body:
1014           if l[0] == ".":
1015             self.w.write(".")
1016           self.w.write(l)
1017           self.w.write("\n")
1018         self.w.write(".\n")
1019       self.w.flush()
1020       return encoded
1021     except IOError, e:
1022       # e.g. EPIPE
1023       self._disconnect()
1024       raise communicationError(self.who, e)
1025     except:
1026       self._disconnect()
1027       raise
1028
1029   def _simple(self, *command): 
1030     # Issue a simple command, throw an exception on error
1031     #
1032     # If an I/O error occurs, disconnect from the server.
1033     #
1034     # On success or 'normal' errors returns response as a (code, details) tuple
1035     #
1036     # On error raise operationError
1037     return self._simple_body(None, *command)
1038  
1039   def _simple_body(self, body, *command):
1040     # Issue a simple command with optional body, throw an exception on error
1041     #
1042     # If an I/O error occurs, disconnect from the server.
1043     #
1044     # On success or 'normal' errors returns response as a (code, details) tuple
1045     #
1046     # On error raise operationError
1047     if self.state == 'disconnected':
1048       self.connect()
1049     if command:
1050       cmd = self._send(body, *command)
1051     else:
1052       cmd = None
1053     res, details = self._response()
1054     if res / 100 == 2 or res == 555:
1055       return res, details
1056     raise operationError(res, details, cmd)
1057
1058   def _body(self):
1059     # Fetch a dot-stuffed body
1060     result = []
1061     while True:
1062       l = self._line()
1063       self._debug(client.debug_body, "<<< %s" % l)
1064       if l != '' and l[0] == '.':
1065         if l == '.':
1066           return result
1067         l = l[1:]
1068       result.append(l)
1069
1070   ########################################################################
1071   # Configuration file parsing
1072
1073   def _readfile(self, path):
1074     # Read a configuration file
1075     #
1076     # Arguments:
1077     #
1078     # path -- path of file to read
1079
1080     # handlers for various commands
1081     def _collection(self, command, args):
1082       if len(args) != 3:
1083         return "'%s' takes three args" % command
1084       self.config["collections"].append(args)
1085       
1086     def _unary(self, command, args):
1087       if len(args) != 1:
1088         return "'%s' takes only one arg" % command
1089       self.config[command] = args[0]
1090
1091     def _include(self, command, args):
1092       if len(args) != 1:
1093         return "'%s' takes only one arg" % command
1094       self._readfile(args[0])
1095
1096     def _any(self, command, args):
1097       self.config[command] = args
1098
1099     # mapping of options to handlers
1100     _options = { "collection": _collection,
1101                  "username": _unary,
1102                  "password": _unary,
1103                  "home": _unary,
1104                  "connect": _any,
1105                  "include": _include }
1106
1107     # the parser
1108     for lno, line in enumerate(file(path, "r")):
1109       try:
1110         fields = _split(line, 'comments')
1111       except _splitError, s:
1112         raise parseError(path, lno + 1, str(s))
1113       if fields:
1114         command = fields[0]
1115         # we just ignore options we don't know about, so as to cope gracefully
1116         # with version skew (and nothing to do with implementor laziness)
1117         if command in _options:
1118           e = _options[command](self, command, fields[1:])
1119           if e:
1120             self._parseError(path, lno + 1, e)
1121
1122   def _parseError(self, path, lno, s):
1123     raise parseError(path, lno, s)
1124
1125 ########################################################################
1126 # monitor class
1127
1128 class monitor:
1129   """DisOrder event log monitor class
1130
1131   Intended to be subclassed with methods corresponding to event log
1132   messages the implementor cares about over-ridden."""
1133
1134   def __init__(self, c=None):
1135     """Constructor for the monitor class
1136
1137     Can be passed a client to use.  If none is specified then one
1138     will be created specially for the purpose.
1139
1140     Arguments:
1141     c -- client"""
1142     if c == None:
1143       c = client();
1144     self.c = c
1145
1146   def run(self):
1147     """Start monitoring logs.  Continues monitoring until one of the
1148     message-specific methods returns False.  Can be called more than
1149     once (but not recursively!)"""
1150     self.c.log(self._callback)
1151
1152   def when(self):
1153     """Return the timestamp of the current (or most recent) event log entry"""
1154     return self.timestamp
1155
1156   def _callback(self, c, line):
1157     try:
1158       bits = _split(line)
1159     except:
1160       return self.invalid(line)
1161     if(len(bits) < 2):
1162       return self.invalid(line)
1163     self.timestamp = int(bits[0], 16)
1164     keyword = bits[1]
1165     bits = bits[2:]
1166     if keyword == 'completed':
1167       if len(bits) == 1:
1168         return self.completed(bits[0])
1169     elif keyword == 'failed':
1170       if len(bits) == 2:
1171         return self.failed(bits[0], bits[1])
1172     elif keyword == 'moved':
1173       if len(bits) == 3:
1174         try:
1175           n = int(bits[1])
1176         except:
1177           return self.invalid(line)
1178         return self.moved(bits[0], n, bits[2])
1179     elif keyword == 'playing':
1180       if len(bits) == 1:
1181         return self.playing(bits[0], None)
1182       elif len(bits) == 2:
1183         return self.playing(bits[0], bits[1])
1184     elif keyword == 'queue' or keyword == 'recent-added':
1185       try:
1186         q = _list2dict(bits)
1187       except:
1188         return self.invalid(line)
1189       if keyword == 'queue':
1190         return self.queue(q)
1191       if keyword == 'recent-added':
1192         return self.recent_added(q)
1193     elif keyword == 'recent-removed':
1194       if len(bits) == 1:
1195         return self.recent_removed(bits[0])
1196     elif keyword == 'removed':
1197       if len(bits) == 1:
1198         return self.removed(bits[0], None)
1199       elif len(bits) == 2:
1200         return self.removed(bits[0], bits[1])
1201     elif keyword == 'scratched':
1202       if len(bits) == 2:
1203         return self.scratched(bits[0], bits[1])
1204     elif keyword == 'rescanned':
1205       return self.rescanned()
1206     return self.invalid(line)
1207
1208   def completed(self, track):
1209     """Called when a track completes.
1210
1211     Arguments:
1212     track -- track that completed"""
1213     return True
1214
1215   def failed(self, track, error):
1216     """Called when a player suffers an error.
1217
1218     Arguments:
1219     track -- track that failed
1220     error -- error indicator"""
1221     return True
1222
1223   def moved(self, id, offset, user):
1224     """Called when a track is moved in the queue.
1225
1226     Arguments:
1227     id -- queue entry ID
1228     offset -- distance moved
1229     user -- user responsible"""
1230     return True
1231
1232   def playing(self, track, user):
1233     """Called when a track starts playing.
1234
1235     Arguments:
1236     track -- track that has started
1237     user -- user that submitted track, or None"""
1238     return True
1239
1240   def queue(self, q):
1241     """Called when a track is added to the queue.
1242
1243     Arguments:
1244     q -- dictionary of new queue entry"""
1245     return True
1246
1247   def recent_added(self, q):
1248     """Called when a track is added to the recently played list
1249
1250     Arguments:
1251     q -- dictionary of new queue entry"""
1252     return True
1253
1254   def recent_removed(self, id):
1255     """Called when a track is removed from the recently played list
1256
1257     Arguments:
1258     id -- ID of removed entry (always the oldest)"""
1259     return True
1260
1261   def removed(self, id, user):
1262     """Called when a track is removed from the queue, either manually
1263     or in order to play it.
1264
1265     Arguments:
1266     id -- ID of removed entry
1267     user -- user responsible (or None if we're playing this track)"""
1268     return True
1269
1270   def scratched(self, track, user):
1271     """Called when a track is scratched
1272
1273     Arguments:
1274     track -- track that was scratched
1275     user -- user responsible"""
1276     return True
1277
1278   def invalid(self, line):
1279     """Called when an event log line cannot be interpreted
1280
1281     Arguments:
1282     line -- line that could not be understood"""
1283     return True
1284
1285   def rescanned(self):
1286     """Called when a rescan completes"""
1287     return True
1288
1289 # Local Variables:
1290 # mode:python
1291 # py-indent-offset:2
1292 # comment-column:40
1293 # fill-column:72
1294 # End: