Cleanup dbindex.c a bit.
[playerz] / dbindex.c
1 /* dbindex.c: Database frontend for music file database.
2
3    Copyright (C) 2019 Michael Zucchi
4
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.
9
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.
14
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/>.
18 */
19
20 // TODO: list.size is really the next id, not size if list items are deleted
21
22 #define _GNU_SOURCE
23
24 #include <sys/types.h>
25 #include <sys/stat.h>
26
27 #include <stddef.h>
28 #include <stdlib.h>
29 #include <string.h>
30 #include <stdint.h>
31 #include <stdio.h>
32
33 #include <assert.h>
34
35 #include <lmdb.h>
36 #include <errno.h>
37
38 #include "dbindex.h"
39 #include "ez-array.h"
40 #include "ez-set.h"
41 #include "ez-blob.h"
42 #include "ez-blob-basic.h"
43 #include "dbmarshal.h"
44
45 // prototype
46 void dblist_dump(dbtxn *txn, dbindex *db);
47
48 /*
49 TODO: playlist should be linked list
50
51   [list] -> [0000][frst][last]
52   [list] -> [file][next][prev]
53
54  */
55
56 /*
57   playlist storage.
58
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]
61
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
64  */
65
66 /* Value stored in file-by-list */
67 struct dbfilelist {
68         uint32_t seq;
69         uint32_t fileid;
70 };
71
72 /* Value stored in list-by-file */
73 struct dblistfile {
74         uint32_t listid;
75         uint32_t seq;
76 };
77
78 struct dbindex {
79         int res; // last result
80
81         int debug;
82
83         MDB_env *env;
84
85         MDB_dbi meta;
86
87         MDB_dbi disk;
88         MDB_dbi disk_by_uuid;   // key is uuid                UNIQUE
89
90         MDB_dbi list;           // playlist to name
91         MDB_dbi list_by_name;
92
93         MDB_dbi file;
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
99
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]
102
103         MDB_dbi file_by_suffix;
104
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;
110 };
111
112 static uint32_t disk_next_id(dbindex *db) {
113         return __atomic_fetch_add(&db->diskid, 1, __ATOMIC_SEQ_CST);
114 }
115
116 static uint32_t list_next_id(dbindex *db) {
117         return __atomic_fetch_add(&db->listid, 1, __ATOMIC_SEQ_CST);
118 }
119
120 static uint32_t file_next_id(dbindex *db) {
121         return __atomic_fetch_add(&db->fileid, 1, __ATOMIC_SEQ_CST);
122 }
123
124 // Find the next primary key in the db
125 static uint32_t find_next_id(MDB_txn *tx, MDB_dbi db) {
126         MDB_cursor *cursor;
127         MDB_val key = { 0 }, data = { 0 };
128         int r;
129         uint32_t id = 1;
130
131         mdb_cursor_open(tx, db, &cursor);
132         r = mdb_cursor_get(cursor, &key, &data, MDB_LAST);
133         if (r == 0) {
134                 assert(key.mv_size == sizeof(id));
135                 memcpy(&id, key.mv_data, sizeof(id));
136                 id += 1;
137         }
138
139         mdb_cursor_close(cursor);
140
141         return id;
142 }
143
144 static int dblist_put(dbtxn *tx, dbindex *db, dblist *list, unsigned int flags);
145
146 static int
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;
150 }
151
152 char *dbindex_home(void) {
153         char *home = getenv("HOME");
154         char *path = NULL;
155
156         if (!home || asprintf(&path, "%s/.local/lib/playerz/index", home) < 0)
157                 abort();
158
159         return path;
160 }
161
162 dbindex *dbindex_open(const char *ipath) {
163         dbindex *db = calloc(sizeof(*db), 1);
164         int res;
165         MDB_txn *tx;
166         char *dpath = ipath ? NULL : dbindex_home();
167         const char *path = ipath ? ipath : dpath;
168
169         res = mdb_env_create(&db->env);
170         if (res)
171                 goto fail;
172         res = mdb_env_set_maxdbs(db->env, 16);
173         if (res)
174                 goto fail;
175         res = mdb_env_set_mapsize(db->env, 1<<28); // 256MB
176         if (res)
177                 goto fail;
178         res = mdb_env_open(db->env, path, 0, 0664);
179         if (res)
180                 goto fail;
181
182         res = mdb_txn_begin(db->env, NULL, 0, &tx);
183         if (res)
184                 goto fail;
185
186         // ??
187         res |= mdb_dbi_open(tx, "#meta", MDB_CREATE, &db->meta);
188
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);
191
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);
194
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);
201
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);
205
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);
210
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);
214
215         // setup system playlists
216         {
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" };
219                 dblist list = { 0 };
220
221                 for (int i=0;i<sizeof(names)/sizeof(names[0]);i++) {
222                         int tmp;
223
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);
229                                 else {
230                                         res |= tmp;
231                                         break;
232                                 }
233                         }
234                 }
235         }
236
237         res |= mdb_txn_commit(tx);
238
239         if (res) {
240                 printf("db setup fail: %s\n", mdb_strerror(res));
241                 // shutdown
242                 mdb_env_close(db->env);
243                 db = NULL;
244         }
245
246         printf("index open: '%s' disk.id=%d list.id=%d file.id=%d\n", path, db->diskid, db->listid, db->fileid);
247         free(dpath);
248
249         return db;
250  fail:
251         printf("db setup `%s' fail: %s\n", path, mdb_strerror(res));
252         // shutdown
253         if (db->env)
254                 mdb_env_close(db->env);
255         free(db);
256         free(dpath);
257
258         return NULL;
259 }
260
261 int dbindex_result(dbindex *db) {
262         return db->res;
263 }
264
265 void dbindex_close(dbindex *db) {
266         if (db) {
267                 mdb_env_close(db->env);
268                 free(db);
269         }
270 }
271
272 MDB_txn *dbindex_begin(dbindex *db, dbtxn *txn, int readonly) {
273         MDB_txn *tx;
274         int flags = readonly ? MDB_RDONLY : 0;
275
276         mdb_txn_begin(db->env, txn, flags, &tx);
277
278         return tx;
279 }
280
281 int dbindex_commit(MDB_txn *tx) {
282         int res = mdb_txn_commit(tx);
283
284         if (res != 0) {
285                 printf("commit failed: %s\n", mdb_strerror(res));
286         }
287
288         return res;
289 }
290
291 void dbindex_abort(MDB_txn *tx) {
292         mdb_txn_abort(tx);
293 }
294
295 int dbstate_get(MDB_txn *tx, dbindex *db, dbid_t listid, dbstate *s) {
296         MDB_val key;
297         MDB_val data;
298         char id[32];
299
300         sprintf(id, "state#%016x", listid);
301         key.mv_data = id;
302         key.mv_size = strlen(id);
303
304         db->res = mdb_get(tx, db->meta, &key, &data);
305
306         if (db->res == 0) {
307                 dbstate *p = (dbstate *)data.mv_data;
308                 if (p->version != ZPL_STATE_VERSION)
309                         return MDB_NOTFOUND;
310
311                 if (db->debug > 1) {
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);
317                 }
318
319                 *s = *p;
320                 return 0;
321         } else {
322                 printf("dbstate get: %s\n", mdb_strerror(db->res));
323         }
324
325         return db->res;
326 }
327
328 int dbstate_del_id(MDB_txn *tx, dbindex *db, dbid_t listid) {
329         MDB_val key;
330         char id[32];
331
332         sprintf(id, "state#%016x", listid);
333         key.mv_data = id;
334         key.mv_size = strlen(id);
335
336         return mdb_del(tx, db->meta, &key, NULL);
337 }
338
339 int dbstate_put(MDB_txn *tx, dbindex *db, dbid_t listid, dbstate *s) {
340         MDB_val key;
341         MDB_val data = { .mv_data = s, .mv_size = sizeof(*s) };
342         char id[32];
343
344         sprintf(id, "state#%016x", listid);
345         key.mv_data = id;
346         key.mv_size = strlen(id);
347
348         s->version = ZPL_STATE_VERSION;
349
350         return mdb_put(tx, db->meta, &key, &data, 0);
351 }
352
353 // get by_path key
354 static char *dbfile_path(dbfile *f) {
355         char *path = malloc(strlen(f->path) + 9);
356
357         sprintf(path, "%08x%s", f->diskid, f->path);
358
359         return path;
360 }
361
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) {
364         MDB_val data;
365
366         db->res = mdb_get(tx, primary, key, &data);
367
368         if (db->res == 0) {
369                 void *p = ez_basic_decode(desc, (ez_blob *)&data);
370
371                 assert(key->mv_size == sizeof(int));
372
373                 memcpy(p, key->mv_data, sizeof(int));
374                 return p;
375         }
376
377         return NULL;
378 }
379
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;
382
383         return (db->res = mdb_get(tx, primary, &key, &dat)) == 0;
384 }
385
386 /**
387  * Retrieve and decode data based on unique secondary key.
388  *
389  * @param secondary key to retrieve
390  * @param data holder
391  */
392 static void *secondary_get_decode(MDB_txn *tx, dbindex *db, ez_blob_desc *desc, MDB_val *key, MDB_dbi primary, MDB_dbi secondary) {
393         MDB_val data;
394
395         db->res = mdb_get(tx, secondary, key, &data);
396
397         if (db->res == 0)
398                 return primary_get_decode(tx, db, desc, &data, primary);
399
400         return NULL;
401 }
402
403 dbdisk *dbdisk_get(dbtxn *tx, dbindex *db, dbid_t diskid) {
404         MDB_val key = { .mv_data = &diskid, .mv_size = sizeof(diskid) };
405
406         return primary_get_decode(tx, db, DBDISK_DESC, &key, db->disk);
407 }
408
409 dbdisk *dbdisk_get_uuid(MDB_txn *tx, dbindex *db, const char *uuid) {
410         MDB_val key  = {
411                 .mv_data = (void *)uuid,
412                 .mv_size = strlen(uuid)
413         };
414
415         return secondary_get_decode(tx, db, DBDISK_DESC, &key, db->disk, db->disk_by_uuid);
416 }
417
418 void dbdisk_free(dbdisk *f) {
419         ez_blob_free(DBDISK_DESC, f);
420 }
421
422 int dbdisk_add(MDB_txn *tx, dbindex *db, dbdisk *d) {
423         MDB_val key, data;
424         int res;
425
426         // Store disk
427         d->id = disk_next_id(db);
428         key.mv_data = &d->id;
429         key.mv_size = sizeof(d->id);
430
431         data.mv_size = ez_basic_size(DBDISK_DESC, d);
432         data.mv_data = NULL;
433         res = mdb_put(tx, db->disk, &key, &data, MDB_NOOVERWRITE | MDB_RESERVE);
434         if (res == 0)
435                 ez_basic_encode_raw(DBDISK_DESC, d, (ez_blob *)&data);
436         else {
437                 printf("db put disk fail: %s\n", mdb_strerror(res));
438                 return res;
439         }
440
441         // Store secondary keys
442         data.mv_data = &d->id;
443         data.mv_size = sizeof(d->id);
444
445         // - by uuid
446         key.mv_data = d->uuid;
447         key.mv_size = strlen(d->uuid);
448
449         res = mdb_put(tx, db->disk_by_uuid, &key, &data, MDB_NOOVERWRITE);
450         return res;
451 }
452
453 static int secondary_list_all(dbtxn *tx, MDB_dbi secondary, ez_array *array) {
454         MDB_cursor *cursor;
455         MDB_val key, data;
456         int res;
457
458         if ((res = mdb_cursor_open(tx, secondary, &cursor)))
459                 return res;
460
461         int op = MDB_FIRST;
462         while ((res = mdb_cursor_get(cursor, &key, &data, op)) == 0) {
463                 if (!ez_array_add(array, data.mv_data, data.mv_size)) {
464                         res = ENOMEM;
465                         break;
466                 }
467                 op = MDB_NEXT;
468         }
469         if (res == MDB_NOTFOUND) {
470                 res = 0;
471         } else {
472                 ez_array_clear(array);
473         }
474
475         mdb_cursor_close(cursor);
476         return res;
477 }
478
479 static int secondary_list_key(dbtxn *tx, MDB_dbi secondary, MDB_val *key, ez_array *array) {
480         MDB_cursor *cursor;
481         MDB_val data;
482         int res;
483
484         if ((res = mdb_cursor_open(tx, secondary, &cursor)))
485                 return res;
486
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)) {
490                         res = ENOMEM;
491                         break;
492                 }
493                 op = MDB_NEXT_DUP;
494         }
495         if (res == MDB_NOTFOUND) {
496                 res = 0;
497         } else {
498                 ez_array_clear(array);
499         }
500
501         mdb_cursor_close(cursor);
502         return res;
503 }
504
505 int dbdisk_del(dbtxn *tx, dbindex *db, dbdisk *disk) {
506         MDB_val key;
507         ez_array array = { 0 };
508         int res = 0;
509
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)))
513                 goto fail;
514
515         dbid_t *fids = array.ea_data;
516         size_t count = array.ea_size / sizeof(*fids);
517
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])))
521                         goto fail;
522         }
523
524         // secondary keys
525
526         // -- by uuid
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)))
530                 goto fail;
531
532         // Remove disk
533         key.mv_data = &disk->id;
534         key.mv_size = sizeof(disk->id);
535         res = mdb_del(tx, db->disk, &key, NULL);
536
537 fail:
538         ez_array_clear(&array);
539         return res;
540 }
541
542 int dbdisk_del_id(dbtxn *tx, dbindex *db, dbid_t diskid) {
543         dbdisk *d = dbdisk_get(tx, db, diskid);
544
545         if (d) {
546                db->res = dbdisk_del(tx, db, d);
547                dbdisk_free(d);
548         }
549
550         return db->res;
551 }
552
553 dbfile *dbfile_get_path(MDB_txn *tx, dbindex *db, dbid_t diskid, const char *path) {
554         char name[strlen(path) + 9];
555         MDB_val key;
556
557         sprintf(name, "%08x", diskid);
558         strcpy(name+8, path);
559
560         key.mv_data = name;
561         key.mv_size = strlen(name);
562
563         return secondary_get_decode(tx, db, DBFILE_DESC, &key, db->file, db->file_by_path);
564 }
565
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);
569
570                 if ((file->full_path = malloc(strlen(disk->mount) + strlen(file->path) + 1)))
571                         sprintf(file->full_path, "%s%s", disk->mount, file->path);
572
573                 dbdisk_free(disk);
574         }
575
576         return file->full_path;
577 }
578
579 int dbfile_inlist(dbtxn *tx, dbindex *db, dbid_t fileid, dbid_t listid) {
580         struct dblistfile datval = { .listid = listid, .seq = 0 };
581         MDB_cursor *cursor;
582         MDB_val key = { .mv_data = &fileid, .mv_size = sizeof(fileid) };
583         MDB_val dat = { .mv_data = &datval, .mv_size = sizeof(datval) };
584         int is = 0;
585
586         if ((db->res = mdb_cursor_open(tx, db->list_by_file, &cursor)))
587                 goto fail1;
588
589         if ((db->res = mdb_cursor_get(cursor, &key, &dat, MDB_GET_BOTH_RANGE)))
590                 goto fail;
591
592         is = ((struct dblistfile *)dat.mv_data)->listid == listid;
593
594 fail:
595         mdb_cursor_close(cursor);
596 fail1:
597         return is;
598 }
599
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;
603
604         return strcmp(a->path, b->path);
605 }
606
607 void dbfile_node_free(struct dbfile_node *n) {
608         if (n) {
609                 dbfile_free(n->file);
610                 free(n);
611         }
612 }
613
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) {
617         MDB_val key, dat;
618         MDB_cursor *cursor;
619         size_t len = strlen(path) + 8;
620         int res;
621         char start[len + 1];
622
623         ez_tree_init(tree, dbfile_node_cmp);
624
625         if ((res = mdb_cursor_open(tx, db->file_by_dir, &cursor)))
626                 goto fail;
627
628         sprintf(start, "%08x%s", diskid, path);
629
630         // path must end in /
631         // scan all items >= path where there is only 1 / more
632
633         key.mv_data = start;
634         key.mv_size = len;
635
636         if ((res = mdb_cursor_get(cursor, &key, &dat, MDB_SET)) == 0)
637                 res = mdb_cursor_get(cursor, &key, &dat, MDB_GET_MULTIPLE);
638         while (res == 0) {
639                 size_t count = dat.mv_size / sizeof(dbid_t);
640                 dbid_t *files = dat.mv_data;
641
642                 for (int i=0;i<count;i++) {
643                         struct dbfile_node *node = malloc(sizeof(*node));
644
645                         node->file = dbfile_get(tx, db, files[i]);
646                         node->path = node->file->path;
647                         ez_tree_put(tree, node);
648                 }
649                 res = mdb_cursor_get(cursor, &key, &dat, MDB_NEXT_MULTIPLE);
650         }
651
652         mdb_cursor_close(cursor);
653 fail:
654         res = res == MDB_NOTFOUND ? 0 : res;
655
656         if (res != 0)
657                 ez_tree_clear(tree, (void(*)(void *))dbfile_node_free);
658         return res;
659 }
660
661 void dbfile_free(dbfile *f) {
662         if (f) {
663                 ez_blob_free(DBFILE_DESC, f);
664         }
665 }
666
667 dbfile *dbfile_get(dbtxn *tx, dbindex *db, dbid_t fileid) {
668         MDB_val key = { .mv_data = &fileid, .mv_size = sizeof(fileid) };
669 #if 1
670         MDB_val data;
671
672         db->res = mdb_get(tx, db->file, &key, &data);
673
674         //printf("dbfile_get(%d) = %d\n", fileid, db->res);
675
676         if (db->res == 0) {
677                 dbfile *p = calloc(1, sizeof(*p));
678
679                 dbfile_decode_raw((ez_blob *)&data, p);
680
681                 p->id = fileid;
682                 return p;
683         }
684
685         return NULL;
686 #else
687         return primary_get_decode(tx, db, DBFILE_DESC, &key, db->file);
688 #endif
689 }
690
691 int dbfile_del_id(dbtxn *tx, dbindex *db, dbid_t fileid) {
692         dbfile *f = dbfile_get(tx, db, fileid);
693
694         if (f) {
695                db->res = dbfile_del(tx, db, f);
696                dbfile_free(f);
697         } else {
698                 printf("no such file: %d\n", fileid);
699         }
700
701         return db->res;
702 }
703
704 // histogram table
705 struct hist_node {
706         struct ez_node sn;
707         dbid_t key;
708         unsigned int count;
709 };
710
711 static unsigned int hist_hash(const void *p) {
712         const struct hist_node *n = p;
713
714         return ez_hash_int32(n->key);
715 }
716
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;
720
721         return a->key == b->key;
722 }
723
724 int dbfile_del(dbtxn *tx, dbindex *db, dbfile *f) {
725         MDB_val key, dat;
726         int res;
727         char *dpath = NULL;
728
729         // Remove secondary keys / constraints
730         dat.mv_data = &f->id;
731         dat.mv_size = sizeof(f->id);
732
733         // - by disk+dir+name (and unique constraint)
734         dpath = dbfile_path(f);
735
736         key.mv_data = dpath;
737         key.mv_size = strlen(dpath);
738         if ((res = mdb_del(tx, db->file_by_path, &key, &dat)))
739                 goto fail;
740
741         // - by disk+dir
742         char *tmp = strrchr(dpath, '/');
743         if (!tmp) {
744                 res = EINVAL;
745                 goto fail;
746         }
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");
750                 goto fail;
751         }
752
753         // - by diskid
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");
758                 goto fail;
759         }
760
761         // - by title
762         if (f->title) {
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");
767                         goto fail;
768                 }
769         }
770
771         // - by artist
772         if (f->artist) {
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");
777                         goto fail;
778                 }
779         }
780
781         // Remove file
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");
786                 goto fail;
787         }
788
789         // check lists, FIXME: cleanup
790         {
791                 MDB_cursor *cursor;
792                 ez_array array = { 0 };
793
794                 // get all lists this file is in
795                 if ((res = mdb_cursor_open(tx, db->list_by_file, &cursor)))
796                         goto fail1;
797
798                 res = mdb_cursor_get(cursor, &key, &dat, MDB_SET);
799                 if (res == MDB_NOTFOUND) {
800                         res = 0;
801                         goto fail2;
802                 }
803                 while (res == 0) {
804                         if (ez_array_add(&array, dat.mv_data, dat.mv_size))
805                                 res = mdb_cursor_get(cursor, &key, &dat, MDB_NEXT_DUP);
806                         else
807                                 res = ENOMEM;
808                 }
809                 mdb_cursor_close(cursor);
810                 if (res != MDB_NOTFOUND)
811                         goto fail2;
812
813                 // delete the entry we just read
814                 if ((res = mdb_del(tx, db->list_by_file, &key, NULL)))
815                         goto fail2;
816
817                 struct dblistfile *files = array.ea_data;
818                 int count = array.ea_size / sizeof(*files);
819
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 = {
824                                 .seq = files[i].seq,
825                                 .fileid = f->id
826                         };
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)))
833                                 goto fail2;
834                 }
835
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;
841
842                         if ((hn = ez_set_get(&counts, &hk)))
843                                 hn->count += 1;
844                         else if ((hn = malloc(sizeof(*hn)))) {
845                                 hn->key = hk.key;
846                                 hn->count = 1;
847                                 ez_set_put(&counts, hn);
848                         } else {
849                                 res = ENOMEM;
850                                 goto fail2;
851                         }
852                 }
853                 ez_set_scan scan;
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);
856
857                         if (list) {
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);
861                         } else {
862                                 res = db->res;
863                         }
864                         dblist_free(list);
865                 }
866
867                 ez_set_clear(&counts);
868         fail2:
869                 mdb_cursor_close(cursor);
870         fail1:
871                 ez_array_clear(&array);
872         }
873 fail:
874         free(dpath);
875         return res;
876 }
877
878 static int dbstrcmp(const char *a, const char *b) {
879         if (a == NULL) {
880                 if (b == NULL)
881                         return 0;
882                 else
883                         return -1;
884         } else if (b == NULL)
885                 return 1;
886
887         return strcmp(a, b);
888 }
889
890 // update file with any changed values
891 // fixes secondary indices
892 int dbfile_update(dbtxn *tx, dbindex *db, dbfile *o, dbfile *f) {
893         MDB_val key, data;
894         int res;
895
896         // Update secondary keys
897         data.mv_data = &f->id;
898         data.mv_size = sizeof(f->id);
899
900         // - path can't change
901         // - diskid can't change
902         // - artist
903         if (dbstrcmp(o->artist, f->artist)) {
904                 if (o->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)))
908                                 goto fail;
909                 }
910
911                 if (f->artist) {
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)))
915                                 goto fail;
916                 }
917         }
918         // - title
919         if (dbstrcmp(o->title, f->title)) {
920                 if (o->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)))
924                                 goto fail;
925                 }
926
927                 if (f->title) {
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)))
931                                 goto fail;
932                 }
933         }
934
935         f->id = o->id;
936         key.mv_data = &f->id;
937         key.mv_size = sizeof(f->id);
938
939         data.mv_size = ez_basic_size(DBFILE_DESC, f);
940         if ((res = mdb_put(tx, db->file, &key, &data, MDB_RESERVE)))
941                 goto fail;
942
943         ez_basic_encode_raw(DBFILE_DESC, f, (ez_blob *)&data);
944 fail:
945         return res;
946 }
947
948 int dbfile_add(MDB_txn *tx, dbindex *db, dbfile *f) {
949         MDB_val key, data;
950         char *dpath = NULL;
951         int res;
952
953         // Check foreign constraints
954         if (!primary_exists(tx, db, f->diskid, db->disk)) {
955                 fprintf(stderr, "FOREIGN KEY: file with unknown disk\n");
956                 return db->res;
957         }
958
959         // Store file
960         f->id = file_next_id(db);
961         key.mv_data = &f->id;
962         key.mv_size = sizeof(f->id);
963
964         data.mv_size = ez_basic_size(DBFILE_DESC, f);
965         res = mdb_put(tx, db->file, &key, &data, MDB_NOOVERWRITE | MDB_RESERVE);
966         if (res == 0)
967                 ez_basic_encode_raw(DBFILE_DESC, f, (ez_blob *)&data);
968
969         if (res != 0) {
970                 printf("db put file fail: %s\n", mdb_strerror(res));
971                 goto fail;
972         }
973
974         // Store secondary keys
975         data.mv_data = &f->id;
976         data.mv_size = sizeof(f->id);
977
978         // - by disk+dir+name (and unique constraint)
979         dpath = dbfile_path(f);
980         key.mv_data = dpath;
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");
984                 goto fail;
985         }
986
987         // - by disk+dir
988         char *tmp = strrchr(dpath, '/');
989         if (!tmp) {
990                 fprintf(stderr, "INTERNAL: no directory for file\n");
991                 res = EINVAL;
992                 goto fail;
993         }
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");
997                 goto fail;
998         }
999
1000         // - by diskid
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)))
1004                 goto fail;
1005
1006         // - by title
1007         if (f->title) {
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)))
1011                         goto fail;
1012         }
1013
1014         // - by artist
1015         if (f->artist) {
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)))
1019                         goto fail;
1020         }
1021
1022  fail:
1023         free(dpath);
1024         return res;
1025 }
1026
1027 /*
1028   Player support functions
1029 */
1030
1031 // A way to iterate through a lit of files, based on an index or something else
1032
1033 #include <sys/types.h>
1034 #include <sys/stat.h>
1035 #include <unistd.h>
1036 #include <dirent.h>
1037
1038 /**
1039  * Check if the disk is mounted.
1040  *
1041  * This is not generally absolutely reliable but is in the context of
1042  * disk-monitor managing the mounts.
1043  *
1044  * It can be used for quickly discarding files that can't be mounted.
1045  *
1046  * This is super-slow, don't bother using it, performing a stat on the file
1047  * will suffice.
1048  */
1049 int dbdisk_mounted(dbdisk *disk) {
1050 #if 0
1051         // Check the device of the entries
1052         char parent[strlen(disk->mount)+1];
1053         char *slash = strrchr(parent, '/');
1054
1055         if (slash) {
1056                 struct stat pst, mst;
1057
1058                 *slash = 0;
1059
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);
1064         }
1065
1066         return 0;
1067 #else
1068         // See if the directory is empty
1069         // yikes, this is slow as fuck
1070         DIR *d = opendir(disk->mount);
1071         int entries = 0;
1072
1073         if (d) {
1074                 struct dirent *de;
1075
1076                 while (entries == 0 && (de = readdir(d))) {
1077                         if (strcmp(de->d_name, ".") == 0
1078                             || strcmp(de->d_name, "..") == 0)
1079                                 continue;
1080                         entries++;
1081                 }
1082                 closedir(d);
1083         }
1084
1085         return entries > 0;
1086 #endif
1087 }
1088
1089
1090 /**
1091  * Find the next file ... in some order or some index.
1092  *
1093  * This is intended to be called occasionally and not to scan the db.
1094  *
1095  * Currently it goes by the path index.
1096  *
1097  * should it be cyclic?
1098  *
1099  * FIXME: ignore files without audio! (duration==0)
1100  *
1101  * @param f file, use NULL to start at the beginning.
1102  */
1103 static int dbfile_iterate(dbindex *db, dbfile **fp, char **pathp, int first, int next) {
1104         MDB_txn *tx;
1105         MDB_val key, data;
1106         MDB_cursor *cursor;
1107         dbfile *file = NULL;
1108         int res;
1109
1110         mdb_txn_begin(db->env, NULL, MDB_RDONLY, &tx);
1111
1112         if ((res = mdb_cursor_open(tx, db->file_by_path, &cursor)))
1113                 goto fail;
1114
1115         /*
1116           Scan based on filename order
1117          */
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;
1121
1122         dbfile_free(*fp);
1123         *fp = NULL;
1124         free(*pathp);
1125         *pathp = NULL;
1126
1127         if (keyval) {
1128                 key.mv_data = keyval;
1129                 key.mv_size = strlen(keyval);
1130
1131                 res = mdb_cursor_get(cursor, &key, &data, MDB_SET);
1132                 res = mdb_cursor_get(cursor, &key, &data, next);
1133         } else {
1134                 res = mdb_cursor_get(cursor, &key, &data, first);
1135         }
1136
1137         while (file == NULL && res == 0) {
1138                 file = primary_get_decode(tx, db, DBFILE_DESC, &data, db->file);
1139                 if (file) {
1140                         int keep;
1141
1142                         if (disk == NULL || file->diskid != disk->id) {
1143                                 dbdisk_free(disk);
1144                                 disk = dbdisk_get(tx, db, file->diskid);
1145                                 mounted = dbdisk_mounted(disk);
1146                         }
1147                         keep = mounted;
1148                         keep = keep && file->duration > 0;
1149                         if (keep) {
1150                                 char path[strlen(disk->mount) + strlen(file->path) + 1];
1151                                 struct stat st;
1152
1153                                 sprintf(path, "%s%s", disk->mount, file->path);
1154
1155                                 keep = lstat(path, &st) == 0 && S_ISREG(st.st_mode);
1156                                 if (keep) {
1157                                         *pathp = strdup(path);
1158                                         *fp = file;
1159                                 }
1160                         }
1161
1162                         if (!keep) {
1163                                 dbfile_free(file);
1164                                 file = NULL;
1165                         }
1166                 }
1167                 if (file == NULL)
1168                         res = mdb_cursor_get(cursor, &key, &data, next);
1169         }
1170
1171         free(keyval);
1172         dbdisk_free(disk);
1173
1174         mdb_cursor_close(cursor);
1175         mdb_txn_commit(tx);
1176
1177         return res;
1178  fail:
1179         mdb_txn_abort(tx);
1180
1181         return res;
1182 }
1183
1184 /**
1185  * Find the next file ... in some order or some index.
1186  *
1187  * This is intended to be called occasionally and not to scan the db.
1188  *
1189  * Currently it goes by the path index.
1190  *
1191  * should it be cyclic?
1192  *
1193  * @param f file, use NULL to start at the beginning.
1194  */
1195 int dbfile_next(dbindex *db, dbfile **f, char **fpath) {
1196         return dbfile_iterate(db, f, fpath, MDB_FIRST, MDB_NEXT);
1197 }
1198
1199 int dbfile_prev(dbindex *db, dbfile **f, char **fpath) {
1200         return dbfile_iterate(db, f, fpath, MDB_LAST, MDB_PREV);
1201 }
1202
1203 /*
1204           Scan based on secondary index.
1205
1206         key.mv_data = &f->diskid;
1207         key.mv_size = sizeof(f->diskid);
1208
1209         data.mv_data = &f->id;
1210         data.mv_size = sizeof(f->id);
1211
1212         //res = mdb_cursor_get(scan->cursor, &scan->key, &scan->data, MDB_GET_BOTH);
1213
1214 */
1215
1216 /* playlist management */
1217
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) };
1222         int res;
1223
1224         if ((res = mdb_put(tx, db->list, &key, &dat, flags | MDB_RESERVE)))
1225                 return res;
1226
1227         ez_basic_encode_raw(DBLIST_DESC, list, (ez_blob *)&dat);
1228
1229         return 0;
1230 }
1231
1232 dblist *dblist_get(dbtxn *tx, dbindex *db, dbid_t listid) {
1233         MDB_val key = { .mv_data = &listid, .mv_size = sizeof(listid) };
1234
1235         return primary_get_decode(tx, db, DBLIST_DESC, &key, db->list);
1236 }
1237
1238 dblist *dblist_get_name(dbtxn *tx, dbindex *db, const char *name) {
1239         MDB_val key  = {
1240                 .mv_data = (void *)name,
1241                 .mv_size = strlen(name)
1242         };
1243
1244         return secondary_get_decode(tx, db, DBLIST_DESC, &key, db->list, db->list_by_name);
1245 }
1246
1247 void dblist_free(dblist *f) {
1248         ez_blob_free(DBLIST_DESC, f);
1249 }
1250
1251 dbid_t dblistid_get_name(dbtxn *tx, dbindex *db, const char *name) {
1252         MDB_val key  = {
1253                 .mv_data = (void *)name,
1254                 .mv_size = strlen(name)
1255         };
1256         MDB_val dat;
1257
1258         db->res = mdb_get(tx, db->list_by_name, &key, &dat);
1259
1260         return db->res == 0 ? *(dbid_t *)dat.mv_data : 0;
1261 }
1262
1263
1264 // put ?  add ?  d->id == 0 -> then add, otherwise put?
1265 int dblist_add(MDB_txn *txn, dbindex *db, dblist *list) {
1266         MDB_txn *tx;
1267         MDB_val key, dat;
1268         int res;
1269
1270         mdb_txn_begin(db->env, txn, 0, &tx);
1271
1272         // Store record
1273         list->id = list_next_id(db);
1274         if ((res = dblist_put(tx, db, list, MDB_NOOVERWRITE)))
1275                 goto fail;
1276
1277         // secondary keys
1278         dat.mv_data = &list->id;
1279         dat.mv_size = sizeof(list->id);
1280
1281         // - by name
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)))
1285                 goto fail;
1286
1287         return mdb_txn_commit(tx);
1288  fail:
1289         mdb_txn_abort(tx);
1290         return res;
1291 }
1292
1293 int dblist_reset(dbtxn *tx, dbindex *db, dbid_t listid) {
1294         MDB_val key, data;
1295         int res;
1296
1297         ez_array array = { 0 };
1298         struct dbfilelist *list;
1299         size_t count;
1300
1301         key.mv_data = &listid;
1302         key.mv_size = sizeof(listid);
1303
1304         res = secondary_list_key(tx, db->file_by_list, &key, &array);
1305         list = array.ea_data;
1306         count = array.ea_size / sizeof(*list);
1307         if (count) {
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");
1314                         goto fail;
1315                 }
1316
1317                 // delete reverse table entries
1318                 struct dblistfile entry = { .listid = listid };
1319
1320                 data.mv_data = &entry;
1321                 data.mv_size = sizeof(entry);
1322
1323                 for (int i=0;i<count;i++) {
1324                         key.mv_data = &list[i].fileid;
1325                         key.mv_size = sizeof(list[i].fileid);
1326
1327                         entry.seq = list[i].seq;
1328
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");
1333                                 } else
1334                                         goto fail;
1335                         }
1336                 }
1337         }
1338
1339 fail:
1340         free(list);
1341         printf("db clear list: %s\n", mdb_strerror(res));
1342         return res;
1343 }
1344
1345 int dblist_del(dbtxn *txn, dbindex *db, dblist *list) {
1346         dbid_t listid = list->id;
1347         MDB_txn *tx;
1348         MDB_val key, data;
1349         MDB_cursor *cursor;
1350         int res;
1351
1352         // TODO: deleting the reverse list can perform GET_BOTH_RANGE i think
1353         // TODO: merge clearing with dblist_reset
1354
1355         mdb_txn_begin(db->env, txn, 0, &tx);
1356
1357         key.mv_data = &listid;
1358         key.mv_size = sizeof(listid);
1359         if ((res = mdb_del(tx, db->list, &key, NULL)))
1360                 goto fail;
1361
1362         res = mdb_del(tx, db->file_by_list, &key, NULL);
1363         if (res != 0 && res != MDB_NOTFOUND)
1364                 goto fail;
1365
1366         res = dbstate_del_id(tx, db, listid);
1367         if (res != 0 && res != MDB_NOTFOUND)
1368                 goto fail;
1369
1370         res = mdb_cursor_open(tx, db->list_by_file, &cursor);
1371
1372         printf("delete reverse list\n");
1373         res = mdb_cursor_get(cursor, &key, &data, MDB_FIRST);
1374         while (res == 0) {
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);
1377
1378                 if (value->listid == listid) {
1379                         printf("delete\n");
1380                         if ((res = mdb_cursor_del(cursor, 0)))
1381                                 goto fail;
1382                         res = mdb_cursor_get(cursor, &key, &data, MDB_GET_CURRENT);
1383                 } else {
1384                         res = mdb_cursor_get(cursor, &key, &data, MDB_NEXT_DUP);
1385                 }
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));
1390                 }
1391         }
1392
1393         mdb_cursor_close(cursor);
1394
1395         // secondary keys
1396
1397         // -- by name
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)
1401                 goto fail;
1402
1403         return mdb_txn_commit(tx);
1404
1405 fail:
1406         printf("db del list fail: %s\n", mdb_strerror(res));
1407         mdb_txn_abort(tx);
1408         return res;
1409 }
1410
1411 int dblist_del_id(dbtxn *tx, dbindex *db, dbid_t listid) {
1412         dblist *list = dblist_get(tx, db, listid);
1413
1414         if (list) {
1415                db->res = dblist_del(tx, db, list);
1416                dblist_free(list);
1417         }
1418
1419         return db->res;
1420 }
1421
1422 // info is in/out, in=listid, fileid, out=listid, seq, fileid
1423 int dblist_add_file(MDB_txn *tx, dbindex *db, dblistcursor *info) {
1424         MDB_val key, dat;
1425         int res;
1426         dblist *list = dblist_get(tx, db, info->listid);
1427
1428         if (!list)
1429                 return db->res;
1430
1431         // Check file exists
1432         if (!primary_exists(tx, db, info->fileid, db->file)) {
1433                 printf("FOREIGN: file doesn't exist\n");
1434                 dblist_free(list);
1435                 return db->res;
1436         }
1437
1438         struct dbfilelist fvalue = { .seq = list->seq + 1, .fileid = info->fileid };
1439         struct dblistfile rvalue = { .listid = list->id, .seq = list->seq + 1 };
1440
1441         key.mv_data = &list->id;
1442         key.mv_size = sizeof(list->id);
1443         dat.mv_data = &fvalue;
1444         dat.mv_size = sizeof(fvalue);
1445
1446         if ((res = mdb_put(tx, db->file_by_list, &key, &dat, MDB_NODUPDATA)))
1447                 goto fail;
1448
1449         key.mv_data = &info->fileid;
1450         key.mv_size = sizeof(info->fileid);
1451         dat.mv_data = &rvalue;
1452         dat.mv_size = sizeof(rvalue);
1453
1454         if ((res = mdb_put(tx, db->list_by_file, &key, &dat, MDB_NODUPDATA)))
1455                 goto fail;
1456
1457         // update list record with changed size/sequence
1458         list->size += 1;
1459         list->seq += 1;
1460         res = dblist_put(tx, db, list, 0);
1461         if (res == 0)
1462                 info->seq = fvalue.seq;
1463 fail:
1464         dblist_free(list);
1465         return res;
1466 }
1467
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);
1471                 int idi = ids[i];
1472                 int idj = ids[i+j];
1473
1474                 ids[i] = idj;
1475                 ids[i+j] = idi;
1476         }
1477 }
1478
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);
1482         int res = 0;
1483
1484         // if the list exists clear it, otherwise create it
1485         if (list) {
1486                 res = dblist_reset(tx, db, list->id);
1487         } else {
1488                 if ((list = calloc(1, sizeof(*list))) == NULL
1489                         || (list->name = strdup(name)) == NULL)
1490                         res = ENOMEM;
1491                 else
1492                         res = dblist_add(tx, db, list);
1493         }
1494
1495         if (res != 0)
1496                 goto fail;
1497
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) };
1501
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) };
1505
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)))
1510                         goto fail;
1511
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)))
1515                         goto fail;
1516         }
1517
1518         // update/fix list record
1519         list->size = count;
1520         list->seq = count;
1521         res = dblist_put(tx, db, list, 0);
1522
1523 fail:
1524         printf("db list update: %s\n", mdb_strerror(res));
1525         dblist_free(list);
1526         return res;
1527 }
1528
1529 // Shuffle a specific list
1530 // load whole list
1531 // shuffle it
1532 // save shuffled version?
1533 int dblist_shuffle(dbtxn *tx, dbindex *db, const char *name) {
1534         return -1;
1535 }
1536
1537 // init sysstem lists, doesn't destroy any existing lists
1538 int dblist_init_system(dbtxn *tx, dbindex *db) {
1539         int res = 0;
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 };
1543
1544         for (int i=0;res == 0 && i<sizeof(names)/sizeof(names[0]);i++) {
1545                 int tmp;
1546
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);
1552                         else
1553                                 res = tmp;
1554                 }
1555         }
1556         return res;
1557 }
1558
1559 // remove all lists and recreate the all list
1560 int dblist_reset_all(dbtxn *tx, dbindex *db) {
1561         int res;
1562
1563         // empty all
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) {
1568                 db->listid = 1;
1569                 if ((res = dblist_init_system(tx, db)) == 0)
1570                         res = dblist_update_all(tx, db);
1571         }
1572         return res;
1573 }
1574
1575 // refresh the all list
1576 // list ordered by path
1577 int dblist_update_all(dbtxn *tx, dbindex *db) {
1578         int res;
1579         size_t count = 0;
1580         dbid_t *fids = NULL;
1581         ez_array array = { 0 };
1582
1583         if ((res = secondary_list_all(tx, db->file_by_path, &array)))
1584                 goto fail;
1585
1586         fids = array.ea_data;
1587         count = array.ea_size / sizeof(*fids);
1588
1589         // Create the all list
1590         if ((res = dblist_update(tx, db, "all", fids, count)))
1591                 goto fail;
1592
1593         // Create the all shuffled list
1594         array_shuffle(fids, count);
1595         res = dblist_update(tx, db, "all#shuffle", fids, count);
1596
1597 fail:
1598         ez_array_clear(&array);
1599         return res;
1600 }
1601
1602 // debug
1603
1604 void dblist_dump(dbtxn *tx, dbindex *db) {
1605         //MDB_txn *tx;
1606         MDB_val key, data;
1607         MDB_cursor *cursor;
1608         int res;
1609
1610         //if (res = mdb_txn_begin(db->env, txn, MDB_RDONLY, &tx)) {
1611         //      printf("failed: %s\n", mdb_strerror(res));
1612         //      return;
1613         //}
1614
1615
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);
1620         while (res == 0) {
1621                 struct dblistfile *value = data.mv_data;
1622                 uint32_t fid = *(uint32_t *)key.mv_data;
1623
1624                 printf("  file=%5d list=%5d seq=%5d\n", fid, value->listid, value->seq);
1625
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);
1629         }
1630         mdb_cursor_close(cursor);
1631
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);
1635         while (res == 0) {
1636                 struct dbfilelist *value = data.mv_data;
1637                 uint32_t lid = *(uint32_t *)key.mv_data;
1638
1639                 printf("  list=%5d file=%5d seq=%5d\n", lid, value->fileid, value->seq);
1640
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);
1644         }
1645         mdb_cursor_close(cursor);
1646
1647
1648         //mdb_txn_commit(tx);
1649 }
1650
1651 // only list + seq is required
1652 int dblist_del_file(MDB_txn *txn, dbindex *db, struct dblistcursor *curr) {
1653         MDB_txn *tx;
1654         MDB_val key, data;
1655         MDB_cursor *cursor;
1656         int res;
1657         struct dbfilelist fvalue = { .seq = curr->seq, .fileid = curr->fileid };
1658         struct dblistfile rvalue = { .listid = curr->listid, .seq = curr->seq };
1659
1660         //printf("list_del_file: lid=%4d seq=%4d fid=%4d\n", curr->listid, curr->seq, curr->fileid);
1661
1662         if ((res = mdb_txn_begin(db->env, txn, 0, &tx)))
1663                 goto fail0;
1664
1665         int delcursor = 0;
1666         int delfile = 0;
1667         int dellist = 0;
1668
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)))
1672                         goto fail;
1673
1674                 key.mv_data = &curr->fileid;
1675                 key.mv_size = sizeof(curr->fileid);
1676                 data.mv_data = &rvalue;
1677                 data.mv_size = sizeof(rvalue);
1678
1679                 if ((res = mdb_cursor_get(cursor, &key, &data, MDB_GET_BOTH_RANGE)))
1680                         goto fail;
1681
1682                 fvalue.seq = curr->seq = ((struct dblistfile *)data.mv_data)->seq;
1683
1684                 //printf("list_del_file: found seq=%4d\n", curr->seq);
1685
1686                 delcursor = 1;
1687                 delfile = 1;
1688         } else if (curr->fileid == 0) {
1689                 // Lookup fileid for list[seq]
1690                 if ((res = mdb_cursor_open(tx, db->file_by_list, &cursor)))
1691                         goto fail;
1692
1693                 key.mv_data = &curr->listid;
1694                 key.mv_size = sizeof(curr->listid);
1695                 data.mv_data = &fvalue;
1696                 data.mv_size = sizeof(fvalue);
1697
1698                 if ((res = mdb_cursor_get(cursor, &key, &data, MDB_GET_BOTH)))
1699                         goto fail;
1700
1701                 curr->fileid = ((struct dbfilelist *)data.mv_data)->fileid;
1702
1703                 //printf("list_del_file: found fid=%4d\n", curr->fileid);
1704
1705                 delcursor = 1;
1706                 dellist = 1;
1707         } else {
1708                 // use supplied values
1709                 delfile = 1;
1710                 dellist = 1;
1711         }
1712
1713         if (delcursor && (res = mdb_cursor_del(cursor, 0)))
1714                 goto fail;
1715
1716         if (delfile) {
1717                 key.mv_data = &curr->listid;
1718                 key.mv_size = sizeof(curr->listid);
1719                 data.mv_data = &fvalue;
1720                 data.mv_size = sizeof(fvalue);
1721
1722                 if ((res = mdb_del(tx, db->file_by_list, &key, &data)))
1723                         goto fail;
1724         }
1725
1726         if (dellist) {
1727                 key.mv_data = &curr->fileid;
1728                 key.mv_size = sizeof(curr->fileid);
1729                 data.mv_data = &rvalue;
1730                 data.mv_size = sizeof(rvalue);
1731
1732                 if ((res = mdb_del(tx, db->list_by_file, &key, &data)))
1733                         goto fail;
1734         }
1735
1736         // update list record with changed size/sequence
1737         dblist *list = dblist_get(tx, db, curr->listid);
1738         if (!list) {
1739                 res = dbindex_result(db);
1740                 goto fail;
1741         }
1742         list->size -= 1;
1743         if ((res = dblist_put(tx, db, list, 0)) != 0)
1744                 goto fail;
1745
1746         if (delcursor)
1747                 mdb_cursor_close(cursor);
1748
1749         mdb_txn_commit(tx);
1750         return 0;
1751
1752 fail:
1753         printf("list_del_file: %s\n", mdb_strerror(res));
1754         if (delcursor)
1755                 mdb_cursor_close(cursor);
1756
1757         mdb_txn_abort(tx);
1758 fail0:
1759         return res;
1760 }
1761
1762 #include <regex.h>
1763
1764 int dbfile_searchx(dbindex *db, const char *pattern, dbfile **results, int maxlen);
1765
1766 // TODO: should run over title index instead?
1767 int dbfile_searchx(dbindex *db, const char *pattern, dbfile **results, int maxlen) {
1768         MDB_txn *tx;
1769         MDB_val key, data;
1770         MDB_cursor *cursor;
1771         int res;
1772         regex_t reg;
1773
1774         printf("search, pattern='%s'\n", pattern);
1775         res = regcomp(&reg, pattern, REG_EXTENDED | REG_ICASE | REG_NOSUB);
1776         if (res != 0)
1777                 return res;
1778
1779         mdb_txn_begin(db->env, NULL, MDB_RDONLY, &tx);
1780
1781         if ((res = mdb_cursor_open(tx, db->file, &cursor)))
1782                 goto fail;
1783
1784         int next = MDB_FIRST;
1785         int i = 0;
1786         while (i < maxlen && (res = mdb_cursor_get(cursor, &key, &data, next)) == 0) {
1787                 if (db->res == 0) {
1788                         dbfile *file = ez_basic_decode(DBFILE_DESC, (ez_blob *)&data);
1789
1790                         if (regexec(&reg, file->title, 0, NULL, 0) == 0) {
1791                                 file->id = *(int *)key.mv_data;
1792                                 results[i++] = file;
1793                         } else {
1794                                 dbfile_free(file);
1795                         }
1796
1797                         next = MDB_NEXT;
1798                 } else
1799                         break;
1800         }
1801         mdb_cursor_close(cursor);
1802         regfree(&reg);
1803         mdb_txn_abort(tx);
1804
1805         return i;
1806
1807 fail:
1808         regfree(&reg);
1809         mdb_txn_abort(tx);
1810         return res;
1811
1812 }
1813
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) {
1818         MDB_txn *tx;
1819         MDB_val key, data;
1820         MDB_cursor *cursor;
1821         int res;
1822         regex_t reg;
1823
1824         // empty search is empty result
1825         if (!pattern[0])
1826                 return 0;
1827
1828         printf("search, pattern='%s'\n", pattern);
1829         res = regcomp(&reg, pattern, REG_EXTENDED | REG_ICASE | REG_NOSUB);
1830         if (res != 0)
1831                 return -1;
1832
1833         mdb_txn_begin(db->env, NULL, MDB_RDONLY, &tx);
1834
1835         if ((res = mdb_cursor_open(tx, db->file_by_title, &cursor)))
1836                 goto fail;
1837
1838         int next = MDB_FIRST;
1839         int i = 0;
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];
1843
1844                 memcpy(title, key.mv_data, key.mv_size);
1845                 title[key.mv_size] = 0;
1846
1847                 {
1848                         dbfile *file = dbfile_get(tx, db, fileid);
1849                         printf("title %s path %s disk %d\n", title, file->path, file->diskid);
1850                         dbfile_free(file);
1851                 }
1852
1853                 if (regexec(&reg, title, 0, NULL, 0) == 0) {
1854                         results[i++] = dbfile_get(tx, db, fileid);
1855                         next = MDB_NEXT;
1856                 } else {
1857                         next = MDB_NEXT_NODUP;
1858                 }
1859
1860         }
1861         mdb_cursor_close(cursor);
1862         regfree(&reg);
1863         mdb_txn_abort(tx);
1864
1865         return i;
1866
1867 fail:
1868         regfree(&reg);
1869         mdb_txn_abort(tx);
1870         return -1;
1871
1872 }
1873
1874 void dump_lists(dbindex *db, dbtxn *txn);
1875 void dump_lists(dbindex *db, dbtxn *txn) {
1876         MDB_txn *tx;
1877         MDB_val key, data;
1878         MDB_cursor *cursor;
1879         int res;
1880
1881         res = mdb_txn_begin(db->env, txn, 0, &tx);
1882         printf("begin txn (%d): %s\n", res, mdb_strerror(res));
1883
1884         res = mdb_cursor_open(tx, db->file_by_list, &cursor);
1885         printf("open cursor (%d): %s\n", res, mdb_strerror(res));
1886
1887         printf("file by list\n");
1888         res = mdb_cursor_get(cursor, &key, &data, MDB_FIRST);
1889         while (res == 0) {
1890                 int *keyp = key.mv_data;
1891                 struct dbfilelist *valp = data.mv_data;
1892
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);
1895         }
1896
1897         mdb_cursor_close(cursor);
1898
1899
1900         res = mdb_cursor_open(tx, db->list_by_file, &cursor);
1901         printf("open cursor (%d): %s\n", res, mdb_strerror(res));
1902
1903         printf("list by file\n");
1904         res = mdb_cursor_get(cursor, &key, &data, MDB_FIRST);
1905         while (res == 0) {
1906                 int *keyp = key.mv_data;
1907                 struct dblistfile *valp = data.mv_data;
1908
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);
1911         }
1912
1913         mdb_cursor_close(cursor);
1914
1915         mdb_txn_abort(tx);
1916 }
1917
1918 void dbindex_dump(dbindex *db);
1919 void dbindex_dump(dbindex *db) {
1920         MDB_txn *tx;
1921
1922         printf("Raw Dump\n");
1923         mdb_txn_begin(db->env, NULL, MDB_RDONLY, &tx);
1924         // dump disks
1925         {
1926                 MDB_cursor *cursor;
1927                 MDB_val key = { 0 }, data = { 0 };
1928                 int res;
1929
1930                 res = mdb_cursor_open(tx, db->disk, &cursor);
1931                 res = mdb_cursor_get(cursor, &key, &data, MDB_FIRST);
1932                 if (res != 0)
1933                         printf(" disks: none\n");
1934                 else
1935                         printf(" disks:\n");
1936                 while (res == 0) {
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);
1943                 }
1944                 mdb_cursor_close(cursor);
1945         }
1946
1947         // dump files
1948         {
1949                 MDB_cursor *cursor;
1950                 MDB_val key = { 0 }, data = { 0 };
1951                 int res;
1952
1953                 mdb_cursor_open(tx, db->file, &cursor);
1954                 res = mdb_cursor_get(cursor, &key, &data, MDB_FIRST);
1955                 if (res != 0)
1956                         printf(" files: none\n");
1957                 else
1958                         printf(" files:\n");
1959                 while (res == 0) {
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);
1966                 }
1967                 mdb_cursor_close(cursor);
1968         }
1969         mdb_txn_abort(tx);
1970 }
1971
1972 /* new dbscan version for for loops*/
1973 // for (foo = dbscan_foo(); foo; foo = dbscan_foo_next()) {
1974 // }
1975 // dbscan_free();
1976
1977 static void *dbscan_decode(dbscan *scan) {
1978         void *memory = calloc(scan->DESC->bd_offset, 1);
1979
1980         if (memory) {
1981                 scan->decode_raw((const ez_blob *)&scan->dat, memory);
1982                 *(dbid_t *)memory = *(dbid_t *)scan->key.mv_data;
1983         } else {
1984                 scan->res = ENOMEM;
1985         }
1986
1987         return memory;
1988 }
1989
1990 void dbscan_free(dbscan *scan) {
1991         if (scan->cursor)
1992                 mdb_cursor_close(scan->cursor);
1993         memset(scan, 0, sizeof(*scan));
1994 }
1995
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) {
1997         scan->tx = tx;
1998         scan->primary = primary;
1999         scan->DESC = desc;
2000         scan->decode_raw = decode_raw;
2001         scan->mode = 0;
2002
2003         if ((scan->res = mdb_cursor_open(tx, primary, &scan->cursor)))
2004                 goto fail;
2005         if ((scan->res = mdb_cursor_get(scan->cursor, &scan->key, &scan->dat, first)))
2006                 goto fail;
2007
2008         return dbscan_decode(scan);
2009 fail:
2010         return NULL;
2011 }
2012
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) {
2014         scan->tx = tx;
2015         scan->primary = primary;
2016         scan->DESC = desc;
2017         scan->decode_raw = decode_raw;
2018         scan->mode = 1;
2019
2020         if ((scan->res = mdb_cursor_open(tx, secondary, &scan->cursor)))
2021                 goto fail;
2022         if ((scan->res = mdb_cursor_get(scan->cursor, &scan->key, &scan->dat, first)))
2023                 goto fail;
2024         scan->key = scan->dat;
2025         if ((scan->res = mdb_get(tx, primary, &scan->key, &scan->dat)))
2026                 goto fail;
2027
2028         return dbscan_decode(scan);
2029 fail:
2030         return NULL;
2031 }
2032
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)))
2036                         goto fail;
2037         } else {
2038                 if ((scan->res = mdb_cursor_get(scan->cursor, &scan->key, &scan->dat, MDB_NEXT_DUP)))
2039                         goto fail;
2040                 scan->key = scan->dat;
2041                 if ((scan->res = mdb_get(scan->tx, scan->primary, &scan->key, &scan->dat)))
2042                         goto fail;
2043         }
2044
2045         return dbscan_decode(scan);
2046 fail:
2047         return NULL;
2048 }
2049
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);
2054 }
2055
2056 dbdisk *dbscan_disk_next(dbscan *scan) {
2057         return dbscan_table_next(scan);
2058 }
2059
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);
2064 }
2065
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);
2070 }
2071
2072 dbfile *dbscan_file_next(dbscan *scan) {
2073         return dbscan_table_next(scan);
2074 }
2075
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);
2078 }
2079
2080 dblist *dbscan_list_next(dbscan *scan) {
2081         return dbscan_table_next(scan);
2082 }
2083
2084 /**
2085  * Init playlist scan from given position.
2086  *
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.
2089  *
2090  */
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;
2095
2096         scan->tx = tx;
2097         scan->DESC = DBFILE_DESC;
2098         scan->decode_raw = dbfile_decode_raw;
2099         scan->primary = db->file;
2100         scan->mode = 2;
2101         scan->list_entry = *info;
2102
2103         if ((scan->res = mdb_cursor_open(tx, db->file_by_list, &scan->cursor)))
2104                 goto fail;
2105
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 };
2109                 MDB_cursor *cursor;
2110
2111                 scan->key = (MDB_val){ sizeof(fileid), &fileid };
2112                 scan->dat.mv_data = &thing;
2113                 scan->dat.mv_size = sizeof(thing);
2114
2115                 if ((scan->res = mdb_cursor_open(tx, db->list_by_file, &cursor)))
2116                         goto fail;
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;
2119                 else {
2120                         mdb_cursor_close(cursor);
2121                         goto fail;
2122                 }
2123                 mdb_cursor_close(cursor);
2124         }
2125
2126         struct dbfilelist thing = { .seq = seq };
2127
2128         scan->key = (MDB_val){ sizeof(listid), &listid };
2129         scan->dat.mv_data = &thing;
2130         scan->dat.mv_size = sizeof(thing);
2131
2132         if ((scan->res = mdb_cursor_get(scan->cursor, &scan->key, &scan->dat, MDB_GET_BOTH_RANGE)))
2133                 goto fail;
2134
2135         struct dbfilelist *value = scan->dat.mv_data;
2136
2137         scan->list_entry.seq = value->seq;
2138         scan->list_entry.fileid = value->fileid;
2139
2140         scan->key.mv_data = &scan->list_entry.fileid;
2141         scan->key.mv_size = sizeof(scan->list_entry.fileid);
2142
2143         if ((scan->res = mdb_get(tx, db->file, &scan->key, &scan->dat)))
2144                 goto fail;
2145
2146         dbfile *file = dbscan_decode(scan);
2147
2148         if (file) {
2149                 info->seq = scan->list_entry.seq;
2150                 info->fileid = scan->list_entry.fileid;
2151                 return file;
2152         }
2153 fail:
2154         return NULL;
2155 }
2156
2157 static dbfile *scan_list_entry_next(dbscan *scan, dblistcursor *info, MDB_cursor_op next0, MDB_cursor_op next1) {
2158         MDB_val key, dat;
2159
2160         if ((scan->res = mdb_cursor_get(scan->cursor, &key, &dat, next1)))
2161                 goto fail;
2162
2163         struct dbfilelist *value = dat.mv_data;
2164
2165         scan->list_entry.seq = value->seq;
2166         scan->list_entry.fileid = value->fileid;
2167
2168         scan->key.mv_data = &scan->list_entry.fileid;
2169         scan->key.mv_size = sizeof(scan->list_entry.fileid);
2170
2171         if ((scan->res = mdb_get(scan->tx, scan->primary, &scan->key, &scan->dat)))
2172                 goto fail;
2173
2174         dbfile *file = dbscan_decode(scan);
2175
2176         if (file) {
2177                 info->seq = scan->list_entry.seq;
2178                 info->fileid = scan->list_entry.fileid;
2179                 return file;
2180         }
2181 fail:
2182         return NULL;
2183 }
2184
2185 dbfile *dbscan_list_entry_next(dbscan *scan, dblistcursor *info) {
2186         return scan_list_entry_next(scan, info, MDB_NEXT, MDB_NEXT_DUP);
2187 }
2188
2189 dbfile *dbscan_list_entry_prev(dbscan *scan, dblistcursor *info) {
2190         return scan_list_entry_next(scan, info, MDB_PREV, MDB_PREV_DUP);
2191 }
2192
2193 // prototyping
2194 int dbfile_clear_suffix(MDB_txn *tx, dbindex *db) {
2195         return mdb_drop(tx, db->file_by_suffix, 0);
2196 }
2197
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) };
2201
2202         return mdb_put(tx, db->file_by_suffix, &key, &data, MDB_NODUPDATA);
2203 }
2204
2205 static void printval(MDB_val key) {
2206         char match[key.mv_size+1];
2207
2208         memcpy(match, key.mv_data, key.mv_size);
2209         match[key.mv_size] = 0;
2210
2211         printf("%s\n", match);
2212 }
2213
2214 size_t dbfile_search_substring(dbtxn *tx, dbindex *db, const char *sub) {
2215         int res;
2216         size_t len = strlen(sub);
2217         MDB_val key = { .mv_data = (char *)sub, .mv_size = len };
2218         MDB_val data;
2219         MDB_cursor *cursor;
2220         size_t total = 0;
2221
2222         if ((res = mdb_cursor_open(tx, db->file_by_suffix, &cursor)))
2223                 goto fail;
2224
2225         res = mdb_cursor_get(cursor, &key, &data, MDB_SET_RANGE);
2226         if (res)
2227                 goto fail;
2228
2229         printval(key);
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;
2233
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;
2237
2238                         printf("multiple: %zd\n", endids - fileids);
2239
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);
2244                                 dbfile_free(file);
2245                                 total++;
2246                         }
2247                         step = MDB_NEXT_MULTIPLE;
2248                 }
2249                 res = mdb_cursor_get(cursor, &key, &data, MDB_NEXT);
2250                 if (res == 0)
2251                         printval(key);
2252         }
2253 fail:
2254         if (res == MDB_NOTFOUND)
2255                 return total;
2256         return -1;
2257 }
2258
2259 static int cmp_fid(const void *ap, const void *bp) {
2260         return *(const int32_t *)ap - *(const int32_t *)bp;
2261 }
2262
2263 /* protoyping */
2264
2265 static int check_path(const dbfile *file, const MDB_val *key) {
2266         char tmp[16];
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;
2271 }
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;
2275 }
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;
2279 }
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;
2283 }
2284 static int check_dir(const dbfile *file, const MDB_val *key) {
2285         char *slash = strrchr(file->path, '/');
2286         char tmp[16];
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;
2292 }
2293
2294 int dbindex_validate(dbindex *db) {
2295         MDB_txn *tx;
2296         MDB_val key, data;
2297         MDB_cursor *cursor;
2298         int res;
2299         int ok = 1;
2300
2301         mdb_txn_begin(db->env, NULL, MDB_RDONLY, &tx);
2302
2303         ez_array array = { 0 };
2304         // All files
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);
2309
2310         uint32_t *fids = array.ea_data;
2311         size_t fids_size = array.ea_size / sizeof(*fids);
2312
2313         printf("read %zd files\n", fids_size);
2314         qsort(fids, fids_size, sizeof(*fids), cmp_fid);
2315
2316         // Check secondary indices
2317         MDB_dbi tables[] = {
2318                 db->file_by_path,
2319                 db->file_by_disk,
2320                 db->file_by_title,
2321                 db->file_by_artist,
2322                 db->file_by_dir,
2323         };
2324         int (*checks[])(const dbfile *file, const MDB_val *key) = {
2325                 check_path,
2326                 check_diskid,
2327                 check_title,
2328                 check_artist,
2329                 check_dir
2330         };
2331
2332         for (int i=0;i<5;i++) {
2333                 size_t count = 0;
2334                 //printf("table %d\n", i);
2335                 mdb_cursor_open(tx, tables[i], &cursor);
2336                 res = mdb_cursor_get(cursor, &key, &data, MDB_FIRST);
2337                 while (res == 0) {
2338                         uint32_t fid = *(uint32_t*)data.mv_data;
2339
2340                         count++;
2341
2342                         if (!bsearch(&fid, fids, fids_size, sizeof(*fids), cmp_fid)) {
2343                                 printf("table %d references missing file\n", i);
2344                                 ok = 0;
2345                         }
2346                         {
2347                                 dbfile *file = dbfile_get(tx, db, fid);
2348                                 if (file) {
2349                                         if (!checks[i](file, &key)) {
2350                                                 printf("failed field check %d\n", i);
2351                                                 ok = 0;
2352                                         }
2353                                 } else {
2354                                         ok = 0;
2355                                 }
2356                                 dbfile_free(file);
2357                         }
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);
2361                 }
2362                 mdb_cursor_close(cursor);
2363
2364                 if (i <= 1 && count != fids_size) {
2365                         printf("file by [thing] miscount %zd != %zd\n", count, fids_size);
2366                         ok = 0;
2367                 }
2368         }
2369 //fail:
2370         ez_array_clear(&array);
2371         dbindex_abort(tx);
2372
2373         return ok;
2374 }
2375
2376 int dbindex_validate_lists(dbindex *db);
2377 int dbindex_validate_lists(dbindex *db) {
2378         MDB_txn *tx;
2379         MDB_val key, data;
2380         MDB_cursor *cursor;
2381         int res;
2382
2383         ez_set list_counts = EZ_INIT_SET(counts, hist_hash, hist_equals, free);
2384         ez_array lidsa = EZ_INIT_ARRAY(lidsa);
2385         dbid_t *lids;
2386         size_t lids_size;
2387
2388         mdb_txn_begin(db->env, NULL, MDB_RDONLY, &tx);
2389
2390         // All listids
2391         if ((res = mdb_cursor_open(tx, db->list, &cursor)) != 0)
2392                 goto fail1;
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)
2397                 goto fail1;
2398         res = 0;
2399
2400         lids = lidsa.ea_data;
2401         lids_size = lidsa.ea_size / sizeof(*lids);
2402
2403         for (int i=0;i<lids_size;i++) {
2404                 struct hist_node *hn = malloc(sizeof(*hn));
2405
2406                 hn->key = lids[i];
2407                 hn->count = 0;
2408                 ez_set_put(&list_counts, hn);
2409         }
2410
2411         MDB_cursor *fcursor;
2412         MDB_cursor *flcursor;
2413         MDB_cursor *lfcursor;
2414
2415         if ((res = mdb_cursor_open(tx, db->file, &fcursor)) != 0)
2416                 goto fail1;
2417         if ((res = mdb_cursor_open(tx, db->file_by_list, &flcursor)) != 0)
2418                 goto fail2;
2419         if ((res = mdb_cursor_open(tx, db->list_by_file, &lfcursor)) != 0)
2420                 goto fail3;
2421
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;
2425                 dbid_t lid;
2426
2427                 assert(key.mv_size == sizeof(dbid_t));
2428                 assert(data.mv_size == sizeof(fvalue));
2429
2430                 lid = *((dbid_t *)key.mv_data);
2431                 memcpy(&fvalue, data.mv_data, sizeof(fvalue));
2432
2433                 printf(" lid=%4d -> seq=%5d fid=%5d\n", lid, fvalue.seq, fvalue.fileid);
2434
2435                 // TODO: check seq is unique (enforced by db i guess)
2436
2437                 struct hist_node hk = { .key = lid };
2438                 struct hist_node *hn = ez_set_get(&list_counts, &hk);
2439                 if (!hn) {
2440                         printf("file_by_list: listid foreign key fail\n");
2441                         goto fail;
2442                 }
2443                 hn->count += 1;
2444
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);
2449                 if (res != 0) {
2450                         printf("file_by_list: fileid missing\n");
2451                         goto fail;
2452                 }
2453
2454                 // check inverse
2455                 struct dblistfile lvalue = { .listid = lid, .seq = fvalue.seq };
2456
2457                 key.mv_data = &fvalue.fileid;
2458                 key.mv_size = sizeof(fvalue.fileid);
2459                 data.mv_data = &lvalue;
2460                 data.mv_size = sizeof(lvalue);
2461
2462                 res = mdb_cursor_get(lfcursor, &key, &data, MDB_GET_BOTH);
2463                 if (res != 0) {
2464                         printf("file_by_list: list_by_file mismatch\n");
2465                         goto fail;
2466                 }
2467         }
2468         if (res != MDB_NOTFOUND)
2469                 goto fail;
2470         res = 0;
2471
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;
2475                 dbid_t fid;
2476
2477                 assert(key.mv_size == sizeof(dbid_t));
2478                 assert(data.mv_size == sizeof(lvalue));
2479
2480                 fid = *((dbid_t *)key.mv_data);
2481                 memcpy(&lvalue, data.mv_data, sizeof(lvalue));
2482
2483                 printf(" fid=%4d -> seq=%5d lid=%5d\n", fid, lvalue.seq, lvalue.listid);
2484
2485                 struct hist_node hk = { .key = lvalue.listid };
2486                 struct hist_node *hn = ez_set_get(&list_counts, &hk);
2487                 if (!hn) {
2488                         printf("list_by_file: listid foreign key fail\n");
2489                         goto fail;
2490                 }
2491                 hn->count += 1;
2492
2493                 // check file exists
2494                 key.mv_data = &fid;
2495                 key.mv_size = sizeof(dbid_t);
2496                 res = mdb_cursor_get(fcursor, &key, &data, MDB_SET_KEY);
2497                 if (res != 0) {
2498                         printf("file_by_list: fileid missing\n");
2499                         goto fail;
2500                 }
2501
2502                 // check inverse
2503                 struct dbfilelist fvalue = { .seq = lvalue.seq /* .fileid ignored */ };
2504
2505                 key.mv_data = &lvalue.listid;
2506                 key.mv_size = sizeof(lvalue.listid);
2507                 data.mv_data = &fvalue;
2508                 data.mv_size = sizeof(fvalue);
2509
2510                 res = mdb_cursor_get(flcursor, &key, &data, MDB_GET_BOTH);
2511                 if (res != 0) {
2512                         printf("list_by_file: file_by_list mismatch, missing seq: %s\n", mdb_strerror(res));
2513                         goto fail;
2514                 }
2515
2516                 fvalue = *((struct dbfilelist *)data.mv_data);
2517                 if (fid != fvalue.fileid) {
2518                         res = MDB_NOTFOUND;
2519                         printf("list_by_file: file_by_list mismatch, fileid\n");
2520                         goto fail;
2521                 }
2522         }
2523         if (res != MDB_NOTFOUND)
2524                 goto fail1;
2525         res = 0;
2526
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);
2532
2533                 if (hn->count != list->size * 2) {
2534                         printf("List size mismatch\n");
2535                         dblist_free(list);
2536                         goto fail;
2537                 }
2538                 dblist_free(list);
2539         }
2540
2541 fail:
2542         mdb_cursor_close(fcursor);
2543 fail3:
2544         mdb_cursor_close(lfcursor);
2545 fail2:
2546         mdb_cursor_close(flcursor);
2547 fail1:
2548         ez_set_clear(&list_counts);
2549         ez_array_clear(&lidsa);
2550
2551         mdb_txn_abort(tx);
2552
2553         return res;
2554 }