chiark / gitweb /
Further playlist tests.
[disorder] / python / disorder.py.in
CommitLineData
460b9539 1#
d8055dc4 2# Copyright (C) 2004, 2005, 2007, 2008 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]+")
0b96f403
RK
65_squote = re.compile("'(([^\\\\']|\\\\[\\\\\"'n])*)'")
66_dquote = re.compile("\"(([^\\\\\"]|\\\\[\\\\\"'n])*)\"")
460b9539 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):
573a33bd
RK
118 """Return the complete response string from the server, with the
119 command 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 426
573a33bd
RK
427 Note that queue IDs are unicode strings (because all track
428 information 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
dd9af5cb 482 def rescan(self, *flags):
460b9539 483 """Rescan one or more collections.
484
460b9539 485 Only trusted users can perform this operation.
486 """
dd9af5cb 487 self._simple("rescan", *flags)
460b9539 488
489 def version(self):
490 """Return the server's version number."""
7b32e917 491 return _split(self._simple("version")[1])[0]
460b9539 492
493 def playing(self):
494 """Return the currently playing track.
495
79cbb91d
RK
496 If a track is playing then it is returned as a dictionary. See
497 disorder_protocol(5) for the meanings of the keys. All keys are
498 plain strings but the values will be unicode strings.
499
460b9539 500 If no track is playing then None is returned."""
501 res, details = self._simple("playing")
502 if res % 10 != 9:
503 try:
504 return _queueEntry(details)
505 except _splitError, s:
506 raise protocolError(self.who, s.str())
507 else:
508 return None
509
510 def _somequeue(self, command):
511 self._simple(command)
512 try:
513 return map(lambda s: _queueEntry(s), self._body())
514 except _splitError, s:
515 raise protocolError(self.who, s.str())
516
517 def recent(self):
518 """Return a list of recently played tracks.
519
520 The return value is a list of dictionaries corresponding to
79cbb91d
RK
521 recently played tracks. The oldest track comes first.
522
523 See disorder_protocol(5) for the meanings of the keys. All keys are
524 plain strings but the values will be unicode strings."""
460b9539 525 return self._somequeue("recent")
526
527 def queue(self):
528 """Return the current queue.
529
530 The return value is a list of dictionaries corresponding to
79cbb91d
RK
531 recently played tracks. The next track to be played comes first.
532
573a33bd
RK
533 See disorder_protocol(5) for the meanings of the keys.
534 All keys are plain strings but the values will be unicode strings."""
460b9539 535 return self._somequeue("queue")
536
537 def _somedir(self, command, dir, re):
538 if re:
539 self._simple(command, dir, re[0])
540 else:
541 self._simple(command, dir)
542 return self._body()
543
544 def directories(self, dir, *re):
545 """List subdirectories of a directory.
546
547 Arguments:
548 dir -- directory to list, or '' for the whole root.
549 re -- regexp that results must match. Optional.
550
551 The return value is a list of the (nonempty) subdirectories of dir.
552 If dir is '' then a list of top-level directories is returned.
553
554 If a regexp is specified then the basename of each result must
555 match. Matching is case-independent. See pcrepattern(3).
556 """
557 return self._somedir("dirs", dir, re)
558
559 def files(self, dir, *re):
560 """List files within a directory.
561
562 Arguments:
563 dir -- directory to list, or '' for the whole root.
564 re -- regexp that results must match. Optional.
565
566 The return value is a list of playable files in dir. If dir is ''
567 then a list of top-level files is returned.
568
569 If a regexp is specified then the basename of each result must
570 match. Matching is case-independent. See pcrepattern(3).
571 """
572 return self._somedir("files", dir, re)
573
574 def allfiles(self, dir, *re):
575 """List subdirectories and files within a directory.
576
577 Arguments:
578 dir -- directory to list, or '' for the whole root.
579 re -- regexp that results must match. Optional.
580
581 The return value is a list of all (nonempty) subdirectories and
582 files within dir. If dir is '' then a list of top-level files and
583 directories is returned.
584
585 If a regexp is specified then the basename of each result must
586 match. Matching is case-independent. See pcrepattern(3).
587 """
588 return self._somedir("allfiles", dir, re)
589
590 def set(self, track, key, value):
591 """Set a preference value.
592
593 Arguments:
594 track -- the track to modify
595 key -- the preference name
596 value -- the new preference value
597 """
598 self._simple("set", track, key, value)
599
600 def unset(self, track, key):
601 """Unset a preference value.
602
603 Arguments:
604 track -- the track to modify
605 key -- the preference to remove
606 """
607 self._simple("set", track, key, value)
608
609 def get(self, track, key):
610 """Get a preference value.
611
612 Arguments:
613 track -- the track to query
614 key -- the preference to remove
615
79cbb91d 616 The return value is the preference.
460b9539 617 """
618 ret, details = self._simple("get", track, key)
fb1bc1f5
RK
619 if ret == 555:
620 return None
621 else:
7b32e917 622 return _split(details)[0]
460b9539 623
624 def prefs(self, track):
625 """Get all the preferences for a track.
626
627 Arguments:
628 track -- the track to query
629
630 The return value is a dictionary of all the track's preferences.
631 Note that even nominally numeric values remain encoded as strings.
632 """
633 self._simple("prefs", track)
634 r = {}
635 for line in self._body():
636 try:
637 kv = _split(line)
638 except _splitError, s:
639 raise protocolError(self.who, s.str())
640 if len(kv) != 2:
641 raise protocolError(self.who, "invalid prefs body line")
642 r[kv[0]] = kv[1]
643 return r
644
645 def _boolean(self, s):
646 return s[1] == 'yes'
647
648 def exists(self, track):
649 """Return true if a track exists
650
651 Arguments:
652 track -- the track to check for"""
653 return self._boolean(self._simple("exists", track))
654
655 def enabled(self):
656 """Return true if playing is enabled"""
657 return self._boolean(self._simple("enabled"))
658
659 def random_enabled(self):
660 """Return true if random play is enabled"""
661 return self._boolean(self._simple("random-enabled"))
662
663 def random_enable(self):
664 """Enable random play."""
665 self._simple("random-enable")
666
667 def random_disable(self):
668 """Disable random play."""
669 self._simple("random-disable")
670
671 def length(self, track):
672 """Return the length of a track in seconds.
673
674 Arguments:
675 track -- the track to query.
676 """
677 ret, details = self._simple("length", track)
678 return int(details)
679
680 def search(self, words):
681 """Search for tracks.
682
683 Arguments:
684 words -- the set of words to search for.
685
686 The return value is a list of track path names, all of which contain
687 all of the required words (in their path name, trackname
688 preferences, etc.)
689 """
f383b2f1 690 self._simple("search", _quote(words))
460b9539 691 return self._body()
692
31773020
RK
693 def tags(self):
694 """List all tags
695
696 The return value is a list of all tags which apply to at least one
697 track."""
698 self._simple("tags")
699 return self._body()
700
460b9539 701 def stats(self):
702 """Get server statistics.
703
704 The return value is list of statistics.
705 """
706 self._simple("stats")
707 return self._body()
708
709 def dump(self):
710 """Get all preferences.
711
712 The return value is an encoded dump of the preferences database.
713 """
714 self._simple("dump")
715 return self._body()
716
717 def set_volume(self, left, right):
718 """Set volume.
719
720 Arguments:
721 left -- volume for the left speaker.
722 right -- volume for the right speaker.
723 """
724 self._simple("volume", left, right)
725
726 def get_volume(self):
727 """Get volume.
728
729 The return value a tuple consisting of the left and right volumes.
730 """
731 ret, details = self._simple("volume")
732 return map(int,string.split(details))
733
734 def move(self, track, delta):
735 """Move a track in the queue.
736
737 Arguments:
738 track -- the name or ID of the track to move
739 delta -- the number of steps towards the head of the queue to move
740 """
741 ret, details = self._simple("move", track, str(delta))
742 return int(details)
743
81e440ce
RK
744 def moveafter(self, target, tracks):
745 """Move a track in the queue
746
747 Arguments:
748 target -- target ID or None
749 tracks -- a list of IDs to move
750
751 If target is '' or is not in the queue then the tracks are moved to
752 the head of the queue.
753
754 Otherwise the tracks are moved to just after the target."""
755 if target is None:
756 target = ''
757 self._simple("moveafter", target, *tracks)
758
460b9539 759 def log(self, callback):
760 """Read event log entries as they happen.
761
762 Each event log entry is handled by passing it to callback.
763
764 The callback takes two arguments, the first is the client and the
765 second the line from the event log.
766
767 The callback should return True to continue or False to stop (don't
dbeb3844 768 forget this, or your program will mysteriously misbehave). Once you
573a33bd
RK
769 stop reading the log the connection is useless and should be
770 deleted.
460b9539 771
772 It is suggested that you use the disorder.monitor class instead of
773 calling this method directly, but this is not mandatory.
774
775 See disorder_protocol(5) for the event log syntax.
776
777 Arguments:
778 callback -- function to call with log entry
779 """
780 ret, details = self._simple("log")
781 while True:
782 l = self._line()
783 self._debug(client.debug_body, "<<< %s" % l)
784 if l != '' and l[0] == '.':
785 if l == '.':
786 return
787 l = l[1:]
788 if not callback(self, l):
789 break
460b9539 790
791 def pause(self):
792 """Pause the current track."""
793 self._simple("pause")
794
795 def resume(self):
796 """Resume after a pause."""
797 self._simple("resume")
798
799 def part(self, track, context, part):
800 """Get a track name part
801
802 Arguments:
803 track -- the track to query
804 context -- the context ('sort' or 'display')
805 part -- the desired part (usually 'artist', 'album' or 'title')
806
807 The return value is the preference
808 """
809 ret, details = self._simple("part", track, context, part)
7b32e917 810 return _split(details)[0]
460b9539 811
f35e5800
RK
812 def setglobal(self, key, value):
813 """Set a global preference value.
814
815 Arguments:
816 key -- the preference name
817 value -- the new preference value
818 """
819 self._simple("set-global", key, value)
820
821 def unsetglobal(self, key):
822 """Unset a global preference value.
823
824 Arguments:
825 key -- the preference to remove
826 """
827 self._simple("set-global", key, value)
828
829 def getglobal(self, key):
830 """Get a global preference value.
831
832 Arguments:
833 key -- the preference to look up
834
835 The return value is the preference
836 """
837 ret, details = self._simple("get-global", key)
fb1bc1f5
RK
838 if ret == 555:
839 return None
840 else:
7b32e917 841 return _split(details)[0]
f35e5800 842
b12be54a
RK
843 def make_cookie(self):
844 """Create a login cookie"""
845 ret, details = self._simple("make-cookie")
eb5dc014 846 return _split(details)[0]
b12be54a
RK
847
848 def revoke(self):
849 """Revoke a login cookie"""
850 self._simple("revoke")
851
f0feb22e
RK
852 def adduser(self, user, password):
853 """Create a user"""
854 self._simple("adduser", user, password)
855
856 def deluser(self, user):
857 """Delete a user"""
858 self._simple("deluser", user)
859
5df73aeb
RK
860 def userinfo(self, user, key):
861 """Get user information"""
862 res, details = self._simple("userinfo", user, key)
863 if res == 555:
864 return None
865 return _split(details)[0]
866
867 def edituser(self, user, key, value):
868 """Set user information"""
869 self._simple("edituser", user, key, value)
870
c3be4f19
RK
871 def users(self):
872 """List all users
873
874 The return value is a list of all users."""
875 self._simple("users")
876 return self._body()
877
ba39faf6
RK
878 def register(self, username, password, email):
879 """Register a user"""
880 res, details = self._simple("register", username, password, email)
881 return _split(details)[0]
882
883 def confirm(self, confirmation):
884 """Confirm a user registration"""
885 res, details = self._simple("confirm", confirmation)
886
2b33c4cf
RK
887 def schedule_list(self):
888 """Get a list of scheduled events """
889 self._simple("schedule-list")
890 return self._body()
891
892 def schedule_del(self, event):
893 """Delete a scheduled event"""
894 self._simple("schedule-del", event)
895
896 def schedule_get(self, event):
573a33bd
RK
897 """Get the details for an event as a dict (returns None if
898 event not found)"""
2b33c4cf
RK
899 res, details = self._simple("schedule-get", event)
900 if res == 555:
901 return None
902 d = {}
903 for line in self._body():
904 bits = _split(line)
905 d[bits[0]] = bits[1]
906 return d
907
908 def schedule_add(self, when, priority, action, *rest):
909 """Add a scheduled event"""
17360d2f 910 self._simple("schedule-add", str(when), priority, action, *rest)
2b33c4cf 911
573a33bd
RK
912 def playlist_delete(self, playlist):
913 """Delete a playlist"""
914 self._simple("playlist-delete", playlist)
915
916 def playlist_get(self, playlist):
917 """Get the contents of a playlist
918
919 The return value is an array of track names, or None if there is no
920 such playlist."""
921 res, details = self._simple("playlist-get", playlist)
922 if res == 555:
923 return None
924 return self._body()
925
926 def playlist_lock(self, playlist):
927 """Lock a playlist. Playlists can only be modified when locked."""
928 self._simple("playlist-lock", playlist)
929
930 def playlist_unlock(self):
931 """Unlock the locked playlist."""
932 self._simple("playlist-unlock")
933
934 def playlist_set(self, playlist, tracks):
935 """Set the contents of a playlist. The playlist must be locked.
936
937 Arguments:
938 playlist -- Playlist to set
939 tracks -- Array of tracks"""
440af55d 940 self._simple_body(tracks, "playlist-set", playlist)
573a33bd
RK
941
942 def playlist_set_share(self, playlist, share):
943 """Set the sharing status of a playlist"""
944 self._simple("playlist-set-share", playlist, share)
945
946 def playlist_get_share(self, playlist):
947 """Returns the sharing status of a playlist"""
948 res, details = self._simple("playlist-get-share", playlist)
949 if res == 555:
950 return None
951 return _split(details)[0]
952
440af55d
RK
953 def playlists(self):
954 """Returns the list of visible playlists"""
955 self._simple("playlists")
956 return self._body()
957
460b9539 958 ########################################################################
959 # I/O infrastructure
960
961 def _line(self):
962 # read one response line and return as some suitable string object
963 #
964 # If an I/O error occurs, disconnect from the server.
965 #
966 # XXX does readline() DTRT regarding character encodings?
967 try:
968 l = self.r.readline()
969 if not re.search("\n", l):
970 raise communicationError(self.who, "peer disconnected")
971 l = l[:-1]
972 except:
973 self._disconnect()
974 raise
975 return unicode(l, "UTF-8")
976
977 def _response(self):
978 # read a response as a (code, details) tuple
979 l = self._line()
980 self._debug(client.debug_proto, "<== %s" % l)
981 m = _response.match(l)
982 if m:
983 return int(m.group(1)), m.group(2)
984 else:
985 raise protocolError(self.who, "invalid response %s")
986
573a33bd
RK
987 def _send(self, body, *command):
988 # Quote and send a command and optional body
f383b2f1
RK
989 #
990 # Returns the encoded command.
460b9539 991 quoted = _quote(command)
992 self._debug(client.debug_proto, "==> %s" % quoted)
993 encoded = quoted.encode("UTF-8")
994 try:
995 self.w.write(encoded)
996 self.w.write("\n")
573a33bd
RK
997 if body != None:
998 for l in body:
999 if l[0] == ".":
1000 self.w.write(".")
1001 self.w.write(l)
1002 self.w.write("\n")
1003 self.w.write(".\n")
460b9539 1004 self.w.flush()
f383b2f1 1005 return encoded
460b9539 1006 except IOError, e:
1007 # e.g. EPIPE
1008 self._disconnect()
1009 raise communicationError(self.who, e)
1010 except:
1011 self._disconnect()
1012 raise
1013
573a33bd 1014 def _simple(self, *command):
460b9539 1015 # Issue a simple command, throw an exception on error
1016 #
1017 # If an I/O error occurs, disconnect from the server.
1018 #
fb1bc1f5 1019 # On success or 'normal' errors returns response as a (code, details) tuple
460b9539 1020 #
1021 # On error raise operationError
573a33bd
RK
1022 return self._simple_body(None, *command)
1023
1024 def _simple_body(self, body, *command):
1025 # Issue a simple command with optional body, throw an exception on error
1026 #
1027 # If an I/O error occurs, disconnect from the server.
1028 #
1029 # On success or 'normal' errors returns response as a (code, details) tuple
1030 #
1031 # On error raise operationError
460b9539 1032 if self.state == 'disconnected':
1033 self.connect()
1034 if command:
573a33bd 1035 cmd = self._send(body, *command)
f383b2f1
RK
1036 else:
1037 cmd = None
460b9539 1038 res, details = self._response()
fb1bc1f5 1039 if res / 100 == 2 or res == 555:
460b9539 1040 return res, details
f383b2f1 1041 raise operationError(res, details, cmd)
460b9539 1042
1043 def _body(self):
1044 # Fetch a dot-stuffed body
1045 result = []
1046 while True:
1047 l = self._line()
1048 self._debug(client.debug_body, "<<< %s" % l)
1049 if l != '' and l[0] == '.':
1050 if l == '.':
1051 return result
1052 l = l[1:]
1053 result.append(l)
1054
1055 ########################################################################
1056 # Configuration file parsing
1057
1058 def _readfile(self, path):
1059 # Read a configuration file
1060 #
1061 # Arguments:
1062 #
1063 # path -- path of file to read
1064
1065 # handlers for various commands
1066 def _collection(self, command, args):
1067 if len(args) != 3:
1068 return "'%s' takes three args" % command
1069 self.config["collections"].append(args)
1070
1071 def _unary(self, command, args):
1072 if len(args) != 1:
1073 return "'%s' takes only one arg" % command
1074 self.config[command] = args[0]
1075
1076 def _include(self, command, args):
1077 if len(args) != 1:
1078 return "'%s' takes only one arg" % command
1079 self._readfile(args[0])
1080
1081 def _any(self, command, args):
1082 self.config[command] = args
1083
1084 # mapping of options to handlers
1085 _options = { "collection": _collection,
1086 "username": _unary,
1087 "password": _unary,
1088 "home": _unary,
1089 "connect": _any,
1090 "include": _include }
1091
1092 # the parser
1093 for lno, line in enumerate(file(path, "r")):
1094 try:
1095 fields = _split(line, 'comments')
1096 except _splitError, s:
1097 raise parseError(path, lno + 1, str(s))
1098 if fields:
1099 command = fields[0]
1100 # we just ignore options we don't know about, so as to cope gracefully
1101 # with version skew (and nothing to do with implementor laziness)
1102 if command in _options:
1103 e = _options[command](self, command, fields[1:])
1104 if e:
1105 self._parseError(path, lno + 1, e)
1106
1107 def _parseError(self, path, lno, s):
1108 raise parseError(path, lno, s)
1109
1110########################################################################
1111# monitor class
1112
1113class monitor:
1114 """DisOrder event log monitor class
1115
573a33bd
RK
1116 Intended to be subclassed with methods corresponding to event log
1117 messages the implementor cares about over-ridden."""
460b9539 1118
1119 def __init__(self, c=None):
1120 """Constructor for the monitor class
1121
1122 Can be passed a client to use. If none is specified then one
1123 will be created specially for the purpose.
1124
1125 Arguments:
1126 c -- client"""
1127 if c == None:
1128 c = client();
1129 self.c = c
1130
1131 def run(self):
1132 """Start monitoring logs. Continues monitoring until one of the
573a33bd
RK
1133 message-specific methods returns False. Can be called more than
1134 once (but not recursively!)"""
460b9539 1135 self.c.log(self._callback)
1136
1137 def when(self):
1138 """Return the timestamp of the current (or most recent) event log entry"""
1139 return self.timestamp
1140
1141 def _callback(self, c, line):
1142 try:
1143 bits = _split(line)
1144 except:
1145 return self.invalid(line)
1146 if(len(bits) < 2):
1147 return self.invalid(line)
1148 self.timestamp = int(bits[0], 16)
1149 keyword = bits[1]
1150 bits = bits[2:]
1151 if keyword == 'completed':
1152 if len(bits) == 1:
1153 return self.completed(bits[0])
1154 elif keyword == 'failed':
1155 if len(bits) == 2:
1156 return self.failed(bits[0], bits[1])
1157 elif keyword == 'moved':
1158 if len(bits) == 3:
1159 try:
1160 n = int(bits[1])
1161 except:
1162 return self.invalid(line)
1163 return self.moved(bits[0], n, bits[2])
1164 elif keyword == 'playing':
1165 if len(bits) == 1:
1166 return self.playing(bits[0], None)
1167 elif len(bits) == 2:
1168 return self.playing(bits[0], bits[1])
1169 elif keyword == 'queue' or keyword == 'recent-added':
1170 try:
1171 q = _list2dict(bits)
1172 except:
1173 return self.invalid(line)
1174 if keyword == 'queue':
1175 return self.queue(q)
1176 if keyword == 'recent-added':
1177 return self.recent_added(q)
1178 elif keyword == 'recent-removed':
1179 if len(bits) == 1:
1180 return self.recent_removed(bits[0])
1181 elif keyword == 'removed':
1182 if len(bits) == 1:
1183 return self.removed(bits[0], None)
1184 elif len(bits) == 2:
1185 return self.removed(bits[0], bits[1])
1186 elif keyword == 'scratched':
1187 if len(bits) == 2:
1188 return self.scratched(bits[0], bits[1])
d8055dc4
RK
1189 elif keyword == 'rescanned':
1190 return self.rescanned()
460b9539 1191 return self.invalid(line)
1192
1193 def completed(self, track):
1194 """Called when a track completes.
1195
1196 Arguments:
1197 track -- track that completed"""
1198 return True
1199
1200 def failed(self, track, error):
1201 """Called when a player suffers an error.
1202
1203 Arguments:
1204 track -- track that failed
1205 error -- error indicator"""
1206 return True
1207
1208 def moved(self, id, offset, user):
1209 """Called when a track is moved in the queue.
1210
1211 Arguments:
1212 id -- queue entry ID
1213 offset -- distance moved
1214 user -- user responsible"""
1215 return True
1216
1217 def playing(self, track, user):
1218 """Called when a track starts playing.
1219
1220 Arguments:
1221 track -- track that has started
1222 user -- user that submitted track, or None"""
1223 return True
1224
1225 def queue(self, q):
1226 """Called when a track is added to the queue.
1227
1228 Arguments:
1229 q -- dictionary of new queue entry"""
1230 return True
1231
1232 def recent_added(self, q):
1233 """Called when a track is added to the recently played list
1234
1235 Arguments:
1236 q -- dictionary of new queue entry"""
1237 return True
1238
1239 def recent_removed(self, id):
1240 """Called when a track is removed from the recently played list
1241
1242 Arguments:
1243 id -- ID of removed entry (always the oldest)"""
1244 return True
1245
1246 def removed(self, id, user):
1247 """Called when a track is removed from the queue, either manually
1248 or in order to play it.
1249
1250 Arguments:
1251 id -- ID of removed entry
1252 user -- user responsible (or None if we're playing this track)"""
1253 return True
1254
1255 def scratched(self, track, user):
1256 """Called when a track is scratched
1257
1258 Arguments:
1259 track -- track that was scratched
1260 user -- user responsible"""
1261 return True
1262
1263 def invalid(self, line):
1264 """Called when an event log line cannot be interpreted
1265
1266 Arguments:
1267 line -- line that could not be understood"""
1268 return True
1269
d8055dc4
RK
1270 def rescanned(self):
1271 """Called when a rescan completes"""
1272 return True
1273
460b9539 1274# Local Variables:
1275# mode:python
1276# py-indent-offset:2
1277# comment-column:40
1278# fill-column:72
1279# End: