chiark / gitweb /
build fixes
[disorder] / server / trackdb.c
index 223dc808c85f2a67457c4b7d5bc52297020d0bb3..c977b6aec71d42d32886d48ea22c627c519e99b5 100644 (file)
@@ -1,6 +1,6 @@
 /*
  * This file is part of DisOrder
- * Copyright (C) 2005, 2006 Richard Kettlewell
+ * Copyright (C) 2005, 2006, 2007 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
@@ -17,6 +17,8 @@
  * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
  * USA
  */
+/** @file server/trackdb.c
+ * @brief Track database */
 
 #include <config.h>
 #include "types.h"
@@ -33,6 +35,8 @@
 #include <sys/time.h>
 #include <sys/resource.h>
 #include <time.h>
+#include <arpa/inet.h>
+#include <sys/wait.h>
 
 #include "event.h"
 #include "mem.h"
@@ -43,7 +47,6 @@
 #include "configuration.h"
 #include "syscalls.h"
 #include "wstat.h"
-#include "words.h"
 #include "printf.h"
 #include "filepart.h"
 #include "trackname.h"
@@ -52,6 +55,8 @@
 #include "cache.h"
 #include "eventlog.h"
 #include "hash.h"
+#include "unicode.h"
+#include "unidata.h"
 
 #define RESCAN "disorder-rescan"
 #define DEADLOCK "disorder-deadlock"
@@ -62,9 +67,10 @@ static const char *getpart(const char *track,
                            const struct kvp *p,
                            int *used_db);
 static int trackdb_alltags_tid(DB_TXN *tid, char ***taglistp);
-static int trackdb_get_global_tid(const char *name,
-                                  DB_TXN *tid,
-                                  const char **rp);
+static char **trackdb_new_tid(int *ntracksp,
+                              int maxtracks,
+                              DB_TXN *tid);
+static int trackdb_expire_noticed_tid(time_t earliest, DB_TXN *tid);
 
 const struct cache_type cache_files_type = { 86400 };
 unsigned long cache_files_hits, cache_files_misses;
@@ -73,11 +79,60 @@ unsigned long cache_files_hits, cache_files_misses;
 
 static const char *home;                /* home had better not change */
 DB_ENV *trackdb_env;                   /* db environment */
-DB *trackdb_tracksdb;                  /* the db itself */
-DB *trackdb_prefsdb;                   /* preferences */
-DB *trackdb_searchdb;                  /* the search database */
+
+/** @brief The tracks database
+ * - Keys are UTF-8(NFC(unicode(path name)))
+ * - Values are encoded key-value pairs
+ * - Data is reconstructable data about tracks that currently exist
+ */
+DB *trackdb_tracksdb;
+
+/** @brief The preferences database
+ *
+ * - Keys are UTF-8(NFC(unicode(path name)))
+ * - Values are encoded key-value pairs
+ * - Data is user data about tracks (that might not exist any more)
+ * and cannot be reconstructed
+ */
+DB *trackdb_prefsdb;
+
+/** @brief The search database
+ *
+ * - Keys are UTF-8(NFKC(casefold(search term)))
+ * - Values are UTF-8(NFC(unicode(path name)))
+ * - There can be more than one value per key
+ * - Presence of key,value means that path matches the search terms
+ * - Only tracks fond in @ref tracks_tracksdb are represented here
+ * - This database can be reconstructed, it contains no user data
+ */
+DB *trackdb_searchdb;
+
+/** @brief The tags database
+ *
+ * - Keys are UTF-8(NFKC(casefold(tag)))
+ * - Values are UTF-8(NFC(unicode(path name)))
+ * - There can be more than one value per key
+ * - Presence of key,value means that path matches the tag
+ * - This is always in sync with the tags preference
+ * - This database can be reconstructed, it contains no user data
+ */
 DB *trackdb_tagsdb;                    /* the tags database */
+
+/** @brief The global preferences database
+ * - Keys are UTF-8(NFC(preference))
+ * - Values are global preference values
+ * - Data is user data and cannot be reconstructed
+ */
 DB *trackdb_globaldb;                   /* global preferences */
