chiark / gitweb /
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 a0baadba4089bd15aaedb52ff5f3f55cf359dfb8..6bc9c475461db24f626572f6bcc0931121c68317 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 fad9e1b4d0489f60a4360311d7d9d6b8103f13d4..ba743ec8411591084668aadd5191cf0db4b80ba7 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 fae610c99bd3e2ba7f16a79d5f88de96ee522f9d..b100106c04abd55f25e4ebdf6c1593b957efe140 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 1486ebfc17d4042d82a1a90e45cfd8b58e4e65c1..db28687923996b11e86afba979e35dfe16eec0fe 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 d06c7eef7f5773d2f205187f58dabd5b47626f1e..fe054a9474aec4ef684cfec3e8efff3b30835a45 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 60e97dfc343ddde47d8216d9b2a92ba3985c4b4c..f980b6f3ea9a1ab1aabbe171dc4d466c996ee58e 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 8fd6d4cf945fcb2107f9d04bd968f4906faa1098..a9a505ff4a88becce79de2dca00000a4b21c3f51 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 ed97c0c805d2227aba8683d96af2569e4f06d400..f2b3457a6f23001d7fff10fc86a2a682d01037b7 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 2669feded9aa0518a8812895d67a11afd4ea5143..f93c0f0677cbe26844722b14acae95906d0259a4 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 781c71a698298d9f6f4a0cc08d44b04e7f8061f9..6866764419cee840879da5ef3766a7dd64a484f0 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 abf1b38b97495e6f48aaa13714924b6016c617fa..7958ea30fe4766fa1dd5c8030f21bde733ccdb4a 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