+* Changes up to version 3.1
+
+** Server
+
+The 'gap' directive will no longer work. It could be restored if there
+is real demand.
+
* Changes up to version 3.0
Important! See README.upgrades when upgrading.
values that can be regenerated on demand.
Other values are stored in the prefs database and never get
automatically deleted.
-.PP
-.nf
-\fBconst char *disorder_track_random(void)
-.fi
-.IP
-Returns a pointer to a copy of the name of a randomly chosen track.
-Each non-alias track has an equal probability of being chosen.
-Aliases are never returned.
-Only available in server plugins.
.SH "PLUGIN FUNCTIONS"
This section describes the functions that you must implement to write various
plugins.
.B gap \fISECONDS\fR
Specifies the number of seconds to leave between tracks.
The default is 0.
+.IP
+NB this option currently DOES NOT WORK. If there is genuine demand it might be
+reinstated.
.TP
.B history \fIINTEGER\fR
Specifies the number of recently played tracks to remember (including
/* buffered reader ************************************************************/
-/** @brief Shut down a reader*
+/** @brief Shut down a reader
*
* This is the only path through which we cancel and close the file descriptor.
* As with the writer case it is given timeout signature to allow it be
static pid_t rescan_pid = -1; /* rescanner PID */
static int initialized, opened; /* state */
-/* tracks matched by required_tags */
-static char **reqtracks;
-static size_t nreqtracks;
-
/* comparison function for keys */
static int compare(DB attribute((unused)) *db_,
const DBT *a, const DBT *b) {
for(n = 0; w[n]; ++n)
if((err = register_tag(track, w[n], tid)))
return err;
- reqtracks = 0;
/* only store the tracks.db entry if it has changed */
if(t_changed && (err = trackdb_putdata(trackdb_tracksdb, track, t, tid, 0)))
return err;
if(trackdb_delkeydata(trackdb_tagsdb,
w[n], track, tid) == DB_LOCK_DEADLOCK)
return err;
- reqtracks = 0;
/* update tracks.db */
if(trackdb_delkey(trackdb_tracksdb, track, tid) == DB_LOCK_DEADLOCK)
return err;
++newtags;
}
}
- reqtracks = 0;
}
}
err = 0;
return 0;
}
-/* Check whether a track is suitable for random play. Returns 0 if it is,
- * DB_NOTFOUND if it is not or DB_LOCK_DEADLOCK if the database gave us
- * that. */
-static int check_suitable(const char *track,
- DB_TXN *tid,
- char **required_tags,
- char **prohibited_tags) {
- char **track_tags;
- time_t last, now;
- struct kvp *p, *t;
- const char *pick_at_random, *played_time;
-
- /* don't pick tracks that aren't in any surviving collection (for instance
- * you've edited the config but the rescan hasn't done its job yet) */
- if(!find_track_root(track)) {
- info("found track not in any collection: %s", track);
- return DB_NOTFOUND;
- }
- /* don't pick aliases - only pick the canonical form */
- if(gettrackdata(track, &t, &p, 0, 0, tid) == DB_LOCK_DEADLOCK)
- return DB_LOCK_DEADLOCK;
- if(kvp_get(t, "_alias_for"))
- return DB_NOTFOUND;
- /* check that random play is not suppressed for this track */
- if((pick_at_random = kvp_get(p, "pick_at_random"))
- && !strcmp(pick_at_random, "0"))
- return DB_NOTFOUND;
- /* don't pick a track that's been played in the last 8 hours */
- if((played_time = kvp_get(p, "played_time"))) {
- last = atoll(played_time);
- now = time(0);
- if(now < last + 8 * 3600) /* TODO configurable */
- return DB_NOTFOUND;
- }
- track_tags = parsetags(kvp_get(p, "tags"));
- /* check that no prohibited tag is present for this track */
- if(prohibited_tags && tag_intersection(track_tags, prohibited_tags))
- return DB_NOTFOUND;
- /* check that at least one required tags is present for this track */
- if(*required_tags && !tag_intersection(track_tags, required_tags))
- return DB_NOTFOUND;
+static pid_t choose_pid = -1;
+static int choose_fd;
+static random_callback *choose_callback;
+static struct dynstr choose_output;
+static unsigned choose_complete;
+static int choose_status;
+#define CHOOSE_RUNNING 1
+#define CHOOSE_READING 2
+
+static void choose_finished(ev_source *ev, unsigned which) {
+ choose_complete |= which;
+ if(choose_complete != (CHOOSE_RUNNING|CHOOSE_READING))
+ return;
+ choose_pid = -1;
+ if(choose_status == 0 && choose_output.nvec > 0) {
+ dynstr_terminate(&choose_output);
+ choose_callback(ev, xstrdup(choose_output.vec));
+ } else
+ choose_callback(ev, 0);
+}
+
+/** @brief Called when @c disorder-choose terminates */
+static int choose_exited(ev_source *ev,
+ pid_t attribute((unused)) pid,
+ int status,
+ const struct rusage attribute((unused)) *rusage,
+ void attribute((unused)) *u) {
+ if(status)
+ error(0, "disorder-choose %s", wstat(status));
+ choose_status = status;
+ choose_finished(ev, CHOOSE_RUNNING);
return 0;
}
-/* attempt to pick a random non-alias track */
-const char *trackdb_random(int tries) {
- DBT key, data;
- DB_BTREE_STAT *sp;
- int err, n;
- DB_TXN *tid;
- const char *track, *candidate;
- db_recno_t r;
- const char *tags;
- char **required_tags, **prohibited_tags, **tp;
- hash *h;
- DBC *c = 0;
+/** @brief Called with data from @c disorder-choose pipe */
+static int choose_readable(ev_source *ev,
+ ev_reader *reader,
+ void *ptr,
+ size_t bytes,
+ int eof,
+ void attribute((unused)) *u) {
+ dynstr_append_bytes(&choose_output, ptr, bytes);
+ ev_reader_consume(reader, bytes);
+ if(eof)
+ choose_finished(ev, CHOOSE_READING);
+ return 0;
+}
- for(;;) {
- tid = trackdb_begin_transaction();
- if((err = trackdb_get_global_tid("required-tags", tid, &tags)))
- goto fail;
- required_tags = parsetags(tags);
- if((err = trackdb_get_global_tid("prohibited-tags", tid, &tags)))
- goto fail;
- prohibited_tags = parsetags(tags);
- track = 0;
- if(*required_tags) {
- /* Bung all the suitable tracks into a hash and convert to a list of keys
- * (to eliminate duplicates). We cache this list since it is possible
- * that it will be very large. */
- if(!reqtracks) {
- h = hash_new(0);
- for(tp = required_tags; *tp; ++tp) {
- c = trackdb_opencursor(trackdb_tagsdb, tid);
- memset(&key, 0, sizeof key);
- key.data = *tp;
- key.size = strlen(*tp);
- n = 0;
- err = c->c_get(c, &key, prepare_data(&data), DB_SET);
- while(err == 0) {
- hash_add(h, xstrndup(data.data, data.size), 0,
- HASH_INSERT_OR_REPLACE);
- ++n;
- err = c->c_get(c, &key, prepare_data(&data), DB_NEXT_DUP);
- }
- switch(err) {
- case 0:
- case DB_NOTFOUND:
- break;
- case DB_LOCK_DEADLOCK:
- goto fail;
- default:
- fatal(0, "error querying tags.db: %s", db_strerror(err));
- }
- trackdb_closecursor(c);
- c = 0;
- if(!n)
- error(0, "required tag %s does not match any tracks", *tp);
- }
- nreqtracks = hash_count(h);
- reqtracks = hash_keys(h);
- }
- while(nreqtracks && !track && tries-- > 0) {
- r = (rand() * (double)nreqtracks / (RAND_MAX + 1.0));
- candidate = reqtracks[r];
- switch(check_suitable(candidate, tid,
- required_tags, prohibited_tags)) {
- case 0:
- track = candidate;
- break;
- case DB_NOTFOUND:
- break;
- case DB_LOCK_DEADLOCK:
- goto fail;
- }
- }
- } else {
- /* No required tags. We pick random record numbers in the database
- * instead. */
- switch(err = trackdb_tracksdb->stat(trackdb_tracksdb, tid, &sp, 0)) {
- case 0:
- break;
- case DB_LOCK_DEADLOCK:
- error(0, "error querying tracks.db: %s", db_strerror(err));
- goto fail;
- default:
- fatal(0, "error querying tracks.db: %s", db_strerror(err));
- }
- if(!sp->bt_nkeys)
- error(0, "cannot pick tracks at random from an empty database");
- while(sp->bt_nkeys && !track && tries-- > 0) {
- /* record numbers count from 1 upwards */
- r = 1 + (rand() * (double)sp->bt_nkeys / (RAND_MAX + 1.0));
- memset(&key, sizeof key, 0);
- key.flags = DB_DBT_MALLOC;
- key.size = sizeof r;
- key.data = &r;
- switch(err = trackdb_tracksdb->get(trackdb_tracksdb, tid, &key, prepare_data(&data),
- DB_SET_RECNO)) {
- case 0:
- break;
- case DB_LOCK_DEADLOCK:
- error(0, "error querying tracks.db: %s", db_strerror(err));
- goto fail;
- default:
- fatal(0, "error querying tracks.db: %s", db_strerror(err));
- }
- candidate = xstrndup(key.data, key.size);
- switch(check_suitable(candidate, tid,
- required_tags, prohibited_tags)) {
- case 0:
- track = candidate;
- break;
- case DB_NOTFOUND:
- break;
- case DB_LOCK_DEADLOCK:
- goto fail;
- }
- }
- }
- break;
-fail:
- trackdb_closecursor(c);
- c = 0;
- trackdb_abort_transaction(tid);
- }
- trackdb_commit_transaction(tid);
- if(!track)
- error(0, "could not pick a random track");
- return track;
+static int choose_read_error(ev_source *ev,
+ int errno_value,
+ void attribute((unused)) *u) {
+ error(errno_value, "error reading disorder-choose pipe");
+ choose_finished(ev, CHOOSE_READING);
+ return 0;
+}
+
+/** @brief Request a random track
+ * @param ev Event source
+ * @param callback Called with random track or NULL
+ * @return 0 if a request was initiated, else -1
+ *
+ * Initiates a random track choice. @p callback will later be called back with
+ * the choice (or NULL on error). If a choice is already underway then -1 is
+ * returned and there will be no additional callback.
+ *
+ * The caller shouldn't assume that the track returned actually exists (it
+ * might be removed between the choice and the callback, or between being added
+ * to the queue and being played).
+ */
+int trackdb_request_random(ev_source *ev,
+ random_callback *callback) {
+ int p[2];
+
+ if(choose_pid != -1)
+ return -1; /* don't run concurrent chooses */
+ xpipe(p);
+ cloexec(p[0]);
+ choose_pid = subprogram(ev, p[1], "disorder-choose", (char *)0);
+ choose_fd = p[0];
+ xclose(p[1]);
+ choose_callback = callback;
+ choose_output.nvec = 0;
+ choose_complete = 0;
+ ev_reader_new(ev, p[0], choose_readable, choose_read_error, 0,
+ "disorder-choose reader"); /* owns p[0] */
+ ev_child(ev, choose_pid, 0, choose_exited, 0); /* owns the subprocess */
+ return 0;
}
/* get a track name given the prefs. Set *used_db to 1 if we got the answer
who ? who : "-");
eventlog("state", state ? "enable_random" : "disable_random", (char *)0);
}
- if(!strcmp(name, "required-tags"))
- reqtracks = 0;
}
int trackdb_set_global_tid(const char *name,
int trackdb_confirm(const char *user, const char *confirmation,
rights_type *rightsp);
+typedef void random_callback(struct ev_source *ev,
+ const char *track);
+int trackdb_request_random(struct ev_source *ev,
+ random_callback *callback);
+
#endif /* TRACKDB_H */
/*
return trackdb_set(track, key, value);
}
-const char *disorder_track_random(void) {
- return trackdb_random(16);
-}
-
/*
Local Variables:
c-basic-offset:2
static ev_source *ev;
-static void rescan_after(long offset);
-static void dbgc_after(long offset);
-static void volumecheck_after(long offset);
-
static const struct option options[] = {
{ "help", no_argument, 0, 'h' },
{ "version", no_argument, 0, 'V' },
exit(0);
}
+/* signals ------------------------------------------------------------------ */
+
/* SIGHUP callback */
static int handle_sighup(ev_source attribute((unused)) *ev_,
int attribute((unused)) sig,
quit(ev);
}
-static int rescan_again(ev_source *ev_,
- const struct timeval attribute((unused)) *now,
- void attribute((unused)) *u) {
- trackdb_rescan(ev_, 1/*check*/);
- rescan_after(86400);
- return 0;
-}
+/* periodic actions --------------------------------------------------------- */
+
+struct periodic_data {
+ void (*callback)(ev_source *);
+ int period;
+};
-static void rescan_after(long offset) {
+static int periodic_callback(ev_source *ev_,
+ const struct timeval attribute((unused)) *now,
+ void *u) {
struct timeval w;
+ struct periodic_data *const pd = u;
+ pd->callback(ev_);
gettimeofday(&w, 0);
- w.tv_sec += offset;
- ev_timeout(ev, 0, &w, rescan_again, 0);
-}
-
-static int dbgc_again(ev_source attribute((unused)) *ev_,
- const struct timeval attribute((unused)) *now,
- void attribute((unused)) *u) {
- trackdb_gc();
- dbgc_after(60);
+ w.tv_sec += pd->period;
+ ev_timeout(ev, 0, &w, periodic_callback, pd);
return 0;
}
-static void dbgc_after(long offset) {
+/** @brief Create a periodic action
+ * @param ev Event loop
+ * @param callback Callback function
+ * @param period Interval between calls in seconds
+ * @param immediate If true, call @p callback straight away
+ */
+static void create_periodic(ev_source *ev_,
+ void (*callback)(ev_source *),
+ int period,
+ int immediate) {
struct timeval w;
+ struct periodic_data *const pd = xmalloc(sizeof *pd);
+ pd->callback = callback;
+ pd->period = period;
+ if(immediate)
+ callback(ev_);
gettimeofday(&w, 0);
- w.tv_sec += offset;
- ev_timeout(ev, 0, &w, dbgc_again, 0);
+ w.tv_sec += period;
+ ev_timeout(ev_, 0, &w, periodic_callback, pd);
}
-static int volumecheck_again(ev_source attribute((unused)) *ev_,
- const struct timeval attribute((unused)) *now,
- void attribute((unused)) *u) {
+static void periodic_rescan(ev_source *ev_) {
+ trackdb_rescan(ev_, 1/*check*/);
+}
+
+static void periodic_database_gc(ev_source attribute((unused)) *ev_) {
+ trackdb_gc();
+}
+
+static void periodic_volume_check(ev_source attribute((unused)) *ev_) {
int l, r;
char lb[32], rb[32];
eventlog("volume", lb, rb, (char *)0);
}
}
- volumecheck_after(60);
- return 0;
}
-static void volumecheck_after(long offset) {
- struct timeval w;
+static void periodic_play_check(ev_source *ev_) {
+ play(ev_);
+}
- gettimeofday(&w, 0);
- w.tv_sec += offset;
- ev_timeout(ev, 0, &w, volumecheck_again, 0);
+static void periodic_add_random(ev_source *ev_) {
+ add_random_track(ev_);
}
/* We fix the path to include the bindir and sbindir we were installed into */
if(ev_signal(ev, SIGTERM, handle_sigterm, 0)) fatal(0, "ev_signal failed");
/* ignore SIGPIPE */
signal(SIGPIPE, SIG_IGN);
- /* Start a rescan straight away */
- trackdb_rescan(ev, 1/*check*/);
- /* We'll rescan again after a day */
- rescan_after(86400);
- /* periodically tidy up the database */
- dbgc_after(60);
- /* periodically check the volume */
- volumecheck_again(0, 0, 0);
- /* set initial state */
- add_random_track();
- play(ev);
+ /* Rescan immediately and then daily */
+ create_periodic(ev, periodic_rescan, 86400, 1/*immediate*/);
+ /* Tidy up the database once a minute */
+ create_periodic(ev, periodic_database_gc, 60, 0);
+ /* Check the volume immediately and then once a minute */
+ create_periodic(ev, periodic_volume_check, 60, 1);
+ /* Check for a playable track once a second */
+ create_periodic(ev, periodic_play_check, 1, 0);
+ /* Try adding a random track immediately and once every ten seconds */
+ create_periodic(ev, periodic_add_random, 10, 1);
/* enter the event loop */
n = ev_run(ev);
/* if we exit the event loop, something must have gone wrong */
speaker_send(speaker_fd, &sm);
}
-/* timeout for play retry */
-static int play_again(ev_source *ev,
- const struct timeval attribute((unused)) *now,
- void attribute((unused)) *u) {
- D(("play_again"));
- play(ev);
- return 0;
-}
-
-/* try calling play() again after @offset@ seconds */
-static void retry_play(ev_source *ev, int offset) {
- struct timeval w;
-
- D(("retry_play(%d)", offset));
- gettimeofday(&w, 0);
- w.tv_sec += offset;
- ev_timeout(ev, 0, &w, play_again, 0);
-}
-
/* Called when the currently playing track finishes playing. This
* might be because the player finished or because the speaker process
* told us so. */
recent_write();
forget_player_pid(playing->id);
playing = 0;
- if(ev) retry_play(ev, config->gap);
+ /* Try to play something else */
+ /* TODO re-support config->gap? */
+ if(ev)
+ play(ev);
}
/* Called when a player terminates. */
speaker_send(speaker_fd, &sm);
}
-int add_random_track(void) {
+/** @brief Called with a new random track
+ * @param track Track name
+ */
+static void chosen_random_track(ev_source *ev,
+ const char *track) {
+ struct queue_entry *q;
+
+ if(!track)
+ return;
+ /* Add the track to the queue */
+ q = queue_add(track, 0, WHERE_END);
+ q->state = playing_random;
+ D(("picked %p (%s) at random", (void *)q, q->track));
+ queue_write();
+ /* Maybe a track can now be played */
+ play(ev);
+}
+
+/** @brief Maybe add a randomly chosen track
+ * @param ev Event loop
+ */
+void add_random_track(ev_source *ev) {
struct queue_entry *q;
- const char *p;
long qlen = 0;
- int rc = 0;
/* If random play is not enabled then do nothing. */
if(shutting_down || !random_is_enabled())
- return 0;
+ return;
/* Count how big the queue is */
for(q = qhead.next; q != &qhead; q = q->next)
++qlen;
- /* Add random tracks until the queue is at the right size */
- while(qlen < config->queue_pad) {
- /* Try to pick a random track */
- if(!(p = trackdb_random(16))) {
- rc = -1;
- break;
- }
- /* Add it to the end of the queue. */
- q = queue_add(p, 0, WHERE_END);
- q->state = playing_random;
- D(("picked %p (%s) at random", (void *)q, q->track));
- ++qlen;
- }
- /* Commit the queue */
- queue_write();
- return rc;
+ /* If it's smaller than the desired size then add a track */
+ if(qlen < config->queue_pad)
+ trackdb_request_random(ev, chosen_random_track);
}
/* try to play a track */
D(("play playing=%p", (void *)playing));
if(shutting_down || playing || !playing_is_enabled()) return;
- /* If the queue is empty then add a random track. */
+ /* See if there's anything to play */
if(qhead.next == &qhead) {
- if(!random_enabled)
- return;
- if(add_random_track()) {
- /* On error, try again in 10s. */
- retry_play(ev, 10);
- return;
- }
- /* Now there must be at least one track in the queue. */
+ /* Queue is empty. We could just wait around since there are periodic
+ * attempts to add a random track anyway. However they are rarer than
+ * attempts to force a track so we initiate one now. */
+ add_random_track(ev);
+ return;
}
+ /* There must be at least one track in the queue. */
q = qhead.next;
/* If random play is disabled but the track is a random one then don't play
* it. play() will be called again when random play is re-enabled. */
queue_played(q);
recent_write();
}
- if(qhead.next == &qhead)
- /* Queue is empty, wait a bit before trying something else (so we don't
- * sit there looping madly in the presence of persistent problem). Note
- * that we might not reliably get a random track lookahead in this case,
- * but if we get here then really there are bigger problems. */
- retry_play(ev, 1);
- else
- /* More in queue, try again now. */
- play(ev);
+ /* Oh well, try the next one */
+ play(ev);
break;
case START_SOFTFAIL:
- /* Try same track again in a bit. */
- retry_play(ev, 10);
+ /* We'll try the same track again shortly. */
break;
case START_OK:
if(q == qhead.next) {
playing->submitter ? playing->submitter : (const char *)0,
(const char *)0);
/* Maybe add a random track. */
- add_random_track();
+ add_random_track(ev);
/* If there is another track in the queue prepare it now. This could
* potentially be a just-added random track. */
if(qhead.next != &qhead)
void enable_playing(const char *who, ev_source *ev) {
trackdb_set_global("playing", "yes", who);
/* Add a random track if necessary. */
- add_random_track();
+ add_random_track(ev);
play(ev);
}
void enable_random(const char *who, ev_source *ev) {
trackdb_set_global("random-play", "yes", who);
- add_random_track();
+ add_random_track(ev);
play(ev);
}
struct queue_entry *q);
/* Abandon a possibly-prepared track. */
-int add_random_track(void);
-/* If random play is enabled then try to add a track to the queue. On success
- * (including deliberartely doing nothing) return 0. On error return -1. */
+void add_random_track(ev_source *ev);
+/* If random play is enabled then try to add a track to the queue. */
#endif /* PLAY_H */
queue_remove(q, c->who);
/* De-prepare the track. */
abandon(c->ev, q);
- /* If we removed a random track then add another one. */
- if(q->state == playing_random)
- add_random_track();
+ /* See about adding a new random track */
+ add_random_track(c->ev);
/* Prepare whatever the next head track is. */
if(qhead.next != &qhead)
prepare(c->ev, qhead.next);