Add new 'playafter' command to protocol, eclient and python.
authorRichard Kettlewell <rjk@greenend.org.uk>
Sat, 14 Nov 2009 11:24:03 +0000 (11:24 +0000)
committerRichard Kettlewell <rjk@greenend.org.uk>
Sat, 14 Nov 2009 11:24:03 +0000 (11:24 +0000)
This allows multiple tracks to be inserted at arbitrary points
in the queue.

doc/disorder_protocol.5.in
lib/eclient.c
lib/eclient.h
lib/queue.c
python/disorder.py.in
server/disorder-server.h
server/play.c
server/queue-ops.c
server/schedule.c
server/server.c
tests/play.py

index a0baadb..6bc9c47 100644 (file)
@@ -207,6 +207,16 @@ Add a track to the queue.
 The response contains the queue ID of the track.
 Requires the \fBplay\fR right.
 .TP
 The response contains the queue ID of the track.
 Requires the \fBplay\fR right.
 .TP
+.B playafter \fITARGET\fR \fITRACK\fR ...
+Add all the tracks in the \fITRACK\fR list to the queue after \fITARGET\fR
+(which should be a track ID).
+If \fITARGET\fR is the empty string then the listed tracks are put
+at the head of the queue.
+.IP
+Currently the success result does \fInot\fR include the new track IDs.
+.IP
+Requires the \fBplay\fR right.
+.TP
 .B playing
 Report what track is playing.
 .IP
 .B playing
 Report what track is playing.
 .IP
index fad9e1b..ba743ec 100644 (file)
@@ -1132,6 +1132,28 @@ int disorder_eclient_play(disorder_eclient *c,
                 "play", track, (char *)0);
 }
 
                 "play", track, (char *)0);
 }
 
+int disorder_eclient_playafter(disorder_eclient *c,
+                               const char *target,
+                               int ntracks,
+                               const char **tracks,
+                               disorder_eclient_no_response *completed,
+                               void *v) {
+  struct vector vec;
+  int n;
+
+  if(!target)
+    target = "";
+  vector_init(&vec);
+  vector_append(&vec, (char *)"playafter");
+  vector_append(&vec, (char *)target);
+  for(n = 0; n < ntracks; ++n)
+    vector_append(&vec, (char *)tracks[n]);
+  stash_command_vector(c, 0/*queuejump*/, no_response_opcallback, completed, v,
+                       -1, 0, vec.nvec, vec.vec);
+  disorder_eclient_polled(c, 0);
+  return 0;
+}
+
 int disorder_eclient_pause(disorder_eclient *c,
                            disorder_eclient_no_response *completed,
                            void *v) {
 int disorder_eclient_pause(disorder_eclient *c,
                            disorder_eclient_no_response *completed,
                            void *v) {
index fae610c..b100106 100644 (file)
@@ -325,6 +325,14 @@ int disorder_eclient_play(disorder_eclient *c,
                           void *v);
 /* add a track to the queue */
 
                           void *v);
 /* add a track to the queue */
 
+int disorder_eclient_playafter(disorder_eclient *c,
+                               const char *target,
+                               int ntracks,
+                               const char **tracks,
+                               disorder_eclient_no_response *completed,
+                               void *v);
+/* insert multiple tracks to an arbitrary point in the queue */
+
 int disorder_eclient_pause(disorder_eclient *c,
                            disorder_eclient_no_response *completed,
                            void *v);
 int disorder_eclient_pause(disorder_eclient *c,
                            disorder_eclient_no_response *completed,
                            void *v);
index 1486ebf..db28687 100644 (file)
@@ -1,6 +1,6 @@
 /*
  * This file is part of DisOrder.
 /*
  * This file is part of DisOrder.
- * Copyright (C) 2004-2008 Richard Kettlewell
+ * Copyright (C) 2004-2009 Richard Kettlewell
  *
  * 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
  *
  * 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
@@ -55,7 +55,10 @@ const char *const track_origins[] = {
 
 #define VALUE(q, offset, type) *(type *)((char *)q + offset)
 
 
 #define VALUE(q, offset, type) *(type *)((char *)q + offset)
 
-/* add new entry @n@ to a doubly linked list just after @b@ */
+/** @brief Insert queue entry @p n just after @p b
+ * @param b Insert after this entry
+ * @param n New entry to insert
+ */
 void queue_insert_entry(struct queue_entry *b, struct queue_entry *n) {
   n->prev = b;
   n->next = b->next;
 void queue_insert_entry(struct queue_entry *b, struct queue_entry *n) {
   n->prev = b;
   n->next = b->next;
index d06c7ee..fe054a9 100644 (file)
@@ -439,6 +439,16 @@ class client:
     res, details = self._simple("play", track)
     return unicode(details)             # because it's unicode in queue() output
 
     res, details = self._simple("play", track)
     return unicode(details)             # because it's unicode in queue() output
 
+  def playafter(self, target, tracks):
+    """Insert tracks into a specific point in the queue.
+
+    Arguments:
+    target -- target ID or None to insert at start of queue
+    tracks -- a list of tracks to play"""
+    if target is None:
+      target = ''
+    self._simple("playafter", target, *tracks)
+
   def remove(self, track):
     """Remove a track from the queue.
 
   def remove(self, track):
     """Remove a track from the queue.
 
index 60e97df..f980b6f 100644 (file)
@@ -134,10 +134,12 @@ void recent_write(void);
 /* write the recently played list out.  Calls @fatal@ on error. */
 
 struct queue_entry *queue_add(const char *track, const char *submitter,
 /* write the recently played list out.  Calls @fatal@ on error. */
 
 struct queue_entry *queue_add(const char *track, const char *submitter,
-                             int where, enum track_origin origin);
+                             int where, const char *target,
+                              enum track_origin origin);
 #define WHERE_START 0                  /* Add to head of queue */
 #define WHERE_END 1                    /* Add to end of queue */
 #define WHERE_BEFORE_RANDOM 2          /* End, or before random track */
 #define WHERE_START 0                  /* Add to head of queue */
 #define WHERE_END 1                    /* Add to end of queue */
 #define WHERE_BEFORE_RANDOM 2          /* End, or before random track */
+#define WHERE_AFTER 3                   /* After the target */
 /* add an entry to the queue.  Return a pointer to the new entry. */
 
 void queue_remove(struct queue_entry *q, const char *who);
 /* add an entry to the queue.  Return a pointer to the new entry. */
 
 void queue_remove(struct queue_entry *q, const char *who);
index 8fd6d4c..a9a505f 100644 (file)
@@ -515,7 +515,7 @@ static void chosen_random_track(ev_source *ev,
   if(!track)
     return;
   /* Add the track to the queue */
   if(!track)
     return;
   /* Add the track to the queue */
-  q = queue_add(track, 0, WHERE_END, origin_random);
+  q = queue_add(track, 0, WHERE_END, NULL, origin_random);
   D(("picked %p (%s) at random", (void *)q, q->track));
   queue_write();
   /* Maybe a track can now be played */
   D(("picked %p (%s) at random", (void *)q, q->track));
   queue_write();
   /* Maybe a track can now be played */
@@ -697,7 +697,8 @@ void scratch(const char *who, const char *id) {
      * bother if playing is disabled) */
     if(playing_is_enabled() && config->scratch.n) {
       int r = rand() * (double)config->scratch.n / (RAND_MAX + 1.0);
      * bother if playing is disabled) */
     if(playing_is_enabled() && config->scratch.n) {
       int r = rand() * (double)config->scratch.n / (RAND_MAX + 1.0);
-      q = queue_add(config->scratch.s[r], who, WHERE_START, origin_scratch);
+      q = queue_add(config->scratch.s[r], who, WHERE_START, NULL, 
+                    origin_scratch);
     }
     notify_scratch(playing->track, playing->submitter, who,
                   xtime(0) - playing->played);
     }
     notify_scratch(playing->track, playing->submitter, who,
                   xtime(0) - playing->played);
index ed97c0c..f2b3457 100644 (file)
@@ -48,9 +48,23 @@ static void queue_id(struct queue_entry *q) {
   q->id = id;
 }
 
   q->id = id;
 }
 
+/** @brief Add a track to the queue
+ * @param track Track to add
+ * @param submitter Who added it, or NULL
+ * @param where Where to add it
+ * @param target ID to add after for @ref WHERE_AFTER
+ * @param origin Track origin
+ * @return New queue entry or NULL
+ *
+ * The queue is NOT saved to disk.
+ *
+ * NULL can only be returned if @ref WHERE_AFTER is used with an invalid
+ * queue ID.
+ */
 struct queue_entry *queue_add(const char *track, const char *submitter,
 struct queue_entry *queue_add(const char *track, const char *submitter,
-                             int where, enum track_origin origin) {
-  struct queue_entry *q, *beforeme;
+                             int where, const char *target,
+                              enum track_origin origin) {
+  struct queue_entry *q, *beforeme, *afterme;
 
   q = xmalloc(sizeof *q);
   q->track = xstrdup(track);
 
   q = xmalloc(sizeof *q);
   q->track = xstrdup(track);
@@ -76,6 +90,20 @@ struct queue_entry *queue_add(const char *track, const char *submitter,
       beforeme = beforeme->prev;
     queue_insert_entry(beforeme->prev, q);
     break;
       beforeme = beforeme->prev;
     queue_insert_entry(beforeme->prev, q);
     break;
+  case WHERE_AFTER:
+    if(!*target)
+      /* Insert at start of queue */
+      afterme = &qhead;
+    else {
+      /* Insert after a specific track */
+      afterme = qhead.next;
+      while(afterme != &qhead && strcmp(afterme->id, target))
+        afterme = afterme->next;
+      if(afterme == &qhead)
+        return NULL;
+    }
+    queue_insert_entry(afterme, q);
+    break;
   }
   /* submitter will be a null pointer for a scratch */
   if(submitter)
   }
   /* submitter will be a null pointer for a scratch */
   if(submitter)
index 2669fed..f93c0f0 100644 (file)
@@ -374,7 +374,7 @@ static void schedule_play(ev_source *ev,
     return;
   }
   info("scheduled event %s: %s play %s", id,  who, track);
     return;
   }
   info("scheduled event %s: %s play %s", id,  who, track);
-  q = queue_add(track, who, WHERE_START, origin_scheduled);
+  q = queue_add(track, who, WHERE_START, NULL, origin_scheduled);
   queue_write();
   if(q == qhead.next && playing)
     prepare(ev, q);
   queue_write();
   if(q == qhead.next && playing)
     prepare(ev, q);
index 781c71a..6866764 100644 (file)
@@ -242,14 +242,14 @@ static int c_play(struct conn *c, char **vec,
     sink_writes(ev_writer_sink(c->w), "550 cannot resolve track\n");
     return 1;
   }
     sink_writes(ev_writer_sink(c->w), "550 cannot resolve track\n");
     return 1;
   }
-  q = queue_add(track, c->who, WHERE_BEFORE_RANDOM, origin_picked);
+  q = queue_add(track, c->who, WHERE_BEFORE_RANDOM, NULL, origin_picked);
   queue_write();
   queue_write();
-  /* If we added the first track, and something is playing, then prepare the
-   * new track.  If nothing is playing then we don't bother as it wouldn't gain
-   * anything. */
-  if(q == qhead.next && playing)
-    prepare(c->ev, q);
   sink_printf(ev_writer_sink(c->w), "252 %s\n", q->id);
   sink_printf(ev_writer_sink(c->w), "252 %s\n", q->id);
+  /* We make sure the track at the head of the queue is prepared, just in case
+   * we added it.  We could be more subtle but prepare() will ensure we don't
+   * prepare the same track twice so there's no point. */
+  if(qhead.next != &qhead)
+    prepare(c->ev, qhead.next);
   /* If the queue was empty but we are for some reason paused then
    * unpause. */
   if(!playing) resume_playing(0);
   /* If the queue was empty but we are for some reason paused then
    * unpause. */
   if(!playing) resume_playing(0);
@@ -257,6 +257,46 @@ static int c_play(struct conn *c, char **vec,
   return 1;                    /* completed */
 }
 
   return 1;                    /* completed */
 }
 
