chiark / gitweb /
verify that tag search works across dump
[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
662 def stats(self):
663 """Get server statistics.
664
665 The return value is list of statistics.
666 """
667 self._simple("stats")
668 return self._body()
669
670 def dump(self):
671 """Get all preferences.
672
673 The return value is an encoded dump of the preferences database.
674 """
675 self._simple("dump")
676 return self._body()
677
678 def set_volume(self, left, right):
679 """Set volume.
680
681 Arguments:
682 left -- volume for the left speaker.
683 right -- volume for the right speaker.
684 """
685 self._simple("volume", left, right)
686
687 def get_volume(self):
688 """Get volume.
689
690 The return value a tuple consisting of the left and right volumes.
691 """
692 ret, details = self._simple("volume")
693 return map(int,string.split(details))
694
695 def move(self, track, delta):
696 """Move a track in the queue.
697
698 Arguments:
699 track -- the name or ID of the track to move
700 delta -- the number of steps towards the head of the queue to move
701 """
702 ret, details = self._simple("move", track, str(delta))
703 return int(details)
704
705 def log(self, callback):
706 """Read event log entries as they happen.
707
708 Each event log entry is handled by passing it to callback.
709
710 The callback takes two arguments, the first is the client and the
711 second the line from the event log.
712
713 The callback should return True to continue or False to stop (don't
714 forget this, or your program will mysteriously misbehave).
715
716 It is suggested that you use the disorder.monitor class instead of
717 calling this method directly, but this is not mandatory.
718
719 See disorder_protocol(5) for the event log syntax.
720
721 Arguments:
722 callback -- function to call with log entry
723 """
724 ret, details = self._simple("log")
725 while True:
726 l = self._line()
727 self._debug(client.debug_body, "<<< %s" % l)
728 if l != '' and l[0] == '.':
729 if l == '.':
730 return
731 l = l[1:]
732 if not callback(self, l):
733 break
734 # tell the server to stop sending, eat the remains of the body,
735 # eat the response
736 self._send("version")
737 self._body()
738 self._response()
739
740 def pause(self):
741 """Pause the current track."""
742 self._simple("pause")
743
744 def resume(self):
745 """Resume after a pause."""
746 self._simple("resume")
747
748 def part(self, track, context, part):
749 """Get a track name part
750
751 Arguments:
752 track -- the track to query
753 context -- the context ('sort' or 'display')
754 part -- the desired part (usually 'artist', 'album' or 'title')
755
756 The return value is the preference
757 """
758 ret, details = self._simple("part", track, context, part)
759 return details
760
f35e5800
RK
761 def setglobal(self, key, value):
762 """Set a global preference value.
763
764 Arguments:
765 key -- the preference name
766 value -- the new preference value
767 """
768 self._simple("set-global", key, value)
769
770 def unsetglobal(self, key):
771 """Unset a global preference value.
772
773 Arguments:
774 key -- the preference to remove
775 """
776 self._simple("set-global", key, value)
777
778 def getglobal(self, key):
779 """Get a global preference value.
780
781 Arguments:
782 key -- the preference to look up
783
784 The return value is the preference
785 """
786 ret, details = self._simple("get-global", key)
fb1bc1f5
RK
787 if ret == 555:
788 return None
789 else:
790 return details
f35e5800 791
460b9539 792 ########################################################################
793 # I/O infrastructure
794
795 def _line(self):
796 # read one response line and return as some suitable string object
797 #
798 # If an I/O error occurs, disconnect from the server.
799 #
800 # XXX does readline() DTRT regarding character encodings?
801 try:
802 l = self.r.readline()
803 if not re.search("\n", l):
804 raise communicationError(self.who, "peer disconnected")
805 l = l[:-1]
806 except:
807 self._disconnect()
808 raise
809 return unicode(l, "UTF-8")
810
811 def _response(self):
812 # read a response as a (code, details) tuple
813 l = self._line()
814 self._debug(client.debug_proto, "<== %s" % l)
815 m = _response.match(l)
816 if m:
817 return int(m.group(1)), m.group(2)
818 else:
819 raise protocolError(self.who, "invalid response %s")
820
821 def _send(self, *command):
f383b2f1
RK
822 # Quote and send a command
823 #
824 # Returns the encoded command.
460b9539 825 quoted = _quote(command)
826 self._debug(client.debug_proto, "==> %s" % quoted)
827 encoded = quoted.encode("UTF-8")
828 try:
829 self.w.write(encoded)
830 self.w.write("\n")
831 self.w.flush()
f383b2f1 832 return encoded
460b9539 833 except IOError, e:
834 # e.g. EPIPE
835 self._disconnect()
836 raise communicationError(self.who, e)
837 except:
838 self._disconnect()
839 raise
840
841 def _simple(self, *command):
842 # Issue a simple command, throw an exception on error
843 #
844 # If an I/O error occurs, disconnect from the server.
845 #
fb1bc1f5 846 # On success or 'normal' errors returns response as a (code, details) tuple
460b9539 847 #
848 # On error raise operationError
849 if self.state == 'disconnected':
850 self.connect()
851 if command:
f383b2f1
RK
852 cmd = self._send(*command)
853 else:
854 cmd = None
460b9539 855 res, details = self._response()
fb1bc1f5 856 if res / 100 == 2 or res == 555:
460b9539 857 return res, details
f383b2f1 858 raise operationError(res, details, cmd)
460b9539 859
860 def _body(self):
861 # Fetch a dot-stuffed body
862 result = []
863 while True:
864 l = self._line()
865 self._debug(client.debug_body, "<<< %s" % l)
866 if l != '' and l[0] == '.':
867 if l == '.':
868 return result
869 l = l[1:]
870 result.append(l)
871
872 ########################################################################
873 # Configuration file parsing
874
875 def _readfile(self, path):
876 # Read a configuration file
877 #
878 # Arguments:
879 #
880 # path -- path of file to read
881
882 # handlers for various commands
883 def _collection(self, command, args):
884 if len(args) != 3:
885 return "'%s' takes three args" % command
886 self.config["collections"].append(args)
887
888 def _unary(self, command, args):
889 if len(args) != 1:
890 return "'%s' takes only one arg" % command
891 self.config[command] = args[0]
892
893 def _include(self, command, args):
894 if len(args) != 1:
895 return "'%s' takes only one arg" % command
896 self._readfile(args[0])
897
898 def _any(self, command, args):
899 self.config[command] = args
900
901 # mapping of options to handlers
902 _options = { "collection": _collection,
903 "username": _unary,
904 "password": _unary,
905 "home": _unary,
906 "connect": _any,
907 "include": _include }
908
909 # the parser
910 for lno, line in enumerate(file(path, "r")):
911 try:
912 fields = _split(line, 'comments')
913 except _splitError, s:
914 raise parseError(path, lno + 1, str(s))
915 if fields:
916 command = fields[0]
917 # we just ignore options we don't know about, so as to cope gracefully
918 # with version skew (and nothing to do with implementor laziness)
919 if command in _options:
920 e = _options[command](self, command, fields[1:])
921 if e:
922 self._parseError(path, lno + 1, e)
923
924 def _parseError(self, path, lno, s):
925 raise parseError(path, lno, s)
926
927########################################################################
928# monitor class
929
930class monitor:
931 """DisOrder event log monitor class
932
933 Intended to be subclassed with methods corresponding to event log messages
934 the implementor cares about over-ridden."""
935
936 def __init__(self, c=None):
937 """Constructor for the monitor class
938
939 Can be passed a client to use. If none is specified then one
940 will be created specially for the purpose.
941
942 Arguments:
943 c -- client"""
944 if c == None:
945 c = client();
946 self.c = c
947
948 def run(self):
949 """Start monitoring logs. Continues monitoring until one of the
950 message-specific methods returns False. Can be called more than once
951 (but not recursively!)"""
952 self.c.log(self._callback)
953
954 def when(self):
955 """Return the timestamp of the current (or most recent) event log entry"""
956 return self.timestamp
957
958 def _callback(self, c, line):
959 try:
960 bits = _split(line)
961 except:
962 return self.invalid(line)
963 if(len(bits) < 2):
964 return self.invalid(line)
965 self.timestamp = int(bits[0], 16)
966 keyword = bits[1]
967 bits = bits[2:]
968 if keyword == 'completed':
969 if len(bits) == 1:
970 return self.completed(bits[0])
971 elif keyword == 'failed':
972 if len(bits) == 2:
973 return self.failed(bits[0], bits[1])
974 elif keyword == 'moved':
975 if len(bits) == 3:
976 try:
977 n = int(bits[1])
978 except:
979 return self.invalid(line)
980 return self.moved(bits[0], n, bits[2])
981 elif keyword == 'playing':
982 if len(bits) == 1:
983 return self.playing(bits[0], None)
984 elif len(bits) == 2:
985 return self.playing(bits[0], bits[1])
986 elif keyword == 'queue' or keyword == 'recent-added':
987 try:
988 q = _list2dict(bits)
989 except:
990 return self.invalid(line)
991 if keyword == 'queue':
992 return self.queue(q)
993 if keyword == 'recent-added':
994 return self.recent_added(q)
995 elif keyword == 'recent-removed':
996 if len(bits) == 1:
997 return self.recent_removed(bits[0])
998 elif keyword == 'removed':
999 if len(bits) == 1:
1000 return self.removed(bits[0], None)
1001 elif len(bits) == 2:
1002 return self.removed(bits[0], bits[1])
1003 elif keyword == 'scratched':
1004 if len(bits) == 2:
1005 return self.scratched(bits[0], bits[1])
1006 return self.invalid(line)
1007
1008 def completed(self, track):
1009 """Called when a track completes.
1010
1011 Arguments:
1012 track -- track that completed"""
1013 return True
1014
1015 def failed(self, track, error):
1016 """Called when a player suffers an error.
1017
1018 Arguments:
1019 track -- track that failed
1020 error -- error indicator"""
1021 return True
1022
1023 def moved(self, id, offset, user):
1024 """Called when a track is moved in the queue.
1025
1026 Arguments:
1027 id -- queue entry ID
1028 offset -- distance moved
1029 user -- user responsible"""
1030 return True
1031
1032 def playing(self, track, user):
1033 """Called when a track starts playing.
1034
1035 Arguments:
1036 track -- track that has started
1037 user -- user that submitted track, or None"""
1038 return True
1039
1040 def queue(self, q):
1041 """Called when a track is added to the queue.
1042
1043 Arguments:
1044 q -- dictionary of new queue entry"""
1045 return True
1046
1047 def recent_added(self, q):
1048 """Called when a track is added to the recently played list
1049
1050 Arguments:
1051 q -- dictionary of new queue entry"""
1052 return True
1053
1054 def recent_removed(self, id):
1055 """Called when a track is removed from the recently played list
1056
1057 Arguments:
1058 id -- ID of removed entry (always the oldest)"""
1059 return True
1060
1061 def removed(self, id, user):
1062 """Called when a track is removed from the queue, either manually
1063 or in order to play it.
1064
1065 Arguments:
1066 id -- ID of removed entry
1067 user -- user responsible (or None if we're playing this track)"""
1068 return True
1069
1070 def scratched(self, track, user):
1071 """Called when a track is scratched
1072
1073 Arguments:
1074 track -- track that was scratched
1075 user -- user responsible"""
1076 return True
1077
1078 def invalid(self, line):
1079 """Called when an event log line cannot be interpreted
1080
1081 Arguments:
1082 line -- line that could not be understood"""
1083 return True
1084
1085# Local Variables:
1086# mode:python
1087# py-indent-offset:2
1088# comment-column:40
1089# fill-column:72
1090# End: