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