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