From: Richard Kettlewell Date: Sun, 20 Apr 2008 13:19:37 +0000 (+0100) Subject: Merge from 3.0 branch X-Git-Tag: 4.0~117 X-Git-Url: https://www.chiark.greenend.org.uk/ucgi/~mdw/git/disorder/commitdiff_plain/7736e1cc4f9caf63686a425cf716ab54c67647e4?hp=195c7e69d3e2fa1e41e4719f7d678b940782fea3 Merge from 3.0 branch --- diff --git a/.bzrignore b/.bzrignore index 5c8238b..e4a97a8 100644 --- a/.bzrignore +++ b/.bzrignore @@ -146,3 +146,4 @@ examples/disorder.rc scripts/teardown sounds/long.ogg sounds/slap.raw +server/disorder-choose diff --git a/CHANGES b/CHANGES index f8e7ae7..19c04bd 100644 --- a/CHANGES +++ b/CHANGES @@ -1,3 +1,21 @@ +* 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. diff --git a/configure.ac b/configure.ac index 1d95de3..ece08f1 100644 --- a/configure.ac +++ b/configure.ac @@ -20,9 +20,9 @@ # 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]) diff --git a/disobedience/properties.c b/disobedience/properties.c index 7ec9042..44379f8 100644 --- a/disobedience/properties.c +++ b/disobedience/properties.c @@ -121,6 +121,7 @@ static const struct pref { { "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 }, }; diff --git a/doc/disobedience.1.in b/doc/disobedience.1.in index 2c7ee78..6ac4202 100644 --- a/doc/disobedience.1.in +++ b/doc/disobedience.1.in @@ -258,6 +258,10 @@ The Tags field determine which tags apply to the track. 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. diff --git a/doc/disorder.1.in b/doc/disorder.1.in index a056d63..61f6f02 100644 --- a/doc/disorder.1.in +++ b/doc/disorder.1.in @@ -299,6 +299,22 @@ if the full version is not present. .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. diff --git a/doc/disorder.3 b/doc/disorder.3 index f3bbe91..ae54acd 100644 --- a/doc/disorder.3 +++ b/doc/disorder.3 @@ -157,15 +157,6 @@ and are lost if the track is deleted; they should only ever have 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. diff --git a/doc/disorder_config.5.in b/doc/disorder_config.5.in index 3641adf..b0f20ae 100644 --- a/doc/disorder_config.5.in +++ b/doc/disorder_config.5.in @@ -368,6 +368,15 @@ It's best to explicitly specify it to be certain. 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. @@ -395,6 +404,9 @@ default is. .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 @@ -581,6 +593,12 @@ The default is 10. 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: diff --git a/lib/configuration.c b/lib/configuration.c index 869f895..b93e1e9 100644 --- a/lib/configuration.c +++ b/lib/configuration.c @@ -957,6 +957,7 @@ static const struct conf conf[] = { { 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 }, @@ -1178,6 +1179,7 @@ static struct config *config_default(void) { 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; diff --git a/lib/configuration.h b/lib/configuration.h index 9388568..de25197 100644 --- a/lib/configuration.h +++ b/lib/configuration.h @@ -226,6 +226,9 @@ struct config { /** @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 */ diff --git a/lib/event.c b/lib/event.c index db8b940..0e97596 100644 --- a/lib/event.c +++ b/lib/event.c @@ -1234,7 +1234,7 @@ int ev_writer_flush(ev_writer *w) { /* 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 diff --git a/lib/trackdb-int.h b/lib/trackdb-int.h index 9ec328e..ac2d3ea 100644 --- a/lib/trackdb-int.h +++ b/lib/trackdb-int.h @@ -102,6 +102,7 @@ int trackdb_delkeydata(DB *db, int trackdb_scan(const char *root, int (*callback)(const char *track, struct kvp *data, + struct kvp *prefs, void *u, DB_TXN *tid), void *u, @@ -144,6 +145,9 @@ int trackdb_get_global_tid(const char *name, DB_TXN *tid, const char **rp); +char **parsetags(const char *s); +int tag_intersection(char **a, char **b); + #endif /* TRACKDB_INT_H */ /* diff --git a/lib/trackdb.c b/lib/trackdb.c index 8945ec3..6a93fba 100644 --- a/lib/trackdb.c +++ b/lib/trackdb.c @@ -156,10 +156,6 @@ static pid_t db_deadlock_pid = -1; /* deadlock manager PID */ 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) { @@ -839,7 +835,7 @@ static int tagchar(int c) { } /* 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; @@ -1038,7 +1034,6 @@ int trackdb_notice_tid(const char *track, 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; @@ -1096,7 +1091,6 @@ int trackdb_obsolete(const char *track, DB_TXN *tid) { 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; @@ -1458,7 +1452,6 @@ int trackdb_set(const char *track, ++newtags; } } - reqtracks = 0; } } err = 0; @@ -1577,7 +1570,7 @@ int trackdb_listkeys(DB *db, struct vector *v, DB_TXN *tid) { } /* 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 */ @@ -1589,176 +1582,93 @@ static int tag_intersection(char **a, char **b) { 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 @@ -2091,15 +2001,16 @@ char **trackdb_search(char **wordlist, int nwordlist, int *ntracks) { 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); @@ -2119,10 +2030,33 @@ int trackdb_scan(const char *root, 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; } @@ -2229,8 +2163,6 @@ void trackdb_set_global(const char *name, 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, diff --git a/lib/trackdb.h b/lib/trackdb.h index 804ff5a..e39de6e 100644 --- a/lib/trackdb.h +++ b/lib/trackdb.h @@ -172,6 +172,11 @@ char **trackdb_listusers(void); 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 */ /* diff --git a/scripts/htmlman b/scripts/htmlman index 5bbc07b..fa966dd 100755 --- a/scripts/htmlman +++ b/scripts/htmlman @@ -53,7 +53,7 @@ if $stdhead; then fi printf "
"
 # this is kind of painful using only BREs
