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