chiark / gitweb /
Merge more 3.0 branch changes
authorRichard Kettlewell <rjk@greenend.org.uk>
Sun, 20 Apr 2008 14:07:20 +0000 (15:07 +0100)
committerRichard Kettlewell <rjk@greenend.org.uk>
Sun, 20 Apr 2008 14:07:20 +0000 (15:07 +0100)
29 files changed:
.bzrignore
CHANGES
configure.ac
disobedience/properties.c
doc/disobedience.1.in
doc/disorder.1.in
doc/disorder.3
doc/disorder_config.5.in
lib/configuration.c
lib/configuration.h
lib/event.c
lib/trackdb-int.h
lib/trackdb.c
lib/trackdb.h
scripts/htmlman
server/Makefile.am
server/api-server.c
server/choose.c [new file with mode: 0644]
server/dcgi.c
server/disorderd.c
server/play.c
server/play.h
server/rescan.c
server/server.c
templates/help.html
templates/options.labels
templates/prefs.html
tests/dtest.py
tests/queue.py

index 5c8238b0a73d2cfcbfc06a52b4aa8b842cbeef33..e4a97a8c7440969e28d3db446214a11d949c2dd9 100644 (file)
@@ -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 6d92b623a64982a6eb00a674b14abe9b2ddd8f0f..e638f434cdb630b2010b347d9e0d0bfdfbf74696 100644 (file)
--- 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.2
 
 Builds --without-server should work again.
index cd663d1d6b602391fdd726fcf46f9c27f60486f9..2be631f7bf2d340ebd49613cb512be0d940ee59d 100644 (file)
@@ -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])
 
index 7ec9042a31e5e5a72c55e0e9db25f1cb778121f0..44379f835af4d6e4fb5c52431bd960322dcde65a 100644 (file)
@@ -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 },
 };
 
