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