scripts/teardown
sounds/long.ogg
sounds/slap.raw
+server/disorder-choose
+* Changes up to version 3.1
+
+** Server
+
+The 'gap' directive will no longer work. It could be restored if there
+is real demand.
+
+*** Random Track Choice
+
+This has been completely rewritten to support new features:
+ - tracks in the recently-played list or in the queue are no longer
+ eligible for random choice
+ - there is a new 'weight' track preference allowing for non-uniform
+ track selection. See disorder(1) for details.
+ - there is a new configuration item replay_min defining the minimum
+ time before a played track can be picked at random. The default is
+ 8 hours (which matches the earlier behaviour).
+
* Changes up to version 3.0.1
Debian upgrades from 2.0.x should now work better.
# USA
#
-AC_INIT([disorder], [3.0+fixes], [richard+disorder@sfere.greenend.org.uk])
+AC_INIT([disorder], [3.0+], [richard+disorder@sfere.greenend.org.uk])
AC_CONFIG_AUX_DIR([config.aux])
-AM_INIT_AUTOMAKE(disorder, [3.0+fixes])
+AM_INIT_AUTOMAKE(disorder, [3.0+])
AC_CONFIG_SRCDIR([server/disorderd.c])
AM_CONFIG_HEADER([config.h])
{ "Album", "album", 0, &preftype_namepart },
{ "Title", "title", 0, &preftype_namepart },
{ "Tags", "tags", "", &preftype_string },
+ { "Weight", "weight", "90000", &preftype_string },
{ "Random", "pick_at_random", "1", &preftype_boolean },
};
Tags are separated by commas and can contain any printing characters except
comma.
.PP
+The Weight field determines the track weight. Tracks with higher weights are
+proportionately more likely to be picked at random. The default weight is
+90000, and the maximum weight is 2147483647.
+.PP
The Random checkbox determines whether the track will be picked at random.
Random play is enabled for every track by default, but it can be turned off
here.
.B unscratched
The number of times the track has been played to completion without
being scratched.
+.TP
+.B weight
+The weight for this track. Weights are non-negative integers which determine
+the relative likelihood of a track being picked at random (i.e. if track A has
+twice the weight of track B then it is twice as likely to be picked at random).
+A track with weight 0 will not be picked at random, though \fBpick_at_random\fR
+is a more sensible way to configure this.
+.IP
+The default weight, used if no weight is set or the weight value is invalid, is
+90000. Note that many other factors than track weight affect whether a track
+will be played - tracks already in the queue will not be picked at random for
+instance.
+.IP
+The maximum allowed weight is 2147483647. If you set a larger value it will be
+clamped to this value. Negative weights will be completely ignored and the
+default value used instead.
.SH NOTES
.B disorder
is locale-aware.
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.
passed to the plugin module.
It must be an absolute path and should not end with a "/".
.TP
+.B cookie_key_lifetime \fISECONDS\fR
+Lifetime of the signing key used in constructing cookies. The default is one
+week.
+.TP
+.B cookie_login_lifetime \fISECONDS\fR
+Lifetime of a cookie enforced by the server. When the cookie expires the user
+will have to log in again even if their browser has remembered the cookie that
+long. The default is one day.
+.TP
.B default_rights \fIRIGHTS\fR
Defines the set of rights given to new users.
The argument is a comma-separated list of rights.
.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
The minimum number of seconds that must elapse between password reminders.
The default is 600, i.e. 10 minutes.
.TP
+.B replay_min \fISECONDS\fR
+The minimum number of seconds that must elapse after a track has been played
+before it can be picked at random. The default is 8 hours. If this is set to
+0 then there is no limit, though current \fBdisorder-choose\fR will not pick
+anything currently listed in the recently-played list.
+.TP
.B sample_format \fIBITS\fB/\fIRATE\fB/\fICHANNELS
Describes the sample format expected by the \fBspeaker_command\fR (below).
The components of the format specification are as follows:
{ C(plugins), &type_string_accum, validate_isdir },
{ C(prefsync), &type_integer, validate_positive },
{ C(queue_pad), &type_integer, validate_positive },
+ { C(replay_min), &type_integer, validate_non_negative },
{ C(refresh), &type_integer, validate_positive },
{ C(reminder_interval), &type_integer, validate_positive },
{ C2(restrict, restrictions), &type_restrict, validate_any },
c->sample_format.channels = 2;
c->sample_format.endian = ENDIAN_NATIVE;
c->queue_pad = 10;
+ c->replay_min = 8 * 3600;
c->api = -1;
c->multicast_ttl = 1;
c->multicast_loop = 1;
/** @brief Target queue length */
long queue_pad;
+ /** @brief Minimum time between a track being played again */
+ long replay_min;
+
struct namepartlist namepart; /* transformations */
/** @brief Termination signal for subprocesses */
/* 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
int trackdb_scan(const char *root,
int (*callback)(const char *track,
struct kvp *data,
+ struct kvp *prefs,
void *u,
DB_TXN *tid),
void *u,
DB_TXN *tid,
const char **rp);
+char **parsetags(const char *s);
+int tag_intersection(char **a, char **b);
+
#endif /* TRACKDB_INT_H */
/*
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) {
}
/* Parse and de-dupe a tag list. If S=0 then assumes "". */
-static char **parsetags(const char *s) {
+char **parsetags(const char *s) {
const char *t;
struct vector v;
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 1 iff sorted tag lists A and B have at least one member in common */
-static int tag_intersection(char **a, char **b) {
+int tag_intersection(char **a, char **b) {
int cmp;
/* Same sort of logic as trackdb_set() above */
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
int trackdb_scan(const char *root,
int (*callback)(const char *track,
struct kvp *data,
+ struct kvp *prefs,
void *u,
DB_TXN *tid),
void *u,
DB_TXN *tid) {
DBC *cursor;
- DBT k, d;
+ DBT k, d, pd;
const size_t root_len = root ? strlen(root) : 0;
int err, cberr;
- struct kvp *data;
+ struct kvp *data, *prefs;
const char *track;
cursor = trackdb_opencursor(trackdb_tracksdb, tid);
data = kvp_urldecode(d.data, d.size);
if(kvp_get(data, "_path")) {
track = xstrndup(k.data, k.size);
+ /* TODO: trackdb_prefsdb is currently a DB_HASH. This means we have to
+ * do a lookup for every single track. In fact this is quite quick:
+ * with around 10,000 tracks a complete scan is around 0.3s on my
+ * 2.2GHz Athlon. However, if it were a DB_BTREE, we could do the same
+ * linear walk as we already do over trackdb_tracksdb, and probably get
+ * even higher performance. That would require upgrade logic to
+ * translate old databases though.
+ */
+ switch(err = trackdb_prefsdb->get(trackdb_prefsdb, tid, &k,
+ prepare_data(&pd), 0)) {
+ case 0:
+ prefs = kvp_urldecode(pd.data, pd.size);
+ break;
+ case DB_NOTFOUND:
+ prefs = 0;
+ break;
+ case DB_LOCK_DEADLOCK:
+ error(0, "getting prefs: %s", db_strerror(err));
+ trackdb_closecursor(cursor);
+ return err;
+ default:
+ fatal(0, "getting prefs: %s", db_strerror(err));
+ }
/* Advance to the next track before the callback so that the callback
* may safely delete the track */
err = cursor->c_get(cursor, &k, &d, DB_NEXT);
- if((cberr = callback(track, data, u, tid))) {
+ if((cberr = callback(track, data, prefs, u, tid))) {
err = cberr;
break;
}
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 */
/*
fi
printf " <pre class=manpage>"
# this is kind of painful using only BREs
-nroff -man "$1" | ${GNUSED} \
+nroff -Tascii -man "$1" | ${GNUSED} \
'1d;$d;
1,/./{/^$/d};
s/&/\&/g;
sbin_PROGRAMS=disorderd disorder-deadlock disorder-rescan disorder-dump \
disorder-speaker disorder-decode disorder-normalize \
- disorder-stats disorder-dbupgrade
+ disorder-stats disorder-dbupgrade disorder-choose
noinst_PROGRAMS=disorder.cgi trackname
AM_CPPFLAGS=-I${top_srcdir}/lib -I../lib
disorder_rescan_LDFLAGS=-export-dynamic
disorder_rescan_DEPENDENCIES=../lib/libdisorder.a
+disorder_choose_SOURCES=choose.c \
+ server-queue.c server-queue.h \
+ api.c api-server.c \
+ exports.c \
+ ../lib/memgc.c
+disorder_choose_LDADD=$(LIBOBJS) ../lib/libdisorder.a \
+ $(LIBDB) $(LIBGC) $(LIBPCRE) $(LIBGCRYPT)
+disorder_choose_LDFLAGS=-export-dynamic
+disorder_choose_DEPENDENCIES=../lib/libdisorder.a
+
disorder_stats_SOURCES=stats.c
disorder_stats_LDADD=$(LIBOBJS) ../lib/libdisorder.a \
$(LIBDB) $(LIBPCRE) $(LIBICONV) $(LIBGCRYPT)
return trackdb_set(track, key, value);
}
-const char *disorder_track_random(void) {
- return trackdb_random(16);
-}
-
/*
Local Variables:
c-basic-offset:2
--- /dev/null
+/*
+ * This file is part of DisOrder
+ * Copyright (C) 2008 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
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
+ * USA
+ */
+/** @file choose.c
+ * @brief Random track chooser
+ *
+ * Picks a track at random and writes it to standard output. If for
+ * any reason no track can be picked - even a trivial reason like a
+ * deadlock - it just exits and expects the server to try again.
+ */
+
+#include <config.h>
+#include "types.h"
+
+#include <getopt.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <db.h>
+#include <locale.h>
+#include <errno.h>
+#include <sys/types.h>
+#include <unistd.h>
+#include <pcre.h>
+#include <string.h>
+#include <fcntl.h>
+#include <syslog.h>
+#include <time.h>
+
+#include "configuration.h"
+#include "log.h"
+#include "defs.h"
+#include "mem.h"
+#include "kvp.h"
+#include "syscalls.h"
+#include "printf.h"
+#include "trackdb.h"
+#include "trackdb-int.h"
+#include "version.h"
+#include "trackname.h"
+#include "queue.h"
+#include "server-queue.h"
+
+static DB_TXN *global_tid;
+
+static const struct option options[] = {
+ { "help", no_argument, 0, 'h' },
+ { "version", no_argument, 0, 'V' },
+ { "config", required_argument, 0, 'c' },
+ { "debug", no_argument, 0, 'd' },
+ { "no-debug", no_argument, 0, 'D' },
+ { "syslog", no_argument, 0, 's' },
+ { "no-syslog", no_argument, 0, 'S' },
+ { 0, 0, 0, 0 }
+};
+
+/* display usage message and terminate */
+static void help(void) {
+ xprintf("Usage:\n"
+ " disorder-choose [OPTIONS]\n"
+ "Options:\n"
+ " --help, -h Display usage message\n"
+ " --version, -V Display version number\n"
+ " --config PATH, -c PATH Set configuration file\n"
+ " --debug, -d Turn on debugging\n"
+ " --[no-]syslog Enable/disable logging to syslog\n"
+ "\n"
+ "Track choose for DisOrder. Not intended to be run\n"
+ "directly.\n");
+ xfclose(stdout);
+ exit(0);
+}
+
+/** @brief Weighted track record */
+struct weighted_track {
+ /** @brief Next track in the list */
+ struct weighted_track *next;
+ /** @brief Track name */
+ const char *track;
+ /** @brief Weight for this track (always positive) */
+ unsigned long weight;
+};
+
+/** @brief List of tracks with nonzero weight */
+static struct weighted_track *tracks;
+
+/** @brief Sum of all weights */
+static unsigned long long total_weight;
+
+/** @brief Count of tracks */
+static long ntracks;
+
+static char **required_tags;
+static char **prohibited_tags;
+
+static int queue_contains(const struct queue_entry *head,
+ const char *track) {
+ const struct queue_entry *q;
+
+ for(q = head->next; q != head; q = q->next)
+ if(!strcmp(q->track, track))
+ return 1;
+ return 0;
+}
+
+/** @brief Compute the weight of a track
+ * @param track Track name (UTF-8)
+ * @param data Track data
+ * @param prefs Track preferences
+ * @return Track weight (non-negative)
+ *
+ * Tracks to be excluded entirely are given a weight of 0.
+ */
+static unsigned long compute_weight(const char *track,
+ struct kvp *data,
+ struct kvp *prefs) {
+ const char *s;
+ char **track_tags;
+ time_t last, now;
+
+ /* Reject tracks not in any collection (race between edit config and
+ * rescan) */
+ if(!find_track_root(track)) {
+ info("found track not in any collection: %s", track);
+ return 0;
+ }
+
+ /* Reject aliases to avoid giving aliased tracks extra weight */
+ if(kvp_get(data, "_alias_for"))
+ return 0;
+
+ /* Reject tracks with random play disabled */
+ if((s = kvp_get(prefs, "pick_at_random"))
+ && !strcmp(s, "0"))
+ return 0;
+
+ /* Reject tracks played within the last 8 hours */
+ if((s = kvp_get(prefs, "played_time"))) {
+ last = atoll(s);
+ now = time(0);
+ if(now < last + config->replay_min)
+ return 0;
+ }
+
+ /* Reject tracks currently in the queue or in the recent list */
+ if(queue_contains(&qhead, track)
+ || queue_contains(&phead, track))
+ return 0;
+
+ /* We'll need tags for a number of things */
+ track_tags = parsetags(kvp_get(prefs, "tags"));
+
+ /* Reject tracks with prohibited tags */
+ if(prohibited_tags && tag_intersection(track_tags, prohibited_tags))
+ return 0;
+
+ /* Reject tracks that lack required tags */
+ if(*required_tags && !tag_intersection(track_tags, required_tags))
+ return 0;
+
+ /* Use the configured weight if available */
+ if((s = kvp_get(prefs, "weight"))) {
+ long n;
+ errno = 0;
+
+ n = strtol(s, 0, 10);
+ if((errno == 0 || errno == ERANGE) && n >= 0)
+ return n;
+ }
+
+ return 90000;
+}
+
+/** @brief Called for each track */
+static int collect_tracks_callback(const char *track,
+ struct kvp *data,
+ struct kvp *prefs,
+ void attribute((unused)) *u,
+ DB_TXN attribute((unused)) *tid) {
+ unsigned long weight = compute_weight(track, data, prefs);
+
+ if(weight) {
+ struct weighted_track *const t = xmalloc(sizeof *t);
+
+ /* Clamp weight so that we can fit in billions of tracks when we do
+ * arithmetic in long long */
+ if(weight > 0x7fffffff)
+ weight = 0x7fffffff;
+ t->next = tracks;
+ t->track = track;
+ t->weight = weight;
+ tracks = t;
+ total_weight += weight;
+ ++ntracks;
+ }
+ return 0;
+}
+
+/** @brief Pick a random integer uniformly from [0, limit) */
+static unsigned long long pick_weight(unsigned long long limit) {
+ unsigned long long n;
+ static int fd = -1;
+ int r;
+
+ if(fd < 0) {
+ if((fd = open("/dev/urandom", O_RDONLY)) < 0)
+ fatal(errno, "opening /dev/urandom");
+ }
+ if((r = read(fd, &n, sizeof n)) < 0)
+ fatal(errno, "reading /dev/urandom");
+ if((size_t)r < sizeof n)
+ fatal(0, "short read from /dev/urandom");
+ return n % limit;
+}
+
+/** @brief Pick a track at random and write it to stdout */
+static void pick_track(void) {
+ long long w;
+ struct weighted_track *t;
+
+ w = pick_weight(total_weight);
+ t = tracks;
+ while(t && w >= t->weight) {
+ w -= t->weight;
+ t = t->next;
+ }
+ if(!t)
+ fatal(0, "ran out of tracks but %lld weighting left", w);
+ xprintf("%s", t->track);
+}
+
+int main(int argc, char **argv) {
+ int n, logsyslog = !isatty(2), err;
+ const char *tags;
+
+ set_progname(argv);
+ mem_init();
+ if(!setlocale(LC_CTYPE, "")) fatal(errno, "error calling setlocale");
+ while((n = getopt_long(argc, argv, "hVc:dDSs", options, 0)) >= 0) {
+ switch(n) {
+ case 'h': help();
+ case 'V': version("disorder-choose");
+ case 'c': configfile = optarg; break;
+ case 'd': debugging = 1; break;
+ case 'D': debugging = 0; break;
+ case 'S': logsyslog = 0; break;
+ case 's': logsyslog = 1; break;
+ default: fatal(0, "invalid option");
+ }
+ }
+ if(logsyslog) {
+ openlog(progname, LOG_PID, LOG_DAEMON);
+ log_default = &log_syslog;
+ }
+ if(config_read(0)) fatal(0, "cannot read configuration");
+ /* Find out current queue/recent list */
+ queue_read();
+ recent_read();
+ /* Generate the candidate track list */
+ trackdb_init(TRACKDB_NO_RECOVER);
+ trackdb_open(TRACKDB_NO_UPGRADE|TRACKDB_READ_ONLY);
+ global_tid = trackdb_begin_transaction();
+ if((err = trackdb_get_global_tid("required-tags", global_tid, &tags)))
+ fatal(0, "error getting required-tags: %s", db_strerror(err));
+ required_tags = parsetags(tags);
+ if((err = trackdb_get_global_tid("prohibited-tags", global_tid, &tags)))
+ fatal(0, "error getting prohibited-tags: %s", db_strerror(err));
+ prohibited_tags = parsetags(tags);
+ if(trackdb_scan(0, collect_tracks_callback, 0, global_tid))
+ exit(1);
+ trackdb_commit_transaction(global_tid);
+ trackdb_close();
+ trackdb_deinit();
+ //info("ntracks=%ld total_weight=%lld", ntracks, total_weight);
+ if(!total_weight)
+ fatal(0, "no tracks match random choice criteria");
+ /* Pick a track */
+ pick_track();
+ xfclose(stdout);
+ return 0;
+}
+
+/*
+Local Variables:
+c-basic-offset:2
+comment-column:40
+fill-column:79
+indent-tabs-mode:nil
+End:
+*/
disorder_unset(ds->g->client, file, "pick_at_random");
else
disorder_set(ds->g->client, file, "pick_at_random", "0");
- if((value = numbered_arg("tags", numfile)))
- disorder_set(ds->g->client, file, "tags", value);
+ if((value = numbered_arg("tags", numfile))) {
+ if(!*value)
+ disorder_unset(ds->g->client, file, "tags");
+ else
+ disorder_set(ds->g->client, file, "tags", value);
+ }
+ if((value = numbered_arg("weight", numfile))) {
+ if(!*value || !strcmp(value, "90000"))
+ disorder_unset(ds->g->client, file, "weight");
+ else
+ disorder_set(ds->g->client, file, "weight", value);
+ }
} else if((name = cgi_get("name"))) {
/* Raw preferences. Not well supported in the templates at the moment. */
value = cgi_get("value");
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 */
/* called for each non-alias track */
static int recheck_list_callback(const char *track,
struct kvp attribute((unused)) *data,
+ struct kvp attribute((unused)) *prefs,
void *u,
DB_TXN attribute((unused)) *tid) {
struct recheck_state *cs = u;
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);
rights, you may not be able to edit track preferences.</p>
<p>The form can be used to
- edit artist, album and title fields for the track as displayed, or
- to set the tags for a track, or to enable or disable random play
+ edit artist, album and title fields for the track as displayed; or
+ to set the tags or weight for a track; or to enable or disable random play
for the track.</p>
<p>Tags are separated by commas and can contain any other printing
characters (including spaces). Leading and trailing spaces are
not significant.</p>
+ <p>Weights determine how likely a track is to be picked at
+ random. Tracks with higher weights are more likely to be picked.
+ The default weight is 90000 and the maximum is 2147483647.</p>
+
<p>By default, any track can be picked for random play. The check
box at the bottom can be used to selectivel enable or disable it
for individual tracks.</p>
# Legend for prefs controls that don't correspond to a heading
label prefs.random "Random play"
label prefs.tags "Tags"
+label prefs.weight "Weight"
# <TITLE> for help page
label help.title "DisOrder Help"
<td class="prefs_value"><input size=40 type=text name="@index@_tags" value="@pref{@arg{@index@_file}@}{tags}@"></td>
</tr>
<tr class=even>
+ <td class="prefs_name">@label:prefs.weight@</td>
+ <td class="prefs_value"><input size=40 type=text name="@index@_weight" value="@pref{@arg{@index@_file}@}{weight}@"></td>
+ </tr>
+ <tr class=odd>
<td class="prefs_name">@label:prefs.random@</td>
<td class="prefs_value"><input type=checkbox
name="@index@_random" value=true
collection fs %s %s/tracks
scratch %s/scratch.ogg
gap 0
+queue_pad 5
stopword 01 02 03 04 05 06 07 08 09 10
stopword 1 2 3 4 5 6 7 8 9
stopword 11 12 13 14 15 16 17 18 19 20
#
import dtest,time,disorder,re
+class wait_monitor(disorder.monitor):
+ def queue(self, q):
+ return False
+
def test():
"""Check the queue is padded to the (default) configured length"""
dtest.start_daemon()
dtest.create_user()
- print " waiting for queue to be populated..."
- class wait_monitor(disorder.monitor):
- def queue(self, q):
- return False
- wait_monitor().run()
c = disorder.client()
- print " getting queue via python module"
+ print " disabling play"
+ c.disable()
+ print " waiting for queue to be populated..."
q = c.queue()
- assert len(q) == 10, "queue is at proper length"
+ while len(q) < 5:
+ print " queue at %d tracks" % len(q)
+ wait_monitor().run()
+ q = c.queue()
print " getting queue via disorder(1)"
q = dtest.command(["disorder",
"--config", disorder._configfile, "--no-per-user-config",
"queue"])
tracks = filter(lambda s: re.match("^track", s), q)
- assert len(tracks) == 10, "queue is at proper length"
+ assert len(tracks) == 5, "queue is at proper length"
print " disabling random play"
c.random_disable()
print " emptying queue"
assert q == [], "checking queue is empty"
print " enabling random play"
c.random_enable()
- print " checking queue refills"
+ print " waiting for queue to refill..."
q = c.queue()
- assert len(q) == 10, "queue is at proper length"
+ while len(q) < 5:
+ print " queue at %d tracks" % len(q)
+ wait_monitor().run()
+ q = c.queue()
print " disabling all play"
c.random_disable()
c.disable()