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