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}) ?(.*)")
73 "sha256": hashlib.sha256,
74 "SHA256": hashlib.sha256,
75 "sha384": hashlib.sha384,
76 "SHA384": hashlib.sha384,
77 "sha512": hashlib.sha512,
78 "SHA512": hashlib.sha512,
83 ########################################################################
86 class Error(Exception):
87 """Base class for DisOrder exceptions."""
89 class _splitError(Error):
91 def __init__(self, value):
94 return str(self.value)
96 class parseError(Error):
97 """Error parsing the configuration file."""
98 def __init__(self, path, line, details):
101 self.details = details
103 return "%s:%d: %s" % (self.path, self.line, self.details)
105 class protocolError(Error):
106 """DisOrder control protocol error.
108 Indicates a mismatch between the client and server's understanding of
109 the control protocol.
111 def __init__(self, who, error):
115 return "%s: %s" % (self.who, str(self.error))
117 class operationError(Error):
118 """DisOrder control protocol error response.
120 Indicates that an operation failed (e.g. an attempt to play a
121 nonexistent track). The connection should still be usable.
123 def __init__(self, res, details, cmd=None):
126 self.details_ = details
128 """Return the complete response string from the server, with the
129 command if available.
131 Excludes the final newline.
133 if self.cmd_ is None:
134 return "%d %s" % (self.res_, self.details_)
136 return "%d %s [%s]" % (self.res_, self.details_, self.cmd_)
138 """Return the response code from the server."""
141 """Returns the detail string from the server."""
144 class communicationError(Error):
145 """DisOrder control protocol communication error.
147 Indicates that communication with the server went wrong, perhaps
148 because the server was restarted. The caller could report an error to
149 the user and wait for further user instructions, or even automatically
152 def __init__(self, who, error):
156 return "%s: %s" % (self.who, str(self.error))
158 ########################################################################
159 # DisOrder-specific text processing
162 # Unescape the contents of a string
166 # s -- string to unescape
168 s = re.sub("\\\\n", "\n", s)
169 s = re.sub("\\\\(.)", "\\1", s)
172 def _split(s, *comments):
173 # Split a string into fields according to the usual Disorder string splitting
178 # s -- string to parse
179 # comments -- if present, parse comments
183 # On success, a list of fields is returned.
185 # On error, disorder.parseError is thrown.
190 if comments and s[0] == '#':
197 # pick of quoted fields of both kinds
202 fields.append(_unescape(m.group(1)))
205 # and unquoted fields
206 m = _unquoted.match(s)
208 fields.append(m.group(0))
211 # anything left must be in error
212 if s[0] == '"' or s[0] == '\'':
213 raise _splitError("invalid quoted string")
215 raise _splitError("syntax error")
219 # Escape the contents of a string
223 # s -- string to escape
225 if re.search("[\\\\\"'\n \t\r]", s) or s == '':
226 s = re.sub(r'[\\"]', r'\\\g<0>', s)
227 s = re.sub("\n", r"\\n", s)
233 # Quote a list of values
234 return ' '.join(map(_escape, list))
237 # Return the value of s in a form suitable for writing to stderr
238 return s.encode(locale.nl_langinfo(locale.CODESET), 'replace')
241 # Convert a list of the form [k1, v1, k2, v2, ..., kN, vN]
242 # to a dictionary {k1:v1, k2:v2, ..., kN:vN}
250 except StopIteration:
255 # parse a queue entry
256 return _list2dict(_split(s))
258 ########################################################################
262 """DisOrder client class.
264 This class provides access to the DisOrder server either on this
265 machine or across the internet.
267 The server to connect to, and the username and password to use, are
268 determined from the configuration files as described in 'man
271 All methods will connect if necessary, as soon as you have a
272 disorder.client object you can start calling operational methods on
275 However if the server is restarted then the next method called on a
276 connection will throw an exception. This may be considered a bug.
278 All methods block until they complete.
280 Operation methods raise communicationError if the connection breaks,
281 protocolError if the response from the server is malformed, or
282 operationError if the response is valid but indicates that the
289 def __init__(self, user=None, password=None):
290 """Constructor for DisOrder client class.
292 The constructor reads the configuration file, but does not connect
295 If the environment variable DISORDER_PYTHON_DEBUG is set then the
296 debug flags are initialised to that value. This can be overridden
297 with the debug() method below.
299 The constructor Raises parseError() if the configuration file is not
302 pw = pwd.getpwuid(os.getuid())
303 self.debugging = int(os.getenv("DISORDER_PYTHON_DEBUG", 0))
304 self.config = { 'collections': [],
305 'username': pw.pw_name,
308 self.password = password
309 home = os.getenv("HOME")
312 privconf = _configfile + "." + pw.pw_name
313 passfile = home + os.sep + ".disorder" + os.sep + "passwd"
314 if os.path.exists(_configfile):
315 self._readfile(_configfile)
316 if os.path.exists(privconf):
317 self._readfile(privconf)
318 if os.path.exists(passfile) and _userconf:
319 self._readfile(passfile)
320 self.state = 'disconnected'
322 def debug(self, bits):
323 """Enable or disable protocol debugging. Debug messages are written
327 bits -- bitmap of operations that should generate debug information
330 debug_proto -- dump control protocol messages (excluding bodies)
331 debug_body -- dump control protocol message bodies
333 self.debugging = bits
335 def _debug(self, bit, s):
337 if self.debugging & bit:
338 sys.stderr.write(_sanitize(s))
339 sys.stderr.write("\n")
342 def connect(self, cookie=None):
343 """c.connect(cookie=None)
345 Connect to the DisOrder server and authenticate.
347 Raises communicationError if connection fails and operationError if
348 authentication fails (in which case disconnection is automatic).
350 May be called more than once to retry connections (e.g. when the
351 server is down). If we are already connected and authenticated,
354 Other operations automatically connect if we're not already
355 connected, so it is not strictly necessary to call this method.
357 If COOKIE is specified then that is used to log in instead of
358 the username/password.
360 if self.state == 'disconnected':
362 self.state = 'connecting'
363 if 'connect' in self.config and len(self.config['connect']) > 0:
364 c = self.config['connect']
365 self.who = repr(c) # temporarily
367 a = socket.getaddrinfo(None, c[0],
373 a = socket.getaddrinfo(c[0], c[1],
379 s = socket.socket(a[0], a[1], a[2]);
381 self.who = "%s" % a[3]
383 s = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM);
384 self.who = self.config['home'] + os.sep + "socket"
386 self.w = s.makefile("wb")
387 self.r = s.makefile("rb")
388 (res, details) = self._simple()
389 (protocol, algo, challenge) = _split(details)
391 raise communicationError(self.who,
392 "unknown protocol version %s" % protocol)
394 if self.user is None:
395 user = self.config['username']
398 if self.password is None:
399 password = self.config['password']
401 password = self.password
404 h.update(binascii.unhexlify(challenge))
405 self._simple("user", user, h.hexdigest())
407 self._simple("cookie", cookie)
408 self.state = 'connected'
409 except socket.error, e:
411 raise communicationError(self.who, e)
416 def _disconnect(self):
417 # disconnect from the server, whatever state we are in
423 self.state = 'disconnected'
425 ########################################################################
428 def play(self, track):
432 track -- the path of the track to play.
434 Returns the ID of the new queue entry.
436 Note that queue IDs are unicode strings (because all track
437 information values are unicode strings).
439 res, details = self._simple("play", track)
440 return unicode(details) # because it's unicode in queue() output
442 def playafter(self, target, tracks):
443 """Insert tracks into a specific point in the queue.
446 target -- target ID or None to insert at start of queue
447 tracks -- a list of tracks to play"""
450 self._simple("playafter", target, *tracks)
452 def remove(self, track):
453 """Remove a track from the queue.
456 track -- the path or ID of the track to remove.
458 self._simple("remove", track)
461 """Enable playing."""
462 self._simple("enable")
464 def disable(self, *now):
468 now -- if present (with any value), the current track is stopped
472 self._simple("disable", "now")
474 self._simple("disable")
476 def scratch(self, *id):
477 """Scratch the currently playing track.
480 id -- if present, the ID of the track to scratch.
483 self._simple("scratch", id[0])
485 self._simple("scratch")
488 """Shut down the server.
490 Only trusted users can perform this operation.
492 self._simple("shutdown")
494 def reconfigure(self):
495 """Make the server reload its configuration.
497 Only trusted users can perform this operation.
499 self._simple("reconfigure")
501 def rescan(self, *flags):
502 """Rescan one or more collections.
504 Only trusted users can perform this operation.
506 self._simple("rescan", *flags)
509 """Return the server's version number."""
510 return _split(self._simple("version")[1])[0]
513 """Return the currently playing track.
515 If a track is playing then it is returned as a dictionary. See
516 disorder_protocol(5) for the meanings of the keys. All keys are
517 plain strings but the values will be unicode strings.
519 If no track is playing then None is returned."""
520 res, details = self._simple("playing")
523 return _queueEntry(details)
524 except _splitError, s:
525 raise protocolError(self.who, s.str())
529 def _somequeue(self, command):
530 self._simple(command)
532 return map(lambda s: _queueEntry(s), self._body())
533 except _splitError, s:
534 raise protocolError(self.who, s.str())
537 """Return a list of recently played tracks.
539 The return value is a list of dictionaries corresponding to
540 recently played tracks. The oldest track comes first.
542 See disorder_protocol(5) for the meanings of the keys. All keys are
543 plain strings but the values will be unicode strings."""
544 return self._somequeue("recent")
547 """Return the current queue.
549 The return value is a list of dictionaries corresponding to
550 recently played tracks. The next track to be played comes first.
552 See disorder_protocol(5) for the meanings of the keys.
553 All keys are plain strings but the values will be unicode strings."""
554 return self._somequeue("queue")
556 def _somedir(self, command, dir, re):
558 self._simple(command, dir, re[0])
560 self._simple(command, dir)
563 def directories(self, dir, *re):
564 """List subdirectories of a directory.
567 dir -- directory to list, or '' for the whole root.
568 re -- regexp that results must match. Optional.
570 The return value is a list of the (nonempty) subdirectories of dir.
571 If dir is '' then a list of top-level directories is returned.
573 If a regexp is specified then the basename of each result must
574 match. Matching is case-independent. See pcrepattern(3).
576 return self._somedir("dirs", dir, re)
578 def files(self, dir, *re):
579 """List files within a directory.
582 dir -- directory to list, or '' for the whole root.
583 re -- regexp that results must match. Optional.
585 The return value is a list of playable files in dir. If dir is ''
586 then a list of top-level files is returned.
588 If a regexp is specified then the basename of each result must
589 match. Matching is case-independent. See pcrepattern(3).
591 return self._somedir("files", dir, re)
593 def allfiles(self, dir, *re):
594 """List subdirectories and files within a directory.
597 dir -- directory to list, or '' for the whole root.
598 re -- regexp that results must match. Optional.
600 The return value is a list of all (nonempty) subdirectories and
601 files within dir. If dir is '' then a list of top-level files and
602 directories is returned.
604 If a regexp is specified then the basename of each result must
605 match. Matching is case-independent. See pcrepattern(3).
607 return self._somedir("allfiles", dir, re)
609 def set(self, track, key, value):
610 """Set a preference value.
613 track -- the track to modify
614 key -- the preference name
615 value -- the new preference value
617 self._simple("set", track, key, value)
619 def unset(self, track, key):
620 """Unset a preference value.
623 track -- the track to modify
624 key -- the preference to remove
626 self._simple("set", track, key, value)
628 def get(self, track, key):
629 """Get a preference value.
632 track -- the track to query
633 key -- the preference to remove
635 The return value is the preference.
637 ret, details = self._simple("get", track, key)
641 return _split(details)[0]
643 def prefs(self, track):
644 """Get all the preferences for a track.
647 track -- the track to query
649 The return value is a dictionary of all the track's preferences.
650 Note that even nominally numeric values remain encoded as strings.
652 self._simple("prefs", track)
654 for line in self._body():
657 except _splitError, s:
658 raise protocolError(self.who, s.str())
660 raise protocolError(self.who, "invalid prefs body line")
664 def _boolean(self, s):
667 def exists(self, track):
668 """Return true if a track exists
671 track -- the track to check for"""
672 return self._boolean(self._simple("exists", track))
675 """Return true if playing is enabled"""
676 return self._boolean(self._simple("enabled"))
678 def random_enabled(self):
679 """Return true if random play is enabled"""
680 return self._boolean(self._simple("random-enabled"))
682 def random_enable(self):
683 """Enable random play."""
684 self._simple("random-enable")
686 def random_disable(self):
687 """Disable random play."""
688 self._simple("random-disable")
690 def length(self, track):
691 """Return the length of a track in seconds.
694 track -- the track to query.
696 ret, details = self._simple("length", track)
699 def search(self, words):
700 """Search for tracks.
703 words -- the set of words to search for.
705 The return value is a list of track path names, all of which contain
706 all of the required words (in their path name, trackname
709 self._simple("search", _quote(words))
715 The return value is a list of all tags which apply to at least one
721 """Get server statistics.
723 The return value is list of statistics.
725 self._simple("stats")
729 """Get all preferences.
731 The return value is an encoded dump of the preferences database.
736 def set_volume(self, left, right):
740 left -- volume for the left speaker.
741 right -- volume for the right speaker.
743 self._simple("volume", left, right)
745 def get_volume(self):
748 The return value a tuple consisting of the left and right volumes.
750 ret, details = self._simple("volume")
751 return map(int,string.split(details))
753 def move(self, track, delta):
754 """Move a track in the queue.
757 track -- the name or ID of the track to move
758 delta -- the number of steps towards the head of the queue to move
760 ret, details = self._simple("move", track, str(delta))
763 def moveafter(self, target, tracks):
764 """Move a track in the queue
767 target -- target ID or None
768 tracks -- a list of IDs to move
770 If target is '' or is not in the queue then the tracks are moved to
771 the head of the queue.
773 Otherwise the tracks are moved to just after the target."""
776 self._simple("moveafter", target, *tracks)
778 def log(self, callback):
779 """Read event log entries as they happen.
781 Each event log entry is handled by passing it to callback.
783 The callback takes two arguments, the first is the client and the
784 second the line from the event log.
786 The callback should return True to continue or False to stop (don't
787 forget this, or your program will mysteriously misbehave). Once you
788 stop reading the log the connection is useless and should be
791 It is suggested that you use the disorder.monitor class instead of
792 calling this method directly, but this is not mandatory.
794 See disorder_protocol(5) for the event log syntax.
797 callback -- function to call with log entry
799 ret, details = self._simple("log")
802 self._debug(client.debug_body, "<<< %s" % l)
803 if l != '' and l[0] == '.':
807 if not callback(self, l):
811 """Pause the current track."""
812 self._simple("pause")
815 """Resume after a pause."""
816 self._simple("resume")
818 def part(self, track, context, part):
819 """Get a track name part
822 track -- the track to query
823 context -- the context ('sort' or 'display')
824 part -- the desired part (usually 'artist', 'album' or 'title')
826 The return value is the preference
828 ret, details = self._simple("part", track, context, part)
829 return _split(details)[0]
831 def setglobal(self, key, value):
832 """Set a global preference value.
835 key -- the preference name
836 value -- the new preference value
838 self._simple("set-global", key, value)
840 def unsetglobal(self, key):
841 """Unset a global preference value.
844 key -- the preference to remove
846 self._simple("set-global", key, value)
848 def getglobal(self, key):
849 """Get a global preference value.
852 key -- the preference to look up
854 The return value is the preference
856 ret, details = self._simple("get-global", key)
860 return _split(details)[0]
862 def make_cookie(self):
863 """Create a login cookie"""
864 ret, details = self._simple("make-cookie")
865 return _split(details)[0]
868 """Revoke a login cookie"""
869 self._simple("revoke")
871 def adduser(self, user, password):
873 self._simple("adduser", user, password)
875 def deluser(self, user):
877 self._simple("deluser", user)
879 def userinfo(self, user, key):
880 """Get user information"""
881 res, details = self._simple("userinfo", user, key)
884 return _split(details)[0]
886 def edituser(self, user, key, value):
887 """Set user information"""
888 self._simple("edituser", user, key, value)
893 The return value is a list of all users."""
894 self._simple("users")
897 def register(self, username, password, email):
898 """Register a user"""
899 res, details = self._simple("register", username, password, email)
900 return _split(details)[0]
902 def confirm(self, confirmation):
903 """Confirm a user registration"""
904 res, details = self._simple("confirm", confirmation)
906 def schedule_list(self):
907 """Get a list of scheduled events """
908 self._simple("schedule-list")
911 def schedule_del(self, event):
912 """Delete a scheduled event"""
913 self._simple("schedule-del", event)
915 def schedule_get(self, event):
916 """Get the details for an event as a dict (returns None if
918 res, details = self._simple("schedule-get", event)
922 for line in self._body():
927 def schedule_add(self, when, priority, action, *rest):
928 """Add a scheduled event"""
929 self._simple("schedule-add", str(when), priority, action, *rest)
932 """Adopt a randomly picked track"""
933 self._simple("adopt", id)
935 def playlist_delete(self, playlist):
936 """Delete a playlist"""
937 res, details = self._simple("playlist-delete", playlist)
939 raise operationError(res, details, "playlist-delete")
941 def playlist_get(self, playlist):
942 """Get the contents of a playlist
944 The return value is an array of track names, or None if there is no
946 res, details = self._simple("playlist-get", playlist)
951 def playlist_lock(self, playlist):
952 """Lock a playlist. Playlists can only be modified when locked."""
953 self._simple("playlist-lock", playlist)
955 def playlist_unlock(self):
956 """Unlock the locked playlist."""
957 self._simple("playlist-unlock")
959 def playlist_set(self, playlist, tracks):
960 """Set the contents of a playlist. The playlist must be locked.
963 playlist -- Playlist to set
964 tracks -- Array of tracks"""
965 self._simple_body(tracks, "playlist-set", playlist)
967 def playlist_set_share(self, playlist, share):
968 """Set the sharing status of a playlist"""
969 self._simple("playlist-set-share", playlist, share)
971 def playlist_get_share(self, playlist):
972 """Returns the sharing status of a playlist"""
973 res, details = self._simple("playlist-get-share", playlist)
976 return _split(details)[0]
979 """Returns the list of visible playlists"""
980 self._simple("playlists")
983 ########################################################################
987 # read one response line and return as some suitable string object
989 # If an I/O error occurs, disconnect from the server.
991 # XXX does readline() DTRT regarding character encodings?
993 l = self.r.readline()
994 if not re.search("\n", l):
995 raise communicationError(self.who, "peer disconnected")
1000 return unicode(l, "UTF-8")
1002 def _response(self):
1003 # read a response as a (code, details) tuple
1005 self._debug(client.debug_proto, "<== %s" % l)
1006 m = _response.match(l)
1008 return int(m.group(1)), m.group(2)
1010 raise protocolError(self.who, "invalid response %s")
1012 def _send(self, body, *command):
1013 # Quote and send a command and optional body
1015 # Returns the encoded command.
1016 quoted = _quote(command)
1017 self._debug(client.debug_proto, "==> %s" % quoted)
1018 encoded = quoted.encode("UTF-8")
1020 self.w.write(encoded)
1034 raise communicationError(self.who, e)
1039 def _simple(self, *command):
1040 # Issue a simple command, throw an exception on error
1042 # If an I/O error occurs, disconnect from the server.
1044 # On success or 'normal' errors returns response as a (code, details) tuple
1046 # On error raise operationError
1047 return self._simple_body(None, *command)
1049 def _simple_body(self, body, *command):
1050 # Issue a simple command with optional body, throw an exception on error
1052 # If an I/O error occurs, disconnect from the server.
1054 # On success or 'normal' errors returns response as a (code, details) tuple
1056 # On error raise operationError
1057 if self.state == 'disconnected':
1060 cmd = self._send(body, *command)
1063 res, details = self._response()
1064 if res / 100 == 2 or res == 555:
1066 raise operationError(res, details, cmd)
1069 # Fetch a dot-stuffed body
1073 self._debug(client.debug_body, "<<< %s" % l)
1074 if l != '' and l[0] == '.':
1080 ########################################################################
1081 # Configuration file parsing
1083 def _readfile(self, path):
1084 # Read a configuration file
1088 # path -- path of file to read
1090 # handlers for various commands
1091 def _collection(self, command, args):
1093 return "'%s' takes three args" % command
1094 self.config["collections"].append(args)
1096 def _unary(self, command, args):
1098 return "'%s' takes only one arg" % command
1099 self.config[command] = args[0]
1101 def _include(self, command, args):
1103 return "'%s' takes only one arg" % command
1104 self._readfile(args[0])
1106 def _any(self, command, args):
1107 self.config[command] = args
1109 # mapping of options to handlers
1110 _options = { "collection": _collection,
1115 "include": _include }
1118 for lno, line in enumerate(file(path, "r")):
1120 fields = _split(line, 'comments')
1121 except _splitError, s:
1122 raise parseError(path, lno + 1, str(s))
1125 # we just ignore options we don't know about, so as to cope gracefully
1126 # with version skew (and nothing to do with implementor laziness)
1127 if command in _options:
1128 e = _options[command](self, command, fields[1:])
1130 self._parseError(path, lno + 1, e)
1132 def _parseError(self, path, lno, s):
1133 raise parseError(path, lno, s)
1135 ########################################################################
1139 """DisOrder event log monitor class
1141 Intended to be subclassed with methods corresponding to event log
1142 messages the implementor cares about over-ridden."""
1144 def __init__(self, c=None):
1145 """Constructor for the monitor class
1147 Can be passed a client to use. If none is specified then one
1148 will be created specially for the purpose.
1157 """Start monitoring logs. Continues monitoring until one of the
1158 message-specific methods returns False. Can be called more than
1159 once (but not recursively!)"""
1160 self.c.log(self._callback)
1163 """Return the timestamp of the current (or most recent) event log entry"""
1164 return self.timestamp
1166 def _callback(self, c, line):
1170 return self.invalid(line)
1172 return self.invalid(line)
1173 self.timestamp = int(bits[0], 16)
1176 if keyword == 'completed':
1178 return self.completed(bits[0])
1179 elif keyword == 'failed':
1181 return self.failed(bits[0], bits[1])
1182 elif keyword == 'moved':
1187 return self.invalid(line)
1188 return self.moved(bits[0], n, bits[2])
1189 elif keyword == 'playing':
1191 return self.playing(bits[0], None)
1192 elif len(bits) == 2:
1193 return self.playing(bits[0], bits[1])
1194 elif keyword == 'queue' or keyword == 'recent-added':
1196 q = _list2dict(bits)
1198 return self.invalid(line)
1199 if keyword == 'queue':
1200 return self.queue(q)
1201 if keyword == 'recent-added':
1202 return self.recent_added(q)
1203 elif keyword == 'recent-removed':
1205 return self.recent_removed(bits[0])
1206 elif keyword == 'removed':
1208 return self.removed(bits[0], None)
1209 elif len(bits) == 2:
1210 return self.removed(bits[0], bits[1])
1211 elif keyword == 'scratched':
1213 return self.scratched(bits[0], bits[1])
1214 elif keyword == 'rescanned':
1215 return self.rescanned()
1216 return self.invalid(line)
1218 def completed(self, track):
1219 """Called when a track completes.
1222 track -- track that completed"""
1225 def failed(self, track, error):
1226 """Called when a player suffers an error.
1229 track -- track that failed
1230 error -- error indicator"""
1233 def moved(self, id, offset, user):
1234 """Called when a track is moved in the queue.
1237 id -- queue entry ID
1238 offset -- distance moved
1239 user -- user responsible"""
1242 def playing(self, track, user):
1243 """Called when a track starts playing.
1246 track -- track that has started
1247 user -- user that submitted track, or None"""
1251 """Called when a track is added to the queue.
1254 q -- dictionary of new queue entry"""
1257 def recent_added(self, q):
1258 """Called when a track is added to the recently played list
1261 q -- dictionary of new queue entry"""
1264 def recent_removed(self, id):
1265 """Called when a track is removed from the recently played list
1268 id -- ID of removed entry (always the oldest)"""
1271 def removed(self, id, user):
1272 """Called when a track is removed from the queue, either manually
1273 or in order to play it.
1276 id -- ID of removed entry
1277 user -- user responsible (or None if we're playing this track)"""
1280 def scratched(self, track, user):
1281 """Called when a track is scratched
1284 track -- track that was scratched
1285 user -- user responsible"""
1288 def invalid(self, line):
1289 """Called when an event log line cannot be interpreted
1292 line -- line that could not be understood"""
1295 def rescanned(self):
1296 """Called when a rescan completes"""
1301 # py-indent-offset:2