From acb9e22a6ec8b5750a1ba2c4f28cc9b236204b2e Mon Sep 17 00:00:00 2001 From: Not Zed Date: Wed, 9 Jun 2021 10:08:35 +0930 Subject: [PATCH] Checkpoint ongoing mess. Added embedded http player. Ongoing work on playlists. Some search functions. --- Makefile | 100 ++- README | 42 ++ TODO | 34 + analyse.c | 209 ++++++ analyse.h | 39 + blobs.c | 74 ++ dbindex.c | 1836 +++++++++++++++++++++++++++++++++++++++++++++-- dbindex.h | 125 +++- disk-indexer.c | 169 ++++- disk-monitor.c | 16 +- disk-util.c | 203 ++++++ http-monitor.c | 543 ++++++++++++++ input-monitor.c | 7 +- music-player.c | 184 ++++- notify.c | 31 +- notify.h | 7 + player.html | 599 ++++++++++++++++ 17 files changed, 4096 insertions(+), 122 deletions(-) create mode 100644 README create mode 100644 TODO create mode 100644 analyse.c create mode 100644 analyse.h create mode 100644 blobs.c create mode 100644 disk-util.c create mode 100644 http-monitor.c create mode 100644 player.html diff --git a/Makefile b/Makefile index 8afde7f..13cec01 100644 --- 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 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 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 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 + . +*/ + +#include +#include +#include +#include +#include + +#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> 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= 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 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 + . +*/ + +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 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 + . +*/ + +#include +#include + +#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), +}; diff --git a/dbindex.c b/dbindex.c index 8309a6b..76d6596 100644 --- a/dbindex.c +++ b/dbindex.c @@ -16,6 +16,9 @@ along with this program. If not, see . */ + +// TODO: list.size is really the next id, not size if list items are deleted + #include #include #include @@ -25,21 +28,25 @@ #include #include +#include #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;iuuid; + 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;iid + }; + 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%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 #include #include @@ -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 + +// 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(®, 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(®, 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(®); + mdb_txn_abort(tx); + + return i; + +fail: + regfree(®); + 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(®, 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(®, title, 0, NULL, 0) == 0) { + results[i++] = dbfile_get(tx, db, fileid); + next = MDB_NEXT; + } else { + next = MDB_NEXT_NODUP; + } + + } + regfree(®); + mdb_txn_abort(tx); + + return i; + +fail: + regfree(®); + 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); +} diff --git a/dbindex.h b/dbindex.h index a9e2bbf..a87f293 100644 --- a/dbindex.h +++ b/dbindex.h @@ -18,22 +18,33 @@ */ #include "ez-blob.h" +#include 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; // 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" diff --git a/disk-indexer.c b/disk-indexer.c index 40f28d4..ba8c0e3 100644 --- a/disk-indexer.c +++ b/disk-indexer.c @@ -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 + 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;iid, 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; diff --git a/disk-monitor.c b/disk-monitor.c index 0b90c0b..67a4d6a 100644 --- a/disk-monitor.c +++ b/disk-monitor.c @@ -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 index 0000000..765089b --- /dev/null +++ b/disk-util.c @@ -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 + . +*/ + +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +#include + +#include + +#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;iid, 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;iid, 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 index 0000000..e14cf62 --- /dev/null +++ b/http-monitor.c @@ -0,0 +1,543 @@ + +#define _GNU_SOURCE + +#include +#include +#include +#include + +#include +#include +#include + +#include +#include "ez-blob-io.h" +#include "ez-list.h" +#include "ez-tree.h" + +#define obstack_chunk_alloc malloc +#define obstack_chunk_free free +#include +#include +#include +#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 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 = "

It Works!

Quit | Player"; + + 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; +} diff --git a/input-monitor.c b/input-monitor.c index a82c496..745e42e 100644 --- a/input-monitor.c +++ b/input-monitor.c @@ -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); } } diff --git a/music-player.c b/music-player.c index 37ca7f1..0d7fc78 100644 --- a/music-player.c +++ b/music-player.c @@ -34,8 +34,10 @@ 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 : ""); + } + 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 @@ -1255,3 +1396,4 @@ int voice_speak(struct audio_player *ap, const char *text) { return 0; } #endif +#endif /* VOICE_MENU */ diff --git a/notify.c b/notify.c index 8edbf3e..a1daf81 100644 --- a/notify.c +++ b/notify.c @@ -28,28 +28,32 @@ #include #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; diff --git a/notify.h b/notify.h index 58e9bf7..5501218 100644 --- 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 index 0000000..d84788a --- /dev/null +++ b/player.html @@ -0,0 +1,599 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+
+ Title + Artist + Path +
+
+ +
+ +
+
+ +
+ + + + + + + +
+ +
+
+ + +
+
+ + + + +
+
+ + + -- 2.39.2