+static int c_playafter(struct conn *c, char **vec,
+                 int attribute((unused)) nvec) {
+  const char *track;
+  struct queue_entry *q;
+  const char *afterme = vec[0];
+
+  for(int n = 1; n < nvec; ++n) {
+    if(!trackdb_exists(vec[n])) {
+      sink_writes(ev_writer_sink(c->w), "550 track is not in database\n");
+      return 1;
+    }
+    if(!(track = trackdb_resolve(vec[n]))) {
+      sink_writes(ev_writer_sink(c->w), "550 cannot resolve track\n");
+      return 1;
+    }
+    q = queue_add(track, c->who, WHERE_AFTER, afterme, origin_picked);
+    if(!q) {
+      sink_printf(ev_writer_sink(c->w), "550 No such ID\n");
+      return 1;
+    }
+    info("added %s as %s after %s", track, q->id, afterme);
+    afterme = q->id;
+  }
+  queue_write();
+  sink_printf(ev_writer_sink(c->w), "252 OK\n");
+  /* We make sure the track at the head of the queue is prepared, just in case
+   * we added it.  We could be more subtle but prepare() will ensure we don't
+   * prepare the same track twice so there's no point. */
+  if(qhead.next != &qhead) {
+    prepare(c->ev, qhead.next);
+    info("prepared %s", qhead.next->id);
+  }
+  /* If the queue was empty but we are for some reason paused then
+   * unpause. */
+  if(!playing)
+    resume_playing(0);
+  play(c->ev);
+  return 1;                    /* completed */
+}
+
 static int c_remove(struct conn *c, char **vec,
                    int attribute((unused)) nvec) {
   struct queue_entry *q;
 static int c_remove(struct conn *c, char **vec,
                    int attribute((unused)) nvec) {
   struct queue_entry *q;
@@ -1833,6 +1873,7 @@ static const struct command {
   { "part",           3, 3,       c_part,           RIGHT_READ },
   { "pause",          0, 0,       c_pause,          RIGHT_PAUSE },
   { "play",           1, 1,       c_play,           RIGHT_PLAY },
   { "part",           3, 3,       c_part,           RIGHT_READ },
   { "pause",          0, 0,       c_pause,          RIGHT_PAUSE },
   { "play",           1, 1,       c_play,           RIGHT_PLAY },
+  { "playafter",      2, INT_MAX, c_playafter,      RIGHT_PLAY },
   { "playing",        0, 0,       c_playing,        RIGHT_READ },
   { "playlist-delete",    1, 1,   c_playlist_delete,    RIGHT_PLAY },
   { "playlist-get",       1, 1,   c_playlist_get,       RIGHT_READ },
   { "playing",        0, 0,       c_playing,        RIGHT_READ },
   { "playlist-delete",    1, 1,   c_playlist_delete,    RIGHT_PLAY },
   { "playlist-get",       1, 1,   c_playlist_get,       RIGHT_READ },
index abf1b38..7958ea3 100755 (executable)
@@ -27,6 +27,8 @@ def test():
     c.random_disable()
     assert c.random_enabled() == False
     track = u"%s/Joe Bloggs/First Album/02:Second track.ogg" % dtest.tracks
     c.random_disable()
     assert c.random_enabled() == False
     track = u"%s/Joe Bloggs/First Album/02:Second track.ogg" % dtest.tracks
+    track2 = u"%s/Joe Bloggs/First Album/04:Fourth track.ogg" % dtest.tracks
+    track3 = u"%s/Joe Bloggs/First Album/05:Fifth track.ogg" % dtest.tracks
     print " adding track to queue"
     c.disable()
     assert c.enabled() == False
     print " adding track to queue"
     c.disable()
     assert c.enabled() == False
@@ -62,6 +64,55 @@ def test():
     t = ts[0]
     assert t['submitter'] == u'fred', "check recent entry submitter"
 
     t = ts[0]
     assert t['submitter'] == u'fred', "check recent entry submitter"
 
+    print " ensuring queue is clear"
+    c.disable()
+    while c.playing() is not None:
+        time.sleep(1)
+    q = c.queue()
+    for qe in q:
+        c.remove(qe["id"])
+
+    print " testing playafter"
+    print "  adding to empty queue"
+    c.playafter(None, [track])
+    q = c.queue()
+    print '\n'.join(map(lambda n: "%d: %s" % (n, q[n]["track"]),
+                        range(0, len(q))))
+    assert len(q) == 1
+    assert q[0]['track'] == track
+    print "  insert at start of queue"
+    c.playafter(None, [track2])
+    q = c.queue()
+    print '\n'.join(map(lambda n: "%d: %s" % (n, q[n]["track"]),
+                        range(0, len(q))))
+    assert len(q) == 2
+    assert q[0]['track'] == track2
+    assert q[1]['track'] == track
+    print "  insert in middle of queue"
+    c.playafter(q[0]['id'], [track3])
+    q = c.queue()
+    print '\n'.join(map(lambda n: "%d: %s" % (n, q[n]["track"]),
+                        range(0, len(q))))
+    assert len(q) == 3
+    assert q[0]['track'] == track2
+    assert q[1]['track'] == track3
+    assert q[2]['track'] == track
+    print "  insert multiple tracks at end of queue"
+    c.playafter(q[2]['id'], [track2, track])
+    q = c.queue()
+    print '\n'.join(map(lambda n: "%d: %s" % (n, q[n]["track"]),
+                        range(0, len(q))))
+    assert len(q) == 5
+    assert q[0]['track'] == track2
+    assert q[1]['track'] == track3
+    assert q[2]['track'] == track
+    assert q[3]['track'] == track2
+    assert q[4]['track'] == track
+
+    print " clearing queue"
+    for qe in q:
+        c.remove(qe["id"])
+
     print " testing scratches"
     retry = False
     scratchlimit = 5
     print " testing scratches"
     retry = False
     scratchlimit = 5