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