1 /* dbindex.c: Database frontend for music file database.
3 Copyright (C) 2019 Michael Zucchi
5 This program is free software: you can redistribute it and/or
6 modify it under the terms of the GNU General Public License as
7 published by the Free Software Foundation, either version 3 of the
8 License, or (at your option) any later version.
10 This program is distributed in the hope that it will be useful, but
11 WITHOUT ANY WARRANTY; without even the implied warranty of
12 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
13 General Public License for more details.
15 You should have received a copy of the GNU General Public License
16 along with this program. If not, see
17 <http://www.gnu.org/licenses/>.
20 // TODO: list.size is really the next id, not size if list items are deleted
24 #include <sys/types.h>
42 #include "ez-blob-basic.h"
43 #include "dbmarshal.h"
46 void dblist_dump(dbtxn *txn, dbindex *db);
49 TODO: playlist should be linked list
51 [list] -> [0000][frst][last]
52 [list] -> [file][next][prev]
59 forward: file_by_list [list.id] -> [seq][file.id] with custom dupsort compare on [seq] only
60 reverse: list_by_file [file.id] -> [list.id][seq]
62 reverse is required to navigate the playlist properly if the sequence order changes.
63 alternative idea: just use the shuffle list as the playlist always and copy it in when necessary
66 /* Value stored in file-by-list */
72 /* Value stored in list-by-file */
79 int res; // last result
88 MDB_dbi disk_by_uuid; // key is uuid UNIQUE
90 MDB_dbi list; // playlist to name
94 MDB_dbi file_by_dir; // key is "diskid{hex}/path" UNIQUE TODO: limited to 511 bytes length
95 MDB_dbi file_by_path; // key is "diskid{hex}/path/name" UNIQUE TODO: limited to 511 bytes length
96 MDB_dbi file_by_disk; // key is diskid FOREIGN
97 MDB_dbi file_by_title; // key is title (maybe all lower case?)
98 MDB_dbi file_by_artist; // key is artist
100 MDB_dbi file_by_list; // key is list, secondary is [seq][fileid] but only sorted/keyed on [seq]
101 MDB_dbi list_by_file; // key is file, secondary is [listid][seq]
103 MDB_dbi file_by_suffix;
105 // This only works for threads not processes
106 // single writer I guess.
107 volatile uint32_t diskid;
108 volatile uint32_t listid;
109 volatile uint32_t fileid;
112 static uint32_t disk_next_id(dbindex *db) {
113 return __atomic_fetch_add(&db->diskid, 1, __ATOMIC_SEQ_CST);
116 static uint32_t list_next_id(dbindex *db) {
117 return __atomic_fetch_add(&db->listid, 1, __ATOMIC_SEQ_CST);
120 static uint32_t file_next_id(dbindex *db) {
121 return __atomic_fetch_add(&db->fileid, 1, __ATOMIC_SEQ_CST);
124 // Find the next primary key in the db
125 static uint32_t find_next_id(MDB_txn *tx, MDB_dbi db) {
127 MDB_val key = { 0 }, data = { 0 };
131 mdb_cursor_open(tx, db, &cursor);
132 r = mdb_cursor_get(cursor, &key, &data, MDB_LAST);
134 assert(key.mv_size == sizeof(id));
135 memcpy(&id, key.mv_data, sizeof(id));
139 mdb_cursor_close(cursor);
144 static int dblist_put(dbtxn *tx, dbindex *db, dblist *list, unsigned int flags);
147 cmp_uint(const MDB_val *a, const MDB_val *b) {
148 return (*(unsigned int *)a->mv_data < *(unsigned int *)b->mv_data) ? -1 :
149 *(unsigned int *)a->mv_data > *(unsigned int *)b->mv_data;
152 char *dbindex_home(void) {
153 char *home = getenv("HOME");
156 if (!home || asprintf(&path, "%s/.local/lib/playerz/index", home) < 0)
162 dbindex *dbindex_open(const char *ipath) {
163 dbindex *db = calloc(sizeof(*db), 1);
166 char *dpath = ipath ? NULL : dbindex_home();
167 const char *path = ipath ? ipath : dpath;
169 res = mdb_env_create(&db->env);
172 res = mdb_env_set_maxdbs(db->env, 16);
175 res = mdb_env_set_mapsize(db->env, 1<<28); // 256MB
178 res = mdb_env_open(db->env, path, 0, 0664);
182 res = mdb_txn_begin(db->env, NULL, 0, &tx);
187 res |= mdb_dbi_open(tx, "#meta", MDB_CREATE, &db->meta);
189 res |= mdb_dbi_open(tx, "disk", MDB_CREATE | MDB_INTEGERKEY, &db->disk);
190 res |= mdb_dbi_open(tx, "disk#uuid", MDB_CREATE, &db->disk_by_uuid);
192 res |= mdb_dbi_open(tx, "list", MDB_CREATE | MDB_INTEGERKEY, &db->list);
193 res |= mdb_dbi_open(tx, "list#name", MDB_CREATE, &db->list_by_name);
195 res |= mdb_dbi_open(tx, "file", MDB_CREATE | MDB_INTEGERKEY, &db->file);
196 res |= mdb_dbi_open(tx, "file#dir", MDB_CREATE | MDB_DUPSORT | MDB_DUPFIXED | MDB_INTEGERDUP, &db->file_by_dir);
197 res |= mdb_dbi_open(tx, "file#path", MDB_CREATE, &db->file_by_path);
198 res |= mdb_dbi_open(tx, "file#disk", MDB_CREATE | MDB_INTEGERKEY | MDB_DUPSORT | MDB_DUPFIXED | MDB_INTEGERDUP , &db->file_by_disk);
199 res |= mdb_dbi_open(tx, "file#title", MDB_CREATE | MDB_DUPSORT | MDB_DUPFIXED | MDB_INTEGERDUP, &db->file_by_title);
200 res |= mdb_dbi_open(tx, "file#artist", MDB_CREATE | MDB_DUPSORT | MDB_DUPFIXED | MDB_INTEGERDUP, &db->file_by_artist);
202 res |= mdb_dbi_open(tx, "file#list", MDB_CREATE | MDB_INTEGERKEY | MDB_DUPSORT | MDB_DUPFIXED, &db->file_by_list);
203 mdb_set_dupsort(tx, db->file_by_list, cmp_uint);
204 res |= mdb_dbi_open(tx, "list#file", MDB_CREATE | MDB_INTEGERKEY | MDB_DUPSORT | MDB_DUPFIXED, &db->list_by_file);
206 // experimental substring search
207 //res |= mdb_dbi_open(tx, "file#suffix", MDB_CREATE | MDB_DUPSORT | MDB_DUPFIXED | MDB_INTEGERDUP , &db->file_by_suffix);
208 //mdb_drop(tx, db->file_by_suffix, 1);
209 res |= mdb_dbi_open(tx, "file#suffix", MDB_CREATE | MDB_DUPSORT | MDB_DUPFIXED | MDB_INTEGERDUP , &db->file_by_suffix);
211 db->diskid = find_next_id(tx, db->disk);
212 db->listid = find_next_id(tx, db->list);
213 db->fileid = find_next_id(tx, db->file);
215 // setup system playlists
217 static const char *names[] = { "all", "all#shuffle", "jukebox", "jukebox#shuffle", "playnow", "playnow#shuffle", "history", "shit" };
218 static const char *descs[] = { "All files ordered by path", "All files shuffled", "All enqueued songs in order", "All endqueued songs shuffled", "All play-now files in order", "All play-now files shuffled", "History", "The shitlist" };
221 for (int i=0;i<sizeof(names)/sizeof(names[0]);i++) {
224 list.name = (char *)names[i];
225 list.desc = (char *)descs[i];
226 if ((tmp = dblist_add(tx, db, &list))) {
227 if (tmp == MDB_KEYEXIST)
228 db->listid = find_next_id(tx, db->list);
237 res |= mdb_txn_commit(tx);
240 printf("db setup fail: %s\n", mdb_strerror(res));
242 mdb_env_close(db->env);
246 printf("index open: '%s' disk.id=%d list.id=%d file.id=%d\n", path, db->diskid, db->listid, db->fileid);
251 printf("db setup `%s' fail: %s\n", path, mdb_strerror(res));
254 mdb_env_close(db->env);
261 int dbindex_result(dbindex *db) {
265 void dbindex_close(dbindex *db) {
267 mdb_env_close(db->env);
272 MDB_txn *dbindex_begin(dbindex *db, dbtxn *txn, int readonly) {
274 int flags = readonly ? MDB_RDONLY : 0;
276 mdb_txn_begin(db->env, txn, flags, &tx);
281 int dbindex_commit(MDB_txn *tx) {
282 int res = mdb_txn_commit(tx);
285 printf("commit failed: %s\n", mdb_strerror(res));
291 void dbindex_abort(MDB_txn *tx) {
295 int dbstate_get(MDB_txn *tx, dbindex *db, dbid_t listid, dbstate *s) {
300 sprintf(id, "state#%016x", listid);
302 key.mv_size = strlen(id);
304 db->res = mdb_get(tx, db->meta, &key, &data);
307 dbstate *p = (dbstate *)data.mv_data;
308 if (p->version != ZPL_STATE_VERSION)
312 printf("dbstate get\n");
313 printf(" state: %08x\n", p->state);
314 printf(" listd: %d\n", p->curr.listid);
315 printf(" seq: %d\n", p->curr.seq);
316 printf(" filed: %d\n", p->curr.fileid);
322 printf("dbstate get: %s\n", mdb_strerror(db->res));
328 int dbstate_del_id(MDB_txn *tx, dbindex *db, dbid_t listid) {
332 sprintf(id, "state#%016x", listid);
334 key.mv_size = strlen(id);
336 return mdb_del(tx, db->meta, &key, NULL);
339 int dbstate_put(MDB_txn *tx, dbindex *db, dbid_t listid, dbstate *s) {
341 MDB_val data = { .mv_data = s, .mv_size = sizeof(*s) };
344 sprintf(id, "state#%016x", listid);
346 key.mv_size = strlen(id);
348 s->version = ZPL_STATE_VERSION;
350 return mdb_put(tx, db->meta, &key, &data, 0);
354 static char *dbfile_path(dbfile *f) {
355 char *path = malloc(strlen(f->path) + 9);
357 sprintf(path, "%08x%s", f->diskid, f->path);
362 // retrive object from main db
363 static void *primary_get_decode(MDB_txn *tx, dbindex *db, ez_blob_desc *desc, MDB_val *key, MDB_dbi primary) {
366 db->res = mdb_get(tx, primary, key, &data);
369 void *p = ez_basic_decode(desc, (ez_blob *)&data);
371 assert(key->mv_size == sizeof(int));
373 memcpy(p, key->mv_data, sizeof(int));
380 static int primary_exists(MDB_txn *tx, dbindex *db, dbid_t id, MDB_dbi primary) {
381 MDB_val key = { .mv_data = &id, .mv_size = sizeof(id) }, dat;
383 return (db->res = mdb_get(tx, primary, &key, &dat)) == 0;
387 * Retrieve and decode data based on unique secondary key.
389 * @param secondary key to retrieve
392 static void *secondary_get_decode(MDB_txn *tx, dbindex *db, ez_blob_desc *desc, MDB_val *key, MDB_dbi primary, MDB_dbi secondary) {
395 db->res = mdb_get(tx, secondary, key, &data);
398 return primary_get_decode(tx, db, desc, &data, primary);
403 dbdisk *dbdisk_get(dbtxn *tx, dbindex *db, dbid_t diskid) {
404 MDB_val key = { .mv_data = &diskid, .mv_size = sizeof(diskid) };
406 return primary_get_decode(tx, db, DBDISK_DESC, &key, db->disk);
409 dbdisk *dbdisk_get_uuid(MDB_txn *tx, dbindex *db, const char *uuid) {
411 .mv_data = (void *)uuid,
412 .mv_size = strlen(uuid)
415 return secondary_get_decode(tx, db, DBDISK_DESC, &key, db->disk, db->disk_by_uuid);
418 void dbdisk_free(dbdisk *f) {
419 ez_blob_free(DBDISK_DESC, f);
422 int dbdisk_add(MDB_txn *tx, dbindex *db, dbdisk *d) {
427 d->id = disk_next_id(db);
428 key.mv_data = &d->id;
429 key.mv_size = sizeof(d->id);
431 data.mv_size = ez_basic_size(DBDISK_DESC, d);
433 res = mdb_put(tx, db->disk, &key, &data, MDB_NOOVERWRITE | MDB_RESERVE);
435 ez_basic_encode_raw(DBDISK_DESC, d, (ez_blob *)&data);
437 printf("db put disk fail: %s\n", mdb_strerror(res));
441 // Store secondary keys
442 data.mv_data = &d->id;
443 data.mv_size = sizeof(d->id);
446 key.mv_data = d->uuid;
447 key.mv_size = strlen(d->uuid);
449 res = mdb_put(tx, db->disk_by_uuid, &key, &data, MDB_NOOVERWRITE);
453 static int secondary_list_all(dbtxn *tx, MDB_dbi secondary, ez_array *array) {
458 if ((res = mdb_cursor_open(tx, secondary, &cursor)))
462 while ((res = mdb_cursor_get(cursor, &key, &data, op)) == 0) {
463 if (!ez_array_add(array, data.mv_data, data.mv_size)) {
469 if (res == MDB_NOTFOUND) {
472 ez_array_clear(array);
475 mdb_cursor_close(cursor);
479 static int secondary_list_key(dbtxn *tx, MDB_dbi secondary, MDB_val *key, ez_array *array) {
484 if ((res = mdb_cursor_open(tx, secondary, &cursor)))
487 int op = MDB_SET_KEY;
488 while ((res = mdb_cursor_get(cursor, key, &data, op)) == 0) {
489 if (!ez_array_add(array, data.mv_data, data.mv_size)) {
495 if (res == MDB_NOTFOUND) {
498 ez_array_clear(array);
501 mdb_cursor_close(cursor);
505 int dbdisk_del(dbtxn *tx, dbindex *db, dbdisk *disk) {
507 ez_array array = { 0 };
510 key.mv_data = &disk->id;
511 key.mv_size = sizeof(disk->id);
512 if ((res = secondary_list_key(tx, db->file_by_disk, &key, &array)))
515 dbid_t *fids = array.ea_data;
516 size_t count = array.ea_size / sizeof(*fids);
518 for (int i=0;i<count;i++) {
519 printf(" file %d\n", fids[i]);
520 if ((res = dbfile_del_id(tx, db, fids[i])))
527 key.mv_data = disk->uuid;
528 key.mv_size = strlen(disk->uuid);
529 if ((res = mdb_del(tx, db->disk_by_uuid, &key, NULL)))
533 key.mv_data = &disk->id;
534 key.mv_size = sizeof(disk->id);
535 res = mdb_del(tx, db->disk, &key, NULL);
538 ez_array_clear(&array);
542 int dbdisk_del_id(dbtxn *tx, dbindex *db, dbid_t diskid) {
543 dbdisk *d = dbdisk_get(tx, db, diskid);
546 db->res = dbdisk_del(tx, db, d);
553 dbfile *dbfile_get_path(MDB_txn *tx, dbindex *db, dbid_t diskid, const char *path) {
554 char name[strlen(path) + 9];
557 sprintf(name, "%08x", diskid);
558 strcpy(name+8, path);
561 key.mv_size = strlen(name);
563 return secondary_get_decode(tx, db, DBFILE_DESC, &key, db->file, db->file_by_path);
566 char *dbfile_full_path(dbtxn *tx, dbindex *db, dbfile *file) {
567 if (!file->full_path) {
568 dbdisk *disk = dbdisk_get(tx, db, file->diskid);
570 if ((file->full_path = malloc(strlen(disk->mount) + strlen(file->path) + 1)))
571 sprintf(file->full_path, "%s%s", disk->mount, file->path);
576 return file->full_path;
579 int dbfile_inlist(dbtxn *tx, dbindex *db, dbid_t fileid, dbid_t listid) {
580 struct dblistfile datval = { .listid = listid, .seq = 0 };
582 MDB_val key = { .mv_data = &fileid, .mv_size = sizeof(fileid) };
583 MDB_val dat = { .mv_data = &datval, .mv_size = sizeof(datval) };
586 if ((db->res = mdb_cursor_open(tx, db->list_by_file, &cursor)))
589 if ((db->res = mdb_cursor_get(cursor, &key, &dat, MDB_GET_BOTH_RANGE)))
592 is = ((struct dblistfile *)dat.mv_data)->listid == listid;
595 mdb_cursor_close(cursor);
600 static int dbfile_node_cmp(const void *ap, const void *bp) {
601 const struct dbfile_node *a = ap;
602 const struct dbfile_node *b = bp;
604 return strcmp(a->path, b->path);
607 void dbfile_node_free(struct dbfile_node *n) {
609 dbfile_free(n->file);
614 // function name/syntax?
615 // path must be of the form "/foo/bar"
616 int dbfile_node_scan_path(dbtxn *tx, dbindex *db, dbid_t diskid, const char *path, ez_tree *tree) {
619 size_t len = strlen(path) + 8;
623 ez_tree_init(tree, dbfile_node_cmp);
625 if ((res = mdb_cursor_open(tx, db->file_by_dir, &cursor)))
628 sprintf(start, "%08x%s", diskid, path);
630 // path must end in /
631 // scan all items >= path where there is only 1 / more
636 if ((res = mdb_cursor_get(cursor, &key, &dat, MDB_SET)) == 0)
637 res = mdb_cursor_get(cursor, &key, &dat, MDB_GET_MULTIPLE);
639 size_t count = dat.mv_size / sizeof(dbid_t);
640 dbid_t *files = dat.mv_data;
642 for (int i=0;i<count;i++) {
643 struct dbfile_node *node = malloc(sizeof(*node));
645 node->file = dbfile_get(tx, db, files[i]);
646 node->path = node->file->path;
647 ez_tree_put(tree, node);
649 res = mdb_cursor_get(cursor, &key, &dat, MDB_NEXT_MULTIPLE);
652 mdb_cursor_close(cursor);
654 res = res == MDB_NOTFOUND ? 0 : res;
657 ez_tree_clear(tree, (void(*)(void *))dbfile_node_free);
661 void dbfile_free(dbfile *f) {
663 ez_blob_free(DBFILE_DESC, f);
667 dbfile *dbfile_get(dbtxn *tx, dbindex *db, dbid_t fileid) {
668 MDB_val key = { .mv_data = &fileid, .mv_size = sizeof(fileid) };
672 db->res = mdb_get(tx, db->file, &key, &data);
674 //printf("dbfile_get(%d) = %d\n", fileid, db->res);
677 dbfile *p = calloc(1, sizeof(*p));
679 dbfile_decode_raw((ez_blob *)&data, p);
687 return primary_get_decode(tx, db, DBFILE_DESC, &key, db->file);
691 int dbfile_del_id(dbtxn *tx, dbindex *db, dbid_t fileid) {
692 dbfile *f = dbfile_get(tx, db, fileid);
695 db->res = dbfile_del(tx, db, f);
698 printf("no such file: %d\n", fileid);
711 static unsigned int hist_hash(const void *p) {
712 const struct hist_node *n = p;
714 return ez_hash_int32(n->key);
717 static int hist_equals(const void *p, const void *q) {
718 const struct hist_node *a = p;
719 const struct hist_node *b = q;
721 return a->key == b->key;
724 int dbfile_del(dbtxn *tx, dbindex *db, dbfile *f) {
729 // Remove secondary keys / constraints
730 dat.mv_data = &f->id;
731 dat.mv_size = sizeof(f->id);
733 // - by disk+dir+name (and unique constraint)
734 dpath = dbfile_path(f);
737 key.mv_size = strlen(dpath);
738 if ((res = mdb_del(tx, db->file_by_path, &key, &dat)))
742 char *tmp = strrchr(dpath, '/');
747 key.mv_size = tmp - dpath;
748 if ((res = mdb_del(tx, db->file_by_dir, &key, &dat))) {
749 printf("fail: file_by_dir\n");
754 key.mv_data = &f->diskid;
755 key.mv_size = sizeof(f->diskid);
756 if ((res = mdb_del(tx, db->file_by_disk, &key, &dat))) {
757 printf("fail: file_by_disk\n");
763 key.mv_data = f->title;
764 key.mv_size = strlen(f->title);
765 if ((res = mdb_del(tx, db->file_by_title, &key, &dat))) {
766 printf("fail: file_by_title\n");
773 key.mv_data = f->artist;
774 key.mv_size = strlen(f->artist);
775 if ((res = mdb_del(tx, db->file_by_artist, &key, &dat))) {
776 printf("fail: file_by_artist\n");
782 key.mv_data = &f->id;
783 key.mv_size = sizeof(f->id);
784 if ((res = mdb_del(tx, db->file, &key, NULL))) {
785 printf("fail: file\n");
789 // check lists, FIXME: cleanup
792 ez_array array = { 0 };
794 // get all lists this file is in
795 if ((res = mdb_cursor_open(tx, db->list_by_file, &cursor)))
798 res = mdb_cursor_get(cursor, &key, &dat, MDB_SET);
799 if (res == MDB_NOTFOUND) {
804 if (ez_array_add(&array, dat.mv_data, dat.mv_size))
805 res = mdb_cursor_get(cursor, &key, &dat, MDB_NEXT_DUP);
809 mdb_cursor_close(cursor);
810 if (res != MDB_NOTFOUND)
813 // delete the entry we just read
814 if ((res = mdb_del(tx, db->list_by_file, &key, NULL)))
817 struct dblistfile *files = array.ea_data;
818 int count = array.ea_size / sizeof(*files);
820 // delete all entries in the lists
821 printf("list entries: %d\n", count);
822 for (int i=0;i<count;i++) {
823 struct dbfilelist fdata = {
827 //printf("delete file %d from list %d @ %d\n", fdata.fileid, files[i].listid, fdata.seq);
828 key.mv_data = &files[i].listid;
829 key.mv_size = sizeof(files[i].listid);
830 dat.mv_data = &fdata;
831 dat.mv_size = sizeof(fdata);
832 if ((res = mdb_del(tx, db->file_by_list, &key, &dat)))
836 // update all the list counts
837 ez_set counts = EZ_INIT_SET(counts, hist_hash, hist_equals, free);
838 for (int i=0;i<count;i++) {
839 struct hist_node hk = { .key = files[i].listid };
840 struct hist_node *hn;
842 if ((hn = ez_set_get(&counts, &hk)))
844 else if ((hn = malloc(sizeof(*hn)))) {
847 ez_set_put(&counts, hn);
854 for (struct hist_node *hn = ez_set_scan_init(&counts, &scan); res == 0 && hn; hn = ez_set_scan_next(&scan)) {
855 dblist *list = dblist_get(tx, db, hn->key);
858 //printf("update '%s' %d -> %d\n", list->name, list->size, list->size - hn->count);
859 list->size -= hn->count;
860 res = dblist_put(tx, db, list, 0);
867 ez_set_clear(&counts);
869 mdb_cursor_close(cursor);
871 ez_array_clear(&array);
878 static int dbstrcmp(const char *a, const char *b) {
884 } else if (b == NULL)
890 // update file with any changed values
891 // fixes secondary indices
892 int dbfile_update(dbtxn *tx, dbindex *db, dbfile *o, dbfile *f) {
896 // Update secondary keys
897 data.mv_data = &f->id;
898 data.mv_size = sizeof(f->id);
900 // - path can't change
901 // - diskid can't change
903 if (dbstrcmp(o->artist, f->artist)) {
905 key.mv_data = o->artist;
906 key.mv_size = strlen(o->artist);
907 if ((res = mdb_del(tx, db->file_by_artist, &key, &data)))
912 key.mv_data = f->artist;
913 key.mv_size = strlen(f->artist);
914 if ((res = mdb_put(tx, db->file_by_artist, &key, &data, 0)))
919 if (dbstrcmp(o->title, f->title)) {
921 key.mv_data = o->title;
922 key.mv_size = strlen(o->title);
923 if ((res = mdb_del(tx, db->file_by_title, &key, &data)))
928 key.mv_data = f->title;
929 key.mv_size = strlen(f->title);
930 if ((res = mdb_put(tx, db->file_by_title, &key, &data, 0)))
936 key.mv_data = &f->id;
937 key.mv_size = sizeof(f->id);
939 data.mv_size = ez_basic_size(DBFILE_DESC, f);
940 if ((res = mdb_put(tx, db->file, &key, &data, MDB_RESERVE)))
943 ez_basic_encode_raw(DBFILE_DESC, f, (ez_blob *)&data);
948 int dbfile_add(MDB_txn *tx, dbindex *db, dbfile *f) {
953 // Check foreign constraints
954 if (!primary_exists(tx, db, f->diskid, db->disk)) {
955 fprintf(stderr, "FOREIGN KEY: file with unknown disk\n");
960 f->id = file_next_id(db);
961 key.mv_data = &f->id;
962 key.mv_size = sizeof(f->id);
964 data.mv_size = ez_basic_size(DBFILE_DESC, f);
965 res = mdb_put(tx, db->file, &key, &data, MDB_NOOVERWRITE | MDB_RESERVE);
967 ez_basic_encode_raw(DBFILE_DESC, f, (ez_blob *)&data);
970 printf("db put file fail: %s\n", mdb_strerror(res));
974 // Store secondary keys
975 data.mv_data = &f->id;
976 data.mv_size = sizeof(f->id);
978 // - by disk+dir+name (and unique constraint)
979 dpath = dbfile_path(f);
981 key.mv_size = strlen(dpath);
982 if ((res = mdb_put(tx, db->file_by_path, &key, &data, MDB_NOOVERWRITE))) {
983 fprintf(stderr, "UNIQUE: path on this disk exists\n");
988 char *tmp = strrchr(dpath, '/');
990 fprintf(stderr, "INTERNAL: no directory for file\n");
994 key.mv_size = tmp - dpath;
995 if ((res = mdb_put(tx, db->file_by_dir, &key, &data, MDB_NODUPDATA))) {
996 fprintf(stderr, "UNIQUE: file on this path exists\n");
1001 key.mv_data = &f->diskid;
1002 key.mv_size = sizeof(f->diskid);
1003 if ((res = mdb_put(tx, db->file_by_disk, &key, &data, 0)))
1008 key.mv_data = f->title;
1009 key.mv_size = strlen(f->title);
1010 if ((res = mdb_put(tx, db->file_by_title, &key, &data, 0)))
1016 key.mv_data = f->artist;
1017 key.mv_size = strlen(f->artist);
1018 if ((res = mdb_put(tx, db->file_by_artist, &key, &data, 0)))
1028 Player support functions
1031 // A way to iterate through a lit of files, based on an index or something else
1033 #include <sys/types.h>
1034 #include <sys/stat.h>
1039 * Check if the disk is mounted.
1041 * This is not generally absolutely reliable but is in the context of
1042 * disk-monitor managing the mounts.
1044 * It can be used for quickly discarding files that can't be mounted.
1046 * This is super-slow, don't bother using it, performing a stat on the file
1049 int dbdisk_mounted(dbdisk *disk) {
1051 // Check the device of the entries
1052 char parent[strlen(disk->mount)+1];
1053 char *slash = strrchr(parent, '/');
1056 struct stat pst, mst;
1060 // Check if it's already mounted
1061 return (stat(disk->mount, &mst) == 0
1062 && stat(parent, &pst) == 0
1063 && mst.st_dev != pst.st_rdev);
1068 // See if the directory is empty
1069 // yikes, this is slow as fuck
1070 DIR *d = opendir(disk->mount);
1076 while (entries == 0 && (de = readdir(d))) {
1077 if (strcmp(de->d_name, ".") == 0
1078 || strcmp(de->d_name, "..") == 0)
1091 * Find the next file ... in some order or some index.
1093 * This is intended to be called occasionally and not to scan the db.
1095 * Currently it goes by the path index.
1097 * should it be cyclic?
1099 * FIXME: ignore files without audio! (duration==0)
1101 * @param f file, use NULL to start at the beginning.
1103 static int dbfile_iterate(dbindex *db, dbfile **fp, char **pathp, int first, int next) {
1107 dbfile *file = NULL;
1110 mdb_txn_begin(db->env, NULL, MDB_RDONLY, &tx);
1112 if ((res = mdb_cursor_open(tx, db->file_by_path, &cursor)))
1116 Scan based on filename order
1118 char *keyval = *fp ? dbfile_path(*fp) : NULL;
1119 dbdisk *disk = *fp ? dbdisk_get(tx, db, (*fp)->diskid) : NULL;
1120 int mounted = *fp ? dbdisk_mounted(disk) : 0;
1128 key.mv_data = keyval;
1129 key.mv_size = strlen(keyval);
1131 res = mdb_cursor_get(cursor, &key, &data, MDB_SET);
1132 res = mdb_cursor_get(cursor, &key, &data, next);
1134 res = mdb_cursor_get(cursor, &key, &data, first);
1137 while (file == NULL && res == 0) {
1138 file = primary_get_decode(tx, db, DBFILE_DESC, &data, db->file);
1142 if (disk == NULL || file->diskid != disk->id) {
1144 disk = dbdisk_get(tx, db, file->diskid);
1145 mounted = dbdisk_mounted(disk);
1148 keep = keep && file->duration > 0;
1150 char path[strlen(disk->mount) + strlen(file->path) + 1];
1153 sprintf(path, "%s%s", disk->mount, file->path);
1155 keep = lstat(path, &st) == 0 && S_ISREG(st.st_mode);
1157 *pathp = strdup(path);
1168 res = mdb_cursor_get(cursor, &key, &data, next);
1174 mdb_cursor_close(cursor);
1185 * Find the next file ... in some order or some index.
1187 * This is intended to be called occasionally and not to scan the db.
1189 * Currently it goes by the path index.
1191 * should it be cyclic?
1193 * @param f file, use NULL to start at the beginning.
1195 int dbfile_next(dbindex *db, dbfile **f, char **fpath) {
1196 return dbfile_iterate(db, f, fpath, MDB_FIRST, MDB_NEXT);
1199 int dbfile_prev(dbindex *db, dbfile **f, char **fpath) {
1200 return dbfile_iterate(db, f, fpath, MDB_LAST, MDB_PREV);
1204 Scan based on secondary index.
1206 key.mv_data = &f->diskid;
1207 key.mv_size = sizeof(f->diskid);
1209 data.mv_data = &f->id;
1210 data.mv_size = sizeof(f->id);
1212 //res = mdb_cursor_get(scan->cursor, &scan->key, &scan->data, MDB_GET_BOTH);
1216 /* playlist management */
1218 // internal put command - flags might include MDB_NOOVERWRITE
1219 static int dblist_put(dbtxn *tx, dbindex *db, dblist *list, unsigned int flags) {
1220 MDB_val key = { .mv_data = &list->id, .mv_size = sizeof(list->id) };
1221 MDB_val dat = { .mv_data = NULL, .mv_size = ez_basic_size(DBLIST_DESC, list) };
1224 if ((res = mdb_put(tx, db->list, &key, &dat, flags | MDB_RESERVE)))
1227 ez_basic_encode_raw(DBLIST_DESC, list, (ez_blob *)&dat);
1232 dblist *dblist_get(dbtxn *tx, dbindex *db, dbid_t listid) {
1233 MDB_val key = { .mv_data = &listid, .mv_size = sizeof(listid) };
1235 return primary_get_decode(tx, db, DBLIST_DESC, &key, db->list);
1238 dblist *dblist_get_name(dbtxn *tx, dbindex *db, const char *name) {
1240 .mv_data = (void *)name,
1241 .mv_size = strlen(name)
1244 return secondary_get_decode(tx, db, DBLIST_DESC, &key, db->list, db->list_by_name);
1247 void dblist_free(dblist *f) {
1248 ez_blob_free(DBLIST_DESC, f);
1251 dbid_t dblistid_get_name(dbtxn *tx, dbindex *db, const char *name) {
1253 .mv_data = (void *)name,
1254 .mv_size = strlen(name)
1258 db->res = mdb_get(tx, db->list_by_name, &key, &dat);
1260 return db->res == 0 ? *(dbid_t *)dat.mv_data : 0;
1264 // put ? add ? d->id == 0 -> then add, otherwise put?
1265 int dblist_add(MDB_txn *txn, dbindex *db, dblist *list) {
1270 mdb_txn_begin(db->env, txn, 0, &tx);
1273 list->id = list_next_id(db);
1274 if ((res = dblist_put(tx, db, list, MDB_NOOVERWRITE)))
1278 dat.mv_data = &list->id;
1279 dat.mv_size = sizeof(list->id);
1282 key.mv_data = list->name;
1283 key.mv_size = strlen(list->name);
1284 if ((res = mdb_put(tx, db->list_by_name, &key, &dat, MDB_NOOVERWRITE)))
1287 return mdb_txn_commit(tx);
1293 int dblist_reset(dbtxn *tx, dbindex *db, dbid_t listid) {
1297 ez_array array = { 0 };
1298 struct dbfilelist *list;
1301 key.mv_data = &listid;
1302 key.mv_size = sizeof(listid);
1304 res = secondary_list_key(tx, db->file_by_list, &key, &array);
1305 list = array.ea_data;
1306 count = array.ea_size / sizeof(*list);
1308 printf("found %zd entries\n", count);
1309 // delete list values
1310 key.mv_data = &listid;
1311 key.mv_size = sizeof(listid);
1312 if ((res = mdb_del(tx, db->file_by_list, &key, NULL))) {
1313 printf(" delete list fail\n");
1317 // delete reverse table entries
1318 struct dblistfile entry = { .listid = listid };
1320 data.mv_data = &entry;
1321 data.mv_size = sizeof(entry);
1323 for (int i=0;i<count;i++) {
1324 key.mv_data = &list[i].fileid;
1325 key.mv_size = sizeof(list[i].fileid);
1327 entry.seq = list[i].seq;
1329 if ((res = mdb_del(tx, db->list_by_file, &key, &data))) {
1330 printf(" delete fail fid=%d -> list=%d seq=%d\n", list[i].fileid, entry.listid, entry.seq);
1331 if (res == MDB_NOTFOUND) {
1332 printf(" db inconsistent, continuing\n");
1341 printf("db clear list: %s\n", mdb_strerror(res));
1345 int dblist_del(dbtxn *txn, dbindex *db, dblist *list) {
1346 dbid_t listid = list->id;
1352 // TODO: deleting the reverse list can perform GET_BOTH_RANGE i think
1353 // TODO: merge clearing with dblist_reset
1355 mdb_txn_begin(db->env, txn, 0, &tx);
1357 key.mv_data = &listid;
1358 key.mv_size = sizeof(listid);
1359 if ((res = mdb_del(tx, db->list, &key, NULL)))
1362 res = mdb_del(tx, db->file_by_list, &key, NULL);
1363 if (res != 0 && res != MDB_NOTFOUND)
1366 res = dbstate_del_id(tx, db, listid);
1367 if (res != 0 && res != MDB_NOTFOUND)
1370 res = mdb_cursor_open(tx, db->list_by_file, &cursor);
1372 printf("delete reverse list\n");
1373 res = mdb_cursor_get(cursor, &key, &data, MDB_FIRST);
1375 struct dblistfile *value = (struct dblistfile *)data.mv_data;
1376 printf("check %d %d:%d\n", *(uint32_t*)key.mv_data, value->listid, value->seq);
1378 if (value->listid == listid) {
1380 if ((res = mdb_cursor_del(cursor, 0)))
1382 res = mdb_cursor_get(cursor, &key, &data, MDB_GET_CURRENT);
1384 res = mdb_cursor_get(cursor, &key, &data, MDB_NEXT_DUP);
1386 printf("next dup: %s\n", mdb_strerror(res));
1387 if (res == MDB_NOTFOUND) {
1388 res = mdb_cursor_get(cursor, &key, &data, MDB_NEXT);
1389 printf("next: %s\n", mdb_strerror(res));
1393 mdb_cursor_close(cursor);
1398 key.mv_data = list->name;
1399 key.mv_size = strlen(list->name);
1400 if ((res = mdb_del(tx, db->list_by_name, &key, NULL)) != 0)
1403 return mdb_txn_commit(tx);
1406 printf("db del list fail: %s\n", mdb_strerror(res));
1411 int dblist_del_id(dbtxn *tx, dbindex *db, dbid_t listid) {
1412 dblist *list = dblist_get(tx, db, listid);
1415 db->res = dblist_del(tx, db, list);
1422 // info is in/out, in=listid, fileid, out=listid, seq, fileid
1423 int dblist_add_file(MDB_txn *tx, dbindex *db, dblistcursor *info) {
1426 dblist *list = dblist_get(tx, db, info->listid);
1431 // Check file exists
1432 if (!primary_exists(tx, db, info->fileid, db->file)) {
1433 printf("FOREIGN: file doesn't exist\n");
1438 struct dbfilelist fvalue = { .seq = list->seq + 1, .fileid = info->fileid };
1439 struct dblistfile rvalue = { .listid = list->id, .seq = list->seq + 1 };
1441 key.mv_data = &list->id;
1442 key.mv_size = sizeof(list->id);
1443 dat.mv_data = &fvalue;
1444 dat.mv_size = sizeof(fvalue);
1446 if ((res = mdb_put(tx, db->file_by_list, &key, &dat, MDB_NODUPDATA)))
1449 key.mv_data = &info->fileid;
1450 key.mv_size = sizeof(info->fileid);
1451 dat.mv_data = &rvalue;
1452 dat.mv_size = sizeof(rvalue);
1454 if ((res = mdb_put(tx, db->list_by_file, &key, &dat, MDB_NODUPDATA)))
1457 // update list record with changed size/sequence
1460 res = dblist_put(tx, db, list, 0);
1462 info->seq = fvalue.seq;
1468 static void array_shuffle(dbid_t *ids, size_t count) {
1469 for (size_t i=0;i<count;i++) {
1470 size_t j = random() % (count-i);
1479 // Set a playlist to a specific sequence
1480 int dblist_update(dbtxn *tx, dbindex *db, const char *name, dbid_t *fids, size_t count) {
1481 dblist *list = dblist_get_name(tx, db, name);
1484 // if the list exists clear it, otherwise create it
1486 res = dblist_reset(tx, db, list->id);
1488 if ((list = calloc(1, sizeof(*list))) == NULL
1489 || (list->name = strdup(name)) == NULL)
1492 res = dblist_add(tx, db, list);
1498 struct dbfilelist listvalue;
1499 MDB_val listdata = { .mv_data= &listvalue, .mv_size = sizeof(listvalue) };
1500 MDB_val listid = { .mv_data = &list->id, .mv_size = sizeof(dbid_t) };
1502 struct dblistfile filevalue = { .listid = list->id };
1503 MDB_val filedata = { .mv_data= &filevalue, .mv_size = sizeof(filevalue) };
1504 MDB_val fileid = { .mv_size = sizeof(dbid_t) };
1506 for (size_t i=0;i<count;i++) {
1507 listvalue.seq = i + 1;
1508 listvalue.fileid = fids[i];
1509 if ((res = mdb_put(tx, db->file_by_list, &listid, &listdata, MDB_NODUPDATA)))
1512 fileid.mv_data = &fids[i];
1513 filevalue.seq = i + 1;
1514 if ((res = mdb_put(tx, db->list_by_file, &fileid, &filedata, MDB_NODUPDATA)))
1518 // update/fix list record
1521 res = dblist_put(tx, db, list, 0);
1524 printf("db list update: %s\n", mdb_strerror(res));
1529 // Shuffle a specific list
1532 // save shuffled version?
1533 int dblist_shuffle(dbtxn *tx, dbindex *db, const char *name) {
1537 // init sysstem lists, doesn't destroy any existing lists
1538 int dblist_init_system(dbtxn *tx, dbindex *db) {
1540 static const char *names[] = { "all", "all#shuffle", "jukebox", "jukebox#shuffle", "playnow", "playnow#shuffle", "shit" };
1541 static const char *descs[] = { "All files ordered by path", "All files shuffled", "Enqueued songs in order", "Endqueued songs shuffled", "Record of play-now files in play order", "Record of play-now files shuffled", "The shitlist" };
1542 dblist list = { 0 };
1544 for (int i=0;res == 0 && i<sizeof(names)/sizeof(names[0]);i++) {
1547 list.name = (char *)names[i];
1548 list.desc = (char *)descs[i];
1549 if ((tmp = dblist_add(tx, db, &list))) {
1550 if (tmp == MDB_KEYEXIST)
1551 db->listid = find_next_id(tx, db->list);
1559 // remove all lists and recreate the all list
1560 int dblist_reset_all(dbtxn *tx, dbindex *db) {
1564 if ((res = mdb_drop(tx, db->list, 0)) == 0
1565 && (res = mdb_drop(tx, db->list_by_name, 0)) == 0
1566 && (res = mdb_drop(tx, db->file_by_list, 0)) == 0
1567 && (res = mdb_drop(tx, db->list_by_file, 0)) == 0) {
1569 if ((res = dblist_init_system(tx, db)) == 0)
1570 res = dblist_update_all(tx, db);
1575 // refresh the all list
1576 // list ordered by path
1577 int dblist_update_all(dbtxn *tx, dbindex *db) {
1580 dbid_t *fids = NULL;
1581 ez_array array = { 0 };
1583 if ((res = secondary_list_all(tx, db->file_by_path, &array)))
1586 fids = array.ea_data;
1587 count = array.ea_size / sizeof(*fids);
1589 // Create the all list
1590 if ((res = dblist_update(tx, db, "all", fids, count)))
1593 // Create the all shuffled list
1594 array_shuffle(fids, count);
1595 res = dblist_update(tx, db, "all#shuffle", fids, count);
1598 ez_array_clear(&array);
1604 void dblist_dump(dbtxn *tx, dbindex *db) {
1610 //if (res = mdb_txn_begin(db->env, txn, MDB_RDONLY, &tx)) {
1611 // printf("failed: %s\n", mdb_strerror(res));
1616 printf("dump all lists\n");
1617 printf(" list_by_file =\n");
1618 res = mdb_cursor_open(tx, db->list_by_file, &cursor);
1619 res = mdb_cursor_get(cursor, &key, &data, MDB_FIRST);
1621 struct dblistfile *value = data.mv_data;
1622 uint32_t fid = *(uint32_t *)key.mv_data;
1624 printf(" file=%5d list=%5d seq=%5d\n", fid, value->listid, value->seq);
1626 res = mdb_cursor_get(cursor, &key, &data, MDB_NEXT_DUP);
1627 if (res == MDB_NOTFOUND)
1628 res = mdb_cursor_get(cursor, &key, &data, MDB_NEXT);
1630 mdb_cursor_close(cursor);
1632 printf(" file_by_list =\n");
1633 res = mdb_cursor_open(tx, db->file_by_list, &cursor);
1634 res = mdb_cursor_get(cursor, &key, &data, MDB_FIRST);
1636 struct dbfilelist *value = data.mv_data;
1637 uint32_t lid = *(uint32_t *)key.mv_data;
1639 printf(" list=%5d file=%5d seq=%5d\n", lid, value->fileid, value->seq);
1641 res = mdb_cursor_get(cursor, &key, &data, MDB_NEXT_DUP);
1642 if (res == MDB_NOTFOUND)
1643 res = mdb_cursor_get(cursor, &key, &data, MDB_NEXT);
1645 mdb_cursor_close(cursor);
1648 //mdb_txn_commit(tx);
1651 // only list + seq is required
1652 int dblist_del_file(MDB_txn *txn, dbindex *db, struct dblistcursor *curr) {
1657 struct dbfilelist fvalue = { .seq = curr->seq, .fileid = curr->fileid };
1658 struct dblistfile rvalue = { .listid = curr->listid, .seq = curr->seq };
1660 //printf("list_del_file: lid=%4d seq=%4d fid=%4d\n", curr->listid, curr->seq, curr->fileid);
1662 if ((res = mdb_txn_begin(db->env, txn, 0, &tx)))
1669 if (curr->seq == 0) {
1670 // No sequence, lookup (first) fileid for the list
1671 if ((res = mdb_cursor_open(tx, db->list_by_file, &cursor)))
1674 key.mv_data = &curr->fileid;
1675 key.mv_size = sizeof(curr->fileid);
1676 data.mv_data = &rvalue;
1677 data.mv_size = sizeof(rvalue);
1679 if ((res = mdb_cursor_get(cursor, &key, &data, MDB_GET_BOTH_RANGE)))
1682 fvalue.seq = curr->seq = ((struct dblistfile *)data.mv_data)->seq;
1684 //printf("list_del_file: found seq=%4d\n", curr->seq);
1688 } else if (curr->fileid == 0) {
1689 // Lookup fileid for list[seq]
1690 if ((res = mdb_cursor_open(tx, db->file_by_list, &cursor)))
1693 key.mv_data = &curr->listid;
1694 key.mv_size = sizeof(curr->listid);
1695 data.mv_data = &fvalue;
1696 data.mv_size = sizeof(fvalue);
1698 if ((res = mdb_cursor_get(cursor, &key, &data, MDB_GET_BOTH)))
1701 curr->fileid = ((struct dbfilelist *)data.mv_data)->fileid;
1703 //printf("list_del_file: found fid=%4d\n", curr->fileid);
1708 // use supplied values
1713 if (delcursor && (res = mdb_cursor_del(cursor, 0)))
1717 key.mv_data = &curr->listid;
1718 key.mv_size = sizeof(curr->listid);
1719 data.mv_data = &fvalue;
1720 data.mv_size = sizeof(fvalue);
1722 if ((res = mdb_del(tx, db->file_by_list, &key, &data)))
1727 key.mv_data = &curr->fileid;
1728 key.mv_size = sizeof(curr->fileid);
1729 data.mv_data = &rvalue;
1730 data.mv_size = sizeof(rvalue);
1732 if ((res = mdb_del(tx, db->list_by_file, &key, &data)))
1736 // update list record with changed size/sequence
1737 dblist *list = dblist_get(tx, db, curr->listid);
1739 res = dbindex_result(db);
1743 if ((res = dblist_put(tx, db, list, 0)) != 0)
1747 mdb_cursor_close(cursor);
1753 printf("list_del_file: %s\n", mdb_strerror(res));
1755 mdb_cursor_close(cursor);
1764 int dbfile_searchx(dbindex *db, const char *pattern, dbfile **results, int maxlen);
1766 // TODO: should run over title index instead?
1767 int dbfile_searchx(dbindex *db, const char *pattern, dbfile **results, int maxlen) {
1774 printf("search, pattern='%s'\n", pattern);
1775 res = regcomp(®, pattern, REG_EXTENDED | REG_ICASE | REG_NOSUB);
1779 mdb_txn_begin(db->env, NULL, MDB_RDONLY, &tx);
1781 if ((res = mdb_cursor_open(tx, db->file, &cursor)))
1784 int next = MDB_FIRST;
1786 while (i < maxlen && (res = mdb_cursor_get(cursor, &key, &data, next)) == 0) {
1788 dbfile *file = ez_basic_decode(DBFILE_DESC, (ez_blob *)&data);
1790 if (regexec(®, file->title, 0, NULL, 0) == 0) {
1791 file->id = *(int *)key.mv_data;
1792 results[i++] = file;
1801 mdb_cursor_close(cursor);
1814 // run over title index
1815 // TODO: use multi-get on the values, so only match key (title) once per duplicate data.
1816 // TODO: use dbscan interface?
1817 int dbfile_search(dbindex *db, const char *pattern, dbfile **results, int maxlen) {
1824 // empty search is empty result
1828 printf("search, pattern='%s'\n", pattern);
1829 res = regcomp(®, pattern, REG_EXTENDED | REG_ICASE | REG_NOSUB);
1833 mdb_txn_begin(db->env, NULL, MDB_RDONLY, &tx);
1835 if ((res = mdb_cursor_open(tx, db->file_by_title, &cursor)))
1838 int next = MDB_FIRST;
1840 while (i < maxlen && (res = mdb_cursor_get(cursor, &key, &data, next)) == 0) {
1841 int fileid = *(int *)data.mv_data;
1842 char title[key.mv_size + 1];
1844 memcpy(title, key.mv_data, key.mv_size);
1845 title[key.mv_size] = 0;
1848 dbfile *file = dbfile_get(tx, db, fileid);
1849 printf("title %s path %s disk %d\n", title, file->path, file->diskid);
1853 if (regexec(®, title, 0, NULL, 0) == 0) {
1854 results[i++] = dbfile_get(tx, db, fileid);
1857 next = MDB_NEXT_NODUP;
1861 mdb_cursor_close(cursor);
1874 void dump_lists(dbindex *db, dbtxn *txn);
1875 void dump_lists(dbindex *db, dbtxn *txn) {
1881 res = mdb_txn_begin(db->env, txn, 0, &tx);
1882 printf("begin txn (%d): %s\n", res, mdb_strerror(res));
1884 res = mdb_cursor_open(tx, db->file_by_list, &cursor);
1885 printf("open cursor (%d): %s\n", res, mdb_strerror(res));
1887 printf("file by list\n");
1888 res = mdb_cursor_get(cursor, &key, &data, MDB_FIRST);
1890 int *keyp = key.mv_data;
1891 struct dbfilelist *valp = data.mv_data;
1893 printf("listid=%d %p { seq = %d fileid=%d }\n", *keyp, valp, valp->seq, valp->fileid);
1894 res = mdb_cursor_get(cursor, &key, &data, MDB_NEXT);
1897 mdb_cursor_close(cursor);
1900 res = mdb_cursor_open(tx, db->list_by_file, &cursor);
1901 printf("open cursor (%d): %s\n", res, mdb_strerror(res));
1903 printf("list by file\n");
1904 res = mdb_cursor_get(cursor, &key, &data, MDB_FIRST);
1906 int *keyp = key.mv_data;
1907 struct dblistfile *valp = data.mv_data;
1909 printf("fileid=%d %p { listid=%d seq=%d }\n", *keyp, valp, valp->listid, valp->seq);
1910 res = mdb_cursor_get(cursor, &key, &data, MDB_NEXT);
1913 mdb_cursor_close(cursor);
1918 void dbindex_dump(dbindex *db);
1919 void dbindex_dump(dbindex *db) {
1922 printf("Raw Dump\n");
1923 mdb_txn_begin(db->env, NULL, MDB_RDONLY, &tx);
1927 MDB_val key = { 0 }, data = { 0 };
1930 res = mdb_cursor_open(tx, db->disk, &cursor);
1931 res = mdb_cursor_get(cursor, &key, &data, MDB_FIRST);
1933 printf(" disks: none\n");
1935 printf(" disks:\n");
1937 dbdisk *p = ez_basic_decode(DBDISK_DESC, (ez_blob *)&data);
1938 p->id = *(int *)key.mv_data;
1939 printf(" id=%4d flags=%08x uuid=%s label=%-30s mount=%s\n",
1940 p->id, p->flags, p->uuid, p->label, p->mount);
1941 ez_blob_free(DBDISK_DESC, p);
1942 res= mdb_cursor_get(cursor, &key, &data, MDB_NEXT);
1944 mdb_cursor_close(cursor);
1950 MDB_val key = { 0 }, data = { 0 };
1953 mdb_cursor_open(tx, db->file, &cursor);
1954 res = mdb_cursor_get(cursor, &key, &data, MDB_FIRST);
1956 printf(" files: none\n");
1958 printf(" files:\n");
1960 dbfile *p = ez_basic_decode(DBFILE_DESC, (ez_blob *)&data);
1961 p->id = *(int *)key.mv_data;
1962 printf(" id=%4d diskid=%4d path=%-30s title=%-30s artist=%s\n",
1963 p->id, p->diskid, p->path, p->title, p->artist);
1964 ez_blob_free(DBFILE_DESC, p);
1965 res = mdb_cursor_get(cursor, &key, &data, MDB_NEXT);
1967 mdb_cursor_close(cursor);
1972 /* new dbscan version for for loops*/
1973 // for (foo = dbscan_foo(); foo; foo = dbscan_foo_next()) {
1977 static void *dbscan_decode(dbscan *scan) {
1978 void *memory = calloc(scan->DESC->bd_offset, 1);
1981 scan->decode_raw((const ez_blob *)&scan->dat, memory);
1982 *(dbid_t *)memory = *(dbid_t *)scan->key.mv_data;
1990 void dbscan_free(dbscan *scan) {
1992 mdb_cursor_close(scan->cursor);
1993 memset(scan, 0, sizeof(*scan));
1996 static void *dbscan_primary_first(dbtxn *tx, dbscan *scan, dbindex *db, MDB_dbi primary, ez_blob_desc *desc, void (*decode_raw)(const ez_blob *blob, void *p), MDB_cursor_op first) {
1998 scan->primary = primary;
2000 scan->decode_raw = decode_raw;
2003 if ((scan->res = mdb_cursor_open(tx, primary, &scan->cursor)))
2005 if ((scan->res = mdb_cursor_get(scan->cursor, &scan->key, &scan->dat, first)))
2008 return dbscan_decode(scan);
2013 static void *dbscan_secondary_first(dbtxn *tx, dbscan *scan, dbindex *db, MDB_dbi primary, MDB_dbi secondary, ez_blob_desc *desc, void (*decode_raw)(const ez_blob *blob, void *p), MDB_cursor_op first) {
2015 scan->primary = primary;
2017 scan->decode_raw = decode_raw;
2020 if ((scan->res = mdb_cursor_open(tx, secondary, &scan->cursor)))
2022 if ((scan->res = mdb_cursor_get(scan->cursor, &scan->key, &scan->dat, first)))
2024 scan->key = scan->dat;
2025 if ((scan->res = mdb_get(tx, primary, &scan->key, &scan->dat)))
2028 return dbscan_decode(scan);
2033 static void *dbscan_table_next(dbscan *scan) {
2034 if (scan->mode == 0) {
2035 if ((scan->res = mdb_cursor_get(scan->cursor, &scan->key, &scan->dat, MDB_NEXT)))
2038 if ((scan->res = mdb_cursor_get(scan->cursor, &scan->key, &scan->dat, MDB_NEXT_DUP)))
2040 scan->key = scan->dat;
2041 if ((scan->res = mdb_get(scan->tx, scan->primary, &scan->key, &scan->dat)))
2045 return dbscan_decode(scan);
2050 dbdisk *dbscan_disk(dbtxn *tx, dbscan *scan, dbindex *db, dbid_t diskid) {
2051 scan->key.mv_data = &diskid;
2052 scan->key.mv_size = sizeof(diskid);
2053 return dbscan_primary_first(tx, scan, db, db->disk, DBDISK_DESC, dbdisk_decode_raw, diskid == 0 ? MDB_FIRST : MDB_SET_RANGE);
2056 dbdisk *dbscan_disk_next(dbscan *scan) {
2057 return dbscan_table_next(scan);
2060 dbfile *dbscan_file(dbtxn *tx, dbscan *scan, dbindex *db, dbid_t fileid) {
2061 scan->key.mv_data = &fileid;
2062 scan->key.mv_size = sizeof(fileid);
2063 return dbscan_primary_first(tx, scan, db, db->file, DBFILE_DESC, dbfile_decode_raw, fileid == 0 ? MDB_FIRST : MDB_SET_RANGE);
2066 dbfile *dbscan_file_by_disk(dbtxn *tx, dbscan *scan, dbindex *db, dbid_t diskid) {
2067 scan->key.mv_data = &diskid;
2068 scan->key.mv_size = sizeof(diskid);
2069 return dbscan_secondary_first(tx, scan, db, db->file, db->file_by_disk, DBFILE_DESC, dbfile_decode_raw, MDB_SET);
2072 dbfile *dbscan_file_next(dbscan *scan) {
2073 return dbscan_table_next(scan);
2076 dblist *dbscan_list(dbtxn *tx, dbscan *scan, dbindex *db, dbid_t listid) {
2077 return dbscan_primary_first(tx, scan, db, db->list, DBLIST_DESC, dblist_decode_raw, listid == 0 ? MDB_FIRST : MDB_SET_RANGE);
2080 dblist *dbscan_list_next(dbscan *scan) {
2081 return dbscan_table_next(scan);
2085 * Init playlist scan from given position.
2087 * If listid == 0 then the scan order is based on the file_by_path index. The fileid must be supplied.
2088 * If listid != 0 then the scan order is based on the playlist. The seq should be supplied. The fileid is optional and if supplied will be used to find next occurance (>= seq) of the file.
2091 dbfile *dbscan_list_entry(dbtxn *tx, dbscan *scan, dbindex *db, dblistcursor *info) {
2092 dbid_t listid = info->listid;
2093 uint32_t seq = info->seq;
2094 dbid_t fileid = info->fileid;
2097 scan->DESC = DBFILE_DESC;
2098 scan->decode_raw = dbfile_decode_raw;
2099 scan->primary = db->file;
2101 scan->list_entry = *info;
2103 if ((scan->res = mdb_cursor_open(tx, db->file_by_list, &scan->cursor)))
2106 // TODO: perhaps if seq is non-zero and fileid is non-zero, lookup next entry of file
2107 if (seq == 0 && fileid != 0) {
2108 struct dblistfile thing = { .listid = listid, .seq = seq };
2111 scan->key = (MDB_val){ sizeof(fileid), &fileid };
2112 scan->dat.mv_data = &thing;
2113 scan->dat.mv_size = sizeof(thing);
2115 if ((scan->res = mdb_cursor_open(tx, db->list_by_file, &cursor)))
2117 if ((scan->res = mdb_cursor_get(cursor, &scan->key, &scan->dat, MDB_GET_BOTH_RANGE)) == 0)
2118 seq = ((struct dblistfile *)scan->dat.mv_data)->seq;
2120 mdb_cursor_close(cursor);
2123 mdb_cursor_close(cursor);
2126 struct dbfilelist thing = { .seq = seq };
2128 scan->key = (MDB_val){ sizeof(listid), &listid };
2129 scan->dat.mv_data = &thing;
2130 scan->dat.mv_size = sizeof(thing);
2132 if ((scan->res = mdb_cursor_get(scan->cursor, &scan->key, &scan->dat, MDB_GET_BOTH_RANGE)))
2135 struct dbfilelist *value = scan->dat.mv_data;
2137 scan->list_entry.seq = value->seq;
2138 scan->list_entry.fileid = value->fileid;
2140 scan->key.mv_data = &scan->list_entry.fileid;
2141 scan->key.mv_size = sizeof(scan->list_entry.fileid);
2143 if ((scan->res = mdb_get(tx, db->file, &scan->key, &scan->dat)))
2146 dbfile *file = dbscan_decode(scan);
2149 info->seq = scan->list_entry.seq;
2150 info->fileid = scan->list_entry.fileid;
2157 static dbfile *scan_list_entry_next(dbscan *scan, dblistcursor *info, MDB_cursor_op next0, MDB_cursor_op next1) {
2160 if ((scan->res = mdb_cursor_get(scan->cursor, &key, &dat, next1)))
2163 struct dbfilelist *value = dat.mv_data;
2165 scan->list_entry.seq = value->seq;
2166 scan->list_entry.fileid = value->fileid;
2168 scan->key.mv_data = &scan->list_entry.fileid;
2169 scan->key.mv_size = sizeof(scan->list_entry.fileid);
2171 if ((scan->res = mdb_get(scan->tx, scan->primary, &scan->key, &scan->dat)))
2174 dbfile *file = dbscan_decode(scan);
2177 info->seq = scan->list_entry.seq;
2178 info->fileid = scan->list_entry.fileid;
2185 dbfile *dbscan_list_entry_next(dbscan *scan, dblistcursor *info) {
2186 return scan_list_entry_next(scan, info, MDB_NEXT, MDB_NEXT_DUP);
2189 dbfile *dbscan_list_entry_prev(dbscan *scan, dblistcursor *info) {
2190 return scan_list_entry_next(scan, info, MDB_PREV, MDB_PREV_DUP);
2194 int dbfile_clear_suffix(MDB_txn *tx, dbindex *db) {
2195 return mdb_drop(tx, db->file_by_suffix, 0);
2198 int dbfile_put_suffix(MDB_txn *tx, dbindex *db, const char *suffix, uint32_t fileid) {
2199 MDB_val key = { .mv_data = (char *)suffix, .mv_size = strlen(suffix) };
2200 MDB_val data = { .mv_data = &fileid, .mv_size = sizeof(fileid) };
2202 return mdb_put(tx, db->file_by_suffix, &key, &data, MDB_NODUPDATA);
2205 static void printval(MDB_val key) {
2206 char match[key.mv_size+1];
2208 memcpy(match, key.mv_data, key.mv_size);
2209 match[key.mv_size] = 0;
2211 printf("%s\n", match);
2214 size_t dbfile_search_substring(dbtxn *tx, dbindex *db, const char *sub) {
2216 size_t len = strlen(sub);
2217 MDB_val key = { .mv_data = (char *)sub, .mv_size = len };
2222 if ((res = mdb_cursor_open(tx, db->file_by_suffix, &cursor)))
2225 res = mdb_cursor_get(cursor, &key, &data, MDB_SET_RANGE);
2230 while (res == 0 && key.mv_size >= len && strncmp(sub, key.mv_data, len) == 0) {
2231 //while (res == 0) {
2232 int step = MDB_GET_MULTIPLE;
2234 while ( (res = mdb_cursor_get(cursor, &key, &data, step)) == 0 ) {
2235 uint32_t *fileids = data.mv_data;
2236 uint32_t *endids = data.mv_data + data.mv_size;
2238 printf("multiple: %zd\n", endids - fileids);
2240 // FIXME: need to merge
2241 while (fileids < endids) {
2242 dbfile *file = dbfile_get(tx, db, *fileids);
2243 printf(" %8d %s\n", *fileids++, file->title);
2247 step = MDB_NEXT_MULTIPLE;
2249 res = mdb_cursor_get(cursor, &key, &data, MDB_NEXT);
2254 if (res == MDB_NOTFOUND)
2259 static int cmp_fid(const void *ap, const void *bp) {
2260 return *(const int32_t *)ap - *(const int32_t *)bp;
2265 static int check_path(const dbfile *file, const MDB_val *key) {
2267 sprintf(tmp, "%08x", file->diskid);
2268 return key->mv_size == strlen(file->path) + 8
2269 && memcmp(tmp, key->mv_data, 8) == 0
2270 && memcmp(file->path, key->mv_data + 8, key->mv_size - 8) == 0;
2272 static int check_diskid(const dbfile *file, const MDB_val *key) {
2273 return key->mv_size == sizeof(dbid_t)
2274 && *(dbid_t*)key->mv_data == file->diskid;
2276 static int check_title(const dbfile *file, const MDB_val *key) {
2277 return key->mv_size == strlen(file->title)
2278 && memcmp(file->title, key->mv_data, key->mv_size) == 0;
2280 static int check_artist(const dbfile *file, const MDB_val *key) {
2281 return key->mv_size == strlen(file->artist)
2282 && memcmp(file->artist, key->mv_data, key->mv_size) == 0;
2284 static int check_dir(const dbfile *file, const MDB_val *key) {
2285 char *slash = strrchr(file->path, '/');
2287 sprintf(tmp, "%08x", file->diskid);
2288 return slash != NULL
2289 && key->mv_size == (slash - file->path) + 8
2290 && memcmp(tmp, key->mv_data, 8) == 0
2291 && memcmp(file->path, key->mv_data + 8, (slash - file->path)) == 0;
2294 int dbindex_validate(dbindex *db) {
2301 mdb_txn_begin(db->env, NULL, MDB_RDONLY, &tx);
2303 ez_array array = { 0 };
2305 mdb_cursor_open(tx, db->file, &cursor);
2306 for (int next = MDB_FIRST; (res = mdb_cursor_get(cursor, &key, &data, next)) == 0; next = MDB_NEXT)
2307 ez_array_add(&array, key.mv_data, key.mv_size);
2308 mdb_cursor_close(cursor);
2310 uint32_t *fids = array.ea_data;
2311 size_t fids_size = array.ea_size / sizeof(*fids);
2313 printf("read %zd files\n", fids_size);
2314 qsort(fids, fids_size, sizeof(*fids), cmp_fid);
2316 // Check secondary indices
2317 MDB_dbi tables[] = {
2324 int (*checks[])(const dbfile *file, const MDB_val *key) = {
2332 for (int i=0;i<5;i++) {
2334 //printf("table %d\n", i);
2335 mdb_cursor_open(tx, tables[i], &cursor);
2336 res = mdb_cursor_get(cursor, &key, &data, MDB_FIRST);
2338 uint32_t fid = *(uint32_t*)data.mv_data;
2342 if (!bsearch(&fid, fids, fids_size, sizeof(*fids), cmp_fid)) {
2343 printf("table %d references missing file\n", i);
2347 dbfile *file = dbfile_get(tx, db, fid);
2349 if (!checks[i](file, &key)) {
2350 printf("failed field check %d\n", i);
2358 res = mdb_cursor_get(cursor, &key, &data, MDB_NEXT_DUP);
2359 if (res == MDB_NOTFOUND)
2360 res = mdb_cursor_get(cursor, &key, &data, MDB_NEXT);
2362 mdb_cursor_close(cursor);
2364 if (i <= 1 && count != fids_size) {
2365 printf("file by [thing] miscount %zd != %zd\n", count, fids_size);
2370 ez_array_clear(&array);
2376 int dbindex_validate_lists(dbindex *db);
2377 int dbindex_validate_lists(dbindex *db) {
2383 ez_set list_counts = EZ_INIT_SET(counts, hist_hash, hist_equals, free);
2384 ez_array lidsa = EZ_INIT_ARRAY(lidsa);
2388 mdb_txn_begin(db->env, NULL, MDB_RDONLY, &tx);
2391 if ((res = mdb_cursor_open(tx, db->list, &cursor)) != 0)
2393 for (int next = MDB_FIRST; (res = mdb_cursor_get(cursor, &key, &data, next)) == 0; next = MDB_NEXT)
2394 ez_array_add(&lidsa, key.mv_data, key.mv_size);
2395 mdb_cursor_close(cursor);
2396 if (res != MDB_NOTFOUND)
2400 lids = lidsa.ea_data;
2401 lids_size = lidsa.ea_size / sizeof(*lids);
2403 for (int i=0;i<lids_size;i++) {
2404 struct hist_node *hn = malloc(sizeof(*hn));
2408 ez_set_put(&list_counts, hn);
2411 MDB_cursor *fcursor;
2412 MDB_cursor *flcursor;
2413 MDB_cursor *lfcursor;
2415 if ((res = mdb_cursor_open(tx, db->file, &fcursor)) != 0)
2417 if ((res = mdb_cursor_open(tx, db->file_by_list, &flcursor)) != 0)
2419 if ((res = mdb_cursor_open(tx, db->list_by_file, &lfcursor)) != 0)
2422 // Check file_by_list and inverse
2423 for (int op = MDB_FIRST; (res = mdb_cursor_get(flcursor, &key, &data, op)) == 0; op = MDB_NEXT) {
2424 struct dbfilelist fvalue;
2427 assert(key.mv_size == sizeof(dbid_t));
2428 assert(data.mv_size == sizeof(fvalue));
2430 lid = *((dbid_t *)key.mv_data);
2431 memcpy(&fvalue, data.mv_data, sizeof(fvalue));
2433 printf(" lid=%4d -> seq=%5d fid=%5d\n", lid, fvalue.seq, fvalue.fileid);
2435 // TODO: check seq is unique (enforced by db i guess)
2437 struct hist_node hk = { .key = lid };
2438 struct hist_node *hn = ez_set_get(&list_counts, &hk);
2440 printf("file_by_list: listid foreign key fail\n");
2445 // check file exists
2446 key.mv_data = &fvalue.fileid;
2447 key.mv_size = sizeof(dbid_t);
2448 res = mdb_cursor_get(fcursor, &key, &data, MDB_SET_KEY);
2450 printf("file_by_list: fileid missing\n");
2455 struct dblistfile lvalue = { .listid = lid, .seq = fvalue.seq };
2457 key.mv_data = &fvalue.fileid;
2458 key.mv_size = sizeof(fvalue.fileid);
2459 data.mv_data = &lvalue;
2460 data.mv_size = sizeof(lvalue);
2462 res = mdb_cursor_get(lfcursor, &key, &data, MDB_GET_BOTH);
2464 printf("file_by_list: list_by_file mismatch\n");
2468 if (res != MDB_NOTFOUND)
2472 // Check list_by_file and inverse
2473 for (int op = MDB_FIRST; (res = mdb_cursor_get(lfcursor, &key, &data, op)) == 0; op = MDB_NEXT) {
2474 struct dblistfile lvalue;
2477 assert(key.mv_size == sizeof(dbid_t));
2478 assert(data.mv_size == sizeof(lvalue));
2480 fid = *((dbid_t *)key.mv_data);
2481 memcpy(&lvalue, data.mv_data, sizeof(lvalue));
2483 printf(" fid=%4d -> seq=%5d lid=%5d\n", fid, lvalue.seq, lvalue.listid);
2485 struct hist_node hk = { .key = lvalue.listid };
2486 struct hist_node *hn = ez_set_get(&list_counts, &hk);
2488 printf("list_by_file: listid foreign key fail\n");
2493 // check file exists
2495 key.mv_size = sizeof(dbid_t);
2496 res = mdb_cursor_get(fcursor, &key, &data, MDB_SET_KEY);
2498 printf("file_by_list: fileid missing\n");
2503 struct dbfilelist fvalue = { .seq = lvalue.seq /* .fileid ignored */ };
2505 key.mv_data = &lvalue.listid;
2506 key.mv_size = sizeof(lvalue.listid);
2507 data.mv_data = &fvalue;
2508 data.mv_size = sizeof(fvalue);
2510 res = mdb_cursor_get(flcursor, &key, &data, MDB_GET_BOTH);
2512 printf("list_by_file: file_by_list mismatch, missing seq: %s\n", mdb_strerror(res));
2516 fvalue = *((struct dbfilelist *)data.mv_data);
2517 if (fid != fvalue.fileid) {
2519 printf("list_by_file: file_by_list mismatch, fileid\n");
2523 if (res != MDB_NOTFOUND)
2527 // Check list sizes match
2528 for (int i=0;i<lids_size;i++) {
2529 dblist *list = dblist_get(tx, db, lids[i]);
2530 struct hist_node hk = { .key = lids[i] };
2531 struct hist_node *hn = ez_set_get(&list_counts, &hk);
2533 if (hn->count != list->size * 2) {
2534 printf("List size mismatch\n");
2542 mdb_cursor_close(fcursor);
2544 mdb_cursor_close(lfcursor);
2546 mdb_cursor_close(flcursor);
2548 ez_set_clear(&list_counts);
2549 ez_array_clear(&lidsa);