index 2c7ee789c1e04157be99f1b259dc6b8e76776be2..6ac4202442246d0fbe5372ae4810c7189dfe9551 100644 (file)
@@ -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.
index a056d63c00262221d386231066cb379e26478527..61f6f02d3b95ecd5f92e918d74c697a1afcaafbc 100644 (file)
@@ -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.
index f3bbe9175d35767fa42c2317450dcdb928df33b3..ae54acd82049d5434f79dfde31bbd99f73240731 100644 (file)
@@ -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.
index 3641adf1dc2a5a241b06f845424681bb34cdeef5..b0f20aed8e9f83b8177f4e44efc15688d116211a 100644 (file)
@@ -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:
index 869f8957c79f49ff731c303f4dc866ee81385bc2..b93e1e9ea16217b06ff9b21cb70c5c1a4486a3f1 100644 (file)
@@ -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;
index 93885689f8cfee7270f4a574fde985634edb5e65..de251970855cc103316b2f503bf192e0dc6528ff 100644 (file)
@@ -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 */
index db8b9404407be8263a6a39288439be8d1c6e9633..0e975962a13f7f8c151de5000e5ff03dd973bdf8 100644 (file)
@@ -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
index 9ec328ee0df42bd6d25dba8a6cfc3dfafda576e4..ac2d3eae95c7e7630db605011d9341bc51d3707c 100644 (file)
@@ -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 */
 
 /*
index 8945ec3a3f6538328934881ff1cfd0a4bceaa7d3..6a93fbaba33562676ea7fd0f828ca64855d2dd10 100644 (file)
@@ -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,
index 804ff5ab30d386421d7b5b23b1b297c47150a347..e39de6e419454b6494a7dd010c7f2529dbaf4a12 100644 (file)
@@ -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 */
 
 /*
index 5bbc07bc194bf17986c6f508b87d19f3acdae6f0..fa966dda477eddc870a0005e26a6261101ddc15b 100755 (executable)
@@ -53,7 +53,7 @@ if $stdhead; then
 fi
 printf "   <pre class=manpage>"
 # this is kind of painful using only BREs
-nroff -man "$1" | ${GNUSED} \
+nroff -Tascii -man "$1" | ${GNUSED} \
                       '1d;$d;
                        1,/./{/^$/d};
                        s/&/\&amp;/g;
index bc58b8486ea965a508786067334c43eac3d10b57..78497a542b94542dcd89a9c1df87c4b297ffb3dd 100644 (file)
@@ -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)
index b1267f0f0f7ac254e8fb0313925ad8c1a9d4b6a3..bc8144e5b8e551e5bb52242bbb14dea9aa2d2f16 100644 (file)
@@ -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 (file)
index 0000000..06c9601
--- /dev/null
@@ -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 <config.h>
+#include "types.h"
+
+#include <getopt.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <db.h>
+#include <locale.h>
+#include <errno.h>
+#include <sys/types.h>
+#include <unistd.h>
+#include <pcre.h>
+#include <string.h>
+#include <fcntl.h>
+#include <syslog.h>
+#include <time.h>
+
+#include "configuration.h"
+#include "log.h"
+#include "defs.h"
+#include "mem.h"
+#include "kvp.h"
+#include "syscalls.h"
+#include "printf.h"
+#include "trackdb.h"
+#include "trackdb-int.h"
+#include "version.h"
+#include "trackname.h"
+#include "queue.h"
+#include "server-queue.h"
+
+static DB_TXN *global_tid;
+
+static const struct option options[] = {
+  { "help", no_argument, 0, 'h' },
+  { "version", no_argument, 0, 'V' },
+  { "config", required_argument, 0, 'c' },
+  { "debug", no_argument, 0, 'd' },
+  { "no-debug", no_argument, 0, 'D' },
+  { "syslog", no_argument, 0, 's' },
+  { "no-syslog", no_argument, 0, 'S' },
+  { 0, 0, 0, 0 }
+};
+
+/* display usage message and terminate */
+static void help(void) {
+  xprintf("Usage:\n"
+         "  disorder-choose [OPTIONS]\n"
+         "Options:\n"
+         "  --help, -h              Display usage message\n"
+         "  --version, -V           Display version number\n"
+         "  --config PATH, -c PATH  Set configuration file\n"
+         "  --debug, -d             Turn on debugging\n"
+          "  --[no-]syslog           Enable/disable logging to syslog\n"
+          "\n"
+          "Track choose for DisOrder.  Not intended to be run\n"
+          "directly.\n");
+  xfclose(stdout);
+  exit(0);
+}
+
+/** @brief Weighted track record */
+struct weighted_track {
+  /** @brief Next track in the list */
+  struct weighted_track *next;
+  /** @brief Track name */
+  const char *track;
+  /** @brief Weight for this track (always positive) */
+  unsigned long weight;
+};
+
+/** @brief List of tracks with nonzero weight */
+static struct weighted_track *tracks;
+
+/** @brief Sum of all weights */
+static unsigned long long total_weight;
+
+/** @brief Count of tracks */
+static long ntracks;
+
+static char **required_tags;
+static char **prohibited_tags;
+
+static int queue_contains(const struct queue_entry *head,
+                          const char *track) {
+  const struct queue_entry *q;
+
+  for(q = head->next; q != head; q = q->next)
+    if(!strcmp(q->track, track))
+      return 1;
+  return 0;
+}
+
+/** @brief Compute the weight of a track
+ * @param track Track name (UTF-8)
+ * @param data Track data
+ * @param prefs Track preferences
+ * @return Track weight (non-negative)
+ *
+ * Tracks to be excluded entirely are given a weight of 0.
+ */
+static unsigned long compute_weight(const char *track,
+                                    struct kvp *data,
+                                    struct kvp *prefs) {
+  const char *s;
+  char **track_tags;
+  time_t last, now;
+
+  /* Reject tracks not in any collection (race between edit config and
+   * rescan) */
+  if(!find_track_root(track)) {
+    info("found track not in any collection: %s", track);
+    return 0;
+  }
+
+  /* Reject aliases to avoid giving aliased tracks extra weight */
+  if(kvp_get(data, "_alias_for"))
+    return 0;
+  
+  /* Reject tracks with random play disabled */
+  if((s = kvp_get(prefs, "pick_at_random"))
+     && !strcmp(s, "0"))
+    return 0;
+
+  /* Reject tracks played within the last 8 hours */
+  if((s = kvp_get(prefs, "played_time"))) {
+    last = atoll(s);
+    now = time(0);
+    if(now < last + config->replay_min)
+      return 0;
+  }
+
+  /* Reject tracks currently in the queue or in the recent list */
+  if(queue_contains(&qhead, track)
+     || queue_contains(&phead, track))
+    return 0;
+
+  /* We'll need tags for a number of things */
+  track_tags = parsetags(kvp_get(prefs, "tags"));
+
+  /* Reject tracks with prohibited tags */
+  if(prohibited_tags && tag_intersection(track_tags, prohibited_tags))
+    return 0;
+
+  /* Reject tracks that lack required tags */
+  if(*required_tags && !tag_intersection(track_tags, required_tags))
+    return 0;
+
+  /* Use the configured weight if available */
+  if((s = kvp_get(prefs, "weight"))) {
+    long n;
+    errno = 0;
+
+    n = strtol(s, 0, 10);
+    if((errno == 0 || errno == ERANGE) && n >= 0)
+      return n;
+  }
+  
+  return 90000;
+}
+
+/** @brief Called for each track */
+static int collect_tracks_callback(const char *track,
+                                  struct kvp *data,
+                                   struct kvp *prefs,
+                                  void attribute((unused)) *u,
+                                  DB_TXN attribute((unused)) *tid) {
+  unsigned long weight = compute_weight(track, data, prefs);
+
+  if(weight) {
+    struct weighted_track *const t = xmalloc(sizeof *t);
+
+    /* Clamp weight so that we can fit in billions of tracks when we do
+     * arithmetic in long long */
+    if(weight > 0x7fffffff)
+      weight = 0x7fffffff;
+    t->next = tracks;
+    t->track = track;
+    t->weight = weight;
+    tracks = t;
+    total_weight += weight;
+    ++ntracks;
+  }
+  return 0;
+}
+
+/** @brief Pick a random integer uniformly from [0, limit) */
+static unsigned long long pick_weight(unsigned long long limit) {
+  unsigned long long n;
+  static int fd = -1;
+  int r;
+
+  if(fd < 0) {
+    if((fd = open("/dev/urandom", O_RDONLY)) < 0)
+      fatal(errno, "opening /dev/urandom");
+  }
+  if((r = read(fd, &n, sizeof n)) < 0)
+    fatal(errno, "reading /dev/urandom");
+  if((size_t)r < sizeof n)
+    fatal(0, "short read from /dev/urandom");
+  return n % limit;
+}
+
+/** @brief Pick a track at random and write it to stdout */
+static void pick_track(void) {
+  long long w;
+  struct weighted_track *t;
+
+  w = pick_weight(total_weight);
+  t = tracks;
+  while(t && w >= t->weight) {
+    w -= t->weight;
+    t = t->next;
+  }
+  if(!t)
+    fatal(0, "ran out of tracks but %lld weighting left", w);
+  xprintf("%s", t->track);
+}
+
+int main(int argc, char **argv) {
+  int n, logsyslog = !isatty(2), err;
+  const char *tags;
+  
+  set_progname(argv);
+  mem_init();
+  if(!setlocale(LC_CTYPE, "")) fatal(errno, "error calling setlocale");
+  while((n = getopt_long(argc, argv, "hVc:dDSs", options, 0)) >= 0) {
+    switch(n) {
+    case 'h': help();
+    case 'V': version("disorder-choose");
+    case 'c': configfile = optarg; break;
+    case 'd': debugging = 1; break;
+    case 'D': debugging = 0; break;
+    case 'S': logsyslog = 0; break;
+    case 's': logsyslog = 1; break;
+    default: fatal(0, "invalid option");
+    }
+  }
+  if(logsyslog) {
+    openlog(progname, LOG_PID, LOG_DAEMON);
+    log_default = &log_syslog;
+  }
+  if(config_read(0)) fatal(0, "cannot read configuration");
+  /* Find out current queue/recent list */
+  queue_read();
+  recent_read();
+  /* Generate the candidate track list */
+  trackdb_init(TRACKDB_NO_RECOVER);
+  trackdb_open(TRACKDB_NO_UPGRADE|TRACKDB_READ_ONLY);
+  global_tid = trackdb_begin_transaction();
+  if((err = trackdb_get_global_tid("required-tags", global_tid, &tags)))
+    fatal(0, "error getting required-tags: %s", db_strerror(err));
+  required_tags = parsetags(tags);
+  if((err = trackdb_get_global_tid("prohibited-tags", global_tid, &tags)))
+    fatal(0, "error getting prohibited-tags: %s", db_strerror(err));
+  prohibited_tags = parsetags(tags);
+  if(trackdb_scan(0, collect_tracks_callback, 0, global_tid))
+    exit(1);
+  trackdb_commit_transaction(global_tid);
+  trackdb_close();
+  trackdb_deinit();
+  //info("ntracks=%ld total_weight=%lld", ntracks, total_weight);
+  if(!total_weight)
+    fatal(0, "no tracks match random choice criteria");
+  /* Pick a track */
+  pick_track();
+  xfclose(stdout);
+  return 0;
+}
+
+/*
+Local Variables:
+c-basic-offset:2
+comment-column:40
+fill-column:79
+indent-tabs-mode:nil
+End:
+*/
index 19e25e0b97aa0a93b8708e0700c5e974a50aed51..33630faa85ea2e037bc3fb73ae9d29c835c2aa8b 100644 (file)
@@ -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");
index 2fd317d05b9d54ca99ab756c5dd1389f81e47616..01d4efe7bac40d34419bbd452c77c5f6b2fa8e4e 100644 (file)
 
 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 */
