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