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