2 # Copyright (C) 2004, 2005, 2007 Richard Kettlewell
4 # This program is free software; you can redistribute it and/or modify
5 # it under the terms of the GNU General Public License as published by
6 # the Free Software Foundation; either version 2 of the License, or
7 # (at your option) any later version.
9 # This program is distributed in the hope that it will be useful, but
10 # WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
12 # General Public License for more details.
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
20 """Python support for DisOrder
22 Provides disorder.client, a class for accessing a DisOrder server.
26 #! /usr/bin/env python
35 #! /usr/bin/env python
39 for path in sys.argv[1:]:
42 See disorder_protocol(5) for details of the communication protocol.
44 NB that this code only supports servers configured to use SHA1-based
45 authentication. If the server demands another hash then it will not be
46 possible to use this module.
59 _configfile
= "pkgconfdir/config"
60 _dbhome
= "pkgstatedir"
63 # various regexps we'll use
64 _ws
= re
.compile(r
"^[ \t\n\r]+")
65 _squote
= re
.compile("'(([^\\\\']|\\\\[\\\\\"'n])+)'")
66 _dquote
= re
.compile("\"(([^\\\\\"]|\\\\[\\\\\"'n])+)\"")
67 _unquoted
= re
.compile("[^\"' \\t\\n\\r][^ \t\n\r]*")
69 _response
= re
.compile("([0-9]{3}) ?(.*)")
73 ########################################################################
76 class Error(Exception):
77 """Base class for DisOrder exceptions."""
79 class _splitError(Error
):
81 def __init__(self
, value
):
84 return str(self
.value
)
86 class parseError(Error
):
87 """Error parsing the configuration file."""
88 def __init__(self
, path
, line
, details
):
91 self
.details
= details
93 return "%s:%d: %s" %
(self
.path
, self
.line
, self
.details
)
95 class protocolError(Error
):
96 """DisOrder control protocol error.
98 Indicates a mismatch between the client and server's understanding of
101 def __init__(self
, who
, error
):
105 return "%s: %s" %
(self
.who
, str(self
.error
))
107 class operationError(Error
):
108 """DisOrder control protocol error response.
110 Indicates that an operation failed (e.g. an attempt to play a
111 nonexistent track). The connection should still be usable.
113 def __init__(self
, res
, details
, cmd
=None):
116 self
.details_
= details
118 """Return the complete response string from the server, with the command
121 Excludes the final newline.
123 if self
.cmd_
is None:
124 return "%d %s" %
(self
.res_
, self
.details_
)
126 return "%d %s [%s]" %
(self
.res_
, self
.details_
, self
.cmd_
)
128 """Return the response code from the server."""
131 """Returns the detail string from the server."""
134 class communicationError(Error
):
135 """DisOrder control protocol communication error.
137 Indicates that communication with the server went wrong, perhaps
138 because the server was restarted. The caller could report an error to
139 the user and wait for further user instructions, or even automatically
142 def __init__(self
, who
, error
):
146 return "%s: %s" %
(self
.who
, str(self
.error
))
148 ########################################################################
149 # DisOrder-specific text processing
152 # Unescape the contents of a string
156 # s -- string to unescape
158 s
= re
.sub("\\\\n", "\n", s
)
159 s
= re
.sub("\\\\(.)", "\\1", s
)
162 def _split(s
, *comments
):
163 # Split a string into fields according to the usual Disorder string splitting
168 # s -- string to parse
169 # comments -- if present, parse comments
173 # On success, a list of fields is returned.
175 # On error, disorder.parseError is thrown.
180 if comments
and s
[0] == '#':
187 # pick of quoted fields of both kinds
192 fields
.append(_unescape(m
.group(1)))
195 # and unquoted fields
196 m
= _unquoted
.match(s
)
198 fields
.append(m
.group(0))
201 # anything left must be in error
202 if s
[0] == '"' or s
[0] == '\'':
203 raise _splitError("invalid quoted string")
205 raise _splitError("syntax error")
209 # Escape the contents of a string
213 # s -- string to escape
215 if re
.search("[\\\\\"'\n \t\r]", s
) or s
== '':
216 s
= re
.sub(r
'[\\"]', r
'\\\g<0>', s
)
217 s
= re
.sub("\n", r
"\\n", s
)
223 # Quote a list of values
224 return ' '.join(map(_escape
, list))
227 # Return the value of s in a form suitable for writing to stderr
228 return s
.encode(locale
.nl_langinfo(locale
.CODESET
), 'replace')
231 # Convert a list of the form [k1, v1, k2, v2, ..., kN, vN]
232 # to a dictionary {k1:v1, k2:v2, ..., kN:vN}
240 except StopIteration:
245 # parse a queue entry
246 return _list2dict(_split(s
))
248 ########################################################################
252 """DisOrder client class.
254 This class provides access to the DisOrder server either on this
255 machine or across the internet.
257 The server to connect to, and the username and password to use, are
258 determined from the configuration files as described in 'man
261 All methods will connect if necessary, as soon as you have a
262 disorder.client object you can start calling operational methods on
265 However if the server is restarted then the next method called on a
266 connection will throw an exception. This may be considered a bug.
268 All methods block until they complete.
270 Operation methods raise communicationError if the connection breaks,
271 protocolError if the response from the server is malformed, or
272 operationError if the response is valid but indicates that the
279 def __init__(self
, user
=None, password
=None):
280 """Constructor for DisOrder client class.
282 The constructor reads the configuration file, but does not connect
285 If the environment variable DISORDER_PYTHON_DEBUG is set then the
286 debug flags are initialised to that value. This can be overridden
287 with the debug() method below.
289 The constructor Raises parseError() if the configuration file is not
292 pw
= pwd
.getpwuid(os
.getuid())
293 self
.debugging
= int(os
.getenv("DISORDER_PYTHON_DEBUG", 0))
294 self
.config
= { 'collections': [],
295 'username': pw
.pw_name
,
298 self
.password
= password
299 home
= os
.getenv("HOME")
302 privconf
= _configfile
+ "." + pw
.pw_name
303 passfile
= home
+ os
.sep
+ ".disorder" + os
.sep
+ "passwd"
304 if os
.path
.exists(_configfile
):
305 self
._readfile(_configfile
)
306 if os
.path
.exists(privconf
):
307 self
._readfile(privconf
)
308 if os
.path
.exists(passfile
) and _userconf
:
309 self
._readfile(passfile
)
310 self
.state
= 'disconnected'
312 def debug(self
, bits
):
313 """Enable or disable protocol debugging. Debug messages are written
317 bits -- bitmap of operations that should generate debug information
320 debug_proto -- dump control protocol messages (excluding bodies)
321 debug_body -- dump control protocol message bodies
323 self
.debugging
= bits
325 def _debug(self
, bit
, s
):
327 if self
.debugging
& bit
:
328 sys
.stderr
.write(_sanitize(s
))
329 sys
.stderr
.write("\n")
332 def connect(self
, cookie
=None):
333 """c.connect(cookie=None)
335 Connect to the DisOrder server and authenticate.
337 Raises communicationError if connection fails and operationError if
338 authentication fails (in which case disconnection is automatic).
340 May be called more than once to retry connections (e.g. when the
341 server is down). If we are already connected and authenticated,
344 Other operations automatically connect if we're not already
345 connected, so it is not strictly necessary to call this method.
347 If COOKIE is specified then that is used to log in instead of
348 the username/password.
350 if self
.state
== 'disconnected':
352 self
.state
= 'connecting'
353 if 'connect' in self
.config
and len(self
.config
['connect']) > 0:
354 c
= self
.config
['connect']
355 self
.who
= repr(c
) # temporarily
357 a
= socket
.getaddrinfo(None, c
[0],
363 a
= socket
.getaddrinfo(c
[0], c
[1],
369 s
= socket
.socket(a
[0], a
[1], a
[2]);
371 self
.who
= "%s" % a
[3]
373 s
= socket
.socket(socket
.AF_UNIX
, socket
.SOCK_STREAM
);
374 self
.who
= self
.config
['home'] + os
.sep
+ "socket"
376 self
.w
= s
.makefile("wb")
377 self
.r
= s
.makefile("rb")
378 (res
, challenge
) = self
._simple()
380 if self
.user
is None:
381 user
= self
.config
['username']
384 if self
.password
is None:
385 password
= self
.config
['password']
387 password
= self
.password
390 h
.update(binascii
.unhexlify(challenge
))
391 self
._simple("user", user
, h
.hexdigest())
393 self
._simple("cookie", cookie
)
394 self
.state
= 'connected'
395 except socket
.error
, e
:
397 raise communicationError(self
.who
, e
)
402 def _disconnect(self
):
403 # disconnect from the server, whatever state we are in
409 self
.state
= 'disconnected'
411 ########################################################################
414 def become(self
, who
):
415 """Become another user.
418 who -- the user to become.
420 Only trusted users can perform this operation.
422 self
._simple("become", who
)
424 def play(self
, track
):
428 track -- the path of the track to play.
430 Returns the ID of the new queue entry.
432 Note that queue IDs are unicode strings (because all track information
433 values are unicode strings).
435 res
, details
= self
._simple("play", track
)
436 return unicode(details
) # because it's unicode in queue() output
438 def remove(self
, track
):
439 """Remove a track from the queue.
442 track -- the path or ID of the track to remove.
444 self
._simple("remove", track
)
447 """Enable playing."""
448 self
._simple("enable")
450 def disable(self
, *now
):
454 now -- if present (with any value), the current track is stopped
458 self
._simple("disable", "now")
460 self
._simple("disable")
462 def scratch(self
, *id):
463 """Scratch the currently playing track.
466 id -- if present, the ID of the track to scratch.
469 self
._simple("scratch", id[0])
471 self
._simple("scratch")
474 """Shut down the server.
476 Only trusted users can perform this operation.
478 self
._simple("shutdown")
480 def reconfigure(self
):
481 """Make the server reload its configuration.
483 Only trusted users can perform this operation.
485 self
._simple("reconfigure")
487 def rescan(self
, pattern
):
488 """Rescan one or more collections.
491 pattern -- glob pattern matching collections to rescan.
493 Only trusted users can perform this operation.
495 self
._simple("rescan", pattern
)
498 """Return the server's version number."""
499 return self
._simple("version")[1]
502 """Return the currently playing track.
504 If a track is playing then it is returned as a dictionary. See
505 disorder_protocol(5) for the meanings of the keys. All keys are
506 plain strings but the values will be unicode strings.
508 If no track is playing then None is returned."""
509 res
, details
= self
._simple("playing")
512 return _queueEntry(details
)
513 except _splitError
, s
:
514 raise protocolError(self
.who
, s
.str())
518 def _somequeue(self
, command
):
519 self
._simple(command
)
521 return map(lambda s
: _queueEntry(s
), self
._body())
522 except _splitError
, s
:
523 raise protocolError(self
.who
, s
.str())
526 """Return a list of recently played tracks.
528 The return value is a list of dictionaries corresponding to
529 recently played tracks. The oldest track comes first.
531 See disorder_protocol(5) for the meanings of the keys. All keys are
532 plain strings but the values will be unicode strings."""
533 return self
._somequeue("recent")
536 """Return the current queue.
538 The return value is a list of dictionaries corresponding to
539 recently played tracks. The next track to be played comes first.
541 See disorder_protocol(5) for the meanings of the keys. All keys are
542 plain strings but the values will be unicode strings."""
543 return self
._somequeue("queue")
545 def _somedir(self
, command
, dir, re
):
547 self
._simple(command
, dir, re
[0])
549 self
._simple(command
, dir)
552 def directories(self
, dir, *re
):
553 """List subdirectories of a directory.
556 dir -- directory to list, or '' for the whole root.
557 re -- regexp that results must match. Optional.
559 The return value is a list of the (nonempty) subdirectories of dir.
560 If dir is '' then a list of top-level directories is returned.
562 If a regexp is specified then the basename of each result must
563 match. Matching is case-independent. See pcrepattern(3).
565 return self
._somedir("dirs", dir, re
)
567 def files(self
, dir, *re
):
568 """List files within a directory.
571 dir -- directory to list, or '' for the whole root.
572 re -- regexp that results must match. Optional.
574 The return value is a list of playable files in dir. If dir is ''
575 then a list of top-level files is returned.
577 If a regexp is specified then the basename of each result must
578 match. Matching is case-independent. See pcrepattern(3).
580 return self
._somedir("files", dir, re
)
582 def allfiles(self
, dir, *re
):
583 """List subdirectories and files within a directory.
586 dir -- directory to list, or '' for the whole root.
587 re -- regexp that results must match. Optional.
589 The return value is a list of all (nonempty) subdirectories and
590 files within dir. If dir is '' then a list of top-level files and
591 directories is returned.
593 If a regexp is specified then the basename of each result must
594 match. Matching is case-independent. See pcrepattern(3).
596 return self
._somedir("allfiles", dir, re
)
598 def set(self
, track
, key
, value
):
599 """Set a preference value.
602 track -- the track to modify
603 key -- the preference name
604 value -- the new preference value
606 self
._simple("set", track
, key
, value
)
608 def unset(self
, track
, key
):
609 """Unset a preference value.
612 track -- the track to modify
613 key -- the preference to remove
615 self
._simple("set", track
, key
, value
)
617 def get(self
, track
, key
):
618 """Get a preference value.
621 track -- the track to query
622 key -- the preference to remove
624 The return value is the preference.
626 ret
, details
= self
._simple("get", track
, key
)
632 def prefs(self
, track
):
633 """Get all the preferences for a track.
636 track -- the track to query
638 The return value is a dictionary of all the track's preferences.
639 Note that even nominally numeric values remain encoded as strings.
641 self
._simple("prefs", track
)
643 for line
in self
._body():
646 except _splitError
, s
:
647 raise protocolError(self
.who
, s
.str())
649 raise protocolError(self
.who
, "invalid prefs body line")
653 def _boolean(self
, s
):
656 def exists(self
, track
):
657 """Return true if a track exists
660 track -- the track to check for"""
661 return self
._boolean(self
._simple("exists", track
))
664 """Return true if playing is enabled"""
665 return self
._boolean(self
._simple("enabled"))
667 def random_enabled(self
):
668 """Return true if random play is enabled"""
669 return self
._boolean(self
._simple("random-enabled"))
671 def random_enable(self
):
672 """Enable random play."""
673 self
._simple("random-enable")
675 def random_disable(self
):
676 """Disable random play."""
677 self
._simple("random-disable")
679 def length(self
, track
):
680 """Return the length of a track in seconds.
683 track -- the track to query.
685 ret
, details
= self
._simple("length", track
)
688 def search(self
, words
):
689 """Search for tracks.
692 words -- the set of words to search for.
694 The return value is a list of track path names, all of which contain
695 all of the required words (in their path name, trackname
698 self
._simple("search", _quote(words
))
704 The return value is a list of all tags which apply to at least one
710 """Get server statistics.
712 The return value is list of statistics.
714 self
._simple("stats")
718 """Get all preferences.
720 The return value is an encoded dump of the preferences database.
725 def set_volume(self
, left
, right
):
729 left -- volume for the left speaker.
730 right -- volume for the right speaker.
732 self
._simple("volume", left
, right
)
734 def get_volume(self
):
737 The return value a tuple consisting of the left and right volumes.
739 ret
, details
= self
._simple("volume")
740 return map(int,string
.split(details
))
742 def move(self
, track
, delta
):
743 """Move a track in the queue.
746 track -- the name or ID of the track to move
747 delta -- the number of steps towards the head of the queue to move
749 ret
, details
= self
._simple("move", track
, str(delta
))
752 def moveafter(self
, target
, tracks
):
753 """Move a track in the queue
756 target -- target ID or None
757 tracks -- a list of IDs to move
759 If target is '' or is not in the queue then the tracks are moved to
760 the head of the queue.
762 Otherwise the tracks are moved to just after the target."""
765 self
._simple("moveafter", target
, *tracks
)
767 def log(self
, callback
):
768 """Read event log entries as they happen.
770 Each event log entry is handled by passing it to callback.
772 The callback takes two arguments, the first is the client and the
773 second the line from the event log.
775 The callback should return True to continue or False to stop (don't
776 forget this, or your program will mysteriously misbehave).
778 It is suggested that you use the disorder.monitor class instead of
779 calling this method directly, but this is not mandatory.
781 See disorder_protocol(5) for the event log syntax.
784 callback -- function to call with log entry
786 ret
, details
= self
._simple("log")
789 self
._debug(client
.debug_body
, "<<< %s" % l
)
790 if l
!= '' and l
[0] == '.':
794 if not callback(self
, l
):
796 # tell the server to stop sending, eat the remains of the body,
798 self
._send("version")
803 """Pause the current track."""
804 self
._simple("pause")
807 """Resume after a pause."""
808 self
._simple("resume")
810 def part(self
, track
, context
, part
):
811 """Get a track name part
814 track -- the track to query
815 context -- the context ('sort' or 'display')
816 part -- the desired part (usually 'artist', 'album' or 'title')
818 The return value is the preference
820 ret
, details
= self
._simple("part", track
, context
, part
)
823 def setglobal(self
, key
, value
):
824 """Set a global preference value.
827 key -- the preference name
828 value -- the new preference value
830 self
._simple("set-global", key
, value
)
832 def unsetglobal(self
, key
):
833 """Unset a global preference value.
836 key -- the preference to remove
838 self
._simple("set-global", key
, value
)
840 def getglobal(self
, key
):
841 """Get a global preference value.
844 key -- the preference to look up
846 The return value is the preference
848 ret
, details
= self
._simple("get-global", key
)
854 def make_cookie(self
):
855 """Create a login cookie"""
856 ret
, details
= self
._simple("make-cookie")
860 """Revoke a login cookie"""
861 self
._simple("revoke")
863 def adduser(self
, user
, password
):
865 self
._simple("adduser", user
, password
)
867 def deluser(self
, user
):
869 self
._simple("deluser", user
)
871 def userinfo(self
, user
, key
):
872 """Get user information"""
873 res
, details
= self
._simple("userinfo", user
, key
)
876 return _split(details
)[0]
878 def edituser(self
, user
, key
, value
):
879 """Set user information"""
880 self
._simple("edituser", user
, key
, value
)
885 The return value is a list of all users."""
886 self
._simple("users")
889 ########################################################################
893 # read one response line and return as some suitable string object
895 # If an I/O error occurs, disconnect from the server.
897 # XXX does readline() DTRT regarding character encodings?
899 l
= self
.r
.readline()
900 if not re
.search("\n", l
):
901 raise communicationError(self
.who
, "peer disconnected")
906 return unicode(l
, "UTF-8")
909 # read a response as a (code, details) tuple
911 self
._debug(client
.debug_proto
, "<== %s" % l
)
912 m
= _response
.match(l
)
914 return int(m
.group(1)), m
.group(2)
916 raise protocolError(self
.who
, "invalid response %s")
918 def _send(self
, *command
):
919 # Quote and send a command
921 # Returns the encoded command.
922 quoted
= _quote(command
)
923 self
._debug(client
.debug_proto
, "==> %s" % quoted
)
924 encoded
= quoted
.encode("UTF-8")
926 self
.w
.write(encoded
)
933 raise communicationError(self
.who
, e
)
938 def _simple(self
, *command
):
939 # Issue a simple command, throw an exception on error
941 # If an I/O error occurs, disconnect from the server.
943 # On success or 'normal' errors returns response as a (code, details) tuple
945 # On error raise operationError
946 if self
.state
== 'disconnected':
949 cmd
= self
._send(*command
)
952 res
, details
= self
._response()
953 if res
/ 100 == 2 or res
== 555:
955 raise operationError(res
, details
, cmd
)
958 # Fetch a dot-stuffed body
962 self
._debug(client
.debug_body
, "<<< %s" % l
)
963 if l
!= '' and l
[0] == '.':
969 ########################################################################
970 # Configuration file parsing
972 def _readfile(self
, path
):
973 # Read a configuration file
977 # path -- path of file to read
979 # handlers for various commands
980 def _collection(self
, command
, args
):
982 return "'%s' takes three args" % command
983 self
.config
["collections"].append(args
)
985 def _unary(self
, command
, args
):
987 return "'%s' takes only one arg" % command
988 self
.config
[command
] = args
[0]
990 def _include(self
, command
, args
):
992 return "'%s' takes only one arg" % command
993 self
._readfile(args
[0])
995 def _any(self
, command
, args
):
996 self
.config
[command
] = args
998 # mapping of options to handlers
999 _options
= { "collection": _collection
,
1004 "include": _include
}
1007 for lno
, line
in enumerate(file(path
, "r")):
1009 fields
= _split(line
, 'comments')
1010 except _splitError
, s
:
1011 raise parseError(path
, lno
+ 1, str(s
))
1014 # we just ignore options we don't know about, so as to cope gracefully
1015 # with version skew (and nothing to do with implementor laziness)
1016 if command
in _options
:
1017 e
= _options
[command
](self
, command
, fields
[1:])
1019 self
._parseError(path
, lno
+ 1, e
)
1021 def _parseError(self
, path
, lno
, s
):
1022 raise parseError(path
, lno
, s
)
1024 ########################################################################
1028 """DisOrder event log monitor class
1030 Intended to be subclassed with methods corresponding to event log messages
1031 the implementor cares about over-ridden."""
1033 def __init__(self
, c
=None):
1034 """Constructor for the monitor class
1036 Can be passed a client to use. If none is specified then one
1037 will be created specially for the purpose.
1046 """Start monitoring logs. Continues monitoring until one of the
1047 message-specific methods returns False. Can be called more than once
1048 (but not recursively!)"""
1049 self
.c
.log(self
._callback
)
1052 """Return the timestamp of the current (or most recent) event log entry"""
1053 return self
.timestamp
1055 def _callback(self
, c
, line
):
1059 return self
.invalid(line
)
1061 return self
.invalid(line
)
1062 self
.timestamp
= int(bits
[0], 16)
1065 if keyword
== 'completed':
1067 return self
.completed(bits
[0])
1068 elif keyword
== 'failed':
1070 return self
.failed(bits
[0], bits
[1])
1071 elif keyword
== 'moved':
1076 return self
.invalid(line
)
1077 return self
.moved(bits
[0], n
, bits
[2])
1078 elif keyword
== 'playing':
1080 return self
.playing(bits
[0], None)
1081 elif len(bits
) == 2:
1082 return self
.playing(bits
[0], bits
[1])
1083 elif keyword
== 'queue' or keyword
== 'recent-added':
1085 q
= _list2dict(bits
)
1087 return self
.invalid(line
)
1088 if keyword
== 'queue':
1089 return self
.queue(q
)
1090 if keyword
== 'recent-added':
1091 return self
.recent_added(q
)
1092 elif keyword
== 'recent-removed':
1094 return self
.recent_removed(bits
[0])
1095 elif keyword
== 'removed':
1097 return self
.removed(bits
[0], None)
1098 elif len(bits
) == 2:
1099 return self
.removed(bits
[0], bits
[1])
1100 elif keyword
== 'scratched':
1102 return self
.scratched(bits
[0], bits
[1])
1103 return self
.invalid(line
)
1105 def completed(self
, track
):
1106 """Called when a track completes.
1109 track -- track that completed"""
1112 def failed(self
, track
, error
):
1113 """Called when a player suffers an error.
1116 track -- track that failed
1117 error -- error indicator"""
1120 def moved(self
, id, offset
, user
):
1121 """Called when a track is moved in the queue.
1124 id -- queue entry ID
1125 offset -- distance moved
1126 user -- user responsible"""
1129 def playing(self
, track
, user
):
1130 """Called when a track starts playing.
1133 track -- track that has started
1134 user -- user that submitted track, or None"""
1138 """Called when a track is added to the queue.
1141 q -- dictionary of new queue entry"""
1144 def recent_added(self
, q
):
1145 """Called when a track is added to the recently played list
1148 q -- dictionary of new queue entry"""
1151 def recent_removed(self
, id):
1152 """Called when a track is removed from the recently played list
1155 id -- ID of removed entry (always the oldest)"""
1158 def removed(self
, id, user
):
1159 """Called when a track is removed from the queue, either manually
1160 or in order to play it.
1163 id -- ID of removed entry
1164 user -- user responsible (or None if we're playing this track)"""
1167 def scratched(self
, track
, user
):
1168 """Called when a track is scratched
1171 track -- track that was scratched
1172 user -- user responsible"""
1175 def invalid(self
, line
):
1176 """Called when an event log line cannot be interpreted
1179 line -- line that could not be understood"""
1184 # py-indent-offset:2