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 command
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 information
437 values are unicode strings).
439 res
, details
= self
._simple("play", track
)
440 return unicode(details
) # because it's unicode in queue() output
442 def remove(self
, track
):
443 """Remove a track from the queue.
446 track -- the path or ID of the track to remove.
448 self
._simple("remove", track
)
451 """Enable playing."""
452 self
._simple("enable")
454 def disable(self
, *now
):
458 now -- if present (with any value), the current track is stopped
462 self
._simple("disable", "now")
464 self
._simple("disable")
466 def scratch(self
, *id):
467 """Scratch the currently playing track.
470 id -- if present, the ID of the track to scratch.
473 self
._simple("scratch", id[0])
475 self
._simple("scratch")
478 """Shut down the server.
480 Only trusted users can perform this operation.
482 self
._simple("shutdown")
484 def reconfigure(self
):
485 """Make the server reload its configuration.
487 Only trusted users can perform this operation.
489 self
._simple("reconfigure")
491 def rescan(self
, *flags
):
492 """Rescan one or more collections.
494 Only trusted users can perform this operation.
496 self
._simple("rescan", *flags
)
499 """Return the server's version number."""
500 return _split(self
._simple("version")[1])[0]
503 """Return the currently playing track.
505 If a track is playing then it is returned as a dictionary. See
506 disorder_protocol(5) for the meanings of the keys. All keys are
507 plain strings but the values will be unicode strings.
509 If no track is playing then None is returned."""
510 res
, details
= self
._simple("playing")
513 return _queueEntry(details
)
514 except _splitError
, s
:
515 raise protocolError(self
.who
, s
.str())
519 def _somequeue(self
, command
):
520 self
._simple(command
)
522 return map(lambda s
: _queueEntry(s
), self
._body())
523 except _splitError
, s
:
524 raise protocolError(self
.who
, s
.str())
527 """Return a list of recently played tracks.
529 The return value is a list of dictionaries corresponding to
530 recently played tracks. The oldest track comes first.
532 See disorder_protocol(5) for the meanings of the keys. All keys are
533 plain strings but the values will be unicode strings."""
534 return self
._somequeue("recent")
537 """Return the current queue.
539 The return value is a list of dictionaries corresponding to
540 recently played tracks. The next track to be played 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("queue")
546 def _somedir(self
, command
, dir, re
):
548 self
._simple(command
, dir, re
[0])
550 self
._simple(command
, dir)
553 def directories(self
, dir, *re
):
554 """List subdirectories of a directory.
557 dir -- directory to list, or '' for the whole root.
558 re -- regexp that results must match. Optional.
560 The return value is a list of the (nonempty) subdirectories of dir.
561 If dir is '' then a list of top-level directories is returned.
563 If a regexp is specified then the basename of each result must
564 match. Matching is case-independent. See pcrepattern(3).
566 return self
._somedir("dirs", dir, re
)
568 def files(self
, dir, *re
):
569 """List files within a directory.
572 dir -- directory to list, or '' for the whole root.
573 re -- regexp that results must match. Optional.
575 The return value is a list of playable files in dir. If dir is ''
576 then a list of top-level files is returned.
578 If a regexp is specified then the basename of each result must
579 match. Matching is case-independent. See pcrepattern(3).
581 return self
._somedir("files", dir, re
)
583 def allfiles(self
, dir, *re
):
584 """List subdirectories and files within a directory.
587 dir -- directory to list, or '' for the whole root.
588 re -- regexp that results must match. Optional.
590 The return value is a list of all (nonempty) subdirectories and
591 files within dir. If dir is '' then a list of top-level files and
592 directories is returned.
594 If a regexp is specified then the basename of each result must
595 match. Matching is case-independent. See pcrepattern(3).
597 return self
._somedir("allfiles", dir, re
)
599 def set(self
, track
, key
, value
):
600 """Set a preference value.
603 track -- the track to modify
604 key -- the preference name
605 value -- the new preference value
607 self
._simple("set", track
, key
, value
)
609 def unset(self
, track
, key
):
610 """Unset a preference value.
613 track -- the track to modify
614 key -- the preference to remove
616 self
._simple("set", track
, key
, value
)
618 def get(self
, track
, key
):
619 """Get a preference value.
622 track -- the track to query
623 key -- the preference to remove
625 The return value is the preference.
627 ret
, details
= self
._simple("get", track
, key
)
631 return _split(details
)[0]
633 def prefs(self
, track
):
634 """Get all the preferences for a track.
637 track -- the track to query
639 The return value is a dictionary of all the track's preferences.
640 Note that even nominally numeric values remain encoded as strings.
642 self
._simple("prefs", track
)
644 for line
in self
._body():
647 except _splitError
, s
:
648 raise protocolError(self
.who
, s
.str())
650 raise protocolError(self
.who
, "invalid prefs body line")
654 def _boolean(self
, s
):
657 def exists(self
, track
):
658 """Return true if a track exists
661 track -- the track to check for"""
662 return self
._boolean(self
._simple("exists", track
))
665 """Return true if playing is enabled"""
666 return self
._boolean(self
._simple("enabled"))
668 def random_enabled(self
):
669 """Return true if random play is enabled"""
670 return self
._boolean(self
._simple("random-enabled"))
672 def random_enable(self
):
673 """Enable random play."""
674 self
._simple("random-enable")
676 def random_disable(self
):
677 """Disable random play."""
678 self
._simple("random-disable")
680 def length(self
, track
):
681 """Return the length of a track in seconds.
684 track -- the track to query.
686 ret
, details
= self
._simple("length", track
)
689 def search(self
, words
):
690 """Search for tracks.
693 words -- the set of words to search for.
695 The return value is a list of track path names, all of which contain
696 all of the required words (in their path name, trackname
699 self
._simple("search", _quote(words
))
705 The return value is a list of all tags which apply to at least one
711 """Get server statistics.
713 The return value is list of statistics.
715 self
._simple("stats")
719 """Get all preferences.
721 The return value is an encoded dump of the preferences database.
726 def set_volume(self
, left
, right
):
730 left -- volume for the left speaker.
731 right -- volume for the right speaker.
733 self
._simple("volume", left
, right
)
735 def get_volume(self
):
738 The return value a tuple consisting of the left and right volumes.
740 ret
, details
= self
._simple("volume")
741 return map(int,string
.split(details
))
743 def move(self
, track
, delta
):
744 """Move a track in the queue.
747 track -- the name or ID of the track to move
748 delta -- the number of steps towards the head of the queue to move
750 ret
, details
= self
._simple("move", track
, str(delta
))
753 def moveafter(self
, target
, tracks
):
754 """Move a track in the queue
757 target -- target ID or None
758 tracks -- a list of IDs to move
760 If target is '' or is not in the queue then the tracks are moved to
761 the head of the queue.
763 Otherwise the tracks are moved to just after the target."""
766 self
._simple("moveafter", target
, *tracks
)
768 def log(self
, callback
):
769 """Read event log entries as they happen.
771 Each event log entry is handled by passing it to callback.
773 The callback takes two arguments, the first is the client and the
774 second the line from the event log.
776 The callback should return True to continue or False to stop (don't
777 forget this, or your program will mysteriously misbehave). Once you
778 stop reading the log the connection is useless and should be deleted.
780 It is suggested that you use the disorder.monitor class instead of
781 calling this method directly, but this is not mandatory.
783 See disorder_protocol(5) for the event log syntax.
786 callback -- function to call with log entry
788 ret
, details
= self
._simple("log")
791 self
._debug(client
.debug_body
, "<<< %s" % l
)
792 if l
!= '' and l
[0] == '.':
796 if not callback(self
, l
):
800 """Pause the current track."""
801 self
._simple("pause")
804 """Resume after a pause."""
805 self
._simple("resume")
807 def part(self
, track
, context
, part
):
808 """Get a track name part
811 track -- the track to query
812 context -- the context ('sort' or 'display')
813 part -- the desired part (usually 'artist', 'album' or 'title')
815 The return value is the preference
817 ret
, details
= self
._simple("part", track
, context
, part
)
818 return _split(details
)[0]
820 def setglobal(self
, key
, value
):
821 """Set a global preference value.
824 key -- the preference name
825 value -- the new preference value
827 self
._simple("set-global", key
, value
)
829 def unsetglobal(self
, key
):
830 """Unset a global preference value.
833 key -- the preference to remove
835 self
._simple("set-global", key
, value
)
837 def getglobal(self
, key
):
838 """Get a global preference value.
841 key -- the preference to look up
843 The return value is the preference
845 ret
, details
= self
._simple("get-global", key
)
849 return _split(details
)[0]
851 def make_cookie(self
):
852 """Create a login cookie"""
853 ret
, details
= self
._simple("make-cookie")
854 return _split(details
)[0]
857 """Revoke a login cookie"""
858 self
._simple("revoke")
860 def adduser(self
, user
, password
):
862 self
._simple("adduser", user
, password
)
864 def deluser(self
, user
):
866 self
._simple("deluser", user
)
868 def userinfo(self
, user
, key
):
869 """Get user information"""
870 res
, details
= self
._simple("userinfo", user
, key
)
873 return _split(details
)[0]
875 def edituser(self
, user
, key
, value
):
876 """Set user information"""
877 self
._simple("edituser", user
, key
, value
)
882 The return value is a list of all users."""
883 self
._simple("users")
886 def register(self
, username
, password
, email
):
887 """Register a user"""
888 res
, details
= self
._simple("register", username
, password
, email
)
889 return _split(details
)[0]
891 def confirm(self
, confirmation
):
892 """Confirm a user registration"""
893 res
, details
= self
._simple("confirm", confirmation
)
895 def schedule_list(self
):
896 """Get a list of scheduled events """
897 self
._simple("schedule-list")
900 def schedule_del(self
, event
):
901 """Delete a scheduled event"""
902 self
._simple("schedule-del", event
)
904 def schedule_get(self
, event
):
905 """Get the details for an event as a dict (returns None if event not found)"""
906 res
, details
= self
._simple("schedule-get", event
)
910 for line
in self
._body():
915 def schedule_add(self
, when
, priority
, action
, *rest
):
916 """Add a scheduled event"""
917 self
._simple("schedule-add", str(when
), priority
, action
, *rest
)
920 """Adopt a randomly picked track"""
921 self
._simple("adopt", id)
923 ########################################################################
927 # read one response line and return as some suitable string object
929 # If an I/O error occurs, disconnect from the server.
931 # XXX does readline() DTRT regarding character encodings?
933 l
= self
.r
.readline()
934 if not re
.search("\n", l
):
935 raise communicationError(self
.who
, "peer disconnected")
940 return unicode(l
, "UTF-8")
943 # read a response as a (code, details) tuple
945 self
._debug(client
.debug_proto
, "<== %s" % l
)
946 m
= _response
.match(l
)
948 return int(m
.group(1)), m
.group(2)
950 raise protocolError(self
.who
, "invalid response %s")
952 def _send(self
, *command
):
953 # Quote and send a command
955 # Returns the encoded command.
956 quoted
= _quote(command
)
957 self
._debug(client
.debug_proto
, "==> %s" % quoted
)
958 encoded
= quoted
.encode("UTF-8")
960 self
.w
.write(encoded
)
967 raise communicationError(self
.who
, e
)
972 def _simple(self
, *command
):
973 # Issue a simple command, throw an exception on error
975 # If an I/O error occurs, disconnect from the server.
977 # On success or 'normal' errors returns response as a (code, details) tuple
979 # On error raise operationError
980 if self
.state
== 'disconnected':
983 cmd
= self
._send(*command
)
986 res
, details
= self
._response()
987 if res
/ 100 == 2 or res
== 555:
989 raise operationError(res
, details
, cmd
)
992 # Fetch a dot-stuffed body
996 self
._debug(client
.debug_body
, "<<< %s" % l
)
997 if l
!= '' and l
[0] == '.':
1003 ########################################################################
1004 # Configuration file parsing
1006 def _readfile(self
, path
):
1007 # Read a configuration file
1011 # path -- path of file to read
1013 # handlers for various commands
1014 def _collection(self
, command
, args
):
1016 return "'%s' takes three args" % command
1017 self
.config
["collections"].append(args
)
1019 def _unary(self
, command
, args
):
1021 return "'%s' takes only one arg" % command
1022 self
.config
[command
] = args
[0]
1024 def _include(self
, command
, args
):
1026 return "'%s' takes only one arg" % command
1027 self
._readfile(args
[0])
1029 def _any(self
, command
, args
):
1030 self
.config
[command
] = args
1032 # mapping of options to handlers
1033 _options
= { "collection": _collection
,
1038 "include": _include
}
1041 for lno
, line
in enumerate(file(path
, "r")):
1043 fields
= _split(line
, 'comments')
1044 except _splitError
, s
:
1045 raise parseError(path
, lno
+ 1, str(s
))
1048 # we just ignore options we don't know about, so as to cope gracefully
1049 # with version skew (and nothing to do with implementor laziness)
1050 if command
in _options
:
1051 e
= _options
[command
](self
, command
, fields
[1:])
1053 self
._parseError(path
, lno
+ 1, e
)
1055 def _parseError(self
, path
, lno
, s
):
1056 raise parseError(path
, lno
, s
)
1058 ########################################################################
1062 """DisOrder event log monitor class
1064 Intended to be subclassed with methods corresponding to event log messages
1065 the implementor cares about over-ridden."""
1067 def __init__(self
, c
=None):
1068 """Constructor for the monitor class
1070 Can be passed a client to use. If none is specified then one
1071 will be created specially for the purpose.
1080 """Start monitoring logs. Continues monitoring until one of the
1081 message-specific methods returns False. Can be called more than once
1082 (but not recursively!)"""
1083 self
.c
.log(self
._callback
)
1086 """Return the timestamp of the current (or most recent) event log entry"""
1087 return self
.timestamp
1089 def _callback(self
, c
, line
):
1093 return self
.invalid(line
)
1095 return self
.invalid(line
)
1096 self
.timestamp
= int(bits
[0], 16)
1099 if keyword
== 'completed':
1101 return self
.completed(bits
[0])
1102 elif keyword
== 'failed':
1104 return self
.failed(bits
[0], bits
[1])
1105 elif keyword
== 'moved':
1110 return self
.invalid(line
)
1111 return self
.moved(bits
[0], n
, bits
[2])
1112 elif keyword
== 'playing':
1114 return self
.playing(bits
[0], None)
1115 elif len(bits
) == 2:
1116 return self
.playing(bits
[0], bits
[1])
1117 elif keyword
== 'queue' or keyword
== 'recent-added':
1119 q
= _list2dict(bits
)
1121 return self
.invalid(line
)
1122 if keyword
== 'queue':
1123 return self
.queue(q
)
1124 if keyword
== 'recent-added':
1125 return self
.recent_added(q
)
1126 elif keyword
== 'recent-removed':
1128 return self
.recent_removed(bits
[0])
1129 elif keyword
== 'removed':
1131 return self
.removed(bits
[0], None)
1132 elif len(bits
) == 2:
1133 return self
.removed(bits
[0], bits
[1])
1134 elif keyword
== 'scratched':
1136 return self
.scratched(bits
[0], bits
[1])
1137 elif keyword
== 'rescanned':
1138 return self
.rescanned()
1139 return self
.invalid(line
)
1141 def completed(self
, track
):
1142 """Called when a track completes.
1145 track -- track that completed"""
1148 def failed(self
, track
, error
):
1149 """Called when a player suffers an error.
1152 track -- track that failed
1153 error -- error indicator"""
1156 def moved(self
, id, offset
, user
):
1157 """Called when a track is moved in the queue.
1160 id -- queue entry ID
1161 offset -- distance moved
1162 user -- user responsible"""
1165 def playing(self
, track
, user
):
1166 """Called when a track starts playing.
1169 track -- track that has started
1170 user -- user that submitted track, or None"""
1174 """Called when a track is added to the queue.
1177 q -- dictionary of new queue entry"""
1180 def recent_added(self
, q
):
1181 """Called when a track is added to the recently played list
1184 q -- dictionary of new queue entry"""
1187 def recent_removed(self
, id):
1188 """Called when a track is removed from the recently played list
1191 id -- ID of removed entry (always the oldest)"""
1194 def removed(self
, id, user
):
1195 """Called when a track is removed from the queue, either manually
1196 or in order to play it.
1199 id -- ID of removed entry
1200 user -- user responsible (or None if we're playing this track)"""
1203 def scratched(self
, track
, user
):
1204 """Called when a track is scratched
1207 track -- track that was scratched
1208 user -- user responsible"""
1211 def invalid(self
, line
):
1212 """Called when an event log line cannot be interpreted
1215 line -- line that could not be understood"""
1218 def rescanned(self
):
1219 """Called when a rescan completes"""
1224 # py-indent-offset:2