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