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