2 # Copyright (C) 2004, 2005, 2007, 2008 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 3 of the License, or
7 # (at your option) any later version.
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.
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/>.
18 """Python support for DisOrder
20 Provides disorder.client, a class for accessing a DisOrder server.
24 #! /usr/bin/env python
33 #! /usr/bin/env python
37 for path in sys.argv[1:]:
40 See disorder_protocol(5) for details of the communication protocol.
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.
57 _configfile
= "pkgconfdir/config"
58 _dbhome
= "pkgstatedir"
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]*")
67 _response
= re
.compile("([0-9]{3}) ?(.*)")
71 ########################################################################
74 class Error(Exception):
75 """Base class for DisOrder exceptions."""
77 class _splitError(Error
):
79 def __init__(self
, value
):
82 return str(self
.value
)
84 class parseError(Error
):
85 """Error parsing the configuration file."""
86 def __init__(self
, path
, line
, details
):
89 self
.details
= details
91 return "%s:%d: %s" %
(self
.path
, self
.line
, self
.details
)
93 class protocolError(Error
):
94 """DisOrder control protocol error.
96 Indicates a mismatch between the client and server's understanding of
99 def __init__(self
, who
, error
):
103 return "%s: %s" %
(self
.who
, str(self
.error
))
105 class operationError(Error
):
106 """DisOrder control protocol error response.
108 Indicates that an operation failed (e.g. an attempt to play a
109 nonexistent track). The connection should still be usable.
111 def __init__(self
, res
, details
, cmd
=None):
114 self
.details_
= details
116 """Return the complete response string from the server, with the
117 command if available.
119 Excludes the final newline.
121 if self
.cmd_
is None:
122 return "%d %s" %
(self
.res_
, self
.details_
)
124 return "%d %s [%s]" %
(self
.res_
, self
.details_
, self
.cmd_
)
126 """Return the response code from the server."""
129 """Returns the detail string from the server."""
132 class communicationError(Error
):
133 """DisOrder control protocol communication error.
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
140 def __init__(self
, who
, error
):
144 return "%s: %s" %
(self
.who
, str(self
.error
))
146 ########################################################################
147 # DisOrder-specific text processing
150 # Unescape the contents of a string
154 # s -- string to unescape
156 s
= re
.sub("\\\\n", "\n", s
)
157 s
= re
.sub("\\\\(.)", "\\1", s
)
160 def _split(s
, *comments
):
161 # Split a string into fields according to the usual Disorder string splitting
166 # s -- string to parse
167 # comments -- if present, parse comments
171 # On success, a list of fields is returned.
173 # On error, disorder.parseError is thrown.
178 if comments
and s
[0] == '#':
185 # pick of quoted fields of both kinds
190 fields
.append(_unescape(m
.group(1)))
193 # and unquoted fields
194 m
= _unquoted
.match(s
)
196 fields
.append(m
.group(0))
199 # anything left must be in error
200 if s
[0] == '"' or s
[0] == '\'':
201 raise _splitError("invalid quoted string")
203 raise _splitError("syntax error")
207 # Escape the contents of a string
211 # s -- string to escape
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
)
221 # Quote a list of values
222 return ' '.join(map(_escape
, list))
225 # Return the value of s in a form suitable for writing to stderr
226 return s
.encode(locale
.nl_langinfo(locale
.CODESET
), 'replace')
229 # Convert a list of the form [k1, v1, k2, v2, ..., kN, vN]
230 # to a dictionary {k1:v1, k2:v2, ..., kN:vN}
238 except StopIteration:
243 # parse a queue entry
244 return _list2dict(_split(s
))
246 ########################################################################
250 """DisOrder client class.
252 This class provides access to the DisOrder server either on this
253 machine or across the internet.
255 The server to connect to, and the username and password to use, are
256 determined from the configuration files as described in 'man
259 All methods will connect if necessary, as soon as you have a
260 disorder.client object you can start calling operational methods on
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.
266 All methods block until they complete.
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
277 def __init__(self
, user
=None, password
=None):
278 """Constructor for DisOrder client class.
280 The constructor reads the configuration file, but does not connect
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.
287 The constructor Raises parseError() if the configuration file is not
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
,
296 self
.password
= password
297 home
= os
.getenv("HOME")
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'
310 def debug(self
, bits
):
311 """Enable or disable protocol debugging. Debug messages are written
315 bits -- bitmap of operations that should generate debug information
318 debug_proto -- dump control protocol messages (excluding bodies)
319 debug_body -- dump control protocol message bodies
321 self
.debugging
= bits
323 def _debug(self
, bit
, s
):
325 if self
.debugging
& bit
:
326 sys
.stderr
.write(_sanitize(s
))
327 sys
.stderr
.write("\n")
330 def connect(self
, cookie
=None):
331 """c.connect(cookie=None)
333 Connect to the DisOrder server and authenticate.
335 Raises communicationError if connection fails and operationError if
336 authentication fails (in which case disconnection is automatic).
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,
342 Other operations automatically connect if we're not already
343 connected, so it is not strictly necessary to call this method.
345 If COOKIE is specified then that is used to log in instead of
346 the username/password.
348 if self
.state
== 'disconnected':
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
355 a
= socket
.getaddrinfo(None, c
[0],
361 a
= socket
.getaddrinfo(c
[0], c
[1],
367 s
= socket
.socket(a
[0], a
[1], a
[2]);
369 self
.who
= "%s" % a
[3]
371 s
= socket
.socket(socket
.AF_UNIX
, socket
.SOCK_STREAM
);
372 self
.who
= self
.config
['home'] + os
.sep
+ "socket"
374 self
.w
= s
.makefile("wb")
375 self
.r
= s
.makefile("rb")
376 (res
, details
) = self
._simple()
377 (protocol
, algo
, challenge
) = _split(details
)
379 raise communicationError(self
.who
,
380 "unknown protocol version %s" % protocol
)
382 if self
.user
is None:
383 user
= self
.config
['username']
386 if self
.password
is None:
387 password
= self
.config
['password']
389 password
= self
.password
390 # TODO support algorithms other than SHA-1
393 h
.update(binascii
.unhexlify(challenge
))
394 self
._simple("user", user
, h
.hexdigest())
396 self
._simple("cookie", cookie
)
397 self
.state
= 'connected'
398 except socket
.error
, e
:
400 raise communicationError(self
.who
, e
)
405 def _disconnect(self
):
406 # disconnect from the server, whatever state we are in
412 self
.state
= 'disconnected'
414 ########################################################################
417 def play(self
, track
):
421 track -- the path of the track to play.
423 Returns the ID of the new queue entry.
425 Note that queue IDs are unicode strings (because all track
426 information values are unicode strings).
428 res
, details
= self
._simple("play", track
)
429 return unicode(details
) # because it's unicode in queue() output
431 def remove(self
, track
):
432 """Remove a track from the queue.
435 track -- the path or ID of the track to remove.
437 self
._simple("remove", track
)
440 """Enable playing."""
441 self
._simple("enable")
443 def disable(self
, *now
):
447 now -- if present (with any value), the current track is stopped
451 self
._simple("disable", "now")
453 self
._simple("disable")
455 def scratch(self
, *id):
456 """Scratch the currently playing track.
459 id -- if present, the ID of the track to scratch.
462 self
._simple("scratch", id[0])
464 self
._simple("scratch")
467 """Shut down the server.
469 Only trusted users can perform this operation.
471 self
._simple("shutdown")
473 def reconfigure(self
):
474 """Make the server reload its configuration.
476 Only trusted users can perform this operation.
478 self
._simple("reconfigure")
480 def rescan(self
, *flags
):
481 """Rescan one or more collections.
483 Only trusted users can perform this operation.
485 self
._simple("rescan", *flags
)
488 """Return the server's version number."""
489 return _split(self
._simple("version")[1])[0]
492 """Return the currently playing track.
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.
498 If no track is playing then None is returned."""
499 res
, details
= self
._simple("playing")
502 return _queueEntry(details
)
503 except _splitError
, s
:
504 raise protocolError(self
.who
, s
.str())
508 def _somequeue(self
, command
):
509 self
._simple(command
)
511 return map(lambda s
: _queueEntry(s
), self
._body())
512 except _splitError
, s
:
513 raise protocolError(self
.who
, s
.str())
516 """Return a list of recently played tracks.
518 The return value is a list of dictionaries corresponding to
519 recently played tracks. The oldest track comes first.
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")
526 """Return the current queue.
528 The return value is a list of dictionaries corresponding to
529 recently played tracks. The next track to be played comes first.
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")
535 def _somedir(self
, command
, dir, re
):
537 self
._simple(command
, dir, re
[0])
539 self
._simple(command
, dir)
542 def directories(self
, dir, *re
):
543 """List subdirectories of a directory.
546 dir -- directory to list, or '' for the whole root.
547 re -- regexp that results must match. Optional.
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.
552 If a regexp is specified then the basename of each result must
553 match. Matching is case-independent. See pcrepattern(3).
555 return self
._somedir("dirs", dir, re
)
557 def files(self
, dir, *re
):
558 """List files within a directory.
561 dir -- directory to list, or '' for the whole root.
562 re -- regexp that results must match. Optional.
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.
567 If a regexp is specified then the basename of each result must
568 match. Matching is case-independent. See pcrepattern(3).
570 return self
._somedir("files", dir, re
)
572 def allfiles(self
, dir, *re
):
573 """List subdirectories and files within a directory.
576 dir -- directory to list, or '' for the whole root.
577 re -- regexp that results must match. Optional.
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.
583 If a regexp is specified then the basename of each result must
584 match. Matching is case-independent. See pcrepattern(3).
586 return self
._somedir("allfiles", dir, re
)
588 def set(self
, track
, key
, value
):
589 """Set a preference value.
592 track -- the track to modify
593 key -- the preference name
594 value -- the new preference value
596 self
._simple("set", track
, key
, value
)
598 def unset(self
, track
, key
):
599 """Unset a preference value.
602 track -- the track to modify
603 key -- the preference to remove
605 self
._simple("set", track
, key
, value
)
607 def get(self
, track
, key
):
608 """Get a preference value.
611 track -- the track to query
612 key -- the preference to remove
614 The return value is the preference.
616 ret
, details
= self
._simple("get", track
, key
)
620 return _split(details
)[0]
622 def prefs(self
, track
):
623 """Get all the preferences for a track.
626 track -- the track to query
628 The return value is a dictionary of all the track's preferences.
629 Note that even nominally numeric values remain encoded as strings.
631 self
._simple("prefs", track
)
633 for line
in self
._body():
636 except _splitError
, s
:
637 raise protocolError(self
.who
, s
.str())
639 raise protocolError(self
.who
, "invalid prefs body line")
643 def _boolean(self
, s
):
646 def exists(self
, track
):
647 """Return true if a track exists
650 track -- the track to check for"""
651 return self
._boolean(self
._simple("exists", track
))
654 """Return true if playing is enabled"""
655 return self
._boolean(self
._simple("enabled"))
657 def random_enabled(self
):
658 """Return true if random play is enabled"""
659 return self
._boolean(self
._simple("random-enabled"))
661 def random_enable(self
):
662 """Enable random play."""
663 self
._simple("random-enable")
665 def random_disable(self
):
666 """Disable random play."""
667 self
._simple("random-disable")
669 def length(self
, track
):
670 """Return the length of a track in seconds.
673 track -- the track to query.
675 ret
, details
= self
._simple("length", track
)
678 def search(self
, words
):
679 """Search for tracks.
682 words -- the set of words to search for.
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
688 self
._simple("search", _quote(words
))
694 The return value is a list of all tags which apply to at least one
700 """Get server statistics.
702 The return value is list of statistics.
704 self
._simple("stats")
708 """Get all preferences.
710 The return value is an encoded dump of the preferences database.
715 def set_volume(self
, left
, right
):
719 left -- volume for the left speaker.
720 right -- volume for the right speaker.
722 self
._simple("volume", left
, right
)
724 def get_volume(self
):
727 The return value a tuple consisting of the left and right volumes.
729 ret
, details
= self
._simple("volume")
730 return map(int,string
.split(details
))
732 def move(self
, track
, delta
):
733 """Move a track in the queue.
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
739 ret
, details
= self
._simple("move", track
, str(delta
))
742 def moveafter(self
, target
, tracks
):
743 """Move a track in the queue
746 target -- target ID or None
747 tracks -- a list of IDs to move
749 If target is '' or is not in the queue then the tracks are moved to
750 the head of the queue.
752 Otherwise the tracks are moved to just after the target."""
755 self
._simple("moveafter", target
, *tracks
)
757 def log(self
, callback
):
758 """Read event log entries as they happen.
760 Each event log entry is handled by passing it to callback.
762 The callback takes two arguments, the first is the client and the
763 second the line from the event log.
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
770 It is suggested that you use the disorder.monitor class instead of
771 calling this method directly, but this is not mandatory.
773 See disorder_protocol(5) for the event log syntax.
776 callback -- function to call with log entry
778 ret
, details
= self
._simple("log")
781 self
._debug(client
.debug_body
, "<<< %s" % l
)
782 if l
!= '' and l
[0] == '.':
786 if not callback(self
, l
):
790 """Pause the current track."""
791 self
._simple("pause")
794 """Resume after a pause."""
795 self
._simple("resume")
797 def part(self
, track
, context
, part
):
798 """Get a track name part
801 track -- the track to query
802 context -- the context ('sort' or 'display')
803 part -- the desired part (usually 'artist', 'album' or 'title')
805 The return value is the preference
807 ret
, details
= self
._simple("part", track
, context
, part
)
808 return _split(details
)[0]
810 def setglobal(self
, key
, value
):
811 """Set a global preference value.
814 key -- the preference name
815 value -- the new preference value
817 self
._simple("set-global", key
, value
)
819 def unsetglobal(self
, key
):
820 """Unset a global preference value.
823 key -- the preference to remove
825 self
._simple("set-global", key
, value
)
827 def getglobal(self
, key
):
828 """Get a global preference value.
831 key -- the preference to look up
833 The return value is the preference
835 ret
, details
= self
._simple("get-global", key
)
839 return _split(details
)[0]
841 def make_cookie(self
):
842 """Create a login cookie"""
843 ret
, details
= self
._simple("make-cookie")
844 return _split(details
)[0]
847 """Revoke a login cookie"""
848 self
._simple("revoke")
850 def adduser(self
, user
, password
):
852 self
._simple("adduser", user
, password
)
854 def deluser(self
, user
):
856 self
._simple("deluser", user
)
858 def userinfo(self
, user
, key
):
859 """Get user information"""
860 res
, details
= self
._simple("userinfo", user
, key
)
863 return _split(details
)[0]
865 def edituser(self
, user
, key
, value
):
866 """Set user information"""
867 self
._simple("edituser", user
, key
, value
)
872 The return value is a list of all users."""
873 self
._simple("users")
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]
881 def confirm(self
, confirmation
):
882 """Confirm a user registration"""
883 res
, details
= self
._simple("confirm", confirmation
)
885 def schedule_list(self
):
886 """Get a list of scheduled events """
887 self
._simple("schedule-list")
890 def schedule_del(self
, event
):
891 """Delete a scheduled event"""
892 self
._simple("schedule-del", event
)
894 def schedule_get(self
, event
):
895 """Get the details for an event as a dict (returns None if
897 res
, details
= self
._simple("schedule-get", event
)
901 for line
in self
._body():
906 def schedule_add(self
, when
, priority
, action
, *rest
):
907 """Add a scheduled event"""
908 self
._simple("schedule-add", str(when
), priority
, action
, *rest
)
911 """Adopt a randomly picked track"""
912 self
._simple("adopt", id)
914 def playlist_delete(self
, playlist
):
915 """Delete a playlist"""
916 res
, details
= self
._simple("playlist-delete", playlist
)
918 raise operationError(res
, details
, "playlist-delete")
920 def playlist_get(self
, playlist
):
921 """Get the contents of a playlist
923 The return value is an array of track names, or None if there is no
925 res
, details
= self
._simple("playlist-get", playlist
)
930 def playlist_lock(self
, playlist
):
931 """Lock a playlist. Playlists can only be modified when locked."""
932 self
._simple("playlist-lock", playlist
)
934 def playlist_unlock(self
):
935 """Unlock the locked playlist."""
936 self
._simple("playlist-unlock")
938 def playlist_set(self
, playlist
, tracks
):
939 """Set the contents of a playlist. The playlist must be locked.
942 playlist -- Playlist to set
943 tracks -- Array of tracks"""
944 self
._simple_body(tracks
, "playlist-set", playlist
)
946 def playlist_set_share(self
, playlist
, share
):
947 """Set the sharing status of a playlist"""
948 self
._simple("playlist-set-share", playlist
, share
)
950 def playlist_get_share(self
, playlist
):
951 """Returns the sharing status of a playlist"""
952 res
, details
= self
._simple("playlist-get-share", playlist
)
955 return _split(details
)[0]
958 """Returns the list of visible playlists"""
959 self
._simple("playlists")
962 ########################################################################
966 # read one response line and return as some suitable string object
968 # If an I/O error occurs, disconnect from the server.
970 # XXX does readline() DTRT regarding character encodings?
972 l
= self
.r
.readline()
973 if not re
.search("\n", l
):
974 raise communicationError(self
.who
, "peer disconnected")
979 return unicode(l
, "UTF-8")
982 # read a response as a (code, details) tuple
984 self
._debug(client
.debug_proto
, "<== %s" % l
)
985 m
= _response
.match(l
)
987 return int(m
.group(1)), m
.group(2)
989 raise protocolError(self
.who
, "invalid response %s")
991 def _send(self
, body
, *command
):
992 # Quote and send a command and optional body
994 # Returns the encoded command.
995 quoted
= _quote(command
)
996 self
._debug(client
.debug_proto
, "==> %s" % quoted
)
997 encoded
= quoted
.encode("UTF-8")
999 self
.w
.write(encoded
)
1013 raise communicationError(self
.who
, e
)
1018 def _simple(self
, *command
):
1019 # Issue a simple command, throw an exception on error
1021 # If an I/O error occurs, disconnect from the server.
1023 # On success or 'normal' errors returns response as a (code, details) tuple
1025 # On error raise operationError
1026 return self
._simple_body(None, *command
)
1028 def _simple_body(self
, body
, *command
):
1029 # Issue a simple command with optional body, throw an exception on error
1031 # If an I/O error occurs, disconnect from the server.
1033 # On success or 'normal' errors returns response as a (code, details) tuple
1035 # On error raise operationError
1036 if self
.state
== 'disconnected':
1039 cmd
= self
._send(body
, *command
)
1042 res
, details
= self
._response()
1043 if res
/ 100 == 2 or res
== 555:
1045 raise operationError(res
, details
, cmd
)
1048 # Fetch a dot-stuffed body
1052 self
._debug(client
.debug_body
, "<<< %s" % l
)
1053 if l
!= '' and l
[0] == '.':
1059 ########################################################################
1060 # Configuration file parsing
1062 def _readfile(self
, path
):
1063 # Read a configuration file
1067 # path -- path of file to read
1069 # handlers for various commands
1070 def _collection(self
, command
, args
):
1072 return "'%s' takes three args" % command
1073 self
.config
["collections"].append(args
)
1075 def _unary(self
, command
, args
):
1077 return "'%s' takes only one arg" % command
1078 self
.config
[command
] = args
[0]
1080 def _include(self
, command
, args
):
1082 return "'%s' takes only one arg" % command
1083 self
._readfile(args
[0])
1085 def _any(self
, command
, args
):
1086 self
.config
[command
] = args
1088 # mapping of options to handlers
1089 _options
= { "collection": _collection
,
1094 "include": _include
}
1097 for lno
, line
in enumerate(file(path
, "r")):
1099 fields
= _split(line
, 'comments')
1100 except _splitError
, s
:
1101 raise parseError(path
, lno
+ 1, str(s
))
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:])
1109 self
._parseError(path
, lno
+ 1, e
)
1111 def _parseError(self
, path
, lno
, s
):
1112 raise parseError(path
, lno
, s
)
1114 ########################################################################
1118 """DisOrder event log monitor class
1120 Intended to be subclassed with methods corresponding to event log
1121 messages the implementor cares about over-ridden."""
1123 def __init__(self
, c
=None):
1124 """Constructor for the monitor class
1126 Can be passed a client to use. If none is specified then one
1127 will be created specially for the purpose.
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
)
1142 """Return the timestamp of the current (or most recent) event log entry"""
1143 return self
.timestamp
1145 def _callback(self
, c
, line
):
1149 return self
.invalid(line
)
1151 return self
.invalid(line
)
1152 self
.timestamp
= int(bits
[0], 16)
1155 if keyword
== 'completed':
1157 return self
.completed(bits
[0])
1158 elif keyword
== 'failed':
1160 return self
.failed(bits
[0], bits
[1])
1161 elif keyword
== 'moved':
1166 return self
.invalid(line
)
1167 return self
.moved(bits
[0], n
, bits
[2])
1168 elif keyword
== 'playing':
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':
1175 q
= _list2dict(bits
)
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':
1184 return self
.recent_removed(bits
[0])
1185 elif keyword
== 'removed':
1187 return self
.removed(bits
[0], None)
1188 elif len(bits
) == 2:
1189 return self
.removed(bits
[0], bits
[1])
1190 elif keyword
== 'scratched':
1192 return self
.scratched(bits
[0], bits
[1])
1193 elif keyword
== 'rescanned':
1194 return self
.rescanned()
1195 return self
.invalid(line
)
1197 def completed(self
, track
):
1198 """Called when a track completes.
1201 track -- track that completed"""
1204 def failed(self
, track
, error
):
1205 """Called when a player suffers an error.
1208 track -- track that failed
1209 error -- error indicator"""
1212 def moved(self
, id, offset
, user
):
1213 """Called when a track is moved in the queue.
1216 id -- queue entry ID
1217 offset -- distance moved
1218 user -- user responsible"""
1221 def playing(self
, track
, user
):
1222 """Called when a track starts playing.
1225 track -- track that has started
1226 user -- user that submitted track, or None"""
1230 """Called when a track is added to the queue.
1233 q -- dictionary of new queue entry"""
1236 def recent_added(self
, q
):
1237 """Called when a track is added to the recently played list
1240 q -- dictionary of new queue entry"""
1243 def recent_removed(self
, id):
1244 """Called when a track is removed from the recently played list
1247 id -- ID of removed entry (always the oldest)"""
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.
1255 id -- ID of removed entry
1256 user -- user responsible (or None if we're playing this track)"""
1259 def scratched(self
, track
, user
):
1260 """Called when a track is scratched
1263 track -- track that was scratched
1264 user -- user responsible"""
1267 def invalid(self
, line
):
1268 """Called when an event log line cannot be interpreted
1271 line -- line that could not be understood"""
1274 def rescanned(self
):
1275 """Called when a rescan completes"""
1280 # py-indent-offset:2