add lightmediascannerd and lightmediascannerctl
authorGustavo Sverzut Barbieri <barbieri@profusion.mobi>
Tue, 27 Aug 2013 03:37:50 +0000 (00:37 -0300)
committerLucas De Marchi <lucas.demarchi@intel.com>
Fri, 30 Aug 2013 14:30:01 +0000 (11:30 -0300)
The daemon and control client are provided as a way to make usage of
lightmediascanner easier in a platform. It will scan directories based
on the startup configuration of categories, their parsers and
directories.

One can request a write lock, avoiding the server to scan and also
report to others that they should not write to DB.

Scanning status, Write Lock and global Update ID are provided as
properties.

The scan method will take a dict of categories and their respective
scan paths. This allows to reduce the scan scope to just few
categories and paths. The categories must exist previously and paths
must be inside the previously known categories directories, this
avoids people doing mistakes. If a category is provided as an empty
list, all directories will be used. If there are no categories
provided, then all categories with all directories will be used.

.gitignore
Makefile.am
configure.ac
org.lightmediascanner.service.in [new file with mode: 0644]
src/bin/.gitignore
src/bin/Makefile.am
src/bin/lightmediascannerctl.c [new file with mode: 0644]
src/bin/lightmediascannerd.c [new file with mode: 0644]

index 8a28311..536ae89 100644 (file)
@@ -34,3 +34,4 @@ m4/ltoptions.m4
 m4/ltsugar.m4
 m4/ltversion.m4
 m4/lt~obsolete.m4
+org.lightmediascanner.service
index bfb141b..709763e 100644 (file)
@@ -24,3 +24,8 @@ pkgconfigdir = $(libdir)/pkgconfig
 pkgconfig_DATA = lightmediascanner.pc
 
 ACLOCAL_AMFLAGS = -I m4
+
+if BUILD_DAEMON
+servicedir = @dbusservicedir@
+service_DATA = org.lightmediascanner.service
+endif
index 5d109c2..7cf8b40 100644 (file)
@@ -119,6 +119,25 @@ AC_LMS_OPTIONAL_MODULE([flac], true, [CHECK_MODULE_FLAC])
 AC_LMS_OPTIONAL_MODULE([wave], true)
 
 
+AC_ARG_ENABLE([daemon],
+        [AC_HELP_STRING([--disable-daemon],
+                [Disable DBus scanner daemon. @<:@default=enable@:>@])],
+        [build_daemon=${enableval}], [build_daemon=yes])
+
+dbusservicedir="${datadir}/dbus-1/services"
+AC_ARG_WITH([dbus-services],
+        [AC_HELP_STRING([--with-dbus-services=DBUS_SERVICES],
+                [Specify a directory to store dbus service files.])],
+        [dbusservicedir=$withval])
+AC_SUBST(dbusservicedir)
+
+dbusdir=""
+if test "$build_daemon" = "yes"; then
+        PKG_CHECK_MODULES(GIO, [gio-2.0 >= 2.32])
+fi
+AM_CONDITIONAL([BUILD_DAEMON], [test "$build_daemon" = "yes"])
+
+
 #####################################################################
 # Default CFLAGS and LDFLAGS
 #####################################################################
