chiark / gitweb /
Fix mis-merged trackdb_open().
[disorder] / lib / eclient.c
index 38edb64..676aa06 100644 (file)
@@ -1,39 +1,36 @@
 /*
  * This file is part of DisOrder.
- * Copyright (C) 2006 Richard Kettlewell
+ * Copyright (C) 2006-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/>.
+ */
+/** @file lib/eclient.c
+ * @brief Client code for event-driven programs
  */
 
-#include <config.h>
-#include "types.h"
+#include "common.h"
 
 #include <sys/types.h>
 #include <sys/socket.h>
 #include <netinet/in.h>
 #include <sys/un.h>
-#include <string.h>
-#include <stdio.h>
 #include <unistd.h>
 #include <errno.h>
 #include <netdb.h>
-#include <stdlib.h>
-#include <assert.h>
 #include <inttypes.h>
 #include <stddef.h>
+#include <time.h>
 
 #include "log.h"
 #include "mem.h"
 
 /* TODO: more commands */
 
+/** @brief How often to send data to the server when receiving logs */
+#define LOG_PROD_INTERVAL 10
+
 /* Types *********************************************************************/
 
+/** @brief Client state */
 enum client_state {
-  state_disconnected,                   /* not connected */
-  state_connecting,                     /* waiting for connect() */
-  state_connected,                      /* connected but not authenticated */
-  state_idle,                           /* not doing anything */
-  state_cmdresponse,                    /* waiting for command resonse */
-  state_body,                           /* accumulating body */
-  state_log,                            /* monitoring log */
+  state_disconnected,          /**< @brief not connected */
+  state_connecting,            /**< @brief waiting for connect() */
+  state_connected,             /**< @brief connected but not authenticated */
+  state_idle,                  /**< @brief not doing anything */
+  state_cmdresponse,           /**< @brief waiting for command resonse */
+  state_body,                  /**< @brief accumulating body */
+  state_log,                   /**< @brief monitoring log */
 };
 
