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 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
119 command if available.
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
, details
) = self
._simple()
379 (protocol
, algo
, challenge
) = _split(details
)
381 raise communicationError(self
.who
,
382 "unknown protocol version %s" % protocol
)
384 if self
.user
is None:
385 user
= self
.config
['username']
388 if self
.password
is None:
389 password
= self
.config
['password']
391 password
= self
.password
392 # TODO support algorithms other than SHA-1
395 h
.update(binascii
.unhexlify(challenge
))
396 self
._simple("user", user
, h
.hexdigest())
398 self
._simple("cookie", cookie
)
399 self
.state
= 'connected'
400 except socket
.error
, e
:
402 raise communicationError(self
.who
, e
)
407 def _disconnect(self
):
408 # disconnect from the server, whatever state we are in
414 self
.state
= 'disconnected'
416 ########################################################################
419 def play(self
, track
):
423 track -- the path of the track to play.
425 Returns the ID of the new queue entry.
427 Note that queue IDs are unicode strings (because all track
428 information values are unicode strings).
430 res
, details
= self
._simple("play", track
)
431 return unicode(details
) # because it's unicode in queue() output
433 def remove(self
, track
):
434 """Remove a track from the queue.
437 track -- the path or ID of the track to remove.
439 self
._simple("remove", track
)
442 """Enable playing."""
443 self
._simple("enable")
445 def disable(self
, *now
):
449 now -- if present (with any value), the current track is stopped
453 self
._simple("disable", "now")
455 self
._simple("disable")
457 def scratch(self
, *id):
458 """Scratch the currently playing track.
461 id -- if present, the ID of the track to scratch.
464 self
._simple("scratch", id[0])
466 self
._simple("scratch")
469 """Shut down the server.
471 Only trusted users can perform this operation.
473 self
._simple("shutdown")
475 def reconfigure(self
):
476 """Make the server reload its configuration.
478 Only trusted users can perform this operation.
480 self
._simple("reconfigure")
482 def rescan(self
, *flags
):
483 """Rescan one or more collections.
485 Only trusted users can perform this operation.
487 self
._simple("rescan", *flags
)
490 """Return the server's version number."""
491 return _split(self
._simple("version")[1])[0]
494 """Return the currently playing track.
496 If a track is playing then it is returned as a dictionary. See
497 disorder_protocol(5) for the meanings of the keys. All keys are
498 plain strings but the values will be unicode strings.
500 If no track is playing then None is returned."""
501 res
, details
= self
._simple("playing")
504 return _queueEntry(details
)
505 except _splitError
, s
:
506 raise protocolError(self
.who
, s
.str())
510 def _somequeue(self
, command
):
511 self
._simple(command
)
513 return map(lambda s
: _queueEntry(s
), self
._body())
514 except _splitError
, s
:
515 raise protocolError(self
.who
, s
.str())
518 """Return a list of recently played tracks.
520 The return value is a list of dictionaries corresponding to
521 recently played tracks. The oldest track comes first.
523 See disorder_protocol(5) for the meanings of the keys. All keys are
524 plain strings but the values will be unicode strings."""
525 return self
._somequeue("recent")
528 """Return the current queue.
530 The return value is a list of dictionaries corresponding to
531 recently played tracks. The next track to be played comes first.
533 See disorder_protocol(5) for the meanings of the keys.
534 All keys are plain strings but the values will be unicode strings."""
535 return self
._somequeue("queue")
537 def _somedir(self
, command
, dir, re
):
539 self
._simple(command
, dir, re
[0])
541 self
._simple(command
, dir)
544 def directories(self
, dir, *re
):
545 """List subdirectories of a directory.
548 dir -- directory to list, or '' for the whole root.
549 re -- regexp that results must match. Optional.
551 The return value is a list of the (nonempty) subdirectories of dir.
552 If dir is '' then a list of top-level directories is returned.
554 If a regexp is specified then the basename of each result must
555 match. Matching is case-independent. See pcrepattern(3).
557 return self
._somedir("dirs", dir, re
)
559 def files(self
, dir, *re
):
560 """List files within a directory.
563 dir -- directory to list, or '' for the whole root.
564 re -- regexp that results must match. Optional.
566 The return value is a list of playable files in dir. If dir is ''
567 then a list of top-level files is returned.
569 If a regexp is specified then the basename of each result must
570 match. Matching is case-independent. See pcrepattern(3).
572 return self
._somedir("files", dir, re
)
574 def allfiles(self
, dir, *re
):
575 """List subdirectories and files within a directory.
578 dir -- directory to list, or '' for the whole root.
579 re -- regexp that results must match. Optional.
581 The return value is a list of all (nonempty) subdirectories and
582 files within dir. If dir is '' then a list of top-level files and
583 directories is returned.
585 If a regexp is specified then the basename of each result must
586 match. Matching is case-independent. See pcrepattern(3).
588 return self
._somedir("allfiles", dir, re
)
590 def set(self
, track
, key
, value
):
591 """Set a preference value.
594 track -- the track to modify
595 key -- the preference name
596 value -- the new preference value
598 self
._simple("set", track
, key
, value
)
600 def unset(self
, track
, key
):
601 """Unset a preference value.
604 track -- the track to modify
605 key -- the preference to remove
607 self
._simple("set", track
, key
, value
)
609 def get(self
, track
, key
):
610 """Get a preference value.
613 track -- the track to query
614 key -- the preference to remove
616 The return value is the preference.
618 ret
, details
= self
._simple("get", track
, key
)
622 return _split(details
)[0]
624 def prefs(self
, track
):
625 """Get all the preferences for a track.
628 track -- the track to query
630 The return value is a dictionary of all the track's preferences.
631 Note that even nominally numeric values remain encoded as strings.
633 self
._simple("prefs", track
)
635 for line
in self
._body():
638 except _splitError
, s
:
639 raise protocolError(self
.who
, s
.str())
641 raise protocolError(self
.who
, "invalid prefs body line")
645 def _boolean(self
, s
):
648 def exists(self
, track
):
649 """Return true if a track exists
652 track -- the track to check for"""
653 return self
._boolean(self
._simple("exists", track
))
656 """Return true if playing is enabled"""
657 return self
._boolean(self
._simple("enabled"))
659 def random_enabled(self
):
660 """Return true if random play is enabled"""
661 return self
._boolean(self
._simple("random-enabled"))
663 def random_enable(self
):
664 """Enable random play."""
665 self
._simple("random-enable")
667 def random_disable(self
):
668 """Disable random play."""
669 self
._simple("random-disable")
671 def length(self
, track
):
672 """Return the length of a track in seconds.
675 track -- the track to query.
677 ret
, details
= self
._simple("length", track
)
680 def search(self
, words
):
681 """Search for tracks.
684 words -- the set of words to search for.
686 The return value is a list of track path names, all of which contain
687 all of the required words (in their path name, trackname
690 self
._simple("search", _quote(words
))
696 The return value is a list of all tags which apply to at least one
702 """Get server statistics.
704 The return value is list of statistics.
706 self
._simple("stats")
710 """Get all preferences.
712 The return value is an encoded dump of the preferences database.
717 def set_volume(self
, left
, right
):
721 left -- volume for the left speaker.
722 right -- volume for the right speaker.
724 self
._simple("volume", left
, right
)
726 def get_volume(self
):
729 The return value a tuple consisting of the left and right volumes.
731 ret
, details
= self
._simple("volume")
732 return map(int,string
.split(details
))
734 def move(self
, track
, delta
):
735 """Move a track in the queue.
738 track -- the name or ID of the track to move
739 delta -- the number of steps towards the head of the queue to move
741 ret
, details
= self
._simple("move", track
, str(delta
))
744 def moveafter(self
, target
, tracks
):
745 """Move a track in the queue
748 target -- target ID or None
749 tracks -- a list of IDs to move
751 If target is '' or is not in the queue then the tracks are moved to
752 the head of the queue.
754 Otherwise the tracks are moved to just after the target."""
757 self
._simple("moveafter", target
, *tracks
)
759 def log(self
, callback
):
760 """Read event log entries as they happen.
762 Each event log entry is handled by passing it to callback.
764 The callback takes two arguments, the first is the client and the
765 second the line from the event log.
767 The callback should return True to continue or False to stop (don't
768 forget this, or your program will mysteriously misbehave). Once you
769 stop reading the log the connection is useless and should be
772 It is suggested that you use the disorder.monitor class instead of
773 calling this method directly, but this is not mandatory.
775 See disorder_protocol(5) for the event log syntax.
778 callback -- function to call with log entry
780 ret
, details
= self
._simple("log")
783 self
._debug(client
.debug_body
, "<<< %s" % l
)
784 if l
!= '' and l
[0] == '.':
788 if not callback(self
, l
):
792 """Pause the current track."""
793 self
._simple("pause")
796 """Resume after a pause."""
797 self
._simple("resume")
799 def part(self
, track
, context
, part
):
800 """Get a track name part
803 track -- the track to query
804 context -- the context ('sort' or 'display')
805 part -- the desired part (usually 'artist', 'album' or 'title')
807 The return value is the preference
809 ret
, details
= self
._simple("part", track
, context
, part
)
810 return _split(details
)[0]
812 def setglobal(self
, key
, value
):
813 """Set a global preference value.
816 key -- the preference name
817 value -- the new preference value
819 self
._simple("set-global", key
, value
)
821 def unsetglobal(self
, key
):
822 """Unset a global preference value.
825 key -- the preference to remove
827 self
._simple("set-global", key
, value
)
829 def getglobal(self
, key
):
830 """Get a global preference value.
833 key -- the preference to look up
835 The return value is the preference
837 ret
, details
= self
._simple("get-global", key
)
841 return _split(details
)[0]
843 def make_cookie(self
):
844 """Create a login cookie"""
845 ret
, details
= self
._simple("make-cookie")
846 return _split(details
)[0]
849 """Revoke a login cookie"""
850 self
._simple("revoke")
852 def adduser(self
, user
, password
):
854 self
._simple("adduser", user
, password
)
856 def deluser(self
, user
):
858 self
._simple("deluser", user
)
860 def userinfo(self
, user
, key
):
861 """Get user information"""
862 res
, details
= self
._simple("userinfo", user
, key
)
865 return _split(details
)[0]
867 def edituser(self
, user
, key
, value
):
868 """Set user information"""
869 self
._simple("edituser", user
, key
, value
)
874 The return value is a list of all users."""
875 self
._simple("users")
878 def register(self
, username
, password
, email
):
879 """Register a user"""
880 res
, details
= self
._simple("register", username
, password
, email
)
881 return _split(details
)[0]
883 def confirm(self
, confirmation
):
884 """Confirm a user registration"""
885 res
, details
= self
._simple("confirm", confirmation
)
887 def schedule_list(self
):
888 """Get a list of scheduled events """
889 self
._simple("schedule-list")
892 def schedule_del(self
, event
):
893 """Delete a scheduled event"""
894 self
._simple("schedule-del", event
)
896 def schedule_get(self
, event
):
897 """Get the details for an event as a dict (returns None if
899 res
, details
= self
._simple("schedule-get", event
)
903 for line
in self
._body():
908 def schedule_add(self
, when
, priority
, action
, *rest
):
909 """Add a scheduled event"""
910 self
._simple("schedule-add", str(when
), priority
, action
, *rest
)
912 def playlist_delete(self
, playlist
):
913 """Delete a playlist"""
914 self
._simple("playlist-delete", playlist
)
916 def playlist_get(self
, playlist
):
917 """Get the contents of a playlist
919 The return value is an array of track names, or None if there is no
921 res
, details
= self
._simple("playlist-get", playlist
)
926 def playlist_lock(self
, playlist
):
927 """Lock a playlist. Playlists can only be modified when locked."""
928 self
._simple("playlist-lock", playlist
)
930 def playlist_unlock(self
):
931 """Unlock the locked playlist."""
932 self
._simple("playlist-unlock")
934 def playlist_set(self
, playlist
, tracks
):
935 """Set the contents of a playlist. The playlist must be locked.
938 playlist -- Playlist to set
939 tracks -- Array of tracks"""
940 self
._simple_body(tracks
, "playlist-set", playlist
)
942 def playlist_set_share(self
, playlist
, share
):
943 """Set the sharing status of a playlist"""
944 self
._simple("playlist-set-share", playlist
, share
)
946 def playlist_get_share(self
, playlist
):
947 """Returns the sharing status of a playlist"""
948 res
, details
= self
._simple("playlist-get-share", playlist
)
951 return _split(details
)[0]
954 """Returns the list of visible playlists"""
955 self
._simple("playlists")
958 ########################################################################
962 # read one response line and return as some suitable string object
964 # If an I/O error occurs, disconnect from the server.
966 # XXX does readline() DTRT regarding character encodings?
968 l
= self
.r
.readline()
969 if not re
.search("\n", l
):
970 raise communicationError(self
.who
, "peer disconnected")
975 return unicode(l
, "UTF-8")
978 # read a response as a (code, details) tuple
980 self
._debug(client
.debug_proto
, "<== %s" % l
)
981 m
= _response
.match(l
)
983 return int(m
.group(1)), m
.group(2)
985 raise protocolError(self
.who
, "invalid response %s")
987 def _send(self
, body
, *command
):
988 # Quote and send a command and optional body
990 # Returns the encoded command.
991 quoted
= _quote(command
)
992 self
._debug(client
.debug_proto
, "==> %s" % quoted
)
993 encoded
= quoted
.encode("UTF-8")
995 self
.w
.write(encoded
)
1009 raise communicationError(self
.who
, e
)
1014 def _simple(self
, *command
):
1015 # Issue a simple command, throw an exception on error
1017 # If an I/O error occurs, disconnect from the server.
1019 # On success or 'normal' errors returns response as a (code, details) tuple
1021 # On error raise operationError
1022 return self
._simple_body(None, *command
)
1024 def _simple_body(self
, body
, *command
):
1025 # Issue a simple command with optional body, throw an exception on error
1027 # If an I/O error occurs, disconnect from the server.
1029 # On success or 'normal' errors returns response as a (code, details) tuple
1031 # On error raise operationError
1032 if self
.state
== 'disconnected':
1035 cmd
= self
._send(body
, *command
)
1038 res
, details
= self
._response()
1039 if res
/ 100 == 2 or res
== 555:
1041 raise operationError(res
, details
, cmd
)
1044 # Fetch a dot-stuffed body
1048 self
._debug(client
.debug_body
, "<<< %s" % l
)
1049 if l
!= '' and l
[0] == '.':
1055 ########################################################################
1056 # Configuration file parsing
1058 def _readfile(self
, path
):
1059 # Read a configuration file
1063 # path -- path of file to read
1065 # handlers for various commands
1066 def _collection(self
, command
, args
):
1068 return "'%s' takes three args" % command
1069 self
.config
["collections"].append(args
)
1071 def _unary(self
, command
, args
):
1073 return "'%s' takes only one arg" % command
1074 self
.config
[command
] = args
[0]
1076 def _include(self
, command
, args
):
1078 return "'%s' takes only one arg" % command
1079 self
._readfile(args
[0])
1081 def _any(self
, command
, args
):
1082 self
.config
[command
] = args
1084 # mapping of options to handlers
1085 _options
= { "collection": _collection
,
1090 "include": _include
}
1093 for lno
, line
in enumerate(file(path
, "r")):
1095 fields
= _split(line
, 'comments')
1096 except _splitError
, s
:
1097 raise parseError(path
, lno
+ 1, str(s
))
1100 # we just ignore options we don't know about, so as to cope gracefully
1101 # with version skew (and nothing to do with implementor laziness)
1102 if command
in _options
:
1103 e
= _options
[command
](self
, command
, fields
[1:])
1105 self
._parseError(path
, lno
+ 1, e
)
1107 def _parseError(self
, path
, lno
, s
):
1108 raise parseError(path
, lno
, s
)
1110 ########################################################################
1114 """DisOrder event log monitor class
1116 Intended to be subclassed with methods corresponding to event log
1117 messages the implementor cares about over-ridden."""
1119 def __init__(self
, c
=None):
1120 """Constructor for the monitor class
1122 Can be passed a client to use. If none is specified then one
1123 will be created specially for the purpose.
1132 """Start monitoring logs. Continues monitoring until one of the
1133 message-specific methods returns False. Can be called more than
1134 once (but not recursively!)"""
1135 self
.c
.log(self
._callback
)
1138 """Return the timestamp of the current (or most recent) event log entry"""
1139 return self
.timestamp
1141 def _callback(self
, c
, line
):
1145 return self
.invalid(line
)
1147 return self
.invalid(line
)
1148 self
.timestamp
= int(bits
[0], 16)
1151 if keyword
== 'completed':
1153 return self
.completed(bits
[0])
1154 elif keyword
== 'failed':
1156 return self
.failed(bits
[0], bits
[1])
1157 elif keyword
== 'moved':
1162 return self
.invalid(line
)
1163 return self
.moved(bits
[0], n
, bits
[2])
1164 elif keyword
== 'playing':
1166 return self
.playing(bits
[0], None)
1167 elif len(bits
) == 2:
1168 return self
.playing(bits
[0], bits
[1])
1169 elif keyword
== 'queue' or keyword
== 'recent-added':
1171 q
= _list2dict(bits
)
1173 return self
.invalid(line
)
1174 if keyword
== 'queue':
1175 return self
.queue(q
)
1176 if keyword
== 'recent-added':
1177 return self
.recent_added(q
)
1178 elif keyword
== 'recent-removed':
1180 return self
.recent_removed(bits
[0])
1181 elif keyword
== 'removed':
1183 return self
.removed(bits
[0], None)
1184 elif len(bits
) == 2:
1185 return self
.removed(bits
[0], bits
[1])
1186 elif keyword
== 'scratched':
1188 return self
.scratched(bits
[0], bits
[1])
1189 elif keyword
== 'rescanned':
1190 return self
.rescanned()
1191 return self
.invalid(line
)
1193 def completed(self
, track
):
1194 """Called when a track completes.
1197 track -- track that completed"""
1200 def failed(self
, track
, error
):
1201 """Called when a player suffers an error.
1204 track -- track that failed
1205 error -- error indicator"""
1208 def moved(self
, id, offset
, user
):
1209 """Called when a track is moved in the queue.
1212 id -- queue entry ID
1213 offset -- distance moved
1214 user -- user responsible"""
1217 def playing(self
, track
, user
):
1218 """Called when a track starts playing.
1221 track -- track that has started
1222 user -- user that submitted track, or None"""
1226 """Called when a track is added to the queue.
1229 q -- dictionary of new queue entry"""
1232 def recent_added(self
, q
):
1233 """Called when a track is added to the recently played list
1236 q -- dictionary of new queue entry"""
1239 def recent_removed(self
, id):
1240 """Called when a track is removed from the recently played list
1243 id -- ID of removed entry (always the oldest)"""
1246 def removed(self
, id, user
):
1247 """Called when a track is removed from the queue, either manually
1248 or in order to play it.
1251 id -- ID of removed entry
1252 user -- user responsible (or None if we're playing this track)"""
1255 def scratched(self
, track
, user
):
1256 """Called when a track is scratched
1259 track -- track that was scratched
1260 user -- user responsible"""
1263 def invalid(self
, line
):
1264 """Called when an event log line cannot be interpreted
1267 line -- line that could not be understood"""
1270 def rescanned(self
):
1271 """Called when a rescan completes"""
1276 # py-indent-offset:2