index 2b6422de6b7f4d98fac0402558dffd8f275f7654..df9158b4fb0eab63c5d124d7dfc8ba11b884d284 100644 (file)
@@ -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);
 }
 
index c40231241e48816fb84bb23a3f76772d97e1441f..aff474ce71d8c0d2bad64e04b5cd6135c5671efa 100644 (file)
@@ -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 */
 
index c715e9190c5987eb83384c594c8ff6995f46256b..2d390f815e00468e3f0049783aebf95853a00b6c 100644 (file)
@@ -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;
index 67b1fdcb4b95b21340c2375f99b495042cecb1d8..79ac7b114d13d0f7ddf9410a2079e24598bda4cd 100644 (file)
@@ -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);
index 3a5511907cfc5b7630e9665b9f338d2c73236d0e..8e298efbbd387dc019aa65115bc67ccb819ebb47 100644 (file)
@@ -212,14 +212,18 @@ USA
     rights, you may not be able to edit track preferences.</p>
     
     <p>The form can be used to
-    edit artist, album and title fields for the track as displayed, or
-    to set the tags for a track, or to enable or disable random play
+    edit artist, album and title fields for the track as displayed; or
+    to set the tags or weight for a track; or to enable or disable random play
     for the track.</p>
 
     <p>Tags are separated by commas and can contain any other printing
     characters (including spaces).  Leading and trailing spaces are
     not significant.</p>
 
+    <p>Weights determine how likely a track is to be picked at
+    random.  Tracks with higher weights are more likely to be picked.
+    The default weight is 90000 and the maximum is 2147483647.</p>
+
     <p>By default, any track can be picked for random play.  The check
     box at the bottom can be used to selectivel enable or disable it
     for individual tracks.</p>
index 82de2d934fb1db162c9bbcc06e58e856cd26854d..55d661a538ed7ae5c5624cf26a404b67ae4cf12b 100644 (file)
@@ -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"
 
 # <TITLE> for help page
 label  help.title              "DisOrder Help"
index 35dc4b6ddc4e7cf044bac7a465123a1ede7e57eb..5e88890148c5f0d7b74e2b38fc29a8078cc6fbfc 100644 (file)
@@ -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
index fff18664a77c951855f3827045145cdad9132fa7..c37872d6baae568e5ff5e955f4c0a071d10f0cb4 100644 (file)
@@ -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
index 7ed10a076aa632419475a745487fec84059a0aaf..dae37b47d473206d825940eb4e7c55796a6e5dbe 100755 (executable)
 #
 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()