+/** @brief Names for @ref client_state */
 static const char *const states[] = {
   "disconnected",
   "connecting",
@@ -79,55 +81,77 @@ static const char *const states[] = {
 
 struct operation;                       /* forward decl */
 
+/** @brief Type of an operation callback */
 typedef void operation_callback(disorder_eclient *c, struct operation *op);
 
-/* A pending operation.  This can be either a command or part of the
- * authentication protocol.  In the former case new commands are appended to
- * the list, in the latter case they are inserted at the front. */
+/** @brief A pending operation.
+ *
+ * This can be either a command or part of the authentication protocol.  In the
+ * former case new commands are appended to the list, in the latter case they
+ * are inserted at the front. */
 struct operation {
-  struct operation *next;               /* next operation */
-  char *cmd;                            /* command to send or 0 */
-  operation_callback *opcallback;       /* internal completion callback */
-  void (*completed)();                  /* user completion callback or 0 */
-  void *v;                              /* data for COMPLETED */
-  disorder_eclient *client;             /* owning client */
-  int sent;                             /* true if sent to server */
+  struct operation *next;          /**< @brief next operation */
+  char *cmd;                       /**< @brief command to send or 0 */
+  char **body;                     /**< @brief command body */
+  operation_callback *opcallback;  /**< @brief internal completion callback */
+  void (*completed)();             /**< @brief user completion callback or 0 */
+  void *v;                         /**< @brief data for COMPLETED */
+  disorder_eclient *client;        /**< @brief owning client */
+
+  /** @brief true if sent to server
+   *
+   * This is cleared by disorder_eclient_close(), forcing all queued
+   * commands to be transparently resent.
+   */
+  int sent;
 };
 
+/** @brief Client structure */
 struct disorder_eclient {
-  const char *ident;
-  int fd;
-  enum client_state state;              /* current state */
-  int authenticated;                    /* true when authenicated */
-  struct dynstr output;                 /* output buffer */
-  struct dynstr input;                  /* input buffer */
-  int eof;                              /* input buffer is at EOF */
-  /* error reporting callbacks */
-  const disorder_eclient_callbacks *callbacks;
-  void *u;
-  /* operation queuue */
-  struct operation *ops, **opstail;
+  char *ident;
+  int fd;                               /**< @brief connection to server */
+  enum client_state state;              /**< @brief current state */
+  int authenticated;                    /**< @brief true when authenicated */
+  struct dynstr output;                 /**< @brief output buffer */
+  struct dynstr input;                  /**< @brief input buffer */
+  int eof;                              /**< @brief input buffer is at EOF */
+  const disorder_eclient_callbacks *callbacks; /**< @brief error callbacks */
+  void *u;                              /**< @brief user data */
+  struct operation *ops;                /**< @brief queue of operations */
+  struct operation **opstail;           /**< @brief queue tail */
   /* accumulated response */
-  int rc;                               /* response code */
-  char *line;                           /* complete line */
-  struct vector vec;                    /* body */
-  /* log client callback */
+  int rc;                               /**< @brief response code */
+  char *line;                           /**< @brief complete line */
+  struct vector vec;                    /**< @brief body */
+
   const disorder_eclient_log_callbacks *log_callbacks;
-  void *log_v;
-  unsigned long statebits;              /* current state */
+  /**< @brief log callbacks
+   *
+   * Once disorder_eclient_log() has been issued this is always set.  When we
+   * re-connect it is checked to re-issue the log command.
+   */
+  void *log_v;                          /**< @brief user data */
+  unsigned long statebits;              /**< @brief latest state */
+
+  time_t last_prod;
+  /**< @brief last time we sent a prod
+   *
+   * When we are receiving log data we send a "prod" byte to the server from
+   * time to time so that we detect broken connections reasonably quickly.  The
+   * server just ignores these bytes.
+   */
+
+  /** @brief Protocol version */
+  int protocol;
+
+  /** @brief True if enabled */
+  int enabled;
 };
 
 /* Forward declarations ******************************************************/
 
-static int start_connect(void *cc,
-                        const struct sockaddr *sa,
-                        socklen_t len,
-                        const char *ident);
+static int start_connect(disorder_eclient *c);
 static void process_line(disorder_eclient *c, char *line);
-static int start_connect(void *cc,
-                        const struct sockaddr *sa,
-                        socklen_t len,
-                        const char *ident);
 static void maybe_connected(disorder_eclient *c);
 static void authbanner_opcallback(disorder_eclient *c,
                                   struct operation *op);
@@ -142,6 +166,8 @@ static void stash_command(disorder_eclient *c,
                           operation_callback *opcallback,
                           void (*completed)(),
                           void *v,
+                          int nbody,
+                          char **body,
                           const char *cmd,
                           ...);
 static void log_opcallback(disorder_eclient *c, struct operation *op);
@@ -157,32 +183,61 @@ static void logentry_removed(disorder_eclient *c, int nvec, char **vec);
 static void logentry_scratched(disorder_eclient *c, int nvec, char **vec);
 static void logentry_state(disorder_eclient *c, int nvec, char **vec);
 static void logentry_volume(disorder_eclient *c, int nvec, char **vec);
+static void logentry_rescanned(disorder_eclient *c, int nvec, char **vec);
+static void logentry_user_add(disorder_eclient *c, int nvec, char **vec);
+static void logentry_user_confirm(disorder_eclient *c, int nvec, char **vec);
+static void logentry_user_delete(disorder_eclient *c, int nvec, char **vec);
+static void logentry_user_edit(disorder_eclient *c, int nvec, char **vec);
+static void logentry_rights_changed(disorder_eclient *c, int nvec, char **vec);
+static void logentry_adopted(disorder_eclient *c, int nvec, char **vec);
+static void logentry_playlist_created(disorder_eclient *c, int nvec, char **vec);
+static void logentry_playlist_deleted(disorder_eclient *c, int nvec, char **vec);
+static void logentry_playlist_modified(disorder_eclient *c, int nvec, char **vec);
 
 /* Tables ********************************************************************/
 
-static const struct logentry_handler {
-  const char *name;
-  int min, max;
+/** @brief One possible log entry */
+struct logentry_handler {
+  const char *name;                     /**< @brief Entry name */
+  int min;                              /**< @brief Minimum arguments */
+  int max;                              /**< @brief Maximum arguments */
   void (*handler)(disorder_eclient *c,
                   int nvec,
-                  char **vec);
-} logentry_handlers[] = {
+                  char **vec);          /**< @brief Handler function */
+};
+
+/** @brief Table for parsing log entries */
+static const struct logentry_handler logentry_handlers[] = {
 #define LE(X, MIN, MAX) { #X, MIN, MAX, logentry_##X }
+  LE(adopted, 2, 2),
   LE(completed, 1, 1),
   LE(failed, 2, 2),
   LE(moved, 1, 1),
   LE(playing, 1, 2),
+  LE(playlist_created, 2, 2),
+  LE(playlist_deleted, 1, 1),
+  LE(playlist_modified, 2, 2),
   LE(queue, 2, INT_MAX),
   LE(recent_added, 2, INT_MAX),
   LE(recent_removed, 1, 1),
   LE(removed, 1, 2),
+  LE(rescanned, 0, 0),
+  LE(rights_changed, 1, 1),
   LE(scratched, 2, 2),
   LE(state, 1, 1),
+  LE(user_add, 1, 1),
+  LE(user_confirm, 1, 1),
+  LE(user_delete, 1, 1),
+  LE(user_edit, 2, 2),
   LE(volume, 2, 2)
 };
 
 /* Setup and teardown ********************************************************/
 
+/** @brief Create a new client
+ *
+ * Does NOT connect the client - connections are made (and re-made) on demand.
+ */
 disorder_eclient *disorder_eclient_new(const disorder_eclient_callbacks *cb,
                                        void *u) {
   disorder_eclient *c = xmalloc(sizeof *c);
@@ -191,16 +246,20 @@ disorder_eclient *disorder_eclient_new(const disorder_eclient_callbacks *cb,
   c->callbacks = cb;
   c->u = u;
   c->opstail = &c->ops;
+  c->enabled = 1;
   vector_init(&c->vec);
   dynstr_init(&c->input);
   dynstr_init(&c->output);
-  if(!config->password) {
-    error(0, "no password set");
-    return 0;
-  }
   return c;
 }
 
+/** @brief Disconnect a client
+ * @param c Client to disconnect
+ *
+ * NB that this routine just disconnnects the TCP connection.  It does not
+ * destroy the client!  If you continue to use it then it will attempt to
+ * reconnect.
+ */
 void disorder_eclient_close(disorder_eclient *c) {
   struct operation *op;
 
@@ -211,6 +270,7 @@ void disorder_eclient_close(disorder_eclient *c) {
     xclose(c->fd);
     c->fd = -1;
     c->state = state_disconnected;
+    c->statebits = 0;
   }
   c->output.nvec = 0;
   c->input.nvec = 0;
@@ -219,11 +279,33 @@ void disorder_eclient_close(disorder_eclient *c) {
   /* We'll need to resend all operations */
   for(op = c->ops; op; op = op->next)
     op->sent = 0;
+  /* Drop our use a hint that we're disconnected */
+  if(c->log_callbacks && c->log_callbacks->state)
+    c->log_callbacks->state(c->log_v, c->statebits);
+}
+
+/** @brief Permit new connection activity */
+void disorder_eclient_enable_connect(disorder_eclient *c) {
+  c->enabled = 1;
+}
+
+/** @brief Suppress new connection activity */
+void disorder_eclient_disable_connect(disorder_eclient *c) {
+  c->enabled = 0;
+}
+
+/** @brief Return current state */
+unsigned long disorder_eclient_state(const disorder_eclient *c) {
+  return c->statebits | (c->state > state_connected ? DISORDER_CONNECTED : 0);
 }
 
 /* Error reporting ***********************************************************/
 
-/* called when a connection error occurs */
+/** @brief called when a connection error occurs
+ *
+ * After this called we will be disconnected (by disorder_eclient_close()),
+ * so there will be a reconnection before any commands can be sent.
+ */
 static int comms_error(disorder_eclient *c, const char *fmt, ...) {
   va_list ap;
   char *s;
@@ -237,7 +319,7 @@ static int comms_error(disorder_eclient *c, const char *fmt, ...) {
   return -1;
 }
 
-/* called when the server reports an error */
+/** @brief called when the server reports an error */
 static int protocol_error(disorder_eclient *c, struct operation *op,
                           int code, const char *fmt, ...) {
   va_list ap;
@@ -253,9 +335,37 @@ static int protocol_error(disorder_eclient *c, struct operation *op,
 
 /* State machine *************************************************************/
 
+/** @brief Send an operation (into the output buffer)
+ * @param op Operation to send
+ */
+static void op_send(struct operation *op) {
+  disorder_eclient *const c = op->client;
+  put(c, op->cmd, strlen(op->cmd));
+  if(op->body) {
+    for(int n = 0; op->body[n]; ++n) {
+      if(op->body[n][0] == '.')
+        put(c, ".", 1);
+      put(c, op->body[n], strlen(op->body[n]));
+      put(c, "\n", 1);
+    }
+    put(c, ".\n", 2);
+  }
+  op->sent = 1;
+}
+
+/** @brief Called when there's something to do
+ * @param c Client
+ * @param mode bitmap of @ref DISORDER_POLL_READ and/or @ref DISORDER_POLL_WRITE.
+ *
+ * This should be called from by your code when the file descriptor is readable
+ * or writable (as requested by the @c poll callback, see @ref
+ * disorder_eclient_callbacks) and in any case from time to time (with @p mode
+ * = 0) to allow for retries to work.
+ */
 void disorder_eclient_polled(disorder_eclient *c, unsigned mode) {
   struct operation *op;
-
+  time_t now;
+  
   D(("disorder_eclient_polled fd=%d state=%s mode=[%s %s]",
      c->fd, states[c->state],
      mode & DISORDER_POLL_READ ? "READ" : "",
@@ -268,7 +378,15 @@ void disorder_eclient_polled(disorder_eclient *c, unsigned mode) {
 
   if(c->state == state_disconnected) {
     D(("state_disconnected"));
-    with_sockaddr(c, start_connect);
+    /* If there is no password yet then we cannot connect */
+    if(!config->password) {
+      comms_error(c, "no password is configured");
+      c->enabled = 0;
+      return;
+    }
+    /* Only try to connect if enabled */
+    if(c->enabled)
+      start_connect(c);
     /* might now be state_disconnected (on error), state_connecting (slow
      * connect) or state_connected (fast connect).  If state_disconnected then
      * we just rely on a periodic callback from the event loop sometime. */
@@ -288,7 +406,7 @@ void disorder_eclient_polled(disorder_eclient *c, unsigned mode) {
     D(("state_connected"));
     /* We just connected.  Initiate the authentication protocol. */
     stash_command(c, 1/*queuejump*/, authbanner_opcallback,
-                  0/*completed*/, 0/*v*/, 0/*cmd*/);
+                  0/*completed*/, 0/*v*/, -1/*nbody*/, 0/*body*/, 0/*cmd*/);
     /* We never stay is state_connected very long.  We could in principle jump
      * straight to state_cmdresponse since there's actually no command to
      * send, but that would arguably be cheating. */
@@ -304,17 +422,13 @@ void disorder_eclient_polled(disorder_eclient *c, unsigned mode) {
       if(c->authenticated) {
         /* Transmit all unsent operations */
         for(op = c->ops; op; op = op->next) {
-          if(!op->sent) {
-            put(c, op->cmd, strlen(op->cmd));
-            op->sent = 1;
-          }
+          if(!op->sent)
+            op_send(op);
         }
       } else {
         /* Just send the head operation */
-        if(c->ops->cmd && !c->ops->sent) {
-          put(c, c->ops->cmd, strlen(c->ops->cmd));
-          c->ops->sent = 1;
-        }
+        if(c->ops->cmd && !c->ops->sent)
+          op_send(c->ops);
       }
       /* Awaiting response for the operation at the head of the list */
       c->state = state_cmdresponse;
@@ -323,6 +437,14 @@ void disorder_eclient_polled(disorder_eclient *c, unsigned mode) {
       c->callbacks->report(c->u, 0);
   }
 
+  /* Queue up a byte to send */
+  if(c->state == state_log
+     && c->output.nvec == 0
+     && time(&now) - c->last_prod > LOG_PROD_INTERVAL) {
+    put(c, "x", 1);
+    c->last_prod = now;
+  }
+  
   if(c->state == state_cmdresponse
      || c->state == state_body
      || c->state == state_log) {
@@ -385,15 +507,14 @@ void disorder_eclient_polled(disorder_eclient *c, unsigned mode) {
   if(c->fd != -1) c->callbacks->poll(c->u, c, c->fd, mode);
 }
 
-/* Called to start connecting */
-static int start_connect(void *cc,
-                        const struct sockaddr *sa,
-                        socklen_t len,
-                        const char *ident) {
-  disorder_eclient *c = cc;
+/** @brief Called to start connecting */
+static int start_connect(disorder_eclient *c) {
+  struct sockaddr *sa;
+  socklen_t len;
 
   D(("start_connect"));
-  c->ident = xstrdup(ident);
+  if((len = find_server(config, &sa, &c->ident)) == (socklen_t)-1)
+    return comms_error(c, "cannot look up server"); /* TODO better error */
   if(c->fd != -1) {
     xclose(c->fd);
     c->fd = -1;
@@ -402,6 +523,7 @@ static int start_connect(void *cc,
     return comms_error(c, "socket: %s", strerror(errno));
   c->eof = 0;
   nonblock(c->fd);
+  cloexec(c->fd);
   if(connect(c->fd, sa, len) < 0) {
     switch(errno) {
     case EINTR:
@@ -412,14 +534,14 @@ static int start_connect(void *cc,
       return 0;
     default:
       /* Signal the error to the caller. */
-      return comms_error(c, "connecting to %s: %s", ident, strerror(errno));
+      return comms_error(c, "connecting to %s: %s", c->ident, strerror(errno));
     }
   } else
     c->state = state_connected;
   return 0;
 }
 
-/* Called when maybe connected */
+/** @brief Called when poll triggers while waiting for a connection */
 static void maybe_connected(disorder_eclient *c) {
   /* We either connected, or got an error. */
   int err;
@@ -434,47 +556,100 @@ static void maybe_connected(disorder_eclient *c) {
     comms_error(c, "connecting to %s: %s", c->ident, strerror(err));
     /* sets state_disconnected */
   } else {
+    char *r;
+    
     /* The connection succeeded */
     c->state = state_connected;
+    byte_xasprintf(&r, "connected to %s", c->ident);
+    c->callbacks->report(c->u, r);
+    /* If this is a log client we expect to get a bunch of updates from the
+     * server straight away */
   }
 }
 
 /* Authentication ************************************************************/
 
+/** @brief Called with the greeting from the server */
 static void authbanner_opcallback(disorder_eclient *c,
                                   struct operation *op) {
   size_t nonce_len;
   const unsigned char *nonce;
   const char *res;
+  char **rvec;
+  int nrvec;
+  const char *protocol, *algorithm, *challenge;
   
   D(("authbanner_opcallback"));
-  if(c->rc / 100 != 2) {
-    /* Banner told us to go away.  We cannot proceed. */
+  if(c->rc / 100 != 2
+     || !(rvec = split(c->line + 4, &nrvec, SPLIT_QUOTES, 0, 0))
+     || nrvec < 1) {
+    /* Banner told us to go away, or was malformed.  We cannot proceed. */
     protocol_error(c, op, c->rc, "%s: %s", c->ident, c->line);
     disorder_eclient_close(c);
     return;
   }
-  nonce = unhex(c->line +  4, &nonce_len);
-  res = authhash(nonce, nonce_len, config->password);
+  switch(nrvec) {
+  case 1:
+    protocol = "1";
+    algorithm = "sha1";
+    challenge = *rvec++;
+    break;
+  case 2:
+    protocol = "1";
+    algorithm = *rvec++;
+    challenge = *rvec++;
+    break;
+  case 3:
+    protocol = *rvec++;
+    algorithm = *rvec++;
+    challenge = *rvec++;
+    break;
+  default:
+    protocol_error(c, op, c->rc, "%s: %s", c->ident, c->line);
+    disorder_eclient_close(c);
+    return;
+  }
+  c->protocol = atoi(protocol);
+  if(c->protocol < 1 || c->protocol > 2) {
+    protocol_error(c, op, c->rc, "%s: %s", c->ident, c->line);
+    disorder_eclient_close(c);
+    return;
+  }
+  nonce = unhex(challenge, &nonce_len);
+  res = authhash(nonce, nonce_len, config->password, algorithm);
+  if(!res) {
+    protocol_error(c, op, c->rc, "%s: unknown authentication algorithm '%s'",
+                   c->ident, algorithm);
+    disorder_eclient_close(c);
+    return;
+  }
   stash_command(c, 1/*queuejump*/, authuser_opcallback, 0/*completed*/, 0/*v*/,
+                -1/*nbody*/, 0/*body*/,
                 "user", quoteutf8(config->username), quoteutf8(res),
                 (char *)0);
 }
 
+/** @brief Called with the response to the @c user command */
 static void authuser_opcallback(disorder_eclient *c,
                                 struct operation *op) {
+  char *r;
+
   D(("authuser_opcallback"));
   if(c->rc / 100 != 2) {
     /* Wrong password or something.  We cannot proceed. */
     protocol_error(c, op, c->rc, "%s: %s", c->ident, c->line);
+    c->enabled = 0;
     disorder_eclient_close(c);
     return;
   }
   /* OK, we're authenticated now. */
   c->authenticated = 1;
+  byte_xasprintf(&r, "authenticated with %s", c->ident);
+  c->callbacks->report(c->u, r);
   if(c->log_callbacks && !(c->ops && c->ops->opcallback == log_opcallback))
     /* We are a log client, switch to logging mode */
     stash_command(c, 0/*queuejump*/, log_opcallback, 0/*completed*/, c->log_v,
+                  -1/*nbody*/, 0/*body*/,
                   "log", (char *)0);
 }
 
@@ -573,7 +748,7 @@ static void process_line(disorder_eclient *c, char *line) {
     case 3:
       /* We need to collect the body. */
       c->state = state_body;
-      c->vec.nvec = 0;
+      vector_init(&c->vec);
       break;
     case 4:
       assert(c->log_callbacks != 0);
@@ -637,6 +812,8 @@ static void stash_command_vector(disorder_eclient *c,
                                  operation_callback *opcallback,
                                  void (*completed)(),
                                  void *v,
+                                 int nbody,
+                                 char **body,
                                  int ncmd,
                                  char **cmd) {
   struct operation *op = xmalloc(sizeof *op);
@@ -655,6 +832,13 @@ static void stash_command_vector(disorder_eclient *c,
     op->cmd = d.vec;
   } else
     op->cmd = 0;                        /* usually, awaiting challenge */
+  if(nbody >= 0) {
+    op->body = xcalloc(nbody + 1, sizeof (char *));
+    for(n = 0; n < nbody; ++n)
+      op->body[n] = xstrdup(body[n]);
+    op->body[n] = 0;
+  } else
+    op->body = NULL;
   op->opcallback = opcallback;
   op->completed = completed;
   op->v = v;
@@ -680,6 +864,8 @@ static void vstash_command(disorder_eclient *c,
                            operation_callback *opcallback,
                            void (*completed)(),
                            void *v,
+                           int nbody,
+                           char **body,
                            const char *cmd, va_list ap) {
   char *arg;
   struct vector vec;
@@ -691,9 +877,11 @@ static void vstash_command(disorder_eclient *c,
     while((arg = va_arg(ap, char *)))
       vector_append(&vec, arg);
     stash_command_vector(c, queuejump, opcallback, completed, v, 
-                         vec.nvec, vec.vec);
+                         nbody, body, vec.nvec, vec.vec);
   } else
-    stash_command_vector(c, queuejump, opcallback, completed, v, 0, 0);
+    stash_command_vector(c, queuejump, opcallback, completed, v,
+                         nbody, body,
+                         0, 0);
 }
 
 static void stash_command(disorder_eclient *c,
@@ -701,49 +889,84 @@ static void stash_command(disorder_eclient *c,
                           operation_callback *opcallback,
                           void (*completed)(),
                           void *v,
+                          int nbody,
+                          char **body,
                           const char *cmd,
                           ...) {
   va_list ap;
 
   va_start(ap, cmd);
-  vstash_command(c, queuejump, opcallback, completed, v, cmd, ap);
+  vstash_command(c, queuejump, opcallback, completed, v, nbody, body, cmd, ap);
   va_end(ap);
 }
 
 /* Command support ***********************************************************/
 
-/* for commands with a simple string response */ 
+static const char *errorstring(disorder_eclient *c) {
+  char *s;
+
+  byte_xasprintf(&s, "%s: %s: %d", c->ident, c->line, c->rc);
+  return s;
+}
+
+/* for commands with a quoted string response */ 
 static void string_response_opcallback(disorder_eclient *c,
                                        struct operation *op) {
+  disorder_eclient_string_response *completed
+    = (disorder_eclient_string_response *)op->completed;
+    
   D(("string_response_callback"));
-  if(c->rc / 100 == 2) {
-    if(op->completed)
-      ((disorder_eclient_string_response *)op->completed)(op->v, c->line + 4);
+  if(c->rc / 100 == 2 || c->rc == 555) {
+    if(op->completed) {
+      if(c->rc == 555)
+        completed(op->v, NULL, NULL);
+      else if(c->protocol >= 2) {
+        char **rr = split(c->line + 4, 0, SPLIT_QUOTES, 0, 0);
+        
+        if(rr && *rr)
+          completed(op->v, NULL, *rr);
+        else
+          /* TODO error message a is bit lame but generally indicates a server
+           * bug rather than anything the user can address */
+          completed(op->v, "error parsing response", NULL);
+      } else
+        completed(op->v, NULL, c->line + 4);
+    }
   } else
-    protocol_error(c, op, c->rc, "%s: %s", c->ident, c->line);
+    completed(op->v, errorstring(c), NULL);
 }
 
 /* for commands with a simple integer response */ 
 static void integer_response_opcallback(disorder_eclient *c,
                                         struct operation *op) {
+  disorder_eclient_integer_response *completed
+    = (disorder_eclient_integer_response *)op->completed;
+
   D(("string_response_callback"));
   if(c->rc / 100 == 2) {
-    if(op->completed)
-      ((disorder_eclient_integer_response *)op->completed)
-        (op->v, strtol(c->line + 4, 0, 10));
+    long n;
+    int e;
+
+    e = xstrtol(&n, c->line + 4, 0, 10);
+    if(e)
+      completed(op->v, strerror(e), 0);
+    else
+      completed(op->v, 0, n);
   } else
-    protocol_error(c, op,  c->rc, "%s: %s", c->ident, c->line);
+    completed(op->v, errorstring(c), 0);
 }
 
 /* for commands with no response */
 static void no_response_opcallback(disorder_eclient *c,
                                    struct operation *op) {
+  disorder_eclient_no_response *completed
+    = (disorder_eclient_no_response *)op->completed;
+
   D(("no_response_callback"));
-  if(c->rc / 100 == 2) {
-    if(op->completed)
-      ((disorder_eclient_no_response *)op->completed)(op->v);
-  } else
-    protocol_error(c, op, c->rc, "%s: %s", c->ident, c->line);
+  if(c->rc / 100 == 2)
+    completed(op->v, NULL);
+  else
+    completed(op->v, errorstring(c));
 }
 
 /* error callback for queue_unmarshall */
@@ -751,13 +974,17 @@ static void eclient_queue_error(const char *msg,
                                 void *u) {
   struct operation *op = u;
 
+  /* TODO don't use protocol_error here */
   protocol_error(op->client, op, -1, "error parsing queue entry: %s", msg);
 }
 
 /* for commands that expect a queue dump */
 static void queue_response_opcallback(disorder_eclient *c,
                                       struct operation *op) {
+  disorder_eclient_queue_response *const completed
+    = (disorder_eclient_queue_response *)op->completed;
   int n;
+  int parse_failed = 0;
   struct queue_entry *q, *qh = 0, **qtail = &qh, *qlast = 0;
   
   D(("queue_response_callback"));
@@ -766,22 +993,29 @@ static void queue_response_opcallback(disorder_eclient *c,
     for(n = 0; n < c->vec.nvec; ++n) {
       q = xmalloc(sizeof *q);
       D(("queue_unmarshall %s", c->vec.vec[n]));
-      if(!queue_unmarshall(q, c->vec.vec[n], eclient_queue_error, op)) {
+      if(!queue_unmarshall(q, c->vec.vec[n], NULL, op)) {
         q->prev = qlast;
         *qtail = q;
         qtail = &q->next;
         qlast = q;
-      }
+      } else
+        parse_failed = 1;
     }
-    if(op->completed)
-      ((disorder_eclient_queue_response *)op->completed)(op->v, qh);
+    /* Currently we pass the partial queue to the callback along with the
+     * error.  This might not be very useful in practice... */
+    if(parse_failed)
+      completed(op->v, "cannot parse result", qh);
+    else
+      completed(op->v, 0, qh);
   } else
-    protocol_error(c, op, c->rc, "%s: %s", c->ident, c->line);
+    completed(op->v, errorstring(c), 0);
 } 
 
 /* for 'playing' */
 static void playing_response_opcallback(disorder_eclient *c,
                                         struct operation *op) {
+  disorder_eclient_queue_response *const completed
+    = (disorder_eclient_queue_response *)op->completed;
   struct queue_entry *q;
 
   D(("playing_response_callback"));
@@ -789,51 +1023,54 @@ static void playing_response_opcallback(disorder_eclient *c,
     switch(c->rc % 10) {
     case 2:
       if(queue_unmarshall(q = xmalloc(sizeof *q), c->line + 4,
-                          eclient_queue_error, c))
-        return;
+                          NULL, c))
+        completed(op->v, "cannot parse result", 0);
+      else
+        completed(op->v, 0, q);
       break;
     case 9:
-      q = 0;
+      completed(op->v, 0, 0);
       break;
     default:
-      protocol_error(c, op, c->rc, "%s: %s", c->ident, c->line);
-      return;
+      completed(op->v, errorstring(c), 0);
+      break;
     }
-    if(op->completed)
-      ((disorder_eclient_queue_response *)op->completed)(op->v, q);
   } else
-    protocol_error(c, op, c->rc, "%s: %s", c->ident, c->line);
+    completed(op->v, errorstring(c), 0);
 }
 
 /* for commands that expect a list of some sort */
 static void list_response_opcallback(disorder_eclient *c,
                                      struct operation *op) {
+  disorder_eclient_list_response *const completed =
+    (disorder_eclient_list_response *)op->completed;
+
   D(("list_response_callback"));
-  if(c->rc / 100 == 2) {
-    if(op->completed)
-      ((disorder_eclient_list_response *)op->completed)(op->v,
-                                                        c->vec.nvec,
-                                                        c->vec.vec);
-  } else
-    protocol_error(c, op, c->rc, "%s: %s", c->ident, c->line);
+  if(c->rc / 100 == 2)
+    completed(op->v, NULL, c->vec.nvec, c->vec.vec);
+  else if(c->rc == 555)
+    completed(op->v, NULL, -1, NULL);
+  else
+    completed(op->v, errorstring(c), 0, 0);
 }
 
 /* for volume */
 static void volume_response_opcallback(disorder_eclient *c,
                                        struct operation *op) {
+  disorder_eclient_volume_response *completed
+    = (disorder_eclient_volume_response *)op->completed;
   int l, r;
 
   D(("volume_response_callback"));
   if(c->rc / 100 == 2) {
     if(op->completed) {
       if(sscanf(c->line + 4, "%d %d", &l, &r) != 2 || l < 0 || r < 0)
-        protocol_error(c, op, -1, "%s: invalid volume response: %s",
-                       c->ident, c->line);
+        completed(op->v, "cannot parse volume response", 0, 0);
       else
-        ((disorder_eclient_volume_response *)op->completed)(op->v, l, r);
+        completed(op->v, 0, l, r);
     }
   } else
-    protocol_error(c, op, c->rc, "%s: %s", c->ident, c->line);
+    completed(op->v, errorstring(c), 0, 0);
 }
 
 static int simple(disorder_eclient *c,
@@ -844,7 +1081,24 @@ static int simple(disorder_eclient *c,
   va_list ap;
 
   va_start(ap, cmd);
-  vstash_command(c, 0/*queuejump*/, opcallback, completed, v, cmd, ap);
+  vstash_command(c, 0/*queuejump*/, opcallback, completed, v, -1, 0, cmd, ap);
+  va_end(ap);
+  /* Give the state machine a kick, since we might be in state_idle */
+  disorder_eclient_polled(c, 0);
+  return 0;
+}
+
+static int simple_body(disorder_eclient *c,
+                       operation_callback *opcallback,
+                       void (*completed)(),
+                       void *v,
+                       int nbody,
+                       char **body,
+                       const char *cmd, ...) {
+  va_list ap;
+
+  va_start(ap, cmd);
+  vstash_command(c, 0/*queuejump*/, opcallback, completed, v, nbody, body, cmd, ap);
   va_end(ap);
   /* Give the state machine a kick, since we might be in state_idle */
   disorder_eclient_polled(c, 0);
@@ -929,7 +1183,7 @@ int disorder_eclient_moveafter(disorder_eclient *c,
   for(n = 0; n < nids; ++n)
     vector_append(&vec, (char *)ids[n]);
   stash_command_vector(c, 0/*queuejump*/, no_response_opcallback, completed, v,
-                       vec.nvec, vec.vec);
+                       -1, 0, vec.nvec, vec.vec);
   disorder_eclient_polled(c, 0);
   return 0;
 }
@@ -1073,16 +1327,301 @@ int disorder_eclient_search(disorder_eclient *c,
                 "search", terms, (char *)0);
 }
 
+int disorder_eclient_nop(disorder_eclient *c,
+                         disorder_eclient_no_response *completed,
+                         void *v) {
+  return simple(c, no_response_opcallback, (void (*)())completed, v, 
+                "nop", (char *)0);
+}
+
+/** @brief Get the last @p max added tracks
+ * @param c Client
+ * @param completed Called with list
+ * @param max Number of tracks to get, 0 for all
+ * @param v Passed to @p completed
+ *
+ * The first track in the list is the most recently added.
+ */
+int disorder_eclient_new_tracks(disorder_eclient *c,
+                                disorder_eclient_list_response *completed,
+                                int max,
+                                void *v) {
+  char limit[32];
+
+  sprintf(limit, "%d", max);
+  return simple(c, list_response_opcallback, (void (*)())completed, v,
+                "new", limit, (char *)0);
+}
+
+static void rtp_response_opcallback(disorder_eclient *c,
+                                    struct operation *op) {
+  disorder_eclient_list_response *const completed =
+    (disorder_eclient_list_response *)op->completed;
+  D(("rtp_response_opcallback"));
+  if(c->rc / 100 == 2) {
+    int nvec;
+    char **vec = split(c->line + 4, &nvec, SPLIT_QUOTES, 0, 0);
+
+    if(vec)
+      completed(op->v, NULL, nvec, vec);
+    else
+      completed(op->v, "error parsing response", 0, 0);
+  } else
+    completed(op->v, errorstring(c), 0, 0);
+}
+
+/** @brief Determine the RTP target address
+ * @param c Client
+ * @param completed Called with address details
+ * @param v Passed to @p completed
+ *
+ * The address details will be two elements, the first being the hostname and
+ * the second the service (port).
+ */
+int disorder_eclient_rtp_address(disorder_eclient *c,
+                                 disorder_eclient_list_response *completed,
+                                 void *v) {
+  return simple(c, rtp_response_opcallback, (void (*)())completed, v,
+                "rtp-address", (char *)0);
+}
+
+/** @brief Get the list of users
+ * @param c Client
+ * @param completed Called with list of users
+ * @param v Passed to @p completed
+ *
+ * The user list is not sorted in any particular order.
+ */
+int disorder_eclient_users(disorder_eclient *c,
+                           disorder_eclient_list_response *completed,
+                           void *v) {
+  return simple(c, list_response_opcallback, (void (*)())completed, v,
+                "users", (char *)0);
+}
+
+/** @brief Delete a user
+ * @param c Client
+ * @param completed Called on completion
+ * @param user User to delete
+ * @param v Passed to @p completed
+ */
+int disorder_eclient_deluser(disorder_eclient *c,
+                             disorder_eclient_no_response *completed,
+                             const char *user,
+                             void *v) {
+  return simple(c, no_response_opcallback, (void (*)())completed, v, 
+                "deluser", user, (char *)0);
+}
+
+/** @brief Get a user property
+ * @param c Client
+ * @param completed Called on completion
+ * @param user User to look up
+ * @param property Property to look up
+ * @param v Passed to @p completed
+ */
+int disorder_eclient_userinfo(disorder_eclient *c,
+                              disorder_eclient_string_response *completed,
+                              const char *user,
+                              const char *property,
+                              void *v) {
+  return simple(c, string_response_opcallback,  (void (*)())completed, v, 
+                "userinfo", user, property, (char *)0);
+}
+
+/** @brief Modify a user property
+ * @param c Client
+ * @param completed Called on completion
+ * @param user User to modify
+ * @param property Property to modify
+ * @param value New property value
+ * @param v Passed to @p completed
+ */
+int disorder_eclient_edituser(disorder_eclient *c,
+                              disorder_eclient_no_response *completed,
+                              const char *user,
+                              const char *property,
+                              const char *value,
+                              void *v) {
+  return simple(c, no_response_opcallback, (void (*)())completed, v, 
+                "edituser", user, property, value, (char *)0);
+}
+
+/** @brief Create a new user
+ * @param c Client
+ * @param completed Called on completion
+ * @param user User to create
+ * @param password Initial password
+ * @param rights Initial rights or NULL
+ * @param v Passed to @p completed
+ */
+int disorder_eclient_adduser(disorder_eclient *c,
+                             disorder_eclient_no_response *completed,
+                             const char *user,
+                             const char *password,
+                             const char *rights,
+                             void *v) {
+  return simple(c, no_response_opcallback, (void (*)())completed, v, 
+                "adduser", user, password, rights, (char *)0);
+}
+
+/** @brief Adopt a track
+ * @param c Client
+ * @param completed Called on completion
+ * @param id Track ID
+ * @param v Passed to @p completed
+ */
+int disorder_eclient_adopt(disorder_eclient *c,
+                           disorder_eclient_no_response *completed,
+                           const char *id,
+                           void *v) {
+  return simple(c, no_response_opcallback, (void (*)())completed, v, 
+                "adopt", id, (char *)0);
+}
+
+/** @brief Get the list of playlists
+ * @param c Client
+ * @param completed Called with list of playlists
+ * @param v Passed to @p completed
+ *
+ * The playlist list is not sorted in any particular order.
+ */
+int disorder_eclient_playlists(disorder_eclient *c,
+                               disorder_eclient_list_response *completed,
+                               void *v) {
+  return simple(c, list_response_opcallback, (void (*)())completed, v,
+                "playlists", (char *)0);
+}
+
+/** @brief Delete a playlist
+ * @param c Client
+ * @param completed Called on completion
+ * @param playlist Playlist to delete
+ * @param v Passed to @p completed
+ */
+int disorder_eclient_playlist_delete(disorder_eclient *c,
+                                     disorder_eclient_no_response *completed,
+                                     const char *playlist,
+                                     void *v) {
+  return simple(c, no_response_opcallback,  (void (*)())completed, v,
+                "playlist-delete", playlist, (char *)0);
+}
+
+/** @brief Lock a playlist
+ * @param c Client
+ * @param completed Called on completion
+ * @param playlist Playlist to lock
+ * @param v Passed to @p completed
+ */
+int disorder_eclient_playlist_lock(disorder_eclient *c,
+                                   disorder_eclient_no_response *completed,
+                                   const char *playlist,
+                                   void *v) {
+  return simple(c, no_response_opcallback,  (void (*)())completed, v,
+                "playlist-lock", playlist, (char *)0);
+}
+
+/** @brief Unlock the locked a playlist
+ * @param c Client
+ * @param completed Called on completion
+ * @param v Passed to @p completed
+ */
+int disorder_eclient_playlist_unlock(disorder_eclient *c,
+                                     disorder_eclient_no_response *completed,
+                                     void *v) {
+  return simple(c, no_response_opcallback,  (void (*)())completed, v,
+                "playlist-unlock", (char *)0);
+}
+
+/** @brief Set a playlist's sharing
+ * @param c Client
+ * @param completed Called on completion
+ * @param playlist Playlist to modify
+ * @param sharing @c "public" or @c "private"
+ * @param v Passed to @p completed
+ */
+int disorder_eclient_playlist_set_share(disorder_eclient *c,
+                                        disorder_eclient_no_response *completed,
+                                        const char *playlist,
+                                        const char *sharing,
+                                        void *v) {
+  return simple(c, no_response_opcallback,  (void (*)())completed, v,
+                "playlist-set-share", playlist, sharing, (char *)0);
+}
+
+/** @brief Get a playlist's sharing
+ * @param c Client
+ * @param completed Called with sharing status
+ * @param playlist Playlist to inspect
+ * @param v Passed to @p completed
+ */
+int disorder_eclient_playlist_get_share(disorder_eclient *c,
+                                        disorder_eclient_string_response *completed,
+                                        const char *playlist,
+                                        void *v) {
+  return simple(c, string_response_opcallback,  (void (*)())completed, v,
+                "playlist-get-share", playlist, (char *)0);
+}
+
+/** @brief Set a playlist
+ * @param c Client
+ * @param completed Called on completion
+ * @param playlist Playlist to modify
+ * @param tracks List of tracks
+ * @param ntracks Number of tracks
+ * @param v Passed to @p completed
+ */
+int disorder_eclient_playlist_set(disorder_eclient *c,
+                                  disorder_eclient_no_response *completed,
+                                  const char *playlist,
+                                  char **tracks,
+                                  int ntracks,
+                                  void *v) {
+  return simple_body(c, no_response_opcallback, (void (*)())completed, v,
+                     ntracks, tracks,
+                     "playlist-set", playlist, (char *)0);
+}
+
+/** @brief Get a playlist's contents
+ * @param c Client
+ * @param completed Called with playlist contents
+ * @param playlist Playlist to inspect
+ * @param v Passed to @p completed
+ */
+int disorder_eclient_playlist_get(disorder_eclient *c,
+                                  disorder_eclient_list_response *completed,
+                                  const char *playlist,
+                                  void *v) {
+  return simple(c, list_response_opcallback,  (void (*)())completed, v,
+                "playlist-get", playlist, (char *)0);
+}
+
 /* Log clients ***************************************************************/
 
+/** @brief Monitor the server log
+ * @param c Client
+ * @param callbacks Functions to call when anything happens
+ * @param v Passed to @p callbacks functions
+ *
+ * Once a client is being used for logging it cannot be used for anything else.
+ * There is magic in authuser_opcallback() to re-submit the @c log command
+ * after reconnection.
+ *
+ * NB that the @c state callback may be called from within this function,
+ * i.e. not solely later on from the event loop callback.
+ */
 int disorder_eclient_log(disorder_eclient *c,
                          const disorder_eclient_log_callbacks *callbacks,
                          void *v) {
   if(c->log_callbacks) return -1;
   c->log_callbacks = callbacks;
   c->log_v = v;
+  /* Repoort initial state */
+  if(c->log_callbacks->state)
+    c->log_callbacks->state(c->log_v, c->statebits);
   stash_command(c, 0/*queuejump*/, log_opcallback, 0/*completed*/, v,
-                "log", (char *)0);
+                -1, 0, "log", (char *)0);
+  disorder_eclient_polled(c, 0);
   return 0;
 }
 
@@ -1097,6 +1636,7 @@ static void log_opcallback(disorder_eclient *c,
 /* error callback for log line parsing */
 static void logline_error(const char *msg, void *u) {
   disorder_eclient *c = u;
+  /* TODO don't use protocol_error here */
   protocol_error(c, c->ops, -1, "error parsing log line: %s", msg);
 }
 
@@ -1106,54 +1646,70 @@ static void logline(disorder_eclient *c, const char *line) {
   char **vec;
   uintmax_t when;
 
-  D(("log_opcallback [%s]", line));
+  D(("logline [%s]", line));
   vec = split(line, &nvec, SPLIT_QUOTES, logline_error, c);
   if(nvec < 2) return;                  /* probably an error, already
                                          * reported */
   if(sscanf(vec[0], "%"SCNxMAX, &when) != 1) {
     /* probably the wrong side of a format change */
+    /* TODO don't use protocol_error here */
     protocol_error(c, c->ops, -1, "invalid log timestamp '%s'", vec[0]);
     return;
   }
   /* TODO: do something with the time */
-  n = TABLE_FIND(logentry_handlers, struct logentry_handler, name, vec[1]);
-  if(n < 0) return;                     /* probably a future command */
+  //fprintf(stderr, "log key: %s\n", vec[1]);
+  n = TABLE_FIND(logentry_handlers, name, vec[1]);
+  if(n < 0) {
+    //fprintf(stderr, "...not found\n");
+    return;                     /* probably a future command */
+  }
   vec += 2;
   nvec -= 2;
-  if(nvec < logentry_handlers[n].min || nvec > logentry_handlers[n].max)
+  if(nvec < logentry_handlers[n].min || nvec > logentry_handlers[n].max) {
+    //fprintf(stderr, "...wrong # args\n");
     return;
+  }
   logentry_handlers[n].handler(c, nvec, vec);
 }
 
 static void logentry_completed(disorder_eclient *c,
                                int attribute((unused)) nvec, char **vec) {
-  if(!c->log_callbacks->completed) return;
-  c->log_callbacks->completed(c->log_v, vec[0]);
+  c->statebits &= ~DISORDER_PLAYING;
+  if(c->log_callbacks->completed)
+    c->log_callbacks->completed(c->log_v, vec[0]);
+  if(c->log_callbacks->state)
+    c->log_callbacks->state(c->log_v, c->statebits | DISORDER_CONNECTED);
 }
 
 static void logentry_failed(disorder_eclient *c,
                             int attribute((unused)) nvec, char **vec) {
-  if(!c->log_callbacks->failed)return;
-  c->log_callbacks->failed(c->log_v, vec[0], vec[1]);
+  c->statebits &= ~DISORDER_PLAYING;
+  if(c->log_callbacks->failed)
+    c->log_callbacks->failed(c->log_v, vec[0], vec[1]);
+  if(c->log_callbacks->state)
+    c->log_callbacks->state(c->log_v, c->statebits | DISORDER_CONNECTED);
 }
 
 static void logentry_moved(disorder_eclient *c,
                            int attribute((unused)) nvec, char **vec) {
-  if(!c->log_callbacks->moved) return;
-  c->log_callbacks->moved(c->log_v, vec[0]);
+  if(c->log_callbacks->moved)
+    c->log_callbacks->moved(c->log_v, vec[0]);
 }
 
 static void logentry_playing(disorder_eclient *c,
                              int attribute((unused)) nvec, char **vec) {
-  if(!c->log_callbacks->playing) return;
-  c->log_callbacks->playing(c->log_v, vec[0], vec[1]);
+  c->statebits |= DISORDER_PLAYING;
+  if(c->log_callbacks->playing)
+    c->log_callbacks->playing(c->log_v, vec[0], vec[1]);
+  if(c->log_callbacks->state)
+    c->log_callbacks->state(c->log_v, c->statebits | DISORDER_CONNECTED);
 }
 
 static void logentry_queue(disorder_eclient *c,
                            int attribute((unused)) nvec, char **vec) {
   struct queue_entry *q;
 
-  if(!c->log_callbacks->completed) return;
+  if(!c->log_callbacks->queue) return;
   q = xmalloc(sizeof *q);
   if(queue_unmarshall_vec(q, nvec, vec, eclient_queue_error, c))
     return;                             /* bogus */
@@ -1173,20 +1729,84 @@ static void logentry_recent_added(disorder_eclient *c,
 
 static void logentry_recent_removed(disorder_eclient *c,
                                     int attribute((unused)) nvec, char **vec) {
-  if(!c->log_callbacks->recent_removed) return;
-  c->log_callbacks->recent_removed(c->log_v, vec[0]);
+  if(c->log_callbacks->recent_removed)
+    c->log_callbacks->recent_removed(c->log_v, vec[0]);
 }
 
 static void logentry_removed(disorder_eclient *c,
                              int attribute((unused)) nvec, char **vec) {
-  if(!c->log_callbacks->removed) return;
-  c->log_callbacks->removed(c->log_v, vec[0], vec[1]);
+  if(c->log_callbacks->removed)
+    c->log_callbacks->removed(c->log_v, vec[0], vec[1]);
+}
+
+static void logentry_rescanned(disorder_eclient *c,
+                               int attribute((unused)) nvec,
+                               char attribute((unused)) **vec) {
+  if(c->log_callbacks->rescanned)
+    c->log_callbacks->rescanned(c->log_v);
 }
 
 static void logentry_scratched(disorder_eclient *c,
                                int attribute((unused)) nvec, char **vec) {
-  if(!c->log_callbacks->scratched) return;
-  c->log_callbacks->scratched(c->log_v, vec[0], vec[1]);
+  c->statebits &= ~DISORDER_PLAYING;
+  if(c->log_callbacks->scratched)
+    c->log_callbacks->scratched(c->log_v, vec[0], vec[1]);
+  if(c->log_callbacks->state)
+    c->log_callbacks->state(c->log_v, c->statebits | DISORDER_CONNECTED);
+}
+
+static void logentry_user_add(disorder_eclient *c,
+                              int attribute((unused)) nvec, char **vec) {
+  if(c->log_callbacks->user_add)
+    c->log_callbacks->user_add(c->log_v, vec[0]);
+}
+
+static void logentry_user_confirm(disorder_eclient *c,
+                              int attribute((unused)) nvec, char **vec) {
+  if(c->log_callbacks->user_confirm)
+    c->log_callbacks->user_confirm(c->log_v, vec[0]);
+}
+
+static void logentry_user_delete(disorder_eclient *c,
+                              int attribute((unused)) nvec, char **vec) {
+  if(c->log_callbacks->user_delete)
+    c->log_callbacks->user_delete(c->log_v, vec[0]);
+}
+
+static void logentry_user_edit(disorder_eclient *c,
+                              int attribute((unused)) nvec, char **vec) {
+  if(c->log_callbacks->user_edit)
+    c->log_callbacks->user_edit(c->log_v, vec[0], vec[1]);
+}
+
+static void logentry_rights_changed(disorder_eclient *c,
+                                    int attribute((unused)) nvec, char **vec) {
+  if(c->log_callbacks->rights_changed) {
+    rights_type r;
+    if(!parse_rights(vec[0], &r, 0/*report*/))
+      c->log_callbacks->rights_changed(c->log_v, r);
+  }
+}
+
+static void logentry_playlist_created(disorder_eclient *c,
+                                      int attribute((unused)) nvec,
+                                      char **vec) {
+  if(c->log_callbacks->playlist_created)
+    c->log_callbacks->playlist_created(c->log_v, vec[0], vec[1]);
+}
+
+static void logentry_playlist_deleted(disorder_eclient *c,
+                                      int attribute((unused)) nvec,
+                                      char **vec) {
+  if(c->log_callbacks->playlist_deleted)
+    c->log_callbacks->playlist_deleted(c->log_v, vec[0]);
+}
+
+static void logentry_playlist_modified(disorder_eclient *c,
+                                      int attribute((unused)) nvec,
+                                      char **vec) {
+  if(c->log_callbacks->playlist_modified)
+    c->log_callbacks->playlist_modified(c->log_v, vec[0], vec[1]);
 }
 
 static const struct {
@@ -1197,23 +1817,26 @@ static const struct {
   { DISORDER_PLAYING_ENABLED, "enable_play", "disable_play" },
   { DISORDER_RANDOM_ENABLED, "enable_random", "disable_random" },
   { DISORDER_TRACK_PAUSED, "pause", "resume" },
+  { DISORDER_PLAYING, "playing", "completed" },
+  { DISORDER_PLAYING, 0, "scratched" },
+  { DISORDER_PLAYING, 0, "failed" },
 };
-#define NSTATES (int)(sizeof states / sizeof *states)
+#define NSTATES (int)(sizeof statestrings / sizeof *statestrings)
 
 static void logentry_state(disorder_eclient *c,
                            int attribute((unused)) nvec, char **vec) {
   int n;
 
   for(n = 0; n < NSTATES; ++n)
-    if(!strcmp(vec[0], statestrings[n].enable)) {
+    if(statestrings[n].enable && !strcmp(vec[0], statestrings[n].enable)) {
       c->statebits |= statestrings[n].bit;
       break;
-    } else if(!strcmp(vec[0], statestrings[n].disable)) {
+    } else if(statestrings[n].disable && !strcmp(vec[0], statestrings[n].disable)) {
       c->statebits &= ~statestrings[n].bit;
       break;
     }
-  if(!c->log_callbacks->state) return;
-  c->log_callbacks->state(c->log_v, c->statebits);
+  if(c->log_callbacks->state) 
+    c->log_callbacks->state(c->log_v, c->statebits | DISORDER_CONNECTED);
 }
 
 static void logentry_volume(disorder_eclient *c,
@@ -1229,6 +1852,51 @@ static void logentry_volume(disorder_eclient *c,
   c->log_callbacks->volume(c->log_v, (int)l, (int)r);
 }
 
+/** @brief Convert @p statebits to a string */
+char *disorder_eclient_interpret_state(unsigned long statebits) {
+  struct dynstr d[1];
+  size_t n;
+
+  static const struct {
+    unsigned long bit;
+    const char *name;
+  } bits[] = {
+    { DISORDER_PLAYING_ENABLED, "playing_enabled" },
+    { DISORDER_RANDOM_ENABLED, "random_enabled" },
+    { DISORDER_TRACK_PAUSED, "track_paused" },
+    { DISORDER_PLAYING, "playing" },
+    { DISORDER_CONNECTED, "connected" },
+  };
+#define NBITS (sizeof bits / sizeof *bits)
+
+  dynstr_init(d);
+  if(!statebits)
+    dynstr_append(d, '0');
+  for(n = 0; n < NBITS; ++n)
+    if(statebits & bits[n].bit) {
+      if(d->nvec)
+        dynstr_append(d, '|');
+      dynstr_append_string(d, bits[n].name);
+      statebits ^= bits[n].bit;
+    }
+  if(statebits) {
+    char s[20];
+
+    if(d->nvec)
+      dynstr_append(d, '|');
+    sprintf(s, "%#lx", statebits);
+    dynstr_append_string(d, s);
+  }
+  dynstr_terminate(d);
+  return d->vec;
+}
+
+static void logentry_adopted(disorder_eclient *c,
+                             int attribute((unused)) nvec, char **vec) {
+  if(c->log_callbacks->adopted) 
+    c->log_callbacks->adopted(c->log_v, vec[0], vec[1]);
+}
+
 /*
 Local Variables:
 c-basic-offset:2