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