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