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