From 7a853280dc559b6a6d30e08daab964dcf64da62b Mon Sep 17 00:00:00 2001 Message-Id: <7a853280dc559b6a6d30e08daab964dcf64da62b.1715189716.git.mdw@distorted.org.uk> From: Mark Wooding Date: Sat, 14 Nov 2009 11:24:03 +0000 Subject: [PATCH] Add new 'playafter' command to protocol, eclient and python. This allows multiple tracks to be inserted at arbitrary points in the queue. Organization: Straylight/Edgeware From: Richard Kettlewell --- doc/disorder_protocol.5.in | 10 +++++++ lib/eclient.c | 22 ++++++++++++++++ lib/eclient.h | 8 ++++++ lib/queue.c | 7 +++-- python/disorder.py.in | 10 +++++++ server/disorder-server.h | 4 ++- server/play.c | 5 ++-- server/queue-ops.c | 32 +++++++++++++++++++++-- server/schedule.c | 2 +- server/server.c | 53 +++++++++++++++++++++++++++++++++----- tests/play.py | 51 ++++++++++++++++++++++++++++++++++++ 11 files changed, 190 insertions(+), 14 deletions(-) diff --git a/doc/disorder_protocol.5.in b/doc/disorder_protocol.5.in index a0baadb..6bc9c47 100644 --- a/doc/disorder_protocol.5.in +++ b/doc/disorder_protocol.5.in @@ -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 +.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 diff --git a/lib/eclient.c b/lib/eclient.c index fad9e1b..ba743ec 100644 --- a/lib/eclient.c +++ b/lib/eclient.c @@ -1132,6 +1132,28 @@ int disorder_eclient_play(disorder_eclient *c, "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) { diff --git a/lib/eclient.h b/lib/eclient.h index fae610c..b100106 100644 --- a/lib/eclient.h +++ b/lib/eclient.h @@ -325,6 +325,14 @@ int disorder_eclient_play(disorder_eclient *c, 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); diff --git a/lib/queue.c b/lib/queue.c index 1486ebf..db28687 100644 --- a/lib/queue.c +++ b/lib/queue.c @@ -1,6 +1,6 @@ /* * 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 @@ -55,7 +55,10 @@ const char *const track_origins[] = { #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; diff --git a/python/disorder.py.in b/python/disorder.py.in index d06c7ee..fe054a9 100644 --- a/python/disorder.py.in +++ b/python/disorder.py.in @@ -439,6 +439,16 @@ class client: 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. diff --git a/server/disorder-server.h b/server/disorder-server.h index 60e97df..f980b6f 100644 --- a/server/disorder-server.h +++ b/server/disorder-server.h @@ -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, - 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_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); diff --git a/server/play.c b/server/play.c index 8fd6d4c..a9a505f 100644 --- a/server/play.c +++ b/server/play.c @@ -515,7 +515,7 @@ static void chosen_random_track(ev_source *ev, 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 */ @@ -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); - 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); diff --git a/server/queue-ops.c b/server/queue-ops.c index ed97c0c..f2b3457 100644 --- a/server/queue-ops.c +++ b/server/queue-ops.c @@ -48,9 +48,23 @@ static void queue_id(struct queue_entry *q) { 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, - 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); @@ -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; + 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) diff --git a/server/schedule.c b/server/schedule.c index 2669fed..f93c0f0 100644 --- a/server/schedule.c +++ b/server/schedule.c @@ -374,7 +374,7 @@ static void schedule_play(ev_source *ev, 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); diff --git a/server/server.c b/server/server.c index 781c71a..6866764 100644 --- a/server/server.c +++ b/server/server.c @@ -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; } - 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(); - /* 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); + /* 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); @@ -257,6 +257,46 @@ static int c_play(struct conn *c, char **vec, 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; @@ -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 }, + { "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 }, diff --git a/tests/play.py b/tests/play.py index abf1b38..7958ea3 100755 --- a/tests/play.py +++ b/tests/play.py @@ -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 + 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 @@ -62,6 +64,55 @@ def test(): 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 -- [mdw]