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