@@ -178,6 +197,7 @@ AC_SUBST([GCLDFLAGS], $with_ldflags)
 AC_OUTPUT([
 lightmediascanner.pc
 lightmediascanner.spec
+org.lightmediascanner.service
 Makefile
 m4/Makefile
 src/Makefile
@@ -213,3 +233,4 @@ Summary:
 SUMMARY_EOF
 
 echo -e " * modules........: $MODS $UNUSED_MODS"
+echo -e " * daemon.........: ${build_daemon}"
diff --git a/org.lightmediascanner.service.in b/org.lightmediascanner.service.in
new file mode 100644 (file)
index 0000000..7c50f24
--- /dev/null
@@ -0,0 +1,3 @@
+[D-BUS Service]
+Name=org.lightmediascanner
+Exec=@prefix@/bin/lightmediascannerd
index eee8ab6..be0f1fa 100644 (file)
@@ -1,2 +1,4 @@
 test
 list-parsers
+lightmediascannerd
+lightmediascannerctl
index cf893d4..adab67d 100644 (file)
@@ -21,3 +21,21 @@ list_parsers_SOURCES = list-parsers.c
 list_parsers_LDADD = $(top_builddir)/src/lib/liblightmediascanner.la \
        @SQLITE3_LIBS@
 list_parsers_DEPENDENCIES = $(top_builddir)/src/lib/liblightmediascanner.la
+
+
+if BUILD_DAEMON
+bin_PROGRAMS = lightmediascannerd lightmediascannerctl
+
+lightmediascannerd_SOURCES = lightmediascannerd.c
+lightmediascannerd_CPPFLAGS = $(AM_CPPFLAGS) @GIO_CFLAGS@
+lightmediascannerd_LDADD = \
+       $(top_builddir)/src/lib/liblightmediascanner.la \
+       @SQLITE3_LIBS@ \
+       @GIO_LIBS@
+lightmediascannerd_DEPENDENCIES = \
+       $(top_builddir)/src/lib/liblightmediascanner.la
+
+lightmediascannerctl_SOURCES = lightmediascannerctl.c
+lightmediascannerctl_CPPFLAGS = $(AM_CPPFLAGS) @GIO_CFLAGS@
+lightmediascannerctl_LDADD = @GIO_LIBS@
+endif
diff --git a/src/bin/lightmediascannerctl.c b/src/bin/lightmediascannerctl.c
new file mode 100644 (file)
index 0000000..f1b3b7c
--- /dev/null
@@ -0,0 +1,487 @@
+#include <gio/gio.h>
+#include <stdlib.h>
+#include <stdio.h>
+#include <string.h>
+#include <time.h>
+
+struct app {
+    int ret;
+    int argc;
+    char **argv;
+    void (*action)(struct app *app);
+    GDBusProxy *proxy;
+    GMainLoop *loop;
+    GTimer *timer;
+};
+
+static void
+print_server(GDBusProxy *proxy)
+{
+    char **props, **itr;
+    char *nameowner;
+
+    nameowner = g_dbus_proxy_get_name_owner(proxy);
+    if (!nameowner) {
+        puts("Server is not running.");
+        return;
+    }
+
+    printf("Server at %s\n", nameowner);
+    props = g_dbus_proxy_get_cached_property_names(proxy);
+    if (!props)
+        return;
+
+    for (itr = props; *itr != NULL; itr++) {
+        GVariant *value = g_dbus_proxy_get_cached_property(proxy, *itr);
+        char *str = g_variant_print(value, TRUE);
+        printf("\t%s = %s\n", *itr, str);
+        g_variant_unref(value);
+        g_free(str);
+    }
+    g_strfreev(props);
+    g_free(nameowner);
+}
+
+
+static void
+do_status(struct app *app)
+{
+    print_server(app->proxy);
+    g_main_loop_quit(app->loop);
+    app->ret = EXIT_SUCCESS;
+}
+
+static void
+on_properties_changed(GDBusProxy *proxy, GVariant *changed, const char *const *invalidated, gpointer user_data)
+{
+    struct app *app = user_data;
+
+    printf("%015.3f --- Properties Changed ---\n",
+           g_timer_elapsed(app->timer, NULL));
+
+    if (g_variant_n_children(changed) > 0) {
+        GVariantIter *itr;
+        const char *prop;
+        GVariant *value;
+
+        printf("Changed Properties:");
+        g_variant_get(changed, "a{sv}", &itr);
+        while (g_variant_iter_loop(itr, "{&sv}", &prop, &value)) {
+            char *str;
+            str = g_variant_print(value, TRUE);
+            printf(" %s=%s", prop, str);
+            g_free(str);
+        }
+        g_variant_iter_free(itr);
+        printf("\n");
+    }
+
+    if (invalidated[0] != NULL) {
+        const char * const *itr;
+        printf("Invalidated Properties:");
+        for (itr = invalidated; *itr != NULL; itr++)
+            printf(" %s", *itr);
+        printf("\n");
+    }
+
+    print_server(proxy);
+}
+
+static gboolean
+do_delayed_print_server(gpointer data)
+{
+    GDBusProxy *proxy = data;
+    char **props;
+    char *nameowner;
+
+    nameowner = g_dbus_proxy_get_name_owner(proxy);
+    if (!nameowner) {
+        print_server(proxy);
+        return FALSE;
+    }
+    g_free(nameowner);
+
+    props = g_dbus_proxy_get_cached_property_names(proxy);
+    if (!props) {
+        g_timeout_add(1000, do_delayed_print_server, proxy);
+        return FALSE;
+    }
+
+    g_strfreev(props);
+    print_server(data);
+    return FALSE;
+}
+
+static void
+on_name_owner_notify(GObject *object, GParamSpec *pspec, gpointer user_data)
+{
+    GDBusProxy *proxy = G_DBUS_PROXY(object);
+    struct app *app = user_data;
+
+    printf("%015.3f --- Name Owner Changed ---\n",
+           g_timer_elapsed(app->timer, NULL));
+    do_delayed_print_server(proxy);
+}
+
+static void
+do_monitor(struct app *app)
+{
+    app->timer = g_timer_new();
+    g_timer_start(app->timer);
+
+    print_server(app->proxy);
+    g_signal_connect(app->proxy, "g-properties-changed",
+                     G_CALLBACK(on_properties_changed),
+                     app);
+    g_signal_connect(app->proxy, "notify::g-name-owner",
+                     G_CALLBACK(on_name_owner_notify),
+                     app);
+}
+
+static void
+on_properties_changed_check_lock(GDBusProxy *proxy, GVariant *changed, const char *const *invalidated, gpointer user_data)
+{
+    struct app *app = user_data;
+    gboolean lost_lock = FALSE;
+
+    if (g_variant_n_children(changed) > 0) {
+        GVariantIter *itr;
+        const char *prop;
+        GVariant *value;
+
+        g_variant_get(changed, "a{sv}", &itr);
+        while (g_variant_iter_loop(itr, "{&sv}", &prop, &value)) {
+            if (strcmp(prop, "WriteLocked") == 0) {
+                if (!g_variant_get_boolean(value))
+                    lost_lock = TRUE;
+                break;
+            }
+        }
+        g_variant_iter_free(itr);
+    }
+
+    if (invalidated[0] != NULL) {
+        const char * const *itr;
+        for (itr = invalidated; *itr != NULL; itr++) {
+            if (strcmp(*itr, "WriteLocked") == 0) {
+                lost_lock = TRUE;
+                break;
+            }
+        }
+    }
+
+    if (lost_lock) {
+        fputs("Lost lock, exit.\n", stderr);
+        app->ret = EXIT_FAILURE;
+        g_main_loop_quit(app->loop);
+    }
+}
+
+static void
+do_write_lock(struct app *app)
+{
+    GVariant *ret;
+    GError *error = NULL;
+    char *nameowner;
+
+    nameowner = g_dbus_proxy_get_name_owner(app->proxy);
+    if (!nameowner) {
+        fputs("Server is not running, cannot get write lock!\n", stderr);
+        app->ret = EXIT_FAILURE;
+        g_main_loop_quit(app->loop);
+        return;
+    }
+
+    printf("Server at %s, try to get write lock\n", nameowner);
+    g_free(nameowner);
+
+    g_signal_connect(app->proxy, "g-properties-changed",
+                     G_CALLBACK(on_properties_changed_check_lock),
+                     app);
+
+    ret = g_dbus_proxy_call_sync(app->proxy, "RequestWriteLock",
+                                 g_variant_new("()"),
+                                 G_DBUS_CALL_FLAGS_NONE, -1, NULL,
+                                 &error);
+
+    if (!ret) {
+        fprintf(stderr, "Could not get write lock: %s\n", error->message);
+        g_error_free(error);
+        app->ret = EXIT_FAILURE;
+        g_main_loop_quit(app->loop);
+        return;
+    }
+
+    g_variant_unref(ret);
+    puts("Got write lock, close program to release it.");
+}
+
+static void
+on_properties_changed_check_scan(GDBusProxy *proxy, GVariant *changed, const char *const *invalidated, gpointer user_data)
+{
+    struct app *app = user_data;
+
+    if (g_variant_n_children(changed) > 0) {
+        GVariantIter *itr;
+        const char *prop;
+        GVariant *value;
+
+        g_variant_get(changed, "a{sv}", &itr);
+        while (g_variant_iter_loop(itr, "{&sv}", &prop, &value)) {
+            if (strcmp(prop, "IsScanning") == 0) {
+                if (g_variant_get_boolean(value) && !app->timer) {
+                    app->timer = g_timer_new();
+                    g_timer_start(app->timer);
+                } else if (!g_variant_get_boolean(value) && app->timer) {
+                    g_timer_stop(app->timer);
+                    app->ret = EXIT_SUCCESS;
+                    g_main_loop_quit(app->loop);
+                }
+                break;
+            }
+        }
+        g_variant_iter_free(itr);
+    }
+
+    if (invalidated[0] != NULL) {
+        const char * const *itr;
+        for (itr = invalidated; *itr != NULL; itr++) {
+            if (strcmp(*itr, "IsScanning") == 0) {
+                fputs("Lost server, exit.\n", stderr);
+                app->ret = EXIT_FAILURE;
+                g_main_loop_quit(app->loop);
+                break;
+            }
+        }
+    }
+}
+
+static void
+populate_scan_params(gpointer key, gpointer value, gpointer user_data)
+{
+    const char *category = key;
+    const GArray *paths = value;
+    GVariantBuilder *builder = user_data;
+    GVariantBuilder *sub;
+    char **itr;
+
+    sub = g_variant_builder_new(G_VARIANT_TYPE("as"));
+    for (itr = (char **)paths->data; *itr != NULL; itr++)
+        g_variant_builder_add(sub, "s", *itr);
+
+    g_variant_builder_add(builder, "{sv}", category, g_variant_builder_end(sub));
+    g_variant_builder_unref(sub);
+}
+
+static void
+do_free_array(gpointer data)
+{
+    g_array_free(data, TRUE);
+}
+
+static void
+do_scan(struct app *app)
+{
+    GVariantBuilder *builder;
+    GVariant *ret;
+    GError *error = NULL;
+    char *nameowner;
+    int i;
+    GHashTable *categories;
+
+    nameowner = g_dbus_proxy_get_name_owner(app->proxy);
+    if (!nameowner) {
+        fputs("Server is not running, cannot start scan!\n", stderr);
+        app->ret = EXIT_FAILURE;
+        g_main_loop_quit(app->loop);
+        return;
+    }
+
+    printf("Server at %s, try to start scan\n", nameowner);
+    g_free(nameowner);
+
+    g_signal_connect(app->proxy, "g-properties-changed",
+                     G_CALLBACK(on_properties_changed_check_scan),
+                     app);
+
+    categories = g_hash_table_new_full(g_str_hash, g_str_equal, NULL, do_free_array);
+    for (i = 0; i < app->argc; i++) {
+        GArray *arr;
+        char *arg = app->argv[i];
+        char *sep = strchr(arg, ':');
+        char *path;
+
+
+        if (!sep) {
+            fprintf(stderr, "Ignored scan parameter: invalid format '%s'\n",
+                    arg);
+            continue;
+        }
+
+        *sep = '\0';
+        path = sep + 1;
+
+        arr = g_hash_table_lookup(categories, arg);
+        if (!arr) {
+            arr = g_array_new(TRUE, FALSE, sizeof(char *));
+            g_hash_table_insert(categories, arg, arr);
+        }
+
+        if (path[0])
+            g_array_append_val(arr, path);
+    }
+
+    builder = g_variant_builder_new(G_VARIANT_TYPE("a{sv}"));
+    g_hash_table_foreach(categories, populate_scan_params, builder);
+
+    ret = g_dbus_proxy_call_sync(app->proxy, "Scan",
+                                 g_variant_new("(a{sv})", builder),
+                                 G_DBUS_CALL_FLAGS_NONE, -1, NULL,
+                                 &error);
+    g_variant_builder_unref(builder);
+    g_hash_table_destroy(categories);
+
+    if (!ret) {
+        fprintf(stderr, "Could not start scan: %s\n", error->message);
+        g_error_free(error);
+        app->ret = EXIT_FAILURE;
+        g_main_loop_quit(app->loop);
+        return;
+    }
+
+    g_variant_unref(ret);
+}
+
+static void
+do_stop(struct app *app)
+{
+    GVariant *ret;
+    GError *error = NULL;
+    char *nameowner;
+
+    nameowner = g_dbus_proxy_get_name_owner(app->proxy);
+    if (!nameowner) {
+        fputs("Server is not running, cannot stop scan!\n", stderr);
+        app->ret = EXIT_FAILURE;
+        g_main_loop_quit(app->loop);
+        return;
+    }
+
+    printf("Server at %s, try to stop scan\n", nameowner);
+    g_free(nameowner);
+
+    ret = g_dbus_proxy_call_sync(app->proxy, "Stop",
+                                 g_variant_new("()"),
+                                 G_DBUS_CALL_FLAGS_NONE, -1, NULL,
+                                 &error);
+
+    if (!ret) {
+        fprintf(stderr, "Could not stop scan: %s\n", error->message);
+        g_error_free(error);
+        app->ret = EXIT_FAILURE;
+        g_main_loop_quit(app->loop);
+        return;
+    }
+
+    app->ret = EXIT_SUCCESS;
+    g_main_loop_quit(app->loop);
+    g_variant_unref(ret);
+}
+
+static gboolean
+do_action(gpointer data)
+{
+    struct app *app = data;
+    app->action(app);
+    return FALSE;
+}
+
+static void
+print_help(const char *prog)
+{
+    printf("Usage:\n"
+           "\t%s <action>\n"
+           "\n"
+           "Action is one of:\n"
+           "\tstatus          print server properties and exit.\n"
+           "\tmonitor         monitor server and its properties.\n"
+           "\twrite-lock      try to get a write-lock and keep it while running.\n"
+           "\tscan [params]   start scan. May receive parameters as a series of \n"
+           "\t                CATEGORY:PATH to limit scan.\n"
+           "\tstop            stop ongoing scan.\n"
+           "\thelp            this message.\n"
+           "\n",
+           prog);
+}
+
+int
+main(int argc, char *argv[])
+{
+    GError *error = NULL;
+    struct app app;
+    int i;
+
+    if (argc < 2) {
+        fprintf(stderr, "Missing action, see --help.\n");
+        return EXIT_FAILURE;
+    }
+
+    for (i = 1; i < argc; i++) {
+        if (strcmp(argv[i], "-h") == 0 || strcmp(argv[i], "--help") == 0) {
+            print_help(argv[0]);
+            return EXIT_SUCCESS;
+        }
+    }
+
+    if (strcmp(argv[1], "status") == 0)
+        app.action = do_status;
+    else if (strcmp(argv[1], "monitor") == 0)
+        app.action = do_monitor;
+    else if (strcmp(argv[1], "write-lock") == 0)
+        app.action = do_write_lock;
+    else if (strcmp(argv[1], "scan") == 0)
+        app.action = do_scan;
+    else if (strcmp(argv[1], "stop") == 0)
+        app.action = do_stop;
+    else if (strcmp(argv[1], "help") == 0) {
+        print_help(argv[0]);
+        return EXIT_SUCCESS;
+    } else {
+        fprintf(stderr, "Unknown action '%s', see --help.\n", argv[1]);
+        return EXIT_FAILURE;
+    }
+
+    app.timer = NULL;
+    app.loop = g_main_loop_new(NULL, FALSE);
+    app.proxy = g_dbus_proxy_new_for_bus_sync(G_BUS_TYPE_SESSION,
+                                              G_DBUS_PROXY_FLAGS_NONE,
+                                              NULL,
+                                              "org.lightmediascanner",
+                                              "/org/lightmediascanner/Scanner1",
+                                              "org.lightmediascanner.Scanner1",
+                                              NULL,
+                                              &error);
+    if (error) {
+        g_error("Could not create proxy: %s", error->message);
+        g_error_free(error);
+        return EXIT_FAILURE;
+    }
+
+    app.argc = argc - 2;
+    app.argv = argv + 2;
+    app.ret = EXIT_SUCCESS;
+
+    g_idle_add(do_action, &app);
+
+    g_main_loop_run(app.loop);
+    g_object_unref(app.proxy);
+    g_main_loop_unref(app.loop);
+
+    if (app.timer) {
+        printf("Elapsed time: %0.3f seconds\n",
+               g_timer_elapsed(app.timer, NULL));
+        g_timer_destroy(app.timer);
+    }
+
+    return app.ret;
+}
diff --git a/src/bin/lightmediascannerd.c b/src/bin/lightmediascannerd.c
new file mode 100644 (file)
index 0000000..606860d
--- /dev/null
@@ -0,0 +1,1313 @@
+#include "lightmediascanner.h"
+#include <gio/gio.h>
+#include <glib-unix.h>
+#include <stdlib.h>
+#include <string.h>
+#include <stdio.h>
+#include <sqlite3.h>
+
+static char *db_path = NULL;
+static char **charsets = NULL;
+static GHashTable *categories = NULL;
+static int commit_interval = 100;
+static int slave_timeout = 60;
+static gboolean vacuum = FALSE;
+static gboolean startup_scan = FALSE;
+
+static GDBusNodeInfo *introspection_data = NULL;
+
+static const char BUS_PATH[] = "/org/lightmediascanner/Scanner1";
+static const char BUS_IFACE[] = "org.lightmediascanner.Scanner1";
+
+static const char introspection_xml[] =
+    "<node>"
+    "  <interface name=\"org.lightmediascanner.Scanner1\">"
+    "    <property name=\"IsScanning\" type=\"b\" access=\"read\" />"
+    "    <property name=\"WriteLocked\" type=\"b\" access=\"read\" />"
+    "    <property name=\"UpdateID\" type=\"t\" access=\"read\" />"
+    "    <property name=\"Categories\" type=\"a{sv}\" access=\"read\" />"
+    "    <method name=\"Scan\">"
+    "      <arg direction=\"in\" type=\"a{sv}\" name=\"specification\" />"
+    "    </method>"
+    "    <method name=\"Stop\" />"
+    "    <method name=\"RequestWriteLock\" />"
+    "    <method name=\"ReleaseWriteLock\" />"
+    "  </interface>"
+    "</node>";
+
+typedef struct scanner_category
+{
+    char *category;
+    GArray *parsers;
+    GArray *dirs;
+} scanner_category_t;
+
+typedef struct scanner_pending
+{
+    char *category;
+    GList *paths;
+} scanner_pending_t;
+
+typedef struct scanner {
+    GDBusConnection *conn;
+    char *write_lock;
+    unsigned write_lock_name_watcher;
+    GDBusMethodInvocation *pending_stop;
+    GList *pending_scan; /* of scanner_pending_t, see scanner_thread_work */
+    GThread *thread; /* see scanner_thread_work */
+    unsigned cleanup_thread_idler; /* see scanner_thread_work */
+    guint64 update_id;
+    struct {
+        unsigned idler; /* not a flag, but g_source tag */
+        unsigned is_scanning : 1;
+        unsigned write_locked : 1;
+        unsigned update_id : 1;
+        unsigned categories: 1;
+    } changed_props;
+} scanner_t;
+
+static scanner_category_t *
+scanner_category_new(const char *category)
+{
+    scanner_category_t *sc = g_new0(scanner_category_t, 1);
+
+    sc->category = g_strdup(category);
+    sc->parsers = g_array_new(TRUE, TRUE, sizeof(char *));
+    sc->dirs = g_array_new(TRUE, TRUE, sizeof(char *));
+
+    return sc;
+}
+
+static void
+scanner_category_destroy(scanner_category_t *sc)
+{
+    char **itr;
+
+    for (itr = (char **)sc->parsers->data; *itr != NULL; itr++)
+        g_free(*itr);
+    for (itr = (char **)sc->dirs->data; *itr != NULL; itr++)
+        g_free(*itr);
+
+    g_array_free(sc->parsers, TRUE);
+    g_array_free(sc->dirs, TRUE);
+    g_free(sc->category);
+
+    g_free(sc);
+}
+
+static void scanner_release_write_lock(scanner_t *scanner);
+
+static void
+scanner_write_lock_vanished(GDBusConnection *conn, const char *name, gpointer data)
+{
+    scanner_t *scanner = data;
+    g_warning("Write lock holder %s vanished, release lock\n", name);
+    scanner_release_write_lock(scanner);
+}
+
+static guint64
+get_update_id(void)
+{
+    const char sql[] = "SELECT version FROM lms_internal WHERE tab='update_id'";
+    sqlite3 *db;
+    sqlite3_stmt *stmt;
+    int ret;
+    guint64 update_id = 0;
+
+    ret = sqlite3_open_v2(db_path, &db, SQLITE_OPEN_READONLY, NULL);
+    if (ret != SQLITE_OK) {
+        g_warning("Couldn't open '%s': %s", db_path, sqlite3_errmsg(db));
+        goto end;
+    }
+
+    if (sqlite3_prepare_v2(db, sql, -1, &stmt, NULL) != SQLITE_OK) {
+        g_warning("Couldn't get update_id from %s: %s",
+                  db_path, sqlite3_errmsg(db));
+        goto end;
+    }
+
+    ret = sqlite3_step(stmt);
+    if (ret ==  SQLITE_DONE)
+        update_id = 0;
+    else if (ret == SQLITE_ROW)
+        update_id = sqlite3_column_int(stmt, 0);
+    else
+        g_warning("Couldn't run SQL to get update_id, ret=%d: %s",
+                  ret, sqlite3_errmsg(db));
+
+    sqlite3_reset(stmt);
+    sqlite3_finalize(stmt);
+
+end:
+    sqlite3_close(db);
+
+    g_debug("update id: %llu", (unsigned long long)update_id);
+    return update_id;
+}
+
+static void
+do_vacuum(void)
+{
+    const char sql[] = "VACUUM";
+    sqlite3 *db;
+    sqlite3_stmt *stmt;
+    int ret;
+
+    ret = sqlite3_open_v2(db_path, &db, SQLITE_OPEN_READWRITE, NULL);
+    if (ret != SQLITE_OK) {
+        g_warning("Couldn't open '%s': %s", db_path, sqlite3_errmsg(db));
+        goto end;
+    }
+
+    if (sqlite3_prepare_v2(db, sql, -1, &stmt, NULL) != SQLITE_OK) {
+        g_warning("Couldn't vacuum from %s: %s",
+                  db_path, sqlite3_errmsg(db));
+        goto end;
+    }
+
+    ret = sqlite3_step(stmt);
+    if (ret != SQLITE_DONE)
+        g_warning("Couldn't run SQL VACUUM, ret=%d: %s",
+                  ret, sqlite3_errmsg(db));
+
+    sqlite3_reset(stmt);
+    sqlite3_finalize(stmt);
+
+end:
+    sqlite3_close(db);
+}
+
+static gboolean
+check_write_locked(const scanner_t *scanner)
+{
+    return scanner->write_lock != NULL || scanner->thread != NULL;
+}
+
+static void
+category_variant_foreach(gpointer key, gpointer value, gpointer user_data)
+{
+    scanner_category_t *sc = value;
+    GVariantBuilder *builder = user_data;
+    GVariantBuilder *sub, *darr, *parr;
+    char **itr;
+
+    darr = g_variant_builder_new(G_VARIANT_TYPE("as"));
+    for (itr = (char **)sc->dirs->data; *itr != NULL; itr++)
+        g_variant_builder_add(darr, "s", *itr);
+
+    parr = g_variant_builder_new(G_VARIANT_TYPE("as"));
+    for (itr = (char **)sc->parsers->data; *itr != NULL; itr++)
+        g_variant_builder_add(parr, "s", *itr);
+
+    sub = g_variant_builder_new(G_VARIANT_TYPE("a{sv}"));
+    g_variant_builder_add(sub, "{sv}", "dirs", g_variant_builder_end(darr));
+    g_variant_builder_add(sub, "{sv}", "parsers", g_variant_builder_end(parr));
+
+    g_variant_builder_add(builder, "{sv}", sc->category,
+                          g_variant_builder_end(sub));
+
+    g_variant_builder_unref(sub);
+    g_variant_builder_unref(parr);
+    g_variant_builder_unref(darr);
+}
+
+static GVariant *
+categories_get_variant(void)
+{
+    GVariantBuilder *builder;
+    GVariant *variant;
+
+    builder = g_variant_builder_new(G_VARIANT_TYPE("a{sv}"));
+
+    g_hash_table_foreach(categories, category_variant_foreach, builder);
+
+    variant = g_variant_builder_end(builder);
+    g_variant_builder_unref(builder);
+
+    return variant;
+}
+
+static gboolean
+scanner_dbus_props_changed(gpointer data)
+{
+    GVariantBuilder *builder;
+    GError *error = NULL;
+    scanner_t *scanner = data;
+    guint64 update_id = 0;
+
+    if (!check_write_locked(scanner))
+        update_id = get_update_id();
+    if (update_id > 0 && update_id != scanner->update_id) {
+        scanner->changed_props.update_id = TRUE;
+        scanner->update_id = update_id;
+    }
+
+    builder = g_variant_builder_new(G_VARIANT_TYPE_ARRAY);
+
+    if (scanner->changed_props.is_scanning) {
+        scanner->changed_props.is_scanning = FALSE;
+        g_variant_builder_add(builder, "{sv}", "IsScanning",
+                              g_variant_new_boolean(scanner->thread != NULL));
+    }
+    if (scanner->changed_props.write_locked) {
+        scanner->changed_props.write_locked = FALSE;
+        g_variant_builder_add(
+            builder, "{sv}", "WriteLocked",
+            g_variant_new_boolean(check_write_locked(scanner)));
+    }
+    if (scanner->changed_props.update_id) {
+        scanner->changed_props.update_id = FALSE;
+        g_variant_builder_add(builder, "{sv}", "UpdateID",
+                              g_variant_new_uint64(scanner->update_id));
+    }
+    if (scanner->changed_props.categories) {
+        scanner->changed_props.categories = FALSE;
+        g_variant_builder_add(builder, "{sv}", "Categories",
+                              categories_get_variant());
+    }
+
+    g_dbus_connection_emit_signal(scanner->conn,
+                                  NULL,
+                                  BUS_PATH,
+                                  "org.freedesktop.DBus.Properties",
+                                  "PropertiesChanged",
+                                  g_variant_new("(sa{sv}as)",
+                                                BUS_IFACE, builder, NULL),
+                                  &error);
+    g_variant_builder_unref(builder);
+    g_assert_no_error(error);
+
+    scanner->changed_props.idler = 0;
+    return FALSE;
+}
+
+static void
+scanner_write_lock_changed(scanner_t *scanner)
+{
+    if (scanner->changed_props.idler == 0)
+        scanner->changed_props.idler = g_idle_add(scanner_dbus_props_changed,
+                                                  scanner);
+
+    scanner->changed_props.write_locked = TRUE;
+}
+
+static void
+scanner_acquire_write_lock(scanner_t *scanner, const char *sender)
+{
+    g_debug("acquired write lock for %s", sender);
+    scanner->write_lock = g_strdup(sender);
+    scanner->write_lock_name_watcher = g_bus_watch_name_on_connection(
+        scanner->conn, sender, G_BUS_NAME_WATCHER_FLAGS_NONE,
+        NULL, scanner_write_lock_vanished, scanner, NULL);
+
+    scanner_write_lock_changed(scanner);
+}
+
+static void
+scanner_release_write_lock(scanner_t *scanner)
+{
+    g_debug("release write lock previously owned by %s", scanner->write_lock);
+
+    g_free(scanner->write_lock);
+    scanner->write_lock = NULL;
+
+    g_bus_unwatch_name(scanner->write_lock_name_watcher);
+    scanner->write_lock_name_watcher = 0;
+
+    scanner_write_lock_changed(scanner);
+}
+
+static void
+scanner_pending_free(scanner_pending_t *pending)
+{
+    g_list_free_full(pending->paths, g_free);
+    g_free(pending->category);
+    g_free(pending);
+}
+
+static scanner_pending_t *
+scanner_pending_get_or_add(scanner_t *scanner, const char *category)
+{
+    scanner_pending_t *pending;
+    GList *n, *nlast = NULL;
+
+    g_assert(scanner->thread == NULL);
+
+    for (n = scanner->pending_scan; n != NULL; n = n->next) {
+        nlast = n;
+        pending = n->data;
+        if (strcmp(pending->category, category) == 0)
+            return pending;
+    }
+
+    pending = g_new0(scanner_pending_t, 1);
+    pending->category = g_strdup(category);
+    pending->paths = NULL;
+
+    /* I can't believe there is no g_list_insert_after() :-( */
+    n = g_list_alloc();
+    n->data = pending;
+    n->next = NULL;
+    n->prev = nlast;
+
+    if (nlast)
+        nlast->next = n;
+    else
+        scanner->pending_scan = n;
+
+    return pending;
+}
+
+/* NOTE:  assumes array was already validated for duplicates/restrictions */
+static void
+scanner_pending_add_all(scanner_pending_t *pending, const GArray *arr)
+{
+    char **itr;
+
+
+    for (itr = (char **)arr->data; *itr != NULL; itr++)
+        pending->paths = g_list_prepend(pending->paths, g_strdup(*itr));
+
+    pending->paths = g_list_reverse(pending->paths);
+}
+
+static void
+scanner_pending_add(scanner_pending_t *pending, const GArray *restrictions, const char *path)
+{
+    GList *n, *nlast;
+    const char * const *itr;
+    gboolean allowed = FALSE;
+
+    for (n = pending->paths; n != NULL; n = n->next) {
+        const char *other = n->data;
+        if (g_str_has_prefix(path, other)) {
+            g_debug("Path already in pending scan in category %s: %s (%s)",
+                    pending->category, path, other);
+            return;
+        }
+    }
+
+    for (itr = (const char *const*)restrictions->data; *itr != NULL; itr++) {
+        if (g_str_has_prefix(path, *itr)) {
+            allowed = TRUE;
+            break;
+        }
+    }
+    if (!allowed) {
+        g_warning("Path is outside of category %s directories: %s",
+                  pending->category, path);
+        return;
+    }
+
+    nlast = NULL;
+    for (n = pending->paths; n != NULL; ) {
+        char *other = n->data;
+
+        nlast = n;
+
+        if (!g_str_has_prefix(other, path))
+            n = n->next;
+        else {
+            GList *tmp;
+
+            g_debug("Path covers previous pending scan in category %s, "
+                    "replace %s (%s)",
+                    pending->category, other, path);
+
+            tmp = n->next;
+            nlast = n->next ? n->next : n->prev;
+
+            pending->paths = g_list_delete_link(pending->paths, n);
+            g_free(other);
+
+            n = tmp;
+        }
+    }
+
+    g_debug("New scan path for category %s: %s", pending->category, path);
+
+    /* I can't believe there is no g_list_insert_after() :-( */
+    n = g_list_alloc();
+    n->data = g_strdup(path);
+    n->next = NULL;
+    n->prev = nlast;
+
+    if (nlast)
+        nlast->next = n;
+    else
+        pending->paths = n;
+}
+
+static void
+scan_params_all(gpointer key, gpointer value, gpointer user_data)
+{
+    scanner_pending_t *pending;
+    scanner_category_t *sc = value;
+    scanner_t *scanner = user_data;
+
+    pending = scanner_pending_get_or_add(scanner, sc->category);
+    scanner_pending_add_all(pending, sc->dirs);
+}
+
+static void
+dbus_scanner_scan_params_set(scanner_t *scanner, GVariant *params)
+{
+    GVariantIter *itr;
+    GVariant *el;
+    char *cat;
+    gboolean empty = TRUE;
+
+    g_variant_get(params, "(a{sv})", &itr);
+    while (g_variant_iter_loop(itr, "{sv}", &cat, &el)) {
+        scanner_category_t *sc;
+        scanner_pending_t *pending;
+        GVariantIter *subitr;
+        char *path;
+        gboolean nodirs = TRUE;
+
+        sc = g_hash_table_lookup(categories, cat);
+        if (!sc) {
+            g_warning("Unexpected scan category: %s, skipped.", cat);
+            continue;
+        }
+
+        pending = scanner_pending_get_or_add(scanner, cat);
+        empty = FALSE;
+
+        g_variant_get(el, "as", &subitr);
+        while (g_variant_iter_loop(subitr, "s", &path)) {
+            scanner_pending_add(pending, sc->dirs, path);
+            nodirs = FALSE;
+        }
+
+        if (nodirs)
+            scanner_pending_add_all(pending, sc->dirs);
+    }
+    g_variant_iter_free(itr);
+
+    if (empty)
+        g_hash_table_foreach(categories, scan_params_all, scanner);
+}
+
+static void
+scanner_is_scanning_changed(scanner_t *scanner)
+{
+    if (scanner->changed_props.idler == 0)
+        scanner->changed_props.idler = g_idle_add(scanner_dbus_props_changed,
+                                                  scanner);
+
+    scanner->changed_props.is_scanning = TRUE;
+}
+
+static void
+scan_progress_cb(lms_t *lms, const char *path, int pathlen, lms_progress_status_t status, void *data)
+{
+    const scanner_t *scanner = data;
+    if (scanner->pending_stop)
+        lms_stop_processing(lms);
+}
+
+static lms_t *
+setup_lms(const char *category, const scanner_t *scanner)
+{
+    scanner_category_t *sc;
+    char **itr;
+    lms_t *lms;
+
+    sc = g_hash_table_lookup(categories, category);
+    if (!sc) {
+        g_error("Unknown category %s", category);
+        return NULL;
+    }
+
+    if (sc->parsers->len == 0) {
+        g_warning("No parsers for category %s", category);
+        return NULL;
+    }
+
+    lms = lms_new(db_path);
+    if (!lms) {
+        g_warning("Failed to create lms");
+        return NULL;
+    }
+
+    lms_set_commit_interval(lms, commit_interval);
+    lms_set_slave_timeout(lms, slave_timeout * 1000);
+
+    if (charsets) {
+        for (itr = charsets; *itr != NULL; itr++)
+            if (lms_charset_add(lms, *itr) != 0)
+                g_warning("Couldn't add charset: %s", *itr);
+    }
+
+    for (itr = (char **)sc->parsers->data; *itr != NULL; itr++) {
+        const char *parser = *itr;
+        lms_plugin_t *plugin;
+
+        if (parser[0] == '/')
+            plugin = lms_parser_add(lms, parser);
+        else
+            plugin = lms_parser_find_and_add(lms, parser);
+
+        if (!plugin)
+            g_warning("Couldn't add parser: %s", parser);
+    }
+
+    lms_set_progress_callback(lms, scan_progress_cb, scanner, NULL);
+
+    return lms;
+}
+
+static gboolean
+scanner_thread_cleanup(gpointer data)
+{
+    scanner_t *scanner = data;
+    gpointer ret;
+
+    g_debug("cleanup scanner work thread");
+    ret = g_thread_join(scanner->thread);
+    g_assert(ret == scanner);
+
+    scanner->thread = NULL;
+    scanner->cleanup_thread_idler = 0;
+
+    if (scanner->pending_stop) {
+        g_dbus_method_invocation_return_value(scanner->pending_stop, NULL);
+        g_object_unref(scanner->pending_stop);
+        scanner->pending_stop =  NULL;
+    }
+
+    g_list_free_full(scanner->pending_scan,
+                     (GDestroyNotify)scanner_pending_free);
+    scanner->pending_scan = NULL;
+
+    scanner_is_scanning_changed(scanner);
+    scanner_write_lock_changed(scanner);
+
+    return FALSE;
+}
+
+/*
+ * Note on thread usage and locks (or lack of locks):
+ *
+ * The main thread is responsible for launching the worker thread,
+ * setting 'scanner->thread' pointer, which is later checked *ONLY* by
+ * main thread. When the thread is done, it will notify the main
+ * thread with scanner_thread_cleanup() so it can unset the pointer
+ * and do whatever it needs, so 'scanner->thread' is exclusively
+ * managed by main thread.
+ *
+ * The other shared data 'scanner->pending_scan' is managed by the
+ * main thread only when 'scanner->thread' is unset. If there is a
+ * worker thread the main thread should never touch that list, thus
+ * there is *NO NEED FOR LOCKS*.
+ *
+ * The thread will stop its work by checking 'scanner->pending_stop',
+ * this is also done without a lock as there is no need for such thing
+ * given above. The stop is also voluntary and it can happen on a
+ * second iteration of work.
+ */
+static gpointer
+scanner_thread_work(gpointer data)
+{
+    GList *lst;
+    scanner_t *scanner = data;
+
+    g_debug("started scanner thread");
+
+    lst = scanner->pending_scan;
+    scanner->pending_scan = NULL;
+    while (lst) {
+        scanner_pending_t *pending;
+        lms_t *lms;
+
+        if (scanner->pending_stop)
+            break;
+
+        pending = lst->data;
+        lst = g_list_delete_link(lst, lst);
+
+        g_debug("scan category: %s", pending->category);
+        lms = setup_lms(pending->category, scanner);
+        if (lms) {
+            while (pending->paths) {
+                char *path;
+
+                if (scanner->pending_stop)
+                    break;
+
+                path = pending->paths->data;
+                pending->paths = g_list_delete_link(pending->paths,
+                                                    pending->paths);
+
+                g_debug("scan category %s, path %s", pending->category, path);
+                if (!scanner->pending_stop)
+                    lms_check(lms, path);
+                if (!scanner->pending_stop &&
+                    g_file_test(path, G_FILE_TEST_EXISTS))
+                    lms_process(lms, path);
+
+                g_free(path);
+            }
+            lms_free(lms);
+        }
+
+        scanner_pending_free(pending);
+    }
+
+    g_debug("finished scanner thread");
+
+    if (vacuum) {
+        GTimer *timer = g_timer_new();
+
+        g_debug("Starting SQL VACUUM...");
+        g_timer_start(timer);
+        do_vacuum();
+        g_timer_stop(timer);
+        g_debug("Finished VACUUM in %0.3f seconds.",
+                g_timer_elapsed(timer, NULL));
+        g_timer_destroy(timer);
+    }
+
+    scanner->cleanup_thread_idler = g_idle_add(scanner_thread_cleanup, scanner);
+
+    return scanner;
+}
+
+static void
+do_scan(scanner_t *scanner)
+{
+    scanner->thread = g_thread_new("scanner", scanner_thread_work, scanner);
+
+    scanner_is_scanning_changed(scanner);
+    scanner_write_lock_changed(scanner);
+}
+
+static void
+dbus_scanner_scan(GDBusMethodInvocation *inv, scanner_t *scanner, GVariant *params)
+{
+    if (scanner->thread) {
+        g_dbus_method_invocation_return_dbus_error(
+            inv, "org.lightmediascanner.AlreadyScanning",
+            "Scanner was already scanning.");
+        return;
+    }
+
+    if (scanner->write_lock) {
+        g_dbus_method_invocation_return_dbus_error(
+            inv, "org.lightmediascanner.WriteLocked",
+            "Data Base has a write lock for another process.");
+        return;
+    }
+
+    dbus_scanner_scan_params_set(scanner, params);
+
+    do_scan(scanner);
+
+    g_dbus_method_invocation_return_value(inv, NULL);
+}
+
+static void
+dbus_scanner_stop(GDBusMethodInvocation *inv, scanner_t *scanner)
+{
+    if (!scanner->thread) {
+        g_dbus_method_invocation_return_dbus_error(
+            inv, "org.lightmediascanner.NotScanning",
+            "Scanner was already stopped.");
+        return;
+    }
+    if (scanner->pending_stop) {
+        g_dbus_method_invocation_return_dbus_error(
+            inv, "org.lightmediascanner.AlreadyStopping",
+            "Scanner was already being stopped.");
+        return;
+    }
+
+    scanner->pending_stop = g_object_ref(inv);
+}
+
+static void
+dbus_scanner_request_write_lock(GDBusMethodInvocation *inv, scanner_t *scanner, const char *sender)
+{
+    if (check_write_locked(scanner)) {
+        if (scanner->write_lock && strcmp(scanner->write_lock, sender) == 0)
+            g_dbus_method_invocation_return_value(inv, NULL);
+        else if (scanner->write_lock)
+            g_dbus_method_invocation_return_dbus_error(
+                inv, "org.lightmediascanner.AlreadyLocked",
+                "Scanner is already locked");
+        else
+            g_dbus_method_invocation_return_dbus_error(
+                inv, "org.lightmediascanner.IsScanning",
+                "Scanner is scanning and can't grant a write lock");
+        return;
+    }
+
+    scanner_acquire_write_lock(scanner, sender);
+    g_dbus_method_invocation_return_value(inv, NULL);
+}
+
+static void
+dbus_scanner_release_write_lock(GDBusMethodInvocation *inv, scanner_t *scanner, const char *sender)
+{
+    if (!scanner->write_lock || strcmp(scanner->write_lock, sender) != 0) {
+        g_dbus_method_invocation_return_dbus_error(
+            inv, "org.lightmediascanner.NotLocked",
+            "Scanner was not locked by you.");
+        return;
+    }
+
+    scanner_release_write_lock(scanner);
+    g_dbus_method_invocation_return_value(inv, NULL);
+}
+
+static void
+scanner_method_call(GDBusConnection *conn, const char *sender, const char *opath, const char *iface, const char *method, GVariant *params, GDBusMethodInvocation *inv, gpointer data)
+{
+    scanner_t *scanner = data;
+
+    if (strcmp(method, "Scan") == 0)
+        dbus_scanner_scan(inv, scanner, params);
+    else if (strcmp(method, "Stop") == 0)
+        dbus_scanner_stop(inv, scanner);
+    else if (strcmp(method, "RequestWriteLock") == 0)
+        dbus_scanner_request_write_lock(inv, scanner, sender);
+    else if (strcmp(method, "ReleaseWriteLock") == 0)
+        dbus_scanner_release_write_lock(inv, scanner, sender);
+}
+
+static void
+scanner_update_id_changed(scanner_t *scanner)
+{
+    if (scanner->changed_props.idler == 0)
+        scanner->changed_props.idler = g_idle_add(scanner_dbus_props_changed,
+                                                  scanner);
+
+    scanner->changed_props.update_id = TRUE;
+}
+
+static void
+scanner_categories_changed(scanner_t *scanner)
+{
+    if (scanner->changed_props.idler == 0)
+        scanner->changed_props.idler = g_idle_add(scanner_dbus_props_changed,
+                                                  scanner);
+
+    scanner->changed_props.categories = TRUE;
+}
+
+static GVariant *
+scanner_get_prop(GDBusConnection *conn, const char *sender, const char *opath, const char *iface, const char *prop,  GError **error, gpointer data)
+{
+    scanner_t *scanner = data;
+    GVariant *ret;
+
+    if (strcmp(prop, "IsScanning") == 0)
+        ret = g_variant_new_boolean(scanner->thread != NULL);
+    else if (strcmp(prop, "WriteLocked") == 0)
+        ret = g_variant_new_boolean(check_write_locked(scanner));
+    else if (strcmp(prop, "UpdateID") == 0) {
+        guint64 update_id = 0;
+
+        if (!check_write_locked(scanner))
+            update_id = get_update_id();
+        if (update_id > 0 && update_id != scanner->update_id) {
+            scanner->update_id = update_id;
+            scanner_update_id_changed(scanner);
+        }
+        ret = g_variant_new_uint64(scanner->update_id);
+    } else if (strcmp(prop, "Categories") == 0)
+        ret = categories_get_variant();
+    else
+        ret = NULL;
+
+    return ret;
+}
+
+static void
+scanner_destroyed(gpointer data)
+{
+    scanner_t *scanner = data;
+
+    g_free(scanner->write_lock);
+
+    if (scanner->write_lock_name_watcher)
+        g_bus_unwatch_name(scanner->write_lock_name_watcher);
+
+    if (scanner->thread) {
+        g_warning("Shutdown while scanning, wait...");
+        g_thread_join(scanner->thread);
+    }
+
+    if (scanner->cleanup_thread_idler) {
+        g_source_remove(scanner->cleanup_thread_idler);
+        scanner_thread_cleanup(scanner);
+    }
+
+    if (scanner->changed_props.idler) {
+        g_source_remove(scanner->changed_props.idler);
+        scanner_dbus_props_changed(scanner);
+    }
+
+    g_assert(scanner->thread == NULL);
+    g_assert(scanner->pending_scan == NULL);
+    g_assert(scanner->cleanup_thread_idler == 0);
+    g_assert(scanner->pending_stop == NULL);
+    g_assert(scanner->changed_props.idler == 0);
+
+    g_free(scanner);
+}
+
+static const GDBusInterfaceVTable scanner_vtable = {
+    scanner_method_call,
+    scanner_get_prop,
+    NULL
+};
+
+static void
+on_name_acquired(GDBusConnection *conn, const gchar *name, gpointer data)
+{
+    GDBusInterfaceInfo *iface;
+    unsigned id;
+    scanner_t *scanner;
+
+    scanner = g_new0(scanner_t, 1);
+    g_assert(scanner != NULL);
+    scanner->conn = conn;
+    scanner->pending_scan = NULL;
+    scanner->update_id = get_update_id();
+
+    iface = g_dbus_node_info_lookup_interface(introspection_data, BUS_IFACE);
+
+    id = g_dbus_connection_register_object(conn,
+                                           BUS_PATH,
+                                           iface,
+                                           &scanner_vtable,
+                                           scanner,
+                                           scanner_destroyed,
+                                           NULL);
+    g_assert(id > 0);
+
+    if (startup_scan) {
+        g_debug("Do startup scan");
+        g_hash_table_foreach(categories, scan_params_all, scanner);
+        do_scan(scanner);
+    }
+
+    scanner_update_id_changed(scanner);
+    scanner_write_lock_changed(scanner);
+    scanner_is_scanning_changed(scanner);
+    scanner_categories_changed(scanner);
+
+    g_debug("Acquired name org.lightmediascanner and registered object");
+}
+
+static gboolean
+str_array_find(const GArray *arr, const char *str)
+{
+    char **itr;
+    for (itr = (char **)arr->data; *itr; itr++)
+        if (strcmp(*itr, str) == 0)
+            return TRUE;
+
+    return FALSE;
+}
+
+static scanner_category_t *
+scanner_category_get_or_add(const char *category)
+{
+    scanner_category_t *sc = g_hash_table_lookup(categories, category);
+    if (sc)
+        return sc;
+
+    sc = scanner_category_new(category);
+    g_hash_table_insert(categories, sc->category, sc);
+    return sc;
+}
+
+static void
+scanner_category_add_parser(scanner_category_t *sc, const char *parser)
+{
+    char *p;
+
+    if (str_array_find(sc->parsers, parser))
+        return;
+
+    p = g_strdup(parser);
+    g_array_append_val(sc->parsers, p);
+}
+
+static void
+scanner_category_add_dir(scanner_category_t *sc, const char *dir)
+{
+    char *p;
+
+    if (str_array_find(sc->dirs, dir))
+        return;
+
+    p = g_strdup(dir);
+    g_array_append_val(sc->dirs, p);
+}
+
+static int
+populate_categories(void *data, const char *path)
+{
+    struct lms_parser_info *info;
+    const char * const *itr;
+    long do_parsers = (long)data;
+
+    info = lms_parser_info(path);
+    if (!info)
+        return 1;
+
+    if (strcmp(info->name, "dummy") == 0)
+        goto end;
+
+    if (!info->categories)
+        goto end;
+
+
+    for (itr = info->categories; *itr != NULL; itr++) {
+        scanner_category_t *sc;
+
+        if (strcmp(*itr, "all") == 0)
+            continue;
+
+        sc = scanner_category_get_or_add(*itr);
+
+        if (do_parsers)
+            scanner_category_add_parser(sc, path);
+    }
+
+end:
+    lms_parser_info_free(info);
+
+    return 1;
+}
+
+static int
+populate_category_all_parsers(void *data, const char *path)
+{
+    struct lms_parser_info *info;
+    scanner_category_t *sc = data;
+
+    info = lms_parser_info(path);
+    if (!info)
+        return 1;
+
+    if (strcmp(info->name, "dummy") != 0)
+        scanner_category_add_parser(sc, path);
+
+    lms_parser_info_free(info);
+    return 1;
+}
+
+static int
+populate_category_parsers(void *data, const char *path, const struct lms_parser_info *info)
+{
+    scanner_category_t *sc = data;
+
+    if (strcmp(info->name, "dummy") != 0)
+        scanner_category_add_parser(sc, path);
+
+    return 1;
+}
+
+static void
+_populate_parser_internal(const char *category, const char *parser)
+{
+    scanner_category_t *sc = scanner_category_get_or_add(category);
+
+    if (strcmp(parser, "all") == 0)
+        lms_parsers_list(populate_category_all_parsers, sc);
+    else if (strcmp(parser, "all-category") == 0)
+        lms_parsers_list_by_category(sc->category, populate_category_parsers,
+                                     sc);
+    else
+        scanner_category_add_parser(sc, parser);
+}
+
+static void
+populate_parser_foreach(gpointer key, gpointer value, gpointer user_data)
+{
+    const char *category = key;
+    const char *parser = user_data;
+    _populate_parser_internal(category, parser);
+}
+
+static void
+populate_parser(const char *category, const char *parser)
+{
+    if (!category)
+        g_hash_table_foreach(categories, populate_parser_foreach,
+                             (gpointer)parser);
+    else
+        _populate_parser_internal(category, parser);
+}
+
+static void
+_populate_dir_internal(const char *category, const char *dir)
+{
+    scanner_category_t *sc = scanner_category_get_or_add(category);
+
+    if (strcmp(dir, "defaults") != 0)
+        scanner_category_add_dir(sc, dir);
+    else {
+        struct {
+            const char *cat;
+            const char *path;
+        } *itr, defaults[] = {
+            {"audio", g_get_user_special_dir(G_USER_DIRECTORY_MUSIC)},
+            {"video", g_get_user_special_dir(G_USER_DIRECTORY_VIDEOS)},
+            {"picture", g_get_user_special_dir(G_USER_DIRECTORY_PICTURES)},
+            {"multimedia", g_get_user_special_dir(G_USER_DIRECTORY_MUSIC)},
+            {"multimedia", g_get_user_special_dir(G_USER_DIRECTORY_VIDEOS)},
+            {"multimedia", g_get_user_special_dir(G_USER_DIRECTORY_PICTURES)},
+            {NULL, NULL}
+        };
+        for (itr = defaults; itr->cat != NULL; itr++) {
+            if (strcmp(itr->cat, category) == 0)
+                scanner_category_add_dir(sc, itr->path);
+        }
+    }
+}
+
+static void
+populate_dir_foreach(gpointer key, gpointer value, gpointer user_data)
+{
+    const char *category = key;
+    const char *dir = user_data;
+    _populate_dir_internal(category, dir);
+}
+
+static void
+populate_dir(const char *category, const char *dir)
+{
+    if (!category)
+        g_hash_table_foreach(categories, populate_dir_foreach,
+                             (gpointer)dir);
+    else
+        _populate_dir_internal(category, dir);
+}
+
+static void
+debug_categories(gpointer key, gpointer value, gpointer user_data)
+{
+    const scanner_category_t *sc = value;
+    const char * const *itr;
+
+    g_debug("category: %s", sc->category);
+
+    if (sc->parsers->len) {
+        for (itr = (const char * const *)sc->parsers->data; *itr != NULL; itr++)
+            g_debug("  parser: %s", *itr);
+    } else
+            g_debug("  parser: <none>");
+
+    if (sc->dirs->len) {
+        for (itr = (const char * const *)sc->dirs->data; *itr != NULL; itr++)
+            g_debug("  dir...: %s", *itr);
+    } else
+            g_debug("  dir...: <none>");
+}
+
+static gboolean
+on_sig_term(gpointer data)
+{
+    GMainLoop *loop = data;
+
+    g_debug("got SIGTERM, exit.");
+    g_main_loop_quit(loop);
+    return FALSE;
+}
+
+static gboolean
+on_sig_int(gpointer data)
+{
+    GMainLoop *loop = data;
+
+    g_debug("got SIGINT, exit.");
+    g_main_loop_quit(loop);
+    return FALSE;
+}
+
+int
+main(int argc, char *argv[])
+{
+    int ret = EXIT_SUCCESS;
+    unsigned id;
+    GMainLoop *loop;
+    GError *error = NULL;
+    GOptionContext *opt_ctx;
+    char **parsers = NULL;
+    char **dirs = NULL;
+    GOptionEntry opt_entries[] = {
+        {"db-path", 'p', 0, G_OPTION_ARG_FILENAME, &db_path,
+         "Path to LightMediaScanner SQLit3 data base, "
+         "defaults to \"~/.config/lightmediascannerd/db.sqlite3\".",
+         "PATH"},
+        {"commit-interval", 'c', 0, G_OPTION_ARG_INT, &commit_interval,
+         "Execute SQL COMMIT after NUMBER files are processed, "
+         "defaults to 100.",
+         "NUMBER"},
+        {"slave-timeout", 't', 0, G_OPTION_ARG_INT, &slave_timeout,
+         "Number of seconds to wait for slave to reply, otherwise kills it. "
+         "Defaults to 60.",
+         "SECONDS"},
+        {"vacuum", 'V', 0, G_OPTION_ARG_NONE, &vacuum,
+         "Execute SQL VACUUM after every scan.", NULL},
+        {"startup-scan", 'S', 0, G_OPTION_ARG_NONE, &startup_scan,
+         "Execute full scan on startup.", NULL},
+        {"charset", 'C', 0, G_OPTION_ARG_STRING_ARRAY, &charsets,
+         "Extra charset to use. (Multiple use)", "CHARSET"},
+        {"parser", 'P', 0, G_OPTION_ARG_STRING_ARRAY, &parsers,
+         "Parsers to use, defaults to all. Format is 'category:parsername' or "
+         "'parsername' to apply parser to all categories. The special "
+         "parsername 'all' declares all known parsers, while 'all-category' "
+         "declares all parsers of that category. If one parser is provided, "
+         "then no defaults are used, you can pre-populate all categories "
+         "with their parsers by using --parser=all-category.",
+         "CATEGORY:PARSER"},
+        {"directory", 'D', 0, G_OPTION_ARG_STRING_ARRAY, &dirs,
+         "Directories to use, defaults to FreeDesktop.Org standard. "
+         "Format is 'category:directory' or 'path' to "
+         "apply directory to all categories. The special directory "
+         "'defaults' declares all directories used by default for that "
+         "category. If one directory is provided, then no defaults are used, "
+         "you can pre-populate all categories with their directories by "
+         "using --directory=defaults.",
+         "CATEGORY:DIRECTORY"},
+        {NULL, 0, 0, 0, NULL, NULL, NULL}
+    };
+
+    opt_ctx = g_option_context_new(
+        "\nLightMediaScanner D-Bus daemon.\n\n"
+        "Usually there is no need to declare options, defaults should "
+        "be good. However one may want the defaults and in addition to scan "
+        "everything in an USB with:\n\n"
+        "\tlightmediascannerd --directory=defaults --parser=all-category "
+        "--directory=usb:/media/usb --parser=usb:all");
+    g_option_context_add_main_entries(opt_ctx, opt_entries,
+                                      "lightmediascannerd");
+    if (!g_option_context_parse(opt_ctx, &argc, &argv, &error)) {
+        g_option_context_free(opt_ctx);
+        g_error("Option parsing failed: %s\n", error->message);
+        g_error_free(error);
+        return EXIT_FAILURE;
+    }
+
+    g_option_context_free(opt_ctx);
+
+    categories = g_hash_table_new_full(
+        g_str_hash, g_str_equal, NULL,
+        (GDestroyNotify)scanner_category_destroy);
+
+    if (!db_path)
+        db_path = g_strdup_printf("%s/lightmediascannerd/db.sqlite3",
+                                  g_get_user_config_dir());
+    if (!g_file_test(db_path, G_FILE_TEST_EXISTS)) {
+        char *dname = g_path_get_dirname(db_path);
+        if (!g_file_test(dname, G_FILE_TEST_EXISTS | G_FILE_TEST_IS_DIR)) {
+            if (g_mkdir_with_parents(dname, 0755) != 0) {
+                g_error("Couldn't create directory %s", dname);
+                g_free(dname);
+                ret = EXIT_FAILURE;
+                goto end_options;
+            }
+        }
+        g_free(dname);
+    }
+
+    if (!parsers)
+        lms_parsers_list(populate_categories, (void *)1L);
+    else {
+        char **itr;
+
+        lms_parsers_list(populate_categories, (void *)0L);
+
+        for (itr = parsers; *itr != NULL; itr++) {
+            char *sep = strchr(*itr, ':');
+            const char *path;
+            if (!sep)
+                path = *itr;
+            else {
+                path = sep + 1;
+                *sep = '\0';
+            }
+
+            if (path[0] != '\0')
+                populate_parser(sep ? *itr : NULL, path);
+
+            if (sep)
+                *sep = ':';
+        }
+    }
+
+    if (!dirs)
+        populate_dir(NULL, "defaults");
+    else {
+        char **itr;
+
+        for (itr = dirs; *itr != NULL; itr++) {
+            char *sep = strchr(*itr, ':');
+            const char *path;
+            if (!sep)
+                path = *itr;
+            else {
+                path = sep + 1;
+                *sep = '\0';
+            }
+
+            if (path[0] != '\0')
+                populate_dir(sep ? *itr : NULL, path);
+
+            if (sep)
+                *sep = ':';
+        }
+    }
+
+    g_debug("db-path: %s", db_path);
+    g_debug("commit-interval: %d files", commit_interval);
+    g_debug("slave-timeout: %d seconds", slave_timeout);
+
+    if (charsets) {
+        char *tmp = g_strjoinv(", ", charsets);
+        g_debug("charsets: %s", tmp);
+        g_free(tmp);
+    } else
+        g_debug("charsets: <none>");
+
+    g_hash_table_foreach(categories, debug_categories, NULL);
+
+    introspection_data = g_dbus_node_info_new_for_xml(introspection_xml, NULL);
+    g_assert(introspection_xml != NULL);
+
+    id = g_bus_own_name(G_BUS_TYPE_SESSION, "org.lightmediascanner",
+                        G_BUS_NAME_OWNER_FLAGS_NONE,
+                        NULL, on_name_acquired, NULL, NULL, NULL);
+
+    g_debug("starting main loop");
+
+    loop = g_main_loop_new(NULL, FALSE);
+    g_unix_signal_add(SIGTERM, on_sig_term, loop);
+    g_unix_signal_add(SIGINT, on_sig_int, loop);
+    g_main_loop_run(loop);
+
+    g_debug("main loop is finished");
+
+    g_bus_unown_name(id);
+    g_main_loop_unref(loop);
+
+    g_dbus_node_info_unref(introspection_data);
+
+end_options:
+    g_free(db_path);
+    g_strfreev(charsets);
+    g_strfreev(parsers);
+    g_strfreev(dirs);
+
+    if (categories)
+        g_hash_table_destroy(categories);
+
+    return ret;
+}