Added embedded http player.
Ongoing work on playlists.
Some search functions.
+# 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
#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
--- /dev/null
+
+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
--- /dev/null
+
+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
--- /dev/null
+/* analyse.c: Word suffix analysis.
+
+ Copyright (C) 2020 Michael Zucchi
+
+ This program is free software: you can redistribute it and/or
+ modify it under the terms of the GNU General Public License as
+ published by the Free Software Foundation, either version 3 of the
+ License, or (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful, but
+ WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see
+ <http://www.gnu.org/licenses/>.
+*/
+
+#include <wchar.h>
+#include <wctype.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+#include "ez-list.h"
+#include "ez-set.h"
+
+#include "analyse.h"
+
+struct wchar_node {
+ ez_node ln;
+ wchar_t value;
+};
+
+#define NODE(x) { .value = x }
+
+static ez_set collapse_set;
+static struct wchar_node collapse_nodes[] = {
+ // All the quote types
+ NODE(0x0027), //'
+ NODE(0x00AB), //«
+ NODE(0x2039), //‹
+ NODE(0x00BB), //»
+ NODE(0x203A), //›
+ NODE(0x201E), //„
+ NODE(0x201C), //“
+ NODE(0x201F), //‟
+ NODE(0x201D), //”
+ NODE(0x2019), //’
+ NODE(0x0022), //"
+ NODE(0x275D), //❝
+ NODE(0x275E), //❞
+ NODE(0x276E), //❮
+ NODE(0x276F), //❯
+ NODE(0x2E42), //⹂
+ NODE(0x301D), //〝
+ NODE(0x301E), //〞
+ NODE(0x301F), //〟
+ NODE(0xFF02), //"
+ NODE(0x201A), //‚
+ NODE(0x2018), //‘
+ NODE(0x201B), //‛
+ NODE(0x275B), //❛
+ NODE(0x275C), //❜
+ NODE(0x275F), //❟
+};
+
+static unsigned int wchar_hash(const void *n) {
+ //return ez_hash_int32(((struct wchar_node *)n)->value);
+ //return ((struct wchar_node *)n)->value;
+ return ((struct wchar_node *)n)->value * 378684 >> 16;
+}
+
+static int wchar_equals(const void *a, const void *b) {
+ return ((struct wchar_node *)a)->value == ((struct wchar_node *)b)->value;
+}
+
+static void done(void) __attribute__ ((destructor));
+static void done(void) {
+ ez_set_clear(&collapse_set);
+}
+
+static void init(void) __attribute__ ((constructor));
+static void init(void) {
+ ez_set_init(&collapse_set, wchar_hash, wchar_equals, NULL);
+ for (int i=0;i<sizeof(collapse_nodes)/sizeof(collapse_nodes[0]);i++)
+ ez_set_put(&collapse_set, &collapse_nodes[i]);
+#if 0
+ // exhaustive search for perfect hash
+ printf("find best\n");
+ int bestc = 1000;
+ int bestj = 0;
+ int bestk = 0;
+ for (int k=0;k<25;k++) {
+ for (int j=1;j<1000000;j++) {
+ int c = 0;
+ char hits[32] = { 0 };
+
+ for (int i=0;i<sizeof(collapse_nodes)/sizeof(collapse_nodes[0]);i++) {
+ int h = ((collapse_nodes[i].value * j) >> k) & 31;
+ //h = wchar_hash(&collapse_nodes[i]) & 31;
+
+ if (hits[h])
+ c++;
+ hits[h]++;
+ }
+ if (c == 0) {
+ printf("best c=%d j=%d k=%d\n", c, j, k);
+ }
+ if (c < bestc) {
+ bestc = c;
+ bestj = j;
+ bestk = k;
+ printf("best c=%d j=%d k=%d\n", bestc, bestj, bestk);
+ }
+ }
+ }
+ printf("best c=%d j=%d k=%d\n", bestc, bestj, bestk);
+#endif
+}
+
+static int iswcollapse(wchar_t c) {
+ struct wchar_node key = { .value = c };
+
+ return ez_set_get(&collapse_set, &key) != NULL;
+}
+/*
+ Want this stuff pre-defined really
+ */
+
+int analyse_words(ez_list *list, int suffix, const char *words) {
+ size_t len = strlen(words);
+ char word[len+1]; // + ??
+ wchar_t lwords[len+1];
+ int count = 0;
+ const char *t = words;
+ mbstate_t state = { 0 };
+
+ len = mbsrtowcs(lwords, &t, len+1, &state);
+ if (len == (size_t)-1) {
+ fprintf(stderr, "'%s' @ '%s'", words, t);
+ perror(" failed");
+ for (int i=0;i<strlen(words);i++)
+ fprintf(stderr, " %02x", words[i] & 0xff);
+ fprintf(stderr, "\n");
+ return -1;
+ }
+
+ //printf("%ls\n", lwords);
+
+ wchar_t c;
+ wchar_t *p = lwords;
+ wchar_t *w = lwords;
+ do {
+ c = *p++;
+
+ if (iswcollapse(c)) {
+ // nopx
+ } else if (iswgraph(c) && !iswpunct(c)) {
+ *w++ = towlower(c);
+ } else {
+ wchar_t *s = lwords;
+
+ *w = 0;
+
+ // TODO: could keep track of start of each multi-byte char and just write those out
+
+ while (w - s >= 3) {
+ const wchar_t *t = s++;
+ mbstate_t state = { 0 };
+
+ len = wcsrtombs(word, &t, sizeof(word), &state);
+ if (len < sizeof(word)) {
+ struct string_node *string = malloc(sizeof(*string) + len + 1);
+
+ strcpy(string->value, word);
+ ez_list_addtail(list, string);
+ count++;
+
+ if (!suffix)
+ break;
+ } else {
+ fprintf(stderr, "overflow %s\n", words);
+ }
+ }
+ w = lwords;
+ }
+ } while (c);
+
+ return count;
+}
+
+void analyse_free(ez_list *list) {
+ struct string_node *w;
+
+ while ((w = ez_list_remhead(list)))
+ free(w);
+}
+#if 0
+int main(int argc, char **argv) {
+ ez_list list = EZ_INIT_LIST(list);
+
+ analyse_words(&list, 0, "this, is a word? Foo-bar O'callahan");
+ analyse_words(&list, 1, "this, is a word? Foo-bar O'callahan");
+
+ analyse_free(&list);
+}
+#endif
--- /dev/null
+/* analyse.h: Word suffix analysis
+
+ Copyright (C) 2020 Michael Zucchi
+
+ This program is free software: you can redistribute it and/or
+ modify it under the terms of the GNU General Public License as
+ published by the Free Software Foundation, either version 3 of the
+ License, or (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful, but
+ WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see
+ <http://www.gnu.org/licenses/>.
+*/
+
+struct string_node {
+ ez_node ln;
+ char value[0];
+};
+
+/**
+ * Break a string into multiple search tokens optionally
+ * creating all suffixes.
+ *
+ * The basic algorithm is to break strings up on spaces
+ * and puncutation and convert them to lower-case. Some
+ * characters such as quotes are dropped instead.
+ *
+ * @param list output list of struct string_node values
+ * @param suffix if set then expand all suffixes
+ * @param words NUL-terminated string
+ */
+int analyse_words(ez_list *list, int suffix, const char *words);
+
+void analyse_free(ez_list *list);
--- /dev/null
+/* blobs.c: Database and IPC data structures.
+
+ Copyright (C) 2021 Michael Zucchi
+
+ This program is free software: you can redistribute it and/or
+ modify it under the terms of the GNU General Public License as
+ published by the Free Software Foundation, either version 3 of the
+ License, or (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful, but
+ WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see
+ <http://www.gnu.org/licenses/>.
+*/
+
+#include <stdint.h>
+#include <sys/time.h>
+
+#include "dbindex.h"
+#include "notify.h"
+
+#include "ez-blob.h"
+
+ez_blob_desc DBDISK_DESC[] = {
+ EZ_BLOB_START(dbdisk, 1, 4),
+ EZ_BLOB_STRING(dbdisk, 1, uuid),
+ EZ_BLOB_STRING(dbdisk, 2, label),
+ EZ_BLOB_STRING(dbdisk, 3, type),
+ EZ_BLOB_STRING(dbdisk, 4, mount),
+};
+
+ez_blob_desc DBFILE_DESC[] = {
+ EZ_BLOB_START(dbfile, 2, 8),
+ EZ_BLOB_INT32(dbfile, 1, diskid),
+ EZ_BLOB_INT64(dbfile, 2, size),
+ EZ_BLOB_INT64(dbfile, 3, mtime),
+ EZ_BLOB_INT64(dbfile, 4, duration),
+ EZ_BLOB_STRING(dbfile, 5, path),
+ EZ_BLOB_STRING(dbfile, 6, title),
+ EZ_BLOB_STRING(dbfile, 7, artist),
+ EZ_BLOB_TRANSIENTP(dbfile, 8, full_path),
+};
+
+ez_blob_desc DBLIST_DESC[] = {
+ EZ_BLOB_START(dblist, 3, 3),
+ EZ_BLOB_INT32(dblist, 1, size),
+ EZ_BLOB_STRING(dblist, 2, name),
+ EZ_BLOB_STRING(dblist, 3, comment),
+};
+
+ez_blob_desc PLAY_SEEK_DESC[] = {
+ EZ_BLOB_START(struct notify_play_seek, 1, 2),
+ EZ_BLOB_INT32(struct notify_play_seek, 1, mode),
+ EZ_BLOB_FLOAT64(struct notify_play_seek, 2, stamp),
+};
+
+ez_blob_desc DEBUG_DESC[] = {
+ EZ_BLOB_START(struct notify_debug, 2, 1),
+ EZ_BLOB_INT32(struct notify_debug, 1, func),
+};
+
+ez_blob_desc KEY_DESC[] = {
+ EZ_BLOB_START(struct notify_key, 3, 1),
+ EZ_BLOB_INT32(struct notify_key, 1, code),
+};
+
+ez_blob_desc GOTO_DESC[] = {
+ EZ_BLOB_START(struct notify_goto, 4, 1),
+ EZ_BLOB_INT32(struct notify_goto, 1, fileid),
+};
along with this program. If not, see
<http://www.gnu.org/licenses/>.
*/
+
+// TODO: list.size is really the next id, not size if list items are deleted
+
#include <stddef.h>
#include <stdlib.h>
#include <string.h>
#include <assert.h>
#include <lmdb.h>
+#include <errno.h>
#include "dbindex.h"
#include "ez-blob.h"
+#include "ez-blob-basic.h"
+
+// prototype
+void dblist_dump(dbtxn *txn, dbindex *db);
ez_blob_desc DBDISK_DESC[] = {
- EZ_BLOB_START(dbdisk),
+ EZ_BLOB_START(dbdisk, 1, 4),
EZ_BLOB_STRING(dbdisk, 1, uuid),
EZ_BLOB_STRING(dbdisk, 2, label),
EZ_BLOB_STRING(dbdisk, 3, type),
EZ_BLOB_STRING(dbdisk, 4, mount),
- EZ_BLOB_END(dbdisk)
};
ez_blob_desc DBFILE_DESC[] = {
- EZ_BLOB_START(dbfile),
+ EZ_BLOB_START(dbfile, 2, 7),
EZ_BLOB_INT32(dbfile, 1, diskid),
EZ_BLOB_INT64(dbfile, 2, size),
EZ_BLOB_INT64(dbfile, 3, mtime),
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 {
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
// This only works for threads not processes
// single writer I guess.
volatile uint32_t diskid;
+ volatile uint32_t listid;
volatile uint32_t fileid;
};
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);
}
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;
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);
{
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);
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:
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) {
return MDB_NOTFOUND;
*s = *p;
return 0;
+ } else {
+ printf("dbstate get: %s\n", mdb_strerror(db->res));
}
return db->res;
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));
}
/**
- * 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
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));
}
+static uint32_t *scan_all(dbscan *scan, ssize_t *sizep) {
+ int fid;
+ int fids_size = 4096;
+ int count = 0;
+ uint32_t *fids = malloc(sizeof(*fids) * fids_size);
+
+ if (!fids)
+ goto fail;
+
+ while ((fid = dbfile_scan_next(scan)) != ~0) {
+ if (count >= fids_size) {
+ uint32_t *tmp;
+
+ fids_size *= 2;
+ tmp = realloc(fids, sizeof(*fids) * fids_size);
+ if (!tmp)
+ goto fail;
+ fids = tmp;
+ }
+ fids[count++] = fid;
+ }
+
+ *sizep = count;
+ return fids;
+fail:
+ free(fids);
+ *sizep = -1;
+ return NULL;
+}
+
+int dbdisk_del(dbtxn *txn, dbindex *db, dbdisk *disk) {
+ MDB_txn *tx;
+
+ mdb_txn_begin(db->env, txn, 0, &tx);
+
+ dbscan *scan = dbfile_scan_disk(tx, db, disk->id);
+ uint32_t *fids = NULL;
+ ssize_t count;
+ int res = 0;
+
+ fids = scan_all(scan, &count);
+ if (count == -1) {
+ res = ENOMEM;
+ goto fail;
+ }
+
+ for (int i=0;i<count;i++) {
+ printf(" file %d\n", fids[i]);
+ if (res = dbfile_del_id(tx, db, fids[i]))
+ goto fail;
+ }
+
+ // secondary keys
+ MDB_val key;
+
+ // -- by uuid
+ key.mv_data = disk->uuid;
+ key.mv_size = strlen(disk->uuid);
+ if (res = mdb_del(tx, db->disk_by_uuid, &key, NULL))
+ goto fail;
+
+ // Remove disk
+ key.mv_data = &disk->id;
+ key.mv_size = sizeof(disk->id);
+ if (res = mdb_del(tx, db->disk, &key, NULL))
+ goto fail;
+
+ printf("ok\n");
+ free(fids);
+ mdb_txn_commit(tx);
+ //mdb_txn_abort(tx);
+ return 0;
+
+fail:
+ free(fids);
+ mdb_txn_abort(tx);
+ return res;
+}
+
+int dbdisk_del_id(dbtxn *tx, dbindex *db, int diskid) {
+ dbdisk *d = dbdisk_get(tx, db, diskid);
+
+ if (d) {
+ db->res = dbdisk_del(tx, db, d);
+ dbdisk_free(d);
+ }
+
+ return db->res;
+}
+
dbfile *dbfile_get_path(MDB_txn *tx, dbindex *db, int diskid, const char *path) {
char name[strlen(path) + 9];
MDB_val key;
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) {
if (f) {
db->res = dbfile_del(tx, db, f);
dbfile_free(f);
+ } else {
+ printf("no such file: %d\n", fileid);
}
return db->res;
if (res = mdb_del(tx, db->file, &key, NULL))
goto fail;
+ // check lists
+ {
+ MDB_cursor *cursor;
+ size_t alloc = 256;
+ size_t size = 0;
+ struct dblistfile *list = malloc(sizeof(*list) * alloc);
+
+ // FIXME: goto fail leaks list
+ if ((res = mdb_cursor_open(tx, db->list_by_file, &cursor)))
+ goto fail;
+
+ res = mdb_cursor_get(cursor, &key, &data, MDB_SET);
+ printf("set list by file: %d\n", res);
+ while (res == 0) {
+ printf(" list: %d @ %d\n", ((struct dblistfile *)data.mv_data)->listid, ((struct dblistfile *)data.mv_data)->seq);
+ if (size >= alloc) {
+ alloc *= 2;
+ list = realloc(list, sizeof(*list) * alloc);
+ }
+ list[size++] = *(struct dblistfile *)data.mv_data;
+
+ res = mdb_cursor_get(cursor, &key, &data, MDB_NEXT_DUP);
+ }
+ mdb_cursor_close(cursor);
+
+ if (res = mdb_del(tx, db->list_by_file, &key, NULL))
+ goto fail;
+
+ printf("list entries: %zd\n", size);
+
+ for (int i=0;i<size;i++) {
+ struct dbfilelist fdata = {
+ .seq = list[i].seq,
+ .fileid = f->id
+ };
+ printf("delete file %d from list %d @ %d\n", fdata.fileid, list[i].listid, fdata.seq);
+ key.mv_data = &list[i].listid;
+ key.mv_size = sizeof(list[i].listid);
+ data.mv_data = &fdata;
+ data.mv_size = sizeof(fdata);
+ if (res = mdb_del(tx, db->file_by_list, &key, &data))
+ goto fail;
+ }
+ free(list);
+ }
+
mdb_txn_commit(tx);
return res;
fail:
+ printf("del failed: %s\n", mdb_strerror(res));
mdb_txn_abort(tx);
return res;
}
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);
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));
}
-// 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;
mdb_cursor_close(scan->cursor);
free(scan);
}
-
+#endif
/**
* Create a newly shuffled playlist.
dbindex_commit(tx);
}
+// create shuffled playlist
+// TODO: start from an existing playlist?
+void dbshuffle_init2(dbindex *db) {
+ dbtxn *tx;
+ dbscan *scan;
+ uint32_t fid;
+ int fids_size = 4096;
+ int count = 0;
+ int res;
+ uint32_t *fids = malloc(sizeof(*fids) * fids_size);
+
+ // TODO: count? just get it from the thing and scan aagain?
+ // find all current fids
+ mdb_txn_begin(db->env, NULL, 0, &tx);
+
+ scan = dbfile_scan_disk(tx, db, -1);
+ while ((fid = dbfile_scan_next(scan)) != ~0) {
+ if (count >= fids_size) {
+ fids_size *= 2;
+ fids = realloc(fids, sizeof(*fids) * fids_size);
+ }
+ fids[count++] = fid;
+ }
+ printf("total %d\n", count);
+ dbfile_scan_close(scan);
+
+ struct dblist list = {
+ .size = count,
+ .name = "shuffle", // maybe i do want them unique after-all?
+ .comment = ""
+ };
+ struct dbfilelist fvalue;
+ struct dblistfile rvalue;
+ MDB_val fkey, fdata;
+ MDB_val rkey, rdata;
+
+ if ((res = dblist_add(tx, db, &list))) {
+ printf("add list\n");
+ goto fail;
+ }
+
+ printf("list add ok id=%d\n", list.id);
+
+ fkey.mv_data = &list.id;
+ fkey.mv_size = sizeof(uint32_t);
+ fdata.mv_data = &fvalue;
+ fdata.mv_size = sizeof(fvalue);
+
+ rkey.mv_size = sizeof(uint32_t);
+ rdata.mv_data = &rvalue;
+ rdata.mv_size = sizeof(rvalue);
+ rvalue.listid = list.id;
+
+ // TODO: can shuffle have repeats??
+
+ // Playlist instead:
+ // [list] [seq][file]
+ // ... only iterate by seq?
+ // ... how to find playlist by file?
+ // ... or just jump by seq?
+
+ for (int i=0;i<count;i++) {
+ int j = random() % (count-i);
+ uint32_t seq = i + 1;
+
+ fid = fids[i+j];
+ fids[i+j] = fids[i];
+
+ fvalue.seq = seq;
+ fvalue.fileid = fid;
+
+ rvalue.seq = seq;
+ rkey.mv_data = &fid;
+
+ printf(" %d->%d\n", seq, fid);
+
+ if ((res = mdb_put(tx, db->file_by_list, &fkey, &fdata, MDB_NODUPDATA)))
+ goto fail;
+
+ if ((res = mdb_put(tx, db->list_by_file, &rkey, &rdata, MDB_NODUPDATA)))
+ goto fail;
+ }
+ free(fids);
+
+ dbindex_commit(tx);
+ return;
+fail:
+ printf("reason: %s\n", mdb_strerror(res));
+ free(fids);
+ mdb_txn_abort(tx);
+ return;
+}
+
/*
Player support functions
*/
+// A way to iterate through a lit of files, based on an index or something else
+
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
* 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
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;
*/
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);
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);
}
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;
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:
//res = mdb_cursor_get(scan->cursor, &scan->key, &scan->data, MDB_GET_BOTH);
*/
+
+/* playlist management */
+dblist *dblist_get(dbtxn *tx, dbindex *db, int id) {
+ MDB_val key = { .mv_data = &id, .mv_size = sizeof(id) };
+
+ return primary_get_decode(tx, db, DBLIST_DESC, &key, db->list);
+}
+
+void dblist_free(dblist *f) {
+ ez_blob_free(DBLIST_DESC, f);
+}
+
+int dblist_reset(dbtxn *tx, dbindex *db) {
+ mdb_drop(tx, db->list, 0);
+ mdb_drop(tx, db->file_by_list, 0);
+ return mdb_drop(tx, db->list_by_file, 0);
+}
+
+// put ? add ? d->id == 0 -> then add, otherwise put?
+int dblist_add(MDB_txn *txn, dbindex *db, dblist *d) {
+ MDB_txn *tx;
+ MDB_val key, data;
+ int res;
+
+ mdb_txn_begin(db->env, txn, 0, &tx);
+
+ // Store record
+ d->id = list_next_id(db);
+ key.mv_data = &d->id;
+ key.mv_size = sizeof(d->id);
+
+ data.mv_size = ez_basic_size(DBLIST_DESC, d);
+ data.mv_data = NULL;
+ res = mdb_put(tx, db->list, &key, &data, MDB_NOOVERWRITE | MDB_RESERVE);
+ if (res == 0) {
+ ez_basic_encode_raw(DBLIST_DESC, d, (ez_blob *)&data);
+ mdb_txn_commit(tx);
+ } else {
+ printf("db put list fail: %s\n", mdb_strerror(res));
+ mdb_txn_abort(tx);
+ }
+
+ return res;
+}
+
+int dblist_del(dbtxn *txn, dbindex *db, int listid) {
+ MDB_txn *tx;
+ MDB_val key, data;
+ MDB_cursor *cursor;
+ int res;
+
+ mdb_txn_begin(db->env, txn, 0, &tx);
+
+ dblist_dump(tx, db);
+
+ key.mv_data = &listid;
+ key.mv_size = sizeof(listid);
+ if (res = mdb_del(tx, db->list, &key, NULL))
+ goto fail;
+
+ if (res = mdb_del(tx, db->file_by_list, &key, NULL))
+ goto fail;
+
+ res = mdb_cursor_open(tx, db->list_by_file, &cursor);
+
+ printf("delete reverse list\n");
+ res = mdb_cursor_get(cursor, &key, &data, MDB_FIRST);
+ while (res == 0) {
+ struct dblistfile *value = (struct dblistfile *)data.mv_data;
+ printf("check %d %d:%d\n", *(uint32_t*)key.mv_data, value->listid, value->seq);
+
+ if (value->listid == listid) {
+ printf("delete\n");
+ if (res = mdb_cursor_del(cursor, 0))
+ goto fail;
+ res = mdb_cursor_get(cursor, &key, &data, MDB_GET_CURRENT);
+ } else {
+ res = mdb_cursor_get(cursor, &key, &data, MDB_NEXT_DUP);
+ }
+ printf("next dup: %s\n", mdb_strerror(res));
+ if (res == MDB_NOTFOUND) {
+ res = mdb_cursor_get(cursor, &key, &data, MDB_NEXT);
+ printf("next: %s\n", mdb_strerror(res));
+ }
+ }
+
+ mdb_cursor_close(cursor);
+
+ dblist_dump(tx, db);
+ return mdb_txn_commit(tx);
+
+fail:
+ printf("db del list fail: %s\n", mdb_strerror(res));
+ mdb_txn_abort(tx);
+ return -1;
+}
+
+int dblist_add_file(MDB_txn *txn, dbindex *db, dblist *d, int fileid) {
+ MDB_txn *tx;
+ MDB_val key, data;
+ int res;
+ struct dbfilelist fvalue = { .seq = d->size + 1, .fileid = fileid };
+ struct dblistfile rvalue = { .listid = d->id, .seq = d->size + 1 };
+
+ mdb_txn_begin(db->env, txn, 0, &tx);
+
+ // Check file exists
+ key.mv_data = &fileid;
+ key.mv_size = sizeof(fileid);
+ if (mdb_get(tx, db->file, &key, &data) != 0) {
+ printf("FOREIGN: file doesn't exist\n");
+ goto fail;
+ }
+
+ // TODO: foriegn constraint on listid ... or just take list name and look it up
+
+ key.mv_data = &d->id;
+ key.mv_size = sizeof(d->id);
+ data.mv_data = &fvalue;
+ data.mv_size = sizeof(fvalue);
+
+ printf("put file by list: listid = %d { seq = %d fileid = %d }\n", d->id, fvalue.seq, fvalue.fileid);
+
+ if ((res = mdb_put(tx, db->file_by_list, &key, &data, MDB_NOOVERWRITE | MDB_NODUPDATA)))
+ goto fail;
+
+ key.mv_data = &fileid;
+ key.mv_size = sizeof(fileid);
+ data.mv_data = &rvalue;
+ data.mv_size = sizeof(rvalue);
+
+ printf("put list by file: fileid = %d { listid = %d .seq = %d }\n", fileid, rvalue.listid, rvalue.seq);
+
+ if ((res = mdb_put(tx, db->list_by_file, &key, &data, MDB_NOOVERWRITE | MDB_NODUPDATA)))
+ goto fail;
+
+ // update list record with changed size
+ // TODO: can i just poke in the size value?
+ d->size += 1;
+
+ key.mv_data = &d->id;
+ key.mv_size = sizeof(d->id);
+
+ data.mv_size = ez_basic_size(DBLIST_DESC, d);
+ data.mv_data = NULL;
+ res = mdb_put(tx, db->list, &key, &data, MDB_RESERVE);
+ if (res == 0)
+ ez_basic_encode_raw(DBLIST_DESC, d, (ez_blob *)&data);
+ printf("update seq %d\n", d->size);
+
+ if (res == 0)
+ return mdb_txn_commit(tx);
+fail:
+ printf("fail: %s\n", mdb_strerror(res));
+ mdb_txn_abort(tx);
+ return res;
+}
+
+void dblist_dump(dbtxn *tx, dbindex *db) {
+ //MDB_txn *tx;
+ MDB_val key, data;
+ MDB_cursor *cursor;
+ int res;
+
+ //if (res = mdb_txn_begin(db->env, txn, MDB_RDONLY, &tx)) {
+ // printf("failed: %s\n", mdb_strerror(res));
+ // return;
+ //}
+
+
+ printf("dump all lists\n");
+ printf("list_by_file =\n");
+ res = mdb_cursor_open(tx, db->list_by_file, &cursor);
+ res = mdb_cursor_get(cursor, &key, &data, MDB_FIRST);
+ while (res == 0) {
+ struct dblistfile *value = data.mv_data;
+ uint32_t fid = *(uint32_t *)key.mv_data;
+
+ printf(" file %d list %d seq %d\n", fid, value->listid, value->seq);
+
+ res = mdb_cursor_get(cursor, &key, &data, MDB_NEXT_DUP);
+ if (res == MDB_NOTFOUND)
+ res = mdb_cursor_get(cursor, &key, &data, MDB_NEXT);
+ }
+ mdb_cursor_close(cursor);
+
+ printf("file_by_list =\n");
+ res = mdb_cursor_open(tx, db->file_by_list, &cursor);
+ res = mdb_cursor_get(cursor, &key, &data, MDB_FIRST);
+ while (res == 0) {
+ struct dbfilelist *value = data.mv_data;
+ uint32_t lid = *(uint32_t *)key.mv_data;
+
+ printf(" list %d file %d seq %d\n", lid, value->fileid, value->seq);
+
+ res = mdb_cursor_get(cursor, &key, &data, MDB_NEXT_DUP);
+ if (res == MDB_NOTFOUND)
+ res = mdb_cursor_get(cursor, &key, &data, MDB_NEXT);
+ }
+ mdb_cursor_close(cursor);
+
+
+ //mdb_txn_commit(tx);
+}
+
+// only list + seq is required
+int dblist_del_file(MDB_txn *txn, dbindex *db, struct dblistcursor *list) {
+ MDB_txn *tx;
+ MDB_val key, data;
+ MDB_cursor *cursor;
+ int res;
+ struct dbfilelist fvalue = { .seq = list->seq, .fileid = list->fileid };
+ struct dblistfile rvalue = { .listid = list->listid, .seq = list->seq };
+
+ dblist_dump(txn, db);
+
+ key.mv_data = &list->listid;
+ key.mv_size = sizeof(list->listid);
+ if (res = mdb_get(txn, db->list, &key, &data))
+ goto fail0;
+
+ // find list:seq from list_by_file
+ // ...
+
+ printf("delete @ %d from list %d\n", list->seq, list->listid);
+
+ if (res = mdb_txn_begin(db->env, txn, 0, &tx))
+ goto fail0;
+
+ // Delete forward and reverse based on all parameters
+
+ key.mv_data = &list->listid;
+ key.mv_size = sizeof(list->listid);
+ data.mv_data = &fvalue;
+ data.mv_size = sizeof(fvalue);
+
+ if (res = mdb_cursor_open(tx, db->file_by_list, &cursor))
+ goto fail1;
+
+ printf(" lookup listid %d seq %d\n", list->listid, fvalue.seq);
+ if (res = mdb_cursor_get(cursor, &key, &data, MDB_GET_BOTH))
+ goto fail;
+
+ printf("file found %d list %d seq %d\n", ((struct dbfilelist *)data.mv_data)->fileid, *(int*)key.mv_data, ((struct dbfilelist *)data.mv_data)->seq);
+
+ printf(" delete file by list\n");
+
+ uint32_t fid = ((struct dbfilelist *)data.mv_data)->fileid;
+
+ if (res = mdb_del(tx, db->file_by_list, &key, &data))
+ goto fail;
+
+ key.mv_data = &fid;
+ key.mv_size = sizeof(fid);
+ data.mv_data = &rvalue;
+ data.mv_size = sizeof(rvalue);
+
+ printf(" delete list by file file=%d list=%d seq=%d\n", fid, rvalue.listid, rvalue.seq);
+ if (res = mdb_del(tx, db->list_by_file, &key, &data))
+ goto fail;
+
+ mdb_cursor_close(cursor);
+
+ dblist_dump(tx, db);
+ mdb_txn_commit(tx);
+ return 0;
+
+fail:
+ printf("fail: %s\n", mdb_strerror(res));
+ mdb_cursor_close(cursor);
+fail1:
+ mdb_txn_abort(tx);
+fail0:
+ return res;
+}
+
+#if 0
+int dblist_iterate(dbtxn *tx, dbindex *db, struct dblistcursor *pos) {
+ //MDB_txn *tx;
+ MDB_val key, data;
+ MDB_cursor *cursor;
+ int res;
+
+ ez_blob_free_raw(DBFILE_DESC, &pos->file);
+ memset(&pos->file, 0, sizeof(pos->file));
+
+ //printf("iterate list %d\n", pos->listid);
+
+ //if (res = mdb_txn_begin(db->env, txn, 0, &tx))
+ // goto fail1;
+
+ //dblist_dump(tx, db);
+
+ if (pos->listid == 0) {
+ // just go by fileid order
+ if ((res = mdb_cursor_open(tx, db->file, &cursor)))
+ goto fail0;
+
+ key.mv_data = &pos->fileid;
+ key.mv_size = sizeof(pos->fileid);
+
+ if (res = mdb_cursor_get(cursor, &key, &data, MDB_SET_RANGE))
+ goto fail;
+ if (res = mdb_cursor_get(cursor, &key, &data, MDB_NEXT))
+ goto fail;
+
+ dbfile_decode_raw((ez_blob *)&data, &pos->file);
+ pos->fileid = pos->file.id = *(uint32_t*)key.mv_data;
+ } else {
+ // go by current info
+ if ((res = mdb_cursor_open(tx, db->file_by_list, &cursor)))
+ goto fail0;
+
+ struct dbfilelist thing = { .seq = pos->seq + 1, .fileid = 0 };
+
+ //printf(" from seq %d file %d\n", thing.seq, thing.fileid);
+
+ key.mv_data = &pos->listid;
+ key.mv_size = sizeof(pos->listid);
+ data.mv_data = &thing;
+ data.mv_size = sizeof(thing);
+
+ if (res = mdb_cursor_get(cursor, &key, &data, MDB_GET_BOTH_RANGE))
+ goto fail;
+
+ struct dbfilelist *value = data.mv_data;
+
+ pos->seq = value->seq;
+ pos->fileid = value->fileid;
+
+ key.mv_data = &pos->fileid;
+ key.mv_size = sizeof(pos->fileid);
+
+ if (res = mdb_get(tx, db->file, &key, &data))
+ goto fail;
+
+ dbfile_decode_raw((ez_blob *)&data, &pos->file);
+ pos->file.id = pos->fileid;
+ }
+
+ mdb_cursor_close(cursor);
+ //mdb_txn_commit(tx);
+
+ return 0;
+fail:
+ mdb_cursor_close(cursor);
+fail0:
+ //mdb_txn_abort(tx);
+//fail1:
+ printf("fail: %s\n", mdb_strerror(res));
+ return res;
+}
+#endif
+#if 0
+static int dbfile_iterate_list(dbindex *db, int listid, dbfile **fp, int dir) {
+ MDB_txn *tx;
+ MDB_val key, data;
+ MDB_cursor *cursor;
+ dbfile *file = NULL;
+ int res;
+
+ mdb_txn_begin(db->env, NULL, MDB_RDONLY, &tx);
+
+ if ((res = mdb_cursor_open(tx, db->file_by_list, &cursor)))
+ goto fail;
+
+ int keyval = *fp ? ((*fp)->id) : -1;
+ dbdisk *disk = *fp ? dbdisk_get(tx, db, (*fp)->diskid) : NULL;
+ int mounted = *fp ? dbdisk_mounted(disk) : 0;
+
+ //printf("\nlist iterate: fid=%d\n", keyval);
+
+ dbfile_free(*fp);
+ *fp = NULL;
+
+ /*
+ This mess is to handle various cases:
+ - first call
+ - next from file
+ */
+
+ if (keyval != -1) {
+ MDB_cursor *rcursor;
+
+ // find current sequence number via GET_BOTH lookup
+ res = mdb_cursor_open(tx, db->list_by_file, &rcursor);
+
+ struct dblistfile rval = { .listid = listid };
+
+ key.mv_data = &keyval;
+ key.mv_size = sizeof(keyval);
+
+ data.mv_data = &rval;
+ data.mv_size = sizeof(rval);
+
+ //rval.seq = keyval - 1;
+
+ //printf("seek : fileid=%d { listid=%d seq=%d }\n", keyval, rval.listid, rval.seq);
+
+ res = mdb_cursor_get(rcursor, &key, &data, MDB_GET_BOTH_RANGE);
+ if (res != 0) {
+ //printf("seek failed = %s\n", mdb_strerror(res));
+ mdb_cursor_close(rcursor);
+ goto fail;
+ }
+ //printf("position: fileid=%d { listid=%d seq=%d }\n", keyval, rval.listid, rval.seq);
+
+ struct dblistfile *dval = data.mv_data;
+ struct dbfilelist fval = { .seq = dval->seq };
+
+ key.mv_data = &listid;
+ key.mv_size = sizeof(listid);
+
+ data.mv_data = &fval;
+ data.mv_size = sizeof(fval);
+
+ //printf("seek : listid=%d { seq = %d fileid=%d }\n", listid, fval.seq, fval.fileid);
+ res = mdb_cursor_get(cursor, &key, &data, MDB_GET_BOTH);
+ //printf("seek: %s\n", mdb_strerror(res));
+ //printf("position: listid=%d { seq = %d fileid=%d }\n", listid, fval.seq, fval.fileid);
+ res = mdb_cursor_get(cursor, &key, &data, dir == 0 ? MDB_NEXT_DUP : MDB_PREV_DUP);
+ //printf("next: %s\n", mdb_strerror(res));
+ //printf("position: listid=%d { seq = %d fileid=%d }\n", listid, fval.seq, fval.fileid);
+
+ mdb_cursor_close(rcursor);
+ } else {
+ key.mv_data = &listid;
+ key.mv_size = sizeof(listid);
+ res = mdb_cursor_get(cursor, &key, &data, MDB_SET);
+ if (res == 0 && dir == 1)
+ res = mdb_cursor_get(cursor, &key, &data, MDB_LAST_DUP);
+
+ //struct dbfilelist *fval = data.mv_data;
+ //printf("first : listid=%d { seq = %d fileid=%d }\n", listid, fval->seq, fval->fileid);
+ }
+
+ while (file == NULL && res == 0) {
+ struct dbfilelist *dval = data.mv_data;
+ MDB_val pkey = { sizeof(int), &dval->fileid };
+
+ file = primary_get_decode(tx, db, DBFILE_DESC, &pkey, db->file);
+ if (file) {
+ int keep;
+
+ //printf("loaded: %d[%d]\n", dval->fileid, dval->seq);
+
+ if (disk == NULL || file->diskid != disk->id) {
+ dbdisk_free(disk);
+ disk = dbdisk_get(tx, db, file->diskid);
+ mounted = dbdisk_mounted(disk);
+ }
+ keep = mounted;
+ keep = keep && file->duration > 0;
+ if (keep) {
+ char path[strlen(disk->mount) + strlen(file->path) + 1];
+ struct stat st;
+
+ sprintf(path, "%s%s", disk->mount, file->path);
+
+ //printf("check %s\n", path);
+
+ keep = lstat(path, &st) == 0 && S_ISREG(st.st_mode);
+ if (keep) {
+ file->full_path = strdup(path);
+ }
+ }
+
+ if (!keep) {
+ dbfile_free(file);
+ file = NULL;
+ }
+ }
+ if (file == NULL)
+ res = mdb_cursor_get(cursor, &key, &data, dir == 0 ? MDB_NEXT_DUP : MDB_PREV_DUP);
+ }
+
+ //free(keyval);
+ dbdisk_free(disk);
+
+ mdb_cursor_close(cursor);
+ mdb_txn_commit(tx);
+
+ *fp = file;
+
+ return res;
+ fail:
+ // close cursor?
+ mdb_txn_abort(tx);
+
+ return res;
+}
+
+int dbfile_next_list(dbindex *db, int listid, dbfile **fp) {
+ return dbfile_iterate_list(db, listid, fp, 0);
+}
+
+int dbfile_prev_list(dbindex *db, int listid, dbfile **fp) {
+ return dbfile_iterate_list(db, listid, fp, 1);
+}
+#endif
+
+#include <regex.h>
+
+// TODO: should run over title index instead?
+int dbfile_searchx(dbindex *db, const char *pattern, dbfile **results, int maxlen) {
+ MDB_txn *tx;
+ MDB_val key, data;
+ MDB_cursor *cursor;
+ int res;
+ regex_t reg;
+
+ printf("search, pattern='%s'\n", pattern);
+ res = regcomp(®, 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);
+}
*/
#include "ez-blob.h"
+#include <lmdb.h>
typedef struct dbdisk dbdisk;
+typedef uint32_t dbid_t;
struct dbdisk {
- int id;
+ dbid_t id;
char *uuid;
char *label;
char *type;
char *mount; // last mount point
};
+typedef struct dblist dblist;
+
+struct dblist {
+ dbid_t id;
+ int size;
+ char *name;
+ char *comment;
+};
+
typedef struct dbfile dbfile;
struct dbfile {
- int id;
- int diskid; // disk it belongs to
+ dbid_t id;
+ dbid_t diskid; // disk it belongs to
uint64_t size; // st_size
uint64_t mtime; // st_mtime
char *title; // music title
char *artist; // music artist
+
+ char *full_path; // <transient> full path including disk name, depends on api
};
/* player state, this is passed around as a struct */
typedef struct dbstate dbstate;
+// FIXME: add playlist id
+// FIXME: add playlist seq (and file)
+
struct dbstate {
- uint32_t size; // don't need to initialise
+ uint32_t size; // don't need to initialise
uint32_t state; // some info on playing
- uint32_t fileid; // file being played
- uint64_t pos; // last approximate position
+
+ dbid_t listid; // list, if any
+ uint32_t seq; // list position, if any
+ dbid_t fileid; // file being played
+
+ uint64_t pos; // last approximate position pts
+ double poss; // last approximate position in seconds
time_t stamp; // last update time
};
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);
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);
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"
#include "dbindex.h"
#include "notify.h"
+#include "analyse.h"
struct indexer {
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"));
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;
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");
dbindex_close(db);
}
+#if 1
+
+
+/*
+ (naive) idea for full text sub-string search inside lmdb
+ just build full suffix tables
+
+ [suffix] [all members]
+
+ sub-string search is:
+ suffix >= query and suffix[0 .. query.length] == query
+
+ */
+#if 0
+void build_suffix(dbtxn *tx, dbindex *db, uint32_t fileid, const char *words) {
+ int state = 0;
+ size_t len = strlen(words);
+ char word[len+1]; // + ??
+ wchar_t lwords[len+1];
+
+ /* convert to wide char astring */
+ size_t res;
+
+ len = mbrtowcs(lwords, words, len, NULL);
+ if (len == (size_t)-1)
+ return;
+
+
+
+ //printf("words: %s\n", words);
+
+ /*
+ Basic idea:
+ Break string into words.
+ Strip puncutation.
+ lower-scase
+ Build suffix tables (in-memory db?)
+
+ we want ' included though, translate other 's into '? or remove all?
+ */
+
+ int high = 0;
+
+ wchar_t c;
+ wchar_t *p = lwords;
+ wchar_t *s = p;
+ do {
+ c = *p;
+
+ if (c == 0 || !iswgraph(c) || iswpunct(c)) {
+ *p = 0;
+ while (p - s >= 3) {
+ wchar_t *t = s++;
+
+ len = wcstombs(word, &t, sizeof(word), NULL);
+ if (len < sizeof(word)) {
+ dbfile_put_suffix(tx, db, word, fileid);
+ } else {
+ fprintf(stderr, "overflow %s\n", words);
+ }
+ }
+ s = ++p;
+ } else {
+ *p++ = towlower(c);
+ }
+ } while (c);
+
+}
+#endif
+
+int dbfile_clear_suffix(dbtxn *tx, dbindex *db);
+
+void suffix(const char *path) {
+ dbindex *db = dbindex_open(path);
+ dbtxn *tx = dbindex_begin(db, NULL, 0);
+
+ dbfile_clear_suffix(tx, db);
+
+ dbscan *scan = dbfile_scan_disk(tx, db, -1);
+ uint32_t fid;
+ ez_list list = EZ_INIT_LIST(list);
+
+ while ((fid = dbfile_scan_next(scan)) != ~0) {
+ dbfile *f = dbfile_get(tx, db, fid);
+ struct string_node *w;
+
+ //printf("%s\n", f->title);
+ analyse_words(&list, 1, f->title);
+ while ((w = ez_list_remhead(&list))) {
+ //printf(" %s\n", w->value);
+ dbfile_put_suffix(tx, db, w->value, f->id);
+ free(w);
+ }
+
+ dbfile_free(f);
+ }
+
+ dbfile_scan_close(scan);
+ dbindex_commit(tx);
+ dbindex_close(db);
+}
+
+void search_suffix(const char *path) {
+ dbindex *db = dbindex_open(path);
+ if (1) {
+ dbtxn *tx = dbindex_begin(db, NULL, 0);
+
+ dbfile_search_substring(tx, db, "union");
+ dbindex_abort(tx);
+ } else {
+ dbfile *matches[50];
+ int len = dbfile_search(db, "cnic", matches, 50);
+ printf("matches: %d\n", len);
+ }
+ dbindex_close(db);
+}
+#endif
+
+#include <locale.h>
+
int main(int argc, char **argv) {
av_log_set_level(AV_LOG_ERROR);
//avcodec_register_all();
//av_register_all();
//avformat_network_init();
+ const char *dbdir = MAIN_INDEX;
+
+ setlocale(LC_ALL, "en_AU.UTF-8");
+
+ if (argc > 2 && strcmp(argv[1], "-d") == 0) {
+ dbdir = argv[2];
+ argv += 2;
+ argc -= 2;
+ }
+
+ mkdir(dbdir, 0700);
+
+ if (1) {
+ //suffix(dbdir);
+ search_suffix(dbdir);
+ return 0;
+ }
+
+
+#if 0
+ {
+ int dbfile_searchx(dbindex *db, const char *pattern, dbfile **results, int maxlen);
+ dbindex *db = dbindex_open(MAIN_INDEX);
+ dbfile *list[150];
+ int len = dbfile_searchx(db, "deep sessions", list, 150);
+
+ for (int i=0;i<len;i++) {
+ printf(" %8d %8d %s %s\n", list[i]->id, list[i]->diskid, list[i]->title, list[i]->path);
+ }
- mkdir(MAIN_INDEX, 0700);
+ dbindex_close(db);
+ return 0;
+ }
+#endif
if (argc > 1) {
if (strcmp(argv[1], "check") == 0)
- check();
+ check(dbdir);
else {
notify_t q = notify_writer_new(NOTIFY_INDEXER);
}
}
} else
- indexer();
+ indexer(dbdir);
return 0;
#include "notify.h"
struct monitor {
- ez_set *mounts;
+ ez_set mounts;
size_t mount_base_size;
char *mount_base;
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);
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);
//
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);
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);
//
notify_close(m->indexer);
free(m->mount_base);
- ez_set_free(m->mounts);
+ ez_set_clear(&m->mounts);
close(s);
}
--- /dev/null
+/* disk-util.c: utilities for managing indices.
+
+ Copyright (C) 2021 Michael Zucchi
+
+ This program is free software: you can redistribute it and/or
+ modify it under the terms of the GNU General Public License as
+ published by the Free Software Foundation, either version 3 of the
+ License, or (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful, but
+ WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see
+ <http://www.gnu.org/licenses/>.
+*/
+
+#include <sys/types.h>
+#include <sys/stat.h>
+#include <sys/time.h>
+#include <unistd.h>
+#include <dirent.h>
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <locale.h>
+
+#include <libavformat/avformat.h>
+
+#include <regex.h>
+
+#include "ez-list.h"
+#include "ez-set.h"
+#include "ez-bitset.h"
+
+#include "dbindex.h"
+
+void dbshuffle_init2(dbindex *db);
+int dbdisk_del_id(dbtxn *txn, dbindex *db, int diskid);
+void dbindex_validate(dbindex *db);
+
+int dblist_reset(dbtxn *tx, dbindex *db);
+int dblist_del_file(dbtxn *txn, dbindex *db, struct dblistcursor *list);
+
+int main(int argc, char **argv) {
+ const char *dbdir = MAIN_INDEX;
+ int fileid = 0, diskid = 0, listid = 0;
+ int seq = 0;
+
+ setlocale(LC_ALL, "en_AU.UTF-8");
+
+ if (argc > 2 && strcmp(argv[1], "-d") == 0) {
+ dbdir = argv[2];
+ argv += 2;
+ argc -= 2;
+ }
+
+ mkdir(dbdir, 0700);
+
+ dbindex *db = dbindex_open(dbdir);
+
+ for (int i=1;i<argc;i++) {
+ const char *cmd = argv[i];
+
+ if (strcmp(cmd, "-f") == 0) {
+ fileid = atoi(argv[++i]);
+ } else if (strcmp(cmd, "-s") == 0) {
+ seq = atoi(argv[++i]);
+ } else if (strcmp(cmd, "-d") == 0) {
+ diskid = atoi(argv[++i]);
+ } else if (strcmp(cmd, "--shuffle") == 0) {
+ dbshuffle_init2(db);
+ } else if (strcmp(cmd, "--file-dump") == 0) {
+ // dump file info
+ // dump lists it's in
+ } else if (strcmp(cmd, "--files") == 0) {
+ dbtxn *tx = dbindex_begin(db, NULL, 1);
+ dbscan scan;
+
+ for (dbfile *file = dbscan_file(tx, &scan, db, fileid); file; file = dbscan_file_next(&scan)) {
+ printf("%4d %-60s '%s'\n", file->id, file->title, file->path);
+ dbfile_free(file);
+ }
+ dbscan_free(&scan);
+ dbindex_abort(tx);
+ } else if (strcmp(cmd, "--file-del") == 0) {
+ dbtxn *tx = dbindex_begin(db, NULL, 0);
+ int fid = atoi(argv[++i]);
+
+ if (dbfile_del_id(tx, db, fid) == 0)
+ dbindex_commit(tx);
+ else
+ dbindex_abort(tx);
+ } else if (strcmp(cmd, "--lists") == 0) {
+ dbtxn *tx = dbindex_begin(db, NULL, 1);
+ dbscan scan;
+
+ for (dblist *list = dbscan_list(tx, &scan, db, 0); list; list = dbscan_list_next(&scan)) {
+ printf("id: %d\n", list->id);
+ printf("list: %s\n", list->name);
+ printf("size: %d\n", list->size);
+ printf("\n");
+ dblist_free(list);
+ }
+ dbscan_free(&scan);
+ dbindex_abort(tx);
+ } else if (strcmp(cmd, "--lists-reset") == 0) {
+ dbtxn *tx = dbindex_begin(db, NULL, 0);
+
+ dblist_reset(tx, db);
+ dbindex_commit(tx);
+ } else if (strcmp(cmd, "--list-add") == 0) {
+ dblist list = {
+ .name = argv[++i]
+ };
+
+ dblist_add(NULL, db, &list);
+ } else if (strcmp(cmd, "--list-del") == 0) {
+ int lid = atoi(argv[++i]);
+
+ dblist_del(NULL, db, lid);
+ } else if (strcmp(cmd, "--list-add-file") == 0) {
+ int lid = atoi(argv[++i]);
+ int fid = atoi(argv[++i]);
+ dbtxn *tx = dbindex_begin(db, NULL, 0);
+ dblist *list = dblist_get(tx, db, lid);
+
+ if (dblist_add_file(tx, db, list, fid) == 0)
+ dbindex_commit(tx);
+ else
+ dbindex_abort(tx);
+ } else if (strcmp(cmd, "--list-del-file") == 0) {
+ int lid = atoi(argv[++i]);
+ int seq = atoi(argv[++i]);
+ //int fid = atoi(argv[++i]);
+ dbtxn *tx = dbindex_begin(db, NULL, 0);
+ struct dblistcursor list = {
+ .listid = lid,
+ .seq = seq,
+ //.fileid = fid
+ };
+
+ if (dblist_del_file(tx, db, &list) == 0)
+ dbindex_commit(tx);
+ else
+ dbindex_abort(tx);
+ } else if (strcmp(cmd, "--list-dump") == 0) {
+ int lid = atoi(argv[++i]);
+ dbtxn *tx = dbindex_begin(db, NULL, 1);
+ dbscan scan;
+
+ for (dbfile *file = dbscan_list_entry(tx, &scan, db, lid, seq, fileid); file; file = dbscan_list_entry_next(&scan)) {
+ printf("%4d seq=%4d title: %-60s %s\n", file->id, dbscan_list_entry_seq(&scan), file->title, file->path);
+ dbfile_free(file);
+ }
+ dbscan_free(&scan);
+ dbindex_abort(tx);
+ } else if (strcmp(cmd, "--disks") == 0) {
+ dbtxn *tx = dbindex_begin(db, NULL, 1);
+ dbscan scan;
+
+ for (dbdisk *disk = dbscan_disk(tx, &scan, db, 0); disk; disk = dbscan_disk_next(&scan)) {
+ printf("id: %d\n", disk->id);
+ printf("uuid: %s\n", disk->uuid);
+ printf("label: %s\n", disk->label);
+ printf("type: %s\n", disk->type);
+ printf("mount: %s\n", disk->mount);
+ printf("\n");
+ dbdisk_free(disk);
+ }
+ dbscan_free(&scan);
+ dbindex_abort(tx);
+ } else if (strcmp(cmd, "--disk-del") == 0) {
+ dbtxn *tx = dbindex_begin(db, NULL, 0);
+ int diskid = atoi(argv[++i]);
+
+ dbdisk_del_id(tx, db, diskid);
+ dbindex_commit(tx);
+ } else if (strcmp(cmd, "--validate") == 0) {
+ dbindex_validate(db);
+ } else if (strcmp(cmd, "--search") == 0) {
+ const char *match = argv[++i];
+ dbfile *results[100];
+ int res = dbfile_search(db, match, results, 100);
+
+ if (res >= 0) {
+ for (int i=0;i<res;i++) {
+ printf("%4d title: %s\n", results[i]->id, results[i]->title);
+ dbfile_free(results[i]);
+ }
+ } else {
+ printf("search failed\n");
+ }
+ }
+ }
+
+ dbindex_close(db);
+
+ return 0;
+}
--- /dev/null
+
+#define _GNU_SOURCE
+
+#include <unistd.h>
+#include <sys/types.h>
+#include <sys/stat.h>
+#include <fcntl.h>
+
+#include <stdlib.h>
+#include <string.h>
+#include <netdb.h>
+
+#include <stdlib.h>
+#include "ez-blob-io.h"
+#include "ez-list.h"
+#include "ez-tree.h"
+
+#define obstack_chunk_alloc malloc
+#define obstack_chunk_free free
+#include <obstack.h>
+#include <stdio.h>
+#include <errno.h>
+#define D(x)
+
+#include "ez-http.h"
+
+#include "dbindex.h"
+#include "notify.h"
+
+#include "player.h"
+
+static dbindex *db;
+static notify_t player;
+
+static struct ez_pair ct_text_html = {
+ .name = "Content-Type",
+ .value = "text/html;charset=utf-8"
+};
+static struct ez_pair ct_text_xml = {
+ .name = "Content-Type",
+ .value = "text/xml"
+};
+static struct ez_pair ct_application_json = {
+ .name = "Content-Type",
+ .value = "application/json"
+};
+static struct ez_pair ce_gzip = {
+ .name = "Content-Encoding",
+ .value = "gzip"
+};
+
+static void obstack_sgrow(struct obstack *os, const char *value) {
+ obstack_grow(os, value, strlen(value));
+}
+
+static int blobio_load(struct ez_blobio *io, const char *path, struct obstack *os) {
+ struct stat st;
+ int res;
+
+ res = stat(path, &st);
+ if (res == 0) {
+ io->data = obstack_alloc(os, st.st_size);
+ if (io->data) {
+ int fd = open(path, O_RDONLY);
+ if (fd != -1) {
+ res = read(fd, io->data, st.st_size);
+ close(fd);
+ if (res == st.st_size) {
+ io->alloc = st.st_size;
+ io->size = st.st_size;
+ io->index = 0;
+ io->mode = BLOBIO_READ;
+ res = 0;
+ } else
+ res = -1;
+ }
+ } else
+ res = -1;
+ }
+
+ return res;
+}
+
+static struct ez_pair *find_param(struct ez_httprequest *r, const char *name) {
+ for (struct ez_pair *w = ez_list_head(&r->params), *n = ez_node_succ(w);n;w=n,n = ez_node_succ(w)) {
+ if (strcasecmp(w->name, name) == 0)
+ return w;
+ }
+ return NULL;
+}
+
+static int param_int(struct ez_httprequest *r, const char *name, int def) {
+ struct ez_pair *h = find_param(r, name);
+
+ if (h && h->value)
+ return atoi(h->value);
+
+ return def;
+}
+
+static double param_float(struct ez_httprequest *r, const char *name, double def) {
+ struct ez_pair *h = find_param(r, name);
+
+ if (h && h->value)
+ return atof(h->value);
+
+ return def;
+}
+
+static const char *param_value(struct ez_httprequest *r, const char *name, const char *def) {
+ struct ez_pair *h = find_param(r, name);
+
+ if (h && h->value)
+ return (h->value);
+
+ return def;
+}
+
+static void duration_value(struct obstack *os, uint64_t ms) {
+ uint64_t s = (ms / 1000) % 60;
+ uint64_t m = (ms / (1000 * 60)) % 60;
+ uint64_t h = ms / (1000 * 60 * 60);
+
+ obstack_printf(os, "\"%02d:%02d:%02d\"", (int)h, (int)m, (int)s);
+}
+
+static void write_file_json(struct obstack *os, dbfile *file) {
+ obstack_printf(os, "\"id\":\"%d\"", file->id);
+
+ if (file->title)
+ obstack_printf(os, ",\"title\":\"%s\"", file->title);
+ if (file->artist)
+ obstack_printf(os, ",\"artist\":\"%s\"", file->artist);
+
+ obstack_sgrow(os, ",\"length\":");
+ duration_value(os, file->duration / 1000);
+ obstack_printf(os, ",\"lengthms\":\"%zd\"", file->duration / 1000);
+}
+
+static void write_state_json(dbindex *db, struct obstack *os) {
+ dbtxn *tx = dbindex_begin(db, NULL, 1);
+ dbstate state;
+ int res = dbstate_get(tx, db, &state);
+ dbfile *file = NULL;
+
+ printf("state.fileid = %d\n", state.fileid);
+ printf("state.pos = %zd\n", state.pos);
+ printf("state.listid = %d\n", state.listid);
+ printf("state.seq = %d\n", state.seq);
+
+ if (res == 0
+ && (state.state & 1)
+ && (file = dbfile_get(tx, db, state.fileid))) {
+ obstack_1grow(os, '{');
+ write_file_json(os, file);
+
+ obstack_printf(os, ",\"path\":\"%s\"", file->path);
+ obstack_printf(os, ",\"status\":\"%s\"", (state.state & 2) ? "paused" : "playing");
+
+ obstack_printf(os, ",\"listid\":\"%u\"", state.listid);
+ obstack_printf(os, ",\"seq\":\"%u\"", state.seq);
+ obstack_printf(os, ",\"fileid\":\"%u\"", state.fileid);
+
+ obstack_sgrow(os, ",\"position\":");
+ duration_value(os, (uint64_t)(state.poss * 1000));
+ obstack_printf(os, ",\"positionms\":\"%.0f\"", (state.poss * 1000));
+ obstack_1grow(os, '}');
+ } else {
+ obstack_sgrow(os, "{ 'status': 'idle' }");
+ }
+
+ dbindex_abort(tx);
+
+ dbfile_free(file);
+}
+
+static int write_lists_json(dbindex *db, struct obstack *io) {
+ dbtxn *tx = dbindex_begin(db, NULL, 1);
+ dbscan scan;
+ int first = 1;
+
+ obstack_1grow(io, '{');
+ obstack_sgrow(io, "\"status\":\"ok\"");
+ obstack_sgrow(io, ",\"items\": [");
+ for (dblist *list = dbscan_list(tx, &scan, db, 0); list; list = dbscan_list_next(&scan)) {
+ if (!first)
+ obstack_1grow(io, ',');
+ first = 0;
+
+ obstack_1grow(io, '{');
+ obstack_printf(io, "\"id\":\"%d\"", list->id);
+ obstack_printf(io, ",\"name\":\"%s\"", list->name);
+ obstack_printf(io, ",\"size\":\"%d\"", list->size);
+ obstack_1grow(io, '}');
+ }
+ obstack_1grow(io, ']');
+ obstack_1grow(io, '}');
+
+ dbscan_free(&scan);
+ dbindex_abort(tx);
+
+ return 0;
+}
+
+/*
+ Given a file-id, (and a playlist??) list the next few in the playlist.
+ */
+static void write_list_json(dbindex *db, struct obstack *io, int listid, int seq, int fileid) {
+ dbscan scan;
+ dbfile *file;
+ dbtxn *tx = dbindex_begin(db, NULL, 1);
+
+ // TODO: get list properly and seq
+
+ obstack_1grow(io, '{');
+ obstack_sgrow(io, "\"list_name\":\"shuffle\"");
+ obstack_sgrow(io, ",\"items\": [");
+
+ file = dbscan_list_entry(tx, &scan, db, listid, seq, fileid);
+ for (int i=0;file && i<10;i++) {
+ obstack_sgrow(io, i > 0 ? ",{" : "{");
+ write_file_json(io, file);
+
+ obstack_printf(io, ",\"listid\":\"%u\"", listid);
+ obstack_printf(io, ",\"seq\":\"%u\"", dbscan_list_entry_seq(&scan));
+
+ obstack_1grow(io, '}');
+
+ dbfile_free(file);
+ file = dbscan_list_entry_next(&scan);
+ }
+ dbfile_free(file);
+ obstack_sgrow(io, "]}");
+
+ dbscan_free(&scan);
+ dbindex_abort(tx);
+}
+
+static void write_search_json(dbindex *db, struct obstack *io, const char *query) {
+ dbfile *results[50];
+ int len = dbfile_search(db, query, results, 50);
+
+ obstack_1grow(io, '{');
+
+ if (len >= 0) {
+ obstack_sgrow(io, "\"status\":\"ok\"");
+ obstack_sgrow(io, ",\"items\": [");
+
+ for (int i=0;i<len;i++) {
+ dbfile *file = results[i];
+
+ obstack_sgrow(io, i > 0 ? ",{" : "{");
+ write_file_json(io, file);
+ obstack_1grow(io, '}');
+
+ dbfile_free(file);
+ }
+
+ obstack_sgrow(io, "]");
+ } else {
+ obstack_sgrow(io, "\"status\":\"error\"");
+ }
+ obstack_1grow(io, '}');
+}
+
+/* ********************************************************************** */
+
+static int handle_player(struct ez_httprequest *req, struct ez_httpresponse *rep) {
+#if 0
+ httpresponse_set_response(rep, 200, "Ok");
+ ez_list_addtail(&rep->http.headers, &ct_text_html);
+ ez_list_addtail(&rep->http.headers, &ce_gzip);
+ http_set_data(&rep->http, player_html, sizeof(player_html));
+#else
+ struct obstack *os = &rep->http.conn->os;
+
+ if (blobio_load(&rep->http.data, "player.html", os) == 0) {
+ httpresponse_set_response(rep, 200, "Ok");
+ ez_list_addtail(&rep->http.headers, &ct_text_html);
+ }
+#endif
+
+ return 0;
+}
+
+static int handle_quit(struct ez_httprequest *r, struct ez_httpresponse *rep) {
+ httpresponse_set_response(rep, 200, "Byte");
+ return 1;
+}
+
+static int handle_root(struct ez_httprequest *req, struct ez_httpresponse *rep) {
+ const char *msg = "<h1>It Works!</h1><a href='/quit'>Quit</a> | <a href='/x'>Player</a>";
+
+ httpresponse_set_response(rep, 200, "Ok");
+ ez_list_addtail(&rep->http.headers, &ct_text_html);
+ http_set_data(&rep->http, msg, strlen(msg));
+
+ return 0;
+}
+
+static int handle_fwd(struct ez_httprequest *req, struct ez_httpresponse *rep) {
+ struct notify_play_seek msg = { .mode = 1, .stamp = +30.0 };
+
+ notify_msg_send(player, NOTIFY_PLAY_SEEK, 0, &msg);
+ httpresponse_set_response(rep, 202, "Ok");
+ return 0;
+}
+
+static int handle_rew(struct ez_httprequest *req, struct ez_httpresponse *rep) {
+ struct notify_play_seek msg = { .mode = 1, .stamp = -30.0 };
+
+ notify_msg_send(player, NOTIFY_PLAY_SEEK, 0, &msg);
+ httpresponse_set_response(rep, 202, "Ok");
+ return 0;
+}
+
+static int handle_pause(struct ez_httprequest *req, struct ez_httpresponse *rep) {
+ notify_msg_send(player, NOTIFY_PLAY_PAUSE, 0, NULL);
+ httpresponse_set_response(rep, 202, "Ok");
+ return 0;
+}
+
+static int handle_next(struct ez_httprequest *req, struct ez_httpresponse *rep) {
+ notify_msg_send(player, NOTIFY_PLAY_NEXT, 0, NULL);
+ httpresponse_set_response(rep, 202, "Ok");
+ return 0;
+}
+
+static int handle_prev(struct ez_httprequest *req, struct ez_httpresponse *rep) {
+ notify_msg_send(player, NOTIFY_PLAY_PREV, 0, NULL);
+ httpresponse_set_response(rep, 202, "Ok");
+ return 0;
+}
+
+static int set_data_json(struct ez_httpresponse *rep) {
+ struct obstack *os = &rep->http.conn->os;
+ size_t size = obstack_object_size(os);
+ void *data = obstack_finish(os);
+
+ if (data) {
+ ez_list_addtail(&rep->http.headers, &ct_application_json);
+ http_set_data(&rep->http, data, size);
+ httpresponse_set_response(rep, 200, "Ok");
+ }
+
+ return 0;
+}
+
+static int handle_status(struct ez_httprequest *req, struct ez_httpresponse *rep) {
+ struct obstack *os = &rep->http.conn->os;
+
+ write_state_json(db, os);
+
+ return set_data_json(rep);
+}
+
+static int handle_lists(struct ez_httprequest *req, struct ez_httpresponse *rep) {
+ struct obstack *os = &rep->http.conn->os;
+
+ if (strcmp(req->method, "GET") == 0) {
+ write_lists_json(db, os);
+ set_data_json(rep);
+ } else if (strcmp(req->method, "POST") == 0) {
+ const char *name = param_value(req, "name", NULL);
+
+ if (name) {
+ dbtxn *tx = dbindex_begin(db, NULL, 0);
+ dblist list = {
+ .name = (char *)name
+ };
+
+ printf("post create list '%s'\n", name);
+
+ if (dblist_add(tx, db, &list) == 0) {
+ ez_pair *location = obstack_alloc(os, sizeof(*location));
+
+ location->name = "Location";
+ obstack_printf(os, "/x/list/%d", list.id);
+ obstack_1grow(os, 0);
+ location->value = obstack_finish(os);
+
+ httpresponse_set_response(rep, 201, "Created");
+ ez_list_addtail(&rep->http.headers, location);
+ dbindex_commit(tx);
+ } else {
+ dbindex_abort(tx);
+ }
+ } else {
+ httpresponse_set_response(rep, 400, "Missing name");
+ }
+ } else {
+ httpresponse_set_response(rep, 400, "Invalid method");
+ }
+
+ return 0;
+}
+
+void http_addheader(struct ez_http *http, const char *name, const char *value) {
+ struct obstack *os = &http->conn->os;
+ ez_pair *location = obstack_alloc(os, sizeof(*location));
+
+ location->name = obstack_copy(os, name, strlen(name)+1);
+ location->value = obstack_copy(os, value, strlen(value)+1);
+ ez_list_addtail(&http->headers, location);
+}
+
+// /x/list/{listid}
+static int handle_list(struct ez_httprequest *req, struct ez_httpresponse *rep) {
+ struct obstack *os = &rep->http.conn->os;
+ int lid = atoi(req->url + strlen("/x/list/"));
+ int fid = param_int(req, "f", 0);
+ int seq = param_int(req, "s", 0); // TODO: should be in /x/list/{listid}/{seq} ?
+
+ printf("%s %s, listid=%d seq=%d fid=%d\n", req->method, req->url, lid, seq, fid);
+
+ if (strcmp(req->method, "GET") == 0) {
+ write_list_json(db, os, lid, seq, fid);
+ return set_data_json(rep);
+ } else if (strcmp(req->method, "POST") == 0) {
+ // /x/list/{listid} post fid=? ignore seq
+ if (fid == 0)
+ goto fail0;
+
+ dbtxn *tx = dbindex_begin(db, NULL, 0);
+ if (!tx)
+ goto fail0;
+ dblist *list = dblist_get(tx, db, lid);
+ if (!list || dblist_add_file(tx, db, list, fid))
+ goto fail1;
+
+ if (dbindex_commit(tx))
+ goto fail2;
+
+ char location[64];
+
+ sprintf(location, "/x/list/%u/%u", list->id, list->size - 1);
+ http_addheader(&rep->http, "Location", location);
+
+ httpresponse_set_response(rep, 201, "Created");
+ dblist_free(list);
+ return 0;
+
+ fail2:
+ dblist_free(list);
+
+ fail1:
+ dbindex_abort(tx);
+ fail0:
+ httpresponse_set_response(rep, 400, "Invalid");
+ return 0;
+ } else {
+ httpresponse_set_response(rep, 400, "Invalid method");
+ return 0;
+ }
+}
+
+static int handle_search(struct ez_httprequest *req, struct ez_httpresponse *rep) {
+ struct obstack *os = &rep->http.conn->os;
+ const char *query = param_value(req, "q", "");
+
+ write_search_json(db, os, query);
+
+ return set_data_json(rep);
+}
+
+// goto specific file /goto?f={file.id}
+// TODO: playlist
+static int handle_goto(struct ez_httprequest *req, struct ez_httpresponse *rep) {
+ int fid = param_int(req, "f", 0);
+
+ // TODO: look it up in the db?
+
+ if (fid != 0) {
+ struct notify_goto gogo = { .fileid = fid };
+
+ notify_msg_send(player, NOTIFY_PLAY_GOTO, 0, &gogo);
+ httpresponse_set_response(rep, 202, "Requested");
+ return 0;
+ } else {
+ httpresponse_set_response(rep, 404, "Not Found");
+ return 0;
+ }
+}
+
+// seek absolute or relative
+static int handle_seek(struct ez_httprequest *req, struct ez_httpresponse *rep) {
+ ez_pair *skip = find_param(req, "skip");
+ ez_pair *seek = find_param(req, "seek");
+ int isskip = skip && skip->value;
+ int isseek = seek && seek->value;
+
+ if (!(isskip ^ isseek)) {
+ httpresponse_set_response(rep, 400, "Bad Request");
+ return 0;
+ }
+
+ struct notify_play_seek msg = { .mode = isseek ? 0 : 1, .stamp = atof(isseek ? seek->value : skip->value) };
+
+ printf("seek: %s stamp=%+5.2f\n", msg.mode ? " set" : "seek", msg.stamp);
+
+ notify_msg_send(player, NOTIFY_PLAY_SEEK, 0, &msg);
+ httpresponse_set_response(rep, 202, "Requested");
+ return 0;
+}
+
+static struct ez_httphandler handler_list[] = {
+ { .path = "/x", .fn = handle_player },
+ { .path = "/x/", .fn = handle_player },
+
+ { .path = "/x/fwd", .fn = handle_fwd },
+ { .path = "/x/rew", .fn = handle_rew },
+ { .path = "/x/pause", .fn = handle_pause },
+ { .path = "/x/next", .fn = handle_next },
+ { .path = "/x/prev", .fn = handle_prev },
+ { .path = "/x/seek", .fn = handle_seek },
+ { .path = "/x/goto", .fn = handle_goto },
+
+ { .path = "/x/status", .fn = handle_status },
+ { .path = "/x/list", .fn = handle_lists },
+ { .path = "/x/list/", .fn = handle_list, .mode=EZ_PATH_PREFIX },
+
+ { .path = "/x/search", .fn = handle_search },
+
+ { .path = "/", .fn = handle_root },
+ { .path = "/quit", .fn = handle_quit },
+};
+
+int main(int argc, char **argv) {
+ struct ez_httpserver serv;
+
+ db = dbindex_open(MAIN_INDEX);
+ player = notify_writer_new(NOTIFY_PLAYER);
+
+ httpserver_init(&serv, 8000);
+ httpserver_addhandlers(&serv, handler_list, sizeof(handler_list)/sizeof(handler_list[0]));
+ httpserver_run(&serv);
+ httpserver_free(&serv);
+
+ notify_close(player);
+ dbindex_close(db);
+
+ return 0;
+}
{ 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) {
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);
}
}
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) {
}
}
} else if (res == 0) {
+ printf(" repeat\n");
monitor_event(m, &m->repeat);
}
}
struct audio_player;
+//#define VOICE_MENU
//#define VOICE_MT
+#ifdef VOICE_MNENU
/*
synchronous voice experiment
*/
#else
static void handle_audio_msg(struct audio_player *ap, struct voice_audio_msg *msg);
#endif
+#endif
/*
Player bits
// playlist management
dbindex *index;
dbfile *playing;
- char *playing_path;
dbstate playing_state;
int quit;
ap->device = strdup(device);
ap->index = dbindex_open(MAIN_INDEX);
+#ifdef VOICE_MENU
audio_init_voice(ap);
+#endif
audio_init_mixer(ap);
return 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);
* 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;
return audio_init_filter(ap);
fail:
- perror("init media");
+ printf("audio_init_media failed\n");
audio_close_media(ap);
return -1;
}
+#include "lmdb.h"
+
int audio_checkpoint_state(struct audio_player *ap) {
dbtxn *tx = dbindex_begin(ap->index, NULL, 0);
+ int res;
if (tx) {
// TODO: meaningful state value
+ /*
if (ap->playing) {
ap->playing_state.state = 1;
ap->playing_state.fileid = ap->playing->id;
} else {
+ // ?
ap->playing_state.state = 0;
ap->playing_state.fileid = 0;
+ }*/
+ if (ap->playing) {
+ ap->playing_state.state = 1 | (ap->paused ? 2 : 0);
+ // TODO: just update pos directly?
+ ap->playing_state.pos = ap->pos;
+ AVRational sb = ap->audio->time_base;
+ ap->playing_state.poss = av_q2d(sb) * ap->pos;
+ } else {
+ ap->playing_state.state = 0;
+ ap->playing_state.pos = 0;
+ ap->playing_state.poss = 0;
}
- printf("checkpoint state=%d file=%d '%s'\n", ap->playing_state.state, ap->playing_state.fileid, ap->playing_path);
- // ap->pos?
- ap->playing_state.pos = 0;
ap->playing_state.stamp = time(NULL);
- if (dbstate_put(tx, ap->index, &ap->playing_state) == 0) {
+
+ printf("checkpoint state=%d file=%d pos=%zd poss=%f '%s'\n", ap->playing_state.state, ap->playing_state.fileid, ap->playing_state.pos, ap->playing_state.poss, ap->playing ? ap->playing->full_path : "");
+ //printf("Checkpoint state, = %d\n", ap->playing_state.state);
+
+ if ((res = dbstate_put(tx, ap->index, &ap->playing_state)) == 0) {
dbindex_commit(tx);
} else {
+ printf("checkpoint failed: %s\n", mdb_strerror(res));
dbindex_abort(tx);
}
+ } else {
+ printf("checkpoint failed to get tx\n");
}
return 0;
}
// Next in play queue
+static int audio_advance_file(struct audio_player *ap, dbfile *(*advance)(dbscan *scan)) {
+ int res;
+ int empty = ap->playing == NULL;
+ dbscan scan;
+ dbfile *file;
+ dbtxn *tx = dbindex_begin(ap->index, NULL, 1);
+ int retry = 0;
+
+ dbfile_free(ap->playing);
+ ap->playing = NULL;
+
+ do {
+ printf("loop: %d\n", retry);
+ file = dbscan_list_entry(tx, &scan, ap->index, ap->playing_state.listid, ap->playing_state.seq, ap->playing_state.fileid);
+ if (file) {
+ printf("entry: %s\n", file->path);
+ dbfile_free(file);
+ file = advance(&scan);
+ printf("next: %s\n", file ? file->path : "<nil>");
+ }
+ while (file) {
+ res = audio_init_media(ap, dbfile_full_path(tx, ap->index, file));
+
+ if (res == (-30798) && !empty) // && >loop?
+ res = 1;
+
+ if (res == 0) {
+ ap->playing = file;
+ ap->playing_state.listid = dbscan_list_entry_listid(&scan);
+ ap->playing_state.seq = dbscan_list_entry_seq(&scan);
+ ap->playing_state.fileid = file->id;
+ audio_checkpoint_state(ap);
+ break;
+ }
+
+ dbfile_free(file);
+ file = advance(&scan);
+ }
+ // repeat at end of list?
+ if (!file && dbscan_res(&scan) == -30798) {
+ ap->playing_state.seq = 0;
+ ap->playing_state.fileid = 0;
+ }
+ dbscan_free(&scan);
+ } while (!file && retry++ == 0);
+
+ dbindex_abort(tx);
+ return res;
+#if 0
+ audio_close_media(ap);
+ do {
+ //res = dbfile_next(ap->index, &ap->playing, &ap->playing_path);
+ printf("a ap playing = %d\n", ap->playing ? ap->playing->id : -1);
+ res = dbfile_next_shuffle(ap->index, &ap->playing, &ap->playing_path);
+ printf("b ap playing = %d\n", ap->playing ? ap->playing->id : -1);
+
+ if (res == 0)
+ res = audio_init_media(ap, ap->playing_path);
+ if (res == (-30798) && !empty) // && >loop?
+ res = 1;
+ } while (res != 0 && res !=(-30798)); // MDB_NOTFOUND
+
+ audio_checkpoint_state(ap);
+
+ //if (res != 0) {
+ // audio_stop_pcm(ap);
+ //}
+
+ return res;
+#endif
+}
+
int audio_next_file(struct audio_player *ap) {
+ return audio_advance_file(ap, dbscan_list_entry_next);
+#if 0
int res;
int empty = ap->playing == NULL;
//}
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;
// 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;
}
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;
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
res = poll(fds, 2, -1); // timeout to check stuff?
if (fds[0].revents & POLLERR
- || fds[0].revents & POLLERR) {
+ || fds[1].revents & POLLERR) {
// what now?
}
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:
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;
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?
}
break;
}
+#endif
case NOTIFY_DEBUG: {
struct notify_debug *d = msg;
Thread starts synthesising, sends audio to music player via throttled message port(?)
*/
+#ifdef VOICE_MENU
#include <espeak-ng/speak_lib.h>
return 0;
}
#endif
+#endif /* VOICE_MENU */
#include <errno.h>
#include "dbindex.h"
-
+#include "ez-blob-basic.h"
#include "notify.h"
// Message handling and routing
static ez_blob_desc PLAY_SEEK_DESC[] = {
- EZ_BLOB_START(struct notify_play_seek),
+ EZ_BLOB_START(struct notify_play_seek, 1, 2),
EZ_BLOB_INT32(struct notify_play_seek, 1, mode),
EZ_BLOB_FLOAT64(struct notify_play_seek, 2, stamp),
- EZ_BLOB_END(struct notify_play_seek)
};
static ez_blob_desc DEBUG_DESC[] = {
- EZ_BLOB_START(struct notify_debug),
+ EZ_BLOB_START(struct notify_debug, 2, 1),
EZ_BLOB_INT32(struct notify_debug, 1, func),
- EZ_BLOB_END(struct notify_debug)
};
static ez_blob_desc KEY_DESC[] = {
- EZ_BLOB_START(struct notify_key),
+ EZ_BLOB_START(struct notify_key, 3, 1),
EZ_BLOB_INT32(struct notify_key, 1, code),
- EZ_BLOB_END(struct notify_key)
+};
+
+static ez_blob_desc GOTO_DESC[] = {
+ EZ_BLOB_START(struct notify_goto, 4, 3),
+ EZ_BLOB_INT32(struct notify_goto, 1, listid),
+ EZ_BLOB_INT32(struct notify_goto, 2, seq),
+ EZ_BLOB_INT32(struct notify_goto, 3, fileid),
};
/**
DEBUG_DESC,
- NULL
+ NULL,
+
+ GOTO_DESC
};
// should be global by default
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);
*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;
NOTIFY_DEBUG, // debug/prototyping command
NOTIFY_SHUFFLE, /* disk-manager: create shuffled playlist */
+ NOTIFY_PLAY_GOTO, /* player - go to fileid */
NOTIFY_SIZEOF
};
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);
--- /dev/null
+<html>
+ <head>
+ <meta name='viewport' content='width=device-width, initial-scale=1'>
+ <style>
+ .items {
+ list-style-type: none;
+ padding: 0;
+ margin: 0;
+ }
+ .items > li {
+ /*border: 1px solid red;*/
+ }
+ .fixed {
+ font-family: monospace;
+ white-space: pre;
+ }
+ .track-list {
+ width: 100%;
+ }
+ .track-list .track {
+ width: 100%;
+ max-width: 5em;
+ overflow: hidden;
+ }
+ .track-list .title,
+ .track-list .artist,
+ .track-list .duration,
+ .track-list .action {
+ font-family: monospace;
+ white-space: pre;
+ }
+ .track-list .duration {
+ vertical-align: text-baseline;
+ }
+ .media-button rect.button-border {
+ fill: steelblue;
+ /* stroke-width: 2px;
+ stroke: steelblue;*/
+ rx: 15%;
+ }
+ .media-button path {
+ fill: white;
+ }
+ /* scrolling lists */
+ .track-list tbody {
+ display: block;
+ overflow-y: auto;
+ height: 20em;
+ }
+ .track-list thead tr {
+ display: block;
+ }
+ /* zebra colours */
+ .track-list tr {
+ background: grey;
+ color: black;
+ }
+ .track-list tr td {
+ padding: 0.25em 0.5em;
+ }
+ .track-list tr.odd {
+ background: darkgrey;
+ color: black;
+ }
+ body {
+ background: lightgrey;
+ font-family: monospace;
+ }
+ .progress-bar {
+ }
+ .progress-indicator {
+ height: 1em;
+ background: slategrey;
+ }
+ #track-info span {
+ padding: 0.25em 0.5em;
+ margin: 0px 1px;
+ background: darkgrey;
+ }
+ .section {
+ border: 1px solid black;
+ margin: 1em;
+ padding: 0.5em 0em;
+ }
+ .player-button:hover,
+ .player-button:focus
+ {
+ box-shadow: 0 0 2px 3px grey;
+ }
+ tr.even .player-button:hover,
+ tr.even .player-button:focus {
+ box-shadow: 0 0 2px 3px darkgrey;
+ }
+
+ </style>
+ <script>
+ /*
+ ideas on playlist and searches
+
+ search: shows some results
+
+ visit: can visit a file, pausing the playlist position.
+ add to list: add to some list
+ ?remove from list?
+
+ */
+
+ var playlistID = 1; // current list, from state?
+ var listStartID = 0; // "coming up" first entry
+ var allPlayLists = [];
+
+ // failed?
+ function requestGET(url, done) {
+ var r = new XMLHttpRequest();
+ r.onreadystatechange = function() {
+ //console.log(r);
+ if (r.readyState === XMLHttpRequest.DONE) {
+ done(r);
+ }
+ }
+ r.open("GET", url);
+ r.send();
+ }
+
+ function playerCommand(name) {
+ var r = new XMLHttpRequest();
+
+ console.log("command " + name);
+ r.onreadystatechange = function() {
+ console.log(r);
+ if (r.readyState === XMLHttpRequest.DONE) {
+ if (r.status === 0 || (r.status >= 200 && r.status < 400)) {
+ console.log("got it");
+ }
+ }
+ }
+ r.open("GET", "/x/" + name);
+ r.send();
+ }
+ // goto button, b.id = "goto-fileid"
+ function playerGotoClick(e) {
+ playerCommand("goto?f=" + e.currentTarget.id.substring(5));
+ }
+ function playerGotoKey(e) {
+ if (e.key == "Enter" || e.key == " ") {
+ playerCommand("goto?f=" + e.currentTarget.id.substring(5));
+ }
+ }
+
+ function playerCommandClick(e) {
+ playerCommand(e.currentTarget.id.substring(7));
+ }
+ function playerCommandKey(e) {
+ if (e.key == "Enter" || e.key == " ") {
+ playerCommand(e.currentTarget.id.substring(7));
+ }
+ }
+
+ function playerCommandClick2(e) {
+ playerCommand(e.currentTarget.getAttribute("query"));
+ }
+ function playerCommandKey2(e) {
+ if (e.key == "Enter" || e.key == " ") {
+ playerCommand(e.currentTarget.getAttribute("query"));
+ }
+ }
+
+ function playerSetPlaylists(items, list) {
+ let selected = items.value;
+
+ while (items.firstChild)
+ items.firstChild.remove();
+
+ for (var i=0; i<list.length; i++) {
+ let v = list[i];
+ let o = document.createElement("option");
+
+ o.value = v.id; // v.name?
+ o.textContent = v.name + " (" + v.size + ")";
+
+ items.appendChild(o);
+ }
+
+ if (selected) {
+ items.value = selected;
+ }
+ }
+
+ function playerUpdateLists() {
+ console.log("polling lists");
+ requestGET("/x/list", function(r) {
+ if (r.status === 200) {
+ let list = JSON.parse(r.response);
+
+ allPlayLists = list.items;
+
+ // TODO: put this somewhere
+ playerSetPlaylists(document.getElementById("lists"), allPlayLists);
+ //playerSetPlaylists(document.getElementById("next-lists"), list);
+ }
+ });
+ }
+ function createIcon(href) {
+ let b = document.createElementNS("http://www.w3.org/2000/svg", "svg");
+
+ b.classList.add('player-button');
+ b.tabIndex = 1;
+ b.setAttribute("viewBox", "-2 -2 28 28");
+ b.setAttribute("width", "3em");
+
+ let u = document.createElementNS("http://www.w3.org/2000/svg", "use");
+ u.setAttribute("href", href);
+ b.appendChild(u);
+ return b;
+ }
+ function playerAppendTracks(items, list, search) {
+ for (var i=0; i<list.length; i++) {
+ let v = list[i];
+ let row = document.createElement("tr");
+ let c = row.insertCell();
+
+ let d = document.createElement("div");
+ let t = document.createElement("div");
+ let a = document.createElement("div");
+
+ c.classList.add("track");
+
+ t.textContent = v.title;
+ t.classList.add("title");
+ a.textContent = v.artist;
+ a.classList.add("artist");
+
+ d.appendChild(t);
+ d.appendChild(a);
+ c.appendChild(d);
+
+ c = row.insertCell();
+ c.textContent = v.length;
+ c.classList.add("duration");
+
+ c = row.insertCell();
+ /*
+ let b = document.createElement("span");
+ b.id = "goto-" + v.id;
+ b.textContent = "\u23f5";// "\u25B6";
+ b.tabIndex = 50;
+ b.setAttribute("query", "goto?f=" + v.id);
+ b.addEventListener("click", playerCommandClick2);
+ b.addEventListener("keypress", playerCommandKey2);
+ */
+ /*
+ let b = document.createElementNS("http://www.w3.org/2000/svg", "svg");
+
+ b.tabIndex = 1;
+ b.setAttribute("query", "goto?f=" + v.id);
+ b.setAttribute("viewBox", "-2 -2 28 28");
+ b.setAttribute("width", "3em");
+ b.addEventListener("click", playerCommandClick2);
+ b.addEventListener("keypress", playerCommandKey2);
+
+ let u = document.createElementNS("http://www.w3.org/2000/svg", "use");
+ u.setAttribute("href", "#icon-play");
+ b.appendChild(u);
+ */
+
+ let b = createIcon("#icon-play");
+ b.setAttribute("query", "goto?f=" + v.id);
+ b.addEventListener("click", playerCommandClick2);
+ b.addEventListener("keypress", playerCommandKey2);
+
+ c.appendChild(b);
+
+ if (search) {
+ b = createIcon("#icon-playlist-add");
+ //b.setAttribute("query", "list/" + v.goto?f=" + v.id);
+ //b.addEventListener("click", playerCommandClick2);
+ //b.addEventListener("keypress", playerCommandKey2);
+ c.appendChild(b);
+ } else {
+ b = createIcon("#icon-nope");
+ c.appendChild(b);
+ }
+
+ c.classList.add("action");
+
+ row.classList.add((i & 1) ? "odd" : "even");
+
+ items.appendChild(row);
+ }
+ }
+
+ function playerUpdateList(fileid, seq) {
+ console.log("polling playlist");
+ requestGET("/x/list/" + playlistID + "?f=" + fileid + "?s=" + seq, function(r) {
+ if (r.status === 200) {
+ let table = document.getElementById("coming-up");
+ let items = document.getElementById("coming-up-list");
+ let list = JSON.parse(r.response);
+
+ while (items.firstChild)
+ items.firstChild.remove();
+
+ if (list.items.length > 0) {
+ table.caption = document.createElement("caption");
+ table.caption.textContent = "Coming up in playlist '" + list.list_name + "' …";
+ }
+
+ playerAppendTracks(items, list.items, 0);
+ /*
+ for (var i=0; i<list.list.length; i++) {
+ let v = list.list[i];
+ let row = document.createElement("tr");
+ let c = row.insertCell();
+
+ c.textContent = v.title;
+ c.classList.add("title");
+ c = row.insertCell();
+ c.textContent = v.artist;
+ c.classList.add("artist");
+ c = row.insertCell();
+ c.textContent = v.length;
+ c.classList.add("duration");
+
+ c = row.insertCell();
+ let b = document.createElement("span");
+ b.id = "goto-" + v.id;
+ b.textContent = "GOTO";
+ b.tabIndex = 50;
+ b.addEventListener("click", playerGotoClick);
+ b.addEventListener("keypress", playerGotoKey);
+ c.appendChild(b);
+
+ items.appendChild(row);
+ } */
+ }
+ listStartID = fileid;
+ });
+ }
+
+ function playerSearch(query) {
+ console.log("search: " + query);
+ requestGET("/x/search?q=" + encodeURIComponent(query), function(r) {
+ if (r.status === 200) {
+ let table = document.getElementById("search-results");
+ let items = document.getElementById("search-results-list");
+ let list = JSON.parse(r.response);
+
+ while (items.firstChild)
+ items.firstChild.remove();
+
+ //table.caption = document.createElement("caption");
+ //if (list.items.length > 0) {
+ // table.caption.textContent = "Matches";
+ //} else {
+ // table.caption.textContent = "No matches";
+ // }
+
+ playerAppendTracks(items, list.items, 1);
+ /*
+ for (var i=0; i<list.items.length; i++) {
+ let v = list.items[i];
+ let row = document.createElement("tr");
+ let c = row.insertCell();
+
+ c.textContent = v.title;
+ c = row.insertCell();
+ c.textContent = v.artist;
+ c = row.insertCell();
+ c.textContent = v.length;
+
+ c = row.insertCell();
+ let b = document.createElement("span");
+ b.id = "goto-" + v.id;
+ b.textContent = "GOTO";
+ b.tabIndex = 50;
+ b.addEventListener("click", playerGotoClick);
+ b.addEventListener("keypress", playerGotoKey);
+ c.appendChild(b);
+
+ // add to playlist button, or whatever
+
+ items.appendChild(row);
+ }*/
+ }
+ });
+ }
+
+ function playerPoll() {
+ var r = new XMLHttpRequest();
+
+ //console.log("polling");
+ r.onreadystatechange = function() {
+ //console.log(r);
+ if (r.readyState === XMLHttpRequest.DONE) {
+ if (r.status == 200) {
+ var s = JSON.parse(r.response);
+
+ document.getElementById("track-title").textContent = s.title;
+ document.getElementById("track-artist").textContent = s.artist;
+ document.getElementById("track-path").textContent = s.path;
+ document.getElementById("track-length").textContent = s.length;
+
+ /*
+ let p = document.getElementById("track-position");
+
+ if (s.status == "playing") {
+ p.textContent = s.position;
+ } else if (s.status == "paused") {
+ p.textContent = s.position + "(paused)";
+ //p.class ? paused
+ } else {
+ p.textContent = "(stopped)";
+ }*/
+
+ document.getElementById("track-progress").style.width = (s.positionms * 100 / s.lengthms) + "%";
+ document.getElementById("track-position").textContent = s.position;
+ document.getElementById("track-length").textContent = s.length;
+
+ if (s.id != listStartID) {
+ playerUpdateList(s.id, s.seq);
+ playerUpdateLists();
+ }
+ } else {
+ document.getElementById("track-title").textContent = "?";
+ document.getElementById("track-artist").textContent = "?";
+ document.getElementById("track-length").textContent = "?";
+ document.getElementById("track-position").textContent = "?";
+ }
+
+ window.setTimeout(playerPoll, 5000);
+ }
+ }
+ r.open("GET", "/x/status");
+ r.send();
+ }
+
+ function playerCreateList(name) {
+ var r = new XMLHttpRequest();
+
+ console.log("create list: " + name);
+
+ r.onreadystatechange = function() {
+ console.log(r);
+ if (r.readyState === XMLHttpRequest.DONE) {
+ if (r.status === 0 || (r.status >= 200 && r.status < 400)) {
+ console.log("got it");
+ playerUpdateLists();
+ }
+ }
+ }
+ r.open("POST", "/x/list?name=" + encodeURIComponent(name));
+ r.send();
+ }
+
+ document.addEventListener("DOMContentLoaded", function() {
+ // query in attribute
+ Array.prototype.forEach.call(document.getElementsByClassName("player-button"), function(b) {
+ b.addEventListener("click", playerCommandClick2);
+ b.addEventListener("keypress", playerCommandKey2);
+ });
+
+ playerPoll();
+ });
+ </script>
+ </head>
+ <body>
+ <svg width='0' height='0' xmlns="http://www.w3.org/2000/svg">
+ <defs>
+ <svg id='icon-forward' class='media-button' xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 0 24 24" width="24">
+ <rect class='button-border' width='24' height='24'/>
+ <path d="M5.58 16.89l5.77-4.07c.56-.4.56-1.24 0-1.63L5.58 7.11C4.91 6.65 4 7.12 4 7.93v8.14c0 .81.91 1.28 1.58.82zM13 7.93v8.14c0 .81.91 1.28 1.58.82l5.77-4.07c.56-.4.56-1.24 0-1.63l-5.77-4.07c-.67-.47-1.58 0-1.58.81z"/>
+ </svg>
+ <svg id='icon-rewind' class='media-button' xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 0 24 24" width="24">
+ <rect class='button-border' width='24' height='24'/>
+ <path d="M11 16.07V7.93c0-.81-.91-1.28-1.58-.82l-5.77 4.07c-.56.4-.56 1.24 0 1.63l5.77 4.07c.67.47 1.58 0 1.58-.81zm1.66-3.25l5.77 4.07c.66.47 1.58-.01 1.58-.82V7.93c0-.81-.91-1.28-1.58-.82l-5.77 4.07c-.57.4-.57 1.24 0 1.64z"/>
+ </svg>
+ <svg id='icon-pause' class='media-button' xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 0 24 24" width="24">
+ <rect class='button-border' width='24' height='24'/>
+ <path d="M8 19c1.1 0 2-.9 2-2V7c0-1.1-.9-2-2-2s-2 .9-2 2v10c0 1.1.9 2 2 2zm6-12v10c0 1.1.9 2 2 2s2-.9 2-2V7c0-1.1-.9-2-2-2s-2 .9-2 2z"/>
+ </svg>
+ <svg id='icon-next' class='media-button' xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 0 24 24" width="24">
+ <rect class='button-border' width='24' height='24'/>
+ <path d="M7.58 16.89l5.77-4.07c.56-.4.56-1.24 0-1.63L7.58 7.11C6.91 6.65 6 7.12 6 7.93v8.14c0 .81.91 1.28 1.58.82zM16 7v10c0 .55.45 1 1 1s1-.45 1-1V7c0-.55-.45-1-1-1s-1 .45-1 1z"/>
+ </svg>
+ <svg id='icon-prev' class='media-button' xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 0 24 24" width="24">
+ <rect class='button-border' width='24' height='24'/>
+ <path d="M7 6c.55 0 1 .45 1 1v10c0 .55-.45 1-1 1s-1-.45-1-1V7c0-.55.45-1 1-1zm3.66 6.82l5.77 4.07c.66.47 1.58-.01 1.58-.82V7.93c0-.81-.91-1.28-1.58-.82l-5.77 4.07c-.57.4-.57 1.24 0 1.64z"/>
+ </svg>
+ <svg id='icon-nope' class='media-button' xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 0 24 24" width="24">
+ <rect class='button-border' width='24' height='24'/>
+ <path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 18c-4.42 0-8-3.58-8-8 0-1.85.63-3.55 1.69-4.9L16.9 18.31C15.55 19.37 13.85 20 12 20zm6.31-3.1L7.1 5.69C8.45 4.63 10.15 4 12 4c4.42 0 8 3.58 8 8 0 1.85-.63 3.55-1.69 4.9z"/>
+ </svg>
+ <svg id='icon-queue-music' class='media-button' xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 0 24 24" width="24">
+ <rect class='button-border' width='24' height='24'/>
+ <path d="M14 6H4c-.55 0-1 .45-1 1s.45 1 1 1h10c.55 0 1-.45 1-1s-.45-1-1-1zm0 4H4c-.55 0-1 .45-1 1s.45 1 1 1h10c.55 0 1-.45 1-1s-.45-1-1-1zM4 16h6c.55 0 1-.45 1-1s-.45-1-1-1H4c-.55 0-1 .45-1 1s.45 1 1 1zM19 6c-1.1 0-2 .9-2 2v6.18c-.31-.11-.65-.18-1-.18-1.84 0-3.28 1.64-2.95 3.54.21 1.21 1.2 2.2 2.41 2.41 1.9.33 3.54-1.11 3.54-2.95V8h2c.55 0 1-.45 1-1s-.45-1-1-1h-2z"/>
+ </svg>
+ <svg id='icon-playlist-add' class='media-button' xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 0 24 24" width="24">
+ <rect class='button-border' width='24' height='24'/>
+ <path d="M13 10H3c-.55 0-1 .45-1 1s.45 1 1 1h10c.55 0 1-.45 1-1s-.45-1-1-1zm0-4H3c-.55 0-1 .45-1 1s.45 1 1 1h10c.55 0 1-.45 1-1s-.45-1-1-1zm5 8v-3c0-.55-.45-1-1-1s-1 .45-1 1v3h-3c-.55 0-1 .45-1 1s.45 1 1 1h3v3c0 .55.45 1 1 1s1-.45 1-1v-3h3c.55 0 1-.45 1-1s-.45-1-1-1h-3zM3 16h6c.55 0 1-.45 1-1s-.45-1-1-1H3c-.55 0-1 .45-1 1s.45 1 1 1z"/>
+ </svg>
+ <svg id='icon-play' class='media-button' xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 0 24 24" width="24">
+ <rect class='button-border' width='24' height='24'/>
+ <path d="M8 6.82v10.36c0 .79.87 1.27 1.54.84l8.14-5.18c.62-.39.62-1.29 0-1.69L9.54 5.98C8.87 5.55 8 6.03 8 6.82z"/>
+ </svg>
+ <svg id='icon-queue' class='media-button' xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 0 24 24" width="24">
+ <rect class='button-border' width='24' height='24'/>
+ <path d="M3 6c-.55 0-1 .45-1 1v13c0 1.1.9 2 2 2h13c.55 0 1-.45 1-1s-.45-1-1-1H5c-.55 0-1-.45-1-1V7c0-.55-.45-1-1-1zm17-4H8c-1.1 0-2 .9-2 2v12c0 1.1.9 2 2 2h12c1.1 0 2-.9 2-2V4c0-1.1-.9-2-2-2zm-2 9h-3v3c0 .55-.45 1-1 1s-1-.45-1-1v-3h-3c-.55 0-1-.45-1-1s.45-1 1-1h3V6c0-.55.45-1 1-1s1 .45 1 1v3h3c.55 0 1 .45 1 1s-.45 1-1 1z"/>
+ </svg>
+ <svg id=icon-add-to-queue' class='media-button' xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 0 24 24" width="24">
+ <rect class='button-border' width='24' height='24'/>
+ <path d="M21 3H3c-1.1 0-2 .9-2 2v12c0 1.1.9 2 2 2h5v1c0 .55.45 1 1 1h6c.55 0 1-.45 1-1v-1h5c1.1 0 2-.9 2-2V5c0-1.11-.9-2-2-2zm-1 14H4c-.55 0-1-.45-1-1V6c0-.55.45-1 1-1h16c.55 0 1 .45 1 1v10c0 .55-.45 1-1 1zm-4-6c0 .55-.45 1-1 1h-2v2c0 .55-.45 1-1 1s-1-.45-1-1v-2H9c-.55 0-1-.45-1-1s.45-1 1-1h2V8c0-.55.45-1 1-1s1 .45 1 1v2h2c.55 0 1 .45 1 1z"/>
+ </svg>
+ <svg id='icon-remove-from-queue' class='media-button' xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 0 24 24" width="24">
+ <rect class='button-border' width='24' height='24'/>
+ <path d="M21 3H3c-1.1 0-2 .9-2 2v12c0 1.1.9 2 2 2h5v1c0 .55.45 1 1 1h6c.55 0 1-.45 1-1v-1h5c1.1 0 2-.9 2-2V5c0-1.11-.9-2-2-2zm-1 14H4c-.55 0-1-.45-1-1V6c0-.55.45-1 1-1h16c.55 0 1 .45 1 1v10c0 .55-.45 1-1 1zm-4-6c0 .55-.45 1-1 1H9c-.55 0-1-.45-1-1s.45-1 1-1h6c.55 0 1 .45 1 1z"/>
+ </svg>
+ <svg id='icon-shuffle' class='media-button' xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 0 24 24" width="24">
+ <rect class='button-border' width='24' height='24'/>
+ <path d="M10.59 9.17L6.12 4.7c-.39-.39-1.02-.39-1.41 0-.39.39-.39 1.02 0 1.41l4.46 4.46 1.42-1.4zm4.76-4.32l1.19 1.19L4.7 17.88c-.39.39-.39 1.02 0 1.41.39.39 1.02.39 1.41 0L17.96 7.46l1.19 1.19c.31.31.85.09.85-.36V4.5c0-.28-.22-.5-.5-.5h-3.79c-.45 0-.67.54-.36.85zm-.52 8.56l-1.41 1.41 3.13 3.13-1.2 1.2c-.31.31-.09.85.36.85h3.79c.28 0 .5-.22.5-.5v-3.79c0-.45-.54-.67-.85-.35l-1.19 1.19-3.13-3.14z"/>
+ </svg>
+ </defs>
+ </svg>
+
+ <div id='player' class='section'>
+
+ <div id='track-panel'>
+ <div id='track-info' style='display:grid;grid-template-columns:8em 1fr;padding:0.5em 0px;'>
+ <span class='head'>Title</span><span id='track-title'></span>
+ <span class='head'>Artist</span><span id='track-artist'></span>
+ <span class='head'>Path</span><span id='track-path'></span>
+ </div>
+ <div class='progress-bar' style='width:100%;display:grid;grid-template-columns:8em 1fr 8em;'>
+ <span id='track-position' style='text-align:center;'></span>
+ <div id='track-progress' class='progress-indicator'></div>
+ <span id='track-length' style='text-align:centre;'></span>
+ </div>
+ </div>
+ <!-- fully resizable
+ <div id='player-buttons' style='width:100%;display:grid;grid-template-columns:3fr repeat(5,minmax(5em, 1fr)) 3fr;justify-content:center;'>
+ <span></span>
+ <svg query='rew' class='player-button' tabindex='1' width='100%' viewbox='-2 -2 28 28'><use href='#icon-rewind'/></svg>
+ <svg query='pause' class='player-button' tabindex='1' width='100%' viewbox='-2 -2 28 28'><use href='#icon-pause'/></svg>
+ <svg query='fwd' fwd' class='player-button' tabindex='1' width='100%' viewbox='-2 -2 28 28'><use href='#icon-forward'/></svg>
+ <svg query='prev' class='player-button' tabindex='1' width='100%' viewbox='-2 -2 28 28'><use href='#icon-prev'/></svg>
+ <svg query='next' class='player-button' tabindex='1' width='100%' viewbox='-2 -2 28 28'><use href='#icon-next'/></svg>
+ <span></span>
+ </div>
+ -->
+ <div id='player-buttons' style='width:100%;display:grid;grid-template-columns:1fr repeat(5,minmax(3em, 6em)) 1fr;justify-content:center;'>
+ <span></span>
+ <svg query='rew' class='player-button' tabindex='1' width='100%' viewbox='-2 -2 28 28'><use href='#icon-rewind'/></svg>
+ <svg query='pause' class='player-button' tabindex='1' width='100%' viewbox='-2 -2 28 28'><use href='#icon-pause'/></svg>
+ <svg query='fwd' fwd' class='player-button' tabindex='1' width='100%' viewbox='-2 -2 28 28'><use href='#icon-forward'/></svg>
+ <svg query='prev' class='player-button' tabindex='1' width='100%' viewbox='-2 -2 28 28'><use href='#icon-prev'/></svg>
+ <svg query='next' class='player-button' tabindex='1' width='100%' viewbox='-2 -2 28 28'><use href='#icon-next'/></svg>
+ <span></span>
+ </div>
+ <!--
+ <div id='player-buttons' style='width:100%;display:grid;grid-template-columns:1fr auto 1fr;justify-content:center;'>
+ <span></span>
+ <div>
+ <svg query='rew' class='player-button' tabindex='1' width='6em' viewbox='-2 -2 28 28'><use href='#icon-rewind'/></svg>
+ <svg query='pause' class='player-button' tabindex='1' width='6em' viewbox='-2 -2 28 28'><use href='#icon-pause'/></svg>
+ <svg query='fwd' fwd' class='player-button' tabindex='1' width='6em' viewbox='-2 -2 28 28'><use href='#icon-forward'/></svg>
+ <svg query='prev' class='player-button' tabindex='1' width='6em' viewbox='-2 -2 28 28'><use href='#icon-prev'/></svg>
+ <svg query='next' class='player-button' tabindex='1' width='6em' viewbox='-2 -2 28 28'><use href='#icon-next'/></svg>
+ </div>
+ <span></span>
+ </div>
+ -->
+ </div>
+ <div style='padding:1em;'>
+ <input id='new-list-name' type='text'> <input type='button' value='Create Playlist' onClick='playerCreateList(document.getElementById("new-list-name").value)'>
+ <select id='lists'></select>
+ </div>
+ <div class='section'>
+ <table id='coming-up' class='track-list'>
+ <!--
+ <thead>
+ <tr><th class='track'>Track<th class='duration'>Length<th class='action'>Action <select id='next-lists'><option>list a</option><option>list b</option></select></tr>
+ </thead> -->
+ <tbody id='coming-up-list'>
+ </tbody>
+ </table>
+ </div>
+ <div id='search' class='section'>
+ <!-- <input type='text' onchange='playerSearch(this.value)'> -->
+ <table id='search-results' class='track-list'>
+ <caption>Search <input type='text' oninput='playerSearch(this.value)'></caption>
+ <!--
+ <thead>
+ <tr><th class='track'>Track<th class='duration'>Length<th class='action'>Action <select id='search-lists'><option>list a</option><option>list b</option></select></tr>
+ </thead> -->
+ <tbody id='search-results-list'>
+ </tbody>
+ </table>
+ </div>
+ </body>
+</html>