#
-# Copyright (C) 2004, 2005, 2007 Richard Kettlewell
+# Copyright (C) 2004, 2005, 2007, 2008 Richard Kettlewell
#
-# This program is free software; you can redistribute it and/or modify
+# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
-# the Free Software Foundation; either version 2 of the License, or
+# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
-# This program is distributed in the hope that it will be useful, but
-# WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
-# General Public License for more details.
-#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
# You should have received a copy of the GNU General Public License
-# along with this program; if not, write to the Free Software
-# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
-# USA
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
"""Python support for DisOrder
import pwd
import socket
import binascii
-import sha
+import hashlib
import sys
import locale
# various regexps we'll use
_ws = re.compile(r"^[ \t\n\r]+")
-_squote = re.compile("'(([^\\\\']|\\\\[\\\\\"'n])+)'")
-_dquote = re.compile("\"(([^\\\\\"]|\\\\[\\\\\"'n])+)\"")
+_squote = re.compile("'(([^\\\\']|\\\\[\\\\\"'n])*)'")
+_dquote = re.compile("\"(([^\\\\\"]|\\\\[\\\\\"'n])*)\"")
_unquoted = re.compile("[^\"' \\t\\n\\r][^ \t\n\r]*")
_response = re.compile("([0-9]{3}) ?(.*)")
+# hashes
+_hashes = {
+ "sha1": hashlib.sha1,
+ "SHA1": hashlib.sha1,
+ "sha256": hashlib.sha256,
+ "SHA256": hashlib.sha256,
+ "sha384": hashlib.sha384,
+ "SHA384": hashlib.sha384,
+ "sha512": hashlib.sha512,
+ "SHA512": hashlib.sha512,
+};
+
version = "_version_"
########################################################################
self.cmd_ = cmd
self.details_ = details
def __str__(self):
- """Return the complete response string from the server, with the command
- if available.
+ """Return the complete response string from the server, with the
+ command if available.
Excludes the final newline.
"""
s.connect(self.who)
self.w = s.makefile("wb")
self.r = s.makefile("rb")
- (res, challenge) = self._simple()
+ (res, details) = self._simple()
+ (protocol, algo, challenge) = _split(details)
+ if protocol != '2':
+ raise communicationError(self.who,
+ "unknown protocol version %s" % protocol)
if cookie is None:
if self.user is None:
user = self.config['username']
password = self.config['password']
else:
password = self.password
- h = sha.sha()
+ h = _hashes[algo]()
h.update(password)
h.update(binascii.unhexlify(challenge))
self._simple("user", user, h.hexdigest())
########################################################################
# Operations
- def become(self, who):
- """Become another user.
-
- Arguments:
- who -- the user to become.
-
- Only trusted users can perform this operation.
- """
- self._simple("become", who)
-
def play(self, track):
"""Play a track.
Returns the ID of the new queue entry.
- Note that queue IDs are unicode strings (because all track information
- values are unicode strings).
+ Note that queue IDs are unicode strings (because all track
+ information values are unicode strings).
"""
res, details = self._simple("play", track)
return unicode(details) # because it's unicode in queue() output
+ def playafter(self, target, tracks):
+ """Insert tracks into a specific point in the queue.
+
+ Arguments:
+ target -- target ID or None to insert at start of queue
+ tracks -- a list of tracks to play"""
+ if target is None:
+ target = ''
+ self._simple("playafter", target, *tracks)
+
def remove(self, track):
"""Remove a track from the queue.
"""
self._simple("reconfigure")
- def rescan(self, pattern):
+ def rescan(self, *flags):
"""Rescan one or more collections.
- Arguments:
- pattern -- glob pattern matching collections to rescan.
-
Only trusted users can perform this operation.
"""
- self._simple("rescan", pattern)
+ self._simple("rescan", *flags)
def version(self):
"""Return the server's version number."""
- return self._simple("version")[1]
+ return _split(self._simple("version")[1])[0]
def playing(self):
"""Return the currently playing track.
The return value is a list of dictionaries corresponding to
recently played tracks. The next track to be played comes first.
- See disorder_protocol(5) for the meanings of the keys. All keys are
- plain strings but the values will be unicode strings."""
+ See disorder_protocol(5) for the meanings of the keys.
+ All keys are plain strings but the values will be unicode strings."""
return self._somequeue("queue")
def _somedir(self, command, dir, re):
if ret == 555:
return None
else:
- return details
+ return _split(details)[0]
def prefs(self, track):
"""Get all the preferences for a track.
second the line from the event log.
The callback should return True to continue or False to stop (don't
- forget this, or your program will mysteriously misbehave).
+ forget this, or your program will mysteriously misbehave). Once you
+ stop reading the log the connection is useless and should be
+ deleted.
It is suggested that you use the disorder.monitor class instead of
calling this method directly, but this is not mandatory.
l = l[1:]
if not callback(self, l):
break
- # tell the server to stop sending, eat the remains of the body,
- # eat the response
- self._send("version")
- self._body()
- self._response()
def pause(self):
"""Pause the current track."""
The return value is the preference
"""
ret, details = self._simple("part", track, context, part)
- return details
+ return _split(details)[0]
def setglobal(self, key, value):
"""Set a global preference value.
if ret == 555:
return None
else:
- return details
+ return _split(details)[0]
def make_cookie(self):
"""Create a login cookie"""
ret, details = self._simple("make-cookie")
- return details
+ return _split(details)[0]
def revoke(self):
"""Revoke a login cookie"""
self._simple("users")
return self._body()
+ def register(self, username, password, email):
+ """Register a user"""
+ res, details = self._simple("register", username, password, email)
+ return _split(details)[0]
+
+ def confirm(self, confirmation):
+ """Confirm a user registration"""
+ res, details = self._simple("confirm", confirmation)
+
+ def schedule_list(self):
+ """Get a list of scheduled events """
+ self._simple("schedule-list")
+ return self._body()
+
+ def schedule_del(self, event):
+ """Delete a scheduled event"""
+ self._simple("schedule-del", event)
+
+ def schedule_get(self, event):
+ """Get the details for an event as a dict (returns None if
+ event not found)"""
+ res, details = self._simple("schedule-get", event)
+ if res == 555:
+ return None
+ d = {}
+ for line in self._body():
+ bits = _split(line)
+ d[bits[0]] = bits[1]
+ return d
+
+ def schedule_add(self, when, priority, action, *rest):
+ """Add a scheduled event"""
+ self._simple("schedule-add", str(when), priority, action, *rest)
+
+ def adopt(self, id):
+ """Adopt a randomly picked track"""
+ self._simple("adopt", id)
+
+ def playlist_delete(self, playlist):
+ """Delete a playlist"""
+ res, details = self._simple("playlist-delete", playlist)
+ if res == 555:
+ raise operationError(res, details, "playlist-delete")
+
+ def playlist_get(self, playlist):
+ """Get the contents of a playlist
+
+ The return value is an array of track names, or None if there is no
+ such playlist."""
+ res, details = self._simple("playlist-get", playlist)
+ if res == 555:
+ return None
+ return self._body()
+
+ def playlist_lock(self, playlist):
+ """Lock a playlist. Playlists can only be modified when locked."""
+ self._simple("playlist-lock", playlist)
+
+ def playlist_unlock(self):
+ """Unlock the locked playlist."""
+ self._simple("playlist-unlock")
+
+ def playlist_set(self, playlist, tracks):
+ """Set the contents of a playlist. The playlist must be locked.
+
+ Arguments:
+ playlist -- Playlist to set
+ tracks -- Array of tracks"""
+ self._simple_body(tracks, "playlist-set", playlist)
+
+ def playlist_set_share(self, playlist, share):
+ """Set the sharing status of a playlist"""
+ self._simple("playlist-set-share", playlist, share)
+
+ def playlist_get_share(self, playlist):
+ """Returns the sharing status of a playlist"""
+ res, details = self._simple("playlist-get-share", playlist)
+ if res == 555:
+ return None
+ return _split(details)[0]
+
+ def playlists(self):
+ """Returns the list of visible playlists"""
+ self._simple("playlists")
+ return self._body()
+
########################################################################
# I/O infrastructure
else:
raise protocolError(self.who, "invalid response %s")
- def _send(self, *command):
- # Quote and send a command
+ def _send(self, body, *command):
+ # Quote and send a command and optional body
#
# Returns the encoded command.
quoted = _quote(command)
try:
self.w.write(encoded)
self.w.write("\n")
+ if body != None:
+ for l in body:
+ if l[0] == ".":
+ self.w.write(".")
+ self.w.write(l)
+ self.w.write("\n")
+ self.w.write(".\n")
self.w.flush()
return encoded
except IOError, e:
self._disconnect()
raise
- def _simple(self, *command):
+ def _simple(self, *command):
# Issue a simple command, throw an exception on error
#
# If an I/O error occurs, disconnect from the server.
# On success or 'normal' errors returns response as a (code, details) tuple
#
# On error raise operationError
+ return self._simple_body(None, *command)
+
+ def _simple_body(self, body, *command):
+ # Issue a simple command with optional body, throw an exception on error
+ #
+ # If an I/O error occurs, disconnect from the server.
+ #
+ # On success or 'normal' errors returns response as a (code, details) tuple
+ #
+ # On error raise operationError
if self.state == 'disconnected':
self.connect()
if command:
- cmd = self._send(*command)
+ cmd = self._send(body, *command)
else:
cmd = None
res, details = self._response()
class monitor:
"""DisOrder event log monitor class
- Intended to be subclassed with methods corresponding to event log messages
- the implementor cares about over-ridden."""
+ Intended to be subclassed with methods corresponding to event log
+ messages the implementor cares about over-ridden."""
def __init__(self, c=None):
"""Constructor for the monitor class
def run(self):
"""Start monitoring logs. Continues monitoring until one of the
- message-specific methods returns False. Can be called more than once
- (but not recursively!)"""
+ message-specific methods returns False. Can be called more than
+ once (but not recursively!)"""
self.c.log(self._callback)
def when(self):
elif keyword == 'scratched':
if len(bits) == 2:
return self.scratched(bits[0], bits[1])
+ elif keyword == 'rescanned':
+ return self.rescanned()
return self.invalid(line)
def completed(self, track):
line -- line that could not be understood"""
return True
+ def rescanned(self):
+ """Called when a rescan completes"""
+ return True
+
# Local Variables:
# mode:python
# py-indent-offset:2