chiark / gitweb /
fixes and test for disorder-dump
[disorder] / python / disorder.py.in
CommitLineData
460b9539 1#
2# Copyright (C) 2004, 2005 Richard Kettlewell
3#
4# This program is free software; you can redistribute it and/or modify
5# it under the terms of the GNU General Public License as published by
6# the Free Software Foundation; either version 2 of the License, or
7# (at your option) any later version.
8#
9# This program is distributed in the hope that it will be useful, but
10# WITHOUT ANY WARRANTY; without even the implied warranty of
11# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
12# General Public License for more details.
13#
14# You should have received a copy of the GNU General Public License
15# along with this program; if not, write to the Free Software
16# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
17# USA
18#
19
20"""Python support for DisOrder
21
22Provides disorder.client, a class for accessing a DisOrder server.
23
24Example 1:
25
26 #! /usr/bin/env python
27 import disorder
28 d = disorder.client()
29 p = d.playing()
30 if p:
31 print p['track']
32
33Example 2:
34
35 #! /usr/bin/env python
36 import disorder
37 import sys
38 d = disorder.client()
39 for path in sys.argv[1:]:
40 d.play(path)
41
42"""
43
44import re
45import string
46import os
47import pwd
48import socket
49import binascii
50import sha
51import sys
52import locale
53
54_configfile = "pkgconfdir/config"
55_dbhome = "pkgstatedir"
eee9d4b3 56_userconf = True
460b9539 57
58# various regexps we'll use
59_ws = re.compile(r"^[ \t\n\r]+")
60_squote = re.compile("'(([^\\\\']|\\\\[\\\\\"'n])+)'")
61_dquote = re.compile("\"(([^\\\\\"]|\\\\[\\\\\"'n])+)\"")
62_unquoted = re.compile("[^\"' \\t\\n\\r][^ \t\n\r]*")
63
64_response = re.compile("([0-9]{3}) ?(.*)")
65
66version = "_version_"
67
68########################################################################
69# exception classes
70
71class Error(Exception):
72 """Base class for DisOrder exceptions."""
73
74class _splitError(Error):
75 # _split failed
76 def __init__(self, value):
77 self.value = value
78 def __str__(self):
79 return str(self.value)
80
81class parseError(Error):
82 """Error parsing the configuration file."""
83 def __init__(self, path, line, details):
84 self.path = path
85 self.line = line
86 self.details = details
87 def __str__(self):
88 return "%s:%d: %s" % (self.path, self.line, self.details)
89
90class protocolError(Error):
91 """DisOrder control protocol error.
92
93 Indicates a mismatch between the client and server's understanding of
94 the control protocol.
95 """
96 def __init__(self, who, error):
97 self.who = who
98 self.error = error
99 def __str__(self):
100 return "%s: %s" % (self.who, str(self.error))
101
102class operationError(Error):
103 """DisOrder control protocol error response.
104
105 Indicates that an operation failed (e.g. an attempt to play a
106 nonexistent track). The connection should still be usable.
107 """
f383b2f1 108 def __init__(self, res, details, cmd=None):
460b9539 109 self.res_ = int(res)
f383b2f1 110 self.cmd_ = cmd
460b9539 111 self.details_ = details
112 def __str__(self):
f383b2f1
RK
113 """Return the complete response string from the server, with the command
114 if available.
460b9539 115
116 Excludes the final newline.
117 """
f383b2f1
RK
118 if self.cmd_ is None:
119 return "%d %s" % (self.res_, self.details_)
120 else:
121 return "%d %s [%s]" % (self.res_, self.details_, self.cmd_)
460b9539 122 def response(self):
123 """Return the response code from the server."""
124 return self.res_
125 def details(self):
126 """Returns the detail string from the server."""
127 return self.details_
128
129class communicationError(Error):
130 """DisOrder control protocol communication error.
131
132 Indicates that communication with the server went wrong, perhaps
133 because the server was restarted. The caller could report an error to
134 the user and wait for further user instructions, or even automatically
135 retry the operation.
136 """
137 def __init__(self, who, error):
138 self.who = who
139 self.error = error
140 def __str__(self):
141 return "%s: %s" % (self.who, str(self.error))
142
143########################################################################
144# DisOrder-specific text processing
145
146def _unescape(s):
147 # Unescape the contents of a string
148 #
149 # Arguments:
150 #
151 # s -- string to unescape
152 #
153 s = re.sub("\\\\n", "\n", s)
154 s = re.sub("\\\\(.)", "\\1", s)
155 return s
156
157def _split(s, *comments):
158 # Split a string into fields according to the usual Disorder string splitting
159 # conventions.
160 #
161 # Arguments:
162 #
163 # s -- string to parse
164 # comments -- if present, parse comments
165 #
166 # Return values:
167 #
168 # On success, a list of fields is returned.
169 #
170 # On error, disorder.parseError is thrown.
171 #
172 fields = []
173 while s != "":
174 # discard comments
175 if comments and s[0] == '#':
176 break
177 # strip spaces
178 m = _ws.match(s)
179 if m:
180 s = s[m.end():]
181 continue
182 # pick of quoted fields of both kinds
183 m = _squote.match(s)
184 if not m:
185 m = _dquote.match(s)
186 if m:
187 fields.append(_unescape(m.group(1)))
188 s = s[m.end():]
189 continue
190 # and unquoted fields
191 m = _unquoted.match(s)
192 if m:
193 fields.append(m.group(0))
194 s = s[m.end():]
195 continue
196 # anything left must be in error
197 if s[0] == '"' or s[0] == '\'':
198 raise _splitError("invalid quoted string")
199 else:
200 raise _splitError("syntax error")
201 return fields
202
203def _escape(s):
204 # Escape the contents of a string
205 #
206 # Arguments:
207 #
208 # s -- string to escape
209 #
210 if re.search("[\\\\\"'\n \t\r]", s) or s == '':
211 s = re.sub(r'[\\"]', r'\\\g<0>', s)
212 s = re.sub("\n", r"\\n", s)
213 return '"' + s + '"'
214 else:
215 return s
216
217def _quote(list):
218 # Quote a list of values
219 return ' '.join(map(_escape, list))
220
221def _sanitize(s):
222 # Return the value of s in a form suitable for writing to stderr
223 return s.encode(locale.nl_langinfo(locale.CODESET), 'replace')
224
225def _list2dict(l):
226 # Convert a list of the form [k1, v1, k2, v2, ..., kN, vN]
227 # to a dictionary {k1:v1, k2:v2, ..., kN:vN}
228 d = {}
229 i = iter(l)
230 try:
231 while True:
232 k = i.next()
233 v = i.next()
234 d[k] = v
235 except StopIteration:
236 pass
237 return d
238
239def _queueEntry(s):
240 # parse a queue entry
241 return _list2dict(_split(s))
242
243########################################################################
244# The client class
245
246class client:
247 """DisOrder client class.
248
249 This class provides access to the DisOrder server either on this
250 machine or across the internet.
251
252 The server to connect to, and the username and password to use, are
253 determined from the configuration files as described in 'man
254 disorder_config'.
255
256 All methods will connect if necessary, as soon as you have a
257 disorder.client object you can start calling operational methods on
258 it.
259
260 However if the server is restarted then the next method called on a
261 connection will throw an exception. This may be considered a bug.
262
263 All methods block until they complete.
264
265 Operation methods raise communicationError if the connection breaks,
266 protocolError if the response from the server is malformed, or
267 operationError if the response is valid but indicates that the
268 operation failed.
269 """
270
271 debug_proto = 0x0001
272 debug_body = 0x0002
273
274 def __init__(self):
275 """Constructor for DisOrder client class.
276
277 The constructor reads the configuration file, but does not connect
278 to the server.
279
280 If the environment variable DISORDER_PYTHON_DEBUG is set then the
281 debug flags are initialised to that value. This can be overridden
282 with the debug() method below.
283
284 The constructor Raises parseError() if the configuration file is not
285 valid.
286 """
287 pw = pwd.getpwuid(os.getuid())
288 self.debugging = int(os.getenv("DISORDER_PYTHON_DEBUG", 0))
289 self.config = { 'collections': [],
290 'username': pw.pw_name,
291 'home': _dbhome }
292 home = os.getenv("HOME")
293 if not home:
294 home = pw.pw_dir
295 privconf = _configfile + "." + pw.pw_name
296 passfile = home + os.sep + ".disorder" + os.sep + "passwd"
297 self._readfile(_configfile)
298 if os.path.exists(privconf):
299 self._readfile(privconf)
eee9d4b3 300 if os.path.exists(passfile) and _userconf:
460b9539 301 self._readfile(passfile)
302 self.state = 'disconnected'
303
304 def debug(self, bits):
305 """Enable or disable protocol debugging. Debug messages are written
306 to sys.stderr.
307
308 Arguments:
309 bits -- bitmap of operations that should generate debug information
310
311 Bitmap values:
312 debug_proto -- dump control protocol messages (excluding bodies)
313 debug_body -- dump control protocol message bodies
314 """
315 self.debugging = bits
316
317 def _debug(self, bit, s):
318 # debug output
319 if self.debugging & bit:
320 sys.stderr.write(_sanitize(s))
321 sys.stderr.write("\n")
322 sys.stderr.flush()
323
324 def connect(self):
325 """Connect to the DisOrder server and authenticate.
326
327 Raises communicationError if connection fails and operationError if
328 authentication fails (in which case disconnection is automatic).
329
330 May be called more than once to retry connections (e.g. when the
331 server is down). If we are already connected and authenticated,
332 this is a no-op.
333
334 Other operations automatically connect if we're not already
335 connected, so it is not strictly necessary to call this method.
336 """
337 if self.state == 'disconnected':
338 try:
339 self.state = 'connecting'
340 if 'connect' in self.config and len(self.config['connect']) > 0:
341 c = self.config['connect']
342 self.who = repr(c) # temporarily
343 if len(c) == 1:
344 a = socket.getaddrinfo(None, c[0],
345 socket.AF_INET,
346 socket.SOCK_STREAM,
347 0,
348 0)
349 else:
350 a = socket.getaddrinfo(c[0], c[1],
351 socket.AF_INET,
352 socket.SOCK_STREAM,
353 0,
354 0)
355 a = a[0]
356 s = socket.socket(a[0], a[1], a[2]);
357 s.connect(a[4])
358 self.who = "%s" % a[3]
359 else:
360 s = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM);
361 self.who = self.config['home'] + os.sep + "socket"
362 s.connect(self.who)
363 self.w = s.makefile("wb")
364 self.r = s.makefile("rb")
365 (res, challenge) = self._simple()
366 h = sha.sha()
367 h.update(self.config['password'])
368 h.update(binascii.unhexlify(challenge))
369 self._simple("user", self.config['username'], h.hexdigest())
370 self.state = 'connected'
371 except socket.error, e:
372 self._disconnect()
373 raise communicationError(self.who, e)
374 except:
375 self._disconnect()
376 raise
377
378 def _disconnect(self):
379 # disconnect from the server, whatever state we are in
380 try:
381 del self.w
382 del self.r
383 except:
384 pass
385 self.state = 'disconnected'
386
387 ########################################################################
388 # Operations
389
390 def become(self, who):
391 """Become another user.
392
393 Arguments:
394 who -- the user to become.
395
396 Only trusted users can perform this operation.
397 """
398 self._simple("become", who)
399
400 def play(self, track):
401 """Play a track.
402
403 Arguments:
404 track -- the path of the track to play.
405 """
406 self._simple("play", track)
407
408 def remove(self, track):
409 """Remove a track from the queue.
410
411 Arguments:
412 track -- the path or ID of the track to remove.
413 """
414 self._simple("remove", track)
415
416 def enable(self):
417 """Enable playing."""
418 self._simple("enable")
419
420 def disable(self, *now):
421 """Disable playing.
422
423 Arguments:
424 now -- if present (with any value), the current track is stopped
425 too.
426 """
427 if now:
428 self._simple("disable", "now")
429 else:
430 self._simple("disable")
431
432 def scratch(self, *id):
433 """Scratch the currently playing track.
434
435 Arguments:
436 id -- if present, the ID of the track to scratch.
437 """
438 if id:
439 self._simple("scratch", id[0])
440 else:
441 self._simple("scratch")
442
443 def shutdown(self):
444 """Shut down the server.
445
446 Only trusted users can perform this operation.
447 """
448 self._simple("shutdown")
449
450 def reconfigure(self):
451 """Make the server reload its configuration.
452
453 Only trusted users can perform this operation.
454 """
455 self._simple("reconfigure")
456
457 def rescan(self, pattern):
458 """Rescan one or more collections.
459
460 Arguments:
461 pattern -- glob pattern matching collections to rescan.
462
463 Only trusted users can perform this operation.
464 """
465 self._simple("rescan", pattern)
466
467 def version(self):
468 """Return the server's version number."""
469 return self._simple("version")[1]
470
471 def playing(self):
472 """Return the currently playing track.
473
474 If a track is playing then it is returned as a dictionary.
475 If no track is playing then None is returned."""
476 res, details = self._simple("playing")
477 if res % 10 != 9:
478 try:
479 return _queueEntry(details)
480 except _splitError, s:
481 raise protocolError(self.who, s.str())
482 else:
483 return None
484
485 def _somequeue(self, command):
486 self._simple(command)
487 try:
488 return map(lambda s: _queueEntry(s), self._body())
489 except _splitError, s:
490 raise protocolError(self.who, s.str())
491
492 def recent(self):
493 """Return a list of recently played tracks.
494
495 The return value is a list of dictionaries corresponding to
496 recently played tracks. The oldest track comes first."""
497 return self._somequeue("recent")
498
499 def queue(self):
500 """Return the current queue.
501
502 The return value is a list of dictionaries corresponding to
503 recently played tracks. The next track to be played comes first."""
504 return self._somequeue("queue")
505
506 def _somedir(self, command, dir, re):
507 if re:
508 self._simple(command, dir, re[0])
509 else:
510 self._simple(command, dir)
511 return self._body()
512
513 def directories(self, dir, *re):
514 """List subdirectories of a directory.
515
516 Arguments:
517 dir -- directory to list, or '' for the whole root.
518 re -- regexp that results must match. Optional.
519
520 The return value is a list of the (nonempty) subdirectories of dir.
521 If dir is '' then a list of top-level directories is returned.
522
523 If a regexp is specified then the basename of each result must
524 match. Matching is case-independent. See pcrepattern(3).
525 """
526 return self._somedir("dirs", dir, re)
527
528 def files(self, dir, *re):
529 """List files within a directory.
530
531 Arguments:
532 dir -- directory to list, or '' for the whole root.
533 re -- regexp that results must match. Optional.
534
535 The return value is a list of playable files in dir. If dir is ''
536 then a list of top-level files is returned.
537
538 If a regexp is specified then the basename of each result must
539 match. Matching is case-independent. See pcrepattern(3).
540 """
541 return self._somedir("files", dir, re)
542
543 def allfiles(self, dir, *re):
544 """List subdirectories and files within a directory.
545
546 Arguments:
547 dir -- directory to list, or '' for the whole root.
548 re -- regexp that results must match. Optional.
549
550 The return value is a list of all (nonempty) subdirectories and
551 files within dir. If dir is '' then a list of top-level files and
552 directories is returned.
553
554 If a regexp is specified then the basename of each result must
555 match. Matching is case-independent. See pcrepattern(3).
556 """
557 return self._somedir("allfiles", dir, re)
558
559 def set(self, track, key, value):
560 """Set a preference value.
561
562 Arguments:
563 track -- the track to modify
564 key -- the preference name
565 value -- the new preference value
566 """
567 self._simple("set", track, key, value)
568
569 def unset(self, track, key):
570 """Unset a preference value.
571
572 Arguments:
573 track -- the track to modify
574 key -- the preference to remove
575 """
576 self._simple("set", track, key, value)
577
578 def get(self, track, key):
579 """Get a preference value.
580
581 Arguments:
582 track -- the track to query
583 key -- the preference to remove
584
585 The return value is the preference
586 """
587 ret, details = self._simple("get", track, key)
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
f35e5800
RK
758 def setglobal(self, key, value):
759 """Set a global preference value.
760
761 Arguments:
762 key -- the preference name
763 value -- the new preference value
764 """
765 self._simple("set-global", key, value)
766
767 def unsetglobal(self, key):
768 """Unset a global preference value.
769
770 Arguments:
771 key -- the preference to remove
772 """
773 self._simple("set-global", key, value)
774
775 def getglobal(self, key):
776 """Get a global preference value.
777
778 Arguments:
779 key -- the preference to look up
780
781 The return value is the preference
782 """
783 ret, details = self._simple("get-global", key)
784 return details
785
460b9539 786 ########################################################################
787 # I/O infrastructure
788
789 def _line(self):
790 # read one response line and return as some suitable string object
791 #
792 # If an I/O error occurs, disconnect from the server.
793 #
794 # XXX does readline() DTRT regarding character encodings?
795 try:
796 l = self.r.readline()
797 if not re.search("\n", l):
798 raise communicationError(self.who, "peer disconnected")
799 l = l[:-1]
800 except:
801 self._disconnect()
802 raise
803 return unicode(l, "UTF-8")
804
805 def _response(self):
806 # read a response as a (code, details) tuple
807 l = self._line()
808 self._debug(client.debug_proto, "<== %s" % l)
809 m = _response.match(l)
810 if m:
811 return int(m.group(1)), m.group(2)
812 else:
813 raise protocolError(self.who, "invalid response %s")
814
815 def _send(self, *command):
f383b2f1
RK
816 # Quote and send a command
817 #
818 # Returns the encoded command.
460b9539 819 quoted = _quote(command)
820 self._debug(client.debug_proto, "==> %s" % quoted)
821 encoded = quoted.encode("UTF-8")
822 try:
823 self.w.write(encoded)
824 self.w.write("\n")
825 self.w.flush()
f383b2f1 826 return encoded
460b9539 827 except IOError, e:
828 # e.g. EPIPE
829 self._disconnect()
830 raise communicationError(self.who, e)
831 except:
832 self._disconnect()
833 raise
834
835 def _simple(self, *command):
836 # Issue a simple command, throw an exception on error
837 #
838 # If an I/O error occurs, disconnect from the server.
839 #
840 # On success returns response as a (code, details) tuple
841 #
842 # On error raise operationError
843 if self.state == 'disconnected':
844 self.connect()
845 if command:
f383b2f1
RK
846 cmd = self._send(*command)
847 else:
848 cmd = None
460b9539 849 res, details = self._response()
850 if res / 100 == 2:
851 return res, details
f383b2f1 852 raise operationError(res, details, cmd)
460b9539 853
854 def _body(self):
855 # Fetch a dot-stuffed body
856 result = []
857 while True:
858 l = self._line()
859 self._debug(client.debug_body, "<<< %s" % l)
860 if l != '' and l[0] == '.':
861 if l == '.':
862 return result
863 l = l[1:]
864 result.append(l)
865
866 ########################################################################
867 # Configuration file parsing
868
869 def _readfile(self, path):
870 # Read a configuration file
871 #
872 # Arguments:
873 #
874 # path -- path of file to read
875
876 # handlers for various commands
877 def _collection(self, command, args):
878 if len(args) != 3:
879 return "'%s' takes three args" % command
880 self.config["collections"].append(args)
881
882 def _unary(self, command, args):
883 if len(args) != 1:
884 return "'%s' takes only one arg" % command
885 self.config[command] = args[0]
886
887 def _include(self, command, args):
888 if len(args) != 1:
889 return "'%s' takes only one arg" % command
890 self._readfile(args[0])
891
892 def _any(self, command, args):
893 self.config[command] = args
894
895 # mapping of options to handlers
896 _options = { "collection": _collection,
897 "username": _unary,
898 "password": _unary,
899 "home": _unary,
900 "connect": _any,
901 "include": _include }
902
903 # the parser
904 for lno, line in enumerate(file(path, "r")):
905 try:
906 fields = _split(line, 'comments')
907 except _splitError, s:
908 raise parseError(path, lno + 1, str(s))
909 if fields:
910 command = fields[0]
911 # we just ignore options we don't know about, so as to cope gracefully
912 # with version skew (and nothing to do with implementor laziness)
913 if command in _options:
914 e = _options[command](self, command, fields[1:])
915 if e:
916 self._parseError(path, lno + 1, e)
917
918 def _parseError(self, path, lno, s):
919 raise parseError(path, lno, s)
920
921########################################################################
922# monitor class
923
924class monitor:
925 """DisOrder event log monitor class
926
927 Intended to be subclassed with methods corresponding to event log messages
928 the implementor cares about over-ridden."""
929
930 def __init__(self, c=None):
931 """Constructor for the monitor class
932
933 Can be passed a client to use. If none is specified then one
934 will be created specially for the purpose.
935
936 Arguments:
937 c -- client"""
938 if c == None:
939 c = client();
940 self.c = c
941
942 def run(self):
943 """Start monitoring logs. Continues monitoring until one of the
944 message-specific methods returns False. Can be called more than once
945 (but not recursively!)"""
946 self.c.log(self._callback)
947
948 def when(self):
949 """Return the timestamp of the current (or most recent) event log entry"""
950 return self.timestamp
951
952 def _callback(self, c, line):
953 try:
954 bits = _split(line)
955 except:
956 return self.invalid(line)
957 if(len(bits) < 2):
958 return self.invalid(line)
959 self.timestamp = int(bits[0], 16)
960 keyword = bits[1]
961 bits = bits[2:]
962 if keyword == 'completed':
963 if len(bits) == 1:
964 return self.completed(bits[0])
965 elif keyword == 'failed':
966 if len(bits) == 2:
967 return self.failed(bits[0], bits[1])
968 elif keyword == 'moved':
969 if len(bits) == 3:
970 try:
971 n = int(bits[1])
972 except:
973 return self.invalid(line)
974 return self.moved(bits[0], n, bits[2])
975 elif keyword == 'playing':
976 if len(bits) == 1:
977 return self.playing(bits[0], None)
978 elif len(bits) == 2:
979 return self.playing(bits[0], bits[1])
980 elif keyword == 'queue' or keyword == 'recent-added':
981 try:
982 q = _list2dict(bits)
983 except:
984 return self.invalid(line)
985 if keyword == 'queue':
986 return self.queue(q)
987 if keyword == 'recent-added':
988 return self.recent_added(q)
989 elif keyword == 'recent-removed':
990 if len(bits) == 1:
991 return self.recent_removed(bits[0])
992 elif keyword == 'removed':
993 if len(bits) == 1:
994 return self.removed(bits[0], None)
995 elif len(bits) == 2:
996 return self.removed(bits[0], bits[1])
997 elif keyword == 'scratched':
998 if len(bits) == 2:
999 return self.scratched(bits[0], bits[1])
1000 return self.invalid(line)
1001
1002 def completed(self, track):
1003 """Called when a track completes.
1004
1005 Arguments:
1006 track -- track that completed"""
1007 return True
1008
1009 def failed(self, track, error):
1010 """Called when a player suffers an error.
1011
1012 Arguments:
1013 track -- track that failed
1014 error -- error indicator"""
1015 return True
1016
1017 def moved(self, id, offset, user):
1018 """Called when a track is moved in the queue.
1019
1020 Arguments:
1021 id -- queue entry ID
1022 offset -- distance moved
1023 user -- user responsible"""
1024 return True
1025
1026 def playing(self, track, user):
1027 """Called when a track starts playing.
1028
1029 Arguments:
1030 track -- track that has started
1031 user -- user that submitted track, or None"""
1032 return True
1033
1034 def queue(self, q):
1035 """Called when a track is added to the queue.
1036
1037 Arguments:
1038 q -- dictionary of new queue entry"""
1039 return True
1040
1041 def recent_added(self, q):
1042 """Called when a track is added to the recently played list
1043
1044 Arguments:
1045 q -- dictionary of new queue entry"""
1046 return True
1047
1048 def recent_removed(self, id):
1049 """Called when a track is removed from the recently played list
1050
1051 Arguments:
1052 id -- ID of removed entry (always the oldest)"""
1053 return True
1054
1055 def removed(self, id, user):
1056 """Called when a track is removed from the queue, either manually
1057 or in order to play it.
1058
1059 Arguments:
1060 id -- ID of removed entry
1061 user -- user responsible (or None if we're playing this track)"""
1062 return True
1063
1064 def scratched(self, track, user):
1065 """Called when a track is scratched
1066
1067 Arguments:
1068 track -- track that was scratched
1069 user -- user responsible"""
1070 return True
1071
1072 def invalid(self, line):
1073 """Called when an event log line cannot be interpreted
1074
1075 Arguments:
1076 line -- line that could not be understood"""
1077 return True
1078
1079# Local Variables:
1080# mode:python
1081# py-indent-offset:2
1082# comment-column:40
1083# fill-column:72
1084# End: