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