+
+/** @brief The noticed database
+ * - Keys are 64-bit big-endian timestamps
+ * - Values are UTF-8(NFC(unicode(path name)))
+ * - There can be more than one value per key
+ * - Presence of key,value means that path was added at the given time
+ * - Data cannot be reconstructed (but isn't THAT important)
+ */
+DB *trackdb_noticeddb;                   /* when track noticed */
 static pid_t db_deadlock_pid = -1;      /* deadlock manager PID */
 static pid_t rescan_pid = -1;           /* rescanner PID */
 static int initialized, opened;         /* state */
@@ -92,9 +147,17 @@ static int compare(DB attribute((unused)) *db_,
   return compare_path_raw(a->data, a->size, b->data, b->size);
 }
 
-/* open environment */
-void trackdb_init(int recover) {
+/** @brief Open database environment
+ * @param flags Flags word
+ *
+ * Flags should be one of:
+ * - @ref TRACKDB_NO_RECOVER
+ * - @ref TRACKDB_NORMAL_RECOVER
+ * - @ref TRACKDB_FATAL_RECOVER
+ */
+void trackdb_init(int flags) {
   int err;
+  const int recover = flags & TRACKDB_RECOVER_MASK;
   static int recover_type[] = { 0, DB_RECOVER, DB_RECOVER_FATAL };
 
   /* sanity checks */
@@ -124,7 +187,7 @@ void trackdb_init(int recover) {
                               |DB_CREATE
                               |recover_type[recover],
                               0666)))
-    fatal(0, "trackdb_env->open: %s", db_strerror(err));
+    fatal(0, "trackdb_env->open %s: %s", config->home, db_strerror(err));
   trackdb_env->set_errpfx(trackdb_env, "DB");
   trackdb_env->set_errfile(trackdb_env, stderr);
   trackdb_env->set_verbose(trackdb_env, DB_VERB_DEADLOCK, 1);
@@ -148,39 +211,36 @@ static int reap_db_deadlock(ev_source attribute((unused)) *ev,
   return 0;
 }
 
-static pid_t subprogram(ev_source *ev, const char *prog) {
+static pid_t subprogram(ev_source *ev, const char *prog,
+                        int outputfd) {
   pid_t pid;
-  int lfd;
 
   /* If we're in the background then trap subprocess stdout/stderr */
-  if(!isatty(2))
-    lfd = logfd(ev, prog);
-  else
-    lfd = -1;
   if(!(pid = xfork())) {
     exitfn = _exit;
-    ev_signal_atfork(ev);
+    if(ev)
+      ev_signal_atfork(ev);
     signal(SIGPIPE, SIG_DFL);
-    if(lfd != -1) {
-      xdup2(lfd, 1);
-      xdup2(lfd, 2);
+    if(outputfd != -1) {
+      xdup2(outputfd, 1);
+      xclose(outputfd);
     }
     /* If we were negatively niced, undo it.  We don't bother checking for
      * error, it's not that important. */
     setpriority(PRIO_PROCESS, 0, 0);
     execlp(prog, prog, "--config", configfile,
            debugging ? "--debug" : "--no-debug",
+           log_default == &log_syslog ? "--syslog" : "--no-syslog",
            (char *)0);
     fatal(errno, "error invoking %s", prog);
   }
-  if(lfd != -1) xclose(lfd);
   return pid;
 }
 
 /* start deadlock manager */
 void trackdb_master(ev_source *ev) {
   assert(db_deadlock_pid == -1);
-  db_deadlock_pid = subprogram(ev, DEADLOCK);
+  db_deadlock_pid = subprogram(ev, DEADLOCK, -1);
   ev_child(ev, db_deadlock_pid, 0, reap_db_deadlock, 0);
   D(("started deadlock manager"));
 }
@@ -228,16 +288,86 @@ static DB *open_db(const char *path,
     if((err = db->set_bt_compare(db, compare)))
       fatal(0, "db->set_bt_compare %s: %s", path, db_strerror(err));
   if((err = db->open(db, 0, path, 0, dbtype,
-                     openflags | DB_AUTO_COMMIT, mode)))
-    fatal(0, "db->open %s: %s", path, db_strerror(err));
+                     openflags | DB_AUTO_COMMIT, mode))) {
+    if((openflags & DB_CREATE) || errno != ENOENT)
+      fatal(0, "db->open %s: %s", path, db_strerror(err));
+    db->close(db, 0);
+    db = 0;
+  }
   return db;
 }
 
