chiark / gitweb /
protogen: simplify handling of integers and times.
[disorder] / lib / eclient.c
index e7395f35456f7a4ec3bdade11e06ef6792226df6..d768f57446bf1e4b906f875dc33b8906b8ecb03b 100644 (file)
@@ -2,20 +2,18 @@
  * This file is part of DisOrder.
  * 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
@@ -94,6 +92,7 @@ typedef void operation_callback(disorder_eclient *c, struct operation *op);
 struct operation {
   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 */
@@ -144,6 +143,9 @@ struct disorder_eclient {
 
   /** @brief Protocol version */
   int protocol;
+
+  /** @brief True if enabled */
+  int enabled;
 };
 
 /* Forward declarations ******************************************************/
@@ -164,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);
@@ -184,6 +188,12 @@ 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);
+static void logentry_global_pref(disorder_eclient *c, int nvec, char **vec);
 
 /* Tables ********************************************************************/
 
@@ -200,15 +210,21 @@ struct logentry_handler {
 /** @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(global_pref, 1, 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),
@@ -232,6 +248,7 @@ 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);
@@ -269,6 +286,16 @@ void disorder_eclient_close(disorder_eclient *c) {
     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);
@@ -310,6 +337,24 @@ 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.
@@ -338,9 +383,12 @@ void disorder_eclient_polled(disorder_eclient *c, unsigned mode) {
     /* If there is no password yet then we cannot connect */
     if(!config->password) {
       comms_error(c, "no password is configured");
+      c->enabled = 0;
       return;
     }
-    start_connect(c);
+    /* 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. */
@@ -360,7 +408,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. */
@@ -376,17 +424,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;
@@ -398,7 +442,7 @@ void disorder_eclient_polled(disorder_eclient *c, unsigned mode) {
   /* Queue up a byte to send */
   if(c->state == state_log
      && c->output.nvec == 0
-     && time(&now) - c->last_prod > LOG_PROD_INTERVAL) {
+     && xtime(&now) - c->last_prod > LOG_PROD_INTERVAL) {
     put(c, "x", 1);
     c->last_prod = now;
   }
@@ -471,7 +515,7 @@ static int start_connect(disorder_eclient *c) {
   socklen_t len;
 
   D(("start_connect"));
-  if((len = find_server(&sa, &c->ident)) == (socklen_t)-1)
+  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);
@@ -582,6 +626,7 @@ static void authbanner_opcallback(disorder_eclient *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);
 }
@@ -595,6 +640,7 @@ static void authuser_opcallback(disorder_eclient *c,
   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;
   }
@@ -605,6 +651,7 @@ static void authuser_opcallback(disorder_eclient *c,
   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);
 }
 
@@ -696,7 +743,7 @@ static void process_line(disorder_eclient *c, char *line) {
          && line[1] >= '0' && line[1] <= '9'
          && line[2] >= '0' && line[2] <= '9'
          && line[3] == ' '))
