Initial import of music player.
authorNot Zed <notzed@gmail.com>
Wed, 18 Nov 2020 03:20:35 +0000 (13:50 +1030)
committerNot Zed <notzed@gmail.com>
Wed, 18 Nov 2020 03:20:35 +0000 (13:50 +1030)
Makefile [new file with mode: 0644]
audio-cmd.c [new file with mode: 0644]
dbindex.c [new file with mode: 0644]
dbindex.h [new file with mode: 0644]
disk-indexer.c [new file with mode: 0644]
disk-monitor.c [new file with mode: 0644]
input-monitor.c [new file with mode: 0644]
music-player.c [new file with mode: 0644]
notify.c [new file with mode: 0644]
notify.h [new file with mode: 0644]

diff --git a/Makefile b/Makefile
new file mode 100644 (file)
index 0000000..8afde7f
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,42 @@
+
+FFMPEG=/opt/ffmpeg/4.0
+LMDB=/home/notzed/src/openldap/libraries/liblmdb
+WARN=-Wno-deprecated-declarations -Wno-parentheses -Wno-unused-but-set-variable
+
+pkgs=ffmpeg lmdb blkid asound espeak
+
+#CFLAGS_ffmpeg=-I$(FFMPEG)/include
+#LDFLAGS_ffmpeg=-L$(FFMPEG)/lib -Wl,-rpath,$(FFMPEG)/lib
+LDLIBS_ffmpeg=-lavformat -lavutil -lavcodec -lswresample -lavfilter
+
+#CFLAGS_lmdb=-I$(LMDB)
+#LDFLAGS_lmdb=-L$(LMDB) -Wl,-rpath,$(LMDB)
+LDLIBS_lmdb=-llmdb
+
+LDLIBS_blkid=-lblkid
+LDLIBS_asound=-lasound
+
+#CFLAGS_espeak=-I/opt/espeak/include
+#LDFLAGS_espeak=-L/opt/espeak/lib -Wl,-rpath,/opt/espeak/lib
+LDLIBS_espeak=-lespeak-ng
+
+CFLAGS=-std=gnu99 -I../libeze
+CFLAGS+=-Wall $(WARN)
+CFLAGS+=-g -O0
+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
+
+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
+
+clean:
+       rm $(PROGS)
+       rm *.o
diff --git a/audio-cmd.c b/audio-cmd.c
new file mode 100644 (file)
index 0000000..87e74a9
--- /dev/null
@@ -0,0 +1,57 @@
+
+#include <string.h>
+#include <stdio.h>
+#include <stdlib.h>
+
+#include "notify.h"
+
+/*
+static const struct {
+       const char *cmd;
+       enum notify_action action;
+} commands[] = {
+       { "pause", NOTIFY_PLAYER_PAUSE },
+       { "play", NOTIFY_PLAYER_PAUSE },
+       { "seek", NOTIFY_PLAYER_SEEKPAUSE },
+       { "pause", NOTIFY_PLAYER_PAUSE },
+};
+*/
+
+int main(int argc, char **argv) {
+       if (argc > 1) {
+               char *cmd = argv[1];
+               notify_t player = notify_writer_new(NOTIFY_PLAYER);
+
+               if (strcmp(cmd, "pause") == 0)
+                       notify_msg_send(player, NOTIFY_PLAY_PAUSE, 1, NULL);
+               else if (strcmp(cmd, "play") == 0)
+                       notify_msg_send(player, NOTIFY_PLAY_PLAY, 0, NULL);
+               else if (strcmp(cmd, "seek") == 0 || strcmp(cmd, "skip") == 0) {
+                       if (argc == 3) {
+                               struct notify_play_seek msg;
+
+                               msg.mode = strcmp(cmd, "skip") == 0;
+                               msg.stamp = strtod(argv[2], NULL);
+                       
+                               notify_msg_send(player, NOTIFY_PLAY_SEEK, 0, &msg);
+                       }
+               } else if (strcmp(cmd, "next") == 0) {
+                       notify_msg_send(player, NOTIFY_PLAY_NEXT, 0, NULL);
+               } else if (strcmp(cmd, "prev") == 0) {
+                       notify_msg_send(player, NOTIFY_PLAY_PREV, 0, NULL);
+               } else if (strcmp(cmd, "quit") == 0) {
+                       notify_msg_send(player, NOTIFY_QUIT, 0, NULL);
+               } else if (strcmp(cmd, "debug") == 0) {
+                       if (argc == 3) {
+                               struct notify_debug msg;
+
+                               msg.func = atoi(argv[2]);
+                       
+                               notify_msg_send(player, NOTIFY_DEBUG, 0, &msg);
+                       }
+               }
+               
+               notify_close(player);
+                       
+       }
+}
diff --git a/dbindex.c b/dbindex.c
new file mode 100644 (file)
index 0000000..2cfd282
--- /dev/null
+++ b/dbindex.c
@@ -0,0 +1,1010 @@
+#include <stddef.h>
+#include <stdlib.h>
+#include <string.h>
+#include <stdint.h>
+#include <stdio.h>
+
+#include <assert.h>
+
+#include <lmdb.h>
+
+#include "dbindex.h"
+#include "ez-blob.h"
+
+ez_blob_desc DBDISK_DESC[] = {
+       EZ_BLOB_START(dbdisk),
+       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_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_END(dbfile)
+};
+
+struct dbindex {
+       int res; // last result
+
+       MDB_env *env;
+
+       MDB_dbi meta;
+
+       MDB_dbi disk;
+       MDB_dbi disk_by_uuid;   // key is uuid                UNIQUE
+
+       MDB_dbi file;
+       MDB_dbi file_by_path;   // key is "diskid{hex}/path"  UNIQUE
+       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
+
+       // ? 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 fileid;
+};
+
+static uint32_t disk_next_id(dbindex *db) {
+       return __atomic_fetch_add(&db->diskid, 1, __ATOMIC_SEQ_CST);
+}
+
+static uint32_t file_next_id(dbindex *db) {
+       return __atomic_fetch_add(&db->fileid, 1, __ATOMIC_SEQ_CST);
+}
+
+// Find the next primary key in the db
+static uint32_t find_next_id(MDB_txn *tx, MDB_dbi db) {
+       MDB_cursor *cursor;
+       MDB_val key = { 0 }, data = { 0 };
+       int r;
+       uint32_t id = 1;
+
+       printf("find last value of db\n");
+       mdb_cursor_open(tx, db, &cursor);
+       r = mdb_cursor_get(cursor, &key, &data, MDB_LAST);
+       if (r == 0) {
+               assert(key.mv_size == sizeof(id));
+               memcpy(&id, key.mv_data, sizeof(id));
+               printf("found, was %d\n", id);
+               id += 1;
+       } else {
+               printf("not found (%d), using %d\n", r, id);
+       }
+
+       mdb_cursor_close(cursor);
+
+       return id;
+}
+
+dbindex *dbindex_open(const char *path) {
+       dbindex *db = calloc(sizeof(*db), 1);
+       int res;
+       MDB_txn *tx;
+
+       res = mdb_env_create(&db->env);
+       if (res)
+               goto fail;
+       res = mdb_env_set_maxdbs(db->env, 16);
+       if (res)
+               goto fail;
+       res = mdb_env_set_mapsize(db->env, 1<<28); // 256MB
+       if (res)
+               goto fail;
+       res = mdb_env_open(db->env, path, 0, 0664);
+       if (res)
+               goto fail;
+
+       res = mdb_txn_begin(db->env, NULL, 0, &tx);
+       if (res)
+               goto fail;
+
+       // ??
+       res |= mdb_dbi_open(tx, "#meta", MDB_CREATE, &db->meta);
+
+       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, "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, "shuffle", MDB_CREATE | MDB_INTEGERKEY, &db->shuffle);
+       res |= mdb_dbi_open(tx, "shuffle#file", MDB_CREATE | MDB_INTEGERKEY, &db->shuffle_by_file);
+
+       db->diskid = find_next_id(tx, db->disk);
+       db->fileid = find_next_id(tx, db->file);
+
+       {
+               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_blob_decode(DBDISK_DESC, data.mv_data, data.mv_size);
+                       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);
+       }
+
+       res |= mdb_txn_commit(tx);
+
+       if (res) {
+               printf("db setup fail: %s\n", mdb_strerror(res));
+               // shutdown
+               mdb_env_close(db->env);
+               db = NULL;
+       }
+
+       printf("dbindex open, disk.id=%d file.id=%d\n", db->diskid, db->fileid);
+
+       return db;
+ fail:
+       printf("db setup `%s' fail: %s\n", path, mdb_strerror(res));
+       // shutdown
+       if (db->env)
+               mdb_env_close(db->env);
+       free(db);
+       return NULL;
+}
+
+void dbindex_close(dbindex *db) {
+       if (db) {
+               mdb_env_close(db->env);
+               free(db);
+       }
+}
+
+MDB_txn *dbindex_begin(dbindex *db, dbtxn *txn, int readonly) {
+       MDB_txn *tx;
+       int flags = readonly ? MDB_RDONLY : 0;
+
+       mdb_txn_begin(db->env, txn, flags, &tx);
+
+       return tx;
+}
+
+void dbindex_commit(MDB_txn *tx) {
+       mdb_txn_commit(tx);
+}
+
+void dbindex_abort(MDB_txn *tx) {
+       mdb_txn_abort(tx);
+}
+
+// get by_path key
+static char *dbfile_path(dbfile *f) {
+       char *path = malloc(strlen(f->path) + 9);
+
+       sprintf(path, "%08x%s", f->diskid, f->path);
+
+       return path;
+}
+
+// retrive object from main db
+static void *primary_get_decode(MDB_txn *tx, dbindex *db, ez_blob_desc *desc, MDB_val *key, MDB_dbi primary) {
+       MDB_val data;
+
+       db->res = mdb_get(tx, primary, key, &data);
+
+       if (db->res == 0) {
+               void *p = ez_blob_decode(desc, data.mv_data, data.mv_size);
+
+               assert(desc[0].bd_type == EZ_BLOB_PK);
+               assert(key->mv_size == sizeof(int));
+
+               memcpy(p, key->mv_data, sizeof(int));
+               return p;
+       }
+
+       return NULL;
+}
+
+/**
+ * Retrieve and decode data based on unique secondayry key.
+ *
+ * @param secondary key to retrieve
+ * @param data holder
+ */
+static void *secondary_get_decode(MDB_txn *tx, dbindex *db, ez_blob_desc *desc, MDB_val *key, MDB_dbi primary, MDB_dbi secondary) {
+       MDB_val data;
+
+       db->res = mdb_get(tx, secondary, key, &data);
+
+       if (db->res == 0)
+               return primary_get_decode(tx, db, desc, &data, primary);
+
+       return NULL;
+}
+
+dbdisk *dbdisk_get(dbtxn *tx, dbindex *db, int diskid) {
+       MDB_val key = { .mv_data = &diskid, .mv_size = sizeof(diskid) };
+
+       return primary_get_decode(tx, db, DBDISK_DESC, &key, db->disk);
+}
+
+dbdisk *dbdisk_get_uuid(MDB_txn *tx, dbindex *db, const char *uuid) {
+       MDB_val key  = {
+               .mv_data = (void *)uuid,
+               .mv_size = strlen(uuid)
+       };
+
+       return secondary_get_decode(tx, db, DBDISK_DESC, &key, db->disk, db->disk_by_uuid);
+}
+
+void dbdisk_free(dbdisk *f) {
+       ez_blob_free(DBDISK_DESC, f);
+}
+
+int dbdisk_add(MDB_txn *txn, dbindex *db, dbdisk *d) {
+       MDB_txn *tx;
+       MDB_val key, data;
+       int res;
+
+       mdb_txn_begin(db->env, txn, 0, &tx);
+
+       // Store disk
+       d->id = disk_next_id(db);
+       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);
+       }
+
+       if (res != 0) {
+               printf("db put disk fail: %s\n", mdb_strerror(res));
+               return res;
+       }
+
+       // Store secondary keys
+       data.mv_data = &d->id;
+       data.mv_size = sizeof(d->id);
+
+       // - by uuid
+       key.mv_data = d->uuid;
+       key.mv_size = strlen(d->uuid);
+
+       res = mdb_put(tx, db->disk_by_uuid, &key, &data, MDB_NOOVERWRITE);
+       if (res == 0) {
+               return mdb_txn_commit(tx);
+       } else if (res == MDB_KEYEXIST) {
+               fprintf(stderr, "UNIQUE: disk uuid exists\n");
+               mdb_txn_abort(tx);
+               return res;
+       } else {
+               printf("db put fail: %s\n", mdb_strerror(res));
+               return res;
+       }
+
+}
+
+dbfile *dbfile_get_path(MDB_txn *tx, dbindex *db, int diskid, const char *path) {
+       char name[strlen(path) + 9];
+       MDB_val key;
+
+       sprintf(name, "%08x", diskid);
+       strcpy(name+8, path);
+
+       key.mv_data = name;
+       key.mv_size = strlen(name);
+
+       return secondary_get_decode(tx, db, DBFILE_DESC, &key, db->file, db->file_by_path);
+}
+
+void dbfile_free(dbfile *f) {
+       ez_blob_free(DBFILE_DESC, f);
+}
+
+dbfile *dbfile_get(dbtxn *tx, dbindex *db, int fileid) {
+       MDB_val key = { .mv_data = &fileid, .mv_size = sizeof(fileid) };
+
+       return primary_get_decode(tx, db, DBFILE_DESC, &key, db->file);
+}
+
+int dbfile_del_id(dbtxn *tx, dbindex *db, int fileid) {
+       dbfile *f = dbfile_get(tx, db, fileid);
+
+       if (f) {
+              db->res = dbfile_del(tx, db, f);
+              dbfile_free(f);
+       }
+
+       return db->res;
+}
+
+int dbfile_del(dbtxn *txn, dbindex *db, dbfile *f) {
+       MDB_txn *tx;
+       MDB_val key, data;
+       int res;
+
+       mdb_txn_begin(db->env, txn, 0, &tx);
+
+       // Remove secondary keys / constraints
+       data.mv_data = &f->id;
+       data.mv_size = sizeof(f->id);
+
+       // - by disk+path (and unique constraint)
+       char *dpath = dbfile_path(f);
+
+       key.mv_data = dpath;
+       key.mv_size = strlen(dpath);
+       res = mdb_del(tx, db->file_by_path, &key, NULL);
+       free(dpath);
+       if (res)
+               goto fail;
+
+       // - by diskid
+       key.mv_data = &f->diskid;
+       key.mv_size = sizeof(f->diskid);
+       if (res = mdb_del(tx, db->file_by_disk, &key, &data))
+               goto fail;
+
+       // - by title
+       if (f->title) {
+               key.mv_data = f->title;
+               key.mv_size = strlen(f->title);
+               if (res = mdb_del(tx, db->file_by_title, &key, &data))
+                       goto fail;
+       }
+
+       // - by artist
+       if (f->artist) {
+               key.mv_data = f->artist;
+               key.mv_size = strlen(f->artist);
+               if (res = mdb_del(tx, db->file_by_artist, &key, &data))
+                       goto fail;
+       }
+
+       // Remove file
+       key.mv_data = &f->id;
+       key.mv_size = sizeof(f->id);
+       if (res = mdb_del(tx, db->file, &key, NULL))
+               goto fail;
+
+       mdb_txn_commit(tx);
+       return res;
+
+ fail:
+       mdb_txn_abort(tx);
+       return res;
+}
+
+int dbstrcmp(const char *a, const char *b) {
+       if (a == NULL) {
+               if (b == NULL)
+                       return 0;
+               else
+                       return -1;
+       } else if (b == NULL)
+               return 1;
+
+       return strcmp(a, b);
+}
+
+// update o with any changed values
+int dbfile_update(dbtxn *txn, dbindex *db, dbfile *o, dbfile *f) {
+       MDB_txn *tx;
+       MDB_val key, data;
+       int res;
+
+       mdb_txn_begin(db->env, txn, 0, &tx);
+
+       // Update secondary keys
+       data.mv_data = &f->id;
+       data.mv_size = sizeof(f->id);
+
+       // - path can't change
+       // - diskid can't change
+       // - artist
+       if (dbstrcmp(o->artist, f->artist)) {
+               if (o->artist) {
+                       key.mv_data = o->artist;
+                       key.mv_size = strlen(o->artist);
+                       if (res = mdb_del(tx, db->file_by_artist, &key, &data))
+                               goto fail;
+               }
+
+               if (f->artist) {
+                       key.mv_data = f->artist;
+                       key.mv_size = strlen(f->artist);
+                       if (res = mdb_put(tx, db->file_by_artist, &key, &data, 0))
+                               goto fail;
+               }
+       }
+       // - title
+       if (dbstrcmp(o->title, f->title)) {
+               if (o->title) {
+                       key.mv_data = o->title;
+                       key.mv_size = strlen(o->title);
+                       if (res = mdb_del(tx, db->file_by_title, &key, &data))
+                               goto fail;
+               }
+
+               if (f->title) {
+                       key.mv_data = f->title;
+                       key.mv_size = strlen(f->title);
+                       if (res = mdb_put(tx, db->file_by_title, &key, &data, 0))
+                               goto fail;
+               }
+       }
+
+       f->id = o->id;
+       key.mv_data = &f->id;
+       key.mv_size = sizeof(f->id);
+
+       data.mv_size = ez_blob_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);
+
+       return mdb_txn_commit(tx);
+
+ fail:
+       mdb_txn_abort(tx);
+       return res;
+}
+
+int dbfile_add(MDB_txn *txn, dbindex *db, dbfile *f) {
+       MDB_txn *tx;
+       MDB_val key, data;
+       int res;
+
+       mdb_txn_begin(db->env, txn, 0, &tx);
+
+       // Check foreign constraints
+       key.mv_data = &f->diskid;
+       key.mv_size = sizeof(f->diskid);
+       res = mdb_get(tx, db->disk, &key, &data);
+       if (res == MDB_NOTFOUND) {
+               mdb_txn_abort(tx);
+               fprintf(stderr, "FOREIGN KEY: file with unknown disk\n");
+               return res;
+       }
+
+       // Store file
+       f->id = file_next_id(db);
+       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);
+       }
+
+       if (res != 0) {
+               printf("db put file fail: %s\n", mdb_strerror(res));
+               goto fail;
+       }
+
+       // Store secondary keys
+       data.mv_data = &f->id;
+       data.mv_size = sizeof(f->id);
+
+       // - by disk+path (and unique constraint)
+       char *dpath = dbfile_path(f);
+
+       key.mv_data = dpath;
+       key.mv_size = strlen(dpath);
+       res = mdb_put(tx, db->file_by_path, &key, &data, MDB_NOOVERWRITE);
+       free(dpath);
+       if (res) {
+               fprintf(stderr, "UNIQUE: path on this disk exists\n");
+               goto fail;
+       }
+
+       // - by diskid
+       key.mv_data = &f->diskid;
+       key.mv_size = sizeof(f->diskid);
+       if (res = mdb_put(tx, db->file_by_disk, &key, &data, 0))
+               goto fail;
+
+       // - by title
+       if (f->title) {
+               key.mv_data = f->title;
+               key.mv_size = strlen(f->title);
+               if (res = mdb_put(tx, db->file_by_title, &key, &data, 0))
+                       goto fail;
+       }
+
+       // - by artist
+       if (f->artist) {
+               key.mv_data = f->artist;
+               key.mv_size = strlen(f->artist);
+               if (res = mdb_put(tx, db->file_by_artist, &key, &data, 0))
+                       goto fail;
+       }
+
+       return mdb_txn_commit(tx);
+
+ fail:
+       mdb_txn_abort(tx);
+       return res;
+}
+
+
+// TODO: this can be made generic for other indices
+struct dbscan {
+       dbindex *db;
+       MDB_cursor *cursor;
+       MDB_val key, data;
+       int keyval;
+       int index;
+       int count;
+};
+
+dbscan *dbfile_scan_disk(dbtxn *tx, dbindex *db, int diskid) {
+       dbscan *scan = malloc(sizeof(*scan));
+       int res;
+
+       scan->db = db;
+       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, db->file_by_disk, &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;
+}
+
+uint32_t dbfile_scan_next(dbscan *scan) {
+       int res = 0;
+
+       while (scan->count > 0) {
+               if (scan->index < scan->count)
+                       return ((int *)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(int);
+               scan->index = 0;
+       }
+
+       return ~0;
+ fail:
+       if (res != MDB_NOTFOUND)
+               fprintf(stderr, "db scan fail: %s\n", mdb_strerror(res));
+       return ~0;
+}
+
+void dbfile_scan_close(dbscan *scan) {
+       if (scan->cursor)
+               mdb_cursor_close(scan->cursor);
+       free(scan);
+}
+
+
+/**
+ * Create a newly shuffled playlist.
+ */
+void dbshuffle_init(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);
+
+       // 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);
+
+       // now write them randomly
+       mdb_drop(tx, db->shuffle, 0);
+       mdb_drop(tx, db->shuffle_by_file, 0);
+
+       for (int i=0;i<count;i++) {
+               int j = random() % (count-i);
+               uint32_t seq = i + 1;
+               MDB_val key, data;
+
+               fid = fids[i+j];
+               fids[i+j] = fids[i];
+
+               key.mv_size = sizeof(int);
+               key.mv_data = &seq;
+               data.mv_size = sizeof(int);
+               data.mv_data = &fid;
+
+               res = mdb_put(tx, db->shuffle, &key, &data, MDB_NOOVERWRITE);
+               res = mdb_put(tx, db->shuffle_by_file, &data, &key, MDB_NOOVERWRITE);
+       }
+       free(fids);
+
+       dbindex_commit(tx);
+}
+
+/*
+  Player support functions
+*/
+
+// A way to iterate through a lit of files, based on an index or something else
+struct dblist {
+};
+
+#include <sys/types.h>
+#include <sys/stat.h>
+#include <unistd.h>
+#include <dirent.h>
+
+/**
+ * Check if the disk is mounted.
+ *
+ * This is not generally absolutely reliable but is in the context of
+ * disk-monitor managing the mounts.
+ *
+ * It can be used for quickly discarding files that can't be mounted.
+ */
+int dbdisk_mounted(dbdisk *disk) {
+#if 0
+       // Check the device of the entries
+       char parent[strlen(disk->mount)+1];
+       char *slash = strrchr(parent, '/');
+
+       if (slash) {
+               struct stat pst, mst;
+
+               *slash = 0;
+
+               // Check if it's already mounted
+               return (stat(disk->mount, &mst) == 0
+                       && stat(parent, &pst) == 0
+                       && mst.st_dev != pst.st_rdev);
+       }
+
+       return 0;
+#else
+       // See if the directory is empty
+       DIR *d = opendir(disk->mount);
+       int entries = 0;
+
+       if (d) {
+               struct dirent *de;
+
+               while (de = readdir(d)) {
+                       if (strcmp(de->d_name, ".") == 0
+                           || strcmp(de->d_name, "..") == 0)
+                               continue;
+                       entries++;
+               }
+               closedir(d);
+       }
+
+       return entries > 0;
+#endif
+}
+
+
+/**
+ * Find the next file ... in some order or some index.
+ *
+ * This is intended to be called occasionally and not to scan the db.
+ *
+ * Currently it goes by the path index.
+ *
+ * should it be cyclic?
+ *
+ * FIXME: ignore files without audio! (duration==0)
+ *
+ * @param f file, use NULL to start at the beginning.
+ */
+static int dbfile_iterate(dbindex *db, dbfile **fp, char **pathp, int first, int next) {
+       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_path, &cursor)))
+               goto fail;
+
+       /*
+         Scan based on filename order
+        */
+       char *keyval = *fp ? dbfile_path(*fp) : NULL;
+       dbdisk *disk = *fp ? dbdisk_get(tx, db, (*fp)->diskid) : NULL;
+       int mounted = *fp ? dbdisk_mounted(disk) : 0;
+
+       dbfile_free(*fp);
+       *fp = NULL;
+       free(*pathp);
+       *pathp = NULL;
+
+       if (keyval) {
+               key.mv_data = keyval;
+               key.mv_size = strlen(keyval);
+
+               res = mdb_cursor_get(cursor, &key, &data, MDB_SET);
+               res = mdb_cursor_get(cursor, &key, &data, next);
+       } else {
+               res = mdb_cursor_get(cursor, &key, &data, first);
+       }
+
+       while (file == NULL && res == 0) {
+               file = primary_get_decode(tx, db, DBFILE_DESC, &data, db->file);
+               if (file) {
+                       int keep;
+
+                       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);
+
+                               keep = lstat(path, &st) == 0 && S_ISREG(st.st_mode);
+                               if (keep) {
+                                       *pathp = strdup(path);
+                                       *fp = file;
+                               }
+                       }
+
+                       if (!keep) {
+                               dbfile_free(file);
+                               file = NULL;
+                       }
+               }
+               if (file == NULL)
+                       res = mdb_cursor_get(cursor, &key, &data, next);
+       }
+
+       free(keyval);
+       dbdisk_free(disk);
+
+       mdb_cursor_close(cursor);
+       mdb_txn_commit(tx);
+
+       return res;
+ fail:
+       mdb_txn_abort(tx);
+
+       return res;
+}
+
+/**
+ * Find the next file ... in some order or some index.
+ *
+ * This is intended to be called occasionally and not to scan the db.
+ *
+ * Currently it goes by the path index.
+ *
+ * should it be cyclic?
+ *
+ * @param f file, use NULL to start at the beginning.
+ */
+int dbfile_next(dbindex *db, dbfile **f, char **fpath) {
+       return dbfile_iterate(db, f, fpath, MDB_FIRST, MDB_NEXT);
+}
+
+int dbfile_prev(dbindex *db, dbfile **f, char **fpath) {
+       return dbfile_iterate(db, f, fpath, MDB_LAST, MDB_PREV);
+}
+
+// this is a bit different to the by-path shuffle, since the path is used again as output rather
+// than the iteration key
+static int dbfile_iterate_shuffle(dbindex *db, dbfile **fp, char **pathp, int first, int next) {
+       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->shuffle, &cursor)))
+       //if ((res = mdb_cursor_open(tx, db->shuffle_by_file, &cursor)))
+               goto fail;
+#if 0
+       printf("scan shuffle\n");
+       res = mdb_cursor_get(cursor, &key, &data, first);
+       while (res == 0) {
+               printf(" seq %d val %d\n", *((int *)key.mv_data), *((int *)data.mv_data));
+               res = mdb_cursor_get(cursor, &key, &data, next);
+       }
+
+       return 0;
+#endif
+
+       /*
+         Scan based on shuffle order
+        */
+       int *keyval = *fp ? &((*fp)->id) : NULL;
+       dbdisk *disk = *fp ? dbdisk_get(tx, db, (*fp)->diskid) : NULL;
+       int mounted = *fp ? dbdisk_mounted(disk) : 0;
+
+       printf("shuffle next, fid=%d\n", keyval ? *keyval : -1);
+
+       if (keyval) {
+               data.mv_data = keyval;
+               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));
+               if (res == MDB_NOTFOUND) {
+                       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);
+
+               res = mdb_cursor_get(cursor, &key, &data, next);
+               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);
+       }
+
+       dbfile_free(*fp);
+       *fp = NULL;
+       free(*pathp);
+       *pathp = NULL;
+
+       while (file == NULL && res == 0) {
+               file = primary_get_decode(tx, db, DBFILE_DESC, &data, db->file);
+               if (file) {
+                       int keep;
+
+                       printf("loaded: %d[%d] %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);
+                       }
+                       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);
+
+                               keep = lstat(path, &st) == 0 && S_ISREG(st.st_mode);
+                               if (keep) {
+                                       *pathp = strdup(path);
+                                       *fp = file;
+                               }
+                       }
+
+                       if (!keep) {
+                               dbfile_free(file);
+                               file = NULL;
+                       }
+               }
+               if (file == NULL)
+                       res = mdb_cursor_get(cursor, &key, &data, next);
+       }
+
+       //free(keyval);
+       dbdisk_free(disk);
+
+       mdb_cursor_close(cursor);
+       mdb_txn_commit(tx);
+
+       printf("laoded fid=%d\n", file->id);
+
+       return res;
+ fail:
+       mdb_txn_abort(tx);
+
+       return res;
+}
+
+int dbfile_next_shuffle(dbindex *db, dbfile **f, char **fpath) {
+       return dbfile_iterate_shuffle(db, f, fpath, MDB_FIRST, MDB_NEXT);
+}
+
+int dbfile_prev_shuffle(dbindex *db, dbfile **f, char **fpath) {
+       return dbfile_iterate_shuffle(db, f, fpath, MDB_LAST, MDB_PREV);
+}
+
+/*
+         Scan based on secondary index.
+
+       key.mv_data = &f->diskid;
+       key.mv_size = sizeof(f->diskid);
+
+       data.mv_data = &f->id;
+       data.mv_size = sizeof(f->id);
+
+       //res = mdb_cursor_get(scan->cursor, &scan->key, &scan->data, MDB_GET_BOTH);
+
+*/
diff --git a/dbindex.h b/dbindex.h
new file mode 100644 (file)
index 0000000..21a5e56
--- /dev/null
+++ b/dbindex.h
@@ -0,0 +1,70 @@
+
+#include "ez-blob.h"
+
+typedef struct dbdisk dbdisk;
+
+struct dbdisk {
+       int id;
+       char *uuid;
+       char *label;
+       char *type;
+       char *mount;            // last mount point
+};
+
+typedef struct dbfile dbfile;
+
+struct dbfile {
+       int id;
+       int diskid;             // disk it belongs to
+
+       uint64_t size;          // st_size
+       uint64_t mtime;         // st_mtime
+
+       uint64_t duration;      // duration from ffmpeg (in uS)
+
+       char *path;             // relative path from disk root (with leading /)
+
+       char *title;            // music title
+       char *artist;           // music artist
+};
+
+typedef struct dbindex dbindex;
+typedef struct MDB_txn dbtxn;
+typedef struct dbscan dbscan;
+
+dbindex *dbindex_open(const char *path);
+void dbindex_close(dbindex *db);
+
+dbtxn *dbindex_begin(dbindex *db, dbtxn *txn, int readonly);
+void dbindex_commit(dbtxn *tx);
+void dbindex_abort(dbtxn *tx);
+
+dbdisk *dbdisk_get_uuid(dbtxn *tx, dbindex *db, const char *uuid);
+void dbdisk_free(dbdisk *d);
+int     dbdisk_add(dbtxn *txn, dbindex *db, dbdisk *d);
+
+dbfile *dbfile_get(dbtxn *tx, dbindex *db, int fileid);
+dbfile *dbfile_get_path(dbtxn *tx, dbindex *db, int diskid, const char *path);
+void dbfile_free(dbfile *f);
+
+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);
+
+dbscan *dbfile_scan_disk(dbtxn *tx, dbindex *db, int diskid);
+uint32_t dbfile_scan_next(dbscan *scan);
+void dbfile_scan_close(dbscan *scan);
+
+extern ez_blob_desc DBDISK_DESC[];
+extern ez_blob_desc DBFILE_DESC[];
+
+/* Player support */
+int dbfile_next(dbindex *db, dbfile **f, char **fpath);
+int dbfile_prev(dbindex *db, dbfile **f, char **fpath);
+
+void dbshuffle_init(dbindex *db);
+
+int dbfile_next_shuffle(dbindex *db, dbfile **f, char **fpath);
+int dbfile_prev_shuffle(dbindex *db, dbfile **f, char **fpath);
+
+#define MAIN_INDEX "/home/notzed/playerz.db"
diff --git a/disk-indexer.c b/disk-indexer.c
new file mode 100644 (file)
index 0000000..84987e2
--- /dev/null
@@ -0,0 +1,640 @@
+
+#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 <libavformat/avformat.h>
+
+#include <regex.h>
+
+#include "ez-list.h"
+#include "ez-set.h"
+#include "ez-bitset.h"
+
+#include "dbindex.h"
+
+#include "notify.h"
+
+struct indexer {
+
+       char *root;
+
+       dbindex *db;
+       dbdisk *disk;
+
+       dbtxn *tx; // global transaction for insert 1 disk
+
+       int isnew;
+
+       ez_list queue;
+       struct dirent *entry;
+
+       ez_bitset *existing;
+
+       // files to care about
+       regex_t match;
+
+       // some stats
+       struct timeval start;
+       int added;
+       int removed;
+       int updated;
+       int unchanged;
+};
+
+
+/*
+
+  Take a filename and generate some indexing info.
+
+ */
+
+struct AVDictionary {
+    int count;
+    AVDictionaryEntry *elems;
+};
+
+const char *pz_dict_get(AVDictionary *dict, const char *key, const char *def) {
+       if (dict) {
+               for (int i=0;i<dict->count;i++)
+                       if (!strcmp(key, dict->elems[i].key))
+                               return dict->elems[i].value;
+       }
+       return def;
+}
+
+static dbfile *scan_info(AVFormatContext *ic) {
+       int audio = 0;
+
+       for (int i=0;!audio && i<ic->nb_streams;i++) {
+               AVStream *s = ic->streams[i];
+               AVCodecContext *c = s->codec;
+
+               if (c->codec_type == AVMEDIA_TYPE_AUDIO) {
+                       //printf("    audio: %s %dHz x%d %ld b/s\n",
+                       //     avcodec_get_name(c->codec_id),
+                       //     c->sample_rate,
+                       //     c->channels,
+                       //     c->bit_rate);
+                       audio = 1;
+               }
+       }
+
+       dbfile *f = NULL;
+
+       if (audio) {
+               f = malloc(sizeof(*f));
+
+               f->id = 0;
+               f->artist = strdup(pz_dict_get(ic->metadata, "artist", "Unknown"));
+               f->title = strdup(pz_dict_get(ic->metadata, "title", "Unknown"));
+
+               //printf("    title: %s\n", f->title);
+               //printf("   artist: %s\n", f->artist);
+
+               if (ic->duration != AV_NOPTS_VALUE) {
+                       //int64_t s = ic->duration / AV_TIME_BASE;
+                       //int64_t u = ic->duration % AV_TIME_BASE;
+
+                       // AV_TIME_BASE = 1 000 000, in us
+                       f->duration = ic->duration;
+
+                       //printf(" duration: %02d:%02d:%02d.%03d\n",
+                       //      (int)(s / 60 / 60),
+                       //     (int)(s / 60 % 60),
+                       //     (int)(s % 60),
+                       //     (int)((u + 500) / 1000));
+               } else {
+                       f->duration = 0;
+               }
+       }
+
+       return f;
+}
+
+static dbfile *scan_file(const char *path) {
+       AVFormatContext *ic = NULL;
+       int res;
+       dbfile *f;
+
+       ic = avformat_alloc_context();
+       res = avformat_open_input(&ic, path, NULL, NULL);
+       if (res != 0)
+               return NULL;
+
+       res = avformat_find_stream_info(ic, NULL);
+
+       //printf("%s\n", path);
+       f = scan_info(ic);
+       //printf("\n");
+       //av_dump_format(ic, 0, path, 0);
+
+       avformat_close_input(&ic);
+
+       return f;
+}
+
+
+
+
+
+
+
+
+struct dir_node {
+       ez_node ln;
+       char path[0];
+};
+
+static struct dir_node *dir_new(const char *path) {
+       struct dir_node *n = malloc(sizeof(*n) + strlen(path)+1);
+
+       strcpy(n->path, path);
+
+       return n;
+}
+
+static void dir_free(struct dir_node *d) {
+       free(d);
+}
+
+void indexer_destroy(struct indexer *ix);
+
+int indexer_init(struct indexer *ix, dbindex *db, const char *path, const char *uuid) {
+       int res;
+
+       memset(ix, 0, sizeof(*ix));
+
+       ez_list_init(&ix->queue);
+       ez_list_addtail(&ix->queue, dir_new(path));
+
+       // find out how big dirent needs to be
+       int name_max = pathconf(path, _PC_NAME_MAX);
+
+       name_max = name_max == -1 ? 256 : name_max;
+       ix->entry = malloc(sizeof(struct dirent) - sizeof(ix->entry->d_name) + name_max + 1);
+
+       ix->root = strdup(path);
+
+       res = regcomp(&ix->match, "\\.(mp3|avi|mpeg|mp2|mpg|mp4|mov)$", REG_ICASE | REG_NOSUB | REG_EXTENDED);
+       if (res != 0) {
+               perror("regcomp");
+               exit(1);
+       }
+
+       // Lookup this disk
+       ix->db = db;
+       ix->tx = dbindex_begin(db, NULL, 0);
+
+       ix->disk = dbdisk_get_uuid(ix->tx, db, uuid);
+       if (!ix->disk) {
+               dbdisk *disk;
+
+               disk = malloc(sizeof(*ix->disk));
+
+               disk->id = 0;
+               disk->uuid = strdup(uuid);
+               disk->label = strdup("unknown");
+               disk->type = strdup("jfs");
+               disk->mount = strdup(path);
+
+               res = dbdisk_add(ix->tx, db, disk);
+               if (res != 0)
+                       goto fail;
+               printf("%8d : add new disk %s\n", disk->id,  uuid);
+               ix->disk = disk;
+       } else {
+               int count = 0;
+               dbtxn *tx = dbindex_begin(db, NULL, 1);
+
+               if (tx) {
+                       dbscan *scan = dbfile_scan_disk(tx, db, ix->disk->id);
+                       uint32_t fid;
+
+                       if (scan) {
+                               ix->existing = ez_bitset_new();
+
+                               while ((fid = dbfile_scan_next(scan)) != ~0) {
+                                       count++;
+                                       ez_bitset_set(ix->existing, fid, 1);
+                               }
+                               dbfile_scan_close(scan);
+                       }
+                       dbindex_commit(tx);
+                       printf("bitset count %d actual count %d\n", ez_bitset_card(ix->existing), count);
+               }
+
+               printf("%8d : add old disk %s (%d existing files)\n", ix->disk->id,  uuid, count);
+       }
+
+       // FIXME: error handling
+
+       return 0;
+
+ fail:
+       if (ix->tx)
+               dbindex_abort(ix->tx);
+
+       indexer_destroy(ix);
+       return res;
+}
+
+void indexer_destroy(struct indexer *ix) {
+       if (ix->existing)
+               ez_bitset_free(ix->existing);
+
+       dbdisk_free(ix->disk);
+
+       struct dir_node *scan;
+       while ((scan = ez_list_remhead(&ix->queue)))
+               dir_free(scan);
+
+       free(ix->entry);
+       free(ix->root);
+       regfree(&ix->match);
+}
+
+/*
+ * Indexer "update" mechanism
+ *
+ * just don't bother.
+ *
+
+  0. must have per-disk db
+
+  1. if the same disk is (re)inserted
+  2. scan files, and copy any existing db entries to new db if the timestamps match
+  3. otherwise create new entries
+  4. delete old db
+
+need to scan twice?
+
+ pass 1: copy any existing entries, updating as required (retain ids)
+ pass 2: create any entries which didn't exist with new id's
+
+single pass:
+
+ nextid is based on highest existing id, just use that for new entries
+
+
+in-place:
+
+ create set of all existing ids in db  (bitmask or something)
+ scan and update, remove any visited from set
+ remove all those left in the set
+
+ -> needs to fuck around with updating secondary indices too!
+
+ what about garbage accumulation?
+
+ */
+
+// Add or update file
+int indexer_add_file(struct indexer *ix, struct stat *st, const char *filepath, const char *diskpath) {
+       dbfile *o = NULL, *f;
+
+       // If already there, and unchanged, do nothing
+       if (ix->existing) {
+               o = dbfile_get_path(ix->tx, ix->db, ix->disk->id, diskpath);
+
+               if (o) {
+                       ez_bitset_set(ix->existing, o->id, 0);
+                       if (o->size == st->st_size && o->mtime == st->st_mtime) {
+                               dbfile_free(o);
+                               ix->unchanged += 1;
+                               return 0;
+                       }
+               }
+       }
+
+       // Get or update metadata
+       // If it isn't an audio file, store it anyway to avoid a re-scan later
+       f = scan_file(filepath);
+       if (!f) {
+               f = malloc(sizeof(*f));
+               memset(f, 0, sizeof(*f));
+       }
+
+       f->path = strdup(diskpath);
+       f->diskid = ix->disk->id;
+       f->size = st->st_size;
+       f->mtime = st->st_mtime;
+
+       if (o) {
+               dbfile_update(ix->tx, ix->db, o, f);
+               ix->updated += 1;
+       } else {
+               dbfile_add(ix->tx, ix->db, f);
+               ix->added += 1;
+       }
+
+       dbfile_free(f);
+       dbfile_free(o);
+
+       return 0;
+}
+
+static void indexer_info(struct indexer *ix) {
+       struct timeval now;
+       time_t secs;
+
+       gettimeofday(&now, NULL);
+       secs = now.tv_sec - ix->start.tv_sec;
+       printf(" %02zd:%02zd:%02zd add: %6d rem: %6d same: %6d\n",
+              secs / (60 * 60),
+              (secs / 60) % 60,
+              secs % 60,
+              ix->added, ix->removed, ix->unchanged);
+}
+
+// scan a disk
+int indexer_scan(struct indexer *ix) {
+       struct dir_node *scan = NULL;
+       int count = 0, res = 0;
+
+       gettimeofday(&ix->start, NULL);
+
+       while ((scan = ez_list_remhead(&ix->queue))) {
+               //printf("scan %s\n", scan->path);
+
+               DIR *d = opendir(scan->path);
+               if (d) {
+                       struct dirent *ep;
+                       int dots = 0;
+
+                       while (readdir_r(d, ix->entry, &ep) == 0 && ep) {
+                               if (strcmp(ep->d_name, ".") == 0
+                                   || strcmp(ep->d_name, "..") == 0) {
+                                       // Hack: remove a usb disk while scanning can cause this to loop
+                                       dots++;
+                                       if (dots > 10)
+                                               break;
+                                       continue;
+                               }
+
+                               //usleep(5000);
+
+                               char name[strlen(ep->d_name) + strlen(scan->path) + 2];
+                               struct stat st;
+
+                               sprintf(name, "%s/%s", scan->path, ep->d_name);
+
+                               //printf(" %s\n", name);
+
+                               // FIXME: use fstatat?
+                               if ((res = lstat(name, &st)) == 0) {
+                                       if (S_ISREG(st.st_mode)) {
+                                               if (regexec(&ix->match, ep->d_name, 0, NULL, 0) == 0) {
+                                                       indexer_add_file(ix, &st, name, name + strlen(ix->root));
+                                                       count++;
+                                                       if ((count % 1000) == 0)
+                                                               indexer_info(ix);
+                                               }
+                                       } else if (S_ISDIR(st.st_mode)) {
+                                               ez_list_addtail(&ix->queue, dir_new(name));
+                                       } else if (S_ISLNK(st.st_mode)) {
+                                               // softlinks are ignored for simplicity reasons
+                                       }
+                               } else {
+                                       perror("lstat");
+                                       if (errno == EIO)
+                                               goto fail;
+                               }
+                       }
+                       closedir(d);
+               } else {
+                       printf("open '%s': %s\n", scan->path, strerror(errno));
+                       if (errno == EIO)
+                               goto fail;
+                       if (errno == ENOENT)
+                               goto fail;
+               }
+               dir_free(scan);
+       }
+
+       if (ix->existing) {
+               printf("stale count: %d\n", ez_bitset_card(ix->existing));
+               ez_bitset_scan scan = { 0 };
+               uint32_t bit;
+
+               for (bit = ez_bitset_scan_init(ix->existing, &scan); bit != ~0; bit = ez_bitset_scan_next(&scan)) {
+                       dbfile *f = dbfile_get(ix->tx, ix->db, bit);
+
+                       if (f) {
+                               printf(" stale %s\n", f->path);
+
+                               dbfile_del(ix->tx, ix->db, f);
+                               ix->removed += 1;
+
+                               dbfile_free(f);
+                       } else {
+                               printf(" ** db corrupt missing file: %d\n", bit);
+                       }
+               }
+       }
+
+       dbindex_commit(ix->tx);
+
+       indexer_info(ix);
+
+       return count;
+
+ fail:
+       if (scan)
+               dir_free(scan);
+
+       dbindex_abort(ix->tx);
+
+       return -1;
+}
+
+static void indexer(void) {
+       dbindex *db = dbindex_open(MAIN_INDEX);
+       notify_t q;
+       int quit = 0;
+
+       if (!db)
+               exit(1);
+
+       q = notify_reader_new(NOTIFY_INDEXER);
+       while (!quit) {
+               dbdisk *disk;
+               unsigned int pri;
+               enum notify_action action;
+
+               printf("Indexer: idle.\n");
+
+               disk = notify_msg_receive(q, &action, &pri);
+
+               switch (action) {
+               case NOTIFY_DISK_ADD: {
+                       struct indexer ix;
+                       int res;
+
+                       printf("Indexer: scanning '%s'.\n", disk->mount);
+
+                       res = indexer_init(&ix, db, disk->mount, disk->uuid);
+                       if (res == 0) {
+                               res = indexer_scan(&ix);
+
+                               if (res >= 0) {
+                                       printf("Scan complete:\n");
+                                       printf(" added: %d\n", ix.added);
+                                       printf(" removed: %d\n", ix.removed);
+                                       printf(" updated: %d\n", ix.updated);
+                                       printf(" unchanged: %d\n", ix.unchanged);
+                               } else {
+                                       printf("Scan aborted\n");
+                               }
+
+                               indexer_destroy(&ix);
+                       }
+                       break;
+               }
+               case NOTIFY_DISK_REMOVE:
+                       // don't really care on this, indexing should fail if it was busy
+                       break;
+               case NOTIFY_SHUFFLE:
+                       printf("Creating shuffled playlist\n");
+                       dbshuffle_init(db);
+                       printf("Shuffle complete.\n");
+                       break;
+               case NOTIFY_QUIT:
+                       quit = 1;
+                       break;
+               default:
+                       break;
+               }
+               notify_msg_free(action, disk);
+       }
+
+       printf("Indexer: quit.\n");
+
+       dbindex_close(db);
+}
+
+void check(void) {
+       dbindex *db = dbindex_open(MAIN_INDEX);
+
+       // Check indices
+       printf("Check file-by-diskid index\n");
+       dbtxn *tx = dbindex_begin(db, NULL, 1);
+       dbscan *scan = dbfile_scan_disk(tx, db, -1);
+       uint32_t fid;
+       int count =0;
+
+       while ((fid = dbfile_scan_next(scan)) != ~0) {
+               dbfile *f = dbfile_get(tx, db, fid);
+               if (f == NULL) {
+                       printf(" %d missing\n", fid);
+               } else {
+                       printf(" in %d\n", fid);
+               }
+               dbfile_free(f);
+               count++;
+       }
+       printf("total %d\n", count);
+       dbfile_scan_close(scan);
+
+       dbindex_close(db);
+}
+
+int main(int argc, char **argv) {
+       av_log_set_level(AV_LOG_ERROR);
+
+       //avcodec_register_all();
+       //av_register_all();
+       //avformat_network_init();
+
+       mkdir(MAIN_INDEX, 0700);
+
+       if (argc > 1) {
+               if (strcmp(argv[1], "check") == 0)
+                       check();
+               else {
+                       notify_t q = notify_writer_new(NOTIFY_INDEXER);
+
+                       if (q) {
+                               if (strcmp(argv[1], "quit") == 0) {
+                                       notify_msg_send(q, NOTIFY_QUIT, 0, 0);
+                               } else if (strcmp(argv[1], "shuffle") == 0) {
+                                       notify_msg_send(q, NOTIFY_SHUFFLE, 0, 0);
+                               } else if (strcmp(argv[1], "add") == 0 && argc == 4) {
+                                       dbdisk disk = {
+                                               .uuid = argv[2],
+                                               .label = "system",
+                                               .type = "system",
+                                               .mount = argv[3]
+                                       };
+                                       notify_msg_send(q, NOTIFY_DISK_ADD, 0, &disk);
+                               }
+                               notify_close(q);
+                       }
+               }
+       } else
+               indexer();
+
+       return 0;
+
+
+       char *uuid = argc > 1 ? argv[1] : "some-disk";
+
+
+       dbindex *db = dbindex_open("/home/notzed/playerz.db");
+       int res;
+
+       if (!db) {
+               return 1;
+       }
+
+       if (0) {
+               dbtxn *tx = dbindex_begin(db, NULL, 1);
+               int diskid = 4;
+               dbscan *scan = dbfile_scan_disk(tx, db, diskid);
+               int count = 0;
+               uint32_t fid;
+
+               while ((fid = dbfile_scan_next(scan)) != ~0) {
+                       count++;
+               }
+               printf(" %d files on disk %d\n", count, diskid);
+
+               dbindex_commit(tx);
+               dbindex_close(db);
+               return 0;
+       }
+
+       struct indexer ix;
+
+       res = indexer_init(&ix, db, "/data/hd4/Music", uuid);
+       if (res == 0) {
+
+               //for (int i=1;i<argc;i++)
+               //      scan_file(argv[i]);
+
+
+               res = indexer_scan(&ix);
+
+               if (res == 0) {
+                       printf("Scan complete:\n");
+                       printf(" added: %d\n", ix.added);
+                       printf(" removed: %d\n", ix.removed);
+                       printf(" updated: %d\n", ix.updated);
+                       printf(" unchanged: %d\n", ix.unchanged);
+               } else {
+                       printf("Scan aborted\n");
+               }
+
+               indexer_destroy(&ix);
+       }
+
+       dbindex_close(db);
+
+       return 0;
+}
diff --git a/disk-monitor.c b/disk-monitor.c
new file mode 100644 (file)
index 0000000..1911395
--- /dev/null
@@ -0,0 +1,437 @@
+
+/*
+  disk-monitor
+
+    detects drive changes
+    mounts/umounts disk
+    sends events to listeners
+
+
+
+    
+FIXME:
+
+ make a setuid-root binary
+ run as unprivileged user
+
+
+ wrap in as-root {
+  ... 
+  mount/unmount
+ }
+
+ can this be wrapped in as-root? {
+   ... it only needs disk group?
+
+   p = blkid_new_probe_from_filename(dev);
+ }
+
+setup {
+ uid_t uid, euid;
+
+ uid = getuid();   // unpriv user
+ euid = geteuid(); // priv user
+}
+
+drop privs {
+ seteuid(uid);
+}
+
+raise privs {
+ seteuid(euid);
+}
+
+check returns.
+
+
+
+also perhaps setresuid() since that isn't so obtuse.
+
+euid = 0
+uid = 1000 or whatever
+
+drop {
+  setresuid(uid, uid, 0);
+}
+raise {
+  setresuid(uid, euid, 0);
+}
+
+
+or use capabilitties
+
+CAP_SETUID    - arbitrary setuid
+CAP_SYS_ADMIN - mount disks
+
+the disk can be read with the disk group - nope, thats a udevd thing
+
+the keyboard can be read with the input group.
+ - but udev sets up the usb thing
+ */
+
+#include <sys/types.h>
+#include <sys/socket.h>
+#include <sys/stat.h>
+#include <linux/netlink.h>
+#include <unistd.h>
+#include <dirent.h>
+#include <fcntl.h>
+
+#include <blkid/blkid.h>
+#include <sys/mount.h>
+
+#include <string.h>
+#include <stdlib.h>
+#include <stdio.h>
+#include <ctype.h>
+
+#include "dbindex.h"
+#include "ez-set.h"
+#include "ez-list.h"
+#include "notify.h"
+
+struct monitor {
+       ez_set *mounts;
+
+       size_t mount_base_size;
+       char *mount_base;
+
+       notify_t indexer;
+};
+
+#define MOUNT_BASE "/run/pzdisk"
+
+struct mdisk {
+       ez_node en;
+
+       char *dev;
+       
+       struct dbdisk disk;
+};
+
+static unsigned int mdisk_hash(const void *mp) {
+       const struct mdisk *m = mp;
+
+       return ez_hash_string(m->dev);
+}
+
+static int mdisk_equals(const void *ap, const void *bp) {
+       const struct mdisk *a = ap, *b = bp;
+
+       return strcmp(a->dev, b->dev) == 0;
+}
+
+static void mdisk_free(void *mp) {
+       struct mdisk *m = mp;
+       
+       free(m->dev);
+       
+       free(m->disk.uuid);
+       free(m->disk.label);
+       free(m->disk.type);
+       free(m->disk.mount);
+       
+       free(m);
+}
+
+static void print_values(const char *dev, blkid_probe p) {
+       int n = blkid_probe_numof_values(p), i;
+
+       printf("blkid probe %s\n", dev);
+       for (i=0;i<n;i++) {
+               const char *name, *value;
+               size_t size;
+               
+               blkid_probe_get_value(p, i, &name, &value, &size);
+               printf(" %s: ", name);
+               fwrite(value, size, 1, stdout);
+               printf("\n");
+       }
+}
+
+void partition_notify(struct monitor *m, enum notify_action action, struct mdisk *md) {
+       int res = notify_msg_send(m->indexer, action, 0, &md->disk);
+
+       if (res)
+               perror("notify indexer");
+}
+
+/*
+ */
+
+static void partition_add(struct monitor *m, const char *dev) {
+       blkid_probe p = NULL;
+       int res;
+       
+       printf("Add partition: %s\n", dev);
+       
+       p = blkid_new_probe_from_filename(dev);
+       if (!p) {
+               perror("probe new");
+               return;
+       }
+
+       blkid_probe_enable_partitions(p, 0);
+       blkid_probe_enable_superblocks(p, 1);
+       
+       res = blkid_do_safeprobe(p);
+       if (res != 0)
+               goto fail;
+
+       print_values(dev, p);
+
+       int ptype, puuid, plabel;
+       const char *type, *uuid, *label;
+       size_t type_size, uuid_size, label_size;
+       
+       ptype = (blkid_probe_lookup_value(p, "TYPE", &type, &type_size) == 0);
+       puuid = (blkid_probe_lookup_value(p, "UUID", &uuid, &uuid_size) == 0);
+       plabel = (blkid_probe_lookup_value(p, "LABEL", &label, &label_size) == 0);
+       
+       if (ptype && puuid) {
+               char *mountp = malloc(m->mount_base_size + uuid_size + 2);
+
+               memcpy(mountp, m->mount_base, m->mount_base_size);
+               mountp[m->mount_base_size] = '/';
+               memcpy(mountp + m->mount_base_size+1, uuid, uuid_size);
+               mountp[m->mount_base_size+1+uuid_size] = 0;
+
+               struct stat dst, mst;
+
+               // Check if it's already mounted
+               if (stat(mountp, &mst) == 0
+                   && stat(dev, &dst) == 0
+                   && mst.st_dev == dst.st_rdev) {
+                       printf(" remount  %s at %s\n", dev, mountp);
+                       res = 0;
+               } else {
+                       printf(" mount   %s at %s\n", dev, mountp);
+                       res = mkdir(mountp, 0777);
+                       perror("mkdir");
+                       res = mount(dev, mountp, type, MS_NOEXEC | MS_RDONLY | MS_NOSUID, NULL);
+                       perror("mount");
+               }
+
+               if (res == 0) {
+                       struct mdisk *md = malloc(sizeof(*md));
+
+                       memset(md, 0, sizeof(*md));
+                       
+                       md->dev = strdup(dev);
+                       md->disk.uuid = strdup(uuid);
+                       md->disk.type = strdup(type);
+                       md->disk.label = plabel ? strdup(label) : NULL;
+                       md->disk.mount = mountp;
+
+                       partition_notify(m, NOTIFY_DISK_ADD, md);
+
+                       md = ez_set_put(m->mounts, md);
+                       if (md) {
+                               printf("mounted twice?\n");
+                               mdisk_free(md);
+                       }
+
+               } else {
+                       free(mountp);
+                       perror("mount");
+               }
+       } else {
+               printf("  nomount %s unknown partition type\n", dev);
+       }
+
+ fail:
+       blkid_free_probe(p);
+}
+
+static void partition_remove(struct monitor *m, const char *dev) {
+       struct mdisk mde = {
+               .dev = (char *)dev
+       };
+       struct mdisk *md;
+       int res;
+       
+       printf("Remove partition: %s\n", dev);
+
+       md = ez_set_remove(m->mounts, &mde);
+       if (md) {
+               partition_notify(m, NOTIFY_DISK_REMOVE, md);
+
+               res = umount2(md->disk.mount, MNT_DETACH);
+               
+               if (res == 0) {
+                       printf(" unmount %s @ %s\n", md->dev, md->disk.mount);
+               } else {
+                       perror("umount2");
+               }
+
+               mdisk_free(md);
+       } else {
+               printf("Unknown partition removed!\n");
+       }
+}
+
+struct oldnode {
+       ez_node ln;
+       
+       dev_t dev;
+       char *mount;
+};
+
+// scan to see if any partitions are already mounted
+static void partition_check(struct monitor *m) {
+       DIR *dir = opendir(m->mount_base);
+       
+       if (dir) {
+               struct stat dstat;
+               struct stat estat;
+               struct dirent *e;
+               int dfd = dirfd(dir);
+               ez_list old = EZ_INIT_LIST(old);
+
+               fstat(dfd, &dstat);
+               
+               while (e = readdir(dir)) {
+                       if (fstatat(dfd, e->d_name, &estat, AT_SYMLINK_NOFOLLOW) == 0
+                           && S_ISDIR(estat.st_mode)
+                           && dstat.st_dev != estat.st_dev) {
+                               struct oldnode *onode = malloc(sizeof(*onode));
+
+                               onode->mount = malloc(m->mount_base_size + strlen(e->d_name) + 2);
+                               sprintf(onode->mount, "%s/%s", m->mount_base, e->d_name);
+                               onode->dev = estat.st_dev;
+
+                               ez_list_addtail(&old, onode);
+                       }
+               }
+               
+               closedir(dir);
+
+               if (!ez_list_empty(&old)){
+                       dir = opendir("/dev");
+                       dfd = dirfd(dir);
+                       while (e = readdir(dir)) {
+                               if (fstatat(dfd, e->d_name, &estat, AT_SYMLINK_NOFOLLOW) == 0) {
+                                       // check if starts with sd?
+                                       struct oldnode *w, *n;
+
+                                       for (w = ez_list_head(&old), n = ez_node_succ(w);n;w=n,n=ez_node_succ(n)) {
+                                               if (w->dev == estat.st_rdev) {
+                                                       char devname[strlen("/dev/") + strlen(e->d_name) + 1];
+
+                                                       sprintf(devname, "/dev/%s", e->d_name);
+                                                       
+                                                       partition_add(m, devname);
+                                                       ez_node_remove(w);
+                                                       free(w->mount);
+                                                       free(w);
+                                               }
+                                       }
+                               }
+                       }
+                       {
+                               struct oldnode *w, *n;
+                               for (w = ez_list_head(&old), n = ez_node_succ(w);n;w=n,n=ez_node_succ(n)) {
+                                       printf("Old mount unknown device %s\n", w->mount);
+                                       ez_node_remove(w);
+                                       free(w->mount);
+                                       free(w);
+                               }
+                       }
+               }
+
+       }
+}
+
+/**
+ * Monitors the KOBJECT_UEVENT socket and checks for hot-plugging of drives.
+ *
+ * Drives are then mounted/unmounted as required.
+ *
+ * Notification events are sent to coincide with these operations.
+ */
+static void monitor(void) {
+       struct monitor *m = malloc(sizeof(*m));
+       int s = socket(AF_NETLINK, SOCK_DGRAM, NETLINK_KOBJECT_UEVENT);
+       struct sockaddr_nl addr = {
+               .nl_family = AF_NETLINK,
+               .nl_pid = getpid(),
+               .nl_groups = ~0
+       };
+       int res;
+       char data[8192];
+       struct msghdr msg;
+       struct iovec iov;
+
+       bind(s, (void *)&addr, sizeof(addr));
+       
+       iov.iov_base = data;
+       iov.iov_len = 8192;
+
+       msg.msg_name = (void *)&(addr);
+       msg.msg_namelen = sizeof(addr);
+       msg.msg_iov = &iov;
+       msg.msg_iovlen = 1;
+
+       memset(data, 0, sizeof(data));
+
+       //
+       m->mount_base = strdup(MOUNT_BASE);
+       m->mount_base_size = strlen(MOUNT_BASE);
+       m->mounts = ez_set_new(mdisk_hash, mdisk_equals, mdisk_free);
+       res = mkdir(m->mount_base, 0777);
+       m->indexer = notify_writer_new(NOTIFY_INDEXER);
+       
+       partition_check(m);
+
+       printf("Monitoring uevents\n");
+       
+       while ((res = recvmsg(s, &msg, 0)) >= 0) {
+               // ignore snot from udevd
+               if (strcmp(data, "libudev") == 0)
+                       continue;
+               
+               char *x = data, *e = data+res;
+               char *action;
+               char *dev;
+               char *type;
+
+               while (x < e) {
+                       //printf(" %s\n", x);
+                       if (strncmp(x, "ACTION=", 7) == 0) {
+                               action = x+7;
+                       } else if (strncmp(x, "DEVNAME=", 8) == 0) {
+                               dev = x+8;
+                       } else if (strncmp(x, "DEVTYPE=", 8) == 0) {
+                               type = x+8;
+                       }
+                               
+                       x += strlen(x) + 1;
+               }
+               //printf("\n");
+               
+               if (dev && type && strcmp(type, "partition") == 0) {
+                       char name[strlen(dev) + strlen("/dev/") + 1];
+
+                       sprintf(name, "/dev/%s", dev);
+
+                       if (strcmp(action, "add") == 0)
+                               partition_add(m, name);
+                       else if (strcmp(action, "remove") == 0)
+                               partition_remove(m, name);
+               }
+               memset(data, 0, res);
+       }
+
+       // unmount everything?
+
+       //
+       notify_close(m->indexer);
+       free(m->mount_base);
+       ez_set_free(m->mounts);
+       
+       close(s);
+}
+
+int main(int argc, char **argv) {
+       monitor();
+}
diff --git a/input-monitor.c b/input-monitor.c
new file mode 100644 (file)
index 0000000..8393e2d
--- /dev/null
@@ -0,0 +1,266 @@
+/**
+   This monitors the keyboard.
+
+   Or in this case a mele air mouse.
+
+   Could run this in-memory but as a separate process it can handle
+   some other crap like usb hotplug easier.
+
+ */
+#include <sys/types.h>
+#include <sys/stat.h>
+#include <fcntl.h>
+#include <poll.h>
+#include <unistd.h>
+
+#include <linux/input.h>
+
+#include <stdlib.h>
+#include <stdio.h>
+#include <stdint.h>
+#include <string.h>
+
+#define INPUT_KB "/dev/input/by-id/usb-2.4G_Wireless_Receiver-event-kbd"
+#define INPUT_MOUSE "/dev/input/by-id/usb-2.4G_Wireless_Receiver-if01-event-mouse"
+
+#include "notify.h"
+
+struct monitor {
+       int keyfd;
+       int mousefd;
+
+       notify_t player;
+
+       struct input_event repeat;
+};
+
+/*
+
+stop x from reading the airmouse
+
+$ xinput --list
+ .. find the airmouse devices (keyboard and mouse)
+$ xinput set-int-prop XXX "Device Enabled" 8 0
+
+*/
+
+/*
+
+
+ /dev/input/by-id/usb-2.4G_Wireless_Receiver-event-kbd
+enter: 28 KEY_ENTER
+home: 102 KEY_HOME
+
+up: 103    KEY_UP
+left: 105  KEY_LEFT
+right: 106 KEY_RIGHT
+down: 108  KEY_DOWN
+
+
+square: 59   KEY_F1
+cross: 60    KEY_F2
+circle: 61   KEY_F3
+triangle: 62 KEY_F4
+
+keyboard: same
+
+/dev/input/by-id/usb-2.4G_Wireless_Receiver-if01-event-mouse
+
+mute: 113         KEY_MUTE
+"-": 114          KEY_VOLUMEDOWN
+"+": 115          KEY_VOLUMEUP
+
+subtitles: 240    KEY_UNKNOWN
+lips: 240         KEY_UNKNOWN
+
+// mouse buttons
+centre: 272       BTN_LEFT
+back: 273         BTN_RIGHT
+menu: 274         BTN_MIDDLE
+
+display out: 377  KEY_TV
+*/
+
+struct keymap {
+       enum notify_action action;
+       short code;
+       unsigned char flags;
+       unsigned char mode;
+};
+
+// flags
+#define GEN_REPEAT 1
+
+static const struct keymap map[] = {
+       { NOTIFY_PLAY_SEEK, KEY_HOME, 0, 0 },   // house button
+       { NOTIFY_PLAY_PREV, KEY_UP },
+       { NOTIFY_PLAY_SEEK, KEY_LEFT, 0, 1},
+       { NOTIFY_PLAY_SEEK, KEY_RIGHT, 0, 2 },
+       { NOTIFY_PLAY_NEXT, KEY_DOWN },
+
+       { NOTIFY_VOLUME_MUTE, KEY_MUTE },
+       { NOTIFY_VOLUME_DOWN, KEY_VOLUMEDOWN, GEN_REPEAT }, // - button
+       { NOTIFY_VOLUME_UP, KEY_VOLUMEUP, GEN_REPEAT },   // + button
+};
+
+static int cmp_key(const void *ap, const void *bp) {
+       int needle = *((uint16_t *)ap);
+       const struct keymap *hay = bp;
+       
+       return needle - hay->code;
+}
+
+struct monitor *monitor_new(void) {
+       struct monitor *m = malloc(sizeof(*m));
+
+       m->keyfd = open(INPUT_KB, O_RDONLY);
+       if (m->keyfd == -1)
+               goto fail;
+       m->mousefd = open(INPUT_MOUSE, O_RDONLY);
+       if (m->mousefd == -1)
+               goto fail1;
+
+       m->player = notify_writer_new(NOTIFY_PLAYER);
+       
+       return m;
+ fail1:
+       close(m->keyfd);
+ fail:
+       free(m);
+       
+       return NULL;
+}
+
+void monitor_free(struct monitor *m) {
+       notify_close(m->player);
+       close(m->mousefd);
+       close(m->keyfd);
+       free(m);
+}
+
+static void monitor_event(struct monitor *m, struct input_event *ev) {
+       if (ev->type == EV_KEY) {
+               struct keymap *key = bsearch(&ev->code, map, sizeof(map)/sizeof(map[0]), sizeof(map[0]), cmp_key);
+
+               if (key) {
+                       // Special keys
+                       printf(" key %d action %d flags %d mode %d\n", key->code, key->action, key->flags, key->mode);
+                       if (ev->value) {
+                               if (key->action == NOTIFY_PLAY_SEEK) {
+                                       struct notify_play_seek msg;
+                                       
+                                       switch (key->mode) {
+                                       case 0:
+                                               msg.mode = 0;
+                                               msg.stamp = 0;
+                                               break;
+                                       case 1:
+                                               msg.mode = 1;
+                                               msg.stamp = -15;
+                                               break;
+                                       case 2:
+                                               msg.mode = 1;
+                                               msg.stamp = +15;
+                                               break;
+                                       }
+                                       notify_msg_send(m->player, key->action, 0, &msg);
+                               } else {
+                                       notify_msg_send(m->player, key->action, 0, NULL);
+                               }
+                               
+                               if (key->flags & GEN_REPEAT) {
+                                       printf("start repeat\n");
+                                       m->repeat = *ev;
+                               }
+                       } else {
+                               if (key->flags & GEN_REPEAT) {
+                                       printf("stop repeat\n");
+                                       memset(&m->repeat, 0, sizeof(m->repeat));
+                               }
+                       }
+               } else if (ev->value) {
+                       // All others, sort of
+                       if (ev->code == BTN_MIDDLE)
+                               ev->code = KEY_MENU;
+                       else if (ev->code == KEY_UNKNOWN)
+                               ev->code = KEY_HELP;
+                       printf(" ev %d %04x %08x\n", ev->type, ev->code, ev->value);
+                       struct notify_key msg = {.code = ev->code };
+                       notify_msg_send(m->player, NOTIFY_KEY, 0, &msg);
+               }
+       }
+}
+
+/*
+               switch (ev.code) {
+               case KEY_ENTER: // enter button
+               case KEY_F1:    // square
+               case KEY_F2:    // cross
+               case KEY_F3:    // circle
+               case KEY_F4:    // triangle
+                       
+               case KEY_HOME:  // house button
+                       
+               case KEY_UP:    // face panel up
+               case KEY_DOWN:  // face panel down
+               case KEY_LEFT:  // face panel left
+               case KEY_RIGHT: // face panel right
+
+               case KEY_MUTE:       // mute
+               case KEY_VOLUMEDOWN: // - button
+               case KEY_VOLUMEUP:   // + button
+
+               case KEY_UNKNOWN: // both the lips and the subtitle button
+
+               case BTN_LEFT:  // face panel centre
+               case BTN_RIGHT: // back button
+               case BTN_MIDDLE: // menu /sandwich button
+
+               case KEY_TV:    // 'output' button
+                       break;
+                       
+               }
+*/
+
+void monitor(struct monitor *m) {
+       struct pollfd polla[2];
+       struct input_event ev;
+       
+       // NB: if the player isn't running this will block
+       
+       polla[0].fd = m->keyfd;
+       polla[0].events = POLLIN;
+       polla[1].fd = m->mousefd;
+       polla[1].events = POLLIN;
+
+       while (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) {
+                       for (int i=0;i<2;i++) {
+                               if (polla[i].revents & POLLERR)
+                                       return;
+                               
+                               if (polla[i].revents & POLLIN ){
+                                       read(polla[i].fd, &ev, sizeof(ev));
+                                       monitor_event(m, &ev);
+                               }
+                       }
+               } else if (res == 0) {
+                       monitor_event(m, &m->repeat);                   
+               }
+       }
+}
+
+int main(int argc, char **argv) {
+       struct monitor *m = monitor_new();
+
+       if (m) {
+               monitor(m);
+               monitor_free(m);
+       }
+
+       return 0;
+}
diff --git a/music-player.c b/music-player.c
new file mode 100644 (file)
index 0000000..f22632b
--- /dev/null
@@ -0,0 +1,1084 @@
+/*
+    Copyright (C) 2019 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 <https://www.gnu.org/licenses/>.
+
+*/
+
+#include <libavformat/avformat.h>
+#include <libswresample/swresample.h>
+
+#include <alsa/asoundlib.h>
+#include <poll.h>
+#include <pthread.h>
+
+#include "notify.h"
+#include "ez-list.h"
+#include "ez-port.h"
+#include "dbindex.h"
+
+struct audio_player;
+
+//#define VOICE_MT
+
+/*
+  synchronous voice experiment
+*/
+int init_voice(struct audio_player *ap);
+void close_voice(struct audio_player *ap);
+int voice_start(struct audio_player *ap);
+int voice_speak(struct audio_player *ap, const char *text);
+int voice_finish(struct audio_player *ap);
+int voice_speak_text(struct audio_player *ap, const char *text);
+
+/*
+  async voice stuff
+ */
+struct voice_audio_msg {
+       struct ez_node ln;
+
+       int type;
+       int seq;
+       int more;               //  more coming?
+
+       int nsamples;
+       short samples[];
+};
+
+int audio_init_voice(struct audio_player *ap);
+int audio_close_voice(struct audio_player *ap);
+int audio_voice_speak(struct audio_player *ap, const char *text);
+static void handle_audio_msg(struct audio_player *ap, struct voice_audio_msg *msg);
+
+#ifndef VOICE_MT
+#define audio_close_voice close_voice
+#define audio_init_voice init_voice
+#define voice_speak_text audio_voice_speak
+#endif
+
+/*
+  Player bits
+*/
+
+int audio_close_media(struct audio_player *ap);
+void audio_close_pcm(struct audio_player *ap);
+
+int audio_init_mixer(struct audio_player *ap);
+
+struct audio_player {
+       notify_t player;        // player control port
+
+       // unused
+       //ez_port *reader;      // reader info port
+       struct ez_port *player_port; // ? -> player
+
+       char *device;
+       snd_pcm_t *aud;         // output device
+       snd_mixer_t *amixer;    // volume control
+
+       // voice output
+       pthread_t voice_thread;
+       struct ez_port *voice_port; // player -> voice thread
+       SwrContext *voice_swr;
+       int voice_swr_rate;
+       volatile int voice_seq;
+       // managed by voice thread
+       volatile int voice_rate;
+       volatile int voice_cur_seq;
+
+       // playlist management
+       dbindex *index;
+       dbfile *playing;
+       char *playing_path;
+
+       int quit;
+       int paused;
+       int paused_tmp;         // temporary pause, e.g. talking or effects, a count
+
+       // not used
+       // << delete
+       int npoll;
+       int naudio;
+       struct pollfd *poll;    // for polling in-port and out-audio
+       int file_seq;           // currently active file, hmm, do i care?
+       //  delete >>
+
+       // per-file state
+       // << delete
+       pthread_t reader_id;
+       volatile int state;     // read by thread to communicate shutdown
+       //  delete >>
+
+       AVFormatContext *fc;
+       AVCodecContext *cc;
+       AVStream *audio;
+
+       SwrContext *swr;        // if needed for output
+       size_t buffer_size;     // for output conversion if required
+       uint8_t *buffer;
+
+       // pre-frame state
+       //AVFrame *frame;// unused?
+       int64_t pos;            // current pos in audio stream units
+
+       char *data[1];          // data pointers for current output, planar or not, may point to 'buffer'
+       int nsamples;           // nsamples remaining for output
+
+};
+
+int audio_mixer_adjust(struct audio_player *ap, int delta);
+
+struct audio_player *audio_player_new(const char *device) {
+       struct audio_player *ap = calloc(sizeof(*ap), 1);
+
+       ap->player = notify_reader_new(NOTIFY_PLAYER);
+       ap->device = strdup(device);
+       ap->index = dbindex_open(MAIN_INDEX);
+
+       audio_init_voice(ap);
+       audio_init_mixer(ap);
+
+       return ap;
+}
+
+int audio_init_mixer(struct audio_player *ap) {
+       int res;
+
+       res = snd_mixer_open(&ap->amixer, 0);
+       if (res == 0) {
+               res = snd_mixer_attach(ap->amixer, ap->device);
+               res = snd_mixer_selem_register(ap->amixer, NULL, NULL);
+               res = snd_mixer_load(ap->amixer);
+       }
+       return res;
+}
+
+static snd_mixer_elem_t *mixer_master(struct audio_player *ap) {
+       snd_mixer_selem_id_t *sid;
+       const char *selem_name = "Master";
+
+       snd_mixer_selem_id_alloca(&sid);
+       snd_mixer_selem_id_set_index(sid, 0);
+       snd_mixer_selem_id_set_name(sid, selem_name);
+
+       return snd_mixer_find_selem(ap->amixer, sid);
+}
+
+int audio_mixer_mute(struct audio_player *ap) {
+       snd_mixer_elem_t* elem = mixer_master(ap);
+       int mute;
+
+       if (elem) {
+               snd_mixer_selem_get_playback_switch(elem, SND_MIXER_SCHN_MONO, &mute);
+               return snd_mixer_selem_set_playback_switch_all(elem, !mute);
+       } else {
+               return -1;
+       }
+}
+
+int audio_mixer_adjust(struct audio_player *ap, int delta) {
+       snd_mixer_elem_t* elem = mixer_master(ap);
+       long min, max, now;
+       int res;
+
+       if (elem) {
+               res = snd_mixer_selem_get_playback_volume_range(elem, &min, &max);
+               snd_mixer_selem_get_playback_volume(elem, SND_MIXER_SCHN_MONO, &now);
+               now += delta;
+               now = now < min ? min : now;
+               now = now > max ? max : now;
+               snd_mixer_selem_set_playback_volume_all(elem, now);
+               return res;
+       } else {
+               return -1;
+       }
+}
+
+void audio_close_mixer(struct audio_player *ap) {
+       if (ap->amixer) {
+               snd_mixer_close(ap->amixer);
+               ap->amixer = NULL;
+       }
+}
+
+void audio_player_free(struct audio_player *ap) {
+       audio_close_media(ap);
+       audio_close_pcm(ap);
+       audio_close_mixer(ap);
+
+       audio_close_voice(ap);
+
+       dbindex_close(ap->index);
+
+       swr_free(&ap->swr);
+
+       free(ap->poll);
+       free(ap->buffer);
+       free(ap->device);
+       free(ap);
+}
+
+int audio_init_poll(struct audio_player *ap) {
+       int npoll = 1;
+       int count = 0;
+
+       if (ap->aud) {
+               count = snd_pcm_poll_descriptors_count(ap->aud);
+       }
+
+       npoll += count;
+       if (ap->npoll < npoll) {
+               ap->npoll = npoll;
+               ap->naudio = count;
+               ap->poll = realloc(ap->poll, sizeof(*ap->poll) * npoll);
+       }
+
+       // player port
+       ap->poll[0].fd = ap->player;
+       ap->poll[0].events = POLLIN;
+       // reader port
+       //ap->poll[1].fd = ez_port_fd(ap->reader);
+       //ap->poll[1].events = POLLIN;
+
+       if (ap->aud)
+               snd_pcm_poll_descriptors(ap->aud, ap->poll+1, count);
+
+       return 0;
+}
+
+/**
+ * Returns a bitmask of things that are ready.
+ *
+ * bit 0 = player incoming message
+ * bit 1 = reader incoming message
+ * bit 2 = audio output ready
+ * @return -1 on error.
+ */
+int audio_poll(struct audio_player *ap) {
+       int res;
+
+       while (1) {
+               res = poll(ap->poll, ap->npoll, -1);
+               if (res == 0) {
+                       unsigned short revents;
+                       int state = 0;
+
+                       if (ap->poll[0].revents & POLLIN)
+                               state |= 1;
+                       //if (ap->poll[1].revents & POLLIN)
+                       //      state |= 2;
+
+                       if (ap->poll[0].revents & POLLERR)
+                               return -EIO;
+                       //if (ap->poll[1].revents & POLLERR)
+                       //      return -EIO;
+
+                       if (ap->aud) {
+                               snd_pcm_poll_descriptors_revents(ap->aud, ap->poll+1, ap->naudio, &revents);
+                               if (revents & POLLOUT)
+                                       state |= 4;
+                               if (revents & POLLERR)
+                                       return -EIO;
+                       }
+
+                       return state;
+               }
+       }
+}
+
+
+static unsigned int hw_sample_rate(struct audio_player *ap) {
+       snd_pcm_hw_params_t *hw;
+       unsigned int actual_rate;
+       int dir;
+       int res;
+
+       snd_pcm_hw_params_alloca(&hw);
+       res = snd_pcm_hw_params_current(ap->aud, hw);
+       res = snd_pcm_hw_params_get_rate(hw, &actual_rate, &dir);
+
+       return actual_rate;
+}
+
+/**
+ * Open, re-open, init and or re-init the audio device for the given sample rate.
+ *
+ * Samples are in stereo interleaved signed 16-bit format.
+ *
+ * The audio device will be in the READY state when this returns successfully.
+ */
+int audio_init_pcm(struct audio_player *ap) {
+       AVCodecContext *cc = ap->cc;
+       int res;
+       int init = 1;
+
+       printf("audio_init_pcm\n");
+
+       if (!ap->aud) {
+               res = snd_pcm_open(&ap->aud, ap->device ? ap->device : "default", SND_PCM_STREAM_PLAYBACK, 0);
+               if (res < 0)
+                       return res;
+       } else {
+               if (hw_sample_rate(ap) == cc->sample_rate) {
+                       printf(" rate unchanged\n");
+                       init = 0;
+               } else if (snd_pcm_state(ap->aud) == SND_PCM_STATE_RUNNING) {
+                       // other states?
+                       res = snd_pcm_drain(ap->aud);
+               }
+       }
+
+       if (init) {
+               res = snd_pcm_set_params(ap->aud,
+                                        SND_PCM_FORMAT_S16_LE,
+                                        SND_PCM_ACCESS_RW_INTERLEAVED,
+                                        2,
+                                        cc->sample_rate,
+                                        1,
+                                        500000);
+               if (res < 0)
+                       return res;
+       }
+
+       // Check actual sample rate of hardware
+       int actual_rate = hw_sample_rate(ap);
+
+       // Create resampler if needed
+       if (cc->sample_fmt != AV_SAMPLE_FMT_S16
+           || cc->channel_layout != AV_CH_LAYOUT_STEREO
+           || cc->sample_rate != actual_rate) {
+               printf(" soft resample %ld:%s:%d -> %d:%s:%d\n",
+                      cc->channel_layout, av_get_sample_fmt_name(cc->sample_fmt), cc->sample_rate,
+                      AV_CH_LAYOUT_STEREO, av_get_sample_fmt_name(AV_SAMPLE_FMT_S16), actual_rate);
+
+               ap->swr = swr_alloc_set_opts(ap->swr,
+                                            AV_CH_LAYOUT_STEREO,       // out_ch_layout
+                                            AV_SAMPLE_FMT_S16, // out_sample_fmt
+                                            actual_rate, // out_sample_rate
+                                            cc->channel_layout,        // in_ch_layout
+                                            cc->sample_fmt,    // in_sample_fmt
+                                            cc->sample_rate,   // in_sample_rate
+                                            0,                 // log_offset
+                                            NULL);                     // log_ctx
+               if (ap->swr == NULL) {
+                       printf("swr create failed\n");
+                       return -1;
+               }
+               swr_init(ap->swr);
+       } else {
+               swr_free(&ap->swr);
+       }
+
+       return audio_init_poll(ap);
+}
+
+int audio_stop_pcm(struct audio_player *ap) {
+       int res;
+
+       if (ap->paused) {
+               ap->paused = 0;
+               snd_pcm_drop(ap->aud);
+               res = snd_pcm_pause(ap->aud, 0);
+       }
+
+       res = snd_pcm_drain(ap->aud);
+
+       return res;
+}
+
+void audio_close_pcm(struct audio_player *ap) {
+       int res;
+
+       snd_pcm_drop(ap->aud);
+       if (ap->paused) {
+               res = snd_pcm_pause(ap->aud, 0);
+       }
+       res = snd_pcm_drain(ap->aud);
+       snd_pcm_close(ap->aud);
+       ap->aud = NULL;
+}
+
+/**
+ * New frame ready.
+ * Set up pointers for pcm output.
+ */
+int audio_init_frame(struct audio_player *ap, AVFrame *frame) {
+       if (ap->swr) {
+               int samples = swr_get_out_samples(ap->swr, frame->nb_samples);
+
+               if (ap->buffer_size < samples * 4) {
+                       ap->buffer_size = samples * 4;
+                       ap->buffer = realloc(ap->buffer, ap->buffer_size);
+               }
+               ap->nsamples = swr_convert(ap->swr, &ap->buffer, samples, (const uint8_t **)frame->data, frame->nb_samples);
+               ap->data[0] = (void *)ap->buffer;
+       } else {
+               ap->data[0] = (void *)frame->data[0];
+               ap->nsamples = frame->nb_samples;
+       }
+       ap->pos = frame->pts;
+
+       return 0;
+}
+
+int audio_send_pcm(struct audio_player *ap) {
+       snd_pcm_sframes_t sent;
+
+       sent = snd_pcm_writei(ap->aud, ap->data[0], ap->nsamples);
+
+       if (sent > 0) {
+               ap->nsamples -= sent;
+               ap->data[0] += sent * 4;
+       }
+
+       if (sent < 0) {
+               printf("snd_pcm_write failed (state=%s): %s\n", snd_pcm_state_name(snd_pcm_state(ap->aud)), snd_strerror(sent));
+               sent = snd_pcm_recover(ap->aud, sent, 0);
+       }
+
+       if (sent < 0) {
+               printf("snd_pcm_write failed: %s\n", snd_strerror(sent));
+               if (sent != EAGAIN || sent != EINTR) {
+                       ap->nsamples = 0;
+               }
+       }
+
+       return sent;
+}
+
+int audio_close_media(struct audio_player *ap) {
+       avcodec_free_context(&ap->cc);
+       avformat_close_input(&ap->fc);
+       ap->audio = NULL;
+
+       // ?? or do we want to flush them?
+       ap->nsamples = 0;
+       return 0;
+}
+
+int audio_init_media(struct audio_player *ap, const char *path) {
+       int res;
+       AVCodec *codec;
+       int audioid;
+
+       printf("audio_init_media '%s'\n", path);
+
+       audio_close_media(ap);
+
+       ap->fc = avformat_alloc_context();
+       res = avformat_open_input(&ap->fc, path, NULL, NULL);
+       if (res != 0)
+               goto fail;
+
+       res = avformat_find_stream_info(ap->fc, NULL);
+       if (res != 0)
+               goto fail;
+
+       av_dump_format(ap->fc, 0, path, 0);
+
+       audioid = av_find_best_stream(ap->fc, AVMEDIA_TYPE_AUDIO, -1, -1, &codec, 0);
+       if (audioid < 0)
+               goto fail;
+
+       for (int i=0;i<ap->fc->nb_streams;i++)
+               ap->fc->streams[i]->discard = i == audioid ? AVDISCARD_DEFAULT : AVDISCARD_ALL;
+
+       ap->audio = ap->fc->streams[audioid];
+       ap->cc = avcodec_alloc_context3(NULL);
+       res = avcodec_parameters_to_context(ap->cc, ap->audio->codecpar);
+       ap->cc->codec_id = codec->id;
+
+       res = avcodec_open2(ap->cc, codec, NULL);
+       if (res < 0)
+               goto fail;
+
+       printf("codec rate: %d\n", ap->cc->sample_rate);
+       printf("codec channels: %d\n", ap->cc->channels);
+       printf("codec layout: %lu\n", ap->cc->channel_layout);
+       printf("codec format: %s\n", av_get_sample_fmt_name(ap->cc->sample_fmt));
+
+       return audio_init_pcm(ap);
+ fail:
+       perror("init media");
+       audio_close_media(ap);
+       return -1;
+}
+
+// Next in play queue
+int audio_next_file(struct audio_player *ap) {
+       int res;
+       int empty = ap->playing == NULL;
+
+       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
+
+       //if (res != 0) {
+       //      audio_stop_pcm(ap);
+       //}
+
+       return res;
+}
+
+int audio_prev_file(struct audio_player *ap) {
+       int res;
+       int empty = ap->playing == NULL;
+
+       audio_close_media(ap);
+       do {
+               //res = dbfile_prev(ap->index, &ap->playing, &ap->playing_path);
+               res = dbfile_prev_shuffle(ap->index, &ap->playing, &ap->playing_path);
+               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
+
+       //if (res != 0) {
+       //      audio_stop_pcm(ap);
+       //}
+
+       return res;
+}
+
+
+void audio_player_control(struct audio_player *ap) {
+       int ready = notify_msg_ready(ap->player);
+
+ mainloop:
+       while (!ap->quit
+              && (ap->cc == NULL || ap->paused || ready || ap->paused_tmp)) {
+               int res;
+               void *msg;
+               enum notify_action action;
+               unsigned int pri;
+
+               if (ap->paused)
+                       printf("player: paused\n");
+               else if (!ready)
+                       printf("player: idle\n");
+
+               // temporary stuff testing async voice
+               //  this will monitor the player port and handle any messages that arrive
+               //  but quit the loop if paused_tmp gets cleared by handle_audio_msg
+#ifdef VOICE_MT
+               int wait = 0;
+               do {
+                       struct pollfd fds[2];
+
+                       fds[0].fd = ap->player;
+                       fds[0].events = POLLIN;
+                       fds[1].fd = ez_port_fd(ap->player_port);
+                       fds[1].events = POLLIN;
+
+                       res = poll(fds, 2, -1); // timeout to check stuff?
+
+                       if (fds[0].revents & POLLERR
+                           || fds[0].revents & POLLERR) {
+                               // what now?
+
+                       }
+
+                       if (fds[1].revents & POLLIN) {
+                               handle_audio_msg(ap, ez_port_take(ap->player_port));
+                       }
+                       wait = (fds[0].revents & POLLIN) == 0;
+
+                       if (wait && !ap->paused_tmp)
+                               goto mainloop;
+               } while (wait);
+#endif
+
+               msg = notify_msg_receive(ap->player, &action, &pri);
+               if (ready > 0)
+                       ready -= 1;
+
+               printf("msg %d\n", action);
+
+               switch (action) {
+               case NOTIFY_QUIT:
+                       ap->quit = 1;
+                       return;
+               case NOTIFY_PLAY_PLAY:
+                       if (ap->paused) {
+                               res = snd_pcm_pause(ap->aud, 0);
+                               ap->paused = 0;
+                       }
+                       break;
+               case NOTIFY_PLAY_PAUSE:
+                       if (ap->cc) {
+                               ap->paused ^= 1;
+                               res = snd_pcm_pause(ap->aud, ap->paused);
+                       }
+                       break;
+               case NOTIFY_PLAY_STOP:
+                       // something something
+                       break;
+               case NOTIFY_PLAY_SEEK:
+                       if (ap->cc) {
+                               struct notify_play_seek *s = msg;
+                               double now = ap->pos * av_q2d(ap->audio->time_base);
+
+                               if (s->mode == 1) {
+                                       double rel = s->stamp;
+                                       int64_t seek = (int64_t)((now + rel) * AV_TIME_BASE);
+                                       int64_t min = rel > 0 ? (int64_t)((now - rel) * AV_TIME_BASE) + 2 : INT64_MIN;
+                                       int64_t max = rel < 0 ? (int64_t)((now - rel) * AV_TIME_BASE) - 2 : INT64_MAX;
+
+                                       printf("seek %f%+f  %ld %ld %ld\n", now, rel, min, seek, max);
+                                       res = avformat_seek_file(ap->fc, -1, min, seek, max, 0);
+                               } else {
+                                       double rel = s->stamp;
+                                       int64_t seek = (int64_t)((rel) * AV_TIME_BASE);
+                                       int64_t min = rel > now ? seek - 2 : INT64_MIN;
+                                       int64_t max = rel < now ? seek + 2 : INT64_MAX;
+
+                                       printf("seek %f  %ld %ld %ld\n", rel, min, seek, max);
+                                       res = avformat_seek_file(ap->fc, -1, min, seek, max, 0);
+                               }
+                               avcodec_flush_buffers(ap->cc);
+                       }
+                       break;
+               case NOTIFY_PLAY_NEXT:
+                       audio_next_file(ap);
+                       break;
+               case NOTIFY_PLAY_PREV:
+                       audio_prev_file(ap);
+                       break;
+               case NOTIFY_VOLUME_UP:
+                       audio_mixer_adjust(ap, +1);
+                       break;
+               case NOTIFY_VOLUME_DOWN:
+                       audio_mixer_adjust(ap, -1);
+                       break;
+               case NOTIFY_VOLUME_MUTE:
+                       audio_mixer_mute(ap);
+                       break;
+               case NOTIFY_KEY: {
+                       /*
+                         Hmm, this might get ugly fast, is there another way?
+
+                       */
+                       struct notify_key *k = msg;
+                       switch (k->code) {
+                       case 139://case KEY_MENU:
+                               // start menu
+                               ap->voice_seq += 1;
+                               audio_voice_speak(ap, "Main Menu.  Play-list, Help, System.");
+                               break;
+                       case 138://case KEY_HELP:
+                               // say current file
+                               // could also get info from the AVFormatContext metadata.
+                               if (ap->playing) {
+                                       audio_voice_speak(ap, "Playing.\n\n");
+                                       if (strcmp(ap->playing->title, "Unknown") == 0) {
+                                               audio_voice_speak(ap, "File.\n\n");
+                                               audio_voice_speak(ap, strrchr(ap->playing->path, '/')+1);
+                                       } else {
+                                               audio_voice_speak(ap, ap->playing->title);
+                                               if (strcmp(ap->playing->artist, "Unknown") != 0) {
+                                                       audio_voice_speak(ap, "by\n\n");
+                                                       audio_voice_speak(ap, ap->playing->artist);
+                                               }
+                                       }
+                               } else {
+                                       audio_voice_speak(ap, "Player Idle");
+                               }
+                               break;
+                       }
+                       break;
+               }
+               case NOTIFY_DEBUG: {
+                       struct notify_debug *d = msg;
+
+                       switch (d->func) {
+                       case 0: {
+                               printf("pre-drain: %d\n", snd_pcm_state(ap->aud));
+                               snd_pcm_drain(ap->aud);
+                               printf("pst-drain: %d\n", snd_pcm_state(ap->aud));
+
+                               // try to re-init the stream
+                               res = snd_pcm_set_params(ap->aud,
+                                                        SND_PCM_FORMAT_S16_LE,
+                                                        SND_PCM_ACCESS_RW_INTERLEAVED,
+                                                        2,
+                                                        //frame->sample_rate,
+                                                        48000,
+                                                        1,
+                                                        500000);
+                               if (res < 0) {
+                                       printf("snd_pcm_set_params: %s\n", snd_strerror(res));
+                                       exit(1);
+                               }
+                               break;
+                       }
+                       case 1: {
+                               // dump some info
+                               snd_pcm_hw_params_t *hw;
+                               snd_output_t *out = NULL;
+
+                               snd_pcm_hw_params_alloca(&hw);
+                               res = snd_pcm_hw_params_current(ap->aud, hw);
+                               printf("snd_pcm_hw_params_current: %s\n", snd_strerror(res));
+                               res = snd_output_stdio_attach(&out, stdout, 0);
+                               printf("snd_output_stdio_attach: %s\n", snd_strerror(res));
+                               snd_pcm_hw_params_dump(hw, out);
+                               break;
+                       }
+                       case 2:
+                               // cause an underrun?
+                               sleep(2);
+                               break;
+                       case 3:
+                               // exit (mem test)
+                               ap->quit = 1;
+                               break;
+                       }
+                       break;
+               }
+               default:
+                       break;
+               }
+               notify_msg_free(action, msg);
+       }
+}
+
+// TODO: the play queue / play list / etc
+void audio_player_loop(struct audio_player *ap) {
+       AVFrame *frame = av_frame_alloc();
+
+       audio_next_file(ap);
+
+       while (1) {
+               int res;
+
+               // Handle any commands
+               audio_player_control(ap);
+
+               if (ap->quit)
+                       break;
+
+               // Flush any remaining samples first
+               if (ap->nsamples) {
+                       res = audio_send_pcm(ap);
+                       continue;
+               }
+
+               // Have file open / reading
+               if (ap->cc) {
+                       AVPacket pkt;
+
+                       res  = avcodec_receive_frame(ap->cc, frame);
+                       if (res >= 0) {
+                               res = audio_init_frame(ap, frame);
+                               res = audio_send_pcm(ap);
+                               continue;
+                       }
+
+                       res = av_read_frame(ap->fc, &pkt);
+                       if (res < 0) {
+                               audio_close_media(ap);
+                               audio_next_file(ap);
+                               continue;
+                       }
+
+                       avcodec_send_packet(ap->cc, &pkt);
+                       av_packet_unref(&pkt);
+               }
+       }
+
+       av_frame_free(&frame);
+
+       // shutdown shit
+}
+
+int main(int argc, char **argv) {
+       struct audio_player *ap;
+       char *device = "plug:dmix";
+
+       device = "default";
+
+       ap = audio_player_new(device);
+       audio_player_loop(ap);
+       audio_player_free(ap);
+
+       snd_config_update_free_global();
+
+       return 0;
+}
+
+
+/*
+ prototyping area
+*/
+
+/*
+
+  This needs another thread.
+
+  Thread starts synthesising, sends audio to music player via throttled message port(?)
+
+ */
+
+#include <espeak-ng/speak_lib.h>
+
+#ifdef VOICE_MT
+struct voice_msg {
+       int type;
+       int seq;
+       char text[];
+};
+
+static void handle_audio_msg(struct audio_player *ap, struct voice_audio_msg *msg) {
+       if (msg->type == 0) {
+               printf("recv: synth samples %d\n", (int)msg->nsamples);
+               if (msg->seq == ap->voice_seq) {
+                       int actual_rate = hw_sample_rate(ap);
+
+                       if (ap->voice_swr == NULL || actual_rate != ap->voice_swr_rate) {
+                               ap->voice_swr = swr_alloc_set_opts
+                                       (ap->voice_swr,
+                                        AV_CH_LAYOUT_STEREO,   // out_ch_layout
+                                        AV_SAMPLE_FMT_S16,     // out_sample_fmt
+                                        actual_rate, // out_sample_rate
+                                        AV_CH_LAYOUT_MONO,     // in_ch_layout
+                                        AV_SAMPLE_FMT_S16,     // in_sample_fmt
+                                        ap->voice_rate,        // in_sample_rate
+                                        0,                     // log_offset
+                                        NULL);                 // log_ctx
+                               ap->voice_swr_rate = actual_rate;
+                               swr_init(ap->voice_swr);
+                       }
+
+                       int nsamples = msg->nsamples;
+                       void *data = msg->samples;
+                       int samples = swr_get_out_samples(ap->voice_swr, nsamples);
+
+                       if (ap->buffer_size < samples * 4) {
+                               ap->buffer_size = samples * 4;
+                               ap->buffer = realloc(ap->buffer, ap->buffer_size);
+                       }
+                       ap->nsamples = swr_convert(ap->voice_swr, &ap->buffer, samples, (const uint8_t **)&data, nsamples);
+                       ap->data[0] = (void *)ap->buffer;
+
+                       // does it need flushing in voice_finish()?
+
+                       audio_send_pcm(ap);
+               } else {
+                       printf("discard stale voice audio\n");
+               }
+       } else if (msg->type == 1) {
+               printf("recv: synth end samples\n");
+               ap->paused_tmp -= 1;
+       }
+       free(msg);
+}
+
+
+static int voice_thread_data(short *data, int nsamples, espeak_EVENT *event) {
+       struct audio_player *ap = event->user_data;
+
+       if (!nsamples)
+               return 0;
+
+       if (ap->voice_seq != ap->voice_cur_seq) {
+               printf("synth: abort decode\n");
+               return -1;
+       }
+
+       struct voice_audio_msg *msg = malloc(sizeof(*msg) + nsamples * sizeof(short));
+
+       msg->type = 0;
+       msg->seq = ap->voice_cur_seq;
+       msg->nsamples = nsamples;
+       memcpy(msg->samples, data, nsamples * sizeof(short));
+
+       ez_port_put(ap->player_port, msg);
+
+       return 0;
+}
+
+static void *voice_thread(void *d) {
+       struct audio_player *ap = d;
+       struct voice_msg *msg;
+       int go = 1;
+
+       ap->voice_rate = espeak_Initialize(AUDIO_OUTPUT_RETRIEVAL, 120, NULL, espeakINITIALIZE_DONT_EXIT);
+
+       // God it talks like a maniac, slow it down a bit
+       espeak_SetSynthCallback(voice_thread_data);
+       espeak_SetParameter(espeakRATE, 160, 0);
+       espeak_SetParameter(espeakWORDGAP, 15, 0);
+
+       printf("Voice: thread started\n");
+
+       while (go && (msg = ez_port_take(ap->voice_port))) {
+               switch (msg->type) {
+               case 0:
+                       go = 0;
+                       break;
+               case 1:
+                       if (ap->voice_seq == msq->seq) {
+                               ap->voice_cur_seq = msg->seq;
+
+                               printf("send: synth startn");
+
+                               espeak_Synth(msg->text, strlen(msg->text)+1, 0, POS_CHARACTER, 0,  espeakCHARS_8BIT | espeakPHONEMES, NULL, ap);
+
+                               struct voice_audio_msg *amsg = calloc(sizeof(*amsg), 1);
+
+                               printf("send: synth end\n");
+                               amsg->type = 1;
+                               amsg->seq = msg->seq;
+                               ez_port_put(ap->player_port, amsg);
+                       }
+                       break;
+               }
+               free(msg);
+       }
+
+       espeak_Terminate();
+
+       printf("Voice: thread done\n");
+
+       return d;
+}
+
+int audio_init_voice(struct audio_player *ap) {
+       ap->voice_port = ez_port_new(8);
+       ap->player_port = ez_port_new(1);
+
+       pthread_create(&ap->voice_thread, NULL, voice_thread, ap);
+
+       return 0;
+}
+
+int audio_close_voice(struct audio_player *ap) {
+       pthread_join(ap->voice_thread, NULL);
+       ez_port_free(ap->voice_port);
+       ez_port_free(ap->player_port);
+       return 0;
+}
+
+int audio_voice_speak(struct audio_player *ap, const char *text) {
+       struct voice_msg *msg;
+
+       msg = malloc(sizeof(*msg) + strlen(text)+1);
+
+       msg->type = 1;
+       msg->seq = ap->voice_seq;
+       strcpy(msg->text, text);
+
+       ez_port_put(ap->voice_port, msg);
+
+       ap->paused_tmp += 1;
+
+       return 0;
+}
+
+#else
+
+static int voice_rate;
+static struct audio_player *player;
+//static snd_pcm_hw_params_t *voice_save;
+static SwrContext *voice_swr;
+
+static int voice_data(short *data, int nsamples, espeak_EVENT*event) {
+       struct audio_player *ap = player;
+
+       printf("samples: %d\n", nsamples);
+       for (int i=0;event[i].type;i++)
+               printf(" event: %d\n", event[i].type);
+
+       int samples = swr_get_out_samples(voice_swr, nsamples);
+
+       if (ap->buffer_size < samples * 4) {
+               ap->buffer_size = samples * 4;
+               ap->buffer = realloc(ap->buffer, ap->buffer_size);
+       }
+       ap->nsamples = swr_convert(voice_swr, &ap->buffer, samples, (const uint8_t **)&data, nsamples);
+       ap->data[0] = (void *)ap->buffer;
+
+       // does it need flushing in voice_finish()?
+
+       audio_send_pcm(ap);
+
+       return 0;
+}
+
+int init_voice(struct audio_player *ap) {
+       voice_rate = espeak_Initialize(AUDIO_OUTPUT_RETRIEVAL, 0, NULL, espeakINITIALIZE_DONT_EXIT);
+
+       if (voice_rate <= 0)
+               return -1;
+
+       // God it talks like a maniac, slow it down a bit
+       espeak_SetSynthCallback(voice_data);
+       espeak_SetParameter(espeakRATE, 160, 0);
+       espeak_SetParameter(espeakWORDGAP, 15, 0);
+
+       player = ap;
+
+       return 0;
+}
+
+void close_voice(struct audio_player *ap) {
+       espeak_Terminate();
+       player = NULL;
+}
+
+int voice_start(struct audio_player *ap) {
+       int actual_rate = hw_sample_rate(ap);
+
+       if (actual_rate != voice_rate) {
+               voice_swr = swr_alloc_set_opts(voice_swr,
+                                              AV_CH_LAYOUT_STEREO,     // out_ch_layout
+                                              AV_SAMPLE_FMT_S16,       // out_sample_fmt
+                                              actual_rate, // out_sample_rate
+                                              AV_CH_LAYOUT_MONO,       // in_ch_layout
+                                              AV_SAMPLE_FMT_S16,       // in_sample_fmt
+                                              voice_rate,      // in_sample_rate
+                                              0,                       // log_offset
+                                              NULL);                   // log_ctx
+               swr_init(voice_swr);
+       }
+
+       return 0;
+}
+
+int voice_finish(struct audio_player *ap) {
+       return 0;
+}
+
+int voice_speak_text(struct audio_player *ap, const char *text) {
+       voice_start(ap);
+       voice_speak(ap, text);
+       voice_finish(ap);
+       return 0;
+}
+
+int voice_speak(struct audio_player *ap, const char *text) {
+       espeak_Synth(text, strlen(text), 0, POS_CHARACTER, 0,  espeakCHARS_8BIT | espeakPHONEMES, NULL, NULL);
+       return 0;
+}
+#endif
diff --git a/notify.c b/notify.c
new file mode 100644 (file)
index 0000000..c9a1d31
--- /dev/null
+++ b/notify.c
@@ -0,0 +1,196 @@
+
+#include <fcntl.h>
+#include <sys/stat.h>
+#include <mqueue.h>
+
+#include <poll.h>
+
+#include <stdint.h>
+#include <stdio.h>
+#include <errno.h>
+
+#include "dbindex.h"
+
+#include "notify.h"
+
+// Message handling and routing
+
+static ez_blob_desc PLAY_SEEK_DESC[] = {
+       EZ_BLOB_START(struct notify_play_seek),
+       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_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_INT32(struct notify_key, 1, code),
+       EZ_BLOB_END(struct notify_key)
+};
+
+/**
+ * Blob for action.
+ * This must match enum notify_action.
+ */
+static ez_blob_desc *action_desc[] = {
+       NULL,
+
+       DBDISK_DESC,
+       DBDISK_DESC,
+
+       NULL,
+       NULL,
+       NULL,
+       PLAY_SEEK_DESC,
+
+       NULL,
+       NULL,
+
+       NULL,
+       NULL,
+       NULL,
+
+       NULL,
+
+       KEY_DESC,
+
+       DEBUG_DESC,
+
+       NULL
+};
+
+// should be global by default
+// could force a smaller one i guess
+static long msg_size;
+
+/**
+ * Create a reader queue, these are blocking.
+ */
+mqd_t notify_reader_new(const char *path) {
+       mqd_t q = mq_open(path, O_RDONLY | O_CREAT, 0600, NULL);
+
+       if (q != -1) {
+               struct mq_attr at;
+               mq_getattr(q, &at);
+               msg_size = at.mq_msgsize;
+       }
+
+       return q;
+}
+
+/**
+ * Create a writer queue, these are non-blocking.
+ */
+mqd_t notify_writer_new(const char *path) {
+       mqd_t q = mq_open(path, O_WRONLY | O_CREAT | O_NONBLOCK, 0600, NULL);
+
+       if (q != -1) {
+               struct mq_attr at;
+               mq_getattr(q, &at);
+               msg_size = at.mq_msgsize;
+       }
+
+       return q;
+}
+
+void notify_close(mqd_t q) {
+       mq_close(q);
+}
+
+void notify_msg_free(enum notify_action action, void *p) {
+       if (action_desc[action]) {
+               ez_blob_free(action_desc[action], p);
+       }
+}
+
+/**
+ * Return the number of messages waiting.
+ */
+int notify_msg_ready(notify_t q) {
+       struct mq_attr at;
+
+       mq_getattr(q, &at);
+       return (int)at.mq_curmsgs;
+}
+
+/**
+ * Send a message.  This does not block if the queue is full.
+ *
+ * @return -1 on error, EGAIN means the queue is full.
+ */
+int notify_msg_send(mqd_t q, enum notify_action action, unsigned int msg_pri, const void *p) {
+       int res = -1;
+
+       if (action < NOTIFY_SIZEOF) {
+               printf("send action %d\n", action);
+               if (action_desc[action]) {
+                       size_t size = ez_blob_size(action_desc[action], p);
+                       char msg[size+1];
+
+                       msg[0] = action;
+                       ez_blob_encode_raw(action_desc[action], p, msg+1, size);
+                       res = mq_send(q, msg, size+1, msg_pri);
+               } else {
+                       res = mq_send(q, (char *)&action, 1, msg_pri);
+               }
+               if (res) {
+                       perror("msg_send");
+               }
+       } else {
+               errno = EINVAL;
+               perror("msg_send: invalid action");
+       }
+
+       return res;
+}
+
+/**
+ * Recieve a message, blocking until one is available.
+ *
+ * @return the (decoded) message, check actionp to determine it's
+ * format.  This may be NULL for no-payload messages or on fatal
+ * error, check the action.
+ */
+void *notify_msg_receive(mqd_t q, enum notify_action *actionp, unsigned int *msg_pri) {
+       char msg[msg_size];
+
+       while (1) {
+               ssize_t size = mq_receive(q, msg, msg_size, msg_pri);
+
+               if (size >= 1) {
+                       enum notify_action action = msg[0];
+
+                       if (action < NOTIFY_SIZEOF) {
+                               *actionp = action;
+
+                               if (action_desc[action]) {
+                                       void *p = ez_blob_decode(action_desc[action], msg+1, size-1);
+
+                                       if (p)
+                                               return p;
+                               } else {
+                                       return NULL;
+                               }
+                       }
+               } else if (size == 0) {
+                       // ignore  or quit?
+               } else {
+                       if (errno == EINTR)
+                               ;
+                       else {
+                               perror("server: mq_receive");
+                               break;
+                       }
+               }
+       }
+
+       *actionp = NOTIFY_QUIT;
+
+       return NULL;
+}
diff --git a/notify.h b/notify.h
new file mode 100644 (file)
index 0000000..b6cc3a2
--- /dev/null
+++ b/notify.h
@@ -0,0 +1,64 @@
+
+#ifndef _NOTIFY_H
+#define _NOTIFY_H
+
+#define NOTIFY_BROKER "/pz.broker"
+#define NOTIFY_INDEXER "/pz.indexer"
+#define NOTIFY_PLAYER "/pz.player"
+
+enum notify_action {
+       NOTIFY_QUIT,
+
+       NOTIFY_DISK_ADD,
+       NOTIFY_DISK_REMOVE,
+
+       NOTIFY_PLAY_PLAY,
+       NOTIFY_PLAY_PAUSE,
+       NOTIFY_PLAY_STOP,
+       NOTIFY_PLAY_SEEK,       // notify_play_seek : absolute, fixed, forward or rewind
+
+       NOTIFY_PLAY_NEXT,
+       NOTIFY_PLAY_PREV,
+
+       NOTIFY_VOLUME_UP,
+       NOTIFY_VOLUME_DOWN,
+       NOTIFY_VOLUME_MUTE,
+
+       NOTIFY_TRACK_INFO,      // etc?
+
+       NOTIFY_KEY,             // some key pressed
+
+       NOTIFY_DEBUG,           // debug/prototyping command
+
+       NOTIFY_SHUFFLE,         /* disk-manager: create shuffled playlist */
+
+       NOTIFY_SIZEOF
+};
+
+typedef int notify_t;
+
+struct notify_play_seek {
+       int mode; // 0 abs, 1 relative
+       double stamp;
+};
+
+struct notify_debug {
+       int func;
+};
+
+struct notify_key {
+       int code;
+};
+
+notify_t notify_reader_new(const char *path);
+notify_t notify_writer_new(const char *path);
+void notify_close(notify_t q);
+
+int notify_msg_ready(notify_t q);
+
+int notify_msg_send(notify_t q, enum notify_action action, unsigned int msg_pri, const void *payload);
+void *notify_msg_receive(notify_t q, enum notify_action *actionp, unsigned int *msg_pri);
+
+void notify_msg_free(enum notify_action action, void *p);
+
+#endif