chiark / gitweb /
More compact mode support. The notebook will disappear and is
[disorder] / python / disorder.py.in
index 9a6b3853c7111e36da6a5d66a019d15e34c53bdb..fe054a9474aec4ef684cfec3e8efff3b30835a45 100644 (file)
@@ -1,20 +1,18 @@
 #
 #
-# 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
 # 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.
 #
 # (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
 # 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
 #
 
 """Python support for DisOrder
@@ -52,7 +50,7 @@ import os
 import pwd
 import socket
 import binascii
 import pwd
 import socket
 import binascii
-import sha
+import hashlib
 import sys
 import locale
 
 import sys
 import locale
 
@@ -62,12 +60,24 @@ _userconf = True
 
 # various regexps we'll use
 _ws = re.compile(r"^[ \t\n\r]+")
 
 # 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}) ?(.*)")
 
 _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_"
 
 ########################################################################
 version = "_version_"
 
 ########################################################################
@@ -115,8 +125,8 @@ class operationError(Error):
     self.cmd_ = cmd
     self.details_ = details
   def __str__(self):
     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.
     """
 
     Excludes the final newline.
     """
@@ -375,7 +385,11 @@ class client:
           s.connect(self.who)
         self.w = s.makefile("wb")
         self.r = s.makefile("rb")
           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']
         if cookie is None:
           if self.user is None:
             user = self.config['username']
@@ -385,7 +399,7 @@ class client:
             password = self.config['password']
           else:
             password = self.password
             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())
           h.update(password)
           h.update(binascii.unhexlify(challenge))
           self._simple("user", user, h.hexdigest())
@@ -411,16 +425,6 @@ class client:
   ########################################################################
   # Operations
 
   ########################################################################
   # 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.
 
   def play(self, track):
     """Play a track.
 
@@ -429,12 +433,22 @@ class client:
 
     Returns the ID of the new queue entry.
 
 
     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
 
     """
     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.
 
   def remove(self, track):
     """Remove a track from the queue.
 
@@ -484,19 +498,16 @@ class client:
     """
     self._simple("reconfigure")
 
     """
     self._simple("reconfigure")
 
-  def rescan(self, pattern):
+  def rescan(self, *flags):
     """Rescan one or more collections.
 
     """Rescan one or more collections.
 
-    Arguments:
-    pattern -- glob pattern matching collections to rescan.
-
     Only trusted users can perform this operation.
     """
     Only trusted users can perform this operation.
     """
-    self._simple("rescan", pattern)
+    self._simple("rescan", *flags)
 
   def version(self):
     """Return the server's version number."""
 
   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.
 
   def playing(self):
     """Return the currently playing track.
@@ -538,8 +549,8 @@ class client:
     The return value is a list of dictionaries corresponding to
     recently played tracks.  The next track to be played comes first.
 
     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):
     return self._somequeue("queue")
 
   def _somedir(self, command, dir, re):
@@ -627,7 +638,7 @@ class client:
     if ret == 555:
       return None
     else:
     if ret == 555:
       return None
     else:
-      return details
+      return _split(details)[0]
 
   def prefs(self, track):
     """Get all the preferences for a track.
 
   def prefs(self, track):
     """Get all the preferences for a track.
@@ -773,7 +784,9 @@ class client:
     second the line from the event log.
     
     The callback should return True to continue or False to stop (don't
     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.
 
     It is suggested that you use the disorder.monitor class instead of
     calling this method directly, but this is not mandatory.
@@ -793,11 +806,6 @@ class client:
         l = l[1:]
       if not callback(self, l):
         break
         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."""
 
   def pause(self):
     """Pause the current track."""
@@ -818,7 +826,7 @@ class client:
     The return value is the preference 
     """
     ret, details = self._simple("part", track, context, part)
     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.
 
   def setglobal(self, key, value):
     """Set a global preference value.
@@ -849,12 +857,12 @@ class client:
     if ret == 555:
       return None
     else:
     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")
 
   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"""
   
   def revoke(self):
     """Revoke a login cookie"""
@@ -886,6 +894,92 @@ class client:
     self._simple("users")
     return self._body()
 
     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
 
   ########################################################################
   # I/O infrastructure
 
@@ -915,8 +1009,8 @@ class client:
     else:
       raise protocolError(self.who, "invalid response %s")
 
     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)
     #
     # Returns the encoded command.
     quoted = _quote(command)
@@ -925,6 +1019,13 @@ class client:
     try:
       self.w.write(encoded)
       self.w.write("\n")
     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.w.flush()
       return encoded
     except IOError, e:
@@ -935,7 +1036,7 @@ class client:
       self._disconnect()
       raise
 
       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.
     # Issue a simple command, throw an exception on error
     #
     # If an I/O error occurs, disconnect from the server.
@@ -943,10 +1044,20 @@ class client:
     # On success or 'normal' errors returns response as a (code, details) tuple
     #
     # On error raise operationError
     # 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:
     if self.state == 'disconnected':
       self.connect()
     if command:
-      cmd = self._send(*command)
+      cmd = self._send(body, *command)
     else:
       cmd = None
     res, details = self._response()
     else:
       cmd = None
     res, details = self._response()
@@ -1027,8 +1138,8 @@ class client:
 class monitor:
   """DisOrder event log monitor class
 
 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 __init__(self, c=None):
     """Constructor for the monitor class
@@ -1044,8 +1155,8 @@ class monitor:
 
   def run(self):
     """Start monitoring logs.  Continues monitoring until one of the
 
   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):
     self.c.log(self._callback)
 
   def when(self):
@@ -1100,6 +1211,8 @@ class monitor:
     elif keyword == 'scratched':
       if len(bits) == 2:
         return self.scratched(bits[0], bits[1])
     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):
     return self.invalid(line)
 
   def completed(self, track):
@@ -1179,6 +1292,10 @@ class monitor:
     line -- line that could not be understood"""
     return True
 
     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
 # Local Variables:
 # mode:python
 # py-indent-offset:2