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
):
90 """Error parsing a quoted string list"""
92 def __init__(self
, value
):
95 return str(self
.value
)
97 class parseError(Error
):
98 """Error parsing the configuration file."""
99 def __init__(self
, path
, line
, details
):
102 self
.details
= details
104 return "%s:%d: %s" %
(self
.path
, self
.line
, self
.details
)
106 class protocolError(Error
):
107 """DisOrder control protocol error.
109 Indicates a mismatch between the client and server's understanding of
110 the control protocol.
112 def __init__(self
, who
, error
):
116 return "%s: %s" %
(self
.who
, str(self
.error
))
118 class operationError(Error
):
119 """DisOrder control protocol error response.
121 Indicates that an operation failed (e.g. an attempt to play a
122 nonexistent track). The connection should still be usable.
124 def __init__(self
, res
, details
, cmd
=None):
127 self
.details_
= details
129 """Return the complete response string from the server, with the
130 command if available.
132 Excludes the final newline.
134 if self
.cmd_
is None:
135 return "%d %s" %
(self
.res_
, self
.details_
)
137 return "%d %s [%s]" %
(self
.res_
, self
.details_
, self
.cmd_
)
139 """Return the response code from the server."""
142 """Returns the detail string from the server."""
145 class communicationError(Error
):
146 """DisOrder control protocol communication error.
148 Indicates that communication with the server went wrong, perhaps
149 because the server was restarted. The caller could report an error to
150 the user and wait for further user instructions, or even automatically
153 def __init__(self
, who
, error
):
157 return "%s: %s" %
(self
.who
, str(self
.error
))
159 ########################################################################
160 # DisOrder-specific text processing
163 # Unescape the contents of a string
167 # s -- string to unescape
169 s
= re
.sub("\\\\n", "\n", s
)
170 s
= re
.sub("\\\\(.)", "\\1", s
)
173 def _split(s
, *comments
):
174 # Split a string into fields according to the usual Disorder string splitting
179 # s -- string to parse
180 # comments -- if present, parse comments
184 # On success, a list of fields is returned.
186 # On error, disorder.parseError is thrown.
191 if comments
and s
[0] == '#':
198 # pick of quoted fields of both kinds
203 fields
.append(_unescape(m
.group(1)))
206 # and unquoted fields
207 m
= _unquoted
.match(s
)
209 fields
.append(m
.group(0))
212 # anything left must be in error
213 if s
[0] == '"' or s
[0] == '\'':
214 raise _splitError("invalid quoted string")
216 raise _splitError("syntax error")
220 # Escape the contents of a string
224 # s -- string to escape
226 if re
.search("[\\\\\"'\n \t\r]", s
) or s
== '':
227 s
= re
.sub(r
'[\\"]', r
'\\\g<0>', s
)
228 s
= re
.sub("\n", r
"\\n", s
)
234 # Quote a list of values
235 return ' '.join(map(_escape
, list))
238 # Return the value of s in a form suitable for writing to stderr
239 return s
.encode(locale
.nl_langinfo(locale
.CODESET
), 'replace')
242 # Convert a list of the form [k1, v1, k2, v2, ..., kN, vN]
243 # to a dictionary {k1:v1, k2:v2, ..., kN:vN}
251 except StopIteration:
256 # parse a queue entry
257 return _list2dict(_split(s
))
259 ########################################################################
263 """DisOrder client class.
265 This class provides access to the DisOrder server either on this
266 machine or across the internet.
268 The server to connect to, and the username and password to use, are
269 determined from the configuration files as described in 'man
272 All methods will connect if necessary, as soon as you have a
273 disorder.client object you can start calling operational methods on
276 However if the server is restarted then the next method called on a
277 connection will throw an exception. This may be considered a bug.
279 All methods block until they complete.
281 Operation methods raise communicationError if the connection breaks,
282 protocolError if the response from the server is malformed, or
283 operationError if the response is valid but indicates that the
290 def __init__(self
, user
=None, password
=None):
291 """Constructor for DisOrder client class.
293 The constructor reads the configuration file, but does not connect
296 If the environment variable DISORDER_PYTHON_DEBUG is set then the
297 debug flags are initialised to that value. This can be overridden
298 with the debug() method below.
300 The constructor Raises parseError() if the configuration file is not
303 pw
= pwd
.getpwuid(os
.getuid())
304 self
.debugging
= int(os
.getenv("DISORDER_PYTHON_DEBUG", 0))
305 self
.config
= { 'collections': [],
306 'username': pw
.pw_name
,
309 self
.password
= password
310 home
= os
.getenv("HOME")
313 privconf
= _configfile
+ "." + pw
.pw_name
314 passfile
= home
+ os
.sep
+ ".disorder" + os
.sep
+ "passwd"
315 if os
.path
.exists(_configfile
):
316 self
._readfile(_configfile
)
317 if os
.path
.exists(privconf
):
318 self
._readfile(privconf
)
319 if os
.path
.exists(passfile
) and _userconf
:
320 self
._readfile(passfile
)
321 self
.state
= 'disconnected'
323 def debug(self
, bits
):
324 """Enable or disable protocol debugging. Debug messages are written
328 bits -- bitmap of operations that should generate debug information
331 debug_proto -- dump control protocol messages (excluding bodies)
332 debug_body -- dump control protocol message bodies
334 self
.debugging
= bits
336 def _debug(self
, bit
, s
):
338 if self
.debugging
& bit
:
339 sys
.stderr
.write(_sanitize(s
))
340 sys
.stderr
.write("\n")
343 def connect(self
, cookie
=None):
344 """c.connect(cookie=None)
346 Connect to the DisOrder server and authenticate.
348 Raises communicationError if connection fails and operationError if
349 authentication fails (in which case disconnection is automatic).
351 May be called more than once to retry connections (e.g. when the
352 server is down). If we are already connected and authenticated,
355 Other operations automatically connect if we're not already
356 connected, so it is not strictly necessary to call this method.
358 If COOKIE is specified then that is used to log in instead of
359 the username/password.
361 if self
.state
== 'disconnected':
363 self
.state
= 'connecting'
364 if 'connect' in self
.config
and len(self
.config
['connect']) > 0:
365 c
= self
.config
['connect']
366 self
.who
= repr(c
) # temporarily
368 a
= socket
.getaddrinfo(None, c
[0],
374 a
= socket
.getaddrinfo(c
[0], c
[1],
380 s
= socket
.socket(a
[0], a
[1], a
[2]);
382 self
.who
= "%s" % a
[3]
384 s
= socket
.socket(socket
.AF_UNIX
, socket
.SOCK_STREAM
);
385 self
.who
= self
.config
['home'] + os
.sep
+ "socket"
387 self
.w
= s
.makefile("wb")
388 self
.r
= s
.makefile("rb")
389 (res
, details
) = self
._simple()
390 (protocol
, algo
, challenge
) = _split(details
)
392 raise communicationError(self
.who
,
393 "unknown protocol version %s" % protocol
)
395 if self
.user
is None:
396 user
= self
.config
['username']
399 if self
.password
is None:
400 password
= self
.config
['password']
402 password
= self
.password
405 h
.update(binascii
.unhexlify(challenge
))
406 self
._simple("user", user
, h
.hexdigest())
408 self
._simple("cookie", cookie
)
409 self
.state
= 'connected'
410 except socket
.error
, e
:
412 raise communicationError(self
.who
, e
)
417 def _disconnect(self
):
418 # disconnect from the server, whatever state we are in
424 self
.state
= 'disconnected'
426 ########################################################################
429 def play(self
, track
):
433 track -- the path of the track to play.
435 Returns the ID of the new queue entry.
437 Note that queue IDs are unicode strings (because all track
438 information values are unicode strings).
440 res
, details
= self
._simple("play", track
)
441 return unicode(details
) # because it's unicode in queue() output
443 def playafter(self
, target
, tracks
):
444 """Insert tracks into a specific point in the queue.
447 target -- target ID or None to insert at start of queue
448 tracks -- a list of tracks to play"""
451 self
._simple("playafter", target
, *tracks
)
453 def remove(self
, track
):
454 """Remove a track from the queue.
457 track -- the path or ID of the track to remove.
459 self
._simple("remove", track
)
462 """Enable playing."""
463 self
._simple("enable")
465 def disable(self
, *now
):
469 now -- if present (with any value), the current track is stopped
473 self
._simple("disable", "now")
475 self
._simple("disable")
477 def scratch(self
, *id):
478 """Scratch the currently playing track.
481 id -- if present, the ID of the track to scratch.
484 self
._simple("scratch", id[0])
486 self
._simple("scratch")
489 """Shut down the server.
491 Only trusted users can perform this operation.
493 self
._simple("shutdown")
495 def reconfigure(self
):
496 """Make the server reload its configuration.
498 Only trusted users can perform this operation.
500 self
._simple("reconfigure")
502 def rescan(self
, *flags
):
503 """Rescan one or more collections.
505 Only trusted users can perform this operation.
507 self
._simple("rescan", *flags
)
510 """Return the server's version number."""
511 return _split(self
._simple("version")[1])[0]
514 """Return the currently playing track.
516 If a track is playing then it is returned as a dictionary. See
517 disorder_protocol(5) for the meanings of the keys. All keys are
518 plain strings but the values will be unicode strings.
520 If no track is playing then None is returned."""
521 res
, details
= self
._simple("playing")
524 return _queueEntry(details
)
525 except _splitError
, s
:
526 raise protocolError(self
.who
, s
.str())
530 def _somequeue(self
, command
):
531 self
._simple(command
)
533 return map(lambda s
: _queueEntry(s
), self
._body())
534 except _splitError
, s
:
535 raise protocolError(self
.who
, s
.str())
538 """Return a list of recently played tracks.
540 The return value is a list of dictionaries corresponding to
541 recently played tracks. The oldest track comes first.
543 See disorder_protocol(5) for the meanings of the keys. All keys are
544 plain strings but the values will be unicode strings."""
545 return self
._somequeue("recent")
548 """Return the current queue.
550 The return value is a list of dictionaries corresponding to
551 recently played tracks. The next track to be played comes first.
553 See disorder_protocol(5) for the meanings of the keys.
554 All keys are plain strings but the values will be unicode strings."""
555 return self
._somequeue("queue")
557 def _somedir(self
, command
, dir, re
):
559 self
._simple(command
, dir, re
[0])
561 self
._simple(command
, dir)
564 def directories(self
, dir, *re
):
565 """List subdirectories of a directory.
568 dir -- directory to list, or '' for the whole root.
569 re -- regexp that results must match. Optional.
571 The return value is a list of the (nonempty) subdirectories of dir.
572 If dir is '' then a list of top-level directories is returned.
574 If a regexp is specified then the basename of each result must
575 match. Matching is case-independent. See pcrepattern(3).
577 return self
._somedir("dirs", dir, re
)
579 def files(self
, dir, *re
):
580 """List files within a directory.
583 dir -- directory to list, or '' for the whole root.
584 re -- regexp that results must match. Optional.
586 The return value is a list of playable files in dir. If dir is ''
587 then a list of top-level files is returned.
589 If a regexp is specified then the basename of each result must
590 match. Matching is case-independent. See pcrepattern(3).
592 return self
._somedir("files", dir, re
)
594 def allfiles(self
, dir, *re
):
595 """List subdirectories and files within a directory.
598 dir -- directory to list, or '' for the whole root.
599 re -- regexp that results must match. Optional.
601 The return value is a list of all (nonempty) subdirectories and
602 files within dir. If dir is '' then a list of top-level files and
603 directories is returned.
605 If a regexp is specified then the basename of each result must
606 match. Matching is case-independent. See pcrepattern(3).
608 return self
._somedir("allfiles", dir, re
)
610 def set(self
, track
, key
, value
):
611 """Set a preference value.
614 track -- the track to modify
615 key -- the preference name
616 value -- the new preference value
618 self
._simple("set", track
, key
, value
)
620 def unset(self
, track
, key
):
621 """Unset a preference value.
624 track -- the track to modify
625 key -- the preference to remove
627 self
._simple("set", track
, key
)
629 def get(self
, track
, key
):
630 """Get a preference value.
633 track -- the track to query
634 key -- the preference to remove
636 The return value is the preference.
638 ret
, details
= self
._simple("get", track
, key
)
642 return _split(details
)[0]
644 def prefs(self
, track
):
645 """Get all the preferences for a track.
648 track -- the track to query
650 The return value is a dictionary of all the track's preferences.
651 Note that even nominally numeric values remain encoded as strings.
653 self
._simple("prefs", track
)
655 for line
in self
._body():
658 except _splitError
, s
:
659 raise protocolError(self
.who
, s
.str())
661 raise protocolError(self
.who
, "invalid prefs body line")
665 def _boolean(self
, s
):
668 def exists(self
, track
):
669 """Return true if a track exists
672 track -- the track to check for"""
673 return self
._boolean(self
._simple("exists", track
))
676 """Return true if playing is enabled"""
677 return self
._boolean(self
._simple("enabled"))
679 def random_enabled(self
):
680 """Return true if random play is enabled"""
681 return self
._boolean(self
._simple("random-enabled"))
683 def random_enable(self
):
684 """Enable random play."""
685 self
._simple("random-enable")
687 def random_disable(self
):
688 """Disable random play."""
689 self
._simple("random-disable")
691 def length(self
, track
):
692 """Return the length of a track in seconds.
695 track -- the track to query.
697 ret
, details
= self
._simple("length", track
)
700 def search(self
, words
):
701 """Search for tracks.
704 words -- the set of words to search for.
706 The return value is a list of track path names, all of which contain
707 all of the required words (in their path name, trackname
710 self
._simple("search", _quote(words
))
716 The return value is a list of all tags which apply to at least one
722 """Get server statistics.
724 The return value is list of statistics.
726 self
._simple("stats")
730 """Get all preferences.
732 The return value is an encoded dump of the preferences database.
737 def set_volume(self
, left
, right
):
741 left -- volume for the left speaker.
742 right -- volume for the right speaker.
744 self
._simple("volume", left
, right
)
746 def get_volume(self
):
749 The return value a tuple consisting of the left and right volumes.
751 ret
, details
= self
._simple("volume")
752 return map(int,string
.split(details
))
754 def move(self
, track
, delta
):
755 """Move a track in the queue.
758 track -- the name or ID of the track to move
759 delta -- the number of steps towards the head of the queue to move
761 ret
, details
= self
._simple("move", track
, str(delta
))
764 def moveafter(self
, target
, tracks
):
765 """Move a track in the queue
768 target -- target ID or None
769 tracks -- a list of IDs to move
771 If target is '' or is not in the queue then the tracks are moved to
772 the head of the queue.
774 Otherwise the tracks are moved to just after the target."""
777 self
._simple("moveafter", target
, *tracks
)
779 def log(self
, callback
):
780 """Read event log entries as they happen.
782 Each event log entry is handled by passing it to callback.
784 The callback takes two arguments, the first is the client and the
785 second the line from the event log.
787 The callback should return True to continue or False to stop (don't
788 forget this, or your program will mysteriously misbehave). Once you
789 stop reading the log the connection is useless and should be
792 It is suggested that you use the disorder.monitor class instead of
793 calling this method directly, but this is not mandatory.
795 See disorder_protocol(5) for the event log syntax.
798 callback -- function to call with log entry
800 ret
, details
= self
._simple("log")
803 self
._debug(client
.debug_body
, "<<< %s" % l
)
804 if l
!= '' and l
[0] == '.':
808 if not callback(self
, l
):
812 """Pause the current track."""
813 self
._simple("pause")
816 """Resume after a pause."""
817 self
._simple("resume")
819 def part(self
, track
, context
, part
):
820 """Get a track name part
823 track -- the track to query
824 context -- the context ('sort' or 'display')
825 part -- the desired part (usually 'artist', 'album' or 'title')
827 The return value is the preference
829 ret
, details
= self
._simple("part", track
, context
, part
)
830 return _split(details
)[0]
832 def setglobal(self
, key
, value
):
833 """Set a global preference value.
836 key -- the preference name
837 value -- the new preference value
839 self
._simple("set-global", key
, value
)
841 def unsetglobal(self
, key
):
842 """Unset a global preference value.
845 key -- the preference to remove
847 self
._simple("set-global", key
)
849 def getglobal(self
, key
):
850 """Get a global preference value.
853 key -- the preference to look up
855 The return value is the preference
857 ret
, details
= self
._simple("get-global", key
)
861 return _split(details
)[0]
863 def make_cookie(self
):
864 """Create a login cookie"""
865 ret
, details
= self
._simple("make-cookie")
866 return _split(details
)[0]
869 """Revoke a login cookie"""
870 self
._simple("revoke")
872 def adduser(self
, user
, password
):
874 self
._simple("adduser", user
, password
)
876 def deluser(self
, user
):
878 self
._simple("deluser", user
)
880 def userinfo(self
, user
, key
):
881 """Get user information"""
882 res
, details
= self
._simple("userinfo", user
, key
)
885 return _split(details
)[0]
887 def edituser(self
, user
, key
, value
):
888 """Set user information"""
889 self
._simple("edituser", user
, key
, value
)
894 The return value is a list of all users."""
895 self
._simple("users")
898 def register(self
, username
, password
, email
):
899 """Register a user"""
900 res
, details
= self
._simple("register", username
, password
, email
)
901 return _split(details
)[0]
903 def confirm(self
, confirmation
):
904 """Confirm a user registration"""
905 res
, details
= self
._simple("confirm", confirmation
)
907 def schedule_list(self
):
908 """Get a list of scheduled events """
909 self
._simple("schedule-list")
912 def schedule_del(self
, event
):
913 """Delete a scheduled event"""
914 self
._simple("schedule-del", event
)
916 def schedule_get(self
, event
):
917 """Get the details for an event as a dict (returns None if
919 res
, details
= self
._simple("schedule-get", event
)
923 for line
in self
._body():
928 def schedule_add(self
, when
, priority
, action
, *rest
):
929 """Add a scheduled event"""
930 self
._simple("schedule-add", str(when
), priority
, action
, *rest
)
933 """Adopt a randomly picked track"""
934 self
._simple("adopt", id)
936 def playlist_delete(self
, playlist
):
937 """Delete a playlist"""
938 res
, details
= self
._simple("playlist-delete", playlist
)
940 raise operationError(res
, details
, "playlist-delete")
942 def playlist_get(self
, playlist
):
943 """Get the contents of a playlist
945 The return value is an array of track names, or None if there is no
947 res
, details
= self
._simple("playlist-get", playlist
)
952 def playlist_lock(self
, playlist
):
953 """Lock a playlist. Playlists can only be modified when locked."""
954 self
._simple("playlist-lock", playlist
)
956 def playlist_unlock(self
):
957 """Unlock the locked playlist."""
958 self
._simple("playlist-unlock")
960 def playlist_set(self
, playlist
, tracks
):
961 """Set the contents of a playlist. The playlist must be locked.
964 playlist -- Playlist to set
965 tracks -- Array of tracks"""
966 self
._simple_body(tracks
, "playlist-set", playlist
)
968 def playlist_set_share(self
, playlist
, share
):
969 """Set the sharing status of a playlist"""
970 self
._simple("playlist-set-share", playlist
, share
)
972 def playlist_get_share(self
, playlist
):
973 """Returns the sharing status of a playlist"""
974 res
, details
= self
._simple("playlist-get-share", playlist
)
977 return _split(details
)[0]
980 """Returns the list of visible playlists"""
981 self
._simple("playlists")
984 ########################################################################
988 # read one response line and return as some suitable string object
990 # If an I/O error occurs, disconnect from the server.
992 # XXX does readline() DTRT regarding character encodings?
994 l
= self
.r
.readline()
995 if not re
.search("\n", l
):
996 raise communicationError(self
.who
, "peer disconnected")
1001 return unicode(l
, "UTF-8")
1003 def _response(self
):
1004 # read a response as a (code, details) tuple
1006 self
._debug(client
.debug_proto
, "<== %s" % l
)
1007 m
= _response
.match(l
)
1009 return int(m
.group(1)), m
.group(2)
1011 raise protocolError(self
.who
, "invalid response %s")
1013 def _send(self
, body
, *command
):
1014 # Quote and send a command and optional body
1016 # Returns the encoded command.
1017 quoted
= _quote(command
)
1018 self
._debug(client
.debug_proto
, "==> %s" % quoted
)
1019 encoded
= quoted
.encode("UTF-8")
1021 self
.w
.write(encoded
)
1035 raise communicationError(self
.who
, e
)
1040 def _simple(self
, *command
):
1041 # Issue a simple command, throw an exception on error
1043 # If an I/O error occurs, disconnect from the server.
1045 # On success or 'normal' errors returns response as a (code, details) tuple
1047 # On error raise operationError
1048 return self
._simple_body(None, *command
)
1050 def _simple_body(self
, body
, *command
):
1051 # Issue a simple command with optional body, throw an exception on error
1053 # If an I/O error occurs, disconnect from the server.
1055 # On success or 'normal' errors returns response as a (code, details) tuple
1057 # On error raise operationError
1058 if self
.state
== 'disconnected':
1061 cmd
= self
._send(body
, *command
)
1064 res
, details
= self
._response()
1065 if res
/ 100 == 2 or res
== 555:
1067 raise operationError(res
, details
, cmd
)
1070 # Fetch a dot-stuffed body
1074 self
._debug(client
.debug_body
, "<<< %s" % l
)
1075 if l
!= '' and l
[0] == '.':
1081 ########################################################################
1082 # Configuration file parsing
1084 def _readfile(self
, path
):
1085 # Read a configuration file
1089 # path -- path of file to read
1091 # handlers for various commands
1092 def _collection(self
, command
, args
):
1094 return "'%s' takes three args" % command
1095 self
.config
["collections"].append(args
)
1097 def _unary(self
, command
, args
):
1099 return "'%s' takes only one arg" % command
1100 self
.config
[command
] = args
[0]
1102 def _include(self
, command
, args
):
1104 return "'%s' takes only one arg" % command
1105 self
._readfile(args
[0])
1107 def _any(self
, command
, args
):
1108 self
.config
[command
] = args
1110 # mapping of options to handlers
1111 _options
= { "collection": _collection
,
1116 "include": _include
}
1119 for lno
, line
in enumerate(file(path
, "r")):
1121 fields
= _split(line
, 'comments')
1122 except _splitError
, s
:
1123 raise parseError(path
, lno
+ 1, str(s
))
1126 # we just ignore options we don't know about, so as to cope gracefully
1127 # with version skew (and nothing to do with implementor laziness)
1128 if command
in _options
:
1129 e
= _options
[command
](self
, command
, fields
[1:])
1131 self
._parseError(path
, lno
+ 1, e
)
1133 def _parseError(self
, path
, lno
, s
):
1134 raise parseError(path
, lno
, s
)
1136 ########################################################################
1140 """DisOrder event log monitor class
1142 Intended to be subclassed with methods corresponding to event log
1143 messages the implementor cares about over-ridden."""
1145 def __init__(self
, c
=None):
1146 """Constructor for the monitor class
1148 Can be passed a client to use. If none is specified then one
1149 will be created specially for the purpose.
1158 """Start monitoring logs. Continues monitoring until one of the
1159 message-specific methods returns False. Can be called more than
1160 once (but not recursively!)"""
1161 self
.c
.log(self
._callback
)
1164 """Return the timestamp of the current (or most recent) event log entry"""
1165 return self
.timestamp
1167 def _callback(self
, c
, line
):
1171 return self
.invalid(line
)
1173 return self
.invalid(line
)
1174 self
.timestamp
= int(bits
[0], 16)
1177 if keyword
== 'completed':
1179 return self
.completed(bits
[0])
1180 elif keyword
== 'failed':
1182 return self
.failed(bits
[0], bits
[1])
1183 elif keyword
== 'moved':
1188 return self
.invalid(line
)
1189 return self
.moved(bits
[0], n
, bits
[2])
1190 elif keyword
== 'playing':
1192 return self
.playing(bits
[0], None)
1193 elif len(bits
) == 2:
1194 return self
.playing(bits
[0], bits
[1])
1195 elif keyword
== 'queue' or keyword
== 'recent-added':
1197 q
= _list2dict(bits
)
1199 return self
.invalid(line
)
1200 if keyword
== 'queue':
1201 return self
.queue(q
)
1202 if keyword
== 'recent-added':
1203 return self
.recent_added(q
)
1204 elif keyword
== 'recent-removed':
1206 return self
.recent_removed(bits
[0])
1207 elif keyword
== 'removed':
1209 return self
.removed(bits
[0], None)
1210 elif len(bits
) == 2:
1211 return self
.removed(bits
[0], bits
[1])
1212 elif keyword
== 'scratched':
1214 return self
.scratched(bits
[0], bits
[1])
1215 elif keyword
== 'rescanned':
1216 return self
.rescanned()
1217 return self
.invalid(line
)
1219 def completed(self
, track
):
1220 """Called when a track completes.
1223 track -- track that completed"""
1226 def failed(self
, track
, error
):
1227 """Called when a player suffers an error.
1230 track -- track that failed
1231 error -- error indicator"""
1234 def moved(self
, id, offset
, user
):
1235 """Called when a track is moved in the queue.
1238 id -- queue entry ID
1239 offset -- distance moved
1240 user -- user responsible"""
1243 def playing(self
, track
, user
):
1244 """Called when a track starts playing.
1247 track -- track that has started
1248 user -- user that submitted track, or None"""
1252 """Called when a track is added to the queue.
1255 q -- dictionary of new queue entry"""
1258 def recent_added(self
, q
):
1259 """Called when a track is added to the recently played list
1262 q -- dictionary of new queue entry"""
1265 def recent_removed(self
, id):
1266 """Called when a track is removed from the recently played list
1269 id -- ID of removed entry (always the oldest)"""
1272 def removed(self
, id, user
):
1273 """Called when a track is removed from the queue, either manually
1274 or in order to play it.
1277 id -- ID of removed entry
1278 user -- user responsible (or None if we're playing this track)"""
1281 def scratched(self
, track
, user
):
1282 """Called when a track is scratched
1285 track -- track that was scratched
1286 user -- user responsible"""
1289 def invalid(self
, line
):
1290 """Called when an event log line cannot be interpreted
1293 line -- line that could not be understood"""
1296 def rescanned(self
):
1297 """Called when a rescan completes"""
1302 # py-indent-offset:2