chiark / gitweb /
disorder.py:
[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()
f5eb2aff 234 d[str(k)] = v
460b9539 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"
f5eb2aff
RK
297 if os.path.exists(_configfile):
298 self._readfile(_configfile)
460b9539 299 if os.path.exists(privconf):
300 self._readfile(privconf)
eee9d4b3 301 if os.path.exists(passfile) and _userconf:
460b9539 302 self._readfile(passfile)
303 self.state = 'disconnected'
304
305 def debug(self, bits):
306 """Enable or disable protocol debugging. Debug messages are written
307 to sys.stderr.
308
309 Arguments:
310 bits -- bitmap of operations that should generate debug information
311
312 Bitmap values:
313 debug_proto -- dump control protocol messages (excluding bodies)
314 debug_body -- dump control protocol message bodies
315 """
316 self.debugging = bits
317
318 def _debug(self, bit, s):
319 # debug output
320 if self.debugging & bit:
321 sys.stderr.write(_sanitize(s))
322 sys.stderr.write("\n")
323 sys.stderr.flush()
324
325 def connect(self):
326 """Connect to the DisOrder server and authenticate.
327
328 Raises communicationError if connection fails and operationError if
329 authentication fails (in which case disconnection is automatic).
330
331 May be called more than once to retry connections (e.g. when the
332 server is down). If we are already connected and authenticated,
333 this is a no-op.
334
335 Other operations automatically connect if we're not already
336 connected, so it is not strictly necessary to call this method.
337 """
338 if self.state == 'disconnected':
339 try:
340 self.state = 'connecting'
341 if 'connect' in self.config and len(self.config['connect']) > 0:
342 c = self.config['connect']
343 self.who = repr(c) # temporarily
344 if len(c) == 1:
345 a = socket.getaddrinfo(None, c[0],
346 socket.AF_INET,
347 socket.SOCK_STREAM,
348 0,
349 0)
350 else:
351 a = socket.getaddrinfo(c[0], c[1],
352 socket.AF_INET,
353 socket.SOCK_STREAM,
354 0,
355 0)
356 a = a[0]
357 s = socket.socket(a[0], a[1], a[2]);
358 s.connect(a[4])
359 self.who = "%s" % a[3]
360 else:
361 s = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM);
362 self.who = self.config['home'] + os.sep + "socket"
363 s.connect(self.who)
364 self.w = s.makefile("wb")
365 self.r = s.makefile("rb")
366 (res, challenge) = self._simple()
367 h = sha.sha()
368 h.update(self.config['password'])
369 h.update(binascii.unhexlify(challenge))
370 self._simple("user", self.config['username'], h.hexdigest())
371 self.state = 'connected'
372 except socket.error, e:
373 self._disconnect()
374 raise communicationError(self.who, e)
375 except:
376 self._disconnect()
377 raise
378
379 def _disconnect(self):
380 # disconnect from the server, whatever state we are in
381 try:
382 del self.w
383 del self.r
384 except:
385 pass
386 self.state = 'disconnected'
387
388 ########################################################################
389 # Operations
390
391 def become(self, who):
392 """Become another user.
393
394 Arguments:
395 who -- the user to become.
396
397 Only trusted users can perform this operation.
398 """
399 self._simple("become", who)
400
401 def play(self, track):
402 """Play a track.
403
404 Arguments:
405 track -- the path of the track to play.
406 """
407 self._simple("play", track)
408
409 def remove(self, track):
410 """Remove a track from the queue.
411
412 Arguments:
413 track -- the path or ID of the track to remove.
414 """
415 self._simple("remove", track)
416
417 def enable(self):
418 """Enable playing."""
419 self._simple("enable")
420
421 def disable(self, *now):
422 """Disable playing.
423
424 Arguments:
425 now -- if present (with any value), the current track is stopped
426 too.
427 """
428 if now:
429 self._simple("disable", "now")
430 else:
431 self._simple("disable")
432
433 def scratch(self, *id):
434 """Scratch the currently playing track.
435
436 Arguments:
437 id -- if present, the ID of the track to scratch.
438 """
439 if id:
440 self._simple("scratch", id[0])
441 else:
442 self._simple("scratch")
443
444 def shutdown(self):
445 """Shut down the server.
446
447 Only trusted users can perform this operation.
448 """
449 self._simple("shutdown")
450
451 def reconfigure(self):
452 """Make the server reload its configuration.
453
454 Only trusted users can perform this operation.
455 """
456 self._simple("reconfigure")
457
458 def rescan(self, pattern):
459 """Rescan one or more collections.
460
461 Arguments:
462 pattern -- glob pattern matching collections to rescan.
463
464 Only trusted users can perform this operation.
465 """
466 self._simple("rescan", pattern)
467
468 def version(self):
469 """Return the server's version number."""
470 return self._simple("version")[1]
471
472 def playing(self):
473 """Return the currently playing track.
474
475 If a track is playing then it is returned as a dictionary.
476 If no track is playing then None is returned."""
477 res, details = self._simple("playing")
478 if res % 10 != 9:
479 try:
480 return _queueEntry(details)
481 except _splitError, s:
482 raise protocolError(self.who, s.str())
483 else:
484 return None
485
486 def _somequeue(self, command):
487 self._simple(command)
488 try:
489 return map(lambda s: _queueEntry(s), self._body())
490 except _splitError, s:
491 raise protocolError(self.who, s.str())
492
493 def recent(self):
494 """Return a list of recently played tracks.
495
496 The return value is a list of dictionaries corresponding to
497 recently played tracks. The oldest track comes first."""
498 return self._somequeue("recent")
499
500 def queue(self):
501 """Return the current queue.
502
503 The return value is a list of dictionaries corresponding to
504 recently played tracks. The next track to be played comes first."""
505 return self._somequeue("queue")
506
507 def _somedir(self, command, dir, re):
508 if re:
509 self._simple(command, dir, re[0])
510 else:
511 self._simple(command, dir)
512 return self._body()
513
514 def directories(self, dir, *re):
515 """List subdirectories of a directory.
516
517 Arguments:
518 dir -- directory to list, or '' for the whole root.
519 re -- regexp that results must match. Optional.
520
521 The return value is a list of the (nonempty) subdirectories of dir.
522 If dir is '' then a list of top-level directories is returned.
523
524 If a regexp is specified then the basename of each result must
525 match. Matching is case-independent. See pcrepattern(3).
526 """
527 return self._somedir("dirs", dir, re)
528
529 def files(self, dir, *re):
530 """List files within a directory.
531
532 Arguments:
533 dir -- directory to list, or '' for the whole root.
534 re -- regexp that results must match. Optional.
535
536 The return value is a list of playable files in dir. If dir is ''
537 then a list of top-level files is returned.
538
539 If a regexp is specified then the basename of each result must
540 match. Matching is case-independent. See pcrepattern(3).
541 """
542 return self._somedir("files", dir, re)
543
544 def allfiles(self, dir, *re):
545 """List subdirectories and files within a directory.
546
547 Arguments:
548 dir -- directory to list, or '' for the whole root.
549 re -- regexp that results must match. Optional.
550
551 The return value is a list of all (nonempty) subdirectories and
552 files within dir. If dir is '' then a list of top-level files and
553 directories is returned.
554
555 If a regexp is specified then the basename of each result must
556 match. Matching is case-independent. See pcrepattern(3).
557 """
558 return self._somedir("allfiles", dir, re)
559
560 def set(self, track, key, value):
561 """Set a preference value.
562
563 Arguments:
564 track -- the track to modify
565 key -- the preference name
566 value -- the new preference value
567 """
568 self._simple("set", track, key, value)
569
570 def unset(self, track, key):
571 """Unset a preference value.
572
573 Arguments:
574 track -- the track to modify
575 key -- the preference to remove
576 """
577 self._simple("set", track, key, value)
578
579 def get(self, track, key):
580 """Get a preference value.
581
582 Arguments:
583 track -- the track to query
584 key -- the preference to remove
585
586 The return value is the preference
587 """
588 ret, details = self._simple("get", track, key)
fb1bc1f5
RK
589 if ret == 555:
590 return None
591 else:
592 return details
460b9539 593
594 def prefs(self, track):
595 """Get all the preferences for a track.
596
597 Arguments:
598 track -- the track to query
599
600 The return value is a dictionary of all the track's preferences.
601 Note that even nominally numeric values remain encoded as strings.
602 """
603 self._simple("prefs", track)
604 r = {}
605 for line in self._body():
606 try:
607 kv = _split(line)
608 except _splitError, s:
609 raise protocolError(self.who, s.str())
610 if len(kv) != 2:
611 raise protocolError(self.who, "invalid prefs body line")
612 r[kv[0]] = kv[1]
613 return r
614
615 def _boolean(self, s):
616 return s[1] == 'yes'
617
618 def exists(self, track):
619 """Return true if a track exists
620
621 Arguments:
622 track -- the track to check for"""
623 return self._boolean(self._simple("exists", track))
624
625 def enabled(self):
626 """Return true if playing is enabled"""
627 return self._boolean(self._simple("enabled"))
628
629 def random_enabled(self):
630 """Return true if random play is enabled"""
631 return self._boolean(self._simple("random-enabled"))
632
633 def random_enable(self):
634 """Enable random play."""
635 self._simple("random-enable")
636
637 def random_disable(self):
638 """Disable random play."""
639 self._simple("random-disable")
640
641 def length(self, track):
642 """Return the length of a track in seconds.
643
644 Arguments:
645 track -- the track to query.
646 """
647 ret, details = self._simple("length", track)
648 return int(details)
649
650 def search(self, words):
651 """Search for tracks.
652
653 Arguments:
654 words -- the set of words to search for.
655
656 The return value is a list of track path names, all of which contain
657 all of the required words (in their path name, trackname
658 preferences, etc.)
659 """
f383b2f1 660 self._simple("search", _quote(words))
460b9539 661 return self._body()
662
31773020
RK
663 def tags(self):
664 """List all tags
665
666 The return value is a list of all tags which apply to at least one
667 track."""
668 self._simple("tags")
669 return self._body()
670
460b9539 671 def stats(self):
672 """Get server statistics.
673
674 The return value is list of statistics.
675 """
676 self._simple("stats")
677 return self._body()
678
679 def dump(self):
680 """Get all preferences.
681
682 The return value is an encoded dump of the preferences database.
683 """
684 self._simple("dump")
685 return self._body()
686
687 def set_volume(self, left, right):
688 """Set volume.
689
690 Arguments:
691 left -- volume for the left speaker.
692 right -- volume for the right speaker.
693 """
694 self._simple("volume", left, right)
695
696 def get_volume(self):
697 """Get volume.
698
699 The return value a tuple consisting of the left and right volumes.
700 """
701 ret, details = self._simple("volume")
702 return map(int,string.split(details))
703
704 def move(self, track, delta):
705 """Move a track in the queue.
706
707 Arguments:
708 track -- the name or ID of the track to move
709 delta -- the number of steps towards the head of the queue to move
710 """
711 ret, details = self._simple("move", track, str(delta))
712 return int(details)
713
714 def log(self, callback):
715 """Read event log entries as they happen.
716
717 Each event log entry is handled by passing it to callback.
718
719 The callback takes two arguments, the first is the client and the
720 second the line from the event log.
721
722 The callback should return True to continue or False to stop (don't
723 forget this, or your program will mysteriously misbehave).
724
725 It is suggested that you use the disorder.monitor class instead of
726 calling this method directly, but this is not mandatory.
727
728 See disorder_protocol(5) for the event log syntax.
729
730 Arguments:
731 callback -- function to call with log entry
732 """
733 ret, details = self._simple("log")
734 while True:
735 l = self._line()
736 self._debug(client.debug_body, "<<< %s" % l)
737 if l != '' and l[0] == '.':
738 if l == '.':
739 return
740 l = l[1:]
741 if not callback(self, l):
742 break
743 # tell the server to stop sending, eat the remains of the body,
744 # eat the response
745 self._send("version")
746 self._body()
747 self._response()
748
749 def pause(self):
750 """Pause the current track."""
751 self._simple("pause")
752
753 def resume(self):
754 """Resume after a pause."""
755 self._simple("resume")
756
757 def part(self, track, context, part):
758 """Get a track name part
759
760 Arguments:
761 track -- the track to query
762 context -- the context ('sort' or 'display')
763 part -- the desired part (usually 'artist', 'album' or 'title')
764
765 The return value is the preference
766 """
767 ret, details = self._simple("part", track, context, part)
768 return details
769
f35e5800
RK
770 def setglobal(self, key, value):
771 """Set a global preference value.
772
773 Arguments:
774 key -- the preference name
775 value -- the new preference value
776 """
777 self._simple("set-global", key, value)
778
779 def unsetglobal(self, key):
780 """Unset a global preference value.
781
782 Arguments:
783 key -- the preference to remove
784 """
785 self._simple("set-global", key, value)
786
787 def getglobal(self, key):
788 """Get a global preference value.
789
790 Arguments:
791 key -- the preference to look up
792
793 The return value is the preference
794 """
795 ret, details = self._simple("get-global", key)
fb1bc1f5
RK
796 if ret == 555:
797 return None
798 else:
799 return details
f35e5800 800
460b9539 801 ########################################################################
802 # I/O infrastructure
803
804 def _line(self):
805 # read one response line and return as some suitable string object
806 #
807 # If an I/O error occurs, disconnect from the server.
808 #
809 # XXX does readline() DTRT regarding character encodings?
810 try:
811 l = self.r.readline()
812 if not re.search("\n", l):
813 raise communicationError(self.who, "peer disconnected")
814 l = l[:-1]
815 except:
816 self._disconnect()
817 raise
818 return unicode(l, "UTF-8")
819
820 def _response(self):
821 # read a response as a (code, details) tuple
822 l = self._line()
823 self._debug(client.debug_proto, "<== %s" % l)
824 m = _response.match(l)
825 if m:
826 return int(m.group(1)), m.group(2)
827 else:
828 raise protocolError(self.who, "invalid response %s")
829
830 def _send(self, *command):
f383b2f1
RK
831 # Quote and send a command
832 #
833 # Returns the encoded command.
460b9539 834 quoted = _quote(command)
835 self._debug(client.debug_proto, "==> %s" % quoted)
836 encoded = quoted.encode("UTF-8")
837 try:
838 self.w.write(encoded)
839 self.w.write("\n")
840 self.w.flush()
f383b2f1 841 return encoded
460b9539 842 except IOError, e:
843 # e.g. EPIPE
844 self._disconnect()
845 raise communicationError(self.who, e)
846 except:
847 self._disconnect()
848 raise
849
850 def _simple(self, *command):
851 # Issue a simple command, throw an exception on error
852 #
853 # If an I/O error occurs, disconnect from the server.
854 #
fb1bc1f5 855 # On success or 'normal' errors returns response as a (code, details) tuple
460b9539 856 #
857 # On error raise operationError
858 if self.state == 'disconnected':
859 self.connect()
860 if command:
f383b2f1
RK
861 cmd = self._send(*command)
862 else:
863 cmd = None
460b9539 864 res, details = self._response()
fb1bc1f5 865 if res / 100 == 2 or res == 555:
460b9539 866 return res, details
f383b2f1 867 raise operationError(res, details, cmd)
460b9539 868
869 def _body(self):
870 # Fetch a dot-stuffed body
871 result = []
872 while True:
873 l = self._line()
874 self._debug(client.debug_body, "<<< %s" % l)
875 if l != '' and l[0] == '.':
876 if l == '.':
877 return result
878 l = l[1:]
879 result.append(l)
880
881 ########################################################################
882 # Configuration file parsing
883
884 def _readfile(self, path):
885 # Read a configuration file
886 #
887 # Arguments:
888 #
889 # path -- path of file to read
890
891 # handlers for various commands
892 def _collection(self, command, args):
893 if len(args) != 3:
894 return "'%s' takes three args" % command
895 self.config["collections"].append(args)
896
897 def _unary(self, command, args):
898 if len(args) != 1:
899 return "'%s' takes only one arg" % command
900 self.config[command] = args[0]
901
902 def _include(self, command, args):
903 if len(args) != 1:
904 return "'%s' takes only one arg" % command
905 self._readfile(args[0])
906
907 def _any(self, command, args):
908 self.config[command] = args
909
910 # mapping of options to handlers
911 _options = { "collection": _collection,
912 "username": _unary,
913 "password": _unary,
914 "home": _unary,
915 "connect": _any,
916 "include": _include }
917
918 # the parser
919 for lno, line in enumerate(file(path, "r")):
920 try:
921 fields = _split(line, 'comments')
922 except _splitError, s:
923 raise parseError(path, lno + 1, str(s))
924 if fields:
925 command = fields[0]
926 # we just ignore options we don't know about, so as to cope gracefully
927 # with version skew (and nothing to do with implementor laziness)
928 if command in _options:
929 e = _options[command](self, command, fields[1:])
930 if e:
931 self._parseError(path, lno + 1, e)
932
933 def _parseError(self, path, lno, s):
934 raise parseError(path, lno, s)
935
936########################################################################
937# monitor class
938
939class monitor:
940 """DisOrder event log monitor class
941
942 Intended to be subclassed with methods corresponding to event log messages
943 the implementor cares about over-ridden."""
944
945 def __init__(self, c=None):
946 """Constructor for the monitor class
947
948 Can be passed a client to use. If none is specified then one
949 will be created specially for the purpose.
950
951 Arguments:
952 c -- client"""
953 if c == None:
954 c = client();
955 self.c = c
956
957 def run(self):
958 """Start monitoring logs. Continues monitoring until one of the
959 message-specific methods returns False. Can be called more than once
960 (but not recursively!)"""
961 self.c.log(self._callback)
962
963 def when(self):
964 """Return the timestamp of the current (or most recent) event log entry"""
965 return self.timestamp
966
967 def _callback(self, c, line):
968 try:
969 bits = _split(line)
970 except:
971 return self.invalid(line)
972 if(len(bits) < 2):
973 return self.invalid(line)
974 self.timestamp = int(bits[0], 16)
975 keyword = bits[1]
976 bits = bits[2:]
977 if keyword == 'completed':
978 if len(bits) == 1:
979 return self.completed(bits[0])
980 elif keyword == 'failed':
981 if len(bits) == 2:
982 return self.failed(bits[0], bits[1])
983 elif keyword == 'moved':
984 if len(bits) == 3:
985 try:
986 n = int(bits[1])
987 except:
988 return self.invalid(line)
989 return self.moved(bits[0], n, bits[2])
990 elif keyword == 'playing':
991 if len(bits) == 1:
992 return self.playing(bits[0], None)
993 elif len(bits) == 2:
994 return self.playing(bits[0], bits[1])
995 elif keyword == 'queue' or keyword == 'recent-added':
996 try:
997 q = _list2dict(bits)
998 except:
999 return self.invalid(line)
1000 if keyword == 'queue':
1001 return self.queue(q)
1002 if keyword == 'recent-added':
1003 return self.recent_added(q)
1004 elif keyword == 'recent-removed':
1005 if len(bits) == 1:
1006 return self.recent_removed(bits[0])
1007 elif keyword == 'removed':
1008 if len(bits) == 1:
1009 return self.removed(bits[0], None)
1010 elif len(bits) == 2:
1011 return self.removed(bits[0], bits[1])
1012 elif keyword == 'scratched':
1013 if len(bits) == 2:
1014 return self.scratched(bits[0], bits[1])
1015 return self.invalid(line)
1016
1017 def completed(self, track):
1018 """Called when a track completes.
1019
1020 Arguments:
1021 track -- track that completed"""
1022 return True
1023
1024 def failed(self, track, error):
1025 """Called when a player suffers an error.
1026
1027 Arguments:
1028 track -- track that failed
1029 error -- error indicator"""
1030 return True
1031
1032 def moved(self, id, offset, user):
1033 """Called when a track is moved in the queue.
1034
1035 Arguments:
1036 id -- queue entry ID
1037 offset -- distance moved
1038 user -- user responsible"""
1039 return True
1040
1041 def playing(self, track, user):
1042 """Called when a track starts playing.
1043
1044 Arguments:
1045 track -- track that has started
1046 user -- user that submitted track, or None"""
1047 return True
1048
1049 def queue(self, q):
1050 """Called when a track is added to the queue.
1051
1052 Arguments:
1053 q -- dictionary of new queue entry"""
1054 return True
1055
1056 def recent_added(self, q):
1057 """Called when a track is added to the recently played list
1058
1059 Arguments:
1060 q -- dictionary of new queue entry"""
1061 return True
1062
1063 def recent_removed(self, id):
1064 """Called when a track is removed from the recently played list
1065
1066 Arguments:
1067 id -- ID of removed entry (always the oldest)"""
1068 return True
1069
1070 def removed(self, id, user):
1071 """Called when a track is removed from the queue, either manually
1072 or in order to play it.
1073
1074 Arguments:
1075 id -- ID of removed entry
1076 user -- user responsible (or None if we're playing this track)"""
1077 return True
1078
1079 def scratched(self, track, user):
1080 """Called when a track is scratched
1081
1082 Arguments:
1083 track -- track that was scratched
1084 user -- user responsible"""
1085 return True
1086
1087 def invalid(self, line):
1088 """Called when an event log line cannot be interpreted
1089
1090 Arguments:
1091 line -- line that could not be understood"""
1092 return True
1093
1094# Local Variables:
1095# mode:python
1096# py-indent-offset:2
1097# comment-column:40
1098# fill-column:72
1099# End: