- 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;
+ 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;