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