-      fatal(0, "invalid response from server: %s", line);
+      disorder_fatal(0, "invalid response from server: %s", line);
     c->rc = (line[0] * 10 + line[1]) * 10 + line[2] - 111 * '0';
     c->line = line;
     switch(c->rc % 10) {
@@ -767,6 +814,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);
@@ -785,6 +834,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;
@@ -810,6 +866,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;
@@ -818,12 +876,37 @@ static void vstash_command(disorder_eclient *c,
   if(cmd) {
     vector_init(&vec);
     vector_append(&vec, (char *)cmd);
-    while((arg = va_arg(ap, char *)))
-      vector_append(&vec, arg);
+    while((arg = va_arg(ap, char *))) {
+      if(arg == disorder__list) {
+       char **list = va_arg(ap, char **);
+       int nlist = va_arg(ap, int);
+       if(nlist < 0) {
+         for(nlist = 0; list[nlist]; ++nlist)
+           ;
+       }
+        vector_append_many(&vec, list, nlist);
+      } else if(arg == disorder__body) {
+       body = va_arg(ap, char **);
+       nbody = va_arg(ap, int);
+      } else if(arg == disorder__integer) {
+        long n = va_arg(ap, long);
+        char buffer[16];
+        snprintf(buffer, sizeof buffer, "%ld", n);
+        vector_append(&vec, xstrdup(buffer));
+      } else if(arg == disorder__time) {
+        time_t n = va_arg(ap, time_t);
+        char buffer[16];
+        snprintf(buffer, sizeof buffer, "%lld", (long long)n);
+        vector_append(&vec, xstrdup(buffer));
+      } else
+        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,
@@ -831,12 +914,14 @@ 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);
 }
 
@@ -856,8 +941,8 @@ static void string_response_opcallback(disorder_eclient *c,
     = (disorder_eclient_string_response *)op->completed;
     
   D(("string_response_callback"));
-  if(c->rc / 100 == 2 || c->rc == 555) {
-    if(op->completed) {
+  if(completed) {
+    if(c->rc / 100 == 2 || c->rc == 555) {
       if(c->rc == 555)
         completed(op->v, NULL, NULL);
       else if(c->protocol >= 2) {
@@ -871,9 +956,9 @@ static void string_response_opcallback(disorder_eclient *c,
           completed(op->v, "error parsing response", NULL);
       } else
         completed(op->v, NULL, c->line + 4);
-    }
-  } else
-    completed(op->v, errorstring(c), NULL);
+    } else
+      completed(op->v, errorstring(c), NULL);
+  }
 }
 
 /* for commands with a simple integer response */ 
@@ -903,10 +988,12 @@ static void no_response_opcallback(disorder_eclient *c,
     = (disorder_eclient_no_response *)op->completed;
 
   D(("no_response_callback"));
-  if(c->rc / 100 == 2)
-    completed(op->v, NULL);
-  else
-    completed(op->v, errorstring(c));
+  if(completed) {
+    if(c->rc / 100 == 2)
+      completed(op->v, NULL);
+    else
+      completed(op->v, errorstring(c));
+  }
 }
 
 /* error callback for queue_unmarshall */
@@ -988,21 +1075,23 @@ static void list_response_opcallback(disorder_eclient *c,
   D(("list_response_callback"));
   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;
+static void pair_integer_response_opcallback(disorder_eclient *c,
+                                             struct operation *op) {
+  disorder_eclient_pair_integer_response *completed
+    = (disorder_eclient_pair_integer_response *)op->completed;
+  long 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)
+      if(sscanf(c->line + 4, "%ld %ld", &l, &r) != 2 || l < 0 || r < 0)
         completed(op->v, "cannot parse volume response", 0, 0);
       else
         completed(op->v, 0, l, r);
@@ -1019,7 +1108,7 @@ 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);
@@ -1027,251 +1116,11 @@ static int simple(disorder_eclient *c,
 }
 
 /* Commands ******************************************************************/
-int disorder_eclient_version(disorder_eclient *c,
-                             disorder_eclient_string_response *completed,
-                             void *v) {
-  return simple(c, string_response_opcallback, (void (*)())completed, v,
-                "version", (char *)0);
-}
-
-int disorder_eclient_namepart(disorder_eclient *c,
-                              disorder_eclient_string_response *completed,
-                              const char *track,
-                              const char *context,
-                              const char *part,
-                              void *v) {
-  return simple(c, string_response_opcallback, (void (*)())completed, v,
-                "part", track, context, part, (char *)0);
-}
-
-int disorder_eclient_play(disorder_eclient *c,
-                          const char *track,
-                          disorder_eclient_no_response *completed,
-                          void *v) {
-  return simple(c, no_response_opcallback, (void (*)())completed, v,
-                "play", track, (char *)0);
-}
-
-int disorder_eclient_pause(disorder_eclient *c,
-                           disorder_eclient_no_response *completed,
-                           void *v) {
-  return simple(c, no_response_opcallback, (void (*)())completed, v,
-                "pause", (char *)0);
-}
-
-int disorder_eclient_resume(disorder_eclient *c,
-                            disorder_eclient_no_response *completed,
-                            void *v) {
-  return simple(c, no_response_opcallback, (void (*)())completed, v,
-                "resume", (char *)0);
-}
-
-int disorder_eclient_scratch(disorder_eclient *c,
-                             const char *id,
-                             disorder_eclient_no_response *completed,
-                             void *v) {
-  return simple(c, no_response_opcallback, (void (*)())completed, v,
-                "scratch", id, (char *)0);
-}
 
 int disorder_eclient_scratch_playing(disorder_eclient *c,
                                      disorder_eclient_no_response *completed,
                                      void *v) {
-  return disorder_eclient_scratch(c, 0, completed, v);
-}
-
-int disorder_eclient_remove(disorder_eclient *c,
-                            const char *id,
-                            disorder_eclient_no_response *completed,
-                            void *v) {
-  return simple(c, no_response_opcallback, (void (*)())completed, v,
-                "remove", id, (char *)0);
-}
-
-int disorder_eclient_moveafter(disorder_eclient *c,
-                               const char *target,
-                               int nids,
-                               const char **ids,
-                               disorder_eclient_no_response *completed,
-                               void *v) {
-  struct vector vec;
-  int n;
-
-  vector_init(&vec);
-  vector_append(&vec, (char *)"moveafter");
-  vector_append(&vec, (char *)target);
-  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);
-  disorder_eclient_polled(c, 0);
-  return 0;
-}
-
-int disorder_eclient_recent(disorder_eclient *c,
-                            disorder_eclient_queue_response *completed,
-                            void *v) {
-  return simple(c, queue_response_opcallback, (void (*)())completed, v,
-                "recent", (char *)0);
-}
-
-int disorder_eclient_queue(disorder_eclient *c,
-                            disorder_eclient_queue_response *completed,
-                            void *v) {
-  return simple(c, queue_response_opcallback, (void (*)())completed, v,
-                "queue", (char *)0);
-}
-
-int disorder_eclient_files(disorder_eclient *c,
-                           disorder_eclient_list_response *completed,
-                           const char *dir,
-                           const char *re,
-                           void *v) {
-  return simple(c, list_response_opcallback, (void (*)())completed, v,
-                "files", dir, re, (char *)0);
-}
-
-int disorder_eclient_dirs(disorder_eclient *c,
-                          disorder_eclient_list_response *completed,
-                          const char *dir,
-                          const char *re,
-                          void *v) {
-  return simple(c, list_response_opcallback, (void (*)())completed, v,
-                "dirs", dir, re, (char *)0);
-}
-
-int disorder_eclient_playing(disorder_eclient *c,
-                             disorder_eclient_queue_response *completed,
-                             void *v) {
-  return simple(c, playing_response_opcallback, (void (*)())completed, v,
-                "playing", (char *)0);
-}
-
-int disorder_eclient_length(disorder_eclient *c,
-                            disorder_eclient_integer_response *completed,
-                            const char *track,
-                            void *v) {
-  return simple(c, integer_response_opcallback, (void (*)())completed, v,
-                "length", track, (char *)0);
-}
-
-int disorder_eclient_volume(disorder_eclient *c,
-                            disorder_eclient_volume_response *completed,
-                            int l, int r,
-                            void *v) {
-  char sl[64], sr[64];
-
-  if(l < 0 && r < 0) {
-    return simple(c, volume_response_opcallback, (void (*)())completed, v,
-                  "volume", (char *)0);
-  } else if(l >= 0 && r >= 0) {
-    assert(l <= 100);
-    assert(r <= 100);
-    byte_snprintf(sl, sizeof sl, "%d", l);
-    byte_snprintf(sr, sizeof sr, "%d", r);
-    return simple(c, volume_response_opcallback, (void (*)())completed, v,
-                  "volume", sl, sr, (char *)0);
-  } else {
-    assert(!"invalid arguments to disorder_eclient_volume");
-    return -1;                          /* gcc is being dim */
-  }
-}
-
-int disorder_eclient_enable(disorder_eclient *c,
-                            disorder_eclient_no_response *completed,
-                            void *v) {
-  return simple(c, no_response_opcallback, (void (*)())completed, v,
-                "enable", (char *)0);
-}
-
-int disorder_eclient_disable(disorder_eclient *c,
-                             disorder_eclient_no_response *completed,
-                             void *v){
-  return simple(c, no_response_opcallback, (void (*)())completed, v,
-                "disable", (char *)0);
-}
-
-int disorder_eclient_random_enable(disorder_eclient *c,
-                                   disorder_eclient_no_response *completed,
-                                   void *v){
-  return simple(c, no_response_opcallback, (void (*)())completed, v,
-                "random-enable", (char *)0);
-}
-
-int disorder_eclient_random_disable(disorder_eclient *c,
-                                    disorder_eclient_no_response *completed,
-                                    void *v){
-  return simple(c, no_response_opcallback, (void (*)())completed, v,
-                "random-disable", (char *)0);
-}
-
-int disorder_eclient_get(disorder_eclient *c,
-                         disorder_eclient_string_response *completed,
-                         const char *track, const char *pref,
-                         void *v) {
-  return simple(c, string_response_opcallback, (void (*)())completed, v, 
-                "get", track, pref, (char *)0);
-}
-
-int disorder_eclient_set(disorder_eclient *c,
-                         disorder_eclient_no_response *completed,
-                         const char *track, const char *pref, 
-                         const char *value,
-                         void *v) {
-  return simple(c, no_response_opcallback, (void (*)())completed, v, 
-                "set", track, pref, value, (char *)0);
-}
-
-int disorder_eclient_unset(disorder_eclient *c,
-                           disorder_eclient_no_response *completed,
-                           const char *track, const char *pref, 
-                           void *v) {
-  return simple(c, no_response_opcallback, (void (*)())completed, v, 
-                "unset", track, pref, (char *)0);
-}
-
-int disorder_eclient_resolve(disorder_eclient *c,
-                             disorder_eclient_string_response *completed,
-                             const char *track,
-                             void *v) {
-  return simple(c, string_response_opcallback,  (void (*)())completed, v, 
-                "resolve", track, (char *)0);
-}
-
-int disorder_eclient_search(disorder_eclient *c,
-                            disorder_eclient_list_response *completed,
-                            const char *terms,
-                            void *v) {
-  if(!split(terms, 0, SPLIT_QUOTES, 0, 0)) return -1;
-  return simple(c, list_response_opcallback, (void (*)())completed, v,
-                "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);
+  return disorder_eclient_scratch(c, completed, 0, v);
 }
 
 static void rtp_response_opcallback(disorder_eclient *c,
@@ -1306,86 +1155,6 @@ int disorder_eclient_rtp_address(disorder_eclient *c,
                 "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);
-}
-
 /* Log clients ***************************************************************/
 
 /** @brief Monitor the server log
@@ -1410,7 +1179,7 @@ int disorder_eclient_log(disorder_eclient *c,
   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;
 }
@@ -1447,12 +1216,18 @@ static void logline(disorder_eclient *c, const char *line) {
     return;
   }
   /* TODO: do something with the time */
+  //fprintf(stderr, "log key: %s\n", vec[1]);
   n = TABLE_FIND(logentry_handlers, name, vec[1]);
-  if(n < 0) return;                     /* probably a future command */
+  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);
 }
 
@@ -1563,6 +1338,36 @@ static void logentry_user_edit(disorder_eclient *c,
     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 {
   unsigned long bit;
   const char *enable;
@@ -1645,6 +1450,20 @@ char *disorder_eclient_interpret_state(unsigned long statebits) {
   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]);
+}
+
+static void logentry_global_pref(disorder_eclient *c,
+                                 int nvec, char **vec) {
+  if(c->log_callbacks->global_pref) 
+    c->log_callbacks->global_pref(c->log_v, vec[0], nvec > 1 ? vec[1] : 0);
+}
+
+#include "eclient-stubs.c"
+
 /*
 Local Variables:
 c-basic-offset:2