Checkpoint ongoing mess.
authorNot Zed <notzed@gmail.com>
Wed, 9 Jun 2021 00:38:35 +0000 (10:08 +0930)
committerNot Zed <notzed@gmail.com>
Wed, 9 Jun 2021 00:38:35 +0000 (10:08 +0930)
Added embedded http player.
Ongoing work on playlists.
Some search functions.

17 files changed:
Makefile
README [new file with mode: 0644]
TODO [new file with mode: 0644]
analyse.c [new file with mode: 0644]
analyse.h [new file with mode: 0644]
blobs.c [new file with mode: 0644]
dbindex.c
dbindex.h
disk-indexer.c
disk-monitor.c
disk-util.c [new file with mode: 0644]
http-monitor.c [new file with mode: 0644]
input-monitor.c
music-player.c
notify.c
notify.h
player.html [new file with mode: 0644]

index 8afde7f..13cec01 100644 (file)
--- a/Makefile
+++ b/Makefile
@@ -1,7 +1,10 @@
 
+# FIXME: dependencies n shit
+
 FFMPEG=/opt/ffmpeg/4.0
 LMDB=/home/notzed/src/openldap/libraries/liblmdb
-WARN=-Wno-deprecated-declarations -Wno-parentheses -Wno-unused-but-set-variable
+EZE=../libeze
+WARN=-Wno-deprecated-declarations -Wno-parentheses -Wno-unused-but-set-variable -Wno-pointer-sign
 
 pkgs=ffmpeg lmdb blkid asound espeak
 
@@ -18,25 +21,100 @@ LDLIBS_asound=-lasound
 
 #CFLAGS_espeak=-I/opt/espeak/include
 #LDFLAGS_espeak=-L/opt/espeak/lib -Wl,-rpath,/opt/espeak/lib
-LDLIBS_espeak=-lespeak-ng
+#LDLIBS_espeak=-lespeak-ng
 
-CFLAGS=-std=gnu99 -I../libeze
+CFLAGS=-std=gnu99 -I$(EZE)
 CFLAGS+=-Wall $(WARN)
+#CFLAGS+=-mavx -mavx2
+#CFLAGS+=-mtune=native
 CFLAGS+=-g -O0
+#CFLAGS+=-O2
 CFLAGS+=$(foreach x,$(pkgs),$(CFLAGS_$(x)))
 LDFLAGS=$(foreach x,$(pkgs),$(LDFLAGS_$(x)))
 LDLIBS=$(foreach x,$(pkgs),$(LDLIBS_$(x))) -lrt -lpthread ../libeze/libeze.a
 
-PROGS=disk-indexer disk-monitor audio-cmd music-player input-monitor
+PROGS=disk-indexer disk-monitor audio-cmd music-player input-monitor http-monitor disk-util
+
+GENERATED=dbmarshal.c dbmarshal.h player.h
 
 all: $(PROGS)
 
-disk-monitor: disk-monitor.o dbindex.o notify.o
-disk-indexer: disk-indexer.o dbindex.o notify.o
-audio-cmd: audio-cmd.o notify.o dbindex.o
-music-player: music-player.o notify.o dbindex.o
-input-monitor: input-monitor.o notify.o dbindex.o
+#disk-monitor: disk-monitor.o dbindex.o dbmarshal.o notify.o
+#disk-indexer: disk-indexer.o dbindex.o dbmarshal.o notify.o analyse.o
+#audio-cmd: audio-cmd.o notify.o blobs.o
+#music-player: music-player.o notify.o dbindex.o dbmarshal.o
+#input-monitor: input-monitor.o notify.o blobs.o
+dump: dump.o dbindex.o
+
+#disk-util: disk-util.o dbindex.o dbmarshal.o
+
+
+dbmarshal.h: blobs.o $(EZE)/ez-blob-compiler
+       $(EZE)/ez-blob-compiler -g header $< DBDISK_DESC DBFILE_DESC DBLIST_DESC > $@~
+       mv $@~ $@
+dbmarshal.c: blobs.o $(EZE)/ez-blob-compiler
+       $(EZE)/ez-blob-compiler $< DBDISK_DESC DBFILE_DESC DBLIST_DESC > $@~
+       mv $@~ $@
+
+dbmarshal.o: dbmarshal.c
+       $(CC) $(CFLAGS) -Wno-unused -c -o $@ $<
 
 clean:
-       rm $(PROGS)
-       rm *.o
+       rm -f $(PROGS)
+       rm -f $(GENERATED)
+       rm -f *.o *.d
+
+test: test.o dbindex.o
+http-monitor.o: http-monitor.c player.h
+http-monitor: http-monitor.o dbindex.o notify.o dbmarshal.o ../libeze/libeze.a
+player.h: player.html
+       echo "const char player_html[] = {" > $@~
+       gzip -9 < $< | od -A none -t d1 | sed 's/\([0-9]\+\)/\1,/g' >> $@~
+       echo "};" >> $@~
+       mv $@~ $@
+
+http-monitor.o: http-monitor.c player.h
+
+#http-monitor.o: http-monitor.c
+#http-monitor: http-monitor.o ez-blob-io.o dbindex.o notify.o dbmarshal.o player.o
+#player.s: player.html
+#      echo -e " .global player_html\n .section .rodata\n .align 32\n  .type player_html,@object\n .size player_html, 1f - player_html\n player_html:" > $@~
+#      gzip -9 < $< | od -A none -t d1 | sed -e 's/^/ .byte /' -e 's/\([0-9]\+\) /\1,/g' >> $@~
+#      echo " 1:" >> $@~
+#      mv $@~ $@
+
+
+engine: engine.o #ez-blob-io.o
+http: http.o
+
+bin_COMMANDS = disk-monitor disk-indexer audio-cmd music-player input-monitor http-monitor disk-util
+
+SOURCES=                                       \
+ analyse.c                                     \
+ audio-cmd.c                                   \
+ blobs.c                                       \
+ dbindex.c                                     \
+ dbmarshal.c                                   \
+ disk-indexer.c                                        \
+ disk-monitor.c                                        \
+ disk-util.c                                   \
+ input-monitor.c                               \
+ music-player.c                                        \
+ notify.c
+
+disk-monitor: disk-monitor.o dbindex.o dbmarshal.o notify.o
+disk-indexer: disk-indexer.o dbindex.o dbmarshal.o notify.o analyse.o
+audio-cmd: audio-cmd.o notify.o blobs.o
+music-player: music-player.o notify.o dbindex.o dbmarshal.o
+input-monitor: input-monitor.o notify.o blobs.o
+disk-util: disk-util.o dbindex.o dbmarshal.o
+http-monitor: http-monitor.o dbindex.o notify.o dbmarshal.o ../libeze/libeze.a
+
+%.d: %.c
+       @rm -rf $@
+       @cc -MM -MT "$*.o" $< -o $@~ $(CFLAGS)
+       @sed 's,\($*\.o\) *:,\1 $@ : ,g' $@~ > $@ && rm $@~
+
+ifeq '$(filter clean dist,$(MAKECMDGOALS))' ''
+-include $(SOURCES:.c=.d))
+endif
diff --git a/README b/README
new file mode 100644 (file)
index 0000000..b68d051
--- /dev/null
+++ b/README
@@ -0,0 +1,42 @@
+
+Design Thoughts
+--------------
+
+Overall playlist behaviour?
+
+ - Need a "current playlist"
+ - When a playlist finishes, go back to the all playlist.
+ - Also need a "scratch playlist"
+
+System playlists
+
+* default / all:shuffle
+  All tracks, shuffled.  The default playlist.
+
+* queue
+  User selected tracks, in order.
+
+* junk
+  The junk list, for later operations
+
+* user list
+  A user playlist
+
+Operations on Search
+
+* Play Now
+  Adds to the play queue and switches to it it isn't started.
+
+* Add to List
+  Add to specified playlist.
+
+Operations on Coming Up
+
+* Play Now
+  Jumps to track
+
+* Junk
+  Add to junk list (or should it be explicit?)
+
+* Remove (user playlist, queue)
+  Remove from playlist
diff --git a/TODO b/TODO
new file mode 100644 (file)
index 0000000..79abcf7
--- /dev/null
+++ b/TODO
@@ -0,0 +1,34 @@
+
+o bugs
+ - something wrong with playlist and direct-play state/reverse lookup?
+
+
+o Multiple playlists
+ - leverage the shuffle code
+ - add a playlist secondary index?
+
+ create table shuffle {
+   seq int,
+   index int,
+   playlist int,
+   primary key (seq),
+   foreign key seq references file(id),
+   index on (index)
+ }
+
+table playlist {
+  id int,
+  shuffleid int,
+
+  text name,
+
+  foreign key id references shuffle(index),
+  foreign key shuffleid references shuffle(index)
+}
+
+o check end of file processing
+ - seems to truncate the last frame?
+
+o web frontend
+ - custom playlists
+  + can use the shuffle code again
diff --git a/analyse.c b/analyse.c
new file mode 100644 (file)
index 0000000..9daccbc
--- /dev/null
+++ b/analyse.c
@@ -0,0 +1,209 @@
+/* analyse.c: Word suffix analysis.
+
+   Copyright (C) 2020 Michael Zucchi
+
+   This program is free software: you can redistribute it and/or
+   modify it under the terms of the GNU General Public License as
+   published by the Free Software Foundation, either version 3 of the
+   License, or (at your option) any later version.
+
+   This program is distributed in the hope that it will be useful, but
+   WITHOUT ANY WARRANTY; without even the implied warranty of
+   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+   General Public License for more details.
+
+   You should have received a copy of the GNU General Public License
+   along with this program. If not, see
+   <http://www.gnu.org/licenses/>.
+*/
+
+#include <wchar.h>
+#include <wctype.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+#include "ez-list.h"
+#include "ez-set.h"
+
+#include "analyse.h"
+
+struct wchar_node {
+       ez_node ln;
+       wchar_t value;
+};
+
+#define NODE(x) { .value = x }
+
+static ez_set collapse_set;
+static struct wchar_node collapse_nodes[] = {
+       // All the quote types
+       NODE(0x0027), //'
+       NODE(0x00AB), //«
+       NODE(0x2039), //‹
+       NODE(0x00BB), //»
+       NODE(0x203A), //›
+       NODE(0x201E), //„
+       NODE(0x201C), //“
+       NODE(0x201F), //‟
+       NODE(0x201D), //”
+       NODE(0x2019), //’
+       NODE(0x0022), //"
+       NODE(0x275D), //❝
+       NODE(0x275E), //❞
+       NODE(0x276E), //❮
+       NODE(0x276F), //❯
+       NODE(0x2E42), //⹂
+       NODE(0x301D), //〝
+       NODE(0x301E), //〞
+       NODE(0x301F), //〟
+       NODE(0xFF02), //"
+       NODE(0x201A), //‚
+       NODE(0x2018), //‘
+       NODE(0x201B), //‛
+       NODE(0x275B), //❛
+       NODE(0x275C), //❜
+       NODE(0x275F), //❟
+};
+
+static unsigned int wchar_hash(const void *n) {
+       //return ez_hash_int32(((struct wchar_node *)n)->value);
+       //return ((struct wchar_node *)n)->value;
+       return ((struct wchar_node *)n)->value * 378684 >> 16;
+}
+
+static int wchar_equals(const void *a, const void *b) {
+       return ((struct wchar_node *)a)->value == ((struct wchar_node *)b)->value;
+}
+
+static void done(void) __attribute__ ((destructor));
+static void done(void) {
+       ez_set_clear(&collapse_set);
+}
+
+static void init(void) __attribute__ ((constructor));
+static void init(void) {
+       ez_set_init(&collapse_set, wchar_hash, wchar_equals, NULL);
+       for (int i=0;i<sizeof(collapse_nodes)/sizeof(collapse_nodes[0]);i++)
+               ez_set_put(&collapse_set, &collapse_nodes[i]);
+#if 0
+       // exhaustive search for perfect hash
+       printf("find best\n");
+       int bestc = 1000;
+       int bestj = 0;
+       int bestk = 0;
+       for (int k=0;k<25;k++) {
+               for (int j=1;j<1000000;j++) {
+                       int c = 0;
+                       char hits[32] = { 0 };
+
+                       for (int i=0;i<sizeof(collapse_nodes)/sizeof(collapse_nodes[0]);i++) {
+                               int h = ((collapse_nodes[i].value * j) >>  k) & 31;
+                               //h = wchar_hash(&collapse_nodes[i]) & 31;
+
+                               if (hits[h])
+                                       c++;
+                               hits[h]++;
+                       }
+                       if (c == 0) {
+                               printf("best c=%d j=%d k=%d\n", c, j, k);
+                       }
+                       if (c < bestc) {
+                               bestc = c;
+                               bestj = j;
+                               bestk = k;
+                               printf("best c=%d j=%d k=%d\n", bestc, bestj, bestk);
+                       }
+               }
+       }
+       printf("best c=%d j=%d k=%d\n", bestc, bestj, bestk);
+#endif
+}
+
+static int iswcollapse(wchar_t c) {
+       struct wchar_node key = { .value = c };
+
+       return ez_set_get(&collapse_set, &key) != NULL;
+}
+/*
+  Want this stuff pre-defined really
+ */
+
+int analyse_words(ez_list *list, int suffix, const char *words) {
+       size_t len = strlen(words);
+       char word[len+1]; // + ??
+       wchar_t lwords[len+1];
+       int count = 0;
+       const char *t = words;
+       mbstate_t state = { 0 };
+
+       len = mbsrtowcs(lwords, &t, len+1, &state);
+       if (len == (size_t)-1) {
+               fprintf(stderr, "'%s' @ '%s'", words, t);
+               perror(" failed");
+               for (int i=0;i<strlen(words);i++)
+                       fprintf(stderr, " %02x", words[i] & 0xff);
+               fprintf(stderr, "\n");
+               return -1;
+       }
+
+       //printf("%ls\n", lwords);
+
+       wchar_t c;
+       wchar_t *p = lwords;
+       wchar_t *w = lwords;
+       do {
+               c = *p++;
+
+               if (iswcollapse(c)) {
+                       // nopx
+               } else if (iswgraph(c) && !iswpunct(c)) {
+                       *w++ = towlower(c);
+               } else {
+                       wchar_t *s = lwords;
+
+                       *w = 0;
+
+                       // TODO: could keep track of start of each multi-byte char and just write those out
+
+                       while (w - s >= 3) {
+                               const wchar_t *t = s++;
+                               mbstate_t state = { 0 };
+
+                               len = wcsrtombs(word, &t, sizeof(word), &state);
+                               if (len < sizeof(word)) {
+                                       struct string_node *string = malloc(sizeof(*string) + len + 1);
+
+                                       strcpy(string->value, word);
+                                       ez_list_addtail(list, string);
+                                       count++;
+
+                                       if (!suffix)
+                                               break;
+                               } else {
+                                       fprintf(stderr, "overflow %s\n", words);
+                               }
+                       }
+                       w = lwords;
+               }
+       } while (c);
+
+       return count;
+}
+
+void analyse_free(ez_list *list) {
+       struct string_node *w;
+
+       while ((w = ez_list_remhead(list)))
+               free(w);
+}
+#if 0
+int main(int argc, char **argv) {
+       ez_list list = EZ_INIT_LIST(list);
+
+       analyse_words(&list, 0, "this, is a word? Foo-bar O'callahan");
+       analyse_words(&list, 1, "this, is a word? Foo-bar O'callahan");
+
+       analyse_free(&list);
+}
+#endif
diff --git a/analyse.h b/analyse.h
new file mode 100644 (file)
index 0000000..a3688ff
--- /dev/null
+++ b/analyse.h
@@ -0,0 +1,39 @@
+/* analyse.h: Word suffix analysis
+
+   Copyright (C) 2020 Michael Zucchi
+
+   This program is free software: you can redistribute it and/or
+   modify it under the terms of the GNU General Public License as
+   published by the Free Software Foundation, either version 3 of the
+   License, or (at your option) any later version.
+
+   This program is distributed in the hope that it will be useful, but
+   WITHOUT ANY WARRANTY; without even the implied warranty of
+   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+   General Public License for more details.
+
+   You should have received a copy of the GNU General Public License
+   along with this program. If not, see
+   <http://www.gnu.org/licenses/>.
+*/
+
+struct string_node {
+       ez_node ln;
+       char value[0];
+};
+
+/**
+ * Break a string into multiple search tokens optionally
+ * creating all suffixes.
+ *
+ * The basic algorithm is to break strings up on spaces
+ * and puncutation and convert them to lower-case.  Some
+ * characters such as quotes are dropped instead.
+ *
+ * @param list output list of struct string_node values
+ * @param suffix if set then expand all suffixes
+ * @param words NUL-terminated string
+ */
+int analyse_words(ez_list *list, int suffix, const char *words);
+
+void analyse_free(ez_list *list);
diff --git a/blobs.c b/blobs.c
new file mode 100644 (file)
index 0000000..0f47034
--- /dev/null
+++ b/blobs.c
@@ -0,0 +1,74 @@
+/* blobs.c: Database and IPC data structures.
+
+   Copyright (C) 2021 Michael Zucchi
+
+   This program is free software: you can redistribute it and/or
+   modify it under the terms of the GNU General Public License as
+   published by the Free Software Foundation, either version 3 of the
+   License, or (at your option) any later version.
+
+   This program is distributed in the hope that it will be useful, but
+   WITHOUT ANY WARRANTY; without even the implied warranty of
+   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+   General Public License for more details.
+
+   You should have received a copy of the GNU General Public License
+   along with this program. If not, see
+   <http://www.gnu.org/licenses/>.
+*/
+
+#include <stdint.h>
+#include <sys/time.h>
+
+#include "dbindex.h"
+#include "notify.h"
+
+#include "ez-blob.h"
+
+ez_blob_desc DBDISK_DESC[] = {
+       EZ_BLOB_START(dbdisk, 1, 4),
+       EZ_BLOB_STRING(dbdisk, 1, uuid),
+       EZ_BLOB_STRING(dbdisk, 2, label),
+       EZ_BLOB_STRING(dbdisk, 3, type),
+       EZ_BLOB_STRING(dbdisk, 4, mount),
+};
+
+ez_blob_desc DBFILE_DESC[] = {
+       EZ_BLOB_START(dbfile, 2, 8),
+       EZ_BLOB_INT32(dbfile, 1, diskid),
+       EZ_BLOB_INT64(dbfile, 2, size),
+       EZ_BLOB_INT64(dbfile, 3, mtime),
+       EZ_BLOB_INT64(dbfile, 4, duration),
+       EZ_BLOB_STRING(dbfile, 5, path),
+       EZ_BLOB_STRING(dbfile, 6, title),
+       EZ_BLOB_STRING(dbfile, 7, artist),
+       EZ_BLOB_TRANSIENTP(dbfile, 8, full_path),
+};
+
+ez_blob_desc DBLIST_DESC[] = {
+       EZ_BLOB_START(dblist, 3, 3),
+       EZ_BLOB_INT32(dblist, 1, size),
+       EZ_BLOB_STRING(dblist, 2, name),
+       EZ_BLOB_STRING(dblist, 3, comment),
+};
+
+ez_blob_desc PLAY_SEEK_DESC[] = {
+       EZ_BLOB_START(struct notify_play_seek, 1, 2),
+       EZ_BLOB_INT32(struct notify_play_seek, 1, mode),
+       EZ_BLOB_FLOAT64(struct notify_play_seek, 2, stamp),
+};
+
+ez_blob_desc DEBUG_DESC[] = {
+       EZ_BLOB_START(struct notify_debug, 2, 1),
+       EZ_BLOB_INT32(struct notify_debug, 1, func),
+};
+
+ez_blob_desc KEY_DESC[] = {
+       EZ_BLOB_START(struct notify_key, 3, 1),
+       EZ_BLOB_INT32(struct notify_key, 1, code),
+};
+
+ez_blob_desc GOTO_DESC[] = {
+       EZ_BLOB_START(struct notify_goto, 4, 1),
+       EZ_BLOB_INT32(struct notify_goto, 1, fileid),
+};
index 8309a6b..76d6596 100644 (file)
--- a/dbindex.c
+++ b/dbindex.c
@@ -16,6 +16,9 @@
    along with this program. If not, see
    <http://www.gnu.org/licenses/>.
 */
+
+// TODO: list.size is really the next id, not size if list items are deleted
+
 #include <stddef.h>
 #include <stdlib.h>
 #include <string.h>
 #include <assert.h>
 
 #include <lmdb.h>
+#include <errno.h>
 
 #include "dbindex.h"
 #include "ez-blob.h"