-/* open track databases */
-void trackdb_open(void) {
+/** @brief Open track databases
+ * @param Flags flags word
+ *
+ * @p flags should be one of:
+ * - @p TRACKDB_NO_UPGRADE, if no upgrade should be attempted
+ * - @p TRACKDB_CAN_UPGRADE, if an upgrade may be attempted
+ * - @p TRACKDB_OPEN_FOR_UPGRADE, if this is disorder-dbupgrade
+ */
+void trackdb_open(int flags) {
+  int newdb, err;
+  pid_t pid;
+
   /* sanity checks */
   assert(opened == 0);
   ++opened;
+  /* check the database version first */
+  trackdb_globaldb = open_db("global.db", 0, DB_HASH, 0, 0666);
+  if(trackdb_globaldb) {
+    /* This is an existing database */
+    const char *s;
+    long oldversion;
+
+    s = trackdb_get_global("_dbversion");
+    /* Close the database again,  we'll open it property below */
+    if((err = trackdb_globaldb->close(trackdb_globaldb, 0)))
+      fatal(0, "error closing global.db: %s", db_strerror(err));
+    trackdb_globaldb = 0;
+    /* Convert version string to an integer */
+    oldversion = s ? atol(s) : 1;
+    if(oldversion > config->dbversion) {
+      /* Database is from the future; we never allow this. */
+      fatal(0, "this version of DisOrder is too old for database version %ld",
+            oldversion);
+    }
+    if(oldversion < config->dbversion) {
+      /* Database version is out of date */
+      switch(flags & TRACKDB_UPGRADE_MASK) {
+      case TRACKDB_NO_UPGRADE:
+        /* This database needs upgrading but this is not permitted */
+        fatal(0, "database needs upgrading from %ld to %ld",
+              oldversion, config->dbversion);
+      case TRACKDB_CAN_UPGRADE:
+        /* This database needs upgrading */
+        info("invoking disorder-dbupgrade to upgrade from %ld to %ld",
+             oldversion, config->dbversion);
+        pid = subprogram(0, "disorder-dbupgrade", -1);
+        while(waitpid(pid, &err, 0) == -1 && errno == EINTR)
+          ;
+        if(err)
+          fatal(0, "disorder-dbupgrade %s", wstat(err));
+        info("disorder-dbupgrade succeeded");
+        break;
+      case TRACKDB_OPEN_FOR_UPGRADE:
+        break;
+      default:
+        abort();
+      }
+    }
+    if(oldversion == config->dbversion && (flags & TRACKDB_OPEN_FOR_UPGRADE)) {
+      /* This doesn't make any sense */
+      fatal(0, "database is already at current version");
+    }
+    newdb = 0;
+  } else {
+    if(flags & TRACKDB_OPEN_FOR_UPGRADE) {
+      /* Cannot upgrade a new database */
+      fatal(0, "cannot upgrade a database that does not exist");
+    }
+    /* This is a brand new database */
+    newdb = 1;
+  }
   /* open the databases */
   trackdb_tracksdb = open_db("tracks.db",
                              DB_RECNUM, DB_BTREE, DB_CREATE, 0666);
@@ -247,6 +377,16 @@ void trackdb_open(void) {
                            DB_DUP|DB_DUPSORT, DB_HASH, DB_CREATE, 0666);
   trackdb_prefsdb = open_db("prefs.db", 0, DB_HASH, DB_CREATE, 0666);
   trackdb_globaldb = open_db("global.db", 0, DB_HASH, DB_CREATE, 0666);
+  trackdb_noticeddb = open_db("noticed.db",
+                             DB_DUPSORT, DB_BTREE, DB_CREATE, 0666);
+  if(newdb) {
+    /* Stash the database version */
+    char buf[32];
+
+    assert(!(flags & TRACKDB_OPEN_FOR_UPGRADE));
+    snprintf(buf, sizeof buf, "%ld", config->dbversion);
+    trackdb_set_global("_dbversion", buf, 0);
+  }
   D(("opened databases"));
 }
 
@@ -267,6 +407,8 @@ void trackdb_close(void) {
     fatal(0, "error closing prefs.db: %s", db_strerror(err));
   if((err = trackdb_globaldb->close(trackdb_globaldb, 0)))
     fatal(0, "error closing global.db: %s", db_strerror(err));
+  if((err = trackdb_noticeddb->close(trackdb_noticeddb, 0)))
+    fatal(0, "error closing noticed.db: %s", db_strerror(err));
   trackdb_tracksdb = trackdb_searchdb = trackdb_prefsdb = 0;
   trackdb_tagsdb = trackdb_globaldb = 0;
   D(("closed databases"));
@@ -485,21 +627,50 @@ static int is_display_pref(const char *name) {
   return !strncmp(name, prefix, (sizeof prefix) - 1);
 }
 
+/** @brief Word_Break property tailor that treats underscores as spaces */
+static int tailor_underscore_Word_Break_Other(uint32_t c) {
+  switch(c) {
+  default:
+    return -1;
+  case 0x005F: /* LOW LINE (SPACING UNDERSCORE) */
+    return unicode_Word_Break_Other;
+  }
+}
+
+/** @brief Normalize and split a string using a given tailoring */
+static void word_split(struct vector *v,
+                       const char *s,
+                       unicode_property_tailor *pt) {
+  size_t nw, nt32, i;
+  uint32_t *t32, **w32;
+
+  /* Convert to UTF-32 */
+  if(!(t32 = utf8_to_utf32(s, strlen(s), &nt32)))
+    return;
+  /* Erase case distinctions */
+  if(!(t32 = utf32_casefold_compat(t32, nt32, &nt32)))
+    return;
+  /* Split into words, treating _ as a space */
+  w32 = utf32_word_split(t32, nt32, &nw, pt);
+  /* Convert words back to UTF-8 and append to result */
+  for(i = 0; i < nw; ++i)
+    vector_append(v, utf32_to_utf8(w32[i], utf32_len(w32[i]), 0));
+}
+
 /* compute the words of a track name */
 static char **track_to_words(const char *track,
                              const struct kvp *p) {
   struct vector v;
-  char **w;
-  int nw;
+  const char *rootless = track_rootless(track);
 
+  if(!rootless)
+    rootless = track;                   /* bodge */
   vector_init(&v);
-  if((w = words(casefold(strip_extension(track_rootless(track))), &nw)))
-    vector_append_many(&v, w, nw);
-
+  rootless = strip_extension(rootless);
+  word_split(&v, strip_extension(rootless), tailor_underscore_Word_Break_Other);
   for(; p; p = p->next)
     if(is_display_pref(p->name))
-      if((w = words(casefold(p->value), &nw)))
-        vector_append_many(&v, w, nw);
+      word_split(&v, p->value, 0);
   vector_terminate(&v);
   return dedupe(v.vec, v.nvec);
 }
@@ -578,11 +749,15 @@ static int compute_alias(char **aliasp,
   const char *s = config->alias, *t, *expansion, *part;
   int c, used_db = 0, slash_prefix, err;
   struct kvp *at;
+  const char *const root = find_track_root(track);
 
-  if(strstr(track, "Troggs"))
-    D(("computing alias for %s", track));
+  if(!root) {
+    /* Bodge for tracks with no root */
+    *aliasp = 0;
+    return 0;
+  }
   dynstr_init(&d);
-  dynstr_append_string(&d, find_track_root(track));
+  dynstr_append_string(&d, root);
   while((c = (unsigned char)*s++)) {
     if(c != '{') {
       dynstr_append(&d, c);
@@ -615,20 +790,11 @@ static int compute_alias(char **aliasp,
    * an alias) */
   switch(err = trackdb_getdata(trackdb_tracksdb, d.vec, &at, tid)) {
   case 0:
-    if(strstr(track, "Troggs"))
-      D(("found a hit for alias"));
     if((s = kvp_get(at, "_alias_for"))
        && !strcmp(s, track)) {
     case DB_NOTFOUND:
-      if(strstr(track, "Troggs"))
-        D(("accepting anyway"));
       *aliasp = d.vec;
     } else {
-      if(strstr(track, "Troggs")) {
-        D(("rejecting"));
-        D(("%s", track));
-        D(("%s", s ? s : "(null)"));
-      }
       *aliasp = 0;
     }
     return 0;
@@ -675,7 +841,9 @@ done:
 
 /* trackdb_notice() **********************************************************/
 
-/* notice a track */
+/** @brief notice a possibly new track
+ * @return @c DB_NOTFOUND if new, 0 if already known
+ */
 int trackdb_notice(const char *track,
                    const char *path) {
   int err;
@@ -693,6 +861,12 @@ int trackdb_notice(const char *track,
   return err;
 }
 
+/** @brief notice a possibly new track
+ * @param track NFC UTF-8 track name
+ * @param path Raw path name
+ * @param tid Transaction ID
+ * @return @c DB_NOTFOUND if new, 0 if already known, @c DB_LOCK_DEADLOCK also
+ */
 int trackdb_notice_tid(const char *track,
                        const char *path,
                        DB_TXN *tid) {
@@ -700,13 +874,13 @@ int trackdb_notice_tid(const char *track,
   struct kvp *t, *a, *p;
   int t_changed, ret;
   char *alias, **w;
-  
+
   /* notice whether the tracks.db entry changes */
   t_changed = 0;
   /* get any existing tracks entry */
   if((err = gettrackdata(track, &t, &p, 0, 0, tid)) == DB_LOCK_DEADLOCK)
     return err;
-  ret = err;
+  ret = err;                            /* 0 or DB_NOTFOUND */
   /* this is a real track */
   t_changed += kvp_set(&t, "_alias_for", 0);
   t_changed += kvp_set(&t, "_path", path);
@@ -733,6 +907,24 @@ int trackdb_notice_tid(const char *track,
   /* only store the tracks.db entry if it has changed */
   if(t_changed && (err = trackdb_putdata(trackdb_tracksdb, track, t, tid, 0)))
     return err;
+  if(ret == DB_NOTFOUND) {
+    uint32_t timestamp[2];
+    time_t now;
+    DBT key, data;
+
+    time(&now);
+    timestamp[0] = htonl((uint64_t)now >> 32);
+    timestamp[1] = htonl((uint32_t)now);
+    memset(&key, 0, sizeof key);
+    key.data = timestamp;
+    key.size = sizeof timestamp;
+    switch(err = trackdb_noticeddb->put(trackdb_noticeddb, tid, &key,
+                                        make_key(&data, track), 0)) {
+    case 0: break;
+    case DB_LOCK_DEADLOCK: return err;
+    default: fatal(0, "error updating noticed.db: %s", db_strerror(err));
+    }
+  }
   return ret;
 }
 
@@ -921,7 +1113,6 @@ static int search_league(struct vector *v, int count, DB_TXN *tid) {
 char **trackdb_stats(int *nstatsp) {
   DB_TXN *tid;
   struct vector v;
-  char *s;
   
   vector_init(&v);
   for(;;) {
@@ -937,12 +1128,6 @@ char **trackdb_stats(int *nstatsp) {
     if(get_stats(&v, trackdb_prefsdb, SI(hash), tid)) goto fail;
     vector_append(&v, (char *)"");
     if(search_league(&v, 10, tid)) goto fail;
-    vector_append(&v, (char *)"");
-    vector_append(&v, (char *)"Server stats:");
-    byte_xasprintf(&s, "track lookup cache hits: %lu", cache_files_hits);
-    vector_append(&v, (char *)s);
-    byte_xasprintf(&s, "track lookup cache misses: %lu", cache_files_misses);
-    vector_append(&v, (char *)s);
     vector_terminate(&v);
     break;
 fail:
@@ -953,6 +1138,89 @@ fail:
   return v.vec;
 }
 
+struct stats_details {
+  void (*done)(char *data, void *u);
+  void *u;
+  int exited;                           /* subprocess exited */
+  int closed;                           /* pipe close */
+  int wstat;                            /* wait status from subprocess */
+  struct dynstr data[1];                /* data read from pipe */
+};
+
+static void stats_complete(struct stats_details *d) {
+  char *s;
+  
+  if(!(d->exited && d->closed))
+    return;
+  byte_xasprintf(&s, "\n"
+                 "Server stats:\n"
+                 "track lookup cache hits: %lu\n"
+                 "track lookup cache misses: %lu\n",
+                 cache_files_hits,
+                 cache_files_misses);
+  dynstr_append_string(d->data, s);
+  dynstr_terminate(d->data);
+  d->done(d->data->vec, d->u);
+}
+
+static int stats_finished(ev_source attribute((unused)) *ev,
+                          pid_t attribute((unused)) pid,
+                          int status,
+                          const struct rusage attribute((unused)) *rusage,
+                          void *u) {
+  struct stats_details *const d = u;
+
+  d->exited = 1;
+  if(status)
+    error(0, "disorder-stats %s", wstat(status));
+  stats_complete(d);
+  return 0;
+}
+
+static int stats_read(ev_source attribute((unused)) *ev,
+                      ev_reader *reader,
+                      void *ptr,
+                      size_t bytes,
+                      int eof,
+                      void *u) {
+  struct stats_details *const d = u;
+
+  dynstr_append_bytes(d->data, ptr, bytes);
+  ev_reader_consume(reader, bytes);
+  if(eof)
+    d->closed = 1;
+  stats_complete(d);
+  return 0;
+}
+
+static int stats_error(ev_source attribute((unused)) *ev,
+                       int errno_value,
+                       void *u) {
+  struct stats_details *const d = u;
+
+  error(errno_value, "error reading from pipe to disorder-stats");
+  d->closed = 1;
+  stats_complete(d);
+  return 0;
+}
+
+void trackdb_stats_subprocess(ev_source *ev,
+                              void (*done)(char *data, void *u),
+                              void *u) {
+  int p[2];
+  pid_t pid;
+  struct stats_details *d = xmalloc(sizeof *d);
+
+  dynstr_init(d->data);
+  d->done = done;
+  d->u = u;
+  xpipe(p);
+  pid = subprogram(ev, "disorder-stats", p[1]);
+  xclose(p[1]);
+  ev_child(ev, pid, 0, stats_finished, d);
+  ev_reader_new(ev, p[0], stats_read, stats_error, d, "disorder-stats reader");
+}
+
 /* set a pref (remove if value=0) */
 int trackdb_set(const char *track,
                 const char *name,
@@ -962,6 +1230,10 @@ int trackdb_set(const char *track,
   int err, cmp;
   char *oldalias, *newalias, **oldtags = 0, **newtags;
 
+  if(value) {
+    /* TODO: if value matches default then set value=0 */
+  }
+  
   for(;;) {
     tid = trackdb_begin_transaction();
     if((err = gettrackdata(track, &t, &p, 0,
@@ -1162,7 +1434,8 @@ static int tag_intersection(char **a, char **b) {
 }
 
 /* Check whether a track is suitable for random play.  Returns 0 if it is,
- * DB_NOTFOUND if it or DB_LOCK_DEADLOCK. */
+ * 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,
@@ -1172,6 +1445,12 @@ static int check_suitable(const char *track,
   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;
@@ -1273,8 +1552,7 @@ const char *trackdb_random(int tries) {
     } else {
       /* No required tags.  We pick random record numbers in the database
        * instead. */
-      switch(err = trackdb_tracksdb->stat(trackdb_tracksdb, tid, &sp,
-                                          DB_RECORDCOUNT)) {
+      switch(err = trackdb_tracksdb->stat(trackdb_tracksdb, tid, &sp, 0)) {
       case 0:
         break;
       case DB_LOCK_DEADLOCK:
@@ -1541,7 +1819,7 @@ char **trackdb_search(char **wordlist, int nwordlist, int *ntracks) {
   /* casefold all the words */
   w = xmalloc(nwordlist * sizeof (char *));
   for(n = 0; n < nwordlist; ++n) {
-    w[n] = casefold(wordlist[n]);
+    w[n] = utf8_casefold_compat(wordlist[n], strlen(wordlist[n]), 0);
     if(checktag(w[n])) ++ntags;         /* count up tags */
   }
   /* find the longest non-stopword */
@@ -1644,22 +1922,37 @@ int trackdb_scan(const char *root,
                  DB_TXN *tid) {
   DBC *cursor;
   DBT k, d;
-  size_t root_len = strlen(root);
-  int err;
+  const size_t root_len = root ? strlen(root) : 0;
+  int err, cberr;
   struct kvp *data;
+  const char *track;
 
   cursor = trackdb_opencursor(trackdb_tracksdb, tid);
-  err = cursor->c_get(cursor, make_key(&k, root), prepare_data(&d),
-                      DB_SET_RANGE);
+  if(root)
+    err = cursor->c_get(cursor, make_key(&k, root), prepare_data(&d),
+                        DB_SET_RANGE);
+  else {
+    memset(&k, 0, sizeof k);
+    err = cursor->c_get(cursor, &k, prepare_data(&d),
+                        DB_FIRST);
+  }
   while(!err) {
-    if(k.size > root_len
-       && !strncmp(k.data, root, root_len)
-       && ((char *)k.data)[root_len] == '/') {
+    if(!root
+       || (k.size > root_len
+           && !strncmp(k.data, root, root_len)
+           && ((char *)k.data)[root_len] == '/')) {
       data = kvp_urldecode(d.data, d.size);
-      if(kvp_get(data, "_path"))
-        if((err = callback(xstrndup(k.data, k.size), data, u, tid)))
+      if(kvp_get(data, "_path")) {
+        track = xstrndup(k.data, k.size);
+        /* 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))) {
+          err = cberr;
           break;
-      err = cursor->c_get(cursor, &k, &d, DB_NEXT);
+        }
+      } else
+        err = cursor->c_get(cursor, &k, &d, DB_NEXT);
     } else
       break;
   }
@@ -1693,6 +1986,7 @@ static int reap_rescan(ev_source attribute((unused)) *ev,
     D(("disorderd-rescan terminate: %s", wstat(status)));
   /* Our cache of file lookups is out of date now */
   cache_clean(&cache_files_type);
+  eventlog("rescanned", (char *)0);
   return 0;
 }
 
@@ -1701,7 +1995,7 @@ void trackdb_rescan(ev_source *ev) {
     error(0, "rescan already underway");
     return;
   }
-  rescan_pid = subprogram(ev, RESCAN);
+  rescan_pid = subprogram(ev, RESCAN, -1);
   ev_child(ev, rescan_pid, 0, reap_rescan, 0);
   D(("started rescanner"));
   
@@ -1721,27 +2015,13 @@ void trackdb_set_global(const char *name,
                         const char *value,
                         const char *who) {
   DB_TXN *tid;
-  DBT k, d;
   int err;
   int state;
 
-  memset(&k, 0, sizeof k);
-  memset(&d, 0, sizeof d);
-  k.data = (void *)name;
-  k.size = strlen(name);
-  if(value) {
-    d.data = (void *)value;
-    d.size = strlen(value);
-  }
   for(;;) {
     tid = trackdb_begin_transaction();
-    if(value)
-      err = trackdb_globaldb->put(trackdb_globaldb, tid, &k, &d, 0);
-    else
-      err = trackdb_globaldb->del(trackdb_globaldb, tid, &k, 0);
-    if(!err || err == DB_NOTFOUND) break;
-    if(err != DB_LOCK_DEADLOCK)
-      fatal(0, "error updating database: %s", db_strerror(err));
+    if(!(err = trackdb_set_global_tid(name, value, tid)))
+      break;
     trackdb_abort_transaction(tid);
   }
   trackdb_commit_transaction(tid);
@@ -1764,6 +2044,30 @@ void trackdb_set_global(const char *name,
     reqtracks = 0;
 }
 
+int trackdb_set_global_tid(const char *name,
+                           const char *value,
+                           DB_TXN *tid) {
+  DBT k, d;
+  int err;
+
+  memset(&k, 0, sizeof k);
+  memset(&d, 0, sizeof d);
+  k.data = (void *)name;
+  k.size = strlen(name);
+  if(value) {
+    d.data = (void *)value;
+    d.size = strlen(value);
+  }
+  if(value)
+    err = trackdb_globaldb->put(trackdb_globaldb, tid, &k, &d, 0);
+  else
+    err = trackdb_globaldb->del(trackdb_globaldb, tid, &k, 0);
+  if(err == DB_LOCK_DEADLOCK) return err;
+  if(err)
+    fatal(0, "error updating database: %s", db_strerror(err));
+  return 0;
+}
+
 const char *trackdb_get_global(const char *name) {
   DB_TXN *tid;
   int err;
@@ -1779,9 +2083,9 @@ const char *trackdb_get_global(const char *name) {
   return r;
 }
 
-static int trackdb_get_global_tid(const char *name,
-                                  DB_TXN *tid,
-                                  const char **rp) {
+int trackdb_get_global_tid(const char *name,
+                           DB_TXN *tid,
+                           const char **rp) {
   DBT k, d;
   int err;
 
@@ -1799,10 +2103,129 @@ static int trackdb_get_global_tid(const char *name,
   case DB_LOCK_DEADLOCK:
     return err;
   default:
-    fatal(0, "error updating database: %s", db_strerror(err));
+    fatal(0, "error reading database: %s", db_strerror(err));
   }
 }
 
+/** @brief Retrieve the most recently added tracks
+ * @param ntracksp Where to put count, or 0
+ * @param maxtracks Maximum number of tracks to retrieve
+ * @return null-terminated array of track names
+ *
+ * The most recently added track is first in the array.
+ */
+char **trackdb_new(int *ntracksp,
+                   int maxtracks) {
+  DB_TXN *tid;
+  char **tracks;
+
+  for(;;) {
+    tid = trackdb_begin_transaction();
+    tracks = trackdb_new_tid(ntracksp, maxtracks, tid);
+    if(tracks)
+      break;
+    trackdb_abort_transaction(tid);
+  }
+  trackdb_commit_transaction(tid);
+  return tracks;
+}
+
+/** @brief Retrieve the most recently added tracks
+ * @param ntracksp Where to put count, or 0
+ * @param maxtracks Maximum number of tracks to retrieve, or 0 for all
+ * @param tid Transaction ID
+ * @return null-terminated array of track names, or NULL on deadlock
+ *
+ * The most recently added track is first in the array.
+ */
+static char **trackdb_new_tid(int *ntracksp,
+                              int maxtracks,
+                              DB_TXN *tid) {
+  DBC *c;
+  DBT k, d;
+  int err = 0;
+  struct vector tracks[1];
+
+  vector_init(tracks);
+  c = trackdb_opencursor(trackdb_noticeddb, tid);
+  while((maxtracks <= 0 || tracks->nvec < maxtracks)
+        && !(err = c->c_get(c, prepare_data(&k), prepare_data(&d), DB_PREV)))
+    vector_append(tracks, xstrndup(d.data, d.size));
+  switch(err) {
+  case 0:                               /* hit maxtracks */
+  case DB_NOTFOUND:                     /* ran out of tracks */
+    break;
+  case DB_LOCK_DEADLOCK:
+    trackdb_closecursor(c);
+    return 0;
+  default:
+    fatal(0, "error reading noticed.db: %s", db_strerror(err));
+  }
+  if((err = trackdb_closecursor(c)))
+    return 0;                           /* deadlock */
+  vector_terminate(tracks);
+  if(ntracksp)
+    *ntracksp = tracks->nvec;
+  return tracks->vec;
+}
+
+/** @brief Expire noticed.db
+ * @param earliest Earliest timestamp to keep
+ */
+void trackdb_expire_noticed(time_t earliest) {
+  DB_TXN *tid;
+
+  for(;;) {
+    tid = trackdb_begin_transaction();
+    if(!trackdb_expire_noticed_tid(earliest, tid))
+      break;
+    trackdb_abort_transaction(tid);
+  }
+  trackdb_commit_transaction(tid);
+}
+
+/** @brief Expire noticed.db
+ * @param earliest Earliest timestamp to keep
+ * @param tid Transaction ID
+ * @return 0 or DB_LOCK_DEADLOCK
+ */
+static int trackdb_expire_noticed_tid(time_t earliest, DB_TXN *tid) {
+  DBC *c;
+  DBT k, d;
+  int err = 0, ret;
+  time_t when;
+  uint32_t *kk;
+  int count = 0;
+
+  c = trackdb_opencursor(trackdb_noticeddb, tid);
+  while(!(err = c->c_get(c, prepare_data(&k), prepare_data(&d), DB_NEXT))) {
+    kk = k.data;
+    when = (time_t)(((uint64_t)ntohl(kk[0]) << 32) + ntohl(kk[1]));
+    if(when >= earliest)
+      break;
+    if((err = c->c_del(c, 0))) {
+      if(err != DB_LOCK_DEADLOCK)
+        fatal(0, "error deleting expired noticed.db entry: %s",
+              db_strerror(err));
+      break;
+    }
+    ++count;
+  }
+  if(err == DB_NOTFOUND)
+    err = 0;
+  if(err && err != DB_LOCK_DEADLOCK)
+    fatal(0, "error expiring noticed.db: %s", db_strerror(err));
+  ret = err;
+  if((err = trackdb_closecursor(c))) {
+    if(err != DB_LOCK_DEADLOCK)
+      fatal(0, "error closing cursor: %s", db_strerror(err));
+    ret = err;
+  }
+  if(!ret && count)
+    info("expired %d tracks from noticed.db", count);
+  return ret;
+}
+
 /* tidying up ****************************************************************/
 
 void trackdb_gc(void) {
@@ -1830,4 +2253,3 @@ fill-column:79
 indent-tabs-mode:nil
 End:
 */
-/* arch-tag:/CBQ6uebrasgkefvjc+fHQ */