chiark / gitweb /
Support schedule-* commands from command-line client. Exiguously tested.
authorRichard Kettlewell <rjk@greenend.org.uk>
Sat, 24 May 2008 11:54:12 +0000 (12:54 +0100)
committerRichard Kettlewell <rjk@greenend.org.uk>
Sat, 24 May 2008 11:54:12 +0000 (12:54 +0100)
clients/disorder.c
doc/disorder.1.in
lib/client.c
lib/client.h
server/schedule.c
tests/schedule.py

index 2088bc4a9fed9224dfd605008b79439d7a4ea160..f20dbbc663aab3fa67d5ba7cd5a96b2e60d3b528 100644 (file)
@@ -468,6 +468,110 @@ static void cf_setup_guest(char **argv) {
     exit(EXIT_FAILURE);
 }
 
+struct scheduled_event {
+  time_t when;
+  struct kvp *actiondata;
+  char *id;
+};
+
+static int compare_event(const void *av, const void *bv) {
+  struct scheduled_event *a = (void *)av, *b = (void *)bv;
+
+  /* Primary sort key is the trigger time */
+  if(a->when < b->when)
+    return -1;
+  else if(a->when > b->when)
+    return 1;
+  /* For events that go off at the same time just sort by ID */
+  return strcmp(a->id, b->id);
+}
+
+static void cf_schedule_list(char attribute((unused)) **argv) {
+  char **ids;
+  int nids, n;
+  struct scheduled_event *events;
+  char tb[128];
+  const char *action, *key, *value, *priority;
+  int prichar;
+
+  /* Get all known events */
+  if(disorder_schedule_list(getclient(), &ids, &nids))
+    exit(EXIT_FAILURE);
+  events = xcalloc(nids, sizeof *events);
+  for(n = 0; n < nids; ++n) {
+    events[n].id = ids[n];
+    if(disorder_schedule_get(getclient(), ids[n], &events[n].actiondata))
+      exit(EXIT_FAILURE);
+    events[n].when = atoll(kvp_get(events[n].actiondata, "when"));
+  }
+  /* Sort by trigger time */
+  qsort(events, nids, sizeof *events, compare_event);
+  /* Display them */
+  for(n = 0; n < nids; ++n) {
+    strftime(tb, sizeof tb, "%Y-%m-%d %H:%M:%S %Z", localtime(&events[n].when));
+    action = kvp_get(events[n].actiondata, "action");
+    priority = kvp_get(events[n].actiondata, "priority");
+    if(!strcmp(priority, "junk"))
+      prichar = 'J';
+    else if(!strcmp(priority, "normal"))
+      prichar = 'N';
+    else
+      prichar = '?';
+    xprintf("%11s %-25s %c %-8s %s",
+           events[n].id, tb, prichar, kvp_get(events[n].actiondata, "who"),
+           action);
+    if(!strcmp(action, "play"))
+      xprintf(" %s",
+             nullcheck(utf82mb(kvp_get(events[n].actiondata, "track"))));
+    else if(!strcmp(action, "set-global")) {
+      key = kvp_get(events[n].actiondata, "key");
+      value = kvp_get(events[n].actiondata, "value");
+      if(value)
+       xprintf(" %s=%s",
+               nullcheck(utf82mb(key)),
+               nullcheck(utf82mb(value)));
+      else
+       xprintf(" %s unset",
+               nullcheck(utf82mb(key)));
+    }
+    xprintf("\n");
+  }
+}
+
+static void cf_schedule_del(char **argv) {
+  if(disorder_schedule_del(getclient(), argv[0]))
+    exit(EXIT_FAILURE);
+}
+
+static void cf_schedule_play(char **argv) {
+  if(disorder_schedule_add(getclient(),
+                          atoll(argv[0]),
+                          argv[1],
+                          "play",
+                          argv[2]))
+    exit(EXIT_FAILURE);
+}
+
+static void cf_schedule_set_global(char **argv) {
+  if(disorder_schedule_add(getclient(),
+                          atoll(argv[0]),
+                          argv[1],
+                          "set-global",
+                          argv[2],
+                          argv[3]))
+    exit(EXIT_FAILURE);
+}
+
+static void cf_schedule_unset_global(char **argv) {
+  if(disorder_schedule_add(getclient(),
+                          atoll(argv[0]),
+                          argv[1],
+                          "set-global",
+                          argv[2],
+                          (char *)0))
+    exit(EXIT_FAILURE);
+}
+
 static const struct command {
   const char *name;
   int min, max;
@@ -480,7 +584,7 @@ static const struct command {
   { "allfiles",       1, 2, cf_allfiles, isarg_regexp, "DIR [~REGEXP]",
                       "List all files and directories in DIR" },
   { "authorize",      1, 2, cf_authorize, isarg_rights, "USERNAME [RIGHTS]",
-                      "Authorize user USERNAME to connect to the server" },
+                      "Authorize user USERNAME to connect" },
   { "deluser",        1, 1, cf_deluser, 0, "USERNAME",
                       "Delete user USERNAME" },
   { "dirs",           1, 2, cf_dirs, isarg_regexp, "DIR [~REGEXP]",
@@ -542,6 +646,16 @@ static const struct command {
                       "Resume after a pause" },
   { "rtp-address",    0, 0, cf_rtp_address, 0, "",
                       "Report server's broadcast address" },
+  { "schedule-del",   1, 1, cf_schedule_del, 0, "EVENT",
+                      "Delete a scheduled event" },
+  { "schedule-list",  0, 0, cf_schedule_list, 0, "",
+                      "List scheduled events" },
+  { "schedule-play",  3, 3, cf_schedule_play, 0, "WHEN PRI TRACK",
+                      "Play TRACK later" },
+  { "schedule-set-global", 4, 4, cf_schedule_set_global, 0, "WHEN PRI NAME VAL",
+                      "Set a global preference later" },
+  { "schedule-unset-global", 3, 3, cf_schedule_unset_global, 0, "WHEN PRI NAME",
+                      "Unset a global preference later" },
   { "scratch",        0, 0, cf_scratch, 0, "",
                       "Scratch the currently playing track" },
   { "scratch-id",     1, 1, cf_scratch, 0, "ID",
index bd22e56d9baa7de8c721abe92d065a8c57399276..3156eb4aa9a1f9089d8dfae437c9a13058be2d08 100644 (file)
@@ -182,6 +182,30 @@ Resume the current track after a pause.
 .B rtp\-address
 Report the RTP brodcast address used by the server (if any).
 .TP
+.B schedule-del \fIEVENT\fR
+Delete a scheduled event.
+.TP
+.B schedule-list
+List scheduled events.
+Each line contains the ID, a timestamp, 'N' or 'J' for normal or junk priority,
+the user, the action and action-specific data.
+.TP
+.B schedule-play \fIWHEN PRIORITY TRACK\fI
+Play \fITRACK\fR at time \fIWHEN\fR (which must currently be a raw \fBtime_t\fR
+value).
+.IP
+\fIPRIORITY\fR should be \fBjunk\fR or \fBnormal\fR.
+This determines how the event is handled if it becomes due when the server is
+down.
+Junk events are just discarded in this case, while normal events will be
+executed when the server comes back up, even if this is much later.
+.TP
+.B schedule-set-global \fIWHEN PRIORITY NAME VALUE\fI
+Set global preference \fINAME\fR to \fIVALUE\fR at time \fIWHEN\fR.
+.TP
+.B schedule-unset-global \fIWHEN PRIORITY NAME\fI
+Unset global preference \fINAME\fR at time \fIWHEN\fR.
+.TP
 .B scratch
 Scratch the currently playing track.
 .TP
index 4dfd10cc27b9b11e321a9d44405595a12a8c1e5d..e7fba81f5d6ab3d1c89f2f401ab7c52e6f8da75c 100644 (file)
@@ -58,6 +58,7 @@
 #include "client-common.h"
 #include "rights.h"
 #include "trackdb.h"
+#include "kvp.h"
 
 /** @brief Client handle contents */
 struct disorder_client {
@@ -1215,6 +1216,90 @@ int disorder_reminder(disorder_client *c, const char *user) {
   return disorder_simple(c, 0, "reminder", user, (char *)0);
 }
 
+/** @brief List scheduled events
+ * @param c Client
+ * @param idsp Where to put list of event IDs
+ * @param nidsp Where to put count of event IDs, or NULL
+ * @return 0 on success, non-0 on error
+ */
+int disorder_schedule_list(disorder_client *c, char ***idsp, int *nidsp) {
+  return disorder_simple_list(c, idsp, nidsp, "schedule-list", (char *)0);
+}
+
+/** @brief Delete a scheduled event
+ * @param c Client
+ * @param id Event ID to delete
+ * @return 0 on success, non-0 on error
+ */
+int disorder_schedule_del(disorder_client *c, const char *id) {
+  return disorder_simple(c, 0, "schedule-del", id, (char *)0);
+}
+
+/** @brief Get details of a scheduled event
+ * @param c Client
+ * @param id Event ID
+ * @param actiondatap Where to put details
+ * @return 0 on success, non-0 on error
+ */
+int disorder_schedule_get(disorder_client *c, const char *id,
+                         struct kvp **actiondatap) {
+  char **lines, **bits;
+  int rc, nbits;
+
+  *actiondatap = 0;
+  if((rc = disorder_simple_list(c, &lines, NULL,
+                               "schedule-get", id, (char *)0)))
+    return rc;
+  while(*lines) {
+    if(!(bits = split(*lines++, &nbits, SPLIT_QUOTES, 0, 0))) {
+      error(0, "invalid schedule-get reply: cannot split line");
+      return -1;
+    }
+    if(nbits != 2) {
+      error(0, "invalid schedule-get reply: wrong number of fields");
+      return -1;
+    }
+    kvp_set(actiondatap, bits[0], bits[1]);
+  }
+  return 0;
+}
+
+/** @brief Add a scheduled event
+ * @param c Client
+ * @param when When to trigger the event
+ * @param priority Event priority ("normal" or "junk")
+ * @param action What action to perform
+ * @param ... Action-specific arguments
+ * @return 0 on success, non-0 on error
+ *
+ * For action @c "play" the next argument is the track.
+ *
+ * For action @c "set-global" next argument is the global preference name
+ * and the final argument the value to set it to, or (char *)0 to unset it.
+ */
+int disorder_schedule_add(disorder_client *c,
+                         time_t when,
+                         const char *priority,
+                         const char *action,
+                         ...) {
+  va_list ap;
+  char when_str[64];
+  int rc;
+
+  snprintf(when_str, sizeof when_str, "%lld", (long long)when);
+  va_start(ap, action);
+  if(!strcmp(action, "play"))
+    rc = disorder_simple(c, 0, when_str, priority,
+                        action, va_arg(ap, char *));
+  else if(!strcmp(action, "set-global"))
+    rc = disorder_simple(c, 0, when_str, priority,
+                        action, va_arg(ap, char *), va_arg(ap, char *));
+  else
+    fatal(0, "unknown action '%s'", action);
+  va_end(ap);
+  return rc;
+}
+
 /*
 Local Variables:
 c-basic-offset:2
index 674272cff6e98f4f3973f25c1480046456e1b1eb..abadef6691ee0d9d807efd3f7ef950510f3a67c5 100644 (file)
@@ -116,6 +116,15 @@ int disorder_make_cookie(disorder_client *c, char **cookiep);
 const char *disorder_last(disorder_client *c);
 int disorder_revoke(disorder_client *c);
 int disorder_reminder(disorder_client *c, const char *user);
+int disorder_schedule_list(disorder_client *c, char ***idsp, int *nidsp);
+int disorder_schedule_del(disorder_client *c, const char *id);
+int disorder_schedule_get(disorder_client *c, const char *id,
+                         struct kvp **actiondatap);
+int disorder_schedule_add(disorder_client *c,
+                         time_t when,
+                         const char *priority,
+                         const char *action,
+                         ...);
 
 #endif /* CLIENT_H */
 
index 47cb6aa93dc197434bd868190cbbc3688d5d612c..7776c3c0486036fe1a912662a6a655bd632c3c54 100644 (file)
@@ -363,6 +363,7 @@ char **schedule_list(int *neventsp) {
   int e;
   struct vector v[1];
 
+  vector_init(v);
   WITH_TRANSACTION(trackdb_listkeys(trackdb_scheduledb, v, tid));
   if(neventsp)
     *neventsp = v->nvec;
index 2e5eaac2af17e9390c53821da22d67f48c1800f5..40b1b07bc00c1a7744f16ebef2af0252431898c9 100755 (executable)
@@ -18,7 +18,7 @@
 # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
 # USA
 #
-import dtest,disorder,time
+import dtest,disorder,time,string
 
 def test():
     """Exercise schedule support"""
@@ -35,6 +35,11 @@ def test():
     track = "%s/Joe Bloggs/First Album/05:Fifth track.ogg" % dtest.tracks
     print " scheduling a track for the future"
     c.schedule_add(now + 4, "normal", "play", track)
+    print " disorder schedule-list output:"
+    print string.join(dtest.command(["disorder",
+                                     "--config", disorder._configfile,
+                                     "--no-per-user-config",
+                                     "schedule-list"]), ""),
     print " waiting for it to play"
     waited = 0
     p = c.playing()
@@ -51,6 +56,11 @@ def test():
     print " scheduling an enable-random for the future"
     now = int(time.time())
     c.schedule_add(now + 4, "normal", "set-global", "random-play", "yes")
+    print " disorder schedule-list output:"
+    print string.join(dtest.command(["disorder",
+                                     "--config", disorder._configfile,
+                                     "--no-per-user-config",
+                                     "schedule-list"]), ""),
     print " waiting for it to take effect"
     waited = 0
     p = c.playing()