-nroff -man "$1" | ${GNUSED} \
+nroff -Tascii -man "$1" | ${GNUSED} \
                       '1d;$d;
                        1,/./{/^$/d};
                        s/&/\&/g;
diff --git a/server/Makefile.am b/server/Makefile.am
index bc58b84..78497a5 100644
--- a/server/Makefile.am
+++ b/server/Makefile.am
@@ -20,7 +20,7 @@
 
 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
@@ -74,6 +74,16 @@ disorder_rescan_LDADD=$(LIBOBJS) ../lib/libdisorder.a \
 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)
diff --git a/server/api-server.c b/server/api-server.c
index b1267f0..bc8144e 100644
--- a/server/api-server.c
+++ b/server/api-server.c
@@ -49,10 +49,6 @@ int disorder_track_set_data(const char *track,
   return trackdb_set(track, key, value);
 }
 
-const char *disorder_track_random(void)  {
-  return trackdb_random(16);
-}
-
 /*
 Local Variables:
 c-basic-offset:2
diff --git a/server/choose.c b/server/choose.c
new file mode 100644
index 0000000..06c9601
--- /dev/null
+++ b/server/choose.c
@@ -0,0 +1,305 @@
+/*
+ * 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 
+#include "types.h"
+
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+
+#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:
+*/
diff --git a/server/dcgi.c b/server/dcgi.c
index 19e25e0..33630fa 100644
--- a/server/dcgi.c
+++ b/server/dcgi.c
@@ -426,8 +426,18 @@ static void process_prefs(dcgi_state *ds, int numfile) {
       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");
diff --git a/server/disorderd.c b/server/disorderd.c
index 2fd317d..01d4efe 100644
--- a/server/disorderd.c
+++ b/server/disorderd.c
@@ -61,10 +61,6 @@
 
 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' },
@@ -94,6 +90,8 @@ static void help(void) {
   exit(0);
 }
 
+/* signals ------------------------------------------------------------------ */
+
 /* SIGHUP callback */
 static int handle_sighup(ev_source attribute((unused)) *ev_,
 			 int attribute((unused)) sig,
@@ -119,41 +117,57 @@ static int handle_sigterm(ev_source attribute((unused)) *ev_,
   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];
 
@@ -166,16 +180,14 @@ static int volumecheck_again(ev_source attribute((unused)) *ev_,
       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 */
@@ -284,17 +296,16 @@ int main(int argc, char **argv) {
   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 */
diff --git a/server/play.c b/server/play.c
index 2b6422d..df9158b 100644
--- a/server/play.c
+++ b/server/play.c
@@ -174,25 +174,6 @@ void speaker_reload(void) {
   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. */
@@ -219,7 +200,10 @@ static void finished(ev_source *ev) {
   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. */
@@ -531,34 +515,40 @@ void abandon(ev_source attribute((unused)) *ev,
   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 */
@@ -568,17 +558,15 @@ void play(ev_source *ev) {
 
   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. */
@@ -593,19 +581,11 @@ void play(ev_source *ev) {
       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) {
@@ -620,7 +600,7 @@ void play(ev_source *ev) {
 	     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)
@@ -638,7 +618,7 @@ int playing_is_enabled(void) {
 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);
 }
 
@@ -654,7 +634,7 @@ int random_is_enabled(void) {
 
 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);
 }
 
diff --git a/server/play.h b/server/play.h
index c402312..aff474c 100644
--- a/server/play.h
+++ b/server/play.h
@@ -75,9 +75,8 @@ void abandon(ev_source *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 */
 
diff --git a/server/rescan.c b/server/rescan.c
index c715e91..2d390f8 100644
--- a/server/rescan.c
+++ b/server/rescan.c
@@ -207,6 +207,7 @@ struct recheck_track {
 /* 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;
diff --git a/server/server.c b/server/server.c
index 67b1fdc..79ac7b1 100644
--- a/server/server.c
+++ b/server/server.c
@@ -272,9 +272,8 @@ static int c_remove(struct conn *c, char **vec,
   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);
diff --git a/templates/help.html b/templates/help.html
index 3a55119..8e298ef 100644
--- a/templates/help.html
+++ b/templates/help.html
@@ -212,14 +212,18 @@ USA
     rights, you may not be able to edit track preferences.

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.

Tags are separated by commas and can contain any other printing characters (including spaces). Leading and trailing spaces are not significant.

+

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.

+

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.

diff --git a/templates/options.labels b/templates/options.labels index 82de2d9..55d661a 100644 --- a/templates/options.labels +++ b/templates/options.labels @@ -131,6 +131,7 @@ label prefs.value Value # Legend for prefs controls that don't correspond to a heading label prefs.random "Random play" label prefs.tags "Tags" +label prefs.weight "Weight" # for help page label help.title "DisOrder Help" diff --git a/templates/prefs.html b/templates/prefs.html index 35dc4b6..5e88890 100644 --- a/templates/prefs.html +++ b/templates/prefs.html @@ -56,6 +56,10 @@ USA <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 diff --git a/tests/dtest.py b/tests/dtest.py index fff1866..c37872d 100644 --- a/tests/dtest.py +++ b/tests/dtest.py @@ -173,6 +173,7 @@ def default_config(encoding="UTF-8"): 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 diff --git a/tests/queue.py b/tests/queue.py index 7ed10a0..dae37b4 100755 --- a/tests/queue.py +++ b/tests/queue.py @@ -20,25 +20,29 @@ # 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" @@ -49,9 +53,12 @@ def test(): 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()