+#include "ez-blob-basic.h"
+
+// prototype
+void dblist_dump(dbtxn *txn, dbindex *db);
 
 ez_blob_desc DBDISK_DESC[] = {
-       EZ_BLOB_START(dbdisk),
+       EZ_BLOB_START(dbdisk, 1, 4),
        EZ_BLOB_STRING(dbdisk, 1, uuid),
        EZ_BLOB_STRING(dbdisk, 2, label),
        EZ_BLOB_STRING(dbdisk, 3, type),
        EZ_BLOB_STRING(dbdisk, 4, mount),
-       EZ_BLOB_END(dbdisk)
 };
 
 ez_blob_desc DBFILE_DESC[] = {
-       EZ_BLOB_START(dbfile),
+       EZ_BLOB_START(dbfile, 2, 7),
        EZ_BLOB_INT32(dbfile, 1, diskid),
        EZ_BLOB_INT64(dbfile, 2, size),
        EZ_BLOB_INT64(dbfile, 3, mtime),
@@ -47,7 +54,62 @@ ez_blob_desc DBFILE_DESC[] = {
        EZ_BLOB_STRING(dbfile, 5, path),
        EZ_BLOB_STRING(dbfile, 6, title),
        EZ_BLOB_STRING(dbfile, 7, artist),
-       EZ_BLOB_END(dbfile)
+       EZ_BLOB_TRANSIENTP(dbfile, 8, full_path),
+};
+
+/*
+TODO: playlist should be linked list
+
+  [list] -> [0000][frst][last]
+  [list] -> [file][next][prev]
+
+ */
+
+/*
+  playlist storage.
+
+  require ordering, so store by index.
+  reverse index?
+
+  ** bad ** too wasteful **
+  Use 64-bit index
+
+  [playlist.id][order] -> [file.id]
+
+  query as > [playlist.id][order] < [playlisti.id+1]
+
+  Reverse:
+
+  [playlist.id][file.id] -> [seq]
+
+
+
+  Alternative:
+
+  forward: list_by_file [list.id] -> [seq][file.id] with custom dupsort compare
+  reverse: file_by_list [file.id] -> [list.id][seq]
+
+  reverse is required to navigate the playlist properly if the sequence order changes.
+  alternative idea: just use the shuffle list as the playlist always?
+ */
+
+/* Value stored in file-by-list */
+struct dbfilelist {
+       uint32_t seq;
+       uint32_t fileid;
+};
+
+/* Value stored in list-by-file */
+struct dblistfile {
+       uint32_t listid;
+       uint32_t seq;
+};
+
+ez_blob_desc DBLIST_DESC[] = {
+       EZ_BLOB_START(dblist, 3, 3),
+       EZ_BLOB_INT32(dblist, 1, size),
+       EZ_BLOB_STRING(dblist, 2, name),
+       EZ_BLOB_STRING(dblist, 3, comment),
 };
 
 struct dbindex {
@@ -60,12 +122,20 @@ struct dbindex {
        MDB_dbi disk;
        MDB_dbi disk_by_uuid;   // key is uuid                UNIQUE
 
+       MDB_dbi list;           // playlist to name
+
        MDB_dbi file;
-       MDB_dbi file_by_path;   // key is "diskid{hex}/path"  UNIQUE
+       MDB_dbi file_by_path;   // key is "diskid{hex}/path"  UNIQUE  TODO: limited to 511 bytes length
        MDB_dbi file_by_disk;   // key is diskid              FOREIGN
        MDB_dbi file_by_title;  // key is title  (maybe all lower case?)
        MDB_dbi file_by_artist; // key is artist
 
+       MDB_dbi file_by_list;   // key is list, secondary is [seq][fileid] but only sorted/keyed on [seq]
+       MDB_dbi list_by_file;   // key is file, secondary is [listid][seq]
+
+       MDB_dbi file_by_suffix;
+
+
        // ? maybe it should be a playlist ?
        MDB_dbi shuffle;        // seq to file
        MDB_dbi shuffle_by_file;// file to seq                FOREIGN
@@ -73,6 +143,7 @@ struct dbindex {
        // This only works for threads not processes
        // single writer I guess.
        volatile uint32_t diskid;
+       volatile uint32_t listid;
        volatile uint32_t fileid;
 };
 
@@ -80,6 +151,10 @@ static uint32_t disk_next_id(dbindex *db) {
        return __atomic_fetch_add(&db->diskid, 1, __ATOMIC_SEQ_CST);
 }
 
+static uint32_t list_next_id(dbindex *db) {
+       return __atomic_fetch_add(&db->listid, 1, __ATOMIC_SEQ_CST);
+}
+
 static uint32_t file_next_id(dbindex *db) {
        return __atomic_fetch_add(&db->fileid, 1, __ATOMIC_SEQ_CST);
 }
@@ -108,6 +183,12 @@ static uint32_t find_next_id(MDB_txn *tx, MDB_dbi db) {
        return id;
 }
 
+static int
+cmp_uint(const MDB_val *a, const MDB_val *b) {
+        return (*(unsigned int *)a->mv_data < *(unsigned int *)b->mv_data) ? -1 :
+                *(unsigned int *)a->mv_data > *(unsigned int *)b->mv_data;
+}
+
 dbindex *dbindex_open(const char *path) {
        dbindex *db = calloc(sizeof(*db), 1);
        int res;
@@ -136,16 +217,31 @@ dbindex *dbindex_open(const char *path) {
        res |= mdb_dbi_open(tx, "disk", MDB_CREATE | MDB_INTEGERKEY, &db->disk);
        res |= mdb_dbi_open(tx, "disk#uuid", MDB_CREATE, &db->disk_by_uuid);
 
+       res |= mdb_dbi_open(tx, "list", MDB_CREATE | MDB_INTEGERKEY, &db->list);
+
        res |= mdb_dbi_open(tx, "file", MDB_CREATE | MDB_INTEGERKEY, &db->file);
        res |= mdb_dbi_open(tx, "file#path", MDB_CREATE, &db->file_by_path);
        res |= mdb_dbi_open(tx, "file#disk", MDB_CREATE | MDB_INTEGERKEY | MDB_DUPSORT | MDB_DUPFIXED | MDB_INTEGERDUP , &db->file_by_disk);
        res |= mdb_dbi_open(tx, "file#title", MDB_CREATE | MDB_DUPSORT | MDB_DUPFIXED | MDB_INTEGERDUP, &db->file_by_title);
        res |= mdb_dbi_open(tx, "file#artist", MDB_CREATE | MDB_DUPSORT | MDB_DUPFIXED | MDB_INTEGERDUP, &db->file_by_artist);
 
+       res |= mdb_dbi_open(tx, "file#list", MDB_CREATE | MDB_INTEGERKEY | MDB_DUPSORT | MDB_DUPFIXED, &db->file_by_list);
+       mdb_set_dupsort(tx, db->file_by_list, cmp_uint);
+       res |= mdb_dbi_open(tx, "list#file", MDB_CREATE | MDB_INTEGERKEY | MDB_DUPSORT | MDB_DUPFIXED, &db->list_by_file);
+       //mdb_set_dupsort(tx, db->list_by_file, cmp_uint);
+
+       // to be replaced with file#list perhaps?
        res |= mdb_dbi_open(tx, "shuffle", MDB_CREATE | MDB_INTEGERKEY, &db->shuffle);
        res |= mdb_dbi_open(tx, "shuffle#file", MDB_CREATE | MDB_INTEGERKEY, &db->shuffle_by_file);
 
+       // experimental substring search
+       //res |= mdb_dbi_open(tx, "file#suffix", MDB_CREATE | MDB_DUPSORT | MDB_DUPFIXED | MDB_INTEGERDUP , &db->file_by_suffix);
+       //mdb_drop(tx, db->file_by_suffix, 1);
+       res |= mdb_dbi_open(tx, "file#suffix", MDB_CREATE | MDB_DUPSORT | MDB_DUPFIXED | MDB_INTEGERDUP , &db->file_by_suffix);
+
+
        db->diskid = find_next_id(tx, db->disk);
+       db->listid = find_next_id(tx, db->list);
        db->fileid = find_next_id(tx, db->file);
 
        {
@@ -157,7 +253,8 @@ dbindex *dbindex_open(const char *path) {
                mdb_cursor_open(tx, db->disk, &cursor);
                r = mdb_cursor_get(cursor, &key, &data, MDB_FIRST);
                while (r == 0) {
-                       dbdisk *p = ez_blob_decode(DBDISK_DESC, data.mv_data, data.mv_size);
+                       dbdisk *p = ez_basic_decode(DBDISK_DESC, (ez_blob *)&data);
+                       p->id = *(int *)key.mv_data;
                        printf("id=%d\n", p->id);
                        printf(" uuid=%s\n", p->uuid);
                        printf(" label=%s\n", p->label);
@@ -178,7 +275,7 @@ dbindex *dbindex_open(const char *path) {
                db = NULL;
        }
 
-       printf("dbindex open, disk.id=%d file.id=%d\n", db->diskid, db->fileid);
+       printf("dbindex open, disk.id=%d list.id=%d file.id=%d\n", db->diskid, db->listid, db->fileid);
 
        return db;
  fail:
@@ -206,8 +303,14 @@ MDB_txn *dbindex_begin(dbindex *db, dbtxn *txn, int readonly) {
        return tx;
 }
 
-void dbindex_commit(MDB_txn *tx) {
-       mdb_txn_commit(tx);
+int dbindex_commit(MDB_txn *tx) {
+       int res = mdb_txn_commit(tx);
+
+       if (res != 0) {
+               printf("commit failed: %s\n", mdb_strerror(res));
+       }
+
+       return res;
 }
 
 void dbindex_abort(MDB_txn *tx) {
@@ -226,6 +329,8 @@ int dbstate_get(MDB_txn *tx, dbindex *db, dbstate *s) {
                        return MDB_NOTFOUND;
                *s = *p;
                return 0;
+       } else {
+               printf("dbstate get: %s\n", mdb_strerror(db->res));
        }
 
        return db->res;
@@ -256,9 +361,8 @@ static void *primary_get_decode(MDB_txn *tx, dbindex *db, ez_blob_desc *desc, MD
        db->res = mdb_get(tx, primary, key, &data);
 
        if (db->res == 0) {
-               void *p = ez_blob_decode(desc, data.mv_data, data.mv_size);
+               void *p = ez_basic_decode(desc, (ez_blob *)&data);
 
-               assert(desc[0].bd_type == EZ_BLOB_PK);
                assert(key->mv_size == sizeof(int));
 
                memcpy(p, key->mv_data, sizeof(int));
@@ -269,7 +373,7 @@ static void *primary_get_decode(MDB_txn *tx, dbindex *db, ez_blob_desc *desc, MD
 }
 
 /**
- * Retrieve and decode data based on unique secondayry key.
+ * Retrieve and decode data based on unique secondary key.
  *
  * @param secondary key to retrieve
  * @param data holder
@@ -316,19 +420,11 @@ int dbdisk_add(MDB_txn *txn, dbindex *db, dbdisk *d) {
        key.mv_data = &d->id;
        key.mv_size = sizeof(d->id);
 
-       if (1) {
-               data.mv_size = ez_blob_size(DBDISK_DESC, d);
-               data.mv_data = NULL;
-               res = mdb_put(tx, db->disk, &key, &data, MDB_NOOVERWRITE | MDB_RESERVE);
-               if (res == 0)
-                       ez_blob_encode_raw(DBDISK_DESC, d, data.mv_data, data.mv_size);
-       } else {
-               void *blob = ez_blob_encode(DBDISK_DESC, d, &data.mv_size);
-
-               data.mv_data = blob;
-               res = mdb_put(tx, db->disk, &key, &data, MDB_NOOVERWRITE);
-               free(blob);
-       }
+       data.mv_size = ez_basic_size(DBDISK_DESC, d);
+       data.mv_data = NULL;
+       res = mdb_put(tx, db->disk, &key, &data, MDB_NOOVERWRITE | MDB_RESERVE);
+       if (res == 0)
+               ez_basic_encode_raw(DBDISK_DESC, d, (ez_blob *)&data);
 
        if (res != 0) {
                printf("db put disk fail: %s\n", mdb_strerror(res));
@@ -357,6 +453,96 @@ int dbdisk_add(MDB_txn *txn, dbindex *db, dbdisk *d) {
 
 }
 
+static uint32_t *scan_all(dbscan *scan, ssize_t *sizep) {
+       int fid;
+       int fids_size = 4096;
+       int count = 0;
+       uint32_t *fids = malloc(sizeof(*fids) * fids_size);
+
+       if (!fids)
+               goto fail;
+
+       while ((fid = dbfile_scan_next(scan)) != ~0) {
+               if (count >= fids_size) {
+                       uint32_t *tmp;
+
+                       fids_size *= 2;
+                       tmp = realloc(fids, sizeof(*fids) * fids_size);
+                       if (!tmp)
+                               goto fail;
+                       fids = tmp;
+               }
+               fids[count++] = fid;
+       }
+
+       *sizep = count;
+       return fids;
+fail:
+       free(fids);
+       *sizep = -1;
+       return NULL;
+}
+
+int dbdisk_del(dbtxn *txn, dbindex *db, dbdisk *disk) {
+       MDB_txn *tx;
+
+       mdb_txn_begin(db->env, txn, 0, &tx);
+
+       dbscan *scan = dbfile_scan_disk(tx, db, disk->id);
+       uint32_t *fids = NULL;
+       ssize_t count;
+       int res = 0;
+
+       fids = scan_all(scan, &count);
+       if (count == -1) {
+               res = ENOMEM;
+               goto fail;
+       }
+
+       for (int i=0;i<count;i++) {
+               printf(" file %d\n", fids[i]);
+               if (res = dbfile_del_id(tx, db, fids[i]))
+                       goto fail;
+       }
+
+       // secondary keys
+       MDB_val key;
+
+       // -- by uuid
+       key.mv_data = disk->uuid;
+       key.mv_size = strlen(disk->uuid);
+       if (res = mdb_del(tx, db->disk_by_uuid, &key, NULL))
+               goto fail;
+
+       // Remove disk
+       key.mv_data = &disk->id;
+       key.mv_size = sizeof(disk->id);
+       if (res = mdb_del(tx, db->disk, &key, NULL))
+               goto fail;
+
+       printf("ok\n");
+       free(fids);
+       mdb_txn_commit(tx);
+       //mdb_txn_abort(tx);
+       return 0;
+
+fail:
+       free(fids);
+       mdb_txn_abort(tx);
+       return res;
+}
+
+int dbdisk_del_id(dbtxn *tx, dbindex *db, int diskid) {
+       dbdisk *d = dbdisk_get(tx, db, diskid);
+
+       if (d) {
+              db->res = dbdisk_del(tx, db, d);
+              dbdisk_free(d);
+       }
+
+       return db->res;
+}
+
 dbfile *dbfile_get_path(MDB_txn *tx, dbindex *db, int diskid, const char *path) {
        char name[strlen(path) + 9];
        MDB_val key;
@@ -370,14 +556,50 @@ dbfile *dbfile_get_path(MDB_txn *tx, dbindex *db, int diskid, const char *path)
        return secondary_get_decode(tx, db, DBFILE_DESC, &key, db->file, db->file_by_path);
 }
 
+char *dbfile_full_path(dbtxn *tx, dbindex *db, dbfile *file) {
+       if (!file->full_path) {
+               dbdisk *disk = dbdisk_get(tx, db, file->diskid);
+
+               if (file->full_path = malloc(strlen(disk->mount) + strlen(file->path) + 1))
+                       sprintf(file->full_path, "%s%s", disk->mount, file->path);
+
+               dbdisk_free(disk);
+       }
+
+       return file->full_path;
+}
+
 void dbfile_free(dbfile *f) {
-       ez_blob_free(DBFILE_DESC, f);
+       if (f) {
+               free(f->full_path);
+               ez_blob_free(DBFILE_DESC, f);
+       }
 }
 
+#include "dbmarshal.h"
+
 dbfile *dbfile_get(dbtxn *tx, dbindex *db, int fileid) {
        MDB_val key = { .mv_data = &fileid, .mv_size = sizeof(fileid) };
+#if 1
+       MDB_val data;
+
+       db->res = mdb_get(tx, db->file, &key, &data);
+
+       printf("dbfile_get(%d) = %d\n", fileid, db->res);
+
+       if (db->res == 0) {
+               dbfile *p = calloc(1, sizeof(*p));
+
+               dbfile_decode_raw((ez_blob *)&data, p);
 
+               p->id = fileid;
+               return p;
+       }
+
+       return NULL;
+#else
        return primary_get_decode(tx, db, DBFILE_DESC, &key, db->file);
+#endif
 }
 
 int dbfile_del_id(dbtxn *tx, dbindex *db, int fileid) {
@@ -386,6 +608,8 @@ int dbfile_del_id(dbtxn *tx, dbindex *db, int fileid) {
        if (f) {
               db->res = dbfile_del(tx, db, f);
               dbfile_free(f);
+       } else {
+               printf("no such file: %d\n", fileid);
        }
 
        return db->res;
@@ -440,10 +664,57 @@ int dbfile_del(dbtxn *txn, dbindex *db, dbfile *f) {
        if (res = mdb_del(tx, db->file, &key, NULL))
                goto fail;
 
+       // check lists
+       {
+               MDB_cursor *cursor;
+               size_t alloc = 256;
+               size_t size = 0;
+               struct dblistfile *list = malloc(sizeof(*list) * alloc);
+
+               // FIXME: goto fail leaks list
+               if ((res = mdb_cursor_open(tx, db->list_by_file, &cursor)))
+                       goto fail;
+
+               res = mdb_cursor_get(cursor, &key, &data, MDB_SET);
+               printf("set list by file: %d\n", res);
+               while (res == 0) {
+                       printf(" list: %d @ %d\n", ((struct dblistfile *)data.mv_data)->listid, ((struct dblistfile *)data.mv_data)->seq);
+                       if (size >= alloc) {
+                               alloc *= 2;
+                               list = realloc(list, sizeof(*list) * alloc);
+                       }
+                       list[size++] = *(struct dblistfile *)data.mv_data;
+
+                       res = mdb_cursor_get(cursor, &key, &data, MDB_NEXT_DUP);
+               }
+               mdb_cursor_close(cursor);
+
+               if (res = mdb_del(tx, db->list_by_file, &key, NULL))
+                       goto fail;
+
+               printf("list entries: %zd\n", size);
+
+               for (int i=0;i<size;i++) {
+                       struct dbfilelist fdata = {
+                               .seq = list[i].seq,
+                               .fileid = f->id
+                       };
+                       printf("delete file %d from list %d @ %d\n", fdata.fileid, list[i].listid, fdata.seq);
+                       key.mv_data = &list[i].listid;
+                       key.mv_size = sizeof(list[i].listid);
+                       data.mv_data = &fdata;
+                       data.mv_size = sizeof(fdata);
+                       if (res = mdb_del(tx, db->file_by_list, &key, &data))
+                               goto fail;
+               }
+               free(list);
+       }
+
        mdb_txn_commit(tx);
        return res;
 
  fail:
+       printf("del failed: %s\n", mdb_strerror(res));
        mdb_txn_abort(tx);
        return res;
 }
@@ -511,11 +782,11 @@ int dbfile_update(dbtxn *txn, dbindex *db, dbfile *o, dbfile *f) {
        key.mv_data = &f->id;
        key.mv_size = sizeof(f->id);
 
-       data.mv_size = ez_blob_size(DBFILE_DESC, f);
+       data.mv_size = ez_basic_size(DBFILE_DESC, f);
        if (res = mdb_put(tx, db->file, &key, &data, MDB_RESERVE))
                goto fail;
 
-       ez_blob_encode_raw(DBFILE_DESC, f, data.mv_data, data.mv_size);
+       ez_basic_encode_raw(DBFILE_DESC, f, (ez_blob *)&data);
 
        return mdb_txn_commit(tx);
 
@@ -546,18 +817,10 @@ int dbfile_add(MDB_txn *txn, dbindex *db, dbfile *f) {
        key.mv_data = &f->id;
        key.mv_size = sizeof(f->id);
 
-       if (1) {
-               data.mv_size = ez_blob_size(DBFILE_DESC, f);
-               res = mdb_put(tx, db->file, &key, &data, MDB_NOOVERWRITE | MDB_RESERVE);
-               if (res == 0)
-                       ez_blob_encode_raw(DBFILE_DESC, f, data.mv_data, data.mv_size);
-       } else {
-               void *blob = ez_blob_encode(DBFILE_DESC, f, &data.mv_size);
-
-               data.mv_data = blob;
-               res = mdb_put(tx, db->file, &key, &data, MDB_NOOVERWRITE);
-               free(blob);
-       }
+       data.mv_size = ez_basic_size(DBFILE_DESC, f);
+       res = mdb_put(tx, db->file, &key, &data, MDB_NOOVERWRITE | MDB_RESERVE);
+       if (res == 0)
+               ez_basic_encode_raw(DBFILE_DESC, f, (ez_blob *)&data);
 
        if (res != 0) {
                printf("db put file fail: %s\n", mdb_strerror(res));
@@ -610,7 +873,8 @@ int dbfile_add(MDB_txn *txn, dbindex *db, dbfile *f) {
 }
 
 
-// TODO: this can be made generic for other indices
+// TODO: this can be made generic for other indices, see later on
+#if 0
 struct dbscan {
        dbindex *db;
        MDB_cursor *cursor;
@@ -695,7 +959,7 @@ void dbfile_scan_close(dbscan *scan) {
                mdb_cursor_close(scan->cursor);
        free(scan);
 }
-
+#endif
 
 /**
  * Create a newly shuffled playlist.
@@ -747,10 +1011,105 @@ void dbshuffle_init(dbindex *db) {
        dbindex_commit(tx);
 }
 
+// create shuffled playlist
+// TODO: start from an existing playlist?
+void dbshuffle_init2(dbindex *db) {
+       dbtxn *tx;
+       dbscan *scan;
+       uint32_t fid;
+       int fids_size = 4096;
+       int count = 0;
+       int res;
+       uint32_t *fids = malloc(sizeof(*fids) * fids_size);
+
+       // TODO: count? just get it from the thing and scan aagain?
+       // find all current fids
+       mdb_txn_begin(db->env, NULL, 0, &tx);
+
+       scan = dbfile_scan_disk(tx, db, -1);
+       while ((fid = dbfile_scan_next(scan)) != ~0) {
+               if (count >= fids_size) {
+                       fids_size *= 2;
+                       fids = realloc(fids, sizeof(*fids) * fids_size);
+               }
+               fids[count++] = fid;
+       }
+       printf("total %d\n", count);
+       dbfile_scan_close(scan);
+
+       struct dblist list = {
+               .size = count,
+               .name = "shuffle", // maybe i do want them unique after-all?
+               .comment = ""
+       };
+       struct dbfilelist fvalue;
+       struct dblistfile rvalue;
+       MDB_val fkey, fdata;
+       MDB_val rkey, rdata;
+
+       if ((res = dblist_add(tx, db, &list))) {
+               printf("add list\n");
+               goto fail;
+       }
+
+       printf("list add ok id=%d\n", list.id);
+
+       fkey.mv_data = &list.id;
+       fkey.mv_size = sizeof(uint32_t);
+       fdata.mv_data = &fvalue;
+       fdata.mv_size = sizeof(fvalue);
+
+       rkey.mv_size = sizeof(uint32_t);
+       rdata.mv_data = &rvalue;
+       rdata.mv_size = sizeof(rvalue);
+       rvalue.listid = list.id;
+
+       // TODO: can shuffle have repeats??
+
+       // Playlist instead:
+       // [list] [seq][file]
+       // ... only iterate by seq?
+       // ... how to find playlist by file?
+       // ... or just jump by seq?
+
+       for (int i=0;i<count;i++) {
+               int j = random() % (count-i);
+               uint32_t seq = i + 1;
+
+               fid = fids[i+j];
+               fids[i+j] = fids[i];
+
+               fvalue.seq = seq;
+               fvalue.fileid = fid;
+
+               rvalue.seq = seq;
+               rkey.mv_data = &fid;
+
+               printf(" %d->%d\n", seq, fid);
+
+               if ((res = mdb_put(tx, db->file_by_list, &fkey, &fdata, MDB_NODUPDATA)))
+                       goto fail;
+
+               if ((res = mdb_put(tx, db->list_by_file, &rkey, &rdata, MDB_NODUPDATA)))
+                       goto fail;
+       }
+       free(fids);
+
+       dbindex_commit(tx);
+       return;
+fail:
+       printf("reason: %s\n", mdb_strerror(res));
+       free(fids);
+       mdb_txn_abort(tx);
+       return;
+}
+
 /*
   Player support functions
 */
 
+// A way to iterate through a lit of files, based on an index or something else
+
 #include <sys/types.h>
 #include <sys/stat.h>
 #include <unistd.h>
@@ -763,6 +1122,9 @@ void dbshuffle_init(dbindex *db) {
  * disk-monitor managing the mounts.
  *
  * It can be used for quickly discarding files that can't be mounted.
+ *
+ * This is super-slow, don't bother using it, performing a stat on the file
+ * will suffice.
  */
 int dbdisk_mounted(dbdisk *disk) {
 #if 0
@@ -784,13 +1146,14 @@ int dbdisk_mounted(dbdisk *disk) {
        return 0;
 #else
        // See if the directory is empty
+       // yikes, this is slow as fuck
        DIR *d = opendir(disk->mount);
        int entries = 0;
 
        if (d) {
                struct dirent *de;
 
-               while (de = readdir(d)) {
+               while (entries == 0 && (de = readdir(d))) {
                        if (strcmp(de->d_name, ".") == 0
                            || strcmp(de->d_name, "..") == 0)
                                continue;
@@ -947,10 +1310,10 @@ static int dbfile_iterate_shuffle(dbindex *db, dbfile **fp, char **pathp, int fi
         */
        int keyval =  *fp ? ((*fp)->id) : -1;
        dbdisk *disk = *fp ? dbdisk_get(tx, db, (*fp)->diskid) : NULL;
-       int mounted = *fp ? dbdisk_mounted(disk) : 0;
+       //int mounted = *fp ? dbdisk_mounted(disk) : 0;
 
 
-       printf("shuffle next, fid=%d\n", keyval);
+       //printf("shuffle next, fid=%d\n", keyval);
 
        dbfile_free(*fp);
        free(*pathp);
@@ -962,18 +1325,17 @@ static int dbfile_iterate_shuffle(dbindex *db, dbfile **fp, char **pathp, int fi
                data.mv_size = sizeof(keyval);
 
                res = mdb_get(tx, db->shuffle_by_file, &data, &key);
-               printf("get by file = %d, id=%d\n", res, *((int *)key.mv_data));
+               //printf("get by file = %d, id=%d\n", res, *((int *)key.mv_data));
                if (res == MDB_NOTFOUND) {
-                       printf("Not found\n");
-
+                       //printf("Not found\n");
                        return -1;
                }
 
                res = mdb_cursor_get(cursor, &key, &data, MDB_SET);
-               printf(" got shuffle id=%d fid=%d\n", *((int *)key.mv_data), *(int *)data.mv_data);
+               //printf(" got shuffle id=%d fid=%d\n", *((int *)key.mv_data), *(int *)data.mv_data);
 
                res = mdb_cursor_get(cursor, &key, &data, next);
-               printf(" next shuffle id=%d fid=%d\n", *((int *)key.mv_data), *(int *)data.mv_data);
+               //printf(" next shuffle id=%d fid=%d\n", *((int *)key.mv_data), *(int *)data.mv_data);
        } else {
                res = mdb_cursor_get(cursor, &key, &data, first);
        }
@@ -983,15 +1345,16 @@ static int dbfile_iterate_shuffle(dbindex *db, dbfile **fp, char **pathp, int fi
                if (file) {
                        int keep;
 
-                       printf("loaded: %d[%zd] %d?\n", *(int *)data.mv_data, data.mv_size, file->id);
+                       //printf("loaded: %d[%zd] %d?\n", *(int *)data.mv_data, data.mv_size, file->id);
 
                        if (disk == NULL || file->diskid != disk->id) {
                                dbdisk_free(disk);
                                disk = dbdisk_get(tx, db, file->diskid);
-                               mounted = dbdisk_mounted(disk);
+                               //mounted = dbdisk_mounted(disk);
                        }
-                       keep = mounted;
-                       keep = keep && file->duration > 0;
+                       //keep = mounted;
+                       //keep = keep && file->duration > 0;
+                       keep = file->duration > 0;
                        if (keep) {
                                char path[strlen(disk->mount) + strlen(file->path) + 1];
                                struct stat st;
@@ -1020,7 +1383,7 @@ static int dbfile_iterate_shuffle(dbindex *db, dbfile **fp, char **pathp, int fi
        mdb_cursor_close(cursor);
        mdb_txn_commit(tx);
 
-       printf("laoded fid=%d\n", file->id);
+       //printf("laoded fid=%d\n", file->id);
 
        return res;
  fail:
@@ -1049,3 +1412,1366 @@ int dbfile_prev_shuffle(dbindex *db, dbfile **f, char **fpath) {
        //res = mdb_cursor_get(scan->cursor, &scan->key, &scan->data, MDB_GET_BOTH);
 
 */
+
+/* playlist management */
+dblist *dblist_get(dbtxn *tx, dbindex *db, int id) {
+       MDB_val key = { .mv_data = &id, .mv_size = sizeof(id) };
+
+       return primary_get_decode(tx, db, DBLIST_DESC, &key, db->list);
+}
+
+void dblist_free(dblist *f) {
+       ez_blob_free(DBLIST_DESC, f);
+}
+
+int dblist_reset(dbtxn *tx, dbindex *db) {
+       mdb_drop(tx, db->list, 0);
+       mdb_drop(tx, db->file_by_list, 0);
+       return mdb_drop(tx, db->list_by_file, 0);
+}
+
+// put ?  add ?  d->id == 0 -> then add, otherwise put?
+int dblist_add(MDB_txn *txn, dbindex *db, dblist *d) {
+       MDB_txn *tx;
+       MDB_val key, data;
+       int res;
+
+       mdb_txn_begin(db->env, txn, 0, &tx);
+
+       // Store record
+       d->id = list_next_id(db);
+       key.mv_data = &d->id;
+       key.mv_size = sizeof(d->id);
+
+       data.mv_size = ez_basic_size(DBLIST_DESC, d);
+       data.mv_data = NULL;
+       res = mdb_put(tx, db->list, &key, &data, MDB_NOOVERWRITE | MDB_RESERVE);
+       if (res == 0) {
+               ez_basic_encode_raw(DBLIST_DESC, d, (ez_blob *)&data);
+               mdb_txn_commit(tx);
+       } else {
+               printf("db put list fail: %s\n", mdb_strerror(res));
+               mdb_txn_abort(tx);
+       }
+
+       return res;
+}
+
+int dblist_del(dbtxn *txn, dbindex *db, int listid) {
+       MDB_txn *tx;
+       MDB_val key, data;
+       MDB_cursor *cursor;
+       int res;
+
+       mdb_txn_begin(db->env, txn, 0, &tx);
+
+       dblist_dump(tx, db);
+
+       key.mv_data = &listid;
+       key.mv_size = sizeof(listid);
+       if (res = mdb_del(tx, db->list, &key, NULL))
+               goto fail;
+
+       if (res = mdb_del(tx, db->file_by_list, &key, NULL))
+               goto fail;
+
+       res = mdb_cursor_open(tx, db->list_by_file, &cursor);
+
+       printf("delete reverse list\n");
+       res = mdb_cursor_get(cursor, &key, &data, MDB_FIRST);
+       while (res == 0) {
+               struct dblistfile *value = (struct dblistfile *)data.mv_data;
+               printf("check %d %d:%d\n", *(uint32_t*)key.mv_data, value->listid, value->seq);
+
+               if (value->listid == listid) {
+                       printf("delete\n");
+                       if (res = mdb_cursor_del(cursor, 0))
+                               goto fail;
+                       res = mdb_cursor_get(cursor, &key, &data, MDB_GET_CURRENT);
+               } else {
+                       res = mdb_cursor_get(cursor, &key, &data, MDB_NEXT_DUP);
+               }
+               printf("next dup: %s\n", mdb_strerror(res));
+               if (res == MDB_NOTFOUND) {
+                       res = mdb_cursor_get(cursor, &key, &data, MDB_NEXT);
+                       printf("next: %s\n", mdb_strerror(res));
+               }
+       }
+
+       mdb_cursor_close(cursor);
+
+       dblist_dump(tx, db);
+       return mdb_txn_commit(tx);
+
+fail:
+       printf("db del list fail: %s\n", mdb_strerror(res));
+       mdb_txn_abort(tx);
+       return -1;
+}
+
+int dblist_add_file(MDB_txn *txn, dbindex *db, dblist *d, int fileid) {
+       MDB_txn *tx;
+       MDB_val key, data;
+       int res;
+       struct dbfilelist fvalue = { .seq = d->size + 1, .fileid = fileid };
+       struct dblistfile rvalue = { .listid = d->id, .seq = d->size + 1 };
+
+       mdb_txn_begin(db->env, txn, 0, &tx);
+
+       // Check file exists
+       key.mv_data = &fileid;
+       key.mv_size = sizeof(fileid);
+       if (mdb_get(tx, db->file, &key, &data) != 0) {
+               printf("FOREIGN: file doesn't exist\n");
+               goto fail;
+       }
+
+       // TODO: foriegn constraint on listid ... or just take list name and look it up
+
+       key.mv_data = &d->id;
+       key.mv_size = sizeof(d->id);
+       data.mv_data = &fvalue;
+       data.mv_size = sizeof(fvalue);
+
+       printf("put file by list: listid = %d { seq = %d fileid = %d }\n", d->id, fvalue.seq, fvalue.fileid);
+
+       if ((res = mdb_put(tx, db->file_by_list, &key, &data, MDB_NOOVERWRITE | MDB_NODUPDATA)))
+               goto fail;
+
+       key.mv_data = &fileid;
+       key.mv_size = sizeof(fileid);
+       data.mv_data = &rvalue;
+       data.mv_size = sizeof(rvalue);
+
+       printf("put list by file: fileid = %d { listid = %d .seq = %d }\n", fileid, rvalue.listid, rvalue.seq);
+
+       if ((res = mdb_put(tx, db->list_by_file, &key, &data, MDB_NOOVERWRITE | MDB_NODUPDATA)))
+               goto fail;
+
+       // update list record with changed size
+       // TODO: can i just poke in the size value?
+       d->size += 1;
+
+       key.mv_data = &d->id;
+       key.mv_size = sizeof(d->id);
+
+       data.mv_size = ez_basic_size(DBLIST_DESC, d);
+       data.mv_data = NULL;
+       res = mdb_put(tx, db->list, &key, &data, MDB_RESERVE);
+       if (res == 0)
+               ez_basic_encode_raw(DBLIST_DESC, d, (ez_blob *)&data);
+       printf("update seq %d\n", d->size);
+
+       if (res == 0)
+               return mdb_txn_commit(tx);
+fail:
+       printf("fail: %s\n", mdb_strerror(res));
+       mdb_txn_abort(tx);
+       return res;
+}
+
+void dblist_dump(dbtxn *tx, dbindex *db) {
+       //MDB_txn *tx;
+       MDB_val key, data;
+       MDB_cursor *cursor;
+       int res;
+
+       //if (res = mdb_txn_begin(db->env, txn, MDB_RDONLY, &tx)) {
+       //      printf("failed: %s\n", mdb_strerror(res));
+       //      return;
+       //}
+
+
+       printf("dump all lists\n");
+       printf("list_by_file =\n");
+       res = mdb_cursor_open(tx, db->list_by_file, &cursor);
+       res = mdb_cursor_get(cursor, &key, &data, MDB_FIRST);
+       while (res == 0) {
+               struct dblistfile *value = data.mv_data;
+               uint32_t fid = *(uint32_t *)key.mv_data;
+
+               printf(" file %d list %d seq %d\n", fid, value->listid, value->seq);
+
+               res = mdb_cursor_get(cursor, &key, &data, MDB_NEXT_DUP);
+               if (res == MDB_NOTFOUND)
+                       res = mdb_cursor_get(cursor, &key, &data, MDB_NEXT);
+       }
+       mdb_cursor_close(cursor);
+
+       printf("file_by_list =\n");
+       res = mdb_cursor_open(tx, db->file_by_list, &cursor);
+       res = mdb_cursor_get(cursor, &key, &data, MDB_FIRST);
+       while (res == 0) {
+               struct dbfilelist *value = data.mv_data;
+               uint32_t lid = *(uint32_t *)key.mv_data;
+
+               printf(" list %d file %d seq %d\n", lid, value->fileid, value->seq);
+
+               res = mdb_cursor_get(cursor, &key, &data, MDB_NEXT_DUP);
+               if (res == MDB_NOTFOUND)
+                       res = mdb_cursor_get(cursor, &key, &data, MDB_NEXT);
+       }
+       mdb_cursor_close(cursor);
+
+
+       //mdb_txn_commit(tx);
+}
+
+// only list + seq is required
+int dblist_del_file(MDB_txn *txn, dbindex *db, struct dblistcursor *list) {
+       MDB_txn *tx;
+       MDB_val key, data;
+       MDB_cursor *cursor;
+       int res;
+       struct dbfilelist fvalue = { .seq = list->seq, .fileid = list->fileid };
+       struct dblistfile rvalue = { .listid = list->listid, .seq = list->seq };
+
+       dblist_dump(txn, db);
+
+       key.mv_data = &list->listid;
+       key.mv_size = sizeof(list->listid);
+       if (res = mdb_get(txn, db->list, &key, &data))
+               goto fail0;
+
+       // find list:seq from list_by_file
+       // ...
+
+       printf("delete @ %d from list %d\n", list->seq, list->listid);
+
+       if (res = mdb_txn_begin(db->env, txn, 0, &tx))
+               goto fail0;
+
+       // Delete forward and reverse based on all parameters
+
+       key.mv_data = &list->listid;
+       key.mv_size = sizeof(list->listid);
+       data.mv_data = &fvalue;
+       data.mv_size = sizeof(fvalue);
+
+       if (res = mdb_cursor_open(tx, db->file_by_list, &cursor))
+               goto fail1;
+
+       printf(" lookup listid %d seq %d\n", list->listid, fvalue.seq);
+       if (res = mdb_cursor_get(cursor, &key, &data, MDB_GET_BOTH))
+               goto fail;
+
+       printf("file found %d list %d seq %d\n", ((struct dbfilelist *)data.mv_data)->fileid, *(int*)key.mv_data, ((struct dbfilelist *)data.mv_data)->seq);
+
+       printf(" delete file by list\n");
+
+       uint32_t fid = ((struct dbfilelist *)data.mv_data)->fileid;
+
+       if (res = mdb_del(tx, db->file_by_list, &key, &data))
+               goto fail;
+
+       key.mv_data = &fid;
+       key.mv_size = sizeof(fid);
+       data.mv_data = &rvalue;
+       data.mv_size = sizeof(rvalue);
+
+       printf(" delete list by file file=%d list=%d seq=%d\n", fid, rvalue.listid, rvalue.seq);
+       if (res = mdb_del(tx, db->list_by_file, &key, &data))
+               goto fail;
+
+       mdb_cursor_close(cursor);
+
+       dblist_dump(tx, db);
+       mdb_txn_commit(tx);
+       return 0;
+
+fail:
+       printf("fail: %s\n", mdb_strerror(res));
+       mdb_cursor_close(cursor);
+fail1:
+       mdb_txn_abort(tx);
+fail0:
+       return res;
+}
+
+#if 0
+int dblist_iterate(dbtxn *tx, dbindex *db, struct dblistcursor *pos) {
+       //MDB_txn *tx;
+       MDB_val key, data;
+       MDB_cursor *cursor;
+       int res;
+
+       ez_blob_free_raw(DBFILE_DESC, &pos->file);
+       memset(&pos->file, 0, sizeof(pos->file));
+
+       //printf("iterate list %d\n", pos->listid);
+
+       //if (res = mdb_txn_begin(db->env, txn, 0, &tx))
+       //      goto fail1;
+
+       //dblist_dump(tx, db);
+
+       if (pos->listid == 0) {
+               // just go by fileid order
+               if ((res = mdb_cursor_open(tx, db->file, &cursor)))
+                       goto fail0;
+
+               key.mv_data = &pos->fileid;
+               key.mv_size = sizeof(pos->fileid);
+
+               if (res = mdb_cursor_get(cursor, &key, &data, MDB_SET_RANGE))
+                       goto fail;
+               if (res = mdb_cursor_get(cursor, &key, &data, MDB_NEXT))
+                       goto fail;
+
+               dbfile_decode_raw((ez_blob *)&data, &pos->file);
+               pos->fileid = pos->file.id = *(uint32_t*)key.mv_data;
+       } else {
+               // go by current info
+               if ((res = mdb_cursor_open(tx, db->file_by_list, &cursor)))
+                       goto fail0;
+
+               struct dbfilelist thing = { .seq = pos->seq + 1, .fileid = 0 };
+
+               //printf(" from seq %d file %d\n", thing.seq, thing.fileid);
+
+               key.mv_data = &pos->listid;
+               key.mv_size = sizeof(pos->listid);
+               data.mv_data = &thing;
+               data.mv_size = sizeof(thing);
+
+               if (res = mdb_cursor_get(cursor, &key, &data, MDB_GET_BOTH_RANGE))
+                       goto fail;
+
+               struct dbfilelist *value = data.mv_data;
+
+               pos->seq = value->seq;
+               pos->fileid = value->fileid;
+
+               key.mv_data = &pos->fileid;
+               key.mv_size = sizeof(pos->fileid);
+
+               if (res = mdb_get(tx, db->file, &key, &data))
+                       goto fail;
+
+               dbfile_decode_raw((ez_blob *)&data, &pos->file);
+               pos->file.id = pos->fileid;
+       }
+
+       mdb_cursor_close(cursor);
+       //mdb_txn_commit(tx);
+
+       return 0;
+fail:
+       mdb_cursor_close(cursor);
+fail0:
+       //mdb_txn_abort(tx);
+//fail1:
+       printf("fail: %s\n", mdb_strerror(res));
+       return res;
+}
+#endif
+#if 0
+static int dbfile_iterate_list(dbindex *db, int listid, dbfile **fp, int dir) {
+       MDB_txn *tx;
+       MDB_val key, data;
+       MDB_cursor *cursor;
+       dbfile *file = NULL;
+       int res;
+
+       mdb_txn_begin(db->env, NULL, MDB_RDONLY, &tx);
+
+       if ((res = mdb_cursor_open(tx, db->file_by_list, &cursor)))
+               goto fail;
+
+       int keyval =  *fp ? ((*fp)->id) : -1;
+       dbdisk *disk = *fp ? dbdisk_get(tx, db, (*fp)->diskid) : NULL;
+       int mounted = *fp ? dbdisk_mounted(disk) : 0;
+
+       //printf("\nlist iterate: fid=%d\n", keyval);
+
+       dbfile_free(*fp);
+       *fp = NULL;
+
+       /*
+         This mess is to handle various cases:
+         - first call
+         - next from file
+       */
+
+       if (keyval != -1) {
+               MDB_cursor *rcursor;
+
+               // find current sequence number via GET_BOTH lookup
+               res = mdb_cursor_open(tx, db->list_by_file, &rcursor);
+
+               struct dblistfile rval = { .listid = listid };
+
+               key.mv_data = &keyval;
+               key.mv_size = sizeof(keyval);
+
+               data.mv_data = &rval;
+               data.mv_size = sizeof(rval);
+
+               //rval.seq = keyval - 1;
+
+               //printf("seek    : fileid=%d { listid=%d seq=%d }\n", keyval,  rval.listid, rval.seq);
+
+               res = mdb_cursor_get(rcursor, &key, &data, MDB_GET_BOTH_RANGE);
+               if (res != 0) {
+                       //printf("seek failed = %s\n", mdb_strerror(res));
+                       mdb_cursor_close(rcursor);
+                       goto fail;
+               }
+               //printf("position: fileid=%d { listid=%d seq=%d }\n", keyval,  rval.listid, rval.seq);
+
+               struct dblistfile *dval = data.mv_data;
+               struct dbfilelist fval = { .seq = dval->seq };
+
+               key.mv_data = &listid;
+               key.mv_size = sizeof(listid);
+
+               data.mv_data = &fval;
+               data.mv_size = sizeof(fval);
+
+               //printf("seek    : listid=%d { seq = %d fileid=%d }\n", listid, fval.seq, fval.fileid);
+               res = mdb_cursor_get(cursor, &key, &data, MDB_GET_BOTH);
+               //printf("seek: %s\n", mdb_strerror(res));
+               //printf("position: listid=%d { seq = %d fileid=%d }\n", listid, fval.seq, fval.fileid);
+               res = mdb_cursor_get(cursor, &key, &data, dir == 0 ? MDB_NEXT_DUP : MDB_PREV_DUP);
+               //printf("next: %s\n", mdb_strerror(res));
+               //printf("position: listid=%d { seq = %d fileid=%d }\n", listid, fval.seq, fval.fileid);
+
+               mdb_cursor_close(rcursor);
+       } else {
+               key.mv_data = &listid;
+               key.mv_size = sizeof(listid);
+               res = mdb_cursor_get(cursor, &key, &data, MDB_SET);
+               if (res == 0 && dir == 1)
+                       res = mdb_cursor_get(cursor, &key, &data, MDB_LAST_DUP);
+
+               //struct dbfilelist *fval = data.mv_data;
+               //printf("first   : listid=%d { seq = %d fileid=%d }\n", listid, fval->seq, fval->fileid);
+       }
+
+       while (file == NULL && res == 0) {
+               struct dbfilelist *dval = data.mv_data;
+               MDB_val pkey = { sizeof(int), &dval->fileid };
+
+               file = primary_get_decode(tx, db, DBFILE_DESC, &pkey, db->file);
+               if (file) {
+                       int keep;
+
+                       //printf("loaded: %d[%d]\n", dval->fileid, dval->seq);
+
+                       if (disk == NULL || file->diskid != disk->id) {
+                               dbdisk_free(disk);
+                               disk = dbdisk_get(tx, db, file->diskid);
+                               mounted = dbdisk_mounted(disk);
+                       }
+                       keep = mounted;
+                       keep = keep && file->duration > 0;
+                       if (keep) {
+                               char path[strlen(disk->mount) + strlen(file->path) + 1];
+                               struct stat st;
+
+                               sprintf(path, "%s%s", disk->mount, file->path);
+
+                               //printf("check %s\n", path);
+
+                               keep = lstat(path, &st) == 0 && S_ISREG(st.st_mode);
+                               if (keep) {
+                                       file->full_path = strdup(path);
+                               }
+                       }
+
+                       if (!keep) {
+                               dbfile_free(file);
+                               file = NULL;
+                       }
+               }
+               if (file == NULL)
+                       res = mdb_cursor_get(cursor, &key, &data, dir == 0 ? MDB_NEXT_DUP : MDB_PREV_DUP);
+       }
+
+       //free(keyval);
+       dbdisk_free(disk);
+
+       mdb_cursor_close(cursor);
+       mdb_txn_commit(tx);
+
+       *fp = file;
+
+       return res;
+ fail:
+       // close cursor?
+       mdb_txn_abort(tx);
+
+       return res;
+}
+
+int dbfile_next_list(dbindex *db, int listid, dbfile **fp) {
+       return dbfile_iterate_list(db, listid, fp, 0);
+}
+
+int dbfile_prev_list(dbindex *db, int listid, dbfile **fp) {
+       return dbfile_iterate_list(db, listid, fp, 1);
+}
+#endif
+
+#include <regex.h>
+
+// TODO: should run over title index instead?
+int dbfile_searchx(dbindex *db, const char *pattern, dbfile **results, int maxlen) {
+       MDB_txn *tx;
+       MDB_val key, data;
+       MDB_cursor *cursor;
+       int res;
+       regex_t reg;
+
+       printf("search, pattern='%s'\n", pattern);
+       res = regcomp(&reg, pattern, REG_EXTENDED | REG_ICASE | REG_NOSUB);
+       if (res != 0)
+               return res;
+
+       mdb_txn_begin(db->env, NULL, MDB_RDONLY, &tx);
+
+       if ((res = mdb_cursor_open(tx, db->file, &cursor)))
+               goto fail;
+
+       int next = MDB_FIRST;
+       int i = 0;
+       while (i < maxlen && (res = mdb_cursor_get(cursor, &key, &data, next)) == 0) {
+               if (db->res == 0) {
+                       dbfile *file = ez_basic_decode(DBFILE_DESC, (ez_blob *)&data);
+
+                       if (regexec(&reg, file->title, 0, NULL, 0) == 0) {
+                               file->id = *(int *)key.mv_data;
+                               results[i++] = file;
+                       } else {
+                               dbfile_free(file);
+                       }
+
+                       next = MDB_NEXT;
+               } else
+                       break;
+       }
+       regfree(&reg);
+       mdb_txn_abort(tx);
+
+       return i;
+
+fail:
+       regfree(&reg);
+       mdb_txn_abort(tx);
+       return res;
+
+}
+
+// run over title index
+// TODO: use multi-get on the values, so only match key (title) once per duplicate data.
+// TODO: use dbscan interface?
+int dbfile_search(dbindex *db, const char *pattern, dbfile **results, int maxlen) {
+       MDB_txn *tx;
+       MDB_val key, data;
+       MDB_cursor *cursor;
+       int res;
+       regex_t reg;
+
+       // empty search is empty result
+       if (!pattern[0])
+               return 0;
+
+       printf("search, pattern='%s'\n", pattern);
+       res = regcomp(&reg, pattern, REG_EXTENDED | REG_ICASE | REG_NOSUB);
+       if (res != 0)
+               return -1;
+
+       mdb_txn_begin(db->env, NULL, MDB_RDONLY, &tx);
+
+       if ((res = mdb_cursor_open(tx, db->file_by_title, &cursor)))
+               goto fail;
+
+       int next = MDB_FIRST;
+       int i = 0;
+       while (i < maxlen && (res = mdb_cursor_get(cursor, &key, &data, next)) == 0) {
+               int fileid = *(int *)data.mv_data;
+               char title[key.mv_size + 1];
+
+               memcpy(title, key.mv_data, key.mv_size);
+               title[key.mv_size] = 0;
+
+               {
+                       dbfile *file = dbfile_get(tx, db, fileid);
+                       printf("title %s path %s disk %d\n", title, file->path, file->diskid);
+                       dbfile_free(file);
+               }
+
+               if (regexec(&reg, title, 0, NULL, 0) == 0) {
+                       results[i++] = dbfile_get(tx, db, fileid);
+                       next = MDB_NEXT;
+               } else {
+                       next = MDB_NEXT_NODUP;
+               }
+
+       }
+       regfree(&reg);
+       mdb_txn_abort(tx);
+
+       return i;
+
+fail:
+       regfree(&reg);
+       mdb_txn_abort(tx);
+       return -1;
+
+}
+
+void dump_lists(dbindex *db, dbtxn *txn) {
+       MDB_txn *tx;
+       MDB_val key, data;
+       MDB_cursor *cursor;
+       int res;
+
+       res = mdb_txn_begin(db->env, txn, 0, &tx);
+       printf("begin txn (%d): %s\n", res, mdb_strerror(res));
+
+       res = mdb_cursor_open(tx, db->file_by_list, &cursor);
+       printf("open cursor (%d): %s\n", res, mdb_strerror(res));
+
+       printf("file by list\n");
+       res = mdb_cursor_get(cursor, &key, &data, MDB_FIRST);
+       while (res == 0) {
+               int *keyp = key.mv_data;
+               struct dbfilelist *valp = data.mv_data;
+
+               printf("listid=%d %p { seq = %d fileid=%d }\n", *keyp, valp, valp->seq, valp->fileid);
+               res = mdb_cursor_get(cursor, &key, &data, MDB_NEXT);
+       }
+
+       mdb_cursor_close(cursor);
+
+
+       res = mdb_cursor_open(tx, db->list_by_file, &cursor);
+       printf("open cursor (%d): %s\n", res, mdb_strerror(res));
+
+       printf("list by file\n");
+       res = mdb_cursor_get(cursor, &key, &data, MDB_FIRST);
+       while (res == 0) {
+               int *keyp = key.mv_data;
+               struct dblistfile *valp = data.mv_data;
+
+               printf("fileid=%d %p { listid=%d seq=%d }\n", *keyp, valp, valp->listid, valp->seq);
+               res = mdb_cursor_get(cursor, &key, &data, MDB_NEXT);
+       }
+
+       mdb_cursor_close(cursor);
+
+       mdb_txn_abort(tx);
+}
+
+void dbindex_dump(dbindex *db) {
+       MDB_txn *tx;
+
+       mdb_txn_begin(db->env, NULL, MDB_RDONLY, &tx);
+       // dump disks
+       {
+               MDB_cursor *cursor;
+               MDB_val key = { 0 }, data = { 0 };
+               int r;
+
+               printf("Known disks:\n");
+               mdb_cursor_open(tx, db->disk, &cursor);
+               r = mdb_cursor_get(cursor, &key, &data, MDB_FIRST);
+               while (r == 0) {
+                       dbdisk *p = ez_basic_decode(DBDISK_DESC, (ez_blob *)&data);
+                       p->id = *(int *)key.mv_data;
+                       printf("id=%d\n", p->id);
+                       printf(" uuid=%s\n", p->uuid);
+                       printf(" label=%s\n", p->label);
+                       printf(" type=%s\n", p->type);
+                       printf(" mount=%s\n", p->mount);
+                       ez_blob_free(DBDISK_DESC, p);
+                       r = mdb_cursor_get(cursor, &key, &data, MDB_NEXT);
+               }
+               mdb_cursor_close(cursor);
+       }
+
+       // dump files
+       {
+               MDB_cursor *cursor;
+               MDB_val key = { 0 }, data = { 0 };
+               int r;
+
+               printf("Known filess:\n");
+               mdb_cursor_open(tx, db->file, &cursor);
+               r = mdb_cursor_get(cursor, &key, &data, MDB_FIRST);
+               while (r == 0) {
+                       dbfile *p = ez_basic_decode(DBFILE_DESC, (ez_blob *)&data);
+                       p->id = *(int *)key.mv_data;
+                       printf("id=%d\n", p->id);
+                       printf(" diskid=%d\n", p->diskid);
+                       printf(" path=%s\n", p->path);
+                       printf(" title=%s\n", p->title);
+                       printf(" artist=%s\n", p->artist);
+                       ez_blob_free(DBFILE_DESC, p);
+                       r = mdb_cursor_get(cursor, &key, &data, MDB_NEXT);
+               }
+               mdb_cursor_close(cursor);
+       }
+}
+
+
+
+
+
+/* scan tables with int keys */
+static dbscan *dbscan_secondary(dbtxn *tx, dbindex *db, MDB_dbi table, int diskid) {
+       dbscan *scan = calloc(1, sizeof(*scan));
+       int res;
+
+       scan->db = db;
+       scan->table = table;
+
+       scan->cursor = NULL;
+
+       scan->keyval = diskid;
+       scan->key.mv_data = &scan->keyval;
+       scan->key.mv_size = sizeof(scan->keyval);
+
+       if ((res = mdb_cursor_open(tx, table, &scan->cursor)))
+               goto fail;
+
+       if (diskid != -1)
+               res = mdb_cursor_get(scan->cursor, &scan->key, &scan->data, MDB_SET);
+       else
+               res = mdb_cursor_get(scan->cursor, &scan->key, &scan->data, MDB_FIRST);
+
+       if (res) {
+               if (res == MDB_NOTFOUND) {
+                       scan->count = 0;
+                       scan->index = 0;
+                       return scan;
+               }
+               goto fail;
+       }
+
+       if ((res = mdb_cursor_get(scan->cursor, &scan->key, &scan->data, MDB_GET_MULTIPLE)))
+               goto fail;
+
+       scan->count = scan->data.mv_size / sizeof(int);
+       scan->index = 0;
+
+       return scan;
+
+ fail:
+       fprintf(stderr, "db scan open fail: %s\n", mdb_strerror(res));
+       dbfile_scan_close(scan);
+       return NULL;
+}
+
+static uint32_t dbscan_secondary_next(dbscan *scan) {
+       int res = 0;
+
+       while (scan->count > 0) {
+               if (scan->index < scan->count)
+                       return ((uint32_t *)scan->data.mv_data)[scan->index++];
+
+               if (res = mdb_cursor_get(scan->cursor, &scan->key, &scan->data, MDB_NEXT_MULTIPLE)) {
+                       if (res == MDB_NOTFOUND && scan->keyval == -1) {
+                               res = mdb_cursor_get(scan->cursor, &scan->key, &scan->data, MDB_NEXT);
+                               if (res == 0)
+                                       res = mdb_cursor_get(scan->cursor, &scan->key, &scan->data, MDB_GET_MULTIPLE);
+                       }
+                       if (res)
+                               goto fail;
+               }
+
+               scan->count = scan->data.mv_size / sizeof(uint32_t);
+               scan->index = 0;
+       }
+
+       return ~0;
+ fail:
+       if (res != MDB_NOTFOUND)
+               fprintf(stderr, "db scan fail: %s\n", mdb_strerror(res));
+       return ~0;
+}
+
+static dbscan *dbscan_primary(dbtxn *tx, dbindex *db, MDB_dbi table, int diskid) {
+       dbscan *scan = calloc(1, sizeof(*scan));
+       int res;
+
+       scan->db = db;
+       scan->table = table;
+
+       scan->keyval = diskid;
+       scan->key.mv_data = &scan->keyval;
+       scan->key.mv_size = sizeof(scan->keyval);
+
+       if ((res = mdb_cursor_open(tx, table, &scan->cursor))) {
+               printf("cursor open failed for table %d\n", table);
+               goto fail;
+       }
+
+       if (diskid != -1)
+               res = mdb_cursor_get(scan->cursor, &scan->key, &scan->data, MDB_SET);
+       else
+               res = mdb_cursor_get(scan->cursor, &scan->key, &scan->data, MDB_FIRST);
+
+       if (res) {
+               if (res == MDB_NOTFOUND) {
+                       scan->count = 0;
+                       scan->index = 0;
+                       return scan;
+               }
+               goto fail;
+       }
+
+       scan->count = 1;
+       scan->index = 0;
+
+       return scan;
+
+ fail:
+       fprintf(stderr, "dbscan_primary fail: %s\n", mdb_strerror(res));
+       dbfile_scan_close(scan);
+       return NULL;
+}
+
+// return 1 if there's a value
+static int dbscan_primary_next(dbscan *scan) {
+       int res = 0;
+
+       while (scan->count > 0) {
+               if (scan->index == 0) {
+                       scan->index += 1;
+                       return 1;
+               }
+
+               if (res = mdb_cursor_get(scan->cursor, &scan->key, &scan->data, MDB_NEXT))
+                       goto fail;
+
+               scan->count = 1;
+               scan->index = 0;
+       }
+
+       return 0;
+ fail:
+       if (res != MDB_NOTFOUND)
+               fprintf(stderr, "db scan fail: %s\n", mdb_strerror(res));
+       return 0;
+}
+
+void dbscan_close(dbscan *scan) {
+       if (scan->cursor)
+               mdb_cursor_close(scan->cursor);
+       free(scan);
+}
+
+
+/* new dbscan version */
+/* copy libeze mode, return on init, next/prev calls */
+
+static __inline__ int dbscan_init(dbtxn *tx, dbscan *scan, dbindex *db, MDB_dbi primary, ez_blob_desc *desc, void (*decode_raw)(const ez_blob *blob, void *p)) {
+       scan->db = db;
+       scan->primary = primary;
+       scan->tx = tx;
+       scan->DESC = desc;
+       scan->decode_raw = decode_raw;
+
+       scan->res = mdb_cursor_open(tx, primary, &scan->cursor);
+
+       return scan->res;
+}
+
+static __inline__ void dbscan_init_key(dbscan *scan, dbid_t keyid) {
+       scan->keyid = keyid;
+       scan->key.mv_data = &scan->keyid;
+       scan->key.mv_size = sizeof(scan->keyid);
+}
+
+static void *dbscan_decode(dbscan *scan) {
+       void *memory = calloc(scan->DESC->bd_offset, 1);
+
+       if (memory) {
+               scan->decode_raw((const ez_blob *)&scan->data, memory);
+               *(dbid_t *)memory = *(dbid_t *)scan->key.mv_data;
+       }
+       return memory;
+}
+
+static void dbscan_close2(dbscan *scan) {
+       if (scan->cursor)
+               mdb_cursor_close(scan->cursor);
+       memset(scan, 0, sizeof(*scan));
+}
+
+void dbscan_free(dbscan *scan) {
+       dbscan_close2(scan);
+}
+
+static void *dbscan_primary2(dbtxn *tx, dbscan *scan, dbindex *db, MDB_dbi primary, ez_blob_desc *desc, void (*decode_raw)(const ez_blob *blob, void *p), dbid_t keyid, MDB_cursor_op op) {
+       if (dbscan_init(tx, scan, db, primary, desc, decode_raw) == 0) {
+               dbscan_init_key(scan, keyid);
+               if ((scan->res = mdb_cursor_get(scan->cursor, &scan->key, &scan->data, op)) == 0)
+                       return dbscan_decode(scan);
+               dbscan_close2(scan);
+       }
+       return NULL;
+}
+
+static void *dbscan_primary2_next(dbscan *scan) {
+       if ((scan->res = mdb_cursor_get(scan->cursor, &scan->key, &scan->data, MDB_NEXT)) == 0)
+               return dbscan_decode(scan);
+
+       return NULL;
+}
+
+/*
+int dbscan_init_primary(dbtxn *tx, dbscan *scan, dbindex *db, MDB_dbi primary, dbid_t keyid) {
+       scan->db = db;
+       scan->primary = primary;
+       scan->tx = tx;
+       scan->keyid = keyid;
+
+       if (scan->res = mdb_cursor_open(tx, primary, &scan->cursor))
+               goto fail;
+
+       scan->key.mv_data = &scan->keyid;
+       scan->key.mv_size = sizeof(scan->keyid);
+
+       return 0;
+fail:
+       return scan->res;
+       }*/
+
+
+dbdisk *dbscan_disk(dbtxn *tx, dbscan *scan, dbindex *db, dbid_t diskid) {
+       return dbscan_primary2(tx, scan, db, db->disk, DBDISK_DESC, dbdisk_decode_raw, diskid, diskid == 0 ? MDB_FIRST : MDB_SET_RANGE);
+}
+
+dbdisk *dbscan_disk_next(dbscan *scan) {
+       return dbscan_primary2_next(scan);
+}
+
+dbfile *dbscan_file(dbtxn *tx, dbscan *scan, dbindex *db, dbid_t fileid) {
+       return dbscan_primary2(tx, scan, db, db->file, DBFILE_DESC, dbfile_decode_raw, fileid, fileid == 0 ? MDB_FIRST : MDB_SET_RANGE);
+}
+
+dbfile *dbscan_file_next(dbscan *scan) {
+       return dbscan_primary2_next(scan);
+}
+
+dblist *dbscan_list(dbtxn *tx, dbscan *scan, dbindex *db, dbid_t listid) {
+       return dbscan_primary2(tx, scan, db, db->list, DBLIST_DESC, dblist_decode_raw, listid, listid == 0 ? MDB_FIRST : MDB_SET_RANGE);
+}
+
+dblist *dbscan_list_next(dbscan *scan) {
+       return dbscan_primary2_next(scan);
+}
+
+/**
+ * Init playlist scan from given position.
+ *
+ * If listid == 0 then the scan order is based on the file_by_path index.  The fileid must be supplied.
+ * 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.
+ *
+ */
+dbfile *dbscan_list_entry(dbtxn *tx, dbscan *scan, dbindex *db, dbid_t listid, int seq, dbid_t fileid) {
+       scan->list_entry.listid = listid;
+       scan->list_entry.seq = seq;
+       scan->list_entry.fileid = fileid;
+
+       if (listid == 0) {
+               if (dbscan_init(tx, scan, db, db->file_by_path, DBFILE_DESC, dbfile_decode_raw) == 0) {
+                       MDB_cursor *cursor;
+
+                       dbscan_init_key(scan, fileid);
+
+                       // Get file or next file
+                       scan->res = mdb_cursor_open(tx, db->file, &cursor);
+                       scan->res = mdb_cursor_get(cursor, &scan->key, &scan->data, MDB_SET_RANGE);
+                       mdb_cursor_close(cursor);
+                       if (scan->res == 0) {
+                               dbfile *file = dbscan_decode(scan);
+
+                               if (file) {
+                                       // position by-path cursor for scanning
+                                       char path[strlen(file->path) + 10];
+                                       MDB_val key;
+
+                                       sprintf(path, "%08x%s", file->diskid, file->path);
+                                       key.mv_data = path;
+                                       key.mv_size = strlen(path);
+                                       scan->res = mdb_cursor_get(scan->cursor, &key, &scan->key, MDB_SET);
+
+                                       return file;
+                               }
+                       }
+               }
+       } else {
+               if (dbscan_init(tx, scan, db, db->file_by_list, DBFILE_DESC, dbfile_decode_raw) == 0) {
+                       // lookup seq of next entry of this file?
+                       if (fileid != 0) {
+                               struct dblistfile thing = { .listid = listid, .seq = seq };
+                               MDB_cursor *cursor;
+
+                               dbscan_init_key(scan, fileid);
+                               scan->data.mv_data = &thing;
+                               scan->data.mv_size = sizeof(thing);
+
+                               scan->res = mdb_cursor_open(tx, db->list_by_file, &cursor);
+                               if ((scan->res = mdb_cursor_get(cursor, &scan->key, &scan->data, MDB_GET_BOTH_RANGE)) == 0)
+                                       seq = ((struct dblistfile *)scan->data.mv_data)->seq;
+                               mdb_cursor_close(cursor);
+                       }
+
+                       struct dbfilelist thing = { .seq = seq };
+
+                       dbscan_init_key(scan, listid);
+
+                       scan->data.mv_data = &thing;
+                       scan->data.mv_size = sizeof(thing);
+
+                       if ((scan->res = mdb_cursor_get(scan->cursor, &scan->key, &scan->data, MDB_GET_BOTH_RANGE)) == 0) {
+                               struct dbfilelist *value = scan->data.mv_data;
+
+                               scan->list_entry.seq = value->seq;
+                               scan->list_entry.fileid = value->fileid;
+
+                               scan->key.mv_data = &scan->list_entry.fileid;
+                               scan->key.mv_size = sizeof(scan->list_entry.fileid);
+
+                               if ((scan->res = mdb_get(tx, db->file, &scan->key, &scan->data)) == 0)
+                                       return dbscan_decode(scan);
+                       }
+                       dbscan_close2(scan);
+               }
+       }
+       return NULL;
+
+}
+
+static dbfile *scan_list_entry_next(dbscan *scan, MDB_cursor_op next0, MDB_cursor_op next1) {
+       MDB_val key;
+
+       if (scan->list_entry.listid == 0) {
+               if ((scan->res = mdb_cursor_get(scan->cursor, &key, &scan->key, next0)) == 0
+                       && (scan->res = mdb_get(scan->tx, scan->db->file, &scan->key, &scan->data)) == 0)
+                       return dbscan_decode(scan);
+       } else {
+               MDB_val data;
+
+               if ((scan->res = mdb_cursor_get(scan->cursor, &key, &data, next1)) == 0) {
+                       struct dbfilelist *value = data.mv_data;
+
+                       scan->list_entry.seq = value->seq;
+                       scan->list_entry.fileid = value->fileid;
+
+                       scan->key.mv_data = &scan->list_entry.fileid;
+                       scan->key.mv_size = sizeof(scan->list_entry.fileid);
+
+                       if ((scan->res = mdb_get(scan->tx, scan->db->file, &scan->key, &scan->data)) == 0)
+                               return dbscan_decode(scan);
+               }
+       }
+
+       return NULL;
+}
+
+dbfile *dbscan_list_entry_next(dbscan *scan) {
+       return scan_list_entry_next(scan, MDB_NEXT, MDB_NEXT_DUP);
+}
+
+dbfile *dbscan_list_entry_prev(dbscan *scan) {
+       return scan_list_entry_next(scan, MDB_PREV, MDB_PREV_DUP);
+}
+
+
+int dbscan_list_entry_seq(dbscan *scan) {
+       return scan->list_entry.seq;
+}
+
+dbid_t  dbscan_list_entry_listid(dbscan *scan) {
+       return scan->list_entry.listid;
+}
+
+/* legacy */
+
+
+dbscan *dbfile_scan_disk(dbtxn *tx, dbindex *db, int diskid) {
+       return dbscan_secondary(tx, db, db->file_by_disk, diskid);
+}
+
+uint32_t dbfile_scan_next(dbscan *scan) {
+       return dbscan_secondary_next(scan);
+}
+
+// TBD
+void dbfile_scan_close(dbscan *scan) {
+       dbscan_close(scan);
+}
+
+dbscan *dbdisk_scan(dbtxn *tx, dbindex *db, int diskid) {
+       return dbscan_primary(tx, db, db->disk, diskid);
+}
+
+dbdisk *dbdisk_next(dbscan *scan) {
+       dbdisk *disk = NULL;
+
+       if (dbscan_primary_next(scan)) {
+               disk = ez_basic_decode(DBDISK_DESC, (ez_blob *)&scan->data);
+               if (disk)
+                       disk->id = *(int *)scan->key.mv_data;
+       }
+
+       return disk;
+}
+
+dbscan *dblist_scan(dbtxn *tx, dbindex *db, int listid) {
+       dbscan *scan = dbscan_primary(tx, db, db->list, listid);
+
+       if (scan) {
+               scan->DESC = DBLIST_DESC;
+               scan->decode_raw = dblist_decode_raw;
+       }
+
+       return scan;
+}
+
+const dblist *dblist_next(dbscan *scan) {
+       if (dbscan_primary_next(scan)) {
+               return dbscan_decode(scan);
+       } else {
+               return NULL;
+       }
+}
+
+dbscan *dblist_file_scan(dbtxn *tx, dbindex *db, int listid, int seq, int fileid) {
+       dbscan *scan = calloc(1, sizeof(*scan));
+
+       scan->tx = tx;
+       scan->db = db;
+
+       scan->DESC = DBFILE_DESC;
+       scan->decode_raw = dbfile_decode_raw;
+
+       scan->list_entry.listid = listid;
+       scan->list_entry.fileid = fileid;
+
+       scan->key.mv_data = &scan->keyval;
+       scan->key.mv_size = sizeof(scan->keyval);
+
+       if (listid == 0) {
+               scan->table = db->file;
+
+               if (db->res = mdb_cursor_open(tx, db->file, &scan->cursor))
+                       goto fail;
+
+               scan->keyval = fileid + 1;
+
+               if (db->res = mdb_cursor_get(scan->cursor, &scan->key, &scan->data, MDB_SET_RANGE))
+                       goto fail;
+       } else {
+               scan->table = db->file_by_list;
+
+               if (db->res = mdb_cursor_open(tx, db->file_by_list, &scan->cursor))
+                       goto fail;
+
+               struct dbfilelist thing = { .seq = seq + 1, .fileid = 0 };
+
+               scan->keyval = listid;
+               scan->data.mv_data = &thing;
+               scan->data.mv_size = sizeof(thing);
+
+               if (db->res = mdb_cursor_get(scan->cursor, &scan->key, &scan->data, MDB_GET_BOTH_RANGE))
+                       goto fail;
+
+               struct dbfilelist *value = scan->data.mv_data;
+
+               scan->list_entry.seq = value->seq;
+               scan->list_entry.fileid = value->fileid;
+
+               scan->key.mv_data = &scan->list_entry.fileid;
+               scan->key.mv_size = sizeof(scan->list_entry.fileid);
+
+               if (db->res = mdb_get(tx, db->file, &scan->key, &scan->data))
+                       goto fail;
+       }
+
+       scan->index = 0;
+       scan->count = 1;
+
+       return scan;
+fail:
+       if (scan->cursor)
+               mdb_cursor_close(scan->cursor);
+       free(scan);
+       return NULL;
+}
+
+const dbfile *dblist_file_next(dbscan *scan, int *seq) {
+       dbindex *db = scan->db;
+
+       if (scan->index == scan->count) {
+               if (scan->list_entry.listid == 0) {
+                       if (db->res = mdb_cursor_get(scan->cursor, &scan->key, &scan->data, MDB_NEXT))
+                               goto fail;
+               } else {
+                       if (db->res = mdb_cursor_get(scan->cursor, &scan->key, &scan->data, MDB_NEXT_DUP))
+                               goto fail;
+
+                       struct dbfilelist *value = scan->data.mv_data;
+
+                       scan->list_entry.seq = value->seq;
+                       scan->list_entry.fileid = value->fileid;
+
+                       scan->key.mv_data = &scan->list_entry.fileid;
+                       scan->key.mv_size = sizeof(scan->list_entry.fileid);
+
+                       if (db->res = mdb_get(scan->tx, db->file, &scan->key, &scan->data))
+                               goto fail;
+               }
+               scan->index = 0;
+               scan->count = 1;
+       }
+
+       scan->index += 1;
+
+       *seq = scan->list_entry.seq;
+
+       return dbscan_decode(scan);
+fail:
+       return NULL;
+}
+
+
+
+// prototyping
+int dbfile_clear_suffix(MDB_txn *tx, dbindex *db) {
+       return mdb_drop(tx, db->file_by_suffix, 0);
+}
+
+int dbfile_put_suffix(MDB_txn *tx, dbindex *db, const char *suffix, uint32_t fileid) {
+       MDB_val key = { .mv_data = (char *)suffix, .mv_size = strlen(suffix) };
+       MDB_val data = { .mv_data = &fileid, .mv_size = sizeof(fileid) };
+
+       return mdb_put(tx, db->file_by_suffix, &key, &data, MDB_NODUPDATA);
+}
+
+static void printval(MDB_val key) {
+       char match[key.mv_size+1];
+
+       memcpy(match, key.mv_data, key.mv_size);
+       match[key.mv_size] = 0;
+
+       printf("%s\n", match);
+}
+
+size_t dbfile_search_substring(dbtxn *tx, dbindex *db, const char *sub) {
+       int res;
+       size_t len = strlen(sub);
+       MDB_val key = { .mv_data = (char *)sub, .mv_size = len };
+       MDB_val data;
+       MDB_cursor *cursor;
+       size_t total = 0;
+
+       if ((res = mdb_cursor_open(tx, db->file_by_suffix, &cursor)))
+               goto fail;
+
+       res = mdb_cursor_get(cursor, &key, &data, MDB_SET_RANGE);
+       if (res)
+               goto fail;
+
+       printval(key);
+       while (res == 0 && key.mv_size >= len && strncmp(sub, key.mv_data, len) == 0) {
+               //while (res == 0) {
+               int step = MDB_GET_MULTIPLE;
+
+               while ( (res = mdb_cursor_get(cursor, &key, &data, step)) == 0 ) {
+                       uint32_t *fileids = data.mv_data;
+                       uint32_t *endids = data.mv_data + data.mv_size;
+
+                       printf("multiple: %zd\n", endids - fileids);
+
+                       // FIXME: need to merge
+                       while (fileids < endids) {
+                               dbfile *file = dbfile_get(tx, db, *fileids);
+                               printf(" %8d %s\n", *fileids++, file->title);
+                               dbfile_free(file);
+                               total++;
+                       }
+                       step = MDB_NEXT_MULTIPLE;
+               }
+               res = mdb_cursor_get(cursor, &key, &data, MDB_NEXT);
+               if (res == 0)
+                       printval(key);
+       }
+fail:
+       if (res == MDB_NOTFOUND)
+               return total;
+       return -1;
+}
+
+static int cmp_fid(const void *ap, const void *bp) {
+       return *(const int32_t *)ap - *(const int32_t *)bp;
+}
+
+/* protoyping */
+void dbindex_validate(dbindex *db) {
+       MDB_txn *tx;
+       MDB_val key, data;
+       MDB_cursor *cursor;
+       int res;
+
+       mdb_txn_begin(db->env, NULL, MDB_RDONLY, &tx);
+
+       size_t alloc = 4096;
+       uint32_t *fids = malloc(sizeof(*fids) * alloc);
+       size_t fids_size = 0;
+
+       // All files
+       mdb_cursor_open(tx, db->file, &cursor);
+       int next = MDB_FIRST;
+       while ((res = mdb_cursor_get(cursor, &key, &data, next)) == 0) {
+               if (fids_size >= alloc) {
+                       alloc *= 2;
+                       fids = realloc(fids, sizeof(*fids) * alloc);
+               }
+               fids[fids_size++] = *(uint32_t *)key.mv_data;
+               next = MDB_NEXT;
+       }
+       mdb_cursor_close(cursor);
+       printf("read %zd files\n", fids_size);
+       qsort(fids, fids_size, sizeof(*fids), cmp_fid);
+
+       // Check secondary indices
+       MDB_dbi tables[] = {
+               db->file_by_path,
+               db->file_by_disk,
+               db->file_by_title,
+               db->file_by_artist,
+       };
+
+       for (int i=0;i<4;i++) {
+               size_t count = 0;
+               printf("table %d\n", i);
+               mdb_cursor_open(tx, tables[i], &cursor);
+               res = mdb_cursor_get(cursor, &key, &data, MDB_FIRST);
+               while (res == 0) {
+                       uint32_t fid = *(uint32_t*)data.mv_data;
+
+                       count++;
+
+                       if (!bsearch(&fid, fids, fids_size, sizeof(*fids), cmp_fid)) {
+                               printf("table %d references missing file\n", i);
+                       }
+
+                       res = mdb_cursor_get(cursor, &key, &data, MDB_NEXT_DUP);
+                       if (res == MDB_NOTFOUND)
+                               res = mdb_cursor_get(cursor, &key, &data, MDB_NEXT);
+               }
+               mdb_cursor_close(cursor);
+
+               if (i == 0 && count != fids_size) {
+                       printf("file by path miscount %zd != %zd\n", count, fids_size);
+               }
+       }
+//fail:
+       dbindex_abort(tx);
+}
index a9e2bbf..a87f293 100644 (file)
--- a/dbindex.h
+++ b/dbindex.h
 */
 
 #include "ez-blob.h"
+#include <lmdb.h>
 
 typedef struct dbdisk dbdisk;
+typedef uint32_t dbid_t;
 
 struct dbdisk {
-       int id;
+       dbid_t id;
        char *uuid;
        char *label;
        char *type;
        char *mount;            // last mount point
 };
 
+typedef struct dblist dblist;
+
+struct dblist {
+       dbid_t id;
+       int size;
+       char *name;
+       char *comment;
+};
+
 typedef struct dbfile dbfile;
 
 struct dbfile {
-       int id;
-       int diskid;             // disk it belongs to
+       dbid_t id;
+       dbid_t diskid;          // disk it belongs to
 
        uint64_t size;          // st_size
        uint64_t mtime;         // st_mtime
@@ -44,16 +55,26 @@ struct dbfile {
 
        char *title;            // music title
        char *artist;           // music artist
+
+       char *full_path;        // <transient> full path including disk name, depends on api
 };
 
 /* player state, this is passed around as a struct */
 typedef struct dbstate dbstate;
 
+// FIXME: add playlist id
+// FIXME: add playlist seq (and file)
+
 struct dbstate {
-       uint32_t size; // don't need to initialise
+       uint32_t size;          // don't need to initialise
        uint32_t state;         // some info on playing
-       uint32_t fileid;        // file being played
-       uint64_t pos;           // last approximate position
+
+       dbid_t listid;          // list, if any
+       uint32_t seq;           // list position, if any
+       dbid_t fileid;          // file being played
+
+       uint64_t pos;           // last approximate position pts
+       double poss;            // last approximate position in seconds
        time_t stamp;           // last update time
 };
 
@@ -65,7 +86,7 @@ dbindex *dbindex_open(const char *path);
 void dbindex_close(dbindex *db);
 
 dbtxn *dbindex_begin(dbindex *db, dbtxn *txn, int readonly);
-void dbindex_commit(dbtxn *tx);
+int dbindex_commit(dbtxn *tx);
 void dbindex_abort(dbtxn *tx);
 
 int dbstate_get(dbtxn *tx, dbindex *db, dbstate *s);
@@ -80,10 +101,14 @@ dbfile *dbfile_get(dbtxn *tx, dbindex *db, int fileid);
 dbfile *dbfile_get_path(dbtxn *tx, dbindex *db, int diskid, const char *path);
 void dbfile_free(dbfile *f);
 
+char *dbfile_full_path(dbtxn *tx, dbindex *db, dbfile *file);
+
+int dbfile_del_id(dbtxn *tx, dbindex *db, int fileid);
 int dbfile_del(dbtxn *txn, dbindex *db, dbfile *f);
 int dbfile_add(dbtxn *txn, dbindex *db, dbfile *f);
 int dbfile_update(dbtxn *txn, dbindex *db, dbfile *o, dbfile *f);
 
+// TBD?
 dbscan *dbfile_scan_disk(dbtxn *tx, dbindex *db, int diskid);
 uint32_t dbfile_scan_next(dbscan *scan);
 void dbfile_scan_close(dbscan *scan);
@@ -100,4 +125,90 @@ void dbshuffle_init(dbindex *db);
 int dbfile_next_shuffle(dbindex *db, dbfile **f, char **fpath);
 int dbfile_prev_shuffle(dbindex *db, dbfile **f, char **fpath);
 
+dblist *dblist_get(dbtxn *tx, dbindex *db, int id);
+void dblist_free(dblist *f);
+
+int dblist_add(dbtxn *txn, dbindex *db, dblist *d);
+int dblist_del(dbtxn *txn, dbindex *db, int listid);
+
+// should these take the list name?
+int dblist_add_file(dbtxn *txn, dbindex *db, dblist *d, int file);
+
+//int dbfile_next_list(dbindex *db, int listid, dbfile **fp);
+//int dbfile_prev_list(dbindex *db, int listid, dbfile **fp);
+
+int dbfile_search(dbindex *db, const char *pattern, dbfile **results, int maxlen);
+
+// scanning routines, to be expanded for other search routines?
+//dbscan *dbdisk_scan(dbtxn *tx, dbindex *db, int diskid);
+//dbdisk *dbdisk_next(dbscan *scan);
+
+//dbscan *dblist_scan(dbtxn *tx, dbindex *db, int listid);
+//const dblist *dblist_next(dbscan *scan);
+void dbscan_close(dbscan *scan);
+
+// prototyping
+int dbfile_put_suffix(dbtxn *tx, dbindex *db, const char *suffix, uint32_t fileid);
+size_t dbfile_search_substring(dbtxn *tx, dbindex *db, const char *sub);
+
+struct dblistcursor {
+       uint32_t listid;
+       uint32_t seq;
+       uint32_t fileid;
+       dbfile file;
+};
+
+//int dblist_iterate(dbtxn *txn, dbindex *db, struct dblistcursor *pos);
+
+// another attempt ...
+//dbscan *dblist_file_scan(dbtxn *tx, dbindex *db, int listid, int seq, int fileid);
+//const dbfile *dblist_file_next(dbscan *scan, int *seq);
+
+
+/* re-usable scan code try two! */
+struct dbscan {
+       dbindex *db;
+       MDB_dbi primary;
+       MDB_dbi secondary; // ?
+       MDB_dbi table; // TBD
+       MDB_cursor *cursor;
+       MDB_val key, data;
+       int keyval; // TBD
+       int index;
+       int count;
+
+       MDB_txn *tx;
+
+       dbid_t keyid;
+       int res;
+
+       const ez_blob_desc *DESC;
+       void (*decode_raw)(const ez_blob *blob, void *p);
+
+       union {
+               struct {
+                       int listid;
+                       int fileid;
+                       int seq;
+               } list_entry;
+       };
+};
+
+void dbscan_free(dbscan *scan);
+static __inline__ int dbscan_res(dbscan *scan) { return scan->res; }
+
+dbdisk *dbscan_disk(dbtxn *tx, dbscan *scan, dbindex *db, dbid_t diskid);
+dbdisk *dbscan_disk_next(dbscan *scan);
+dbfile *dbscan_file(dbtxn *tx, dbscan *scan, dbindex *db, dbid_t fileid);
+dbfile *dbscan_file_next(dbscan *scan);
+dblist *dbscan_list(dbtxn *tx, dbscan *scan, dbindex *db, dbid_t listid);
+dblist *dbscan_list_next(dbscan *scan);
+
+// TODO: s/entry/file/
+dbfile *dbscan_list_entry(dbtxn *tx, dbscan *scan, dbindex *db, dbid_t listid, int seq, dbid_t fileid);
+dbfile *dbscan_list_entry_next(dbscan *scan);
+dbfile *dbscan_list_entry_prev(dbscan *scan);
+int     dbscan_list_entry_seq(dbscan *scan);
+dbid_t  dbscan_list_entry_listid(dbscan *scan);
+
 #define MAIN_INDEX "/home/notzed/playerz.db"
index 40f28d4..ba8c0e3 100644 (file)
@@ -38,6 +38,7 @@
 #include "dbindex.h"
 
 #include "notify.h"
+#include "analyse.h"
 
 struct indexer {
 
@@ -107,7 +108,7 @@ static dbfile *scan_info(AVFormatContext *ic) {
        dbfile *f = NULL;
 
        if (audio) {
-               f = malloc(sizeof(*f));
+               f = calloc(1, sizeof(*f));
 
                f->id = 0;
                f->artist = strdup(pz_dict_get(ic->metadata, "artist", "Unknown"));
@@ -472,8 +473,8 @@ int indexer_scan(struct indexer *ix) {
        return -1;
 }
 
-static void indexer(void) {
-       dbindex *db = dbindex_open(MAIN_INDEX);
+static void indexer(const char *path) {
+       dbindex *db = dbindex_open(path);
        notify_t q;
        int quit = 0;
 
@@ -537,8 +538,8 @@ static void indexer(void) {
        dbindex_close(db);
 }
 
-void check(void) {
-       dbindex *db = dbindex_open(MAIN_INDEX);
+void check(const char *path) {
+       dbindex *db = dbindex_open(path);
 
        // Check indices
        printf("Check file-by-diskid index\n");
@@ -563,18 +564,170 @@ void check(void) {
        dbindex_close(db);
 }
 
+#if 1
+
+
+/*
+  (naive) idea for full text sub-string search inside lmdb
+  just build full suffix tables
+
+  [suffix] [all members]
+
+  sub-string search is:
+   suffix >= query and suffix[0 .. query.length] == query
+
+ */
+#if 0
+void build_suffix(dbtxn *tx, dbindex *db, uint32_t fileid, const char *words) {
+       int state = 0;
+       size_t len = strlen(words);
+       char word[len+1]; // + ??
+       wchar_t lwords[len+1];
+
+       /* convert to wide char astring */
+       size_t res;
+
+       len = mbrtowcs(lwords, words, len, NULL);
+       if (len == (size_t)-1)
+               return;
+
+
+
+       //printf("words: %s\n", words);
+
+       /*
+         Basic idea:
+         Break string into words.
+         Strip puncutation.
+         lower-scase
+         Build suffix tables (in-memory db?)
+
+         we want ' included though, translate other 's into '?  or remove all?
+        */
+
+       int high = 0;
+
+       wchar_t c;
+       wchar_t *p = lwords;
+       wchar_t *s = p;
+       do {
+               c = *p;
+
+               if (c == 0 || !iswgraph(c) || iswpunct(c)) {
+                       *p = 0;
+                       while (p - s >= 3) {
+                               wchar_t *t = s++;
+
+                               len = wcstombs(word, &t, sizeof(word), NULL);
+                               if (len < sizeof(word)) {
+                                       dbfile_put_suffix(tx, db, word, fileid);
+                               } else {
+                                       fprintf(stderr, "overflow %s\n", words);
+                               }
+                       }
+                       s = ++p;
+               } else {
+                       *p++ = towlower(c);
+               }
+       } while (c);
+
+}
+#endif
+
+int dbfile_clear_suffix(dbtxn *tx, dbindex *db);
+
+void suffix(const char *path) {
+       dbindex *db = dbindex_open(path);
+       dbtxn *tx = dbindex_begin(db, NULL, 0);
+
+       dbfile_clear_suffix(tx, db);
+
+       dbscan *scan = dbfile_scan_disk(tx, db, -1);
+       uint32_t fid;
+       ez_list list = EZ_INIT_LIST(list);
+
+       while ((fid = dbfile_scan_next(scan)) != ~0) {
+               dbfile *f = dbfile_get(tx, db, fid);
+               struct string_node *w;
+
+               //printf("%s\n", f->title);
+               analyse_words(&list, 1, f->title);
+               while ((w = ez_list_remhead(&list))) {
+                       //printf(" %s\n", w->value);
+                       dbfile_put_suffix(tx, db, w->value, f->id);
+                       free(w);
+               }
+
+               dbfile_free(f);
+       }
+
+       dbfile_scan_close(scan);
+       dbindex_commit(tx);
+       dbindex_close(db);
+}
+
+void search_suffix(const char *path) {
+       dbindex *db = dbindex_open(path);
+       if (1) {
+               dbtxn *tx = dbindex_begin(db, NULL, 0);
+
+               dbfile_search_substring(tx, db, "union");
+               dbindex_abort(tx);
+       } else {
+               dbfile *matches[50];
+               int len = dbfile_search(db, "cnic", matches, 50);
+               printf("matches: %d\n", len);
+       }
+       dbindex_close(db);
+}
+#endif
+
+#include <locale.h>
+
 int main(int argc, char **argv) {
        av_log_set_level(AV_LOG_ERROR);
 
        //avcodec_register_all();
        //av_register_all();
        //avformat_network_init();
+       const char *dbdir = MAIN_INDEX;
+
+       setlocale(LC_ALL, "en_AU.UTF-8");
+
+       if (argc > 2 && strcmp(argv[1], "-d") == 0) {
+               dbdir = argv[2];
+               argv += 2;
+               argc -= 2;
+       }
+
+       mkdir(dbdir, 0700);
+
+       if (1) {
+               //suffix(dbdir);
+               search_suffix(dbdir);
+               return 0;
+       }
+
+
+#if 0
+       {
+               int dbfile_searchx(dbindex *db, const char *pattern, dbfile **results, int maxlen);
+               dbindex *db = dbindex_open(MAIN_INDEX);
+               dbfile *list[150];
+               int len = dbfile_searchx(db, "deep sessions", list, 150);
+
+               for (int i=0;i<len;i++) {
+                       printf(" %8d %8d %s  %s\n", list[i]->id, list[i]->diskid, list[i]->title, list[i]->path);
+               }
 
-       mkdir(MAIN_INDEX, 0700);
+               dbindex_close(db);
+               return 0;
+       }
+#endif
 
        if (argc > 1) {
                if (strcmp(argv[1], "check") == 0)
-                       check();
+                       check(dbdir);
                else {
                        notify_t q = notify_writer_new(NOTIFY_INDEXER);
 
@@ -596,7 +749,7 @@ int main(int argc, char **argv) {
                        }
                }
        } else
-               indexer();
+               indexer(dbdir);
 
        return 0;
 
index 0b90c0b..67a4d6a 100644 (file)
@@ -109,7 +109,7 @@ the keyboard can be read with the input group.
 #include "notify.h"
 
 struct monitor {
-       ez_set *mounts;
+       ez_set mounts;
 
        size_t mount_base_size;
        char *mount_base;
@@ -243,7 +243,7 @@ static void partition_add(struct monitor *m, const char *dev) {
 
                        partition_notify(m, NOTIFY_DISK_ADD, md);
 
-                       md = ez_set_put(m->mounts, md);
+                       md = ez_set_put(&m->mounts, md);
                        if (md) {
                                printf("mounted twice?\n");
                                mdisk_free(md);
@@ -270,7 +270,7 @@ static void partition_remove(struct monitor *m, const char *dev) {
 
        printf("Remove partition: %s\n", dev);
 
-       md = ez_set_remove(m->mounts, &mde);
+       md = ez_set_remove(&m->mounts, &mde);
        if (md) {
                partition_notify(m, NOTIFY_DISK_REMOVE, md);
 
@@ -395,7 +395,7 @@ static void monitor(void) {
        //
        m->mount_base = strdup(MOUNT_BASE);
        m->mount_base_size = strlen(MOUNT_BASE);
-       m->mounts = ez_set_new(mdisk_hash, mdisk_equals, mdisk_free);
+       ez_set_init(&m->mounts, mdisk_hash, mdisk_equals, mdisk_free);
        res = mkdir(m->mount_base, 0777);
        m->indexer = notify_writer_new(NOTIFY_INDEXER);
 
@@ -409,9 +409,9 @@ static void monitor(void) {
                        continue;
 
                char *x = data, *e = data+res;
-               char *action;
-               char *dev;
-               char *type;
+               char *action = "";
+               char *dev = "";
+               char *type = "";
 
                while (x < e) {
                        //printf(" %s\n", x);
@@ -445,7 +445,7 @@ static void monitor(void) {
        //
        notify_close(m->indexer);
        free(m->mount_base);
-       ez_set_free(m->mounts);
+       ez_set_clear(&m->mounts);
 
        close(s);
 }
diff --git a/disk-util.c b/disk-util.c
new file mode 100644 (file)
index 0000000..765089b
--- /dev/null
@@ -0,0 +1,203 @@
+/* disk-util.c: utilities for managing indices.
+
+   Copyright (C) 2021 Michael Zucchi
+
+   This program is free software: you can redistribute it and/or
+   modify it under the terms of the GNU General Public License as
+   published by the Free Software Foundation, either version 3 of the
+   License, or (at your option) any later version.
+
+   This program is distributed in the hope that it will be useful, but
+   WITHOUT ANY WARRANTY; without even the implied warranty of
+   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+   General Public License for more details.
+
+   You should have received a copy of the GNU General Public License
+   along with this program. If not, see
+   <http://www.gnu.org/licenses/>.
+*/
+
+#include <sys/types.h>
+#include <sys/stat.h>
+#include <sys/time.h>
+#include <unistd.h>
+#include <dirent.h>
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <locale.h>
+
+#include <libavformat/avformat.h>
+
+#include <regex.h>
+
+#include "ez-list.h"
+#include "ez-set.h"
+#include "ez-bitset.h"
+
+#include "dbindex.h"
+
+void dbshuffle_init2(dbindex *db);
+int dbdisk_del_id(dbtxn *txn, dbindex *db, int diskid);
+void dbindex_validate(dbindex *db);
+
+int dblist_reset(dbtxn *tx, dbindex *db);
+int dblist_del_file(dbtxn *txn, dbindex *db, struct dblistcursor *list);
+
+int main(int argc, char **argv) {
+       const char *dbdir = MAIN_INDEX;
+       int fileid = 0, diskid = 0, listid = 0;
+       int seq = 0;
+
+       setlocale(LC_ALL, "en_AU.UTF-8");
+
+       if (argc > 2 && strcmp(argv[1], "-d") == 0) {
+               dbdir = argv[2];
+               argv += 2;
+               argc -= 2;
+       }
+
+       mkdir(dbdir, 0700);
+
+       dbindex *db = dbindex_open(dbdir);
+
+       for (int i=1;i<argc;i++) {
+               const char *cmd = argv[i];
+
+               if (strcmp(cmd, "-f") == 0) {
+                       fileid = atoi(argv[++i]);
+               } else if (strcmp(cmd, "-s") == 0) {
+                       seq = atoi(argv[++i]);
+               } else if (strcmp(cmd, "-d") == 0) {
+                       diskid = atoi(argv[++i]);
+               } else if (strcmp(cmd, "--shuffle") == 0) {
+                       dbshuffle_init2(db);
+               } else if (strcmp(cmd, "--file-dump") == 0) {
+                       // dump file info
+                       // dump lists it's in
+               } else if (strcmp(cmd, "--files") == 0) {
+                       dbtxn *tx = dbindex_begin(db, NULL, 1);
+                       dbscan scan;
+
+                       for (dbfile *file = dbscan_file(tx, &scan, db, fileid); file; file = dbscan_file_next(&scan)) {
+                               printf("%4d %-60s '%s'\n", file->id, file->title, file->path);
+                               dbfile_free(file);
+                       }
+                       dbscan_free(&scan);
+                       dbindex_abort(tx);
+               } else if (strcmp(cmd, "--file-del") == 0) {
+                       dbtxn *tx = dbindex_begin(db, NULL, 0);
+                       int fid = atoi(argv[++i]);
+
+                       if (dbfile_del_id(tx, db, fid) == 0)
+                               dbindex_commit(tx);
+                       else
+                               dbindex_abort(tx);
+               } else if (strcmp(cmd, "--lists") == 0) {
+                       dbtxn *tx = dbindex_begin(db, NULL, 1);
+                       dbscan scan;
+
+                       for (dblist *list = dbscan_list(tx, &scan, db, 0); list; list = dbscan_list_next(&scan)) {
+                               printf("id: %d\n", list->id);
+                               printf("list: %s\n", list->name);
+                               printf("size: %d\n", list->size);
+                               printf("\n");
+                               dblist_free(list);
+                       }
+                       dbscan_free(&scan);
+                       dbindex_abort(tx);
+               } else if (strcmp(cmd, "--lists-reset") == 0) {
+                       dbtxn *tx = dbindex_begin(db, NULL, 0);
+
+                       dblist_reset(tx, db);
+                       dbindex_commit(tx);
+               } else if (strcmp(cmd, "--list-add") == 0) {
+                       dblist list = {
+                               .name = argv[++i]
+                       };
+
+                       dblist_add(NULL, db, &list);
+               } else if (strcmp(cmd, "--list-del") == 0) {
+                       int lid = atoi(argv[++i]);
+
+                       dblist_del(NULL, db, lid);
+               } else if (strcmp(cmd, "--list-add-file") == 0) {
+                       int lid = atoi(argv[++i]);
+                       int fid = atoi(argv[++i]);
+                       dbtxn *tx = dbindex_begin(db, NULL, 0);
+                       dblist *list = dblist_get(tx, db, lid);
+
+                       if (dblist_add_file(tx, db, list, fid) == 0)
+                               dbindex_commit(tx);
+                       else
+                               dbindex_abort(tx);
+               } else if (strcmp(cmd, "--list-del-file") == 0) {
+                       int lid = atoi(argv[++i]);
+                       int seq = atoi(argv[++i]);
+                       //int fid = atoi(argv[++i]);
+                       dbtxn *tx = dbindex_begin(db, NULL, 0);
+                       struct dblistcursor list = {
+                               .listid = lid,
+                               .seq = seq,
+                               //.fileid = fid
+                       };
+
+                       if (dblist_del_file(tx, db, &list) == 0)
+                               dbindex_commit(tx);
+                       else
+                               dbindex_abort(tx);
+               } else if (strcmp(cmd, "--list-dump") == 0) {
+                       int lid = atoi(argv[++i]);
+                       dbtxn *tx = dbindex_begin(db, NULL, 1);
+                       dbscan scan;
+
+                       for (dbfile *file = dbscan_list_entry(tx, &scan, db, lid, seq, fileid); file; file = dbscan_list_entry_next(&scan)) {
+                               printf("%4d seq=%4d title: %-60s %s\n", file->id, dbscan_list_entry_seq(&scan), file->title, file->path);
+                               dbfile_free(file);
+                       }
+                       dbscan_free(&scan);
+                       dbindex_abort(tx);
+               } else if (strcmp(cmd, "--disks") == 0) {
+                       dbtxn *tx = dbindex_begin(db, NULL, 1);
+                       dbscan scan;
+
+                       for (dbdisk *disk = dbscan_disk(tx, &scan, db, 0); disk; disk = dbscan_disk_next(&scan)) {
+                               printf("id: %d\n", disk->id);
+                               printf("uuid: %s\n", disk->uuid);
+                               printf("label: %s\n", disk->label);
+                               printf("type: %s\n", disk->type);
+                               printf("mount: %s\n", disk->mount);
+                               printf("\n");
+                               dbdisk_free(disk);
+                       }
+                       dbscan_free(&scan);
+                       dbindex_abort(tx);
+               } else if (strcmp(cmd, "--disk-del") == 0) {
+                       dbtxn *tx = dbindex_begin(db, NULL, 0);
+                       int diskid = atoi(argv[++i]);
+
+                       dbdisk_del_id(tx, db, diskid);
+                       dbindex_commit(tx);
+               } else if (strcmp(cmd, "--validate") == 0) {
+                       dbindex_validate(db);
+               } else if (strcmp(cmd, "--search") == 0) {
+                       const char *match = argv[++i];
+                       dbfile *results[100];
+                       int res = dbfile_search(db, match, results, 100);
+
+                       if (res >= 0) {
+                               for (int i=0;i<res;i++) {
+                                       printf("%4d title: %s\n", results[i]->id, results[i]->title);
+                                       dbfile_free(results[i]);
+                               }
+                       } else {
+                               printf("search failed\n");
+                       }
+               }
+       }
+
+       dbindex_close(db);
+
+       return 0;
+}
diff --git a/http-monitor.c b/http-monitor.c
new file mode 100644 (file)
index 0000000..e14cf62
--- /dev/null
@@ -0,0 +1,543 @@
+
+#define _GNU_SOURCE
+
+#include <unistd.h>
+#include <sys/types.h>
+#include <sys/stat.h>
+#include <fcntl.h>
+
+#include <stdlib.h>
+#include <string.h>
+#include <netdb.h>
+
+#include <stdlib.h>
+#include "ez-blob-io.h"
+#include "ez-list.h"
+#include "ez-tree.h"
+
+#define obstack_chunk_alloc malloc
+#define obstack_chunk_free free
+#include <obstack.h>
+#include <stdio.h>
+#include <errno.h>
+#define D(x)
+
+#include "ez-http.h"
+
+#include "dbindex.h"
+#include "notify.h"
+
+#include "player.h"
+
+static dbindex *db;
+static notify_t player;
+
+static struct ez_pair ct_text_html = {
+       .name = "Content-Type",
+       .value = "text/html;charset=utf-8"
+};
+static struct ez_pair ct_text_xml = {
+       .name = "Content-Type",
+       .value = "text/xml"
+};
+static struct ez_pair ct_application_json = {
+       .name = "Content-Type",
+       .value = "application/json"
+};
+static struct ez_pair ce_gzip = {
+       .name = "Content-Encoding",
+       .value = "gzip"
+};
+
+static void obstack_sgrow(struct obstack *os, const char *value) {
+       obstack_grow(os, value, strlen(value));
+}
+
+static int blobio_load(struct ez_blobio *io, const char *path, struct obstack *os) {
+       struct stat st;
+       int res;
+
+       res = stat(path, &st);
+       if (res == 0) {
+               io->data = obstack_alloc(os, st.st_size);
+               if (io->data) {
+                       int fd = open(path, O_RDONLY);
+                       if (fd != -1) {
+                               res = read(fd, io->data, st.st_size);
+                               close(fd);
+                               if (res == st.st_size) {
+                                       io->alloc = st.st_size;
+                                       io->size = st.st_size;
+                                       io->index = 0;
+                                       io->mode = BLOBIO_READ;
+                                       res = 0;
+                               } else
+                                       res = -1;
+                       }
+               } else
+                       res = -1;
+       }
+
+       return res;
+}
+
+static struct ez_pair *find_param(struct ez_httprequest *r, const char *name) {
+       for (struct ez_pair *w = ez_list_head(&r->params), *n = ez_node_succ(w);n;w=n,n = ez_node_succ(w)) {
+               if (strcasecmp(w->name, name) == 0)
+                       return w;
+       }
+       return NULL;
+}
+
+static int param_int(struct ez_httprequest *r, const char *name, int def) {
+       struct ez_pair *h = find_param(r, name);
+
+       if (h && h->value)
+               return atoi(h->value);
+
+       return def;
+}
+
+static double param_float(struct ez_httprequest *r, const char *name, double def) {
+       struct ez_pair *h = find_param(r, name);
+
+       if (h && h->value)
+               return atof(h->value);
+
+       return def;
+}
+
+static const char *param_value(struct ez_httprequest *r, const char *name, const char *def) {
+       struct ez_pair *h = find_param(r, name);
+
+       if (h && h->value)
+               return (h->value);
+
+       return def;
+}
+
+static void duration_value(struct obstack *os, uint64_t ms) {
+       uint64_t s = (ms / 1000) % 60;
+       uint64_t m = (ms / (1000 * 60)) % 60;
+       uint64_t h = ms / (1000 * 60 * 60);
+
+       obstack_printf(os, "\"%02d:%02d:%02d\"", (int)h, (int)m, (int)s);
+}
+
+static void write_file_json(struct obstack *os, dbfile *file) {
+       obstack_printf(os, "\"id\":\"%d\"", file->id);
+
+       if (file->title)
+               obstack_printf(os, ",\"title\":\"%s\"", file->title);
+       if (file->artist)
+               obstack_printf(os, ",\"artist\":\"%s\"", file->artist);
+
+       obstack_sgrow(os, ",\"length\":");
+       duration_value(os, file->duration / 1000);
+       obstack_printf(os, ",\"lengthms\":\"%zd\"", file->duration / 1000);
+}
+
+static void write_state_json(dbindex *db, struct obstack *os) {
+       dbtxn *tx = dbindex_begin(db, NULL, 1);
+       dbstate state;
+       int res = dbstate_get(tx, db, &state);
+       dbfile *file = NULL;
+
+       printf("state.fileid = %d\n", state.fileid);
+       printf("state.pos = %zd\n", state.pos);
+       printf("state.listid = %d\n", state.listid);
+       printf("state.seq = %d\n", state.seq);
+
+       if (res == 0
+           && (state.state & 1)
+           && (file = dbfile_get(tx, db, state.fileid))) {
+               obstack_1grow(os, '{');
+               write_file_json(os, file);
+
+               obstack_printf(os, ",\"path\":\"%s\"", file->path);
+               obstack_printf(os, ",\"status\":\"%s\"", (state.state & 2) ? "paused" : "playing");
+
+               obstack_printf(os, ",\"listid\":\"%u\"", state.listid);
+               obstack_printf(os, ",\"seq\":\"%u\"", state.seq);
+               obstack_printf(os, ",\"fileid\":\"%u\"", state.fileid);
+
+               obstack_sgrow(os, ",\"position\":");
+               duration_value(os, (uint64_t)(state.poss * 1000));
+               obstack_printf(os, ",\"positionms\":\"%.0f\"", (state.poss * 1000));
+               obstack_1grow(os, '}');
+       } else {
+               obstack_sgrow(os, "{ 'status': 'idle' }");
+       }
+
+       dbindex_abort(tx);
+
+       dbfile_free(file);
+}
+
+static int write_lists_json(dbindex *db, struct obstack *io) {
+       dbtxn *tx = dbindex_begin(db, NULL, 1);
+       dbscan scan;
+       int first = 1;
+
+       obstack_1grow(io, '{');
+       obstack_sgrow(io, "\"status\":\"ok\"");
+       obstack_sgrow(io, ",\"items\": [");
+       for (dblist *list = dbscan_list(tx, &scan, db, 0); list; list = dbscan_list_next(&scan)) {
+               if (!first)
+                       obstack_1grow(io, ',');
+               first = 0;
+
+               obstack_1grow(io, '{');
+               obstack_printf(io, "\"id\":\"%d\"", list->id);
+               obstack_printf(io, ",\"name\":\"%s\"", list->name);
+               obstack_printf(io, ",\"size\":\"%d\"", list->size);
+               obstack_1grow(io, '}');
+       }
+       obstack_1grow(io, ']');
+       obstack_1grow(io, '}');
+
+       dbscan_free(&scan);
+       dbindex_abort(tx);
+
+       return 0;
+}
+
+/*
+  Given a file-id, (and a playlist??) list the next few in the playlist.
+ */
+static void write_list_json(dbindex *db, struct obstack *io, int listid, int seq, int fileid) {
+       dbscan scan;
+       dbfile *file;
+       dbtxn *tx = dbindex_begin(db, NULL, 1);
+
+       // TODO: get list properly and seq
+
+       obstack_1grow(io, '{');
+       obstack_sgrow(io, "\"list_name\":\"shuffle\"");
+       obstack_sgrow(io, ",\"items\": [");
+
+       file = dbscan_list_entry(tx, &scan, db, listid, seq, fileid);
+       for (int i=0;file && i<10;i++) {
+               obstack_sgrow(io, i > 0 ? ",{" : "{");
+               write_file_json(io, file);
+
+               obstack_printf(io, ",\"listid\":\"%u\"", listid);
+               obstack_printf(io, ",\"seq\":\"%u\"", dbscan_list_entry_seq(&scan));
+
+               obstack_1grow(io, '}');
+
+               dbfile_free(file);
+               file = dbscan_list_entry_next(&scan);
+       }
+       dbfile_free(file);
+       obstack_sgrow(io, "]}");
+
+       dbscan_free(&scan);
+       dbindex_abort(tx);
+}
+
+static void write_search_json(dbindex *db, struct obstack *io, const char *query) {
+       dbfile *results[50];
+       int len = dbfile_search(db, query, results, 50);
+
+       obstack_1grow(io, '{');
+
+       if (len >= 0) {
+               obstack_sgrow(io, "\"status\":\"ok\"");
+               obstack_sgrow(io, ",\"items\": [");
+
+               for (int i=0;i<len;i++) {
+                       dbfile *file = results[i];
+
+                       obstack_sgrow(io, i > 0 ? ",{" : "{");
+                       write_file_json(io, file);
+                       obstack_1grow(io, '}');
+
+                       dbfile_free(file);
+               }
+
+               obstack_sgrow(io, "]");
+       } else {
+               obstack_sgrow(io, "\"status\":\"error\"");
+       }
+       obstack_1grow(io, '}');
+}
+
+/* ********************************************************************** */
+
+static int handle_player(struct ez_httprequest *req, struct ez_httpresponse *rep) {
+#if 0
+       httpresponse_set_response(rep, 200, "Ok");
+       ez_list_addtail(&rep->http.headers, &ct_text_html);
+       ez_list_addtail(&rep->http.headers, &ce_gzip);
+       http_set_data(&rep->http, player_html, sizeof(player_html));
+#else
+       struct obstack *os = &rep->http.conn->os;
+
+       if (blobio_load(&rep->http.data, "player.html", os) == 0) {
+               httpresponse_set_response(rep, 200, "Ok");
+               ez_list_addtail(&rep->http.headers, &ct_text_html);
+       }
+#endif
+
+       return 0;
+}
+
+static int handle_quit(struct ez_httprequest *r, struct ez_httpresponse *rep) {
+       httpresponse_set_response(rep, 200, "Byte");
+       return 1;
+}
+
+static int handle_root(struct ez_httprequest *req, struct ez_httpresponse *rep) {
+       const char *msg = "<h1>It Works!</h1><a href='/quit'>Quit</a> | <a href='/x'>Player</a>";
+
+       httpresponse_set_response(rep, 200, "Ok");
+       ez_list_addtail(&rep->http.headers, &ct_text_html);
+       http_set_data(&rep->http, msg, strlen(msg));
+
+       return 0;
+}
+
+static int handle_fwd(struct ez_httprequest *req, struct ez_httpresponse *rep) {
+       struct notify_play_seek msg = { .mode = 1, .stamp = +30.0 };
+
+       notify_msg_send(player, NOTIFY_PLAY_SEEK, 0, &msg);
+       httpresponse_set_response(rep, 202, "Ok");
+       return 0;
+}
+
+static int handle_rew(struct ez_httprequest *req, struct ez_httpresponse *rep) {
+       struct notify_play_seek msg = { .mode = 1, .stamp = -30.0 };
+
+       notify_msg_send(player, NOTIFY_PLAY_SEEK, 0, &msg);
+       httpresponse_set_response(rep, 202, "Ok");
+       return 0;
+}
+
+static int handle_pause(struct ez_httprequest *req, struct ez_httpresponse *rep) {
+       notify_msg_send(player, NOTIFY_PLAY_PAUSE, 0, NULL);
+       httpresponse_set_response(rep, 202, "Ok");
+       return 0;
+}
+
+static int handle_next(struct ez_httprequest *req, struct ez_httpresponse *rep) {
+       notify_msg_send(player, NOTIFY_PLAY_NEXT, 0, NULL);
+       httpresponse_set_response(rep, 202, "Ok");
+       return 0;
+}
+
+static int handle_prev(struct ez_httprequest *req, struct ez_httpresponse *rep) {
+       notify_msg_send(player, NOTIFY_PLAY_PREV, 0, NULL);
+       httpresponse_set_response(rep, 202, "Ok");
+       return 0;
+}
+
+static int set_data_json(struct ez_httpresponse *rep) {
+       struct obstack *os = &rep->http.conn->os;
+       size_t size = obstack_object_size(os);
+       void *data = obstack_finish(os);
+
+       if (data) {
+               ez_list_addtail(&rep->http.headers, &ct_application_json);
+               http_set_data(&rep->http, data, size);
+               httpresponse_set_response(rep, 200, "Ok");
+       }
+
+       return 0;
+}
+
+static int handle_status(struct ez_httprequest *req, struct ez_httpresponse *rep) {
+       struct obstack *os = &rep->http.conn->os;
+
+       write_state_json(db, os);
+
+       return set_data_json(rep);
+}
+
+static int handle_lists(struct ez_httprequest *req, struct ez_httpresponse *rep) {
+       struct obstack *os = &rep->http.conn->os;
+
+       if (strcmp(req->method, "GET") == 0) {
+               write_lists_json(db, os);
+               set_data_json(rep);
+       } else if (strcmp(req->method, "POST") == 0) {
+               const char *name = param_value(req, "name", NULL);
+
+               if (name) {
+                       dbtxn *tx = dbindex_begin(db, NULL, 0);
+                       dblist list = {
+                               .name = (char *)name
+                       };
+
+                       printf("post create list '%s'\n", name);
+
+                       if (dblist_add(tx, db, &list) == 0) {
+                               ez_pair *location = obstack_alloc(os, sizeof(*location));
+
+                               location->name = "Location";
+                               obstack_printf(os, "/x/list/%d", list.id);
+                               obstack_1grow(os, 0);
+                               location->value = obstack_finish(os);
+
+                               httpresponse_set_response(rep, 201, "Created");
+                               ez_list_addtail(&rep->http.headers, location);
+                               dbindex_commit(tx);
+                       } else {
+                               dbindex_abort(tx);
+                       }
+               } else {
+                       httpresponse_set_response(rep, 400, "Missing name");
+               }
+       } else {
+               httpresponse_set_response(rep, 400, "Invalid method");
+       }
+
+       return 0;
+}
+
+void http_addheader(struct ez_http *http, const char *name, const char *value) {
+       struct obstack *os = &http->conn->os;
+       ez_pair *location = obstack_alloc(os, sizeof(*location));
+
+       location->name = obstack_copy(os, name, strlen(name)+1);
+       location->value = obstack_copy(os, value, strlen(value)+1);
+       ez_list_addtail(&http->headers, location);
+}
+
+// /x/list/{listid}
+static int handle_list(struct ez_httprequest *req, struct ez_httpresponse *rep) {
+       struct obstack *os = &rep->http.conn->os;
+       int lid = atoi(req->url + strlen("/x/list/"));
+       int fid = param_int(req, "f", 0);
+       int seq = param_int(req, "s", 0); // TODO: should be in /x/list/{listid}/{seq} ?
+
+       printf("%s %s, listid=%d seq=%d fid=%d\n", req->method, req->url, lid, seq, fid);
+
+       if (strcmp(req->method, "GET") == 0) {
+               write_list_json(db, os, lid, seq, fid);
+               return set_data_json(rep);
+       } else if (strcmp(req->method, "POST") == 0) {
+               // /x/list/{listid} post fid=?  ignore seq
+               if (fid == 0)
+                       goto fail0;
+
+               dbtxn *tx = dbindex_begin(db, NULL, 0);
+               if (!tx)
+                       goto fail0;
+               dblist *list = dblist_get(tx, db, lid);
+               if (!list || dblist_add_file(tx, db, list, fid))
+                       goto fail1;
+
+               if (dbindex_commit(tx))
+                       goto fail2;
+
+               char location[64];
+
+               sprintf(location, "/x/list/%u/%u", list->id, list->size - 1);
+               http_addheader(&rep->http, "Location", location);
+
+               httpresponse_set_response(rep, 201, "Created");
+               dblist_free(list);
+               return 0;
+
+       fail2:
+               dblist_free(list);
+
+       fail1:
+               dbindex_abort(tx);
+       fail0:
+               httpresponse_set_response(rep, 400, "Invalid");
+               return 0;
+       } else {
+               httpresponse_set_response(rep, 400, "Invalid method");
+               return 0;
+       }
+}
+
+static int handle_search(struct ez_httprequest *req, struct ez_httpresponse *rep) {
+       struct obstack *os = &rep->http.conn->os;
+       const char *query = param_value(req, "q", "");
+
+       write_search_json(db, os, query);
+
+       return set_data_json(rep);
+}
+
+// goto specific file /goto?f={file.id}
+// TODO: playlist
+static int handle_goto(struct ez_httprequest *req, struct ez_httpresponse *rep) {
+       int fid = param_int(req, "f", 0);
+
+       // TODO: look it up in the db?
+
+       if (fid != 0) {
+               struct notify_goto gogo = { .fileid = fid };
+
+               notify_msg_send(player, NOTIFY_PLAY_GOTO, 0, &gogo);
+               httpresponse_set_response(rep, 202, "Requested");
+               return 0;
+       } else {
+               httpresponse_set_response(rep, 404, "Not Found");
+               return 0;
+       }
+}
+
+// seek absolute or relative
+static int handle_seek(struct ez_httprequest *req, struct ez_httpresponse *rep) {
+       ez_pair *skip = find_param(req, "skip");
+       ez_pair *seek = find_param(req, "seek");
+       int isskip = skip && skip->value;
+       int isseek = seek && seek->value;
+
+       if (!(isskip ^ isseek)) {
+               httpresponse_set_response(rep, 400, "Bad Request");
+               return 0;
+       }
+
+       struct notify_play_seek msg = { .mode = isseek ? 0 : 1, .stamp = atof(isseek ? seek->value : skip->value) };
+
+       printf("seek: %s stamp=%+5.2f\n", msg.mode ? " set" : "seek", msg.stamp);
+
+       notify_msg_send(player, NOTIFY_PLAY_SEEK, 0, &msg);
+       httpresponse_set_response(rep, 202, "Requested");
+       return 0;
+}
+
+static struct ez_httphandler handler_list[] = {
+       { .path = "/x", .fn = handle_player },
+       { .path = "/x/", .fn = handle_player },
+
+       { .path = "/x/fwd", .fn = handle_fwd },
+       { .path = "/x/rew", .fn = handle_rew },
+       { .path = "/x/pause", .fn = handle_pause },
+       { .path = "/x/next", .fn = handle_next },
+       { .path = "/x/prev", .fn = handle_prev },
+       { .path = "/x/seek", .fn = handle_seek },
+       { .path = "/x/goto", .fn = handle_goto },
+
+       { .path = "/x/status", .fn = handle_status },
+       { .path = "/x/list", .fn = handle_lists },
+       { .path = "/x/list/", .fn = handle_list, .mode=EZ_PATH_PREFIX },
+
+       { .path = "/x/search", .fn = handle_search },
+
+       { .path = "/", .fn = handle_root },
+       { .path = "/quit", .fn = handle_quit },
+};
+
+int main(int argc, char **argv) {
+       struct ez_httpserver serv;
+
+       db = dbindex_open(MAIN_INDEX);
+       player = notify_writer_new(NOTIFY_PLAYER);
+
+       httpserver_init(&serv, 8000);
+       httpserver_addhandlers(&serv, handler_list, sizeof(handler_list)/sizeof(handler_list[0]));
+       httpserver_run(&serv);
+       httpserver_free(&serv);
+
+       notify_close(player);
+       dbindex_close(db);
+
+       return 0;
+}
index a82c496..745e42e 100644 (file)
@@ -120,6 +120,8 @@ static const struct keymap map[] = {
        { NOTIFY_VOLUME_MUTE, KEY_MUTE },
        { NOTIFY_VOLUME_DOWN, KEY_VOLUMEDOWN, GEN_REPEAT }, // - button
        { NOTIFY_VOLUME_UP, KEY_VOLUMEUP, GEN_REPEAT },   // + button
+
+       { NOTIFY_PLAY_PAUSE, BTN_LEFT },
 };
 
 static int cmp_key(const void *ap, const void *bp) {
@@ -207,6 +209,8 @@ static void monitor_event(struct monitor *m, struct input_event *ev) {
                        struct notify_key msg = {.code = ev->code };
                        notify_msg_send(m->player, NOTIFY_KEY, 0, &msg);
                }
+       } else {
+               //printf("?ev %d %04x %08x\n", ev->type, ev->code, ev->value);
        }
 }
 
@@ -253,7 +257,7 @@ void monitor(struct monitor *m) {
        polla[1].events = POLLIN;
 
        while (1) {
-               printf("poll, timeout = %d\n", m->repeat.value != 0 ? 50 : -1);
+               //printf("poll, timeout = %d\n", m->repeat.value != 0 ? 50 : -1);
 
                int res = poll(polla, 2, m->repeat.value != 0 ? 50 : -1);
                if (res > 0) {
@@ -267,6 +271,7 @@ void monitor(struct monitor *m) {
                                }
                        }
                } else if (res == 0) {
+                       printf(" repeat\n");
                        monitor_event(m, &m->repeat);
                }
        }
index 37ca7f1..0d7fc78 100644 (file)
 
 struct audio_player;
 
+//#define VOICE_MENU
 //#define VOICE_MT
 
+#ifdef VOICE_MNENU
 /*
   synchronous voice experiment
 */
@@ -71,6 +73,7 @@ int audio_voice_speak(struct audio_player *ap, const char *text);
 #else
 static void handle_audio_msg(struct audio_player *ap, struct voice_audio_msg *msg);
 #endif
+#endif
 
 /*
   Player bits
@@ -109,7 +112,6 @@ struct audio_player {
        // playlist management
        dbindex *index;
        dbfile *playing;
-       char *playing_path;
        dbstate playing_state;
 
        int quit;
@@ -160,7 +162,9 @@ struct audio_player *audio_player_new(const char *device) {
        ap->device = strdup(device);
        ap->index = dbindex_open(MAIN_INDEX);
 
+#ifdef VOICE_MENU
        audio_init_voice(ap);
+#endif
        audio_init_mixer(ap);
 
        return ap;
@@ -231,11 +235,11 @@ void audio_player_free(struct audio_player *ap) {
        audio_close_pcm(ap);
        audio_close_mixer(ap);
 
+#ifdef VOICE_MENU
        audio_close_voice(ap);
+#endif
 
        dbindex_close(ap->index);
-
-       free(ap->playing_path);
        dbfile_free(ap->playing);
 
        free(ap->poll);
@@ -331,6 +335,8 @@ static unsigned int hw_sample_rate(struct audio_player *ap) {
  * Samples are in stereo interleaved signed 16-bit format.
  *
  * The audio device will be in the READY state when this returns successfully.
+ *
+ * FIXME: if this fails it should exit.
  */
 int audio_init_pcm(struct audio_player *ap) {
        int res;
@@ -578,39 +584,133 @@ int audio_init_media(struct audio_player *ap, const char *path) {
 
        return audio_init_filter(ap);
  fail:
-       perror("init media");
+       printf("audio_init_media failed\n");
        audio_close_media(ap);
        return -1;
 }
 
+#include "lmdb.h"
+
 int audio_checkpoint_state(struct audio_player *ap) {
        dbtxn *tx = dbindex_begin(ap->index, NULL, 0);
+       int res;
 
        if (tx) {
                // TODO: meaningful state value
+               /*
                if (ap->playing) {
                        ap->playing_state.state = 1;
                        ap->playing_state.fileid = ap->playing->id;
                } else {
+                       // ?
                        ap->playing_state.state = 0;
                        ap->playing_state.fileid = 0;
+                       }*/
+               if (ap->playing) {
+                       ap->playing_state.state = 1 | (ap->paused ? 2 : 0);
+                       // TODO: just update pos directly?
+                       ap->playing_state.pos = ap->pos;
+                       AVRational sb = ap->audio->time_base;
+                       ap->playing_state.poss = av_q2d(sb) * ap->pos;
+               } else {
+                       ap->playing_state.state = 0;
+                       ap->playing_state.pos = 0;
+                       ap->playing_state.poss = 0;
                }
-               printf("checkpoint state=%d file=%d '%s'\n", ap->playing_state.state, ap->playing_state.fileid, ap->playing_path);
-               // ap->pos?
-               ap->playing_state.pos = 0;
                ap->playing_state.stamp = time(NULL);
-               if (dbstate_put(tx, ap->index, &ap->playing_state) == 0) {
+
+               printf("checkpoint state=%d file=%d pos=%zd poss=%f '%s'\n", ap->playing_state.state, ap->playing_state.fileid, ap->playing_state.pos, ap->playing_state.poss, ap->playing ? ap->playing->full_path : "");
+               //printf("Checkpoint state, = %d\n", ap->playing_state.state);
+
+               if ((res = dbstate_put(tx, ap->index, &ap->playing_state)) == 0) {
                        dbindex_commit(tx);
                } else {
+                       printf("checkpoint failed: %s\n", mdb_strerror(res));
                        dbindex_abort(tx);
                }
+       } else {
+               printf("checkpoint failed to get tx\n");
        }
 
        return 0;
 }
 
 // Next in play queue
+static int audio_advance_file(struct audio_player *ap, dbfile *(*advance)(dbscan *scan)) {
+       int res;
+       int empty = ap->playing == NULL;
+       dbscan scan;
+       dbfile *file;
+       dbtxn *tx = dbindex_begin(ap->index, NULL, 1);
+       int retry = 0;
+
+       dbfile_free(ap->playing);
+       ap->playing = NULL;
+
+       do {
+               printf("loop: %d\n", retry);
+               file = dbscan_list_entry(tx, &scan, ap->index, ap->playing_state.listid, ap->playing_state.seq, ap->playing_state.fileid);
+               if (file) {
+                       printf("entry: %s\n", file->path);
+                       dbfile_free(file);
+                       file = advance(&scan);
+                       printf("next: %s\n", file ? file->path : "<nil>");
+               }
+               while (file) {
+                       res = audio_init_media(ap, dbfile_full_path(tx, ap->index, file));
+
+                       if (res == (-30798) && !empty) // && >loop?
+                               res = 1;
+
+                       if (res == 0) {
+                               ap->playing = file;
+                               ap->playing_state.listid = dbscan_list_entry_listid(&scan);
+                               ap->playing_state.seq = dbscan_list_entry_seq(&scan);
+                               ap->playing_state.fileid = file->id;
+                               audio_checkpoint_state(ap);
+                               break;
+                       }
+
+                       dbfile_free(file);
+                       file = advance(&scan);
+               }
+               // repeat at end of list?
+               if (!file && dbscan_res(&scan) == -30798) {
+                       ap->playing_state.seq = 0;
+                       ap->playing_state.fileid = 0;
+               }
+               dbscan_free(&scan);
+       } while (!file && retry++ == 0);
+
+       dbindex_abort(tx);
+       return res;
+#if 0
+       audio_close_media(ap);
+       do {
+               //res = dbfile_next(ap->index, &ap->playing, &ap->playing_path);
+               printf("a ap playing = %d\n", ap->playing ? ap->playing->id : -1);
+               res = dbfile_next_shuffle(ap->index, &ap->playing, &ap->playing_path);
+               printf("b ap playing = %d\n", ap->playing ? ap->playing->id : -1);
+
+               if (res == 0)
+                       res = audio_init_media(ap, ap->playing_path);
+               if (res == (-30798) && !empty) // && >loop?
+                       res = 1;
+       } while (res != 0 && res !=(-30798)); // MDB_NOTFOUND
+
+       audio_checkpoint_state(ap);
+
+       //if (res != 0) {
+       //      audio_stop_pcm(ap);
+       //}
+
+       return res;
+#endif
+}
+
 int audio_next_file(struct audio_player *ap) {
+       return audio_advance_file(ap, dbscan_list_entry_next);
+#if 0
        int res;
        int empty = ap->playing == NULL;
 
@@ -635,9 +735,12 @@ int audio_next_file(struct audio_player *ap) {
        //}
 
        return res;
+#endif
 }
 
 int audio_prev_file(struct audio_player *ap) {
+       return audio_advance_file(ap, dbscan_list_entry_prev);
+#if 0
        int res;
        int empty = ap->playing == NULL;
 
@@ -657,6 +760,29 @@ int audio_prev_file(struct audio_player *ap) {
        //      audio_stop_pcm(ap);
        //}
 
+       return res;
+#endif
+}
+
+int audio_goto_file(struct audio_player *ap, int fileid) {
+       int res = -1; // MDB_NOTFOUND
+       dbtxn *tx = dbindex_begin(ap->index, NULL, 1);
+       dbfile *file = dbfile_get(tx, ap->index, fileid);
+
+       printf("player goto: %d  diskid=%d\n", fileid, file->diskid);
+
+       if (file) {
+               dbfile_full_path(tx, ap->index, file);
+
+               ap->playing = file;
+
+               res = audio_init_media(ap, file->full_path);
+               if (res == 0) {
+                       audio_checkpoint_state(ap);
+               }
+       }
+       dbindex_abort(tx);
+
        return res;
 }
 
@@ -666,31 +792,33 @@ int audio_restore_state(struct audio_player *ap) {
        dbtxn *tx = dbindex_begin(ap->index, NULL, 1);
        int res = -1;
 
-       if (dbstate_get(tx, ap->index, &ap->playing_state) == 0 && ap->playing_state.state == 1) {
+       // FIXME: playlist stuff?
+
+       if ((res = dbstate_get(tx, ap->index, &ap->playing_state)) == 0 && ap->playing_state.state == 1) {
                dbfile *file = dbfile_get(tx, ap->index, ap->playing_state.fileid);
 
+               printf("restoring file %s\n", file->path);
                if (file) {
-                       dbdisk *disk = dbdisk_get(tx, ap->index, file->diskid);
-
-                       if (disk) {
-                               char path[strlen(disk->mount) + strlen(file->path) + 1];
-
-                               sprintf(path, "%s%s", disk->mount, file->path);
+                       if (dbfile_full_path(tx, ap->index, file)) {
                                ap->playing = file;
-                               ap->playing_path = strdup(path);
-
-                               dbdisk_free(disk);
 
-                               res = audio_init_media(ap, ap->playing_path);
+                               res = audio_init_media(ap, file->full_path);
+                               // seek ?
                        } else {
                                dbfile_free(file);
                        }
                }
+       } else {
+               // FIXME: check playing_state.state == 1 separate to error test above
+               res = -1;
+               printf("unable to restore state\n");
+               // ??
+               //ap->playing_state.listid = 1;
        }
 
        dbindex_commit(tx);
 
-       printf("restore state=%d file=%d '%s'\n", ap->playing_state.state, ap->playing_state.fileid, ap->playing_path);
+       printf("restore state=%d file=%d '%s'\n", ap->playing_state.state, ap->playing_state.fileid, ap->playing ? ap->playing->full_path : NULL);
 
        if (res == 0)
                return res;
@@ -703,6 +831,9 @@ int audio_restore_state(struct audio_player *ap) {
 void audio_player_control(struct audio_player *ap) {
        int ready = notify_msg_ready(ap->player);
 
+       if (time(NULL) != ap->playing_state.stamp)
+               audio_checkpoint_state(ap);
+
 #ifdef VOICE_MT
  mainloop:
 #endif
@@ -734,7 +865,7 @@ void audio_player_control(struct audio_player *ap) {
                        res = poll(fds, 2, -1); // timeout to check stuff?
 
                        if (fds[0].revents & POLLERR
-                           || fds[0].revents & POLLERR) {
+                           || fds[1].revents & POLLERR) {
                                // what now?
 
                        }
@@ -763,12 +894,14 @@ void audio_player_control(struct audio_player *ap) {
                        if (ap->paused) {
                                res = snd_pcm_pause(ap->aud, 0);
                                ap->paused = 0;
+                               audio_checkpoint_state(ap);
                        }
                        break;
                case NOTIFY_PLAY_PAUSE:
                        if (ap->cc) {
                                ap->paused ^= 1;
                                res = snd_pcm_pause(ap->aud, ap->paused);
+                               audio_checkpoint_state(ap);
                        }
                        break;
                case NOTIFY_PLAY_STOP:
@@ -812,6 +945,11 @@ void audio_player_control(struct audio_player *ap) {
                case NOTIFY_PLAY_PREV:
                        audio_prev_file(ap);
                        break;
+               case NOTIFY_PLAY_GOTO: {
+                       struct notify_goto *g = msg;
+
+                       audio_goto_file(ap, g->fileid);
+                       break; }
                case NOTIFY_VOLUME_UP:
                        audio_mixer_adjust(ap, +1);
                        break;
@@ -821,6 +959,7 @@ void audio_player_control(struct audio_player *ap) {
                case NOTIFY_VOLUME_MUTE:
                        audio_mixer_mute(ap);
                        break;
+#ifdef VOICE_MENU
                case NOTIFY_KEY: {
                        /*
                          Hmm, this might get ugly fast, is there another way?
@@ -855,6 +994,7 @@ void audio_player_control(struct audio_player *ap) {
                        }
                        break;
                }
+#endif
                case NOTIFY_DEBUG: {
                        struct notify_debug *d = msg;
 
@@ -1012,6 +1152,7 @@ int main(int argc, char **argv) {
   Thread starts synthesising, sends audio to music player via throttled message port(?)
 
  */
+#ifdef VOICE_MENU
 
 #include <espeak-ng/speak_lib.h>
 
@@ -1255,3 +1396,4 @@ int voice_speak(struct audio_player *ap, const char *text) {
        return 0;
 }
 #endif
+#endif /* VOICE_MENU */
index 8edbf3e..a1daf81 100644 (file)
--- a/notify.c
+++ b/notify.c
 #include <errno.h>
 
 #include "dbindex.h"
-
+#include "ez-blob-basic.h"
 #include "notify.h"
 
 // Message handling and routing
 
 static ez_blob_desc PLAY_SEEK_DESC[] = {
-       EZ_BLOB_START(struct notify_play_seek),
+       EZ_BLOB_START(struct notify_play_seek, 1, 2),
        EZ_BLOB_INT32(struct notify_play_seek, 1, mode),
        EZ_BLOB_FLOAT64(struct notify_play_seek, 2, stamp),
-       EZ_BLOB_END(struct notify_play_seek)
 };
 
 static ez_blob_desc DEBUG_DESC[] = {
-       EZ_BLOB_START(struct notify_debug),
+       EZ_BLOB_START(struct notify_debug, 2, 1),
        EZ_BLOB_INT32(struct notify_debug, 1, func),
-       EZ_BLOB_END(struct notify_debug)
 };
 
 static ez_blob_desc KEY_DESC[] = {
-       EZ_BLOB_START(struct notify_key),
+       EZ_BLOB_START(struct notify_key, 3, 1),
        EZ_BLOB_INT32(struct notify_key, 1, code),
-       EZ_BLOB_END(struct notify_key)
+};
+
+static ez_blob_desc GOTO_DESC[] = {
+       EZ_BLOB_START(struct notify_goto, 4, 3),
+       EZ_BLOB_INT32(struct notify_goto, 1, listid),
+       EZ_BLOB_INT32(struct notify_goto, 2, seq),
+       EZ_BLOB_INT32(struct notify_goto, 3, fileid),
 };
 
 /**
@@ -80,7 +84,9 @@ static ez_blob_desc *action_desc[] = {
 
        DEBUG_DESC,
 
-       NULL
+       NULL,
+
+       GOTO_DESC
 };
 
 // should be global by default
@@ -148,11 +154,13 @@ int notify_msg_send(mqd_t q, enum notify_action action, unsigned int msg_pri, co
        if (action < NOTIFY_SIZEOF) {
                printf("send action %d\n", action);
                if (action_desc[action]) {
-                       size_t size = ez_blob_size(action_desc[action], p);
+                       size_t size = ez_basic_size(action_desc[action], p);
                        char msg[size+1];
+                       ez_blob blob = { .eb_size = size, .eb_data = msg + 1 };
 
                        msg[0] = action;
-                       ez_blob_encode_raw(action_desc[action], p, msg+1, size);
+                       ez_basic_encode_raw(action_desc[action], p, &blob);
+
                        res = mq_send(q, msg, size+1, msg_pri);
                } else {
                        res = mq_send(q, (char *)&action, 1, msg_pri);
@@ -188,7 +196,8 @@ void *notify_msg_receive(mqd_t q, enum notify_action *actionp, unsigned int *msg
                                *actionp = action;
 
                                if (action_desc[action]) {
-                                       void *p = ez_blob_decode(action_desc[action], msg+1, size-1);
+                                       ez_blob blob = { .eb_size = size - 1, .eb_data = msg + 1 };
+                                       void *p = ez_basic_decode(action_desc[action], &blob);
 
                                        if (p)
                                                return p;
index 58e9bf7..5501218 100644 (file)
--- a/notify.h
+++ b/notify.h
@@ -49,6 +49,7 @@ enum notify_action {
        NOTIFY_DEBUG,           // debug/prototyping command
 
        NOTIFY_SHUFFLE,         /* disk-manager: create shuffled playlist */
+       NOTIFY_PLAY_GOTO,       /* player - go to fileid */
 
        NOTIFY_SIZEOF
 };
@@ -68,6 +69,12 @@ struct notify_key {
        int code;
 };
 
+struct notify_goto {
+       int listid;
+       int seq;
+       int fileid;
+};
+
 notify_t notify_reader_new(const char *path);
 notify_t notify_writer_new(const char *path);
 void notify_close(notify_t q);
diff --git a/player.html b/player.html
new file mode 100644 (file)
index 0000000..d84788a
--- /dev/null
@@ -0,0 +1,599 @@
+<html>
+  <head>
+    <meta name='viewport' content='width=device-width, initial-scale=1'>
+    <style>
+     .items {
+        list-style-type: none;
+        padding: 0;
+        margin: 0;
+     }
+     .items > li {
+        /*border: 1px solid red;*/
+     }
+     .fixed {
+        font-family: monospace;
+        white-space: pre;
+     }
+     .track-list {
+        width: 100%;
+     }
+     .track-list .track {
+        width: 100%;
+        max-width: 5em;
+        overflow: hidden;
+     }
+     .track-list .title,
+     .track-list .artist,
+     .track-list .duration,
+     .track-list .action {
+        font-family: monospace;
+        white-space: pre;
+     }
+     .track-list .duration {
+        vertical-align: text-baseline;
+     }
+     .media-button rect.button-border {
+        fill: steelblue;
+        /* stroke-width: 2px;
+           stroke: steelblue;*/
+        rx: 15%;
+     }
+     .media-button path {
+        fill: white;
+     }
+     /* scrolling lists */
+     .track-list tbody {
+        display: block;
+        overflow-y: auto;
+        height: 20em;
+     }
+     .track-list thead tr {
+        display: block;
+     }
+     /* zebra colours */
+     .track-list tr {
+        background: grey;
+        color: black;
+     }
+     .track-list tr td {
+        padding: 0.25em 0.5em;
+     }
+     .track-list tr.odd {
+        background: darkgrey;
+        color: black;
+     }
+     body {
+        background: lightgrey;
+        font-family: monospace;
+     }
+     .progress-bar {
+     }
+     .progress-indicator {
+        height: 1em;
+        background: slategrey;
+     }
+     #track-info span {
+        padding: 0.25em 0.5em;
+        margin: 0px 1px;
+        background: darkgrey;
+     }
+     .section {
+        border: 1px solid black;
+        margin: 1em;
+        padding: 0.5em 0em;
+     }
+     .player-button:hover,
+     .player-button:focus
+     {
+        box-shadow: 0 0 2px 3px grey;
+     }
+     tr.even .player-button:hover,
+     tr.even .player-button:focus {
+            box-shadow: 0 0 2px 3px darkgrey;
+        }
+
+    </style>
+    <script>
+     /*
+     ideas on playlist and searches
+
+       search: shows some results
+
+       visit: can visit a file, pausing the playlist position.
+       add to list: add to some list
+       ?remove from list?
+
+     */
+
+     var playlistID = 1; // current list, from state?
+     var listStartID = 0; // "coming up" first entry
+     var allPlayLists = [];
+
+     // failed?
+     function requestGET(url, done) {
+       var r = new XMLHttpRequest();
+       r.onreadystatechange = function() {
+        //console.log(r);
+        if (r.readyState === XMLHttpRequest.DONE) {
+          done(r);
+        }
+       }
+       r.open("GET", url);
+       r.send();
+     }
+
+     function playerCommand(name) {
+       var r = new XMLHttpRequest();
+
+       console.log("command " + name);
+       r.onreadystatechange = function() {
+        console.log(r);
+        if (r.readyState === XMLHttpRequest.DONE) {
+          if (r.status === 0 || (r.status >= 200 && r.status < 400)) {
+            console.log("got it");
+          }
+        }
+       }
+       r.open("GET", "/x/" + name);
+       r.send();
+     }
+     // goto button, b.id = "goto-fileid"
+     function playerGotoClick(e) {
+       playerCommand("goto?f=" + e.currentTarget.id.substring(5));
+     }
+     function playerGotoKey(e) {
+       if (e.key == "Enter" || e.key == " ") {
+        playerCommand("goto?f=" + e.currentTarget.id.substring(5));
+       }
+     }
+
+     function playerCommandClick(e) {
+       playerCommand(e.currentTarget.id.substring(7));
+     }
+     function playerCommandKey(e) {
+       if (e.key == "Enter" || e.key == " ") {
+        playerCommand(e.currentTarget.id.substring(7));
+       }
+     }
+
+     function playerCommandClick2(e) {
+       playerCommand(e.currentTarget.getAttribute("query"));
+     }
+     function playerCommandKey2(e) {
+       if (e.key == "Enter" || e.key == " ") {
+        playerCommand(e.currentTarget.getAttribute("query"));
+       }
+     }
+
+     function playerSetPlaylists(items, list) {
+       let selected = items.value;
+
+       while (items.firstChild)
+        items.firstChild.remove();
+
+       for (var i=0; i<list.length; i++) {
+        let v = list[i];
+        let o = document.createElement("option");
+
+        o.value = v.id; // v.name?
+        o.textContent = v.name + " (" + v.size + ")";
+
+        items.appendChild(o);
+       }
+
+       if (selected) {
+        items.value = selected;
+       }
+     }
+
+     function playerUpdateLists() {
+       console.log("polling lists");
+       requestGET("/x/list", function(r) {
+        if (r.status === 200) {
+          let list = JSON.parse(r.response);
+
+          allPlayLists = list.items;
+
+          // TODO: put this somewhere
+          playerSetPlaylists(document.getElementById("lists"), allPlayLists);
+          //playerSetPlaylists(document.getElementById("next-lists"), list);
+        }
+       });
+     }
+     function createIcon(href) {
+       let b = document.createElementNS("http://www.w3.org/2000/svg", "svg");
+
+       b.classList.add('player-button');
+       b.tabIndex = 1;
+       b.setAttribute("viewBox", "-2 -2 28 28");
+       b.setAttribute("width", "3em");
+
+       let u = document.createElementNS("http://www.w3.org/2000/svg", "use");
+       u.setAttribute("href", href);
+       b.appendChild(u);
+       return b;
+     }
+     function playerAppendTracks(items, list, search) {
+       for (var i=0; i<list.length; i++) {
+        let v = list[i];
+        let row = document.createElement("tr");
+        let c = row.insertCell();
+
+        let d = document.createElement("div");
+        let t = document.createElement("div");
+        let a = document.createElement("div");
+
+        c.classList.add("track");
+
+        t.textContent = v.title;
+        t.classList.add("title");
+        a.textContent = v.artist;
+        a.classList.add("artist");
+
+        d.appendChild(t);
+        d.appendChild(a);
+        c.appendChild(d);
+
+        c = row.insertCell();
+        c.textContent = v.length;
+        c.classList.add("duration");
+
+        c = row.insertCell();
+        /*
+           let b = document.createElement("span");
+           b.id = "goto-" + v.id;
+           b.textContent = "\u23f5";// "\u25B6";
+           b.tabIndex = 50;
+           b.setAttribute("query", "goto?f=" + v.id);
+           b.addEventListener("click", playerCommandClick2);
+           b.addEventListener("keypress", playerCommandKey2);
+         */
+        /*
+           let b = document.createElementNS("http://www.w3.org/2000/svg", "svg");
+
+           b.tabIndex = 1;
+           b.setAttribute("query", "goto?f=" + v.id);
+           b.setAttribute("viewBox", "-2 -2 28 28");
+           b.setAttribute("width", "3em");
+           b.addEventListener("click", playerCommandClick2);
+           b.addEventListener("keypress", playerCommandKey2);
+
+           let u = document.createElementNS("http://www.w3.org/2000/svg", "use");
+           u.setAttribute("href", "#icon-play");
+           b.appendChild(u);
+         */
+
+        let b = createIcon("#icon-play");
+        b.setAttribute("query", "goto?f=" + v.id);
+        b.addEventListener("click", playerCommandClick2);
+        b.addEventListener("keypress", playerCommandKey2);
+
+        c.appendChild(b);
+
+        if (search) {
+          b = createIcon("#icon-playlist-add");
+          //b.setAttribute("query", "list/" + v.goto?f=" + v.id);
+          //b.addEventListener("click", playerCommandClick2);
+          //b.addEventListener("keypress", playerCommandKey2);
+          c.appendChild(b);
+        } else {
+          b = createIcon("#icon-nope");
+          c.appendChild(b);
+        }
+
+        c.classList.add("action");
+
+        row.classList.add((i & 1) ? "odd" : "even");
+
+        items.appendChild(row);
+       }
+     }
+
+     function playerUpdateList(fileid, seq) {
+       console.log("polling playlist");
+       requestGET("/x/list/" + playlistID + "?f=" + fileid + "?s=" + seq, function(r) {
+        if (r.status === 200) {
+          let table = document.getElementById("coming-up");
+          let items = document.getElementById("coming-up-list");
+          let list = JSON.parse(r.response);
+
+          while (items.firstChild)
+            items.firstChild.remove();
+
+          if (list.items.length > 0) {
+            table.caption = document.createElement("caption");
+            table.caption.textContent = "Coming up in playlist '" + list.list_name + "' …";
+          }
+
+          playerAppendTracks(items, list.items, 0);
+          /*
+             for (var i=0; i<list.list.length; i++) {
+             let v = list.list[i];
+             let row = document.createElement("tr");
+             let c = row.insertCell();
+
+             c.textContent = v.title;
+             c.classList.add("title");
+             c = row.insertCell();
+             c.textContent = v.artist;
+             c.classList.add("artist");
+             c = row.insertCell();
+             c.textContent = v.length;
+             c.classList.add("duration");
+
+             c = row.insertCell();
+             let b = document.createElement("span");
+             b.id = "goto-" + v.id;
+             b.textContent = "GOTO";
+             b.tabIndex = 50;
+             b.addEventListener("click", playerGotoClick);
+             b.addEventListener("keypress", playerGotoKey);
+             c.appendChild(b);
+
+             items.appendChild(row);
+             } */
+        }
+        listStartID = fileid;
+       });
+     }
+
+     function playerSearch(query) {
+       console.log("search: " + query);
+       requestGET("/x/search?q=" + encodeURIComponent(query), function(r) {
+        if (r.status === 200) {
+          let table = document.getElementById("search-results");
+          let items = document.getElementById("search-results-list");
+          let list = JSON.parse(r.response);
+
+          while (items.firstChild)
+            items.firstChild.remove();
+
+          //table.caption = document.createElement("caption");
+          //if (list.items.length > 0) {
+          //  table.caption.textContent = "Matches";
+          //} else {
+          //  table.caption.textContent = "No matches";
+         // }
+
+          playerAppendTracks(items, list.items, 1);
+          /*
+             for (var i=0; i<list.items.length; i++) {
+             let v = list.items[i];
+             let row = document.createElement("tr");
+             let c = row.insertCell();
+
+             c.textContent = v.title;
+             c = row.insertCell();
+             c.textContent = v.artist;
+             c = row.insertCell();
+             c.textContent = v.length;
+
+             c = row.insertCell();
+             let b = document.createElement("span");
+             b.id = "goto-" + v.id;
+             b.textContent = "GOTO";
+             b.tabIndex = 50;
+             b.addEventListener("click", playerGotoClick);
+             b.addEventListener("keypress", playerGotoKey);
+             c.appendChild(b);
+
+             // add to playlist button, or whatever
+
+             items.appendChild(row);
+             }*/
+        }
+       });
+     }
+
+     function playerPoll() {
+       var r = new XMLHttpRequest();
+
+       //console.log("polling");
+       r.onreadystatechange = function() {
+        //console.log(r);
+        if (r.readyState === XMLHttpRequest.DONE) {
+          if (r.status == 200) {
+            var s = JSON.parse(r.response);
+
+            document.getElementById("track-title").textContent = s.title;
+            document.getElementById("track-artist").textContent = s.artist;
+            document.getElementById("track-path").textContent = s.path;
+            document.getElementById("track-length").textContent = s.length;
+
+            /*
+               let p = document.getElementById("track-position");
+
+               if (s.status == "playing") {
+               p.textContent = s.position;
+               } else if (s.status == "paused") {
+               p.textContent = s.position + "(paused)";
+               //p.class ? paused
+               } else {
+               p.textContent = "(stopped)";
+               }*/
+
+            document.getElementById("track-progress").style.width = (s.positionms * 100 / s.lengthms) + "%";
+            document.getElementById("track-position").textContent = s.position;
+            document.getElementById("track-length").textContent = s.length;
+
+            if (s.id != listStartID) {
+              playerUpdateList(s.id, s.seq);
+              playerUpdateLists();
+            }
+          } else {
+            document.getElementById("track-title").textContent = "?";
+            document.getElementById("track-artist").textContent = "?";
+            document.getElementById("track-length").textContent = "?";
+            document.getElementById("track-position").textContent = "?";
+          }
+
+          window.setTimeout(playerPoll, 5000);
+        }
+       }
+       r.open("GET", "/x/status");
+       r.send();
+     }
+
+     function playerCreateList(name) {
+       var r = new XMLHttpRequest();
+
+       console.log("create list: " + name);
+
+       r.onreadystatechange = function() {
+        console.log(r);
+        if (r.readyState === XMLHttpRequest.DONE) {
+          if (r.status === 0 || (r.status >= 200 && r.status < 400)) {
+            console.log("got it");
+            playerUpdateLists();
+          }
+        }
+       }
+       r.open("POST", "/x/list?name=" + encodeURIComponent(name));
+       r.send();
+     }
+
+     document.addEventListener("DOMContentLoaded", function() {
+       // query in attribute
+       Array.prototype.forEach.call(document.getElementsByClassName("player-button"), function(b) {
+        b.addEventListener("click", playerCommandClick2);
+        b.addEventListener("keypress", playerCommandKey2);
+       });
+
+       playerPoll();
+     });
+    </script>
+  </head>
+  <body>
+    <svg width='0' height='0' xmlns="http://www.w3.org/2000/svg">
+      <defs>
+       <svg id='icon-forward' class='media-button' xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 0 24 24" width="24">
+         <rect class='button-border' width='24' height='24'/>
+         <path d="M5.58 16.89l5.77-4.07c.56-.4.56-1.24 0-1.63L5.58 7.11C4.91 6.65 4 7.12 4 7.93v8.14c0 .81.91 1.28 1.58.82zM13 7.93v8.14c0 .81.91 1.28 1.58.82l5.77-4.07c.56-.4.56-1.24 0-1.63l-5.77-4.07c-.67-.47-1.58 0-1.58.81z"/>
+       </svg>
+       <svg id='icon-rewind' class='media-button' xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 0 24 24" width="24">
+         <rect class='button-border' width='24' height='24'/>
+         <path d="M11 16.07V7.93c0-.81-.91-1.28-1.58-.82l-5.77 4.07c-.56.4-.56 1.24 0 1.63l5.77 4.07c.67.47 1.58 0 1.58-.81zm1.66-3.25l5.77 4.07c.66.47 1.58-.01 1.58-.82V7.93c0-.81-.91-1.28-1.58-.82l-5.77 4.07c-.57.4-.57 1.24 0 1.64z"/>
+       </svg>
+       <svg id='icon-pause' class='media-button' xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 0 24 24" width="24">
+         <rect class='button-border' width='24' height='24'/>
+         <path d="M8 19c1.1 0 2-.9 2-2V7c0-1.1-.9-2-2-2s-2 .9-2 2v10c0 1.1.9 2 2 2zm6-12v10c0 1.1.9 2 2 2s2-.9 2-2V7c0-1.1-.9-2-2-2s-2 .9-2 2z"/>
+       </svg>
+       <svg id='icon-next' class='media-button' xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 0 24 24" width="24">
+         <rect class='button-border' width='24' height='24'/>
+         <path d="M7.58 16.89l5.77-4.07c.56-.4.56-1.24 0-1.63L7.58 7.11C6.91 6.65 6 7.12 6 7.93v8.14c0 .81.91 1.28 1.58.82zM16 7v10c0 .55.45 1 1 1s1-.45 1-1V7c0-.55-.45-1-1-1s-1 .45-1 1z"/>
+       </svg>
+       <svg id='icon-prev' class='media-button' xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 0 24 24" width="24">
+         <rect class='button-border' width='24' height='24'/>
+         <path d="M7 6c.55 0 1 .45 1 1v10c0 .55-.45 1-1 1s-1-.45-1-1V7c0-.55.45-1 1-1zm3.66 6.82l5.77 4.07c.66.47 1.58-.01 1.58-.82V7.93c0-.81-.91-1.28-1.58-.82l-5.77 4.07c-.57.4-.57 1.24 0 1.64z"/>
+       </svg>
+       <svg id='icon-nope' class='media-button' xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 0 24 24" width="24">
+         <rect class='button-border' width='24' height='24'/>
+         <path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 18c-4.42 0-8-3.58-8-8 0-1.85.63-3.55 1.69-4.9L16.9 18.31C15.55 19.37 13.85 20 12 20zm6.31-3.1L7.1 5.69C8.45 4.63 10.15 4 12 4c4.42 0 8 3.58 8 8 0 1.85-.63 3.55-1.69 4.9z"/>
+       </svg>
+       <svg id='icon-queue-music' class='media-button' xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 0 24 24" width="24">
+         <rect class='button-border' width='24' height='24'/>
+         <path d="M14 6H4c-.55 0-1 .45-1 1s.45 1 1 1h10c.55 0 1-.45 1-1s-.45-1-1-1zm0 4H4c-.55 0-1 .45-1 1s.45 1 1 1h10c.55 0 1-.45 1-1s-.45-1-1-1zM4 16h6c.55 0 1-.45 1-1s-.45-1-1-1H4c-.55 0-1 .45-1 1s.45 1 1 1zM19 6c-1.1 0-2 .9-2 2v6.18c-.31-.11-.65-.18-1-.18-1.84 0-3.28 1.64-2.95 3.54.21 1.21 1.2 2.2 2.41 2.41 1.9.33 3.54-1.11 3.54-2.95V8h2c.55 0 1-.45 1-1s-.45-1-1-1h-2z"/>
+       </svg>
+       <svg id='icon-playlist-add' class='media-button' xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 0 24 24" width="24">
+         <rect class='button-border' width='24' height='24'/>
+         <path d="M13 10H3c-.55 0-1 .45-1 1s.45 1 1 1h10c.55 0 1-.45 1-1s-.45-1-1-1zm0-4H3c-.55 0-1 .45-1 1s.45 1 1 1h10c.55 0 1-.45 1-1s-.45-1-1-1zm5 8v-3c0-.55-.45-1-1-1s-1 .45-1 1v3h-3c-.55 0-1 .45-1 1s.45 1 1 1h3v3c0 .55.45 1 1 1s1-.45 1-1v-3h3c.55 0 1-.45 1-1s-.45-1-1-1h-3zM3 16h6c.55 0 1-.45 1-1s-.45-1-1-1H3c-.55 0-1 .45-1 1s.45 1 1 1z"/>
+       </svg>
+       <svg id='icon-play' class='media-button' xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 0 24 24" width="24">
+         <rect class='button-border' width='24' height='24'/>
+         <path d="M8 6.82v10.36c0 .79.87 1.27 1.54.84l8.14-5.18c.62-.39.62-1.29 0-1.69L9.54 5.98C8.87 5.55 8 6.03 8 6.82z"/>
+       </svg>
+       <svg id='icon-queue' class='media-button' xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 0 24 24" width="24">
+         <rect class='button-border' width='24' height='24'/>
+         <path d="M3 6c-.55 0-1 .45-1 1v13c0 1.1.9 2 2 2h13c.55 0 1-.45 1-1s-.45-1-1-1H5c-.55 0-1-.45-1-1V7c0-.55-.45-1-1-1zm17-4H8c-1.1 0-2 .9-2 2v12c0 1.1.9 2 2 2h12c1.1 0 2-.9 2-2V4c0-1.1-.9-2-2-2zm-2 9h-3v3c0 .55-.45 1-1 1s-1-.45-1-1v-3h-3c-.55 0-1-.45-1-1s.45-1 1-1h3V6c0-.55.45-1 1-1s1 .45 1 1v3h3c.55 0 1 .45 1 1s-.45 1-1 1z"/>
+       </svg>
+       <svg id=icon-add-to-queue' class='media-button' xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 0 24 24" width="24">
+         <rect class='button-border' width='24' height='24'/>
+         <path d="M21 3H3c-1.1 0-2 .9-2 2v12c0 1.1.9 2 2 2h5v1c0 .55.45 1 1 1h6c.55 0 1-.45 1-1v-1h5c1.1 0 2-.9 2-2V5c0-1.11-.9-2-2-2zm-1 14H4c-.55 0-1-.45-1-1V6c0-.55.45-1 1-1h16c.55 0 1 .45 1 1v10c0 .55-.45 1-1 1zm-4-6c0 .55-.45 1-1 1h-2v2c0 .55-.45 1-1 1s-1-.45-1-1v-2H9c-.55 0-1-.45-1-1s.45-1 1-1h2V8c0-.55.45-1 1-1s1 .45 1 1v2h2c.55 0 1 .45 1 1z"/>
+       </svg>
+       <svg id='icon-remove-from-queue' class='media-button' xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 0 24 24" width="24">
+         <rect class='button-border' width='24' height='24'/>
+         <path d="M21 3H3c-1.1 0-2 .9-2 2v12c0 1.1.9 2 2 2h5v1c0 .55.45 1 1 1h6c.55 0 1-.45 1-1v-1h5c1.1 0 2-.9 2-2V5c0-1.11-.9-2-2-2zm-1 14H4c-.55 0-1-.45-1-1V6c0-.55.45-1 1-1h16c.55 0 1 .45 1 1v10c0 .55-.45 1-1 1zm-4-6c0 .55-.45 1-1 1H9c-.55 0-1-.45-1-1s.45-1 1-1h6c.55 0 1 .45 1 1z"/>
+       </svg>
+       <svg id='icon-shuffle' class='media-button' xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 0 24 24" width="24">
+         <rect class='button-border' width='24' height='24'/>
+         <path d="M10.59 9.17L6.12 4.7c-.39-.39-1.02-.39-1.41 0-.39.39-.39 1.02 0 1.41l4.46 4.46 1.42-1.4zm4.76-4.32l1.19 1.19L4.7 17.88c-.39.39-.39 1.02 0 1.41.39.39 1.02.39 1.41 0L17.96 7.46l1.19 1.19c.31.31.85.09.85-.36V4.5c0-.28-.22-.5-.5-.5h-3.79c-.45 0-.67.54-.36.85zm-.52 8.56l-1.41 1.41 3.13 3.13-1.2 1.2c-.31.31-.09.85.36.85h3.79c.28 0 .5-.22.5-.5v-3.79c0-.45-.54-.67-.85-.35l-1.19 1.19-3.13-3.14z"/>
+       </svg>
+      </defs>
+    </svg>
+
+    <div id='player' class='section'>
+
+      <div id='track-panel'>
+       <div id='track-info' style='display:grid;grid-template-columns:8em 1fr;padding:0.5em 0px;'>
+         <span class='head'>Title</span><span id='track-title'></span>
+         <span class='head'>Artist</span><span id='track-artist'></span>
+         <span class='head'>Path</span><span id='track-path'></span>
+       </div>
+       <div class='progress-bar' style='width:100%;display:grid;grid-template-columns:8em 1fr 8em;'>
+         <span id='track-position' style='text-align:center;'></span>
+         <div id='track-progress' class='progress-indicator'></div>
+         <span id='track-length' style='text-align:centre;'></span>
+       </div>
+      </div>
+      <!-- fully resizable
+          <div id='player-buttons' style='width:100%;display:grid;grid-template-columns:3fr repeat(5,minmax(5em, 1fr)) 3fr;justify-content:center;'>
+          <span></span>
+          <svg query='rew' class='player-button' tabindex='1' width='100%' viewbox='-2 -2 28 28'><use href='#icon-rewind'/></svg>
+          <svg query='pause' class='player-button' tabindex='1' width='100%' viewbox='-2 -2 28 28'><use href='#icon-pause'/></svg>
+          <svg query='fwd' fwd' class='player-button' tabindex='1' width='100%' viewbox='-2 -2 28 28'><use href='#icon-forward'/></svg>
+          <svg query='prev' class='player-button' tabindex='1' width='100%' viewbox='-2 -2 28 28'><use href='#icon-prev'/></svg>
+          <svg query='next' class='player-button' tabindex='1' width='100%' viewbox='-2 -2 28 28'><use href='#icon-next'/></svg>
+          <span></span>
+          </div>
+      -->
+      <div id='player-buttons' style='width:100%;display:grid;grid-template-columns:1fr repeat(5,minmax(3em, 6em)) 1fr;justify-content:center;'>
+       <span></span>
+       <svg query='rew' class='player-button' tabindex='1' width='100%' viewbox='-2 -2 28 28'><use href='#icon-rewind'/></svg>
+       <svg query='pause' class='player-button' tabindex='1' width='100%' viewbox='-2 -2 28 28'><use href='#icon-pause'/></svg>
+       <svg query='fwd' fwd' class='player-button' tabindex='1' width='100%' viewbox='-2 -2 28 28'><use href='#icon-forward'/></svg>
+       <svg query='prev' class='player-button' tabindex='1' width='100%' viewbox='-2 -2 28 28'><use href='#icon-prev'/></svg>
+       <svg query='next' class='player-button' tabindex='1' width='100%' viewbox='-2 -2 28 28'><use href='#icon-next'/></svg>
+       <span></span>
+      </div>
+      <!--
+      <div id='player-buttons' style='width:100%;display:grid;grid-template-columns:1fr auto 1fr;justify-content:center;'>
+       <span></span>
+       <div>
+       <svg query='rew' class='player-button' tabindex='1' width='6em' viewbox='-2 -2 28 28'><use href='#icon-rewind'/></svg>
+       <svg query='pause' class='player-button' tabindex='1' width='6em' viewbox='-2 -2 28 28'><use href='#icon-pause'/></svg>
+       <svg query='fwd' fwd' class='player-button' tabindex='1' width='6em' viewbox='-2 -2 28 28'><use href='#icon-forward'/></svg>
+       <svg query='prev' class='player-button' tabindex='1' width='6em' viewbox='-2 -2 28 28'><use href='#icon-prev'/></svg>
+       <svg query='next' class='player-button' tabindex='1' width='6em' viewbox='-2 -2 28 28'><use href='#icon-next'/></svg>
+       </div>
+       <span></span>
+      </div>
+      -->
+    </div>
+    <div style='padding:1em;'>
+      <input id='new-list-name' type='text'> <input type='button' value='Create Playlist' onClick='playerCreateList(document.getElementById("new-list-name").value)'>
+      <select id='lists'></select>
+    </div>
+    <div class='section'>
+      <table id='coming-up' class='track-list'>
+       <!--
+       <thead>
+         <tr><th class='track'>Track<th class='duration'>Length<th class='action'>Action <select id='next-lists'><option>list a</option><option>list b</option></select></tr>
+       </thead> -->
+       <tbody id='coming-up-list'>
+       </tbody>
+      </table>
+    </div>
+    <div id='search' class='section'>
+      <!-- <input type='text' onchange='playerSearch(this.value)'> -->
+      <table id='search-results' class='track-list'>
+       <caption>Search <input type='text' oninput='playerSearch(this.value)'></caption>
+       <!--
+            <thead>
+            <tr><th class='track'>Track<th class='duration'>Length<th class='action'>Action <select id='search-lists'><option>list a</option><option>list b</option></select></tr>
+            </thead> -->
+       <tbody id='search-results-list'>
+       </tbody>
+      </table>
+    </div>
+  </body>
+</html>