X-Git-Url: http://www.chiark.greenend.org.uk/ucgi/~mdw/git/disorder/blobdiff_plain/7378b5081fde6f48140327d5c072a8231934f027..561d077df8d0494aeeaf3f30d847f489336cbfd6:/lib/trackdb.c diff --git a/lib/trackdb.c b/lib/trackdb.c index 53d7cf3..1b16445 100644 --- a/lib/trackdb.c +++ b/lib/trackdb.c @@ -2,20 +2,18 @@ * This file is part of DisOrder * Copyright (C) 2005-2008 Richard Kettlewell * - * This program is free software; you can redistribute it and/or modify + * 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 + * the Free Software Foundation, either version 3 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. - * + * + * 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 + * along with this program. If not, see . */ /** @file lib/trackdb.c * @brief Track database @@ -23,15 +21,11 @@ * This file is getting in desparate need of splitting up... */ -#include -#include "types.h" +#include "common.h" -#include -#include #include #include #include -#include #include #include #include @@ -39,7 +33,6 @@ #include #include #include -#include #include #include #include @@ -65,6 +58,7 @@ #include "unicode.h" #include "unidata.h" #include "base64.h" +#include "sendmail.h" #define RESCAN "disorder-rescan" #define DEADLOCK "disorder-deadlock" @@ -88,8 +82,14 @@ int trackdb_existing_database; /* setup and teardown ********************************************************/ -static const char *home; /* home had better not change */ -DB_ENV *trackdb_env; /* db environment */ +/** @brief Database home directory + * + * All database files live below here. It had better never change. + */ +static const char *home; + +/** @brief Database environment */ +DB_ENV *trackdb_env; /** @brief The tracks database * - Keys are UTF-8(NFC(unicode(path name))) @@ -145,6 +145,17 @@ DB *trackdb_globaldb; /* global preferences */ */ DB *trackdb_noticeddb; /* when track noticed */ +/** @brief The schedule database + * + * - Keys are ID strings, generated at random + * - Values are encoded key-value pairs + * - There can be more than one value per key + * - Data cannot be reconstructed + * + * See @ref server/schedule.c for further information. + */ +DB *trackdb_scheduledb; + /** @brief The user database * - Keys are usernames * - Values are encoded key-value pairs @@ -152,11 +163,55 @@ DB *trackdb_noticeddb; /* when track noticed */ */ DB *trackdb_usersdb; -static pid_t db_deadlock_pid = -1; /* deadlock manager PID */ -static pid_t rescan_pid = -1; /* rescanner PID */ -static int initialized, opened; /* state */ +/** @brief The playlists database + * - Keys are playlist names + * - Values are encoded key-value pairs + * - Data is user data and cannot be reconstructed + */ +DB *trackdb_playlistsdb; + +/** @brief Deadlock manager PID */ +static pid_t db_deadlock_pid = -1; + +/** @brief Rescanner PID */ +static pid_t rescan_pid = -1; + +/** @brief Set when the database environment exists */ +static int initialized; + +/** @brief Set when databases are open */ +static int opened; + +/** @brief Current stats subprocess PIDs */ +static hash *stats_pids; + +/** @brief PID of current random track chooser (disorder-choose) */ +static pid_t choose_pid = -1; + +/** @brief Our end of pipe from disorder-choose */ +static int choose_fd; + +/** @brief Callback to supply random track to */ +static random_callback *choose_callback; + +/** @brief Accumulator for output from disorder-choose */ +static struct dynstr choose_output; + +/** @brief Current completion status of disorder-choose + * A bitmap of @ref CHOOSE_READING and @ref CHOOSE_RUNNING. + */ +static unsigned choose_complete; + +/* @brief Exit status from disorder-choose */ +static int choose_status; + +/** @brief disorder-choose process is running */ +#define CHOOSE_RUNNING 1 + +/** @brief disorder-choose pipe is still open */ +#define CHOOSE_READING 2 -/* comparison function for keys */ +/** @brief Comparison function for filename-based keys */ static int compare(DB attribute((unused)) *db_, const DBT *a, const DBT *b) { return compare_path_raw(a->data, a->size, b->data, b->size); @@ -250,7 +305,7 @@ void trackdb_init(int flags) { D(("initialized database environment")); } -/* called when deadlock manager terminates */ +/** @brief Called when deadlock manager terminates */ static int reap_db_deadlock(ev_source attribute((unused)) *ev, pid_t attribute((unused)) pid, int status, @@ -265,6 +320,18 @@ static int reap_db_deadlock(ev_source attribute((unused)) *ev, return 0; } +/** @brief Start a subprogram + * @param ev Event loop + * @param outputfd File descriptor to redirect @c stdout to, or -1 + * @param prog Program name + * @param ... Arguments + * @return PID + * + * Starts a subprocess. Adds the following arguments: + * - @c --config to ensure the right config file is used + * - @c --debug or @c --no-debug to match debug settings + * - @c --syslog or @c --no-syslog to match log settings + */ static pid_t subprogram(ev_source *ev, int outputfd, const char *prog, ...) { pid_t pid; @@ -304,7 +371,11 @@ static pid_t subprogram(ev_source *ev, int outputfd, const char *prog, return pid; } -/* start deadlock manager */ +/** @brief Start deadlock manager + * @param ev Event loop + * + * Called from the main server (only). + */ void trackdb_master(ev_source *ev) { assert(db_deadlock_pid == -1); db_deadlock_pid = subprogram(ev, -1, DEADLOCK, (char *)0); @@ -312,8 +383,34 @@ void trackdb_master(ev_source *ev) { D(("started deadlock manager")); } -/* close environment */ -void trackdb_deinit(void) { +/** @brief Kill a subprocess and wait for it to terminate + * @param ev Event loop or NULL + * @param pid Process ID or -1 + * @param what Description of subprocess + * + * Used during trackdb_deinit(). This function blocks so don't use it for + * normal teardown as that will hang the server. + */ +static void terminate_and_wait(ev_source *ev, + pid_t pid, + const char *what) { + int err; + + if(pid == -1) + return; + if(kill(pid, SIGTERM) < 0) + fatal(errno, "error killing %s", what); + /* wait for the rescanner to finish */ + while(waitpid(pid, &err, 0) == -1 && errno == EINTR) + ; + if(ev) + ev_child_cancel(ev, pid); +} + +/** @brief Close database environment + * @param ev Event loop + */ +void trackdb_deinit(ev_source *ev) { int err; /* sanity checks */ @@ -324,32 +421,40 @@ void trackdb_deinit(void) { if((err = trackdb_env->close(trackdb_env, 0))) fatal(0, "trackdb_env->close: %s", db_strerror(err)); - if(rescan_pid != -1) { - /* shut down the rescanner */ - if(kill(rescan_pid, SIGTERM) < 0) - fatal(errno, "error killing rescanner"); - /* wait for the rescanner to finish */ - while(waitpid(rescan_pid, &err, 0) == -1 && errno == EINTR) - ; - } + terminate_and_wait(ev, rescan_pid, "disorder-rescan"); + rescan_pid = -1; + terminate_and_wait(ev, choose_pid, "disorder-choose"); + choose_pid = -1; - /* TODO kill any stats subprocesses */ + if(stats_pids) { + char **ks = hash_keys(stats_pids); - /* finally terminate the deadlock manager */ - if(db_deadlock_pid != -1 && kill(db_deadlock_pid, SIGTERM) < 0) - fatal(errno, "error killing deadlock manager"); - db_deadlock_pid = -1; + while(*ks) { + pid_t pid = atoi(*ks++); + terminate_and_wait(ev, pid, "disorder-stats"); + } + stats_pids = NULL; + } + terminate_and_wait(ev, db_deadlock_pid, "disorder-deadlock"); + db_deadlock_pid = -1; D(("deinitialized database environment")); } -/* open a specific database */ +/** @brief Open a specific database + * @param path Relative path to database + * @param dbflags Database flags: DB_DUP, DB_DUPSORT, etc + * @param dbtype Database type: DB_HASH, DB_BTREE, etc + * @param openflags Open flags: DB_RDONLY, DB_CREATE, etc + * @param mode Permission mask: usually 0666 + * @return Database handle + */ static DB *open_db(const char *path, u_int32_t dbflags, DBTYPE dbtype, u_int32_t openflags, int mode) { - int err; + int err, err2; DB *db; D(("open %s", path)); @@ -364,8 +469,14 @@ static DB *open_db(const char *path, 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))) { - if((openflags & DB_CREATE) || errno != ENOENT) + if((openflags & DB_CREATE) || errno != ENOENT) { + if((err2 = db->close(db, 0))) + error(0, "db->close: %s", db_strerror(err2)); + trackdb_close(); + trackdb_env->close(trackdb_env,0); + trackdb_env = 0; fatal(0, "db->open %s: %s", path, db_strerror(err)); + } db->close(db, 0); db = 0; } @@ -460,7 +571,9 @@ void trackdb_open(int flags) { trackdb_globaldb = open_db("global.db", 0, DB_HASH, dbflags, 0666); trackdb_noticeddb = open_db("noticed.db", DB_DUPSORT, DB_BTREE, dbflags, 0666); - if(!trackdb_existing_database) { + trackdb_scheduledb = open_db("schedule.db", 0, DB_HASH, dbflags, 0666); + trackdb_playlistsdb = open_db("playlists.db", 0, DB_HASH, dbflags, 0666); + if(!trackdb_existing_database && !(flags & TRACKDB_READ_ONLY)) { /* Stash the database version */ char buf[32]; @@ -471,36 +584,39 @@ void trackdb_open(int flags) { D(("opened databases")); } -/* close track databases */ +/** @brief Close track databases */ void trackdb_close(void) { int err; /* sanity checks */ assert(opened == 1); --opened; - if((err = trackdb_tracksdb->close(trackdb_tracksdb, 0))) - fatal(0, "error closing tracks.db: %s", db_strerror(err)); - if((err = trackdb_searchdb->close(trackdb_searchdb, 0))) - fatal(0, "error closing search.db: %s", db_strerror(err)); - if((err = trackdb_tagsdb->close(trackdb_tagsdb, 0))) - fatal(0, "error closing tags.db: %s", db_strerror(err)); - if((err = trackdb_prefsdb->close(trackdb_prefsdb, 0))) - 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)); - if((err = trackdb_usersdb->close(trackdb_usersdb, 0))) - fatal(0, "error closing users.db: %s", db_strerror(err)); - trackdb_tracksdb = trackdb_searchdb = trackdb_prefsdb = 0; - trackdb_tagsdb = trackdb_globaldb = 0; +#define CLOSE(N, V) do { \ + if(V && (err = V->close(V, 0))) \ + fatal(0, "error closing %s: %s", N, db_strerror(err)); \ + V = 0; \ +} while(0) + CLOSE("tracks.db", trackdb_tracksdb); + CLOSE("search.db", trackdb_searchdb); + CLOSE("tags.db", trackdb_tagsdb); + CLOSE("prefs.db", trackdb_prefsdb); + CLOSE("global.db", trackdb_globaldb); + CLOSE("noticed.db", trackdb_noticeddb); + CLOSE("schedule.db", trackdb_scheduledb); + CLOSE("users.db", trackdb_usersdb); + CLOSE("playlists.db", trackdb_playlistsdb); D(("closed databases")); } /* generic db routines *******************************************************/ -/* fetch and decode a database entry. Returns 0, DB_NOTFOUND or - * DB_LOCK_DEADLOCK. */ +/** @brief Fetch and decode a database entry + * @param db Database + * @param track Track name + * @param kp Where to put decoded list (or NULL if you don't care) + * @param tid Owning transaction + * @return 0, @c DB_NOTFOUND or @c DB_LOCK_DEADLOCK + */ int trackdb_getdata(DB *db, const char *track, struct kvp **kp, @@ -511,10 +627,12 @@ int trackdb_getdata(DB *db, switch(err = db->get(db, tid, make_key(&key, track), prepare_data(&data), 0)) { case 0: - *kp = kvp_urldecode(data.data, data.size); + if(kp) + *kp = kvp_urldecode(data.data, data.size); return 0; case DB_NOTFOUND: - *kp = 0; + if(kp) + *kp = 0; return err; case DB_LOCK_DEADLOCK: error(0, "error querying database: %s", db_strerror(err)); @@ -524,8 +642,14 @@ int trackdb_getdata(DB *db, } } -/* encode and store a database entry. Returns 0, DB_KEYEXIST or - * DB_LOCK_DEADLOCK. */ +/** @brief Encode and store a database entry + * @param db Database + * @param track Track name + * @param k List of key/value pairs to store + * @param tid Owning transaction + * @param flags DB flags e.g. DB_NOOVERWRITE + * @return 0, DB_KEYEXIST or DB_LOCK_DEADLOCK + */ int trackdb_putdata(DB *db, const char *track, const struct kvp *k, @@ -571,7 +695,11 @@ int trackdb_delkey(DB *db, } } -/* open a database cursor */ +/** @brief Open a database cursor + * @param db Database + * @param tid Owning transaction + * @return Cursor + */ DBC *trackdb_opencursor(DB *db, DB_TXN *tid) { int err; DBC *c; @@ -583,7 +711,10 @@ DBC *trackdb_opencursor(DB *db, DB_TXN *tid) { return c; } -/* close a database cursor; returns 0 or DB_LOCK_DEADLOCK */ +/** @brief Close a database cursor + * @param c Cursor + * @return 0 or DB_LOCK_DEADLOCK + */ int trackdb_closecursor(DBC *c) { int err; @@ -599,7 +730,16 @@ int trackdb_closecursor(DBC *c) { } } -/* delete a (key,data) pair. Returns 0, DB_NOTFOUND or DB_LOCK_DEADLOCK. */ +/** @brief Delete a key/data pair + * @param db Database + * @param word Key + * @param track Data + * @param tid Owning transaction + * @return 0, DB_NOTFOUND or DB_LOCK_DEADLOCK + * + * Used by the search and tags databases, hence the odd parameter names. + * See also register_word(). + */ int trackdb_delkeydata(DB *db, const char *word, const char *track, @@ -637,7 +777,9 @@ int trackdb_delkeydata(DB *db, return err; } -/* start a transaction */ +/** @brief Start a transaction + * @return Transaction + */ DB_TXN *trackdb_begin_transaction(void) { DB_TXN *tid; int err; @@ -647,7 +789,11 @@ DB_TXN *trackdb_begin_transaction(void) { return tid; } -/* abort transaction */ +/** @brief Abort transaction + * @param tid Transaction (or NULL) + * + * If @p tid is NULL then nothing happens. + */ void trackdb_abort_transaction(DB_TXN *tid) { int err; @@ -656,7 +802,9 @@ void trackdb_abort_transaction(DB_TXN *tid) { fatal(0, "tid->abort: %s", db_strerror(err)); } -/* commit transaction */ +/** @brief Commit transaction + * @param tid Transaction (must not be NULL) + */ void trackdb_commit_transaction(DB_TXN *tid) { int err; @@ -666,12 +814,26 @@ void trackdb_commit_transaction(DB_TXN *tid) { /* search/tags shared code ***************************************************/ -/* comparison function used by dedupe() */ +/** @brief Comparison function used by dedupe() + * @param a Pointer to first key + * @param b Pointer to second key + * @return -1, 0 or 1 + * + * Passed to qsort(). + */ static int wordcmp(const void *a, const void *b) { return strcmp(*(const char **)a, *(const char **)b); } -/* sort and de-dupe VEC */ +/** @brief Sort and de-duplicate @p vec + * @param vec Vector to sort + * @param nvec Length of @p vec + * @return @p vec + * + * The returned vector is NULL-terminated, and there must be room for this NULL + * even if there are no duplicates (i.e. it must have more than @p nvec + * elements.) + */ static char **dedupe(char **vec, int nvec) { int m, n; @@ -687,7 +849,17 @@ static char **dedupe(char **vec, int nvec) { return vec; } -/* update a key/track database. Returns 0 or DB_DEADLOCK. */ +/** @brief Store a key/data pair + * @param db Database + * @param what Description + * @param track Data + * @param word Key + * @param tid Owning transaction + * @return 0 or DB_DEADLOCK + * + * Used by the search and tags databases, hence the odd parameter names. + * See also trackdb_delkeydata(). + */ static int register_word(DB *db, const char *what, const char *track, const char *word, DB_TXN *tid) { @@ -709,13 +881,22 @@ static int register_word(DB *db, const char *what, /* search primitives *********************************************************/ -/* return true iff NAME is a trackname_display_ pref */ +/** @brief Return true iff @p name is a trackname_display_ pref + * @param name Preference name + * @return Non-zero iff @p name is a trackname_display_ pref + */ static int is_display_pref(const char *name) { static const char prefix[] = "trackname_display_"; return !strncmp(name, prefix, (sizeof prefix) - 1); } -/** @brief Word_Break property tailor that treats underscores as spaces */ +/** @brief Word_Break property tailor that treats underscores as spaces + * @param c Code point + * @return Tailored property or -1 to use standard value + * + * Passed to utf32_word_split() when splitting a track name into words. + * See word_split() and @ref unicode_property_tailor. + */ static int tailor_underscore_Word_Break_Other(uint32_t c) { switch(c) { default: @@ -741,7 +922,20 @@ static size_t remove_combining_chars(uint32_t *s, size_t ns) { return t - start; } -/** @brief Normalize and split a string using a given tailoring */ +/** @brief Normalize and split a string using a given tailoring + * @param v Where to store words from string + * @param s Input string + * @param pt Word_Break property tailor, or NULL + * + * The output words will be: + * - case-folded + * - have any combination characters stripped + * - not include any word break code points (as tailored) + * + * Used by track_to_words(), with @p pt set to @ref + * tailor_underscore_Word_Break_Other, and by normalize_tag() with no + * tailoring. + */ static void word_split(struct vector *v, const char *s, unicode_property_tailor *pt) { @@ -797,7 +991,11 @@ static char *normalize_tag(const char *s, size_t ns) { return d->vec; } -/* compute the words of a track name */ +/** @brief Compute the words of a track name + * @param track Track name + * @param p Preferences (for display prefs) + * @return NULL-terminated, de-duplicated list or words + */ static char **track_to_words(const char *track, const struct kvp *p) { struct vector v; @@ -815,7 +1013,10 @@ static char **track_to_words(const char *track, return dedupe(v.vec, v.nvec); } -/* return nonzero iff WORD is a stopword */ +/** @brief Test for a stopword + * @param word Word + * @return Non-zero if @p word is a stopword + */ static int stopword(const char *word) { int n; @@ -825,7 +1026,12 @@ static int stopword(const char *word) { return n < config->stopword.n; } -/* record that WORD appears in TRACK. Returns 0 or DB_LOCK_DEADLOCK. */ +/** @brief Register a search term + * @param track Track name + * @param word A word that appears in the name of @p track + * @param tid Owning transaction + * @return 0 or DB_LOCK_DEADLOCK + */ static int register_search_word(const char *track, const char *word, DB_TXN *tid) { if(stopword(word)) return 0; @@ -834,7 +1040,13 @@ static int register_search_word(const char *track, const char *word, /* Tags **********************************************************************/ -/* Return nonzero if C is a valid tag character */ +/** @brief Test for tag characters + * @param c Character + * @return Non-zero if @p c is a tag character + * + * The current rule is that commas and the control characters 0-31 are not + * allowed but anything else is permitted. This is arguably a bit loose. + */ static int tagchar(int c) { switch(c) { case ',': @@ -844,7 +1056,12 @@ static int tagchar(int c) { } } -/* Parse and de-dupe a tag list. If S=0 then assumes "". */ +/** @brief Parse a tag list + * @param s Tag list or NULL (equivalent to "") + * @return Parsed tag list + * + * The tags will be normalized (as per normalize_tag()) and de-duplicated. + */ char **parsetags(const char *s) { const char *t; struct vector v; @@ -873,7 +1090,12 @@ char **parsetags(const char *s) { return dedupe(v.vec, v.nvec); } -/* Record that TRACK has TAG. Returns 0 or DB_LOCK_DEADLOCK. */ +/** @brief Register a tag + * @param track Track name + * @param tag Tag name + * @param tid Owning transaction + * @return 0 or DB_LOCK_DEADLOCK + */ static int register_tag(const char *track, const char *tag, DB_TXN *tid) { return register_word(trackdb_tagsdb, "tags", track, tag, tid); } @@ -1014,7 +1236,8 @@ int trackdb_notice_tid(const char *track, int err, n; struct kvp *t, *a, *p; int t_changed, ret; - char *alias, **w; + char *alias, **w, *noticed; + time_t now; /* notice whether the tracks.db entry changes */ t_changed = 0; @@ -1025,6 +1248,12 @@ int trackdb_notice_tid(const char *track, /* this is a real track */ t_changed += kvp_set(&t, "_alias_for", 0); t_changed += kvp_set(&t, "_path", path); + xtime(&now); + if(ret == DB_NOTFOUND) { + /* It's a new track; record the time */ + byte_xasprintf(¬iced, "%lld", (long long)now); + t_changed += kvp_set(&t, "_noticed", noticed); + } /* if we have an alias record it in the database */ if((err = compute_alias(&alias, track, p, tid))) return err; if(alias) { @@ -1049,10 +1278,8 @@ int trackdb_notice_tid(const char *track, 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); @@ -1325,7 +1552,7 @@ static void stats_complete(struct stats_details *d) { } static int stats_finished(ev_source attribute((unused)) *ev, - pid_t attribute((unused)) pid, + pid_t pid, int status, const struct rusage attribute((unused)) *rusage, void *u) { @@ -1335,6 +1562,9 @@ static int stats_finished(ev_source attribute((unused)) *ev, if(status) error(0, "disorder-stats %s", wstat(status)); stats_complete(d); + char *k; + byte_xasprintf(&k, "%lu", (unsigned long)pid); + hash_remove(stats_pids, k); return 0; } @@ -1379,7 +1609,69 @@ void trackdb_stats_subprocess(ev_source *ev, pid = subprogram(ev, p[1], "disorder-stats", (char *)0); 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"); + if(!ev_reader_new(ev, p[0], stats_read, stats_error, d, + "disorder-stats reader")) + fatal(0, "ev_reader_new for disorder-stats reader failed"); + /* Remember the PID */ + if(!stats_pids) + stats_pids = hash_new(1); + char *k; + byte_xasprintf(&k, "%lu", (unsigned long)pid); + hash_add(stats_pids, k, "", HASH_INSERT); +} + +/** @brief Parse a track name part preference + * @param name Preference name + * @param partp Where to store part name + * @param contextp Where to store context name + * @return 0 on success, non-0 if parse fails + */ +static int trackdb__parse_namepref(const char *name, + char **partp, + char **contextp) { + char *c; + static const char prefix[] = "trackname_"; + + if(strncmp(name, prefix, strlen(prefix))) + return -1; /* not trackname_* at all */ + name += strlen(prefix); + /* There had better be a _ between context and part */ + c = strchr(name, '_'); + if(!c) + return -1; + /* Context is first in the pref name even though most APIs have the part + * first. Confusing; sorry. */ + *contextp = xstrndup(name, c - name); + ++c; + /* There had better NOT be a second _ */ + if(strchr(c, '_')) + return -1; + *partp = xstrdup(c); + return 0; +} + +/** @brief Compute the default value for a track preference + * @param track Track name + * @param name Preference name + * @return Default value or 0 if none/not known + */ +static const char *trackdb__default(const char *track, const char *name) { + char *context, *part; + + if(!trackdb__parse_namepref(name, &part, &context)) { + /* We can work out the default for a trackname_ pref */ + return trackname_part(track, context, part); + } else if(!strcmp(name, "weight")) { + /* We know the default weight */ + return "90000"; + } else if(!strcmp(name, "pick_at_random")) { + /* By default everything is eligible for picking at random */ + return "1"; + } else if(!strcmp(name, "tags")) { + /* By default everything no track has any tags */ + return ""; + } + return 0; } /* set a pref (remove if value=0) */ @@ -1390,9 +1682,15 @@ int trackdb_set(const char *track, DB_TXN *tid; int err, cmp; char *oldalias, *newalias, **oldtags = 0, **newtags; + const char *def; + /* If the value matches the default then unset instead, to keep the database + * tidy. Older versions did not have this feature so your database may yet + * have some default values stored in it. */ if(value) { - /* TODO: if value matches default then set value=0 */ + def = trackdb__default(track, name); + if(def && !strcmp(value, def)) + value = 0; } for(;;) { @@ -1592,15 +1890,6 @@ int tag_intersection(char **a, char **b) { return 0; } -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)) @@ -1675,8 +1964,9 @@ int trackdb_request_random(ev_source *ev, 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] */ + if(!ev_reader_new(ev, p[0], choose_readable, choose_read_error, 0, + "disorder-choose reader")) /* owns p[0] */ + fatal(0, "ev_reader_new for disorder-choose reader failed"); ev_child(ev, choose_pid, 0, choose_exited, 0); /* owns the subprocess */ return 0; } @@ -1778,7 +2068,7 @@ static int do_list(struct vector *v, const char *dir, char *ptr; int err; size_t l, last_dir_len = 0; - char *last_dir = 0, *track, *alias; + char *last_dir = 0, *track; struct kvp *p; dl = strlen(dir); @@ -1811,12 +2101,35 @@ static int do_list(struct vector *v, const char *dir, if((err = trackdb_getdata(trackdb_prefsdb, track, &p, tid)) == DB_LOCK_DEADLOCK) goto deadlocked; + /* There's an awkward question here... + * + * If a track shares a directory with its alias then we could + * do one of three things: + * - report both. Looks ridiculuous in most UIs. + * - report just the alias. Remarkably inconvenient to write + * UI code for! + * - report just the real name. Ugly if the UI doesn't prettify + * names via the name parts. + */ +#if 1 + /* If this file is an alias for a track in the same directory then we + * skip it */ + struct kvp *t = kvp_urldecode(d.data, d.size); + const char *alias_target = kvp_get(t, "_alias_for"); + if(!(alias_target + && !strcmp(d_dirname(alias_target), + d_dirname(track)))) + if(track_matches(dl, k.data, k.size, re)) + vector_append(v, track); +#else /* if this file has an alias in the same directory then we skip it */ + char *alias; if((err = compute_alias(&alias, track, p, tid))) goto deadlocked; if(!(alias && !strcmp(d_dirname(alias), d_dirname(track)))) if(track_matches(dl, k.data, k.size, re)) vector_append(v, track); +#endif } } err = cursor->c_get(cursor, &k, &d, DB_NEXT); @@ -2092,6 +2405,28 @@ int trackdb_scan(const char *root, /* trackdb_rescan ************************************************************/ +/** @brief Node in the list of rescan-complete callbacks */ +struct rescanned_node { + struct rescanned_node *next; + void (*rescanned)(void *ru); + void *ru; +}; + +/** @brief List of rescan-complete callbacks */ +static struct rescanned_node *rescanned_list; + +/** @brief Add a rescan completion callback */ +void trackdb_add_rescanned(void (*rescanned)(void *ru), + void *ru) { + if(rescanned) { + struct rescanned_node *n = xmalloc(sizeof *n); + n->next = rescanned_list; + n->rescanned = rescanned; + n->ru = ru; + rescanned_list = n; + } +} + /* called when the rescanner terminates */ static int reap_rescan(ev_source attribute((unused)) *ev, pid_t pid, @@ -2106,23 +2441,37 @@ static int reap_rescan(ev_source attribute((unused)) *ev, /* Our cache of file lookups is out of date now */ cache_clean(&cache_files_type); eventlog("rescanned", (char *)0); + /* Call rescanned callbacks */ + while(rescanned_list) { + void (*rescanned)(void *u_) = rescanned_list->rescanned; + void *ru = rescanned_list->ru; + + rescanned_list = rescanned_list->next; + rescanned(ru); + } return 0; } /** @brief Initiate a rescan * @param ev Event loop or 0 to block * @param recheck 1 to recheck lengths, 0 to suppress check + * @param rescanned Called on completion (if not NULL) + * @param ru Passed to @p rescanned */ -void trackdb_rescan(ev_source *ev, int recheck) { +void trackdb_rescan(ev_source *ev, int recheck, + void (*rescanned)(void *ru), + void *ru) { int w; if(rescan_pid != -1) { + trackdb_add_rescanned(rescanned, ru); error(0, "rescan already underway"); return; } rescan_pid = subprogram(ev, -1, RESCAN, recheck ? "--check" : "--no-check", (char *)0); + trackdb_add_rescanned(rescanned, ru); if(ev) { ev_child(ev, rescan_pid, 0, reap_rescan, 0); D(("started rescanner")); @@ -2142,6 +2491,11 @@ int trackdb_rescan_cancel(void) { return 1; } +/** @brief Return true if a rescan is underway */ +int trackdb_rescan_underway(void) { + return rescan_pid != -1; +} + /* global prefs **************************************************************/ void trackdb_set_global(const char *name, @@ -2276,12 +2630,24 @@ static char **trackdb_new_tid(int *ntracksp, DBT k, d; int err = 0; struct vector tracks[1]; + hash *h = hash_new(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)); + && !(err = c->c_get(c, prepare_data(&k), prepare_data(&d), DB_PREV))) { + char *const track = xstrndup(d.data, d.size); + /* Don't add any track more than once */ + if(hash_add(h, track, "", HASH_INSERT)) + continue; + /* See if the track still exists */ + err = trackdb_getdata(trackdb_tracksdb, track, NULL/*kp*/, tid); + if(err == DB_NOTFOUND) + continue; /* It doesn't, skip it */ + if(err == DB_LOCK_DEADLOCK) + break; /* Doh */ + vector_append(tracks, track); + } switch(err) { case 0: /* hit maxtracks */ case DB_NOTFOUND: /* ran out of tracks */ @@ -2393,8 +2759,10 @@ static int trusted(const char *user) { * Currently we only allow the letters and digits in ASCII. We could be more * liberal than this but it is a nice simple test. It is critical that * semicolons are never allowed. + * + * NB also used by playlist_parse_name() to validate playlist names! */ -static int valid_username(const char *user) { +int valid_username(const char *user) { if(!*user) return 0; while(*user) { @@ -2438,7 +2806,7 @@ static int create_user(const char *user, kvp_set(&k, "email", email); if(confirmation) kvp_set(&k, "confirmation", confirmation); - snprintf(s, sizeof s, "%jd", (intmax_t)time(0)); + snprintf(s, sizeof s, "%jd", (intmax_t)xtime(0)); kvp_set(&k, "created", s); return trackdb_putdata(trackdb_usersdb, user, k, tid, flags); } @@ -2563,6 +2931,7 @@ int trackdb_adduser(const char *user, user, rights, email); else info("created user '%s' with rights '%s'", user, rights); + eventlog("user_add", user, (char *)0); return 0; } } @@ -2580,6 +2949,7 @@ int trackdb_deluser(const char *user) { return -1; } info("deleted user '%s'", user); + eventlog("user_delete", user, (char *)0); return 0; } @@ -2641,8 +3011,8 @@ int trackdb_edituserinfo(const char *user, } } else if(!strcmp(key, "email")) { if(*value) { - if(!strchr(value, '@')) { - error(0, "invalid email address '%s' for user '%s'", user, value); + if(!email_valid(value)) { + error(0, "invalid email address '%s' for user '%s'", value, user); return -1; } } else @@ -2659,8 +3029,10 @@ int trackdb_edituserinfo(const char *user, if(e) { error(0, "unknown user '%s'", user); return -1; - } else + } else { + eventlog("user_edit", user, key, (char *)0); return 0; + } } /** @brief List all users @@ -2726,6 +3098,7 @@ int trackdb_confirm(const char *user, const char *confirmation, switch(e) { case 0: info("registration confirmed for user '%s'", user); + eventlog("user_confirm", user, (char *)0); return 0; case DB_NOTFOUND: error(0, "confirmation for nonexistent user '%s'", user);