chiark / gitweb /
disobedience --help doesn't DISPLAY to be set
[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
f0feb22e 279 def __init__(self, user=None, password=None):
460b9539 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 }
f0feb22e
RK
297 self.user = user
298 self.password = password
460b9539 299 home = os.getenv("HOME")
300 if not home:
301 home = pw.pw_dir
302 privconf = _configfile + "." + pw.pw_name
303 passfile = home + os.sep + ".disorder" + os.sep + "passwd"
f5eb2aff
RK
304 if os.path.exists(_configfile):
305 self._readfile(_configfile)
460b9539 306 if os.path.exists(privconf):
307 self._readfile(privconf)
eee9d4b3 308 if os.path.exists(passfile) and _userconf:
460b9539 309 self._readfile(passfile)
310 self.state = 'disconnected'
311
312 def debug(self, bits):
313 """Enable or disable protocol debugging. Debug messages are written
314 to sys.stderr.
315
316 Arguments:
317 bits -- bitmap of operations that should generate debug information
318
319 Bitmap values:
320 debug_proto -- dump control protocol messages (excluding bodies)
321 debug_body -- dump control protocol message bodies
322 """
323 self.debugging = bits
324
325 def _debug(self, bit, s):
326 # debug output
327 if self.debugging & bit:
328 sys.stderr.write(_sanitize(s))
329 sys.stderr.write("\n")
330 sys.stderr.flush()
331
b12be54a
RK
332 def connect(self, cookie=None):
333 """c.connect(cookie=None)
334
335 Connect to the DisOrder server and authenticate.
460b9539 336
337 Raises communicationError if connection fails and operationError if
338 authentication fails (in which case disconnection is automatic).
339
340 May be called more than once to retry connections (e.g. when the
341 server is down). If we are already connected and authenticated,
342 this is a no-op.
343
344 Other operations automatically connect if we're not already
345 connected, so it is not strictly necessary to call this method.
b12be54a
RK
346
347 If COOKIE is specified then that is used to log in instead of
348 the username/password.
460b9539 349 """
350 if self.state == 'disconnected':
351 try:
352 self.state = 'connecting'
353 if 'connect' in self.config and len(self.config['connect']) > 0:
354 c = self.config['connect']
355 self.who = repr(c) # temporarily
356 if len(c) == 1:
357 a = socket.getaddrinfo(None, c[0],
358 socket.AF_INET,
359 socket.SOCK_STREAM,
360 0,
361 0)
362 else:
363 a = socket.getaddrinfo(c[0], c[1],
364 socket.AF_INET,
365 socket.SOCK_STREAM,
366 0,
367 0)
368 a = a[0]
369 s = socket.socket(a[0], a[1], a[2]);
370 s.connect(a[4])
371 self.who = "%s" % a[3]
372 else:
373 s = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM);
374 self.who = self.config['home'] + os.sep + "socket"
375 s.connect(self.who)
376 self.w = s.makefile("wb")
377 self.r = s.makefile("rb")
7b32e917
RK
378 (res, details) = self._simple()
379 (protocol, algo, challenge) = _split(details)
380 if protocol != '2':
381 raise communicationError(self.who,
382 "unknown protocol version %s" % protocol)
b12be54a 383 if cookie is None:
f0feb22e
RK
384 if self.user is None:
385 user = self.config['username']
386 else:
387 user = self.user
388 if self.password is None:
389 password = self.config['password']
390 else:
391 password = self.password
b3141726 392 # TODO support algorithms other than SHA-1
b12be54a 393 h = sha.sha()
f0feb22e 394 h.update(password)
b12be54a 395 h.update(binascii.unhexlify(challenge))
f0feb22e 396 self._simple("user", user, h.hexdigest())
b12be54a
RK
397 else:
398 self._simple("cookie", cookie)
460b9539 399 self.state = 'connected'
400 except socket.error, e:
401 self._disconnect()
402 raise communicationError(self.who, e)
403 except:
404 self._disconnect()
405 raise
406
407 def _disconnect(self):
408 # disconnect from the server, whatever state we are in
409 try:
410 del self.w
411 del self.r
412 except:
413 pass
414 self.state = 'disconnected'
415
416 ########################################################################
417 # Operations
418
460b9539 419 def play(self, track):
420 """Play a track.
421
422 Arguments:
423 track -- the path of the track to play.
81e440ce
RK
424
425 Returns the ID of the new queue entry.
79cbb91d
RK
426
427 Note that queue IDs are unicode strings (because all track information
428 values are unicode strings).
460b9539 429 """
81e440ce
RK
430 res, details = self._simple("play", track)
431 return unicode(details) # because it's unicode in queue() output
460b9539 432
433 def remove(self, track):
434 """Remove a track from the queue.
435
436 Arguments:
437 track -- the path or ID of the track to remove.
438 """
439 self._simple("remove", track)
440
441 def enable(self):
442 """Enable playing."""
443 self._simple("enable")
444
445 def disable(self, *now):
446 """Disable playing.
447
448 Arguments:
449 now -- if present (with any value), the current track is stopped
450 too.
451 """
452 if now:
453 self._simple("disable", "now")
454 else:
455 self._simple("disable")
456
457 def scratch(self, *id):
458 """Scratch the currently playing track.
459
460 Arguments:
461 id -- if present, the ID of the track to scratch.
462 """
463 if id:
464 self._simple("scratch", id[0])
465 else:
466 self._simple("scratch")
467
468 def shutdown(self):
469 """Shut down the server.
470
471 Only trusted users can perform this operation.
472 """
473 self._simple("shutdown")
474
475 def reconfigure(self):
476 """Make the server reload its configuration.
477
478 Only trusted users can perform this operation.
479 """
480 self._simple("reconfigure")
481
482 def rescan(self, pattern):
483 """Rescan one or more collections.
484
485 Arguments:
486 pattern -- glob pattern matching collections to rescan.
487
488 Only trusted users can perform this operation.
489 """
490 self._simple("rescan", pattern)
491
492 def version(self):
493 """Return the server's version number."""
7b32e917 494 return _split(self._simple("version")[1])[0]
460b9539 495
496 def playing(self):
497 """Return the currently playing track.
498
79cbb91d
RK
499 If a track is playing then it is returned as a dictionary. See
500 disorder_protocol(5) for the meanings of the keys. All keys are
501 plain strings but the values will be unicode strings.
502
460b9539 503 If no track is playing then None is returned."""
504 res, details = self._simple("playing")
505 if res % 10 != 9:
506 try:
507 return _queueEntry(details)
508 except _splitError, s:
509 raise protocolError(self.who, s.str())
510 else:
511 return None
512
513 def _somequeue(self, command):
514 self._simple(command)
515 try:
516 return map(lambda s: _queueEntry(s), self._body())
517 except _splitError, s:
518 raise protocolError(self.who, s.str())
519
520 def recent(self):
521 """Return a list of recently played tracks.
522
523 The return value is a list of dictionaries corresponding to
79cbb91d
RK
524 recently played tracks. The oldest track comes first.
525
526 See disorder_protocol(5) for the meanings of the keys. All keys are
527 plain strings but the values will be unicode strings."""
460b9539 528 return self._somequeue("recent")
529
530 def queue(self):
531 """Return the current queue.
532
533 The return value is a list of dictionaries corresponding to
79cbb91d
RK
534 recently played tracks. The next track to be played comes first.
535
536 See disorder_protocol(5) for the meanings of the keys. All keys are
537 plain strings but the values will be unicode strings."""
460b9539 538 return self._somequeue("queue")
539
540 def _somedir(self, command, dir, re):
541 if re:
542 self._simple(command, dir, re[0])
543 else:
544 self._simple(command, dir)
545 return self._body()
546
547 def directories(self, dir, *re):
548 """List subdirectories of 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 the (nonempty) subdirectories of dir.
555 If dir is '' then a list of top-level directories is returned.
556
557 If a regexp is specified then the basename of each result must
558 match. Matching is case-independent. See pcrepattern(3).
559 """
560 return self._somedir("dirs", dir, re)
561
562 def files(self, dir, *re):
563 """List files within a directory.
564
565 Arguments:
566 dir -- directory to list, or '' for the whole root.
567 re -- regexp that results must match. Optional.
568
569 The return value is a list of playable files in dir. If dir is ''
570 then a list of top-level files is returned.
571
572 If a regexp is specified then the basename of each result must
573 match. Matching is case-independent. See pcrepattern(3).
574 """
575 return self._somedir("files", dir, re)
576
577 def allfiles(self, dir, *re):
578 """List subdirectories and files within a directory.
579
580 Arguments:
581 dir -- directory to list, or '' for the whole root.
582 re -- regexp that results must match. Optional.
583
584 The return value is a list of all (nonempty) subdirectories and
585 files within dir. If dir is '' then a list of top-level files and
586 directories is returned.
587
588 If a regexp is specified then the basename of each result must
589 match. Matching is case-independent. See pcrepattern(3).
590 """
591 return self._somedir("allfiles", dir, re)
592
593 def set(self, track, key, value):
594 """Set a preference value.
595
596 Arguments:
597 track -- the track to modify
598 key -- the preference name
599 value -- the new preference value
600 """
601 self._simple("set", track, key, value)
602
603 def unset(self, track, key):
604 """Unset a preference value.
605
606 Arguments:
607 track -- the track to modify
608 key -- the preference to remove
609 """
610 self._simple("set", track, key, value)
611
612 def get(self, track, key):
613 """Get a preference value.
614
615 Arguments:
616 track -- the track to query
617 key -- the preference to remove
618
79cbb91d 619 The return value is the preference.
460b9539 620 """
621 ret, details = self._simple("get", track, key)
fb1bc1f5
RK
622 if ret == 555:
623 return None
624 else:
7b32e917 625 return _split(details)[0]
460b9539 626
627 def prefs(self, track):
628 """Get all the preferences for a track.
629
630 Arguments:
631 track -- the track to query
632
633 The return value is a dictionary of all the track's preferences.
634 Note that even nominally numeric values remain encoded as strings.
635 """
636 self._simple("prefs", track)
637 r = {}
638 for line in self._body():
639 try:
640 kv = _split(line)
641 except _splitError, s:
642 raise protocolError(self.who, s.str())
643 if len(kv) != 2:
644 raise protocolError(self.who, "invalid prefs body line")
645 r[kv[0]] = kv[1]
646 return r
647
648 def _boolean(self, s):
649 return s[1] == 'yes'
650
651 def exists(self, track):
652 """Return true if a track exists
653
654 Arguments:
655 track -- the track to check for"""
656 return self._boolean(self._simple("exists", track))
657
658 def enabled(self):
659 """Return true if playing is enabled"""
660 return self._boolean(self._simple("enabled"))
661
662 def random_enabled(self):
663 """Return true if random play is enabled"""
664 return self._boolean(self._simple("random-enabled"))
665
666 def random_enable(self):
667 """Enable random play."""
668 self._simple("random-enable")
669
670 def random_disable(self):
671 """Disable random play."""
672 self._simple("random-disable")
673
674 def length(self, track):
675 """Return the length of a track in seconds.
676
677 Arguments:
678 track -- the track to query.
679 """
680 ret, details = self._simple("length", track)
681 return int(details)
682
683 def search(self, words):
684 """Search for tracks.
685
686 Arguments:
687 words -- the set of words to search for.
688
689 The return value is a list of track path names, all of which contain
690 all of the required words (in their path name, trackname
691 preferences, etc.)
692 """
f383b2f1 693 self._simple("search", _quote(words))
460b9539 694 return self._body()
695
31773020
RK
696 def tags(self):
697 """List all tags
698
699 The return value is a list of all tags which apply to at least one
700 track."""
701 self._simple("tags")
702 return self._body()
703
460b9539 704 def stats(self):
705 """Get server statistics.
706
707 The return value is list of statistics.
708 """
709 self._simple("stats")
710 return self._body()
711
712 def dump(self):
713 """Get all preferences.
714
715 The return value is an encoded dump of the preferences database.
716 """
717 self._simple("dump")
718 return self._body()
719
720 def set_volume(self, left, right):
721 """Set volume.
722
723 Arguments:
724 left -- volume for the left speaker.
725 right -- volume for the right speaker.
726 """
727 self._simple("volume", left, right)
728
729 def get_volume(self):
730 """Get volume.
731
732 The return value a tuple consisting of the left and right volumes.
733 """
734 ret, details = self._simple("volume")
735 return map(int,string.split(details))
736
737 def move(self, track, delta):
738 """Move a track in the queue.
739
740 Arguments:
741 track -- the name or ID of the track to move
742 delta -- the number of steps towards the head of the queue to move
743 """
744 ret, details = self._simple("move", track, str(delta))
745 return int(details)
746
81e440ce
RK
747 def moveafter(self, target, tracks):
748 """Move a track in the queue
749
750 Arguments:
751 target -- target ID or None
752 tracks -- a list of IDs to move
753
754 If target is '' or is not in the queue then the tracks are moved to
755 the head of the queue.
756
757 Otherwise the tracks are moved to just after the target."""
758 if target is None:
759 target = ''
760 self._simple("moveafter", target, *tracks)
761
460b9539 762 def log(self, callback):
763 """Read event log entries as they happen.
764
765 Each event log entry is handled by passing it to callback.
766
767 The callback takes two arguments, the first is the client and the
768 second the line from the event log.
769
770 The callback should return True to continue or False to stop (don't
771 forget this, or your program will mysteriously misbehave).
772
773 It is suggested that you use the disorder.monitor class instead of
774 calling this method directly, but this is not mandatory.
775
776 See disorder_protocol(5) for the event log syntax.
777
778 Arguments:
779 callback -- function to call with log entry
780 """
781 ret, details = self._simple("log")
782 while True:
783 l = self._line()
784 self._debug(client.debug_body, "<<< %s" % l)
785 if l != '' and l[0] == '.':
786 if l == '.':
787 return
788 l = l[1:]
789 if not callback(self, l):
790 break
791 # tell the server to stop sending, eat the remains of the body,
792 # eat the response
793 self._send("version")
794 self._body()
795 self._response()
796
797 def pause(self):
798 """Pause the current track."""
799 self._simple("pause")
800
801 def resume(self):
802 """Resume after a pause."""
803 self._simple("resume")
804
805 def part(self, track, context, part):
806 """Get a track name part
807
808 Arguments:
809 track -- the track to query
810 context -- the context ('sort' or 'display')
811 part -- the desired part (usually 'artist', 'album' or 'title')
812
813 The return value is the preference
814 """
815 ret, details = self._simple("part", track, context, part)
7b32e917 816 return _split(details)[0]
460b9539 817
f35e5800
RK
818 def setglobal(self, key, value):
819 """Set a global preference value.
820
821 Arguments:
822 key -- the preference name
823 value -- the new preference value
824 """
825 self._simple("set-global", key, value)
826
827 def unsetglobal(self, key):
828 """Unset a global preference value.
829
830 Arguments:
831 key -- the preference to remove
832 """
833 self._simple("set-global", key, value)
834
835 def getglobal(self, key):
836 """Get a global preference value.
837
838 Arguments:
839 key -- the preference to look up
840
841 The return value is the preference
842 """
843 ret, details = self._simple("get-global", key)
fb1bc1f5
RK
844 if ret == 555:
845 return None
846 else:
7b32e917 847 return _split(details)[0]
f35e5800 848
b12be54a
RK
849 def make_cookie(self):
850 """Create a login cookie"""
851 ret, details = self._simple("make-cookie")
eb5dc014 852 return _split(details)[0]
b12be54a
RK
853
854 def revoke(self):
855 """Revoke a login cookie"""
856 self._simple("revoke")
857
f0feb22e
RK
858 def adduser(self, user, password):
859 """Create a user"""
860 self._simple("adduser", user, password)
861
862 def deluser(self, user):
863 """Delete a user"""
864 self._simple("deluser", user)
865
5df73aeb
RK
866 def userinfo(self, user, key):
867 """Get user information"""
868 res, details = self._simple("userinfo", user, key)
869 if res == 555:
870 return None
871 return _split(details)[0]
872
873 def edituser(self, user, key, value):
874 """Set user information"""
875 self._simple("edituser", user, key, value)
876
c3be4f19
RK
877 def users(self):
878 """List all users
879
880 The return value is a list of all users."""
881 self._simple("users")
882 return self._body()
883
ba39faf6
RK
884 def register(self, username, password, email):
885 """Register a user"""
886 res, details = self._simple("register", username, password, email)
887 return _split(details)[0]
888
889 def confirm(self, confirmation):
890 """Confirm a user registration"""
891 res, details = self._simple("confirm", confirmation)
892
460b9539 893 ########################################################################
894 # I/O infrastructure
895
896 def _line(self):
897 # read one response line and return as some suitable string object
898 #
899 # If an I/O error occurs, disconnect from the server.
900 #
901 # XXX does readline() DTRT regarding character encodings?
902 try:
903 l = self.r.readline()
904 if not re.search("\n", l):
905 raise communicationError(self.who, "peer disconnected")
906 l = l[:-1]
907 except:
908 self._disconnect()
909 raise
910 return unicode(l, "UTF-8")
911
912 def _response(self):
913 # read a response as a (code, details) tuple
914 l = self._line()
915 self._debug(client.debug_proto, "<== %s" % l)
916 m = _response.match(l)
917 if m:
918 return int(m.group(1)), m.group(2)
919 else:
920 raise protocolError(self.who, "invalid response %s")
921
922 def _send(self, *command):
f383b2f1
RK
923 # Quote and send a command
924 #
925 # Returns the encoded command.
460b9539 926 quoted = _quote(command)
927 self._debug(client.debug_proto, "==> %s" % quoted)
928 encoded = quoted.encode("UTF-8")
929 try:
930 self.w.write(encoded)
931 self.w.write("\n")
932 self.w.flush()
f383b2f1 933 return encoded
460b9539 934 except IOError, e:
935 # e.g. EPIPE
936 self._disconnect()
937 raise communicationError(self.who, e)
938 except:
939 self._disconnect()
940 raise
941
942 def _simple(self, *command):
943 # Issue a simple command, throw an exception on error
944 #
945 # If an I/O error occurs, disconnect from the server.
946 #
fb1bc1f5 947 # On success or 'normal' errors returns response as a (code, details) tuple
460b9539 948 #
949 # On error raise operationError
950 if self.state == 'disconnected':
951 self.connect()
952 if command:
f383b2f1
RK
953 cmd = self._send(*command)
954 else:
955 cmd = None
460b9539 956 res, details = self._response()
fb1bc1f5 957 if res / 100 == 2 or res == 555:
460b9539 958 return res, details
f383b2f1 959 raise operationError(res, details, cmd)
460b9539 960
961 def _body(self):
962 # Fetch a dot-stuffed body
963 result = []
964 while True:
965 l = self._line()
966 self._debug(client.debug_body, "<<< %s" % l)
967 if l != '' and l[0] == '.':
968 if l == '.':
969 return result
970 l = l[1:]
971 result.append(l)
972
973 ########################################################################
974 # Configuration file parsing
975
976 def _readfile(self, path):
977 # Read a configuration file
978 #
979 # Arguments:
980 #
981 # path -- path of file to read
982
983 # handlers for various commands
984 def _collection(self, command, args):
985 if len(args) != 3:
986 return "'%s' takes three args" % command
987 self.config["collections"].append(args)
988
989 def _unary(self, command, args):
990 if len(args) != 1:
991 return "'%s' takes only one arg" % command
992 self.config[command] = args[0]
993
994 def _include(self, command, args):
995 if len(args) != 1:
996 return "'%s' takes only one arg" % command
997 self._readfile(args[0])
998
999 def _any(self, command, args):
1000 self.config[command] = args
1001
1002 # mapping of options to handlers
1003 _options = { "collection": _collection,
1004 "username": _unary,
1005 "password": _unary,
1006 "home": _unary,
1007 "connect": _any,
1008 "include": _include }
1009
1010 # the parser
1011 for lno, line in enumerate(file(path, "r")):
1012 try:
1013 fields = _split(line, 'comments')
1014 except _splitError, s:
1015 raise parseError(path, lno + 1, str(s))
1016 if fields:
1017 command = fields[0]
1018 # we just ignore options we don't know about, so as to cope gracefully
1019 # with version skew (and nothing to do with implementor laziness)
1020 if command in _options:
1021 e = _options[command](self, command, fields[1:])
1022 if e:
1023 self._parseError(path, lno + 1, e)
1024
1025 def _parseError(self, path, lno, s):
1026 raise parseError(path, lno, s)
1027
1028########################################################################
1029# monitor class
1030
1031class monitor:
1032 """DisOrder event log monitor class
1033
1034 Intended to be subclassed with methods corresponding to event log messages
1035 the implementor cares about over-ridden."""
1036
1037 def __init__(self, c=None):
1038 """Constructor for the monitor class
1039
1040 Can be passed a client to use. If none is specified then one
1041 will be created specially for the purpose.
1042
1043 Arguments:
1044 c -- client"""
1045 if c == None:
1046 c = client();
1047 self.c = c
1048
1049 def run(self):
1050 """Start monitoring logs. Continues monitoring until one of the
1051 message-specific methods returns False. Can be called more than once
1052 (but not recursively!)"""
1053 self.c.log(self._callback)
1054
1055 def when(self):
1056 """Return the timestamp of the current (or most recent) event log entry"""
1057 return self.timestamp
1058
1059 def _callback(self, c, line):
1060 try:
1061 bits = _split(line)
1062 except:
1063 return self.invalid(line)
1064 if(len(bits) < 2):
1065 return self.invalid(line)
1066 self.timestamp = int(bits[0], 16)
1067 keyword = bits[1]
1068 bits = bits[2:]
1069 if keyword == 'completed':
1070 if len(bits) == 1:
1071 return self.completed(bits[0])
1072 elif keyword == 'failed':
1073 if len(bits) == 2:
1074 return self.failed(bits[0], bits[1])
1075 elif keyword == 'moved':
1076 if len(bits) == 3:
1077 try:
1078 n = int(bits[1])
1079 except:
1080 return self.invalid(line)
1081 return self.moved(bits[0], n, bits[2])
1082 elif keyword == 'playing':
1083 if len(bits) == 1:
1084 return self.playing(bits[0], None)
1085 elif len(bits) == 2:
1086 return self.playing(bits[0], bits[1])
1087 elif keyword == 'queue' or keyword == 'recent-added':
1088 try:
1089 q = _list2dict(bits)
1090 except:
1091 return self.invalid(line)
1092 if keyword == 'queue':
1093 return self.queue(q)
1094 if keyword == 'recent-added':
1095 return self.recent_added(q)
1096 elif keyword == 'recent-removed':
1097 if len(bits) == 1:
1098 return self.recent_removed(bits[0])
1099 elif keyword == 'removed':
1100 if len(bits) == 1:
1101 return self.removed(bits[0], None)
1102 elif len(bits) == 2:
1103 return self.removed(bits[0], bits[1])
1104 elif keyword == 'scratched':
1105 if len(bits) == 2:
1106 return self.scratched(bits[0], bits[1])
1107 return self.invalid(line)
1108
1109 def completed(self, track):
1110 """Called when a track completes.
1111
1112 Arguments:
1113 track -- track that completed"""
1114 return True
1115
1116 def failed(self, track, error):
1117 """Called when a player suffers an error.
1118
1119 Arguments:
1120 track -- track that failed
1121 error -- error indicator"""
1122 return True
1123
1124 def moved(self, id, offset, user):
1125 """Called when a track is moved in the queue.
1126
1127 Arguments:
1128 id -- queue entry ID
1129 offset -- distance moved
1130 user -- user responsible"""
1131 return True
1132
1133 def playing(self, track, user):
1134 """Called when a track starts playing.
1135
1136 Arguments:
1137 track -- track that has started
1138 user -- user that submitted track, or None"""
1139 return True
1140
1141 def queue(self, q):
1142 """Called when a track is added to the queue.
1143
1144 Arguments:
1145 q -- dictionary of new queue entry"""
1146 return True
1147
1148 def recent_added(self, q):
1149 """Called when a track is added to the recently played list
1150
1151 Arguments:
1152 q -- dictionary of new queue entry"""
1153 return True
1154
1155 def recent_removed(self, id):
1156 """Called when a track is removed from the recently played list
1157
1158 Arguments:
1159 id -- ID of removed entry (always the oldest)"""
1160 return True
1161
1162 def removed(self, id, user):
1163 """Called when a track is removed from the queue, either manually
1164 or in order to play it.
1165
1166 Arguments:
1167 id -- ID of removed entry
1168 user -- user responsible (or None if we're playing this track)"""
1169 return True
1170
1171 def scratched(self, track, user):
1172 """Called when a track is scratched
1173
1174 Arguments:
1175 track -- track that was scratched
1176 user -- user responsible"""
1177 return True
1178
1179 def invalid(self, line):
1180 """Called when an event log line cannot be interpreted
1181
1182 Arguments:
1183 line -- line that could not be understood"""
1184 return True
1185
1186# Local Variables:
1187# mode:python
1188# py-indent-offset:2
1189# comment-column:40
1190# fill-column:72
1191# End: