[rygel-gst-0-10-plugins] Intial media-export plugin for gst-0.10



commit c03769189f0d3a40278fe2e88cd7b70fc3eae553
Author: Murray Cumming <murrayc murrayc com>
Date:   Tue Dec 11 11:52:25 2012 +0100

    Intial media-export plugin for gst-0.10

 configure.ac                                       |   38 +-
 data/Makefile.am                                   |   13 -
 data/common.am                                     |    4 +-
 src/Makefile.am                                    |   47 +-
 src/media-export/Makefile.am                       |   58 ++
 .../rygel-media-export-database-cursor.vala        |  143 ++++
 src/media-export/rygel-media-export-database.vala  |  214 +++++
 .../rygel-media-export-db-container.vala           |   99 +++
 .../rygel-media-export-dbus-service.vala           |   54 ++
 .../rygel-media-export-dummy-container.vala        |   45 +
 src/media-export/rygel-media-export-harvester.vala |  256 ++++++
 .../rygel-media-export-harvesting-task.vala        |  366 ++++++++
 src/media-export/rygel-media-export-item.vala      |  268 ++++++
 .../rygel-media-export-jpeg-writer.vala            |   66 ++
 .../rygel-media-export-leaf-query-container.vala   |   53 ++
 .../rygel-media-export-media-cache-upgrader.vala   |  368 ++++++++
 .../rygel-media-export-media-cache.vala            |  896 ++++++++++++++++++++
 .../rygel-media-export-metadata-extractor.vala     |  155 ++++
 .../rygel-media-export-music-item.vala}            |   34 +-
 .../rygel-media-export-node-query-container.vala   |   91 ++
 .../rygel-media-export-null-container.vala         |   47 +
 .../rygel-media-export-object-factory.vala         |   91 ++
 .../rygel-media-export-photo-item.vala}            |   26 +-
 src/media-export/rygel-media-export-plugin.vala    |  113 +++
 ...rygel-media-export-query-container-factory.vala |  268 ++++++
 .../rygel-media-export-query-container.vala        |   90 ++
 .../rygel-media-export-recursive-file-monitor.vala |  113 +++
 .../rygel-media-export-root-container.vala         |  484 +++++++++++
 .../rygel-media-export-sql-factory.vala            |  305 +++++++
 .../rygel-media-export-sql-function.vala}          |   22 +-
 .../rygel-media-export-sql-operator.vala           |   73 ++
 .../rygel-media-export-sqlite-wrapper.vala         |   80 ++
 .../rygel-media-export-video-item.vala}            |   33 +-
 .../rygel-media-export-writable-db-container.vala  |   55 ++
 src/rygel-audio-transcoder.vala                    |  118 ---
 src/rygel-avc-transcoder.vala                      |   67 --
 src/rygel-gst-data-source.vala                     |  262 ------
 src/rygel-gst-media-engine.vala                    |  119 ---
 src/rygel-gst-sink.vala                            |  144 ----
 src/rygel-gst-transcoder.vala                      |  165 ----
 src/rygel-gst-utils.vala                           |  117 ---
 src/rygel-l16-transcoder.vala                      |  101 ---
 src/rygel-mp2ts-transcoder.vala                    |  112 ---
 src/rygel-video-transcoder.vala                    |  107 ---
 44 files changed, 4942 insertions(+), 1438 deletions(-)
---
diff --git a/configure.ac b/configure.ac
index 074c8c0..66bdc3e 100644
--- a/configure.ac
+++ b/configure.ac
@@ -6,7 +6,7 @@ AC_INIT([rygel-gst-0-10-plugins],
 	[http://live.gnome.org/Rygel])
 AC_CONFIG_AUX_DIR([build-aux])
 
-AC_CONFIG_SRCDIR([src/rygel-gst-utils.vala])
+AC_CONFIG_SRCDIR([src/media-export/rygel-media-export-plugin.vala])
 AC_CONFIG_HEADERS([config.h])
 AC_CONFIG_MACRO_DIR([m4])
 
@@ -28,14 +28,43 @@ LT_INIT([dlopen disable-static])
 
 dnl Required versions of library packages
 LIBRYGEL_SERVER_REQUIRED=0.17.4
+GUPNP_REQUIRED=0.19.0
+GUPNP_AV_REQUIRED=0.11.2
 GUPNP_DLNA_REQUIRED=0.5.0
 GSTREAMER_REQUIRED=0.10.36
 GSTPBU_REQUIRED=0.10.35
-REQUIRED_MODULES='rygel-server-2.0 >= $LIBRYGEL_SERVER_REQUIRED gupnp-dlna-1.0 >= $GUPNP_DLNA_REQUIRED gstreamer-0.10 >= $GSTREAMER_REQUIRED gstreamer-base-0.10 >= $GSTREAMER_REQUIRED gstreamer-pbutils-0.10 >= $GSTPBU_REQUIRED'
-PKG_CHECK_MODULES([DEPS], [$REQUIRED_MODULES])
+GIO_REQUIRED=2.26
+GEE_REQUIRED=0.8.0
+
+dnl Additional requirements for media-export plugin
+GSTREAMER_TAG_REQUIRED=0.10.28
+GSTREAMER_APP_REQUIRED=0.10.28
+LIBSQLITE3_REQUIRED=3.5
+
+
+RYGEL_BASE_MODULES='gupnp-1.0 >= $GUPNP_REQUIRED gee-0.8 >= $GEE_REQUIRED'
+RYGEL_COMMON_MODULES="$RYGEL_BASE_MODULES gupnp-av-1.0 >= $GUPNP_AV_REQUIRED"
+PKG_CHECK_MODULES([RYGEL_PLUGIN_MEDIA_EXPORT_DEPS], [$RYGEL_COMMON_MODULES rygel-server-2.0 >= $LIBRYGEL_SERVER_REQUIRED gio-2.0 >= $GIO_REQUIRED gupnp-dlna-1.0 >= $GUPNP_DLNA_REQUIRED gstreamer-tag-0.10 >= $GSTREAMER_TAG_REQUIRED gstreamer-app-0.10 >= $GSTREAMER_TAG_REQUIRED sqlite3 >= $LIBSQLITE3_REQUIRED])
+
+AC_CHECK_HEADER([unistr.h],
+                AC_CHECK_LIB([unistring],
+                             [u8_strcoll],
+                             [have_unistring=yes],[have_unistring=no]))
+if test "x$have_unistring" = "xyes"; then
+    AC_DEFINE([HAVE_UNISTRING],[1],[Use libunistring for collation])
+    COLLATION_CFLAGS=
+    COLLATION_LIBS=-lunistring
+    AC_SUBST([COLLATION_CFLAGS])
+    AC_SUBST([COLLATION_LIBS])
+fi
+
+
+RYGEL_BASE_MODULES_VALAFLAGS='--pkg gupnp-1.0 --pkg gee-0.8'
+RYGEL_COMMON_MODULES_VALAFLAGS="$RYGEL_BASE_MODULES_VALAFLAGS --pkg gupnp-av-1.0"
+RYGEL_PLUGIN_MEDIA_EXPORT_DEPS_VALAFLAGS="$RYGEL_COMMON_MODULES_VALAFLAGS ---pkg rygel-server-2.0 --pkg gupnp-1.0 --pkg gee-0.8 --pkg gio-2.0 --pkg gupnp-dlna-1.0 --pkg gstreamer-tag-0.10 --pkg gstreamer-app-0.10 --pkg sqlite3"
 
 VALA_REQUIRED=0.16.1
-REQUIRED_MODULES='rygel-server-2.0 gupnp-dlna-1.0 gstreamer-0.10 gstreamer-base-0.10 gstreamer-pbutils-0.10'
+VALA_REQUIRED_MODULES='rygel-server-2.0 gupnp-1.0 gee-0.8 gio-2.0 gupnp-dlna-1.0 gstreamer-0.10 gstreamer-base-0.10 gstreamer-tag-0.10 gstreamer-app-0.10 sqlite3 posix'
 RYGEL_CHECK_VALA([$VALA_REQUIRED],
                  [$VALA_REQUIRED_MODULES])
 
@@ -79,6 +108,7 @@ AC_CONFIG_FILES([
 Makefile
 data/Makefile
 src/Makefile
+src/media-export/Makefile
 po/Makefile.in
 ])
 AC_OUTPUT
diff --git a/data/Makefile.am b/data/Makefile.am
index e015e30..6f8f568 100644
--- a/data/Makefile.am
+++ b/data/Makefile.am
@@ -1,15 +1,2 @@
 include common.am
 
-preset_DATA = $(srcdir)/presets/ffenc_aac.prs \
-	$(srcdir)/presets/ffenc_mp2.prs \
-	$(srcdir)/presets/ffenc_mpeg2video.prs \
-	$(srcdir)/presets/ffenc_wmav1.prs \
-	$(srcdir)/presets/ffenc_wmv1.prs \
-	$(srcdir)/presets/GstFaac.prs \
-	$(srcdir)/presets/GstLameMP3Enc.prs \
-	$(srcdir)/presets/GstMP4Mux.prs \
-	$(srcdir)/presets/GstTwoLame.prs \
-	$(srcdir)/presets/GstX264Enc.prs
-
-EXTRA_DIST = $(preset_DATA)
-
diff --git a/data/common.am b/data/common.am
index d9cb1a3..5227f57 100644
--- a/data/common.am
+++ b/data/common.am
@@ -1,2 +1,2 @@
-shareddir = $(datadir)/rygel-media-engine-gst-0-10
-presetdir = $(shareddir)/presets
+#shareddir = $(datadir)/rygel-media-export-gst-0-10
+#presetdir = $(shareddir)/presets
diff --git a/src/Makefile.am b/src/Makefile.am
index c8b1953..6a2459d 100644
--- a/src/Makefile.am
+++ b/src/Makefile.am
@@ -1,46 +1 @@
-include $(top_srcdir)/data/common.am
-
-engine_LTLIBRARIES = librygel-media-engine-gst-0-10.la
-enginedir = $(libdir)/rygel-2.0/engines
-
-
-AM_CFLAGS = -DG_LOG_DOMAIN='"MediaEngine-GStreamer-0.10"' \
-	-DPRESET_DIR='"$(presetdir)"' \
-	-include config.h \
-	$(DEPS_CFLAGS)
-
-librygel_media_engine_gst_0_10_la_SOURCES = \
-	rygel-aac-transcoder.vala \
-	rygel-audio-transcoder.vala \
-	rygel-avc-transcoder.vala \
-	rygel-gst-data-source.vala \
-	rygel-gst-media-engine.vala \
-	rygel-gst-sink.vala \
-	rygel-gst-transcoder.vala \
-	rygel-gst-utils.vala \
-	rygel-l16-transcoder.vala \
-	rygel-mp2ts-transcoder.vala \
-	rygel-mp3-transcoder.vala \
-	rygel-video-transcoder.vala \
-	rygel-wmv-transcoder.vala
-
-librygel_media_engine_gst_0_10_la_VALAFLAGS = \
-	--pkg rygel-server-2.0 \
-	--pkg gupnp-dlna-1.0 \
-	--pkg gstreamer-0.10 \
-	--pkg gstreamer-base-0.10 \
-	--pkg gstreamer-pbutils-0.10 \
-	--library rygel-media-engine-gst-0-10 \
-	--use-header \
-	--header=rygel-media-engine-gst-0-10.h
-
-rygel-media-engine-gst.h rygel-media-engine-gstreamer.vapi: librygel_media_engine_gst_la_vala.stamp
-
-librygel_media_engine_gst_0_10_la_LIBADD = \
-	$(DEPS_LIBS)
-
-librygel_media_engine_gst_0_10_la_LDFLAGS = $(RYGEL_PLUGIN_LINKER_FLAGS)
-
-EXTRA_DIST = \
-	rygel-media-engine-gst-0-10.vapi \
-	rygel-media-engine-gst-0-10.h
+SUBDIRS = media-export
diff --git a/src/media-export/Makefile.am b/src/media-export/Makefile.am
new file mode 100644
index 0000000..95a8dfc
--- /dev/null
+++ b/src/media-export/Makefile.am
@@ -0,0 +1,58 @@
+include $(top_srcdir)/data/common.am
+
+plugin_LTLIBRARIES = librygel-media-export-gst-0-10.la
+plugindir = $(libdir)/rygel-2.0/plugins
+
+librygel_media_export_gst_0_10_la_SOURCES = \
+	rygel-media-export-plugin.vala \
+	rygel-media-export-database.vala \
+	rygel-media-export-database-cursor.vala \
+	rygel-media-export-sqlite-wrapper.vala \
+	rygel-media-export-db-container.vala \
+	rygel-media-export-sql-factory.vala \
+	rygel-media-export-media-cache.vala \
+	rygel-media-export-sql-operator.vala \
+	rygel-media-export-sql-function.vala \
+	rygel-media-export-media-cache-upgrader.vala \
+	rygel-media-export-metadata-extractor.vala \
+	rygel-media-export-null-container.vala \
+	rygel-media-export-dummy-container.vala \
+	rygel-media-export-root-container.vala \
+	rygel-media-export-query-container.vala \
+	rygel-media-export-query-container-factory.vala \
+	rygel-media-export-node-query-container.vala \
+	rygel-media-export-leaf-query-container.vala \
+	rygel-media-export-dbus-service.vala \
+	rygel-media-export-recursive-file-monitor.vala \
+	rygel-media-export-harvester.vala \
+	rygel-media-export-harvesting-task.vala \
+	rygel-media-export-item.vala \
+	rygel-media-export-jpeg-writer.vala \
+	rygel-media-export-object-factory.vala \
+	rygel-media-export-writable-db-container.vala \
+	rygel-media-export-music-item.vala \
+	rygel-media-export-video-item.vala \
+	rygel-media-export-photo-item.vala \
+	rygel-media-export-collate.c
+
+librygel_media_export_gst_0_10_la_VALAFLAGS = \
+	$(RYGEL_PLUGIN_MEDIA_EXPORT_DEPS_CFLAGS)
+
+librygel_media_export_gst_0_10_la_CFLAGS = \
+	$(RYGEL_PLUGIN_MEDIA_EXPORT_DEPS_CFLAGS) \
+	$(COLLATION_CFLAGS) \
+	-DG_LOG_DOMAIN='"MediaExport-gst-0-10"'
+
+librygel_media_export_gst_0_10_la_LIBADD = \
+	$(RYGEL_PLUGIN_MEDIA_EXPORT_DEPS_LIBS) \
+	$(COLLATION_LIBS)
+
+librygel_media_export_gst_0_10_la_LDFLAGS = $(RYGEL_PLUGIN_LINKER_FLAGS)
+
+
+rygel-media-export-gst-0-10.h rygel-media-export-gstreamer-0-10.vapi: librygel_media_engine_gst_0_10_la_vala.stamp
+
+
+EXTRA_DIST = \
+	rygel-media-export-gst-0-10.vapi \
+	rygel-media-export-gst-0-10.h
diff --git a/src/media-export/rygel-media-export-database-cursor.vala b/src/media-export/rygel-media-export-database-cursor.vala
new file mode 100644
index 0000000..6c3f0dc
--- /dev/null
+++ b/src/media-export/rygel-media-export-database-cursor.vala
@@ -0,0 +1,143 @@
+/*
+ * Copyright (C) 2011 Jens Georg <mail jensge org>.
+ *
+ * Author: Jens Georg <mail jensge org>
+ *
+ * This file is part of Rygel.
+ *
+ * Rygel is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * Rygel 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 Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+
+using Sqlite;
+
+internal class Rygel.MediaExport.DatabaseCursor : SqliteWrapper {
+    private Statement statement;
+    private int current_state = -1;
+    private bool dirty = true;
+
+    /**
+     * Prepare a SQLite statement from a SQL string
+     *
+     * This function uses the type of the GValue passed in values to determine
+     * which _bind function to use.
+     *
+     * Supported types are: int, long, int64, uint64, string and pointer.
+     * @note the only pointer supported is the null pointer as provided by
+     * Database  null  This is a special value to bind a column to NULL
+     *
+     * @param db SQLite database this cursor belongs to
+     * @param sql statement to execute
+     * @param values array of values to bind to the SQL statement or null if
+     * none
+     */
+    public DatabaseCursor (Sqlite.Database   db,
+                           string            sql,
+                           GLib.Value[]?     arguments) throws DatabaseError {
+        base.wrap (db);
+
+        this.throw_if_code_is_error (db.prepare_v2 (sql,
+                                                    -1,
+                                                    out this.statement,
+                                                    null));
+        if (arguments == null) {
+            return;
+        }
+
+        for (var i = 1; i <= arguments.length; ++i) {
+            unowned GLib.Value current_value = arguments[i - 1];
+
+            if (current_value.holds (typeof (int))) {
+                statement.bind_int (i, current_value.get_int ());
+            } else if (current_value.holds (typeof (int64))) {
+                statement.bind_int64 (i, current_value.get_int64 ());
+            } else if (current_value.holds (typeof (uint64))) {
+                statement.bind_int64 (i, (int64) current_value.get_uint64 ());
+            } else if (current_value.holds (typeof (long))) {
+                statement.bind_int64 (i, current_value.get_long ());
+            } else if (current_value.holds (typeof (string))) {
+                statement.bind_text (i, current_value.get_string ());
+            } else if (current_value.holds (typeof (void *))) {
+                if (current_value.peek_pointer () == null) {
+                    statement.bind_null (i);
+                } else {
+                    assert_not_reached ();
+                }
+            } else {
+                var type = current_value.type ();
+                warning (_("Unsupported type %s"), type.name ());
+                assert_not_reached ();
+            }
+
+            this.throw_if_db_has_error ();
+        }
+    }
+
+    /**
+     * Check if the cursor has more rows left
+     *
+     * @return true if more rows left, false otherwise
+     */
+    public bool has_next () {
+        if (this.dirty) {
+            this.current_state = this.statement.step ();
+            this.dirty = false;
+        }
+
+        return this.current_state == Sqlite.ROW || this.current_state == -1;
+    }
+
+    /**
+     * Get the next row of this cursor.
+     *
+     * This function uses pointers instead of unowned because var doesn't work
+     * with unowned.
+     *
+     * @return a pointer to the current row
+     */
+    public Statement* next () throws DatabaseError {
+        this.has_next ();
+        this.throw_if_code_is_error (this.current_state);
+        this.dirty = true;
+
+        return this.statement;
+    }
+
+    // convenience functions for "foreach"
+
+    /**
+     * Return a iterator to the cursor to use with foreach
+     *
+     * @return an iterator wrapping the cursor
+     */
+    public Iterator iterator () {
+        return new Iterator (this);
+    }
+
+    public class Iterator {
+        public DatabaseCursor cursor;
+
+        public Iterator (DatabaseCursor cursor) {
+            this.cursor = cursor;
+        }
+
+        public bool next () {
+            return this.cursor.has_next ();
+        }
+
+        public unowned Statement @get () throws DatabaseError {
+            return this.cursor.next ();
+        }
+    }
+}
diff --git a/src/media-export/rygel-media-export-database.vala b/src/media-export/rygel-media-export-database.vala
new file mode 100644
index 0000000..16c007d
--- /dev/null
+++ b/src/media-export/rygel-media-export-database.vala
@@ -0,0 +1,214 @@
+/*
+ * Copyright (C) 2009,2011 Jens Georg <mail jensge org>.
+ *
+ * Author: Jens Georg <mail jensge org>
+ *
+ * This file is part of Rygel.
+ *
+ * Rygel is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * Rygel 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 Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+
+using Sqlite;
+
+public errordomain Rygel.MediaExport.DatabaseError {
+    IO_ERROR,
+    SQLITE_ERROR
+}
+
+namespace Rygel.MediaExport {
+    extern static int utf8_collate_str (string a, string b);
+}
+
+/**
+ * This class is a thin wrapper around SQLite's database object.
+ *
+ * It adds statement preparation based on GValue and a cancellable exec
+ * function.
+ */
+internal class Rygel.MediaExport.Database : SqliteWrapper {
+
+    /**
+     * Function to implement the custom SQL function 'contains'
+     */
+    private static void utf8_contains (Sqlite.Context context,
+                                       Sqlite.Value[] args)
+                                       requires (args.length == 2) {
+        if (args[1].to_text() == null) {
+           context.result_int (0);
+
+           return;
+        }
+
+        var pattern = Regex.escape_string (args[1].to_text ());
+        if (Regex.match_simple (pattern,
+                                args[0].to_text (),
+                                RegexCompileFlags.CASELESS)) {
+            context.result_int (1);
+        } else {
+            context.result_int (0);
+        }
+    }
+
+    /**
+     * Function to implement the custom SQLite collation 'CASEFOLD'.
+     *
+     * Uses utf8 case-fold to compare the strings.
+     */
+    private static int utf8_collate (int alen, void* a, int blen, void* b) {
+        // unowned to prevent array copy
+        unowned uint8[] _a = (uint8[]) a;
+        _a.length = alen;
+
+        unowned uint8[] _b = (uint8[]) b;
+        _b.length = blen;
+
+        var str_a = ((string) _a);
+        var str_b = ((string) _b);
+
+        return utf8_collate_str (str_a, str_b);
+    }
+
+    /**
+     * Open a database in the user's cache directory as defined by XDG
+     *
+     * @param name of the database, used to build full path
+     * (<cache-dir>/rygel/<name>.db)
+     */
+    public Database (string name) throws DatabaseError {
+        var dirname = Path.build_filename (Environment.get_user_cache_dir (),
+                                           "rygel");
+        DirUtils.create_with_parents (dirname, 0750);
+        var db_file = Path.build_filename (dirname, "%s.db".printf (name));
+
+        base (db_file);
+
+        debug ("Using database file %s", db_file);
+
+        this.exec ("PRAGMA synchronous = OFF");
+        this.exec ("PRAGMA temp_store = MEMORY");
+        this.exec ("PRAGMA count_changes = OFF");
+
+        this.db.create_function ("contains",
+                                 2,
+                                 Sqlite.UTF8,
+                                 null,
+                                 Database.utf8_contains,
+                                 null,
+                                 null);
+
+        this.db.create_collation ("CASEFOLD",
+                                  Sqlite.UTF8,
+                                  Database.utf8_collate);
+    }
+
+    /**
+     * SQL query function.
+     *
+     * Use for all queries that return a result set.
+     *
+     * @param sql The SQL query to run.
+     * @param args Values to bind in the SQL query or null.
+     * @throws DatabaseError if the underlying SQLite operation fails.
+     */
+    public DatabaseCursor exec_cursor (string        sql,
+                                       GLib.Value[]? arguments = null)
+                                       throws DatabaseError {
+        return new DatabaseCursor (this.db, sql, arguments);
+    }
+
+    /**
+     * Simple SQL query execution function.
+     *
+     * Use for all queries that don't return anything.
+     *
+     * @param sql The SQL query to run.
+     * @param args Values to bind in the SQL query or null.
+     * @throws DatabaseError if the underlying SQLite operation fails.
+     */
+    public void exec (string        sql,
+                      GLib.Value[]? arguments = null)
+                      throws DatabaseError {
+        if (arguments == null) {
+            this.throw_if_code_is_error (this.db.exec (sql));
+
+            return;
+        }
+
+        var cursor = this.exec_cursor (sql, arguments);
+        while (cursor.has_next ()) {
+            cursor.next ();
+        }
+    }
+
+    /**
+     * Execute a SQL query that returns a single number.
+     *
+     * @param sql The SQL query to run.
+     * @param args Values to bind in the SQL query or null.
+     * @return The contents of the first row's column as an int.
+     * @throws DatabaseError if the underlying SQLite operation fails.
+     */
+    public int query_value (string        sql,
+                             GLib.Value[]? args = null)
+                             throws DatabaseError {
+        var cursor = this.exec_cursor (sql, args);
+        var statement = cursor.next ();
+        return statement->column_int (0);
+    }
+
+    /**
+     * Analyze triggers of database
+     */
+    public void analyze () {
+        this.db.exec ("ANALYZE");
+    }
+
+    /**
+     * Special GValue to pass to exec or exec_cursor to bind a column to
+     * NULL
+     */
+    public static GLib.Value @null () {
+        GLib.Value v = GLib.Value (typeof (void *));
+        v.set_pointer (null);
+
+        return v;
+    }
+
+    /**
+     * Start a transaction
+     */
+    public void begin () throws DatabaseError {
+        this.exec ("BEGIN");
+    }
+
+    /**
+     * Commit a transaction
+     */
+    public void commit () throws DatabaseError {
+        this.exec ("COMMIT");
+    }
+
+    /**
+     * Rollback a transaction
+     */
+    public void rollback () {
+        try {
+            this.exec ("ROLLBACK");
+        } catch (DatabaseError error) {
+            critical (_("Failed to roll back transaction: %s"),
+                      error.message);
+        }
+    }
+}
diff --git a/src/media-export/rygel-media-export-db-container.vala b/src/media-export/rygel-media-export-db-container.vala
new file mode 100644
index 0000000..48d580e
--- /dev/null
+++ b/src/media-export/rygel-media-export-db-container.vala
@@ -0,0 +1,99 @@
+/*
+ * Copyright (C) 2009 Jens Georg <mail jensge org>.
+ *
+ * Author: Jens Georg <mail jensge org>
+ *
+ * This file is part of Rygel.
+ *
+ * Rygel is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * Rygel 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 Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+using GUPnP;
+using Gee;
+
+public class Rygel.MediaExport.DBContainer : MediaContainer,
+                                             SearchableContainer {
+    protected MediaCache media_db;
+    public ArrayList<string> search_classes { get; set; }
+
+    public DBContainer (MediaCache media_db, string id, string title) {
+        base (id, null, title, 0);
+
+        this.media_db = media_db;
+        this.search_classes = new ArrayList<string> ();
+        this.container_updated.connect ( () => { this.count_children (); });
+        this.count_children ();
+    }
+
+    private void count_children () {
+        try {
+            this.child_count = this.media_db.get_child_count (this.id);
+        } catch (DatabaseError error) {
+            debug ("Could not get child count from database: %s",
+                   error.message);
+            this.child_count = 0;
+        }
+    }
+
+    public override async MediaObjects? get_children (
+                                                     uint         offset,
+                                                     uint         max_count,
+                                                     string       sort_criteria,
+                                                     Cancellable? cancellable)
+                                                     throws GLib.Error {
+        return this.media_db.get_children (this,
+                                           sort_criteria,
+                                           offset,
+                                           max_count);
+    }
+
+    public virtual async MediaObjects? search (SearchExpression? expression,
+                                               uint              offset,
+                                               uint              max_count,
+                                               out uint          total_matches,
+                                               string            sort_criteria,
+                                               Cancellable?      cancellable)
+                                               throws GLib.Error {
+        MediaObjects children = null;
+
+        try {
+            children = this.media_db.get_objects_by_search_expression
+                                        (expression,
+                                         this.id,
+                                         sort_criteria,
+                                         offset,
+                                         max_count,
+                                         out total_matches);
+        } catch (MediaCacheError error) {
+            if (error is MediaCacheError.UNSUPPORTED_SEARCH) {
+                children = yield this.simple_search (expression,
+                                                     offset,
+                                                     max_count,
+                                                     out total_matches,
+                                                     sort_criteria,
+                                                     cancellable);
+            } else {
+                throw error;
+            }
+        }
+
+        return children;
+    }
+
+    public override async MediaObject? find_object (string       id,
+                                                    Cancellable? cancellable)
+                                                    throws Error {
+        return this.media_db.get_object (id);
+    }
+}
diff --git a/src/media-export/rygel-media-export-dbus-service.vala b/src/media-export/rygel-media-export-dbus-service.vala
new file mode 100644
index 0000000..5c84453
--- /dev/null
+++ b/src/media-export/rygel-media-export-dbus-service.vala
@@ -0,0 +1,54 @@
+/*
+ * Copyright (C) 2009 Jens Georg <mail jensge org>.
+ *
+ * This file is part of Rygel.
+ *
+ * Rygel is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * Rygel 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 Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+
+[DBus (name = "org.gnome.Rygel.MediaExport1")]
+public class Rygel.MediaExport.DBusService : Object {
+    private const string RYGEL_MEDIA_EXPORT_PATH =
+                                        "/org/gnome/Rygel/MediaExport1";
+
+    private RootContainer root_container;
+
+    public DBusService (RootContainer root_container) throws GLib.Error {
+        this.root_container = root_container;
+
+        try {
+            var connection = Bus.get_sync (BusType.SESSION);
+
+            if (likely (connection != null)) {
+                connection.register_object (RYGEL_MEDIA_EXPORT_PATH, this);
+            }
+        } catch (IOError err) {
+            warning (_("Failed to attach to D-Bus session bus: %s"),
+                     err.message);
+        }
+    }
+
+    public void AddUri (string uri) {
+        this.root_container.add_uri (uri);
+    }
+
+    public void RemoveUri (string uri) {
+        this.root_container.remove_uri (uri);
+    }
+
+    public string[] GetUris () {
+        return this.root_container.get_dynamic_uris ();
+    }
+}
diff --git a/src/media-export/rygel-media-export-dummy-container.vala b/src/media-export/rygel-media-export-dummy-container.vala
new file mode 100644
index 0000000..50c68d3
--- /dev/null
+++ b/src/media-export/rygel-media-export-dummy-container.vala
@@ -0,0 +1,45 @@
+/*
+ * Copyright (C) 2009,2010 Jens Georg <mail jensge org>.
+ *
+ * This file is part of Rygel.
+ *
+ * Rygel is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * Rygel 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 Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+using Gee;
+
+internal class Rygel.MediaExport.DummyContainer : NullContainer {
+    public File file;
+    public Gee.List<string> children;
+
+    public DummyContainer (File           file,
+                           MediaContainer parent) {
+        this.id = MediaCache.get_id (file);
+        this.title = file.get_basename ();
+        this.parent_ref = parent;
+        this.file = file;
+        this.uris.add (file.get_uri ());
+        try {
+            this.children = MediaCache.get_default ().get_child_ids (this.id);
+            this.child_count = this.children.size;
+        } catch (Error error) {
+            this.children = new ArrayList<string> ();
+            this.child_count = 0;
+        }
+    }
+
+    public void seen (File file) {
+        this.children.remove (MediaCache.get_id (file));
+    }
+}
diff --git a/src/media-export/rygel-media-export-harvester.vala b/src/media-export/rygel-media-export-harvester.vala
new file mode 100644
index 0000000..02a7f16
--- /dev/null
+++ b/src/media-export/rygel-media-export-harvester.vala
@@ -0,0 +1,256 @@
+/*
+ * Copyright (C) 2010 Jens Georg <mail jensge org>.
+ *
+ * This file is part of Rygel.
+ *
+ * Rygel is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * Rygel 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 Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+using Gee;
+
+/**
+ * This class takes care of the book-keeping of running and finished
+ * extraction tasks running within the media-export plugin
+ */
+internal class Rygel.MediaExport.Harvester : GLib.Object {
+    private const uint FILE_CHANGE_DEFAULT_GRACE_PERIOD = 5;
+
+    private HashMap<File, HarvestingTask> tasks;
+    private HashMap<File, uint> extraction_grace_timers;
+    private MetadataExtractor extractor;
+    private RecursiveFileMonitor monitor;
+    private Cancellable cancellable;
+
+    // Properties
+    public ArrayList<File> locations { get; private set; }
+
+    public signal void done ();
+
+    /**
+     * Create a new instance of the meta-data extraction manager.
+     */
+    public Harvester (Cancellable     cancellable,
+                      ArrayList<File> locations) {
+        this.cancellable = cancellable;
+        this.locations = new ArrayList<File> ((EqualDataFunc<File>) File.equal);
+        foreach (var file in locations) {
+            if (file.query_exists ()) {
+                this.locations.add (file);
+            }
+        }
+
+        this.extractor = new MetadataExtractor ();
+
+        this.monitor = new RecursiveFileMonitor (cancellable);
+        this.monitor.changed.connect (this.on_file_changed);
+
+        this.tasks = new HashMap<File, HarvestingTask>
+                                        ((HashDataFunc<File>) File.hash,
+                                         (EqualDataFunc<File>) File.equal);
+        this.extraction_grace_timers = new HashMap<File, uint>
+                                        ((HashDataFunc<File>) File.hash,
+                                         (EqualDataFunc<File>) File.equal);
+    }
+
+    /**
+     * Put a file on queue for meta-data extraction
+     *
+     * @param file the file to investigate
+     * @param parent container of the filer to be harvested
+     * @param flag optional flag for the container to set in the database
+     */
+    public void schedule (File           file,
+                          MediaContainer parent,
+                          string?        flag = null) {
+        this.extraction_grace_timers.unset (file);
+        if (this.extractor == null) {
+            warning (_("No metadata extractor available. Will not crawl."));
+
+            return;
+        }
+
+        // Cancel a probably running harvester
+        this.cancel (file);
+
+        var task = new HarvestingTask (new MetadataExtractor (),
+                                       this.monitor,
+                                       file,
+                                       parent,
+                                       flag);
+        task.cancellable = this.cancellable;
+        task.completed.connect (this.on_file_harvested);
+        this.tasks[file] = task;
+        task.run.begin ();
+    }
+
+    /**
+     * Cancel a running meta-data extraction run
+     *
+     * @param file file cancel the current run for
+     */
+    public void cancel (File file) {
+        if (this.tasks.has_key (file)) {
+            var task = this.tasks[file];
+            task.completed.disconnect (this.on_file_harvested);
+            this.tasks.unset (file);
+            task.cancel ();
+        }
+    }
+
+    /**
+     * Callback for finished harvester.
+     *
+     * Updates book-keeping hash.
+     * @param state_machine HarvestingTask sending the event
+     */
+    private void on_file_harvested (StateMachine state_machine) {
+        var task = state_machine as HarvestingTask;
+        var file = task.origin;
+        message (_("'%s' harvested"), file.get_uri ());
+
+        this.tasks.unset (file);
+        if (this.tasks.is_empty) {
+            done ();
+        }
+    }
+
+    private void on_file_changed (File             file,
+                                  File?            other,
+                                  FileMonitorEvent event) {
+        try {
+            switch (event) {
+                case FileMonitorEvent.CREATED:
+                case FileMonitorEvent.CHANGES_DONE_HINT:
+                    this.on_changes_done (file);
+                    break;
+                case FileMonitorEvent.DELETED:
+                    this.on_file_removed (file);
+                    break;
+                default:
+                    break;
+            }
+        } catch (Error error) { }
+    }
+
+    private void on_file_added (File file) {
+        debug ("Filesystem events settled for %s, scheduling extractionâ",
+               file.get_uri ());
+        try {
+            var cache = MediaCache.get_default ();
+            var info = file.query_info (FileAttribute.STANDARD_TYPE + "," +
+                                        FileAttribute.STANDARD_CONTENT_TYPE,
+                                        FileQueryInfoFlags.NONE,
+                                        this.cancellable);
+            if (info.get_file_type () == FileType.DIRECTORY ||
+                info.get_content_type ().has_prefix ("image/") ||
+                info.get_content_type ().has_prefix ("video/") ||
+                info.get_content_type ().has_prefix ("audio/") ||
+                info.get_content_type () == "application/ogg") {
+                string id;
+                try {
+                    MediaContainer parent_container = null;
+                    var current = file;
+                    do {
+                        var parent = current.get_parent ();
+                        id = MediaCache.get_id (parent);
+                        parent_container = cache.get_object (id)
+                                        as MediaContainer;
+
+                        if (parent_container == null) {
+                            current = parent;
+                        }
+
+                        if (current in this.locations) {
+                            // We have reached the top
+                            parent_container = cache.get_object
+                                        (RootContainer.FILESYSTEM_FOLDER_ID)
+                                        as MediaContainer;
+
+                            break;
+                        }
+                    } while (parent_container == null);
+
+                    this.schedule (current, parent_container);
+                } catch (DatabaseError error) {
+                    warning (_("Error fetching object '%s' from database: %s"),
+                            id,
+                            error.message);
+                }
+            } else {
+                debug ("%s is not eligible for extraction", file.get_uri ());
+            }
+        } catch (Error error) {
+            warning (_("Failed to access media cache: %s"), error.message);
+        }
+    }
+
+    private void on_file_removed (File file) throws Error {
+        var cache = MediaCache.get_default ();
+        if (this.extraction_grace_timers.has_key (file)) {
+            Source.remove (this.extraction_grace_timers[file]);
+            this.extraction_grace_timers.unset (file);
+        }
+
+        this.cancel (file);
+        try {
+            // the full object is fetched instead of simply calling
+            // exists because we need the parent to signalize the
+            // change
+            var id = MediaCache.get_id (file);
+            var object = cache.get_object (id);
+            var parent = null as MediaContainer;
+
+            while (object != null) {
+                parent = object.parent;
+                cache.remove_object (object);
+                if (parent == null) {
+                    break;
+                }
+
+                parent.child_count--;
+                if (parent.child_count != 0) {
+                    break;
+                }
+
+                object = parent;
+            }
+
+            if (parent != null) {
+                parent.updated ();
+            }
+        } catch (Error error) {
+            warning (_("Error removing object from database: %s"),
+                     error.message);
+        }
+    }
+
+    private void on_changes_done (File file) throws Error {
+        if (this.extraction_grace_timers.has_key (file)) {
+            Source.remove (this.extraction_grace_timers[file]);
+        } else {
+            debug ("Starting grace timer for harvesting %sâ",
+                    file.get_uri ());
+        }
+
+        SourceFunc callback = () => {
+            this.on_file_added (file);
+
+            return false;
+        };
+
+        var timeout = Timeout.add_seconds (FILE_CHANGE_DEFAULT_GRACE_PERIOD,
+                                           (owned) callback);
+        this.extraction_grace_timers[file] = timeout;
+    }
+}
diff --git a/src/media-export/rygel-media-export-harvesting-task.vala b/src/media-export/rygel-media-export-harvesting-task.vala
new file mode 100644
index 0000000..a0fdb67
--- /dev/null
+++ b/src/media-export/rygel-media-export-harvesting-task.vala
@@ -0,0 +1,366 @@
+/*
+ * Copyright (C) 2009 Jens Georg <mail jensge org>.
+ *
+ * This file is part of Rygel.
+ *
+ * Rygel is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * Rygel 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 Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+
+using GLib;
+using Gee;
+
+public class Rygel.MediaExport.HarvestingTask : Rygel.StateMachine,
+                                                GLib.Object {
+    public File origin;
+    private MetadataExtractor extractor;
+    private MediaCache cache;
+    private GLib.Queue<MediaContainer> containers;
+    private Gee.Queue<File> files;
+    private RecursiveFileMonitor monitor;
+    private string flag;
+    private MediaContainer parent;
+    private const int BATCH_SIZE = 256;
+
+    public Cancellable cancellable { get; set; }
+
+    private const string HARVESTER_ATTRIBUTES =
+                                        FileAttribute.STANDARD_NAME + "," +
+                                        FileAttribute.STANDARD_TYPE + "," +
+                                        FileAttribute.TIME_MODIFIED + "," +
+                                        FileAttribute.STANDARD_CONTENT_TYPE + "," +
+                                        FileAttribute.STANDARD_SIZE;
+
+    public HarvestingTask (MetadataExtractor    extractor,
+                           RecursiveFileMonitor monitor,
+                           File                 file,
+                           MediaContainer       parent,
+                           string?              flag = null) {
+        this.extractor = extractor;
+        this.origin = file;
+        this.parent = parent;
+
+        try {
+            this.cache = MediaCache.get_default ();
+        } catch (Error error) {
+            // This should not happen. As the harvesting tasks are created
+            // long after the first call to get_default which - if fails -
+            // will make the whole root-container creation fail
+            assert_not_reached ();
+        }
+
+        this.extractor.extraction_done.connect (on_extracted_cb);
+        this.extractor.error.connect (on_extractor_error_cb);
+
+        this.files = new LinkedList<File> ();
+        this.containers = new GLib.Queue<MediaContainer> ();
+        this.monitor = monitor;
+        this.flag = flag;
+    }
+
+    public void cancel () {
+        // detach from common cancellable; otherwise everything would be
+        // cancelled like file monitoring, other harvesters etc.
+        this.cancellable = new Cancellable ();
+        this.cancellable.cancel ();
+    }
+
+    /**
+     * Extract all metainformation from a given file.
+     *
+     * What action will be taken depends on the arguments
+     * * file is a simple file. Then only information of this
+     *   file will be extracted
+     * * file is a directory and recursive is false. The children
+     *   of the directory (if not directories themselves) will be
+     *   enqueued for extraction
+     * * file is a directory and recursive is true. ++ All ++ children
+     *   of the directory will be enqueued for extraction, even directories
+     *
+     * No matter how many children are contained within file's hierarchy,
+     * only one event is sent when all the children are done.
+     */
+    public async void run () {
+        try {
+            var info = yield this.origin.query_info_async
+                                        (HARVESTER_ATTRIBUTES,
+                                         FileQueryInfoFlags.NONE,
+                                         Priority.DEFAULT,
+                                         this.cancellable);
+
+            if (this.process_file (this.origin, info, this.parent)) {
+                if (info.get_file_type () != FileType.DIRECTORY) {
+                    this.containers.push_tail (this.parent);
+                }
+                this.on_idle ();
+            } else {
+                this.completed ();
+            }
+        } catch (Error error) {
+            if (!(error is IOError.CANCELLED)) {
+                warning (_("Failed to harvest file %s: %s"),
+                         this.origin.get_uri (),
+                         error.message);
+            } else {
+                debug ("Harvesting of uri %s was cancelled",
+                       this.origin.get_uri ());
+            }
+            this.completed ();
+        }
+    }
+
+    /**
+     * Add a file to the meta-data extraction queue.
+     *
+     * The file will only be added to the queue if one of the following
+     * conditions is met:
+     *   - The file is not in the cache
+     *   - The current mtime of the file is larger than the cached
+     *   - The size has changed
+     * @param file to check
+     * @param info FileInfo of the file to check
+     * @return true, if the file has been queued, false otherwise.
+     */
+    private bool push_if_changed_or_unknown (File       file,
+                                             FileInfo   info) {
+        int64 timestamp;
+        int64 size;
+        try {
+            if (this.cache.exists (file, out timestamp, out size)) {
+                int64 mtime = (int64) info.get_attribute_uint64
+                                        (FileAttribute.TIME_MODIFIED);
+
+                if (mtime > timestamp ||
+                    info.get_size () != size) {
+                    this.files.offer (file);
+
+                    return true;
+                }
+            } else {
+                this.files.offer (file);
+
+                return true;
+            }
+        } catch (Error error) {
+            warning (_("Failed to query database: %s"), error.message);
+        }
+
+        return false;
+    }
+
+    private bool process_file (File           file,
+                               FileInfo       info,
+                               MediaContainer parent) {
+        if (info.get_name ()[0] == '.') {
+            return false;
+        }
+
+        if (info.get_file_type () == FileType.DIRECTORY) {
+            // queue directory for processing later
+            this.monitor.add.begin (file);
+
+            var container = new DummyContainer (file, parent);
+            this.containers.push_tail (container);
+            try {
+                this.cache.save_container (container);
+            } catch (Error err) {
+                warning (_("Failed to update database: %s"), err.message);
+
+                return false;
+            }
+
+            return true;
+        } else {
+            // Check if the file needs to be harvested at all either because
+            // it is denied by filter or it hasn't updated
+            if (info.get_content_type ().has_prefix ("image/") ||
+                info.get_content_type ().has_prefix ("video/") ||
+                info.get_content_type ().has_prefix ("audio/") ||
+                info.get_content_type () == "application/ogg") {
+                return this.push_if_changed_or_unknown (file, info);
+            }
+
+            return false;
+        }
+    }
+
+    private bool process_children (GLib.List<FileInfo>? list) {
+        if (list == null || this.cancellable.is_cancelled ()) {
+            return false;
+        }
+
+        var container = this.containers.peek_head () as DummyContainer;
+
+        foreach (var info in list) {
+            var file = container.file.get_child (info.get_name ());
+
+            container.seen (file);
+            this.process_file (file, info, container);
+        }
+
+        return true;
+    }
+
+    private async void enumerate_directory () {
+        var directory = (this.containers.peek_head () as DummyContainer).file;
+        try {
+            var enumerator = yield directory.enumerate_children_async
+                                        (HARVESTER_ATTRIBUTES,
+                                         FileQueryInfoFlags.NONE,
+                                         Priority.DEFAULT,
+                                         this.cancellable);
+
+            GLib.List<FileInfo> list = null;
+            do {
+                list = yield enumerator.next_files_async (BATCH_SIZE,
+                                                          Priority.DEFAULT,
+                                                          this.cancellable);
+            } while (this.process_children (list));
+
+            yield enumerator.close_async (Priority.DEFAULT, this.cancellable);
+        } catch (Error err) {
+            warning (_("failed to enumerate folder: %s"), err.message);
+        }
+
+        this.cleanup_database ();
+        this.do_update ();
+    }
+
+    private void cleanup_database () {
+        var container = this.containers.peek_head () as DummyContainer;
+
+        // delete all children which are not in filesystem anymore
+        try {
+            foreach (var child in container.children) {
+                this.cache.remove_by_id (child);
+            }
+        } catch (DatabaseError error) {
+            warning (_("Failed to get children of container %s: %s"),
+                     container.id,
+                     error.message);
+        }
+    }
+
+    private bool on_idle () {
+        if (this.cancellable.is_cancelled ()) {
+            this.completed ();
+
+            return false;
+        }
+
+        if (this.files.size > 0) {
+            debug ("Scheduling file %s for meta-data extractionâ",
+                   this.files.peek ().get_uri ());
+            this.extractor.extract (this.files.peek ());
+        } else if (this.containers.get_length () > 0) {
+            this.enumerate_directory.begin ();
+        } else {
+            // nothing to do
+            if (this.flag != null) {
+                try {
+                    this.cache.flag_object (this.origin,
+                                            this.flag);
+                } catch (Error error) {};
+            }
+            parent.updated (parent);
+
+            this.completed ();
+        }
+
+        return false;
+    }
+
+    private void on_extracted_cb (File                   file,
+                                  GUPnP.DLNAInformation? dlna,
+                                  FileInfo               file_info) {
+        if (this.cancellable.is_cancelled ()) {
+            this.completed ();
+        }
+
+        var entry = this.files.peek ();
+        if (entry == null || file != entry) {
+            // this event may be triggered by another instance
+            // just ignore it
+           return;
+        }
+
+        MediaItem item;
+        if (dlna == null) {
+            item = ItemFactory.create_simple (this.containers.peek_head (),
+                                              file,
+                                              file_info);
+        } else {
+            item = ItemFactory.create_from_info (this.containers.peek_head (),
+                                                 file,
+                                                 dlna,
+                                                 file_info);
+        }
+
+        if (item != null) {
+            item.parent_ref = this.containers.peek_head ();
+            try {
+                this.cache.save_item (item);
+            } catch (Error error) {
+                // Ignore it for now
+            }
+        }
+
+        this.files.poll ();
+        this.do_update ();
+    }
+
+    private void on_extractor_error_cb (File file, Error error) {
+        var entry = this.files.peek ();
+        if (entry == null || file != entry) {
+            // this event may be triggered by another instance
+            // just ignore it
+            return;
+        }
+
+        // error is only emitted if even the basic information extraction
+        // failed; there's not much to do here, just print the information and
+        // go to the next file
+
+        debug ("Skipping %s; extraction completely failed: %s",
+               file.get_uri (),
+               error.message);
+
+        this.files.poll ();
+        this.do_update ();
+    }
+
+    /**
+     * If all files of a container were processed, notify the container
+     * about this and set the updating signal.
+     * Reschedule the iteration and extraction
+     */
+    private void do_update () {
+        if (this.files.size == 0 &&
+            this.containers.get_length () != 0) {
+            var container = this.containers.peek_head ();
+            try {
+                var cache = MediaCache.get_default ();
+                if (cache.get_child_count (container.id) > 0) {
+                    var head = this.containers.peek_head ();
+                    head.updated (head);
+                } else {
+                    cache.remove_by_id (container.id);
+                }
+            } catch (Error error) { }
+            this.containers.pop_head ();
+        }
+
+        this.on_idle ();
+    }
+}
diff --git a/src/media-export/rygel-media-export-item.vala b/src/media-export/rygel-media-export-item.vala
new file mode 100644
index 0000000..c8b46e3
--- /dev/null
+++ b/src/media-export/rygel-media-export-item.vala
@@ -0,0 +1,268 @@
+/*
+ * Copyright (C) 2008 Zeeshan Ali <zeenix gmail com>.
+ * Copyright (C) 2008 Nokia Corporation.
+ *
+ * Author: Zeeshan Ali <zeenix gmail com>
+ *
+ * This file is part of Rygel.
+ *
+ * Rygel is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * Rygel 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 Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+
+using GUPnP;
+using Gst;
+
+/**
+ * Represents MediaExport item.
+ */
+namespace Rygel.MediaExport.ItemFactory {
+    public static MediaItem create_simple (MediaContainer parent,
+                                           File           file,
+                                           FileInfo       info) {
+        var title = info.get_display_name ();
+        MediaItem item;
+        var mime = ContentType.get_mime_type (info.get_content_type ());
+
+        if (mime.has_prefix ("video/")) {
+            item = new VideoItem (MediaCache.get_id (file), parent, title);
+        } else if (mime.has_prefix ("image/")) {
+            item = new PhotoItem (MediaCache.get_id (file), parent, title);
+        } else {
+            item = new MusicItem (MediaCache.get_id (file), parent, title);
+        }
+
+        item.mime_type = mime;
+        item.size = (int64) info.get_size ();
+        item.modified = info.get_attribute_uint64
+                                        (FileAttribute.TIME_MODIFIED);
+        item.add_uri (file.get_uri ());
+
+        return item;
+    }
+
+    public static MediaItem? create_from_info
+                                        (MediaContainer        parent,
+                                         File                  file,
+                                         GUPnP.DLNAInformation dlna_info,
+                                         FileInfo              file_info) {
+        MediaItem item;
+        string id = MediaCache.get_id (file);
+        GLib.List<DiscovererAudioInfo> audio_streams;
+        GLib.List<DiscovererVideoInfo> video_streams;
+
+        audio_streams = dlna_info.info.get_audio_streams ();
+        video_streams = dlna_info.info.get_video_streams ();
+
+        if (audio_streams == null && video_streams == null) {
+            debug ("%s had neither audio nor video/picture " +
+                   "streams. Ignoring.",
+                   file.get_uri ());
+
+            return null;
+        }
+
+        if (audio_streams == null && video_streams.data.is_image()) {
+            item = new PhotoItem (id, parent, "");
+            return fill_photo_item (item as PhotoItem,
+                                    file,
+                                    dlna_info,
+                                    video_streams.data,
+                                    file_info);
+        } else if (video_streams != null) {
+            item = new VideoItem (id, parent, "");
+
+            var audio_info = null as DiscovererAudioInfo;
+            if (audio_streams != null) {
+                audio_info = audio_streams.data;
+            }
+
+            return fill_video_item (item as VideoItem,
+                                    file,
+                                    dlna_info,
+                                    video_streams.data,
+                                    audio_info,
+                                    file_info);
+        } else if (audio_streams != null) {
+            item = new MusicItem (id, parent, "");
+            return fill_music_item (item as MusicItem,
+                                    file,
+                                    dlna_info,
+                                    audio_streams.data,
+                                    file_info);
+        } else {
+            return null;
+        }
+    }
+
+    private static void fill_audio_item (AudioItem            item,
+                                         DLNAInformation      dlna_info,
+                                         DiscovererAudioInfo? audio_info) {
+        if (dlna_info.info.get_duration () > 0) {
+            item.duration = (long) (dlna_info.info.get_duration () / Gst.SECOND);
+        } else {
+            item.duration = -1;
+        }
+
+        if (audio_info != null) {
+            if (audio_info.get_tags () != null) {
+                uint tmp;
+                audio_info.get_tags ().get_uint (TAG_BITRATE, out tmp);
+                item.bitrate = (int) tmp / 8;
+            }
+            item.channels = (int) audio_info.get_channels ();
+            item.sample_freq = (int) audio_info.get_sample_rate ();
+        }
+    }
+
+
+    private static MediaItem fill_video_item (VideoItem            item,
+                                              File                 file,
+                                              DLNAInformation      dlna_info,
+                                              DiscovererVideoInfo  video_info,
+                                              DiscovererAudioInfo? audio_info,
+                                              FileInfo             file_info) {
+        fill_audio_item (item as AudioItem, dlna_info, audio_info);
+        fill_media_item (item, file, dlna_info, file_info);
+
+        item.width = (int) video_info.get_width ();
+        item.height = (int) video_info.get_height ();
+
+        var color_depth = (int) video_info.get_depth ();
+        item.color_depth = (color_depth == 0) ? -1 : color_depth;
+
+        return item;
+    }
+
+    private static MediaItem fill_photo_item (PhotoItem           item,
+                                              File                file,
+                                              DLNAInformation     dlna_info,
+                                              DiscovererVideoInfo video_info,
+                                              FileInfo            file_info) {
+        fill_media_item (item, file, dlna_info, file_info);
+
+        item.width = (int) video_info.get_width ();
+        item.height = (int) video_info.get_height ();
+
+        var color_depth = (int) video_info.get_depth ();
+        item.color_depth = (color_depth == 0) ? -1 : color_depth;
+
+        return item;
+    }
+
+    private static MediaItem fill_music_item (MusicItem            item,
+                                              File                 file,
+                                              DLNAInformation      dlna_info,
+                                              DiscovererAudioInfo? audio_info,
+                                              FileInfo             file_info) {
+        fill_audio_item (item as AudioItem, dlna_info, audio_info);
+        fill_media_item (item, file, dlna_info, file_info);
+
+        if (audio_info != null) {
+            if (audio_info.get_tags () != null) {
+                unowned Gst.Buffer buffer;
+                audio_info.get_tags ().get_buffer (TAG_IMAGE, out buffer);
+                if (buffer != null) {
+                    var structure = buffer.caps.get_structure (0);
+                    int image_type;
+                    structure.get_enum ("image-type",
+                            typeof (Gst.TagImageType),
+                            out image_type);
+                    switch (image_type) {
+                        case TagImageType.UNDEFINED:
+                        case TagImageType.FRONT_COVER:
+                            var store = MediaArtStore.get_default ();
+                            var thumb = store.get_media_art_file ("album",
+                                    item,
+                                    true);
+                            try {
+                                var writer = new JPEGWriter ();
+                                writer.write (buffer, thumb);
+                            } catch (Error error) {}
+                            break;
+                        default:
+                            break;
+                    }
+                }
+            }
+
+            string artist;
+            dlna_info.info.get_tags ().get_string (TAG_ARTIST, out artist);
+            item.artist = artist;
+
+            string album;
+            dlna_info.info.get_tags ().get_string (TAG_ALBUM, out album);
+            item.album = album;
+
+            string genre;
+            dlna_info.info.get_tags ().get_string (TAG_GENRE, out genre);
+            item.genre = genre;
+
+            uint tmp;
+            dlna_info.info.get_tags ().get_uint (TAG_ALBUM_VOLUME_NUMBER,
+                                                 out tmp);
+            item.disc = (int) tmp;
+
+            dlna_info.info.get_tags() .get_uint (TAG_TRACK_NUMBER, out tmp);
+            item.track_number = (int) tmp;
+        }
+
+        return item;
+    }
+
+    private static void fill_media_item (MediaItem       item,
+                                         File            file,
+                                         DLNAInformation dlna_info,
+                                         FileInfo        file_info) {
+        string title = null;
+
+        if (dlna_info.info.get_tags () == null ||
+            !dlna_info.info.get_tags ().get_string (TAG_TITLE, out title)) {
+            title = file_info.get_display_name ();
+        }
+
+        item.title = title;
+
+        if (dlna_info.info.get_tags () != null) {
+            GLib.Date? date;
+            if (dlna_info.info.get_tags ().get_date (TAG_DATE, out date)) {
+                char[] datestr = new char[30];
+                date.strftime (datestr, "%F");
+                item.date = (string) datestr;
+            }
+        }
+
+        // use mtime if no time tag was available
+        var mtime = file_info.get_attribute_uint64
+                                        (FileAttribute.TIME_MODIFIED);
+
+        if (item.date == null) {
+            TimeVal tv = { (long) mtime, 0 };
+            item.date = tv.to_iso8601 ();
+        }
+
+        item.size = (int64) file_info.get_size ();
+        item.modified = (int64) mtime;
+        if (dlna_info.name != null) {
+            item.dlna_profile = dlna_info.name;
+            item.mime_type = dlna_info.mime;
+        } else {
+            item.mime_type = ContentType.get_mime_type
+                                        (file_info.get_content_type ());
+        }
+
+        item.add_uri (file.get_uri ());
+    }
+}
+
diff --git a/src/media-export/rygel-media-export-jpeg-writer.vala b/src/media-export/rygel-media-export-jpeg-writer.vala
new file mode 100644
index 0000000..675bfd2
--- /dev/null
+++ b/src/media-export/rygel-media-export-jpeg-writer.vala
@@ -0,0 +1,66 @@
+/*
+ * Copyright (C) 2010 Jens Georg <mail jensge org>.
+ *
+ * Author: Jens Georg <mail jensge org>
+ *
+ * This file is part of Rygel.
+ *
+ * Rygel is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * Rygel 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 Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+
+using Gst;
+
+/**
+ * Utility class to write media-art content to JPEG files
+ *
+ * This uses a gstreamer pipeline to transcode the image tag as contained in
+ * MP3 files. This class is single-shot, use and then throw away.
+ */
+internal class Rygel.MediaExport.JPEGWriter : GLib.Object {
+    private Bin bin;
+    private AppSrc appsrc;
+    private MainLoop loop;
+    private dynamic Element sink;
+
+    public JPEGWriter () throws Error {
+        this.bin = Gst.parse_launch ("appsrc name=src ! decodebin2 ! " +
+                                     "ffmpegcolorspace ! " +
+                                     "jpegenc ! giosink name=sink") as Bin;
+        this.appsrc = bin.get_by_name ("src") as AppSrc;
+        this.sink = bin.get_by_name ("sink");
+        var bus = bin.get_bus ();
+        bus.add_signal_watch ();
+        bus.message["eos"].connect(() => { this.loop.quit (); });
+        bus.message["error"].connect(() => { this.loop.quit (); });
+        this.loop = new MainLoop (null, false);
+    }
+
+    /**
+     * Write a Gst.Buffer as retrieved from the Gst.TagList to disk.
+     *
+     * @param buffer The Gst.Buffer as obtained from tag list
+     * @param file   A GLib.File pointing to the target location
+     *
+     * FIXME This uses a nested main-loop to block which is ugly.
+     */
+    public void write (Gst.Buffer buffer, File file) {
+        this.sink.file = file;
+        this.appsrc.push_buffer (buffer);
+        this.appsrc.end_of_stream ();
+        this.bin.set_state (State.PLAYING);
+        this.loop.run ();
+        this.bin.set_state (State.NULL);
+    }
+}
diff --git a/src/media-export/rygel-media-export-leaf-query-container.vala b/src/media-export/rygel-media-export-leaf-query-container.vala
new file mode 100644
index 0000000..bd82a88
--- /dev/null
+++ b/src/media-export/rygel-media-export-leaf-query-container.vala
@@ -0,0 +1,53 @@
+/*
+ * Copyright (C) 2011 Jens Georg <mail jensge org>.
+ *
+ * This file is part of Rygel.
+ *
+ * Rygel is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * Rygel 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 Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+
+internal class Rygel.MediaExport.LeafQueryContainer : QueryContainer {
+    public LeafQueryContainer (MediaCache       cache,
+                               SearchExpression expression,
+                               string           id,
+                               string           name) {
+        base (cache, expression, id, name);
+    }
+
+    public override async MediaObjects? get_children
+                                        (uint         offset,
+                                         uint         max_count,
+                                         string       sort_criteria,
+                                         Cancellable? cancellable)
+                                         throws GLib.Error {
+        uint total_matches;
+        var children = yield this.search (null,
+                                          offset,
+                                          max_count,
+                                          out total_matches,
+                                          sort_criteria,
+                                          cancellable);
+        foreach (var child in children) {
+            child.parent = this;
+        }
+
+        return children;
+    }
+
+    protected override int count_children () throws Error {
+        return (int) this.media_db.get_object_count_by_search_expression
+                                        (this.expression, null);
+    }
+}
diff --git a/src/media-export/rygel-media-export-media-cache-upgrader.vala b/src/media-export/rygel-media-export-media-cache-upgrader.vala
new file mode 100644
index 0000000..c1f540e
--- /dev/null
+++ b/src/media-export/rygel-media-export-media-cache-upgrader.vala
@@ -0,0 +1,368 @@
+/*
+ * Copyright (C) 2010 Jens Georg <mail jensge org>.
+ *
+ * Author: Jens Georg <mail jensge org>
+ *
+ * This file is part of Rygel.
+ *
+ * Rygel is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * Rygel 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 Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+using Gee;
+
+internal class Rygel.MediaExport.MediaCacheUpgrader {
+    private unowned Database database;
+    private unowned SQLFactory sql;
+
+    private const string UPDATE_V3_V4_STRING_2 =
+    "UPDATE meta_data SET object_fk = " +
+        "(SELECT upnp_id FROM Object WHERE metadata_fk = meta_data.id)";
+
+    private const string UPDATE_V3_V4_STRING_3 =
+    "ALTER TABLE Object ADD timestamp INTEGER";
+
+    private const string UPDATE_V3_V4_STRING_4 =
+    "UPDATE Object SET timestamp = 0";
+
+    public MediaCacheUpgrader (Database database, SQLFactory sql) {
+        this.database = database;
+        this.sql = sql;
+    }
+
+    public bool needs_upgrade (out int current_version) throws Error {
+        current_version = this.database.query_value (
+                                        "SELECT version FROM schema_info");
+
+        return current_version < int.parse (SQLFactory.SCHEMA_VERSION);
+    }
+
+    public void fix_schema () throws Error {
+        var matching_schema_count = this.database.query_value (
+                                        "SELECT count(*) FROM " +
+                                        "sqlite_master WHERE sql " +
+                                        "LIKE 'CREATE TABLE Meta_Data" +
+                                        "%object_fk TEXT UNIQUE%'");
+        if (matching_schema_count == 0) {
+            try {
+                message ("Found faulty schema, forcing full reindex");
+                database.begin ();
+                database.exec ("DELETE FROM Object WHERE upnp_id IN (" +
+                               "SELECT DISTINCT object_fk FROM meta_data)");
+                database.exec ("DROP TABLE Meta_Data");
+                database.exec (this.sql.make (SQLString.TABLE_METADATA));
+                database.commit ();
+            } catch (Error error) {
+                database.rollback ();
+                warning ("Failed to force reindex to fix database: " +
+                        error.message);
+            }
+        }
+    }
+
+    public void ensure_indices () {
+        try {
+            this.database.exec (this.sql.make (SQLString.INDEX_COMMON));
+            this.database.analyze ();
+        } catch (Error error) {
+            warning ("Failed to create indices: " +
+                     error.message);
+        }
+    }
+
+    public void upgrade (int old_version) {
+        debug ("Older schema detected. Upgrading...");
+        int current_version = int.parse (SQLFactory.SCHEMA_VERSION);
+        while (old_version < current_version) {
+            if (this.database != null) {
+                switch (old_version) {
+                    case 3:
+                        update_v3_v4 ();
+                        break;
+                    case 4:
+                        update_v4_v5 ();
+                        break;
+                    case 5:
+                        update_v5_v6 ();
+                        break;
+                    case 6:
+                        update_v6_v7 ();
+                        break;
+                    case 7:
+                        update_v7_v8 ();
+                        break;
+                    case 8:
+                        update_v8_v9 ();
+                        break;
+                    case 9:
+                        update_v9_v10 ();
+                        break;
+                    case 10:
+                        update_v10_v11 ();
+                        break;
+                    default:
+                        warning ("Cannot upgrade");
+                        database = null;
+                        break;
+                }
+                old_version++;
+            }
+        }
+    }
+
+    private void force_reindex () throws DatabaseError {
+        database.exec ("UPDATE Object SET timestamp = 0");
+    }
+
+    private void update_v3_v4 () {
+        try {
+            database.begin ();
+            database.exec ("ALTER TABLE Meta_Data RENAME TO _Meta_Data");
+            database.exec (this.sql.make (SQLString.TABLE_METADATA));
+            database.exec ("INSERT INTO meta_data (size, mime_type, " +
+                           "duration, width, height, class, author, album, " +
+                           "date, bitrate, sample_freq, bits_per_sample, " +
+                           "channels, track, color_depth, object_fk) SELECT " +
+                           "size, mime_type, duration, width, height, class, " +
+                           "author, album, date, bitrate, sample_freq, " +
+                           "bits_per_sample, channels, track, color_depth, " +
+                           "o.upnp_id FROM _Meta_Data JOIN object o " +
+                           "ON id = o.metadata_fk");
+            database.exec ("DROP TABLE _Meta_Data");
+            database.exec (UPDATE_V3_V4_STRING_3);
+            database.exec (UPDATE_V3_V4_STRING_4);
+            database.exec (this.sql.make (SQLString.TRIGGER_COMMON));
+            database.exec ("UPDATE schema_info SET version = '4'");
+            database.commit ();
+        } catch (DatabaseError error) {
+            database.rollback ();
+            warning ("Database upgrade failed: %s", error.message);
+            database = null;
+        }
+    }
+
+    private void update_v4_v5 () {
+        Gee.Queue<string> queue = new LinkedList<string> ();
+        try {
+            database.begin ();
+            database.exec ("DROP TRIGGER IF EXISTS trgr_delete_children");
+            database.exec (this.sql.make (SQLString.TABLE_CLOSURE));
+            // this is to have the database generate the closure table
+            database.exec ("ALTER TABLE Object RENAME TO _Object");
+            database.exec ("CREATE TABLE Object AS SELECT * FROM _Object");
+            database.exec ("DELETE FROM Object");
+            database.exec (this.sql.make (SQLString.TRIGGER_CLOSURE));
+            database.exec ("INSERT INTO _Object (upnp_id, type_fk, title, " +
+                           "timestamp) VALUES ('0', 0, 'Root', 0)");
+            database.exec ("INSERT INTO Object (upnp_id, type_fk, title, " +
+                           "timestamp) VALUES ('0', 0, 'Root', 0)");
+
+            queue.offer ("0");
+            while (!queue.is_empty) {
+                GLib.Value[] args = { queue.poll () };
+                var cursor = this.database.exec_cursor (
+                                        "SELECT upnp_id FROM _Object WHERE " +
+                                        "parent = ?",
+                                        args);
+                foreach (var statement in cursor) {
+                    queue.offer (statement.column_text (0));
+                }
+
+                database.exec ("INSERT INTO Object SELECT * FROM _OBJECT " +
+                               "WHERE parent = ?",
+                               args);
+            }
+            database.exec ("DROP TABLE Object");
+            database.exec ("ALTER TABLE _Object RENAME TO Object");
+            // the triggers created above have been dropped automatically
+            // so we need to recreate them
+            database.exec (this.sql.make (SQLString.TRIGGER_CLOSURE));
+            database.exec (this.sql.make (SQLString.INDEX_COMMON));
+            database.exec ("UPDATE schema_info SET version = '5'");
+            database.commit ();
+            database.exec ("VACUUM");
+            database.analyze ();
+        } catch (DatabaseError err) {
+            database.rollback ();
+            warning ("Database upgrade failed: %s", err.message);
+            database = null;
+        }
+    }
+
+    private void update_v5_v6 () {
+        try {
+            database.begin ();
+            database.exec ("DROP TABLE object_type");
+            database.exec ("DROP TRIGGER IF EXISTS trgr_delete_uris");
+            database.exec ("ALTER TABLE Object ADD COLUMN uri TEXT");
+            database.exec ("UPDATE Object SET uri = (SELECT uri " +
+                     "FROM uri WHERE Uri.object_fk == Object.upnp_id LIMIT 1)");
+            database.exec ("DROP INDEX IF EXISTS idx_uri_fk");
+            database.exec ("DROP TABLE Uri");
+            database.exec ("UPDATE schema_info SET version = '6'");
+            database.commit ();
+            database.exec ("VACUUM");
+            database.analyze ();
+        } catch (DatabaseError error) {
+            database.rollback ();
+            warning ("Database upgrade failed: %s", error.message);
+            database = null;
+        }
+    }
+
+    private void update_v6_v7 () {
+        try {
+            database.begin ();
+            database.exec ("ALTER TABLE meta_data ADD COLUMN dlna_profile TEXT");
+            database.exec ("UPDATE schema_info SET version = '7'");
+            force_reindex ();
+            database.commit ();
+            database.exec ("VACUUM");
+            database.analyze ();
+        } catch (DatabaseError error) {
+            database.rollback ();
+            warning ("Database upgrade failed: %s", error.message);
+            database = null;
+        }
+    }
+
+    private void update_v7_v8 () {
+        try {
+            database.begin ();
+            database.exec ("ALTER TABLE object ADD COLUMN flags TEXT");
+            database.exec ("ALTER TABLE meta_data ADD COLUMN genre TEXT");
+            database.exec ("UPDATE schema_info SET version = '8'");
+            force_reindex ();
+            database.commit ();
+            database.exec ("VACUUM");
+            database.analyze ();
+        } catch (DatabaseError error) {
+            database.rollback ();
+            warning ("Database upgrade failed: %s", error.message);
+            database = null;
+        }
+    }
+
+    private void update_v8_v9 () {
+        try {
+            this.database.begin ();
+            this.database.exec ("DROP TRIGGER trgr_update_closure");
+            this.database.exec ("DROP TRIGGER trgr_delete_closure");
+            this.database.exec ("ALTER TABLE Closure RENAME TO _Closure");
+            this.database.exec (this.sql.make (SQLString.TABLE_CLOSURE));
+            this.database.exec ("INSERT INTO Closure (ancestor, " +
+                                "descendant, depth) SELECT DISTINCT " +
+                                "ancestor, descendant, depth FROM " +
+                                "_Closure");
+            this.database.exec (this.sql.make (SQLString.TRIGGER_CLOSURE));
+            this.database.exec ("DROP TABLE _Closure");
+            this.database.exec ("UPDATE schema_info SET version = '9'");
+            this.database.commit ();
+            this.database.exec ("VACUUM");
+        } catch (DatabaseError error) {
+            database.rollback ();
+            warning ("Database upgrade failed: %s", error.message);
+            database = null;
+        }
+    }
+
+    // This isn't really a schema update but a semantics update
+    private void update_v9_v10 () {
+        try {
+            var queue = new LinkedList<string> ();
+            this.database.begin ();
+            this.database.exec ("DELETE FROM Object WHERE upnp_id LIKE '" +
+                                QueryContainer.PREFIX + "%'");
+            this.database.exec ("DROP TRIGGER trgr_update_closure");
+            this.database.exec ("DROP TRIGGER trgr_delete_closure");
+            this.database.exec ("DROP INDEX idx_parent");
+            this.database.exec ("DROP INDEX idx_meta_data_fk");
+            this.database.exec ("DROP INDEX IF EXISTS idx_closure");
+            this.database.exec ("DROP TABLE Closure");
+
+            // keep meta-data although we're deleting loads of objects
+            this.database.exec ("DROP TRIGGER trgr_delete_metadata");
+
+            this.database.exec ("INSERT OR REPLACE INTO Object (parent, upnp_id, " +
+                                "type_fk, title, timestamp) VALUES " +
+                                "('0', '" +
+                                RootContainer.FILESYSTEM_FOLDER_ID +
+                                "', 0, '" +
+                                _(RootContainer.FILESYSTEM_FOLDER_NAME) +
+                                "', 0)");
+            this.database.exec ("UPDATE Object SET parent = '" +
+                                RootContainer.FILESYSTEM_FOLDER_ID +
+                                "' WHERE parent = '0' AND upnp_id " +
+                                "NOT LIKE 'virtual-%' AND upnp_id " +
+                                "<> '" +
+                                RootContainer.FILESYSTEM_FOLDER_ID +
+                                "'");
+            this.database.exec ("ALTER TABLE Object RENAME TO _Object");
+            this.database.exec ("CREATE TABLE Object AS SELECT * FROM _Object");
+            this.database.exec ("DELETE FROM Object");
+            this.database.exec (this.sql.make (SQLString.TABLE_CLOSURE));
+            this.database.exec (this.sql.make (SQLString.TRIGGER_CLOSURE));
+            this.database.exec ("INSERT INTO Closure (ancestor, descendant, " +
+                                "depth) VALUES ('0','0',0)");
+            queue.offer ("0");
+            while (!queue.is_empty) {
+                GLib.Value[] args = { queue.poll () };
+                var cursor = this.database.exec_cursor (
+                                        "SELECT upnp_id FROM _Object WHERE " +
+                                        "parent = ?",
+                                        args);
+                foreach (var statement in cursor) {
+                    queue.offer (statement.column_text (0));
+                }
+
+                database.exec ("INSERT INTO Object SELECT * FROM _Object " +
+                               "WHERE parent = ?",
+                               args);
+            }
+            database.exec ("DROP TABLE Object");
+            this.database.exec ("ALTER TABLE _Object RENAME TO Object");
+            database.exec (this.sql.make (SQLString.INDEX_COMMON));
+            database.exec (this.sql.make (SQLString.TRIGGER_COMMON));
+            this.database.exec (this.sql.make (SQLString.TRIGGER_CLOSURE));
+            database.exec ("UPDATE schema_info SET version = '10'");
+            database.commit ();
+            database.exec ("VACUUM");
+            database.analyze ();
+        } catch (DatabaseError error) {
+            database.rollback ();
+            warning ("Database upgrade failed: %s", error.message);
+            database = null;
+        }
+    }
+
+    private void update_v10_v11 () {
+        try {
+            this.database.begin ();
+            this.database.exec ("ALTER TABLE Meta_Data " +
+                                "   ADD COLUMN disc INTEGER");
+            // Force reindexing of audio data to get disc number
+            this.database.exec ("UPDATE Object SET timestamp = 0 WHERE " +
+                                "  upnp_id IN (" +
+                                "SELECT object_fk FROM Meta_Data WHERE " +
+                                "  class LIKE 'object.item.audioItem.%')");
+            this.database.exec ("UPDATE schema_info SET version = '11'");
+            database.commit ();
+            database.exec ("VACUUM");
+            database.analyze ();
+        } catch (DatabaseError error) {
+            database.rollback ();
+            warning ("Database upgrade failed: %s", error.message);
+            database = null;
+        }
+    }
+}
diff --git a/src/media-export/rygel-media-export-media-cache.vala b/src/media-export/rygel-media-export-media-cache.vala
new file mode 100644
index 0000000..257929a
--- /dev/null
+++ b/src/media-export/rygel-media-export-media-cache.vala
@@ -0,0 +1,896 @@
+/*
+ * Copyright (C) 2009,2010 Jens Georg <mail jensge org>.
+ *
+ * Author: Jens Georg <mail jensge org>
+ *
+ * This file is part of Rygel.
+ *
+ * Rygel is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * Rygel 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 Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+
+
+using Gee;
+using GUPnP;
+using Sqlite;
+
+public errordomain Rygel.MediaExport.MediaCacheError {
+    SQLITE_ERROR,
+    GENERAL_ERROR,
+    INVALID_TYPE,
+    UNSUPPORTED_SEARCH
+}
+
+internal enum Rygel.MediaExport.ObjectType {
+    CONTAINER,
+    ITEM
+}
+
+internal struct Rygel.MediaExport.ExistsCacheEntry {
+    int64 mtime;
+    int64 size;
+}
+
+/**
+ * Persistent storage of media objects
+ *
+ *  MediaExportDB is a sqlite3 backed persistent storage of media objects
+ */
+public class Rygel.MediaExport.MediaCache : Object {
+    // Private members
+    private Database                           db;
+    private ObjectFactory                      factory;
+    private SQLFactory                         sql;
+    private HashMap<string, ExistsCacheEntry?> exists_cache;
+
+    // Private static members
+    private static MediaCache instance;
+
+    // Constructors
+    private MediaCache () throws Error {
+        this.sql = new SQLFactory ();
+        this.open_db ("media-export");
+        this.factory = new ObjectFactory ();
+        this.get_exists_cache ();
+    }
+
+    // Public static functions
+    public static string get_id (File file) {
+        return Checksum.compute_for_string (ChecksumType.MD5,
+                                            file.get_uri ());
+    }
+
+    public static MediaCache get_default () throws Error {
+        if (instance == null) {
+            instance = new MediaCache ();
+        }
+
+        return instance;
+    }
+
+    // Public functions
+    public void remove_by_id (string id) throws DatabaseError {
+        GLib.Value[] values = { id };
+        this.db.exec (this.sql.make (SQLString.DELETE), values);
+    }
+
+    public void remove_object (MediaObject object) throws DatabaseError,
+                                                          MediaCacheError {
+        this.remove_by_id (object.id);
+    }
+
+    public void save_container (MediaContainer container) throws Error {
+        try {
+            db.begin ();
+            create_object (container);
+            db.commit ();
+        } catch (DatabaseError error) {
+            db.rollback ();
+
+            throw error;
+        }
+    }
+
+    public void save_item (Rygel.MediaItem item) throws Error {
+        try {
+            db.begin ();
+            save_metadata (item);
+            create_object (item);
+            db.commit ();
+        } catch (DatabaseError error) {
+            warning (_("Failed to add item with ID %s: %s"),
+                     item.id,
+                     error.message);
+            db.rollback ();
+
+            throw error;
+        }
+    }
+
+    public MediaObject? get_object (string object_id) throws DatabaseError {
+        GLib.Value[] values = { object_id };
+        MediaObject parent = null;
+
+        var cursor = this.exec_cursor (SQLString.GET_OBJECT, values);
+
+        foreach (var statement in cursor) {
+            var parent_container = parent as MediaContainer;
+            var object = this.get_object_from_statement
+                                        (parent_container,
+                                         statement);
+            object.parent_ref = parent_container;
+            parent = object;
+        }
+
+        return parent;
+    }
+
+    public MediaContainer? get_container (string container_id)
+                                          throws DatabaseError,
+                                                 MediaCacheError {
+        var object = get_object (container_id);
+        if (object != null && !(object is MediaContainer)) {
+            throw new MediaCacheError.INVALID_TYPE ("Object with id %s is " +
+                                                    "not a MediaContainer",
+                                                    container_id);
+        }
+
+        return object as MediaContainer;
+    }
+
+    public int get_child_count (string container_id) throws DatabaseError {
+        GLib.Value[] values = { container_id };
+
+        return this.query_value (SQLString.CHILD_COUNT, values);
+    }
+
+
+    public bool exists (File      file,
+                        out int64 timestamp,
+                        out int64 size) throws DatabaseError {
+        var uri = file.get_uri ();
+        GLib.Value[] values = { uri };
+
+        if (this.exists_cache.has_key (uri)) {
+            var entry = this.exists_cache.get (uri);
+            this.exists_cache.unset (uri);
+            timestamp = entry.mtime;
+            size = entry.size;
+
+            return true;
+        }
+
+        var cursor = this.exec_cursor (SQLString.EXISTS, values);
+        var statement = cursor.next ();
+        timestamp = statement->column_int64 (1);
+        size = statement->column_int64 (2);
+
+        return statement->column_int (0) == 1;
+    }
+
+    public MediaObjects get_children (MediaContainer container,
+                                      string         sort_criteria,
+                                      long           offset,
+                                      long           max_count)
+                                      throws Error {
+        MediaObjects children = new MediaObjects ();
+
+        GLib.Value[] values = { container.id,
+                                offset,
+                                max_count };
+
+        var sql = this.sql.make (SQLString.GET_CHILDREN);
+        var sort_order = this.translate_sort_criteria (sort_criteria);
+        var cursor = this.db.exec_cursor (sql.printf (sort_order), values);
+
+        foreach (var statement in cursor) {
+            children.add (this.get_object_from_statement (container,
+                                                          statement));
+            children.last ().parent_ref = container;
+        }
+
+        return children;
+    }
+
+    public MediaObjects get_objects_by_search_expression
+                                        (SearchExpression? expression,
+                                         string?           container_id,
+                                         string            sort_criteria,
+                                         uint              offset,
+                                         uint              max_count,
+                                         out uint          total_matches)
+                                         throws Error {
+        var args = new GLib.ValueArray (0);
+        var filter = this.translate_search_expression (expression, args);
+
+        if (expression != null) {
+            debug ("Original search: %s", expression.to_string ());
+            debug ("Parsed search expression: %s", filter);
+        }
+
+        var max_objects = modify_limit (max_count);
+        total_matches = (uint) get_object_count_by_filter (filter,
+                                                           args,
+                                                           container_id);
+
+        return this.get_objects_by_filter (filter,
+                                           args,
+                                           container_id,
+                                           sort_criteria,
+                                           offset,
+                                           max_objects);
+    }
+
+    public long get_object_count_by_search_expression
+                                        (SearchExpression? expression,
+                                         string?           container_id)
+                                         throws Error {
+        var args = new GLib.ValueArray (0);
+        var filter = this.translate_search_expression (expression, args);
+
+        if (expression != null) {
+            debug ("Original search: %s", expression.to_string ());
+            debug ("Parsed search expression: %s", filter);
+        }
+
+        for (int i = 0; i < args.n_values; i++) {
+            var arg = args.get_nth (i);
+            debug ("Arg %d: %s", i, arg.holds (typeof (string)) ?
+                                        arg.get_string () :
+                                        arg.strdup_contents ());
+        }
+
+        return this.get_object_count_by_filter (filter,
+                                                args,
+                                                container_id);
+    }
+
+    public long get_object_count_by_filter
+                                        (string          filter,
+                                         GLib.ValueArray args,
+                                         string?         container_id)
+                                         throws Error {
+        if (container_id != null) {
+            GLib.Value v = container_id;
+            args.prepend (v);
+        }
+
+        debug ("Parameters to bind: %u", args.n_values);
+        unowned string pattern;
+        SQLString string_id;
+        if (container_id != null) {
+            string_id = SQLString.GET_OBJECT_COUNT_BY_FILTER_WITH_ANCESTOR;
+        } else {
+            string_id = SQLString.GET_OBJECT_COUNT_BY_FILTER;
+        }
+        pattern = this.sql.make (string_id);
+
+        return this.db.query_value (pattern.printf (filter), args.values);
+    }
+
+    public MediaObjects get_objects_by_filter (string          filter,
+                                               GLib.ValueArray args,
+                                               string?         container_id,
+                                               string          sort_criteria,
+                                               long            offset,
+                                               long            max_count)
+                                               throws Error {
+        var children = new MediaObjects ();
+        GLib.Value v = offset;
+        args.append (v);
+        v = max_count;
+        args.append (v);
+        MediaContainer parent = null;
+
+        debug ("Parameters to bind: %u", args.n_values);
+        for (int i = 0; i < args.n_values; i++) {
+            var arg = args.get_nth (i);
+            debug ("Arg %d: %s", i, arg.holds (typeof (string)) ?
+                                        arg.get_string () :
+                                        arg.strdup_contents ());
+        }
+
+        unowned string sql;
+        if (container_id != null) {
+            sql = this.sql.make (SQLString.GET_OBJECTS_BY_FILTER_WITH_ANCESTOR);
+        } else {
+            sql = this.sql.make (SQLString.GET_OBJECTS_BY_FILTER);
+        }
+
+        var sort_order = this.translate_sort_criteria (sort_criteria);
+        var cursor = this.db.exec_cursor (sql.printf (filter, sort_order),
+                                          args.values);
+        foreach (var statement in cursor) {
+            unowned string parent_id = statement.column_text (DetailColumn.PARENT);
+
+            if (parent == null || parent_id != parent.id) {
+                parent = new NullContainer ();
+                parent.id = parent_id;
+            }
+
+            if (parent != null) {
+                children.add (this.get_object_from_statement (parent,
+                                                              statement));
+                children.last ().parent_ref = parent;
+            } else {
+                warning ("Inconsistent database: item %s " +
+                         "has no parent %s",
+                         statement.column_text (DetailColumn.ID),
+                         parent_id);
+            }
+        }
+
+        return children;
+    }
+
+    public void debug_statistics () {
+        try {
+            debug ("Database statistics:");
+            var cursor = this.exec_cursor (SQLString.STATISTICS);
+            foreach (var statement in cursor) {
+                debug ("%s: %d",
+                       statement.column_text (0),
+                       statement.column_int (1));
+            }
+        } catch (Error error) { }
+    }
+
+    public ArrayList<string> get_child_ids (string container_id)
+                                            throws DatabaseError {
+        ArrayList<string> children = new ArrayList<string> ();
+        GLib.Value[] values = { container_id  };
+
+        var cursor = this.exec_cursor (SQLString.CHILD_IDS, values);
+        foreach (var statement in cursor) {
+            children.add (statement.column_text (0));
+        }
+
+        return children;
+    }
+
+    public Gee.List<string> get_meta_data_column_by_filter
+                                        (string          column,
+                                         string          filter,
+                                         GLib.ValueArray args,
+                                         long            offset,
+                                         long            max_count)
+                                         throws Error {
+        GLib.Value v = offset;
+        args.append (v);
+        v = max_count;
+        args.append (v);
+
+        var data = new ArrayList<string> ();
+
+        unowned string sql = this.sql.make (SQLString.GET_META_DATA_COLUMN);
+        var cursor = this.db.exec_cursor (sql.printf (column, filter),
+                                          args.values);
+        foreach (var statement in cursor) {
+            data.add (statement.column_text (0));
+        }
+
+        return data;
+    }
+
+    public Gee.List<string> get_object_attribute_by_search_expression
+                                        (string            attribute,
+                                         SearchExpression? expression,
+                                         long              offset,
+                                         uint              max_count)
+                                         throws Error {
+        var args = new ValueArray (0);
+        var filter = this.translate_search_expression (expression,
+                                                       args,
+                                                       "AND");
+
+        debug ("Parsed filter: %s", filter);
+
+        var column = this.map_operand_to_column (attribute);
+        var max_objects = modify_limit (max_count);
+
+        return this.get_meta_data_column_by_filter (column,
+                                                    filter,
+                                                    args,
+                                                    offset,
+                                                    max_objects);
+    }
+
+    public void flag_object (File file, string flag) throws Error {
+        GLib.Value[] args = { flag, file.get_uri () };
+        this.db.exec ("UPDATE Object SET flags = ? WHERE uri = ?", args);
+    }
+
+    public Gee.List<string> get_flagged_uris (string flag) throws Error {
+        var uris = new ArrayList<string> ();
+        const string query = "SELECT uri FROM object WHERE flags = ?";
+
+        GLib.Value[] args = { flag };
+
+        var cursor = this.db.exec_cursor (query, args);
+        foreach (var statement in cursor) {
+            uris.add (statement.column_text (0));
+        }
+
+        return uris;
+    }
+
+    // Private functions
+    private void get_exists_cache () throws DatabaseError {
+        this.exists_cache = new HashMap<string, ExistsCacheEntry?> ();
+        var cursor = this.exec_cursor (SQLString.EXISTS_CACHE);
+        foreach (var statement in cursor) {
+            var entry = ExistsCacheEntry ();
+            entry.mtime = statement.column_int64 (1);
+            entry.size = statement.column_int64 (0);
+            this.exists_cache.set (statement.column_text (2), entry);
+        }
+    }
+
+    private uint modify_limit (uint max_count) {
+        if (max_count == 0) {
+            return -1;
+        } else {
+            return max_count;
+        }
+    }
+
+    private void open_db (string name) throws Error {
+        this.db = new Database (name);
+        int old_version = -1;
+        int current_version = int.parse (SQLFactory.SCHEMA_VERSION);
+
+        try {
+            var upgrader = new MediaCacheUpgrader (this.db, this.sql);
+            if (upgrader.needs_upgrade (out old_version)) {
+                upgrader.upgrade (old_version);
+            } else if (old_version == current_version) {
+                upgrader.fix_schema ();
+            } else {
+                warning ("The version \"%d\" of the detected database" +
+                         " is newer than our supported version \"%d\"",
+                         old_version,
+                         current_version);
+                this.db = null;
+
+                throw new MediaCacheError.GENERAL_ERROR ("Database format" +
+                                                         " not supported");
+            }
+            upgrader.ensure_indices ();
+        } catch (DatabaseError error) {
+            debug ("Could not find schema version;" +
+                   " checking for empty database...");
+            try {
+                var rows = this.db.query_value ("SELECT count(type) FROM " +
+                                                "sqlite_master WHERE rowid=1");
+                if (rows == 0) {
+                    debug ("Empty database, creating new schema version %s",
+                            SQLFactory.SCHEMA_VERSION);
+                    if (!create_schema ()) {
+                        this.db = null;
+
+                        return;
+                    }
+                } else {
+                    warning ("Incompatible schema... cannot proceed");
+                    this.db = null;
+
+                    return;
+                }
+            } catch (DatabaseError error) {
+                warning ("Something weird going on: %s", error.message);
+                this.db = null;
+
+                throw new MediaCacheError.GENERAL_ERROR ("Invalid database");
+            }
+        }
+    }
+
+    private void save_metadata (Rygel.MediaItem item) throws Error {
+        // Fill common properties
+        GLib.Value[] values = { item.size,
+                                item.mime_type,
+                                -1,
+                                -1,
+                                item.upnp_class,
+                                Database.null (),
+                                Database.null (),
+                                item.date,
+                                -1,
+                                -1,
+                                -1,
+                                -1,
+                                -1,
+                                -1,
+                                -1,
+                                item.id,
+                                item.dlna_profile,
+                                Database.null (),
+                                -1};
+
+        if (item is AudioItem) {
+            var audio_item = item as AudioItem;
+            values[14] = audio_item.duration;
+            values[8] = audio_item.bitrate;
+            values[9] = audio_item.sample_freq;
+            values[10] = audio_item.bits_per_sample;
+            values[11] = audio_item.channels;
+            if (item is MusicItem) {
+                var music_item = item as MusicItem;
+                values[5] = music_item.artist;
+                values[6] = music_item.album;
+                values[17] = music_item.genre;
+                values[12] = music_item.track_number;
+                values[18] = music_item.disc;
+            }
+        }
+
+        if (item is VisualItem) {
+            var visual_item = item as VisualItem;
+            values[2] = visual_item.width;
+            values[3] = visual_item.height;
+            values[13] = visual_item.color_depth;
+            if (item is VideoItem) {
+                var video_item = item as VideoItem;
+                values[5] = video_item.author;
+            }
+        }
+
+        this.db.exec (this.sql.make (SQLString.SAVE_METADATA), values);
+    }
+
+    private void create_object (MediaObject item) throws Error {
+        int type = ObjectType.CONTAINER;
+        GLib.Value parent;
+
+        if (item is MediaItem) {
+            type = ObjectType.ITEM;
+        }
+
+        if (item.parent == null) {
+            parent = Database  null ();
+        } else {
+            parent = item.parent.id;
+        }
+
+        GLib.Value[] values = { item.id,
+                                item.title,
+                                type,
+                                parent,
+                                item.modified,
+                                item.uris.size == 0 ? null : item.uris[0]
+                              };
+        this.db.exec (this.sql.make (SQLString.INSERT), values);
+    }
+
+    /**
+     * Create the current schema.
+     *
+     * If schema creation fails, schema will be rolled back
+     * completely.
+     *
+     * @returns: true on success, false on failure
+     */
+    private bool create_schema () {
+        try {
+            db.begin ();
+            db.exec (this.sql.make (SQLString.SCHEMA));
+            db.exec (this.sql.make (SQLString.TRIGGER_COMMON));
+            db.exec (this.sql.make (SQLString.TABLE_CLOSURE));
+            db.exec (this.sql.make (SQLString.INDEX_COMMON));
+            db.exec (this.sql.make (SQLString.TRIGGER_CLOSURE));
+            db.commit ();
+            db.analyze ();
+
+            return true;
+        } catch (Error err) {
+            warning ("Failed to create schema: %s", err.message);
+            db.rollback ();
+        }
+
+        return false;
+   }
+
+    private MediaObject? get_object_from_statement (MediaContainer? parent,
+                                                    Statement       statement) {
+        MediaObject object = null;
+        unowned string title = statement.column_text (DetailColumn.TITLE);
+        unowned string object_id = statement.column_text (DetailColumn.ID);
+        unowned string uri = statement.column_text (DetailColumn.URI);
+
+        switch (statement.column_int (DetailColumn.TYPE)) {
+            case 0:
+                // this is a container
+                object = factory.get_container (this, object_id, title, 0, uri);
+
+                var container = object as MediaContainer;
+                if (uri != null) {
+                    container.uris.add (uri);
+                }
+                break;
+            case 1:
+                // this is an item
+                unowned string upnp_class = statement.column_text
+                                        (DetailColumn.CLASS);
+                object = factory.get_item (this,
+                                           parent,
+                                           object_id,
+                                           title,
+                                           upnp_class);
+                fill_item (statement, object as MediaItem);
+
+                if (uri != null) {
+                    (object as MediaItem).add_uri (uri);
+                }
+                break;
+            default:
+                assert_not_reached ();
+        }
+
+        if (object != null) {
+            object.modified = statement.column_int64 (DetailColumn.TIMESTAMP);
+            if (object.modified  == int64.MAX && object is MediaItem) {
+                object.modified = 0;
+                (object as MediaItem).place_holder = true;
+            }
+        }
+
+        return object;
+    }
+
+    private void fill_item (Statement statement, MediaItem item) {
+        // Fill common properties
+        item.date = statement.column_text (DetailColumn.DATE);
+        item.mime_type = statement.column_text (DetailColumn.MIME_TYPE);
+        item.dlna_profile = statement.column_text (DetailColumn.DLNA_PROFILE);
+        item.size = statement.column_int64 (DetailColumn.SIZE);
+
+        if (item is AudioItem) {
+            var audio_item = item as AudioItem;
+            audio_item.duration = (long) statement.column_int64
+                                        (DetailColumn.DURATION);
+            audio_item.bitrate = statement.column_int (DetailColumn.BITRATE);
+            audio_item.sample_freq = statement.column_int
+                                        (DetailColumn.SAMPLE_FREQ);
+            audio_item.bits_per_sample = statement.column_int
+                                        (DetailColumn.BITS_PER_SAMPLE);
+            audio_item.channels = statement.column_int (DetailColumn.CHANNELS);
+            if (item is MusicItem) {
+                var music_item = item as MusicItem;
+                music_item.artist = statement.column_text (DetailColumn.AUTHOR);
+                music_item.album = statement.column_text (DetailColumn.ALBUM);
+                music_item.genre = statement.column_text (DetailColumn.GENRE);
+                music_item.track_number = statement.column_int
+                                        (DetailColumn.TRACK);
+                music_item.lookup_album_art ();
+            }
+        }
+
+        if (item is VisualItem) {
+            var visual_item = item as VisualItem;
+            visual_item.width = statement.column_int (DetailColumn.WIDTH);
+            visual_item.height = statement.column_int (DetailColumn.HEIGHT);
+            visual_item.color_depth = statement.column_int
+                                        (DetailColumn.COLOR_DEPTH);
+            if (item is VideoItem) {
+                var video_item = item as VideoItem;
+                video_item.author = statement.column_text (DetailColumn.AUTHOR);
+            }
+        }
+    }
+
+    private string translate_search_expression
+                                        (SearchExpression? expression,
+                                         ValueArray        args,
+                                         string            prefix = "WHERE")
+                                         throws Error {
+        if (expression == null) {
+            return "";
+        }
+
+        var filter = this.search_expression_to_sql (expression, args);
+
+        return " %s %s".printf (prefix, filter);
+    }
+
+    private string? search_expression_to_sql (SearchExpression? expression,
+                                             GLib.ValueArray   args)
+                                             throws Error {
+        if (expression == null) {
+            return "";
+        }
+
+        if (expression is LogicalExpression) {
+            return this.logical_expression_to_sql
+                                        (expression as LogicalExpression, args);
+        } else {
+            return this.relational_expression_to_sql
+                                        (expression as RelationalExpression,
+                                         args);
+        }
+    }
+
+    private string? logical_expression_to_sql (LogicalExpression? expression,
+                                               GLib.ValueArray    args)
+                                               throws Error {
+        string left_sql_string = search_expression_to_sql (expression.operand1,
+                                                           args);
+        string right_sql_string = search_expression_to_sql (expression.operand2,
+                                                            args);
+        string operator_sql_string = "OR";
+
+        if (expression.op == LogicalOperator.AND) {
+            operator_sql_string = "AND";
+        }
+
+        return "(%s %s %s)".printf (left_sql_string,
+                                    operator_sql_string,
+                                    right_sql_string);
+    }
+
+    private string? map_operand_to_column (string     operand,
+                                           out string? collate = null)
+                                           throws Error {
+        string column = null;
+        bool use_collation = false;
+
+        switch (operand) {
+            case "res":
+                column = "o.uri";
+                break;
+            case "res duration":
+                column = "m.duration";
+                break;
+            case "@refID":
+                column = "NULL";
+                break;
+            case "@id":
+                column = "o.upnp_id";
+                break;
+            case "@parentID":
+                column = "o.parent";
+                break;
+            case "upnp:class":
+                column = "m.class";
+                break;
+            case "dc:title":
+                column = "o.title";
+                use_collation = true;
+                break;
+            case "upnp:artist":
+            case "dc:creator":
+                column = "m.author";
+                use_collation = true;
+                break;
+            case "dc:date":
+                column = "strftime(\"%Y\", m.date)";
+                break;
+            case "upnp:album":
+                column = "m.album";
+                use_collation = true;
+                break;
+            case "upnp:genre":
+            case "dc:genre":
+                // FIXME: Remove dc:genre, upnp:genre is the correct one
+                column = "m.genre";
+                use_collation = true;
+                break;
+            case "upnp:originalTrackNumber":
+                column = "m.track";
+                break;
+            case "rygel:originalVolumeNumber":
+                column = "m.disc";
+                break;
+            default:
+                var message = "Unsupported column %s".printf (operand);
+
+                throw new MediaCacheError.UNSUPPORTED_SEARCH (message);
+        }
+
+        if (use_collation) {
+            collate = "COLLATE CASEFOLD";
+        } else {
+            collate = "";
+        }
+
+        return column;
+    }
+
+    private string? relational_expression_to_sql (RelationalExpression? exp,
+                                                  GLib.ValueArray       args)
+                                                  throws Error {
+        GLib.Value? v = null;
+        string collate = null;
+
+        string column = map_operand_to_column (exp.operand1, out collate);
+        SqlOperator operator;
+
+        switch (exp.op) {
+            case SearchCriteriaOp.EXISTS:
+                string sql_function;
+                if (exp.operand2 == "true") {
+                    sql_function = "%s IS NOT NULL AND %s != ''";
+                } else {
+                    sql_function = "%s IS NULL OR %s = ''";
+                }
+
+                return sql_function.printf (column, column);
+            case SearchCriteriaOp.EQ:
+            case SearchCriteriaOp.NEQ:
+            case SearchCriteriaOp.LESS:
+            case SearchCriteriaOp.LEQ:
+            case SearchCriteriaOp.GREATER:
+            case SearchCriteriaOp.GEQ:
+                v = exp.operand2;
+                operator = new SqlOperator.from_search_criteria_op
+                                        (exp.op, column, collate);
+                break;
+            case SearchCriteriaOp.CONTAINS:
+                operator = new SqlFunction ("contains", column);
+                v = exp.operand2;
+                break;
+            case SearchCriteriaOp.DOES_NOT_CONTAIN:
+                operator = new SqlFunction ("NOT contains", column);
+                v = exp.operand2;
+                break;
+            case SearchCriteriaOp.DERIVED_FROM:
+                operator = new SqlOperator ("LIKE", column);
+                v = "%s%%".printf (exp.operand2);
+                break;
+            default:
+                warning ("Unsupported op %d", exp.op);
+                return null;
+        }
+
+        if (v != null) {
+            args.append (v);
+        }
+
+        return operator.to_string ();
+    }
+
+    private DatabaseCursor exec_cursor (SQLString      id,
+                                        GLib.Value[]?  values = null)
+                                        throws DatabaseError {
+        return this.db.exec_cursor (this.sql.make (id), values);
+    }
+
+    private int query_value (SQLString      id,
+                             GLib.Value[]?  values = null)
+                             throws DatabaseError {
+        return this.db.query_value (this.sql.make (id), values);
+    }
+
+    private string translate_sort_criteria (string sort_criteria) {
+        string? collate;
+        var builder = new StringBuilder("ORDER BY ");
+        var fields = sort_criteria.split (",");
+        foreach (var field in fields) {
+            try {
+                var column = this.map_operand_to_column (field[1:field.length],
+                                                         out collate);
+                if (field != fields[0]) {
+                    builder.append (",");
+                }
+                builder.append_printf ("%s %s %s ",
+                                       column,
+                                       collate,
+                                       field[0] == '-' ? "DESC" : "ASC");
+            } catch (Error error) {
+                warning ("Skipping nsupported field: %s", field);
+            }
+        }
+
+        return builder.str;
+    }
+}
diff --git a/src/media-export/rygel-media-export-metadata-extractor.vala b/src/media-export/rygel-media-export-metadata-extractor.vala
new file mode 100644
index 0000000..8c29212
--- /dev/null
+++ b/src/media-export/rygel-media-export-metadata-extractor.vala
@@ -0,0 +1,155 @@
+/*
+ * Copyright (C) 2008 Zeeshan Ali (Khattak) <zeeshanak gnome org>.
+ * Copyright (C) 2009 Jens Georg <mail jensge org>.
+ *
+ * Author: Zeeshan Ali (Khattak) <zeeshanak gnome org>
+ * Author: Jens Georg <mail jensge org>
+ *
+ * This file is part of Rygel.
+ *
+ * Rygel is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * Rygel 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 Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+
+
+using Gst;
+using Gee;
+using GUPnP;
+
+/**
+ * Metadata extractor based on Gstreamer. Just set the URI of the media on the
+ * uri property, it will extact the metadata for you and emit signal
+ * metadata_available for each key/value pair extracted.
+ */
+public class Rygel.MediaExport.MetadataExtractor: GLib.Object {
+    /* Signals */
+    public signal void extraction_done (File                   file,
+                                        GUPnP.DLNAInformation? dlna,
+                                        FileInfo               file_info);
+
+    /**
+     * Signalize that an error occured during metadata extraction
+     */
+    public signal void error (File file, Error err);
+
+    private GUPnP.DLNADiscoverer discoverer;
+    /**
+     * We export a GLib.File-based API but GstDiscoverer works with URIs, so
+     * we store uri->GLib.File mappings in this hashmap, so that we can get
+     * the GLib.File back from the URI in on_discovered().
+     */
+    private HashMap<string, File> file_hash;
+    private uint64 timeout = 10; /* seconds */
+
+    private bool extract_metadata;
+
+    public MetadataExtractor () {
+        this.file_hash = new HashMap<string, File> ();
+
+        var config = MetaConfig.get_default ();
+        try {
+            this.extract_metadata = config.get_bool ("MediaExport",
+                                                     "extract-metadata");
+        } catch (Error error) {
+            this.extract_metadata = true;
+        }
+
+
+        if (this.extract_metadata) {
+
+        }
+    }
+
+    public void extract (File file) {
+        if (this.extract_metadata) {
+            string uri = file.get_uri ();
+            this.file_hash.set (uri, file);
+            var gst_timeout = (ClockTime) (this.timeout * Gst.SECOND);
+            this.discoverer = new GUPnP.DLNADiscoverer (gst_timeout,
+                                                        true,
+                                                        true);
+            this.discoverer.done.connect (on_done);
+            this.discoverer.start ();
+            this.discoverer.discover_uri (uri);
+        } else {
+            this.extract_basic_information (file);
+        }
+    }
+
+    private void on_done (GUPnP.DLNAInformation dlna,
+                          GLib.Error            err) {
+        this.discoverer.done.disconnect (on_done);
+        this.discoverer = null;
+        var file = this.file_hash.get (dlna.info.get_uri ());
+        if (file == null) {
+            warning ("File %s already handled, ignoring event",
+                     dlna.info.get_uri ());
+
+            return;
+        }
+
+        this.file_hash.unset (dlna.info.get_uri ());
+
+        if ((dlna.info.get_result () & Gst.DiscovererResult.TIMEOUT) != 0) {
+            debug ("Extraction timed out on %s", file.get_uri ());
+
+            // set dlna to null to extract basic file information
+            dlna = null;
+        } else if ((dlna.info.get_result () &
+                    Gst.DiscovererResult.ERROR) != 0) {
+            this.error (file, err);
+
+            return;
+        }
+
+        this.extract_basic_information (file, dlna);
+    }
+
+    private void extract_basic_information (File file,
+                                            DLNAInformation? dlna = null) {
+        try {
+            FileInfo file_info;
+
+            try {
+                file_info = file.query_info
+                                        (FileAttribute.STANDARD_CONTENT_TYPE
+                                         + "," +
+                                         FileAttribute.STANDARD_SIZE + "," +
+                                         FileAttribute.TIME_MODIFIED + "," +
+                                         FileAttribute.STANDARD_DISPLAY_NAME,
+                                         FileQueryInfoFlags.NONE,
+                                         null);
+            } catch (Error error) {
+                warning (_("Failed to query content type for '%s'"),
+                        file.get_uri ());
+
+                // signal error to parent
+                this.error (file, error);
+
+                throw error;
+            }
+
+            this.extraction_done (file,
+                                  dlna,
+                                  file_info);
+        } catch (Error error) {
+            debug ("Failed to extract basic metadata from %s: %s",
+                   file.get_uri (),
+                   error.message);
+            this.error (file, error);
+        }
+
+    }
+
+}
diff --git a/src/rygel-wmv-transcoder.vala b/src/media-export/rygel-media-export-music-item.vala
similarity index 53%
copy from src/rygel-wmv-transcoder.vala
copy to src/media-export/rygel-media-export-music-item.vala
index 947ee04..4d27b9a 100644
--- a/src/rygel-wmv-transcoder.vala
+++ b/src/media-export/rygel-media-export-music-item.vala
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2009 Jens Georg <mail jensge org>.
+ * Copyright (C) 2012 Jens Georg <mail jensge org>.
  *
  * Author: Jens Georg <mail jensge org>
  *
@@ -19,21 +19,25 @@
  * along with this program; if not, write to the Free Software Foundation,
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
  */
-using Gst;
-using GUPnP;
 
-internal class Rygel.WMVTranscoder : Rygel.VideoTranscoder {
-    private const int VIDEO_BITRATE = 1200;
-    private const int AUDIO_BITRATE = 64;
+/**
+ * Own MusicItem class to provide disc number inside music item for sorting
+ * and metadata extraction.
+ */
+internal class Rygel.MediaExport.MusicItem : Rygel.MusicItem,
+                                             Rygel.UpdatableObject {
+    public int disc;
+
+    public MusicItem (string         id,
+                      MediaContainer parent,
+                      string         title,
+                      string         upnp_class = Rygel.MusicItem.UPNP_CLASS) {
+        base (id, parent, title, upnp_class);
+    }
 
-    public WMVTranscoder () {
-        base ("video/x-ms-wmv",
-              "WMVHIGH_FULL",
-              AUDIO_BITRATE,
-              VIDEO_BITRATE,
-              "video/x-ms-asf,parsed=true",
-              "audio/x-wma,channels=2,wmaversion=1",
-              "video/x-wmv,wmvversion=1",
-              "wmv");
+    public async void commit () throws Error {
+        var cache = MediaCache.get_default ();
+        cache.save_item (this);
     }
+
 }
diff --git a/src/media-export/rygel-media-export-node-query-container.vala b/src/media-export/rygel-media-export-node-query-container.vala
new file mode 100644
index 0000000..1d40084
--- /dev/null
+++ b/src/media-export/rygel-media-export-node-query-container.vala
@@ -0,0 +1,91 @@
+/*
+ * Copyright (C) 2011 Jens Georg <mail jensge org>.
+ *
+ * This file is part of Rygel.
+ *
+ * Rygel is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * Rygel 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 Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+
+internal class Rygel.MediaExport.NodeQueryContainer : QueryContainer {
+    private string template;
+    private string attribute;
+
+    public NodeQueryContainer (MediaCache       cache,
+                               SearchExpression expression,
+                               string           id,
+                               string           name,
+                               string           template,
+                               string           attribute) {
+        base (cache, expression, id, name);
+
+        this.template = template;
+        this.attribute = attribute;
+
+        // base constructor does count_children but it depends on template and
+        // attribute; so we have to call it again here after those two have
+        // been set.
+        try {
+            this.child_count = this.count_children ();
+        } catch (Error error) {};
+    }
+
+    // MediaContainer overrides
+
+    public override async MediaObjects? get_children
+                                        (uint         offset,
+                                         uint         max_count,
+                                         string       sort_criteria,
+                                         Cancellable? cancellable)
+                                         throws GLib.Error {
+        var children = new MediaObjects ();
+        var data = this.media_db.get_object_attribute_by_search_expression
+                                        (this.attribute,
+                                         this.expression,
+                                         // sort criteria
+                                         offset,
+                                         max_count);
+
+        foreach (var meta_data in data) {
+            var new_id = Uri.escape_string (meta_data, "", true);
+            // template contains URL escaped text. This means it might
+            // contain '%' chars which will makes sprintf crash
+            new_id = this.template.replace ("%s", new_id);
+            var factory = QueryContainerFactory.get_default ();
+            var container = factory.create_from_description (this.media_db,
+                                                             new_id,
+                                                             meta_data);
+            container.parent = this;
+            children.add (container);
+        }
+
+        return children;
+    }
+
+    // QueryContainer overrides
+
+    protected override int count_children () throws Error {
+        // Happens during construction
+        if (this.attribute == null || this.expression == null) {
+            return 0;
+        }
+
+        var data = this.media_db.get_object_attribute_by_search_expression
+                                        (this.attribute,
+                                         this.expression,
+                                         0,
+                                         -1);
+        return data.size;
+    }
+}
diff --git a/src/media-export/rygel-media-export-null-container.vala b/src/media-export/rygel-media-export-null-container.vala
new file mode 100644
index 0000000..6309fbc
--- /dev/null
+++ b/src/media-export/rygel-media-export-null-container.vala
@@ -0,0 +1,47 @@
+/*
+ * Copyright (C) 2009 Jens Georg <mail jensge org>.
+ *
+ * This file is part of Rygel.
+ *
+ * Rygel is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * Rygel 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 Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+
+using Rygel;
+using Gee;
+
+/**
+ * This is an empty container used to satisfy rygel if no mediadb could be
+ * created
+ */
+internal class Rygel.NullContainer : MediaContainer {
+    public NullContainer () {
+        base.root ("MediaExport", 0);
+    }
+
+    public override async MediaObjects? get_children (
+                                                     uint         offset,
+                                                     uint         max_count,
+                                                     string       sort_criteria,
+                                                     Cancellable? cancellable)
+                                                     throws Error {
+        return new MediaObjects ();
+    }
+
+    public override async MediaObject? find_object (string id,
+                                                    Cancellable? cancellable)
+                                                    throws Error {
+        return null;
+    }
+}
diff --git a/src/media-export/rygel-media-export-object-factory.vala b/src/media-export/rygel-media-export-object-factory.vala
new file mode 100644
index 0000000..5be14bd
--- /dev/null
+++ b/src/media-export/rygel-media-export-object-factory.vala
@@ -0,0 +1,91 @@
+/*
+ * Copyright (C) 2010 Jens Georg <mail jensge org>.
+ *
+ * Author: Jens Georg <mail jensge org>
+ *
+ * This file is part of Rygel.
+ *
+ * Rygel is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * Rygel 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 Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+
+internal class Rygel.MediaExport.ObjectFactory : Object {
+    /**
+     * Return a new instance of DBContainer
+     *
+     * @param media_db instance of MediaDB
+     * @param title title of the container
+     * @param child_count number of children in the container
+     */
+    public virtual DBContainer get_container (MediaCache media_db,
+                                              string     id,
+                                              string     title,
+                                              uint       child_count,
+                                              string?    uri) {
+        if (id == "0") {
+            try {
+                return RootContainer.get_instance () as DBContainer;
+            } catch (Error error) {
+                // Must not fail - plugin is disabled if this fails
+                assert_not_reached ();
+            }
+        } else if (id == RootContainer.FILESYSTEM_FOLDER_ID) {
+            try {
+                var root_container = RootContainer.get_instance ()
+                                        as RootContainer;
+
+                return root_container.get_filesystem_container ()
+                                        as DBContainer;
+            } catch (Error error) { assert_not_reached (); }
+        }
+
+        if (id.has_prefix (QueryContainer.PREFIX)) {
+            var factory = QueryContainerFactory.get_default ();
+            return factory.create_from_id (media_db, id, title);
+        }
+
+        if (uri == null) {
+            return new DBContainer (media_db, id, title);
+        }
+
+        return new WritableDbContainer (media_db, id, title);
+    }
+
+    /**
+     * Return a new instance of MediaItem
+     *
+     * @param media_db instance of MediaDB
+     * @param id id of the item
+     * @param title title of the item
+     * @param upnp_class upnp_class of the item
+     */
+    public virtual MediaItem get_item (MediaCache     media_db,
+                                       MediaContainer parent,
+                                       string         id,
+                                       string         title,
+                                       string         upnp_class) {
+        switch (upnp_class) {
+            case Rygel.MusicItem.UPNP_CLASS:
+            case Rygel.AudioItem.UPNP_CLASS:
+                return new MusicItem (id, parent, title);
+            case Rygel.VideoItem.UPNP_CLASS:
+                return new VideoItem (id, parent, title);
+            case Rygel.PhotoItem.UPNP_CLASS:
+            case Rygel.ImageItem.UPNP_CLASS:
+                return new PhotoItem (id, parent, title);
+            default:
+                assert_not_reached ();
+        }
+    }
+}
diff --git a/src/rygel-aac-transcoder.vala b/src/media-export/rygel-media-export-photo-item.vala
similarity index 56%
rename from src/rygel-aac-transcoder.vala
rename to src/media-export/rygel-media-export-photo-item.vala
index c92df8a..d6e938f 100644
--- a/src/rygel-aac-transcoder.vala
+++ b/src/media-export/rygel-media-export-photo-item.vala
@@ -1,7 +1,7 @@
 /*
- * Copyright (C) 2011 Nokia Corporation.
+ * Copyright (C) 2012 Intel Corporation.
  *
- * Author: Luis de Bethencourt <luis debethencourt collabora com>
+ * Author: Jens Georg <jensg openismus com>
  *
  * This file is part of Rygel.
  *
@@ -20,16 +20,18 @@
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
  */
 
-/**
- * Transcoder for 3GP stream containing MPEG4 audio (AAC).
- */
-internal class Rygel.AACTranscoder : Rygel.AudioTranscoder {
-    private const int BITRATE = 256;
-    private const string CODEC = "audio/mpeg,mpegversion=4," +
-                                 "stream-format=adts,rate=44100,base-profile=lc";
+internal class Rygel.MediaExport.PhotoItem : Rygel.PhotoItem,
+                                             Rygel.UpdatableObject {
+    public PhotoItem (string         id,
+                      MediaContainer parent,
+                      string         title,
+                      string         upnp_class = Rygel.PhotoItem.UPNP_CLASS) {
+        base (id, parent, title, upnp_class);
+    }
 
-    public AACTranscoder () {
-        base ("audio/vnd.dlna.adts", "AAC_ADTS_320", BITRATE, null, CODEC, "adts");
-        this.preset = "Rygel AAC_ADTS_320 preset";
+    public async void commit () throws Error {
+        var cache = MediaCache.get_default ();
+        cache.save_item (this);
     }
+
 }
diff --git a/src/media-export/rygel-media-export-plugin.vala b/src/media-export/rygel-media-export-plugin.vala
new file mode 100644
index 0000000..42df8b3
--- /dev/null
+++ b/src/media-export/rygel-media-export-plugin.vala
@@ -0,0 +1,113 @@
+/*
+ * Copyright (C) 2008-2009 Jens Georg <mail jensge org>.
+ *
+ * This file is part of Rygel.
+ *
+ * Rygel is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * Rygel 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 Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+
+using Rygel;
+using GUPnP;
+
+private const string TRACKER_PLUGIN = "Tracker";
+
+/**
+ * Simple plugin which exposes the media contents of a directory via UPnP.
+ *
+ */
+public void module_init (PluginLoader loader) {
+    if (loader.plugin_disabled (MediaExport.Plugin.NAME)) {
+        message ("Plugin '%s' disabled by user, ignoring..",
+                 MediaExport.Plugin.NAME);
+
+        return;
+    }
+
+    MediaExport.Plugin plugin;
+
+    try {
+        plugin = new MediaExport.Plugin ();
+    } catch (Error error) {
+        warning ("Failed to initialize plugin '%s': %s. Ignoring..",
+                 MediaExport.Plugin.NAME,
+                 error.message);
+
+        return;
+    }
+
+    Idle.add (() => {
+       foreach (var loaded_plugin in loader.list_plugins ()) {
+            on_plugin_available (loaded_plugin, plugin);
+       }
+
+       loader.plugin_available.connect ((new_plugin) => {
+           on_plugin_available (new_plugin, plugin);
+       });
+
+       return false;
+    });
+
+    loader.add_plugin (plugin);
+}
+
+public void on_plugin_available (Plugin plugin, Plugin our_plugin) {
+    if (plugin.name == TRACKER_PLUGIN) {
+        if (our_plugin.active && !plugin.active) {
+            // Tracker plugin might be activated later
+            plugin.notify["active"].connect (() => {
+                if (plugin.active) {
+                    shutdown_media_export ();
+                    our_plugin.active = !plugin.active;
+                }
+            });
+        } else if (our_plugin.active == plugin.active) {
+            if (plugin.active) {
+                shutdown_media_export ();
+            } else {
+                message ("Plugin '%s' inactivate, activating '%s' plugin",
+                         TRACKER_PLUGIN,
+                         MediaExport.Plugin.NAME);
+            }
+            our_plugin.active = !plugin.active;
+        }
+    }
+}
+
+private void shutdown_media_export () {
+    message ("Deactivating plugin '%s' in favor of plugin '%s'",
+             MediaExport.Plugin.NAME,
+             TRACKER_PLUGIN);
+    try {
+        var config = MetaConfig.get_default ();
+        var enabled = config.get_bool ("MediaExport", "enabled");
+        if (enabled) {
+            var root = Rygel.MediaExport.RootContainer.get_instance ()
+                                        as Rygel.MediaExport.RootContainer;
+
+            root.shutdown ();
+        }
+    } catch (Error error) {};
+}
+
+public class Rygel.MediaExport.Plugin : Rygel.MediaServerPlugin {
+    public const string NAME = "MediaExport";
+
+    public Plugin () throws Error {
+        base (RootContainer.get_instance (),
+              NAME,
+              null,
+              PluginCapabilities.UPLOAD);
+    }
+}
diff --git a/src/media-export/rygel-media-export-query-container-factory.vala b/src/media-export/rygel-media-export-query-container-factory.vala
new file mode 100644
index 0000000..c2120a6
--- /dev/null
+++ b/src/media-export/rygel-media-export-query-container-factory.vala
@@ -0,0 +1,268 @@
+/*
+ * Copyright (C) 2011 Jens Georg <mail jensge org>.
+ *
+ * This file is part of Rygel.
+ *
+ * Rygel is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * Rygel 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 Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+using Gee;
+using GUPnP;
+
+internal class Rygel.MediaExport.QueryContainerFactory : Object {
+    // private static members
+    private static QueryContainerFactory instance;
+
+    // private members
+    private HashMap<string,string> virtual_container_map;
+
+    // public static functions
+    public static QueryContainerFactory get_default () {
+        if (unlikely (instance == null)) {
+            instance = new QueryContainerFactory ();
+        }
+
+        return instance;
+    }
+
+    // constructors
+    private QueryContainerFactory () {
+        this.virtual_container_map = new HashMap<string, string> ();
+    }
+
+    // public functions
+
+    /**
+     * Register a plaintext description for a query container. The passed
+     * string will be modified to the checksum id of the container.
+     *
+     * @param id Originally contains the plaintext id which is replaced with
+     *           the hashed id on return.
+     */
+    public void register_id (ref string id) {
+        var md5 = Checksum.compute_for_string (ChecksumType.MD5, id);
+
+        if (!this.virtual_container_map.has_key (md5)) {
+            this.virtual_container_map[md5] = id;
+            debug ("Registering %s for %s", md5, id);
+        }
+
+        id = QueryContainer.PREFIX + md5;
+    }
+
+    /**
+     * Get the plaintext definition from a hashed id.
+     *
+     * Inverse function of register_id().
+     *
+     * @param hash A hashed id
+     * @return the plaintext defintion of the virtual folder
+     */
+    public string? get_virtual_container_definition (string hash) {
+        var id = hash.replace (QueryContainer.PREFIX, "");
+
+        return this.virtual_container_map[id];
+    }
+
+    /**
+     * Factory method.
+     *
+     * Create a QueryContainer directly from MD5 hashed id.
+     *
+     * @param cache An instance of the meta-data cache
+     * @param id    The hashed id of the container
+     * @param name  An the title of the container. If not supplied, it will
+     *              be derived from the plain-text description of the
+     *              container
+     * @return A new instance of QueryContainer or null if id does not exist
+     */
+    public QueryContainer? create_from_id (MediaCache cache,
+                                          string     id,
+                                          string     name = "") {
+        var definition = this.get_virtual_container_definition (id);
+        if (definition == null) {
+            return null;
+        }
+
+        return this.create_from_description (cache, definition, name);
+    }
+
+    /**
+     * Factory method.
+     *
+     * Create a QueryContainer from a plain-text description string.
+     *
+     * @param cache      An instance of the meta-data cache
+     * @param definition Plain-text defintion of the query-container
+     * @param name       The title of the container. If not supplied, it
+     *                   will be derived from the plain-text description of
+     *                   the container
+     * @return A new instance of QueryContainer
+     */
+    public QueryContainer create_from_description (MediaCache cache,
+                                                   string     definition,
+                                                   string     name = "") {
+        var title = name;
+        string attribute = null;
+        string pattern = null;
+        string upnp_class = null;
+        var id = definition;
+        QueryContainer container;
+
+        this.register_id (ref id);
+
+        var expression = this.parse_description (definition,
+                                                 out pattern,
+                                                 out attribute,
+                                                 out upnp_class,
+                                                 ref title);
+
+        if (pattern == null || pattern == "") {
+            container =  new LeafQueryContainer (cache,
+                                                 expression,
+                                                 id,
+                                                 title);
+        } else {
+            container = new NodeQueryContainer (cache,
+                                                expression,
+                                                id,
+                                                title,
+                                                pattern,
+                                                attribute);
+        }
+
+        if (upnp_class != null) {
+            container.upnp_class = upnp_class;
+            if (upnp_class == MediaContainer.MUSIC_ALBUM) {
+                container.sort_criteria = MediaContainer.ALBUM_SORT_CRITERIA;
+            }
+        }
+
+        return container;
+    }
+
+    // private methods
+
+    /**
+     * Map a DIDL attribute to a UPnP container class.
+     *
+     * @return A matching UPnP class for the attribute or null if it can't be
+     *         mapped.
+     */
+    private string? map_upnp_class (string attribute) {
+        switch (attribute) {
+            case "upnp:album":
+                return MediaContainer.MUSIC_ALBUM;
+            case "dc:creator":
+            case "upnp:artist":
+                return MediaContainer.MUSIC_ARTIST;
+            case "dc:genre":
+                return MediaContainer.MUSIC_GENRE;
+            default:
+                return null;
+        }
+    }
+
+    /**
+     * Parse a plaintext container description into a search expression.
+     *
+     * Also generates a name for the container and other meta-data necessary
+     * for node containers.
+     *
+     * @param description The plaintext container description
+     * @param pattern     Contains the pattern used for child containers if
+     *                    descrption is for a node container, null otherwise.
+     * @param attribute   Contains the UPnP attribute the container describes.
+     * @param name        If passed empty, name will be generated from the
+     *                    description.
+     * @return A SearchExpression corresponding to the non-variable part of
+     *         the description.
+     */
+    private SearchExpression parse_description (string     description,
+                                                out string pattern,
+                                                out string attribute,
+                                                out string upnp_class,
+                                                ref string name) {
+        var args = description.split (",");
+        var expression = null as SearchExpression;
+        pattern = null;
+        attribute = null;
+        upnp_class = null;
+
+        int i = 0;
+        while (i < args.length) {
+            string previous_attribute = attribute;
+
+            attribute = args[i].replace (QueryContainer.PREFIX, "");
+            attribute = Uri.unescape_string (attribute);
+
+            if (args[i + 1] != "?") {
+                this.update_search_expression (ref expression,
+                                               args[i],
+                                               args[i + 1]);
+
+                // We're on the end of the list, map UPnP class
+                if (i + 2 == args.length) {
+                    upnp_class = this.map_upnp_class (attribute);
+                }
+            } else {
+                args[i + 1] = "%s";
+                pattern = string.joinv (",", args);
+
+                // This container has the previouss attribute's content, so
+                // use that to map the UPnP class.
+                upnp_class = this.map_upnp_class (previous_attribute);
+
+                if (name == "" && i > 0) {
+                    name = Uri.unescape_string (args[i - 1]);
+                }
+
+                break;
+            }
+
+            i += 2;
+        }
+
+        return expression;
+    }
+
+    /**
+     * Update a SearchExpression with a new key = value condition.
+     *
+     * Will modifiy the passed expression to (expression AND (key = value))
+     *
+     * @param expression The expression to update or null to create a new one
+     * @param key        Key of the key/value condition
+     * @param value      Value of the key/value condition
+     */
+    private void update_search_expression (ref SearchExpression? expression,
+                                           string                key,
+                                           string                @value) {
+        var subexpression = new RelationalExpression ();
+        var clean_key = key.replace (QueryContainer.PREFIX, "");
+        subexpression.operand1 = Uri.unescape_string (clean_key);
+        subexpression.op = SearchCriteriaOp.EQ;
+        subexpression.operand2 = Uri.unescape_string (@value);
+
+        if (expression != null) {
+            var conjunction = new LogicalExpression ();
+            conjunction.operand1 = expression;
+            conjunction.operand2 = subexpression;
+            conjunction.op = LogicalOperator.AND;
+            expression = conjunction;
+        } else {
+            expression = subexpression;
+        }
+    }
+}
diff --git a/src/media-export/rygel-media-export-query-container.vala b/src/media-export/rygel-media-export-query-container.vala
new file mode 100644
index 0000000..99f77cd
--- /dev/null
+++ b/src/media-export/rygel-media-export-query-container.vala
@@ -0,0 +1,90 @@
+/*
+ * Copyright (C) 2009,2010 Jens Georg <mail jensge org>.
+ *
+ * This file is part of Rygel.
+ *
+ * Rygel is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * Rygel 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 Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+
+using Gee;
+using GUPnP;
+
+internal abstract class Rygel.MediaExport.QueryContainer : DBContainer {
+    // public static members
+    public static const string PREFIX = "virtual-container:";
+
+    // protected members
+    protected SearchExpression expression;
+
+    // constructors
+    public QueryContainer (MediaCache       cache,
+                           SearchExpression expression,
+                           string           id,
+                           string           name) {
+        base (cache, id, name);
+
+        this.expression = expression;
+
+        try {
+            this.child_count = this.count_children ();
+        } catch (Error error) {
+            this.child_count = 0;
+        }
+    }
+
+    // public methods
+    public async override MediaObjects? search (SearchExpression? expression,
+                                                uint              offset,
+                                                uint              max_count,
+                                                out uint          total_matches,
+                                                string            sort_criteria,
+                                                Cancellable?      cancellable)
+                                                throws GLib.Error {
+        MediaObjects children = null;
+
+        SearchExpression combined_expression;
+
+        if (expression == null) {
+            combined_expression = this.expression;
+        } else {
+            var local_expression = new LogicalExpression ();
+            local_expression.operand1 = this.expression;
+            local_expression.op = LogicalOperator.AND;
+            local_expression.operand2 = expression;
+            combined_expression = local_expression;
+        }
+
+        try {
+            children = this.media_db.get_objects_by_search_expression
+                                        (combined_expression,
+                                         null,
+                                         sort_criteria,
+                                         offset,
+                                         max_count,
+                                         out total_matches);
+        } catch (MediaCacheError error) {
+            if (error is MediaCacheError.UNSUPPORTED_SEARCH) {
+                children = new MediaObjects ();
+                total_matches = 0;
+            } else {
+                throw error;
+            }
+        }
+
+        return children;
+    }
+
+    protected abstract int count_children () throws Error;
+}
diff --git a/src/media-export/rygel-media-export-recursive-file-monitor.vala b/src/media-export/rygel-media-export-recursive-file-monitor.vala
new file mode 100644
index 0000000..75035ab
--- /dev/null
+++ b/src/media-export/rygel-media-export-recursive-file-monitor.vala
@@ -0,0 +1,113 @@
+/*
+ * Copyright (C) 2009 Jens Georg <mail jensge org>.
+ *
+ * This file is part of Rygel.
+ *
+ * Rygel is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * Rygel 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 Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+
+
+using Gee;
+
+public class Rygel.MediaExport.RecursiveFileMonitor : Object {
+    private Cancellable        cancellable;
+    HashMap<File, FileMonitor> monitors;
+    bool                       monitor_changes;
+
+    public RecursiveFileMonitor (Cancellable? cancellable) {
+        this.monitor_changes = true;
+        try {
+            var config = MetaConfig.get_default ();
+            this.monitor_changes = config.get_bool ("MediaExport",
+                                                    "monitor-changes");
+        } catch (Error error) {
+            this.monitor_changes = true;
+        }
+
+        if (!this.monitor_changes) {
+            message (_("Will not monitor file changes"));
+        }
+
+        this.cancellable = cancellable;
+        this.monitors = new HashMap<File, FileMonitor> ((HashDataFunc<File>) File.hash,
+                                                        (EqualDataFunc<File>) File.equal);
+        if (cancellable != null) {
+            cancellable.cancelled.connect (this.cancel);
+        }
+    }
+
+    public void on_monitor_changed (File             file,
+                                    File?            other_file,
+                                    FileMonitorEvent event_type) {
+        this.changed (file, other_file, event_type);
+
+        switch (event_type) {
+            case FileMonitorEvent.CREATED:
+                this.add.begin (file);
+
+                break;
+            case FileMonitorEvent.DELETED:
+                var file_monitor = this.monitors.get (file);
+                if (file_monitor != null) {
+                    debug ("Folder %s gone; removing watch",
+                           file.get_uri ());
+                    this.monitors.unset (file);
+                    file_monitor.cancel ();
+                    file_monitor.changed.disconnect (this.on_monitor_changed);
+                }
+
+                break;
+            default:
+                // do nothing
+                break;
+        }
+    }
+
+    public async void add (File file) {
+        if (!this.monitor_changes ||
+             this.monitors.has_key (file)) {
+            return;
+        }
+
+        try {
+            var info = yield file.query_info_async
+                                        (FileAttribute.STANDARD_TYPE,
+                                         FileQueryInfoFlags.NONE,
+                                         Priority.DEFAULT,
+                                         null);
+            if (info.get_file_type () == FileType.DIRECTORY) {
+                var file_monitor = file.monitor_directory
+                                        (FileMonitorFlags.NONE,
+                                         this.cancellable);
+                this.monitors.set (file, file_monitor);
+                file_monitor.changed.connect (this.on_monitor_changed);
+            }
+        } catch (Error err) {
+            warning (_("Failed to get file info for %s"), file.get_uri ());
+        }
+    }
+
+    public void cancel () {
+        foreach (var monitor in this.monitors.values) {
+            monitor.cancel ();
+        }
+
+        this.monitors.clear ();
+    }
+
+    public signal void changed (File             file,
+                                File?            other_file,
+                                FileMonitorEvent event_type);
+}
diff --git a/src/media-export/rygel-media-export-root-container.vala b/src/media-export/rygel-media-export-root-container.vala
new file mode 100644
index 0000000..5fdd310
--- /dev/null
+++ b/src/media-export/rygel-media-export-root-container.vala
@@ -0,0 +1,484 @@
+/*
+ * Copyright (C) 2009,2010 Jens Georg <mail jensge org>.
+ *
+ * This file is part of Rygel.
+ *
+ * Rygel is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * Rygel 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 Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+
+using Gee;
+using GUPnP;
+
+internal struct Rygel.MediaExport.FolderDefinition {
+    string title;
+    string definition;
+}
+
+const Rygel.MediaExport.FolderDefinition[] VIRTUAL_FOLDERS_DEFAULT = {
+    { N_("Year"), "dc:date,?" },
+    { N_("All"),  "" }
+};
+
+const Rygel.MediaExport.FolderDefinition[] VIRTUAL_FOLDERS_MUSIC = {
+    { N_("Artist"), "upnp:artist,?,upnp:album,?" },
+    { N_("Album"),  "upnp:album,?" },
+    { N_("Genre"),  "dc:genre,?" }
+};
+
+/**
+ * Represents the root container.
+ */
+public class Rygel.MediaExport.RootContainer : Rygel.MediaExport.DBContainer {
+    private DBusService    service;
+    private Harvester      harvester;
+    private Cancellable    cancellable;
+    private MediaContainer filesystem_container;
+    private ulong          harvester_signal_id;
+
+    private static MediaContainer instance = null;
+    private static Error          creation_error = null;
+
+    internal const string FILESYSTEM_FOLDER_NAME = N_("Files & Folders");
+    internal const string FILESYSTEM_FOLDER_ID   = "Filesystem";
+
+    private const string SEARCH_CONTAINER_PREFIX = QueryContainer.PREFIX +
+                                                   "upnp:class," +
+                                                   Rygel.MusicItem.UPNP_CLASS +
+                                                   ",";
+
+    public static MediaContainer get_instance () throws Error {
+        if (RootContainer.instance == null) {
+            try {
+                RootContainer.instance = new RootContainer ();
+            } catch (Error error) {
+                // cache error for further calls and create Null container
+                RootContainer.instance = new NullContainer ();
+                RootContainer.creation_error = error;
+            }
+        } else {
+            if (creation_error != null) {
+                throw creation_error;
+            }
+        }
+
+        return RootContainer.instance;
+    }
+
+    public MediaContainer get_filesystem_container () {
+        return this.filesystem_container;
+    }
+
+    public void shutdown () {
+        this.cancellable.cancel ();
+    }
+
+    // DBus utility methods
+
+    public void add_uri (string uri) {
+        var file = File.new_for_commandline_arg (uri);
+        this.harvester.schedule (file,
+                                 this.filesystem_container,
+                                 "DBUS");
+    }
+
+    public void remove_uri (string uri) {
+        var file = File.new_for_commandline_arg (uri);
+        var id = MediaCache.get_id (file);
+
+        this.harvester.cancel (file);
+        try {
+            this.media_db.remove_by_id (id);
+        } catch (Error error) {
+            warning (_("Failed to remove URI: %s"), error.message);
+        }
+    }
+
+    public string[] get_dynamic_uris () {
+        try {
+            var uris = this.media_db.get_flagged_uris ("DBUS");
+
+            return uris.to_array ();
+        } catch (Error error) { }
+
+        return new string[0];
+    }
+
+    // MediaContainer overrides
+
+    public override async MediaObject? find_object (string       id,
+                                                    Cancellable? cancellable)
+                                                    throws Error {
+        var object = yield base.find_object (id, cancellable);
+
+        if (object == null && id.has_prefix (QueryContainer.PREFIX)) {
+            var factory = QueryContainerFactory.get_default ();
+            var container = factory.create_from_id (this.media_db, id);
+            if (container != null) {
+                container.parent = this;
+            }
+
+            return container;
+        }
+
+        return object;
+    }
+
+    public override async MediaObjects? search (SearchExpression? expression,
+                                                uint              offset,
+                                                uint              max_count,
+                                                out uint          total_matches,
+                                                string            sort_criteria,
+                                                Cancellable?      cancellable)
+                                                throws GLib.Error {
+         if (expression == null) {
+            return yield base.search (expression,
+                                      offset,
+                                      max_count,
+                                      out total_matches,
+                                      sort_criteria,
+                                      cancellable);
+        }
+
+        MediaObjects list;
+        MediaContainer query_container = null;
+        string upnp_class = null;
+
+        if (expression is RelationalExpression) {
+            var relational_expression = expression as RelationalExpression;
+
+            query_container = search_to_virtual_container
+                                        (relational_expression);
+            upnp_class = relational_expression.operand2;
+        } else if (is_search_in_virtual_container (expression,
+                                                   out query_container)) {
+            // do nothing. query_container is filled then
+        }
+
+        if (query_container != null) {
+            list = yield query_container.get_children (offset,
+                                                       max_count,
+                                                       sort_criteria,
+                                                       cancellable);
+            total_matches = query_container.child_count;
+
+            if (upnp_class != null) {
+                foreach (var object in list) {
+                    object.upnp_class = upnp_class;
+                }
+            }
+
+            return list;
+        } else {
+            return yield base.search (expression,
+                                      offset,
+                                      max_count,
+                                      out total_matches,
+                                      sort_criteria,
+                                      cancellable);
+        }
+    }
+
+
+
+    private ArrayList<File> get_shared_uris () {
+        ArrayList<string> uris;
+        ArrayList<File> actual_uris;
+
+        var config = MetaConfig.get_default ();
+
+        try {
+            uris = config.get_string_list ("MediaExport", "uris");
+        } catch (Error error) {
+            uris = new ArrayList<string> ();
+        }
+
+        try {
+            uris.add_all (this.media_db.get_flagged_uris ("DBUS"));
+        } catch (Error error) {}
+
+        actual_uris = new ArrayList<File> ();
+
+        var home_dir = File.new_for_path (Environment.get_home_dir ());
+        unowned string pictures_dir = Environment.get_user_special_dir
+                                        (UserDirectory.PICTURES);
+        unowned string videos_dir = Environment.get_user_special_dir
+                                        (UserDirectory.VIDEOS);
+        unowned string music_dir = Environment.get_user_special_dir
+                                        (UserDirectory.MUSIC);
+
+        foreach (var uri in uris) {
+            var file = File.new_for_commandline_arg (uri);
+            if (likely (file != home_dir)) {
+                var actual_uri = uri;
+
+                if (likely (pictures_dir != null)) {
+                    actual_uri = actual_uri.replace ("@PICTURES@", pictures_dir);
+                }
+                if (likely (videos_dir != null)) {
+                    actual_uri = actual_uri.replace ("@VIDEOS@", videos_dir);
+                }
+                if (likely (music_dir != null)) {
+                    actual_uri = actual_uri.replace ("@MUSIC@", music_dir);
+                }
+
+                // protect against special directories expanding to $HOME
+                file = File.new_for_commandline_arg (actual_uri);
+                if (file == home_dir) {
+                    continue;
+                }
+            }
+
+            actual_uris.add (file);
+        }
+
+        return actual_uris;
+    }
+
+    private QueryContainer? search_to_virtual_container (
+                                       RelationalExpression expression) {
+        if (expression.operand1 == "upnp:class" &&
+            expression.op == SearchCriteriaOp.EQ) {
+            string id = SEARCH_CONTAINER_PREFIX;
+            switch (expression.operand2) {
+                case "object.container.album.musicAlbum":
+                    id += "upnp:album,?";
+
+                    break;
+                case "object.container.person.musicArtist":
+                    id += "dc:creator,?,upnp:album,?";
+
+                    break;
+                case "object.container.genre.musicGenre":
+                    id += "dc:genre,?";
+
+                    break;
+                default:
+                    return null;
+            }
+
+            var factory = QueryContainerFactory.get_default ();
+
+            return factory.create_from_description (this.media_db, id);
+        }
+
+        return null;
+    }
+
+    /**
+     * Check if a passed search expression is a simple search in a virtual
+     * container.
+     *
+     * @param expression the expression to check
+     * @param new_id contains the id of the virtual container constructed from
+     *               the search
+     * @param upnp_class contains the class of the container the search was
+     *                   looking in
+     * @return true if it was a search in virtual container, false otherwise.
+     * @note This works single level only. Enough to satisfy Xbox music
+     *       browsing, but may need refinement
+     */
+    private bool is_search_in_virtual_container (SearchExpression   expression,
+                                                 out MediaContainer container) {
+        RelationalExpression virtual_expression = null;
+        QueryContainer query_container;
+
+        container = null;
+
+        if (!(expression is LogicalExpression)) {
+            return false;
+        }
+
+        var logical_expression = expression as LogicalExpression;
+
+        if (!(logical_expression.operand1 is RelationalExpression &&
+            logical_expression.operand2 is RelationalExpression &&
+            logical_expression.op == LogicalOperator.AND)) {
+
+            return false;
+        }
+
+        var left_expression = logical_expression.operand1 as RelationalExpression;
+        var right_expression = logical_expression.operand2 as RelationalExpression;
+
+        query_container = search_to_virtual_container (left_expression);
+        if (query_container == null) {
+            query_container = search_to_virtual_container (right_expression);
+            if (query_container != null) {
+                virtual_expression = left_expression;
+            } else {
+                return false;
+            }
+        } else {
+            virtual_expression = right_expression;
+        }
+
+        var factory = QueryContainerFactory.get_default ();
+        var plaintext_id = factory.get_virtual_container_definition
+                                        (query_container.id);
+
+        var last_argument = plaintext_id.replace (QueryContainer.PREFIX, "");
+
+        var escaped_detail = Uri.escape_string (virtual_expression.operand2,
+                                                "",
+                                                true);
+        var new_id = "%s%s,%s,%s".printf (QueryContainer.PREFIX,
+                                          virtual_expression.operand1,
+                                          escaped_detail,
+                                          last_argument);
+
+        container = factory.create_from_description (this.media_db,
+                                                     new_id);
+
+        return true;
+    }
+
+
+    /**
+     * Create a new root container.
+     */
+    private RootContainer () throws Error {
+        var db = MediaCache.get_default ();
+
+        base (db, "0", _("@REALNAME@'s media"));
+
+        this.cancellable = new Cancellable ();
+
+        try {
+            this.service = new DBusService (this);
+        } catch (Error err) {
+            warning (_("Failed to create MediaExport D-Bus service: %s"),
+                     err.message);
+        }
+
+        try {
+            this.media_db.save_container (this);
+        } catch (Error error) { } // do nothing
+
+        try {
+            this.filesystem_container = new DBContainer
+                                        (media_db,
+                                         FILESYSTEM_FOLDER_ID,
+                                         _(FILESYSTEM_FOLDER_NAME));
+            this.filesystem_container.parent = this;
+            this.media_db.save_container (this.filesystem_container);
+        } catch (Error error) { }
+
+        ArrayList<string> ids;
+        try {
+            ids = media_db.get_child_ids (FILESYSTEM_FOLDER_ID);
+        } catch (DatabaseError e) {
+            ids = new ArrayList<string> ();
+        }
+
+        this.harvester = new Harvester (this.cancellable,
+                                        this.get_shared_uris ());
+        this.harvester_signal_id = this.harvester.done.connect
+                                        (on_initial_harvesting_done);
+
+        foreach (var file in this.harvester.locations) {
+            ids.remove (MediaCache.get_id (file));
+            this.harvester.schedule (file,
+                                     this.filesystem_container);
+        }
+
+        foreach (var id in ids) {
+            debug ("ID %s no longer in config; deleting...", id);
+            try {
+                this.media_db.remove_by_id (id);
+            } catch (DatabaseError error) {
+                warning (_("Failed to remove entry: %s"), error.message);
+            }
+        }
+
+        this.updated ();
+    }
+
+    private void on_initial_harvesting_done () {
+        this.harvester.disconnect (this.harvester_signal_id);
+        this.media_db.debug_statistics ();
+        this.add_default_virtual_folders ();
+        this.updated ();
+
+        this.filesystem_container.container_updated.connect( () => {
+            this.add_default_virtual_folders ();
+            this.updated ();
+        });
+    }
+
+    private void add_default_virtual_folders () {
+        try {
+            this.add_virtual_containers_for_class (_("Music"),
+                                                   Rygel.MusicItem.UPNP_CLASS,
+                                                   VIRTUAL_FOLDERS_MUSIC);
+            this.add_virtual_containers_for_class (_("Pictures"),
+                                                   Rygel.PhotoItem.UPNP_CLASS);
+            this.add_virtual_containers_for_class (_("Videos"),
+                                                   Rygel.VideoItem.UPNP_CLASS);
+        } catch (Error error) {};
+    }
+
+    private void add_folder_definition (MediaContainer   container,
+                                        string           item_class,
+                                        FolderDefinition definition)
+                                        throws Error {
+        var id = "%supnp:class,%s,%s".printf (QueryContainer.PREFIX,
+                                               item_class,
+                                               definition.definition);
+        if (id.has_suffix (",")) {
+            id = id.slice (0,-1);
+        }
+
+        var factory = QueryContainerFactory.get_default ();
+        var query_container = factory.create_from_description
+                                        (this.media_db,
+                                         id,
+                                         _(definition.title));
+
+        if (query_container.child_count > 0) {
+            query_container.parent = container;
+            this.media_db.save_container (query_container);
+        } else {
+            this.media_db.remove_by_id (id);
+        }
+    }
+
+    private void add_virtual_containers_for_class
+                                        (string              parent,
+                                         string              item_class,
+                                         FolderDefinition[]? definitions = null)
+                                         throws Error {
+        var container = new NullContainer ();
+        container.parent = this;
+        container.title = parent;
+        container.id = "virtual-parent:" + item_class;
+        this.media_db.save_container (container);
+
+        foreach (var definition in VIRTUAL_FOLDERS_DEFAULT) {
+            this.add_folder_definition (container, item_class, definition);
+        }
+
+        if (definitions != null) {
+            foreach (var definition in definitions) {
+                this.add_folder_definition (container, item_class, definition);
+            }
+        }
+
+        if (this.media_db.get_child_count (container.id) == 0) {
+            this.media_db.remove_by_id (container.id);
+        } else {
+            container.updated ();
+        }
+    }
+}
diff --git a/src/media-export/rygel-media-export-sql-factory.vala b/src/media-export/rygel-media-export-sql-factory.vala
new file mode 100644
index 0000000..a7a7b08
--- /dev/null
+++ b/src/media-export/rygel-media-export-sql-factory.vala
@@ -0,0 +1,305 @@
+/*
+ * Copyright (C) 2010,2011 Jens Georg <mail jensge org>.
+ *
+ * Author: Jens Georg <mail jensge org>
+ *
+ * This file is part of Rygel.
+ *
+ * Rygel is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * Rygel 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 Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+
+internal enum Rygel.MediaExport.DetailColumn {
+    TYPE,
+    TITLE,
+    SIZE,
+    MIME_TYPE,
+    WIDTH,
+    HEIGHT,
+    CLASS,
+    AUTHOR,
+    ALBUM,
+    DATE,
+    BITRATE,
+    SAMPLE_FREQ,
+    BITS_PER_SAMPLE,
+    CHANNELS,
+    TRACK,
+    COLOR_DEPTH,
+    DURATION,
+    ID,
+    PARENT,
+    TIMESTAMP,
+    URI,
+    DLNA_PROFILE,
+    GENRE,
+    DISC
+}
+
+internal enum Rygel.MediaExport.SQLString {
+    SAVE_METADATA,
+    INSERT,
+    DELETE,
+    GET_OBJECT,
+    GET_CHILDREN,
+    GET_OBJECTS_BY_FILTER,
+    GET_OBJECTS_BY_FILTER_WITH_ANCESTOR,
+    GET_OBJECT_COUNT_BY_FILTER,
+    GET_OBJECT_COUNT_BY_FILTER_WITH_ANCESTOR,
+    GET_META_DATA_COLUMN,
+    CHILD_COUNT,
+    EXISTS,
+    CHILD_IDS,
+    TABLE_METADATA,
+    TABLE_CLOSURE,
+    TRIGGER_CLOSURE,
+    TRIGGER_COMMON,
+    INDEX_COMMON,
+    SCHEMA,
+    EXISTS_CACHE,
+    STATISTICS,
+}
+
+internal class Rygel.MediaExport.SQLFactory : Object {
+    private const string SAVE_META_DATA_STRING =
+    "INSERT OR REPLACE INTO meta_data " +
+        "(size, mime_type, width, height, class, " +
+         "author, album, date, bitrate, " +
+         "sample_freq, bits_per_sample, channels, " +
+         "track, color_depth, duration, object_fk, dlna_profile, genre, disc) VALUES " +
+         "(?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)";
+
+    private const string INSERT_OBJECT_STRING =
+    "INSERT OR REPLACE INTO Object (upnp_id, title, type_fk, parent, timestamp, uri) " +
+        "VALUES (?,?,?,?,?,?)";
+
+    private const string DELETE_BY_ID_STRING =
+    "DELETE FROM Object WHERE upnp_id IN " +
+        "(SELECT descendant FROM closure WHERE ancestor = ?)";
+
+    private const string ALL_DETAILS_STRING =
+    "o.type_fk, o.title, m.size, m.mime_type, m.width, " +
+    "m.height, m.class, m.author, m.album, m.date, m.bitrate, " +
+    "m.sample_freq, m.bits_per_sample, m.channels, m.track, " +
+    "m.color_depth, m.duration, o.upnp_id, o.parent, o.timestamp, " +
+    "o.uri, m.dlna_profile, m.genre, m.disc ";
+
+    private const string GET_OBJECT_WITH_PATH =
+    "SELECT DISTINCT " + ALL_DETAILS_STRING +
+    "FROM Object o " +
+        "JOIN Closure c ON (o.upnp_id = c.ancestor) " +
+        "LEFT OUTER JOIN meta_data m ON (o.upnp_id = m.object_fk) " +
+            "WHERE c.descendant = ? ORDER BY c.depth DESC";
+
+    /**
+     * This is the database query used to retrieve the children for a
+     * given object.
+     *
+     * Sorting is as follows:
+     *   - by type: containers first, then items if both are present
+     *   - by upnp_class: items are sorted according to their class
+     *   - by track: sorted by track
+     *   - and after that alphabetically
+     */
+    private const string GET_CHILDREN_STRING =
+    "SELECT " + ALL_DETAILS_STRING +
+    "FROM Object o " +
+        "JOIN Closure c ON (o.upnp_id = c.descendant) " +
+        "LEFT OUTER JOIN meta_data m " +
+        "ON c.descendant = m.object_fk " +
+    "WHERE c.ancestor = ? AND c.depth = 1 %s" +
+    "LIMIT ?,?";
+
+    private const string GET_OBJECTS_BY_FILTER_STRING_WITH_ANCESTOR =
+    "SELECT DISTINCT " + ALL_DETAILS_STRING +
+    "FROM Object o " +
+        "JOIN Closure c ON o.upnp_id = c.descendant AND c.ancestor = ? " +
+        "LEFT OUTER JOIN meta_data m " +
+            "ON o.upnp_id = m.object_fk %s %s " +
+    "LIMIT ?,?";
+
+    private const string GET_OBJECTS_BY_FILTER_STRING =
+    "SELECT DISTINCT " + ALL_DETAILS_STRING +
+    "FROM Object o " +
+        "LEFT OUTER JOIN meta_data m " +
+            "ON o.upnp_id = m.object_fk %s %s " +
+    "LIMIT ?,?";
+
+    private const string GET_OBJECT_COUNT_BY_FILTER_STRING_WITH_ANCESTOR =
+    "SELECT COUNT(o.type_fk) FROM Object o " +
+        "JOIN Closure c ON o.upnp_id = c.descendant AND c.ancestor = ? " +
+        "LEFT OUTER JOIN meta_data m " +
+            "ON o.upnp_id = m.object_fk %s";
+
+    private const string GET_OBJECT_COUNT_BY_FILTER_STRING =
+    "SELECT COUNT(1) FROM meta_data m %s";
+
+    private const string CHILDREN_COUNT_STRING =
+    "SELECT COUNT(upnp_id) FROM Object WHERE Object.parent = ?";
+
+    private const string OBJECT_EXISTS_STRING =
+    "SELECT COUNT(1), timestamp, m.size FROM Object " +
+        "JOIN meta_data m ON m.object_fk = upnp_id " +
+        "WHERE Object.uri = ?";
+
+    private const string GET_CHILD_ID_STRING =
+    "SELECT upnp_id FROM OBJECT WHERE parent = ?";
+
+    private const string GET_META_DATA_COLUMN_STRING =
+    "SELECT DISTINCT %s AS _column FROM meta_data AS m " +
+        "WHERE _column IS NOT NULL %s ORDER BY _column COLLATE CASEFOLD " +
+    "LIMIT ?,?";
+
+    internal const string SCHEMA_VERSION = "11";
+    internal const string CREATE_META_DATA_TABLE_STRING =
+    "CREATE TABLE meta_data (size INTEGER NOT NULL, " +
+                            "mime_type TEXT NOT NULL, " +
+                            "dlna_profile TEXT, " +
+                            "duration INTEGER, " +
+                            "width INTEGER, " +
+                            "height INTEGER, " +
+                            "class TEXT NOT NULL, " +
+                            "author TEXT, " +
+                            "album TEXT, " +
+                            "genre TEXT, " +
+                            "date TEXT, " +
+                            "bitrate INTEGER, " +
+                            "sample_freq INTEGER, " +
+                            "bits_per_sample INTEGER, " +
+                            "channels INTEGER, " +
+                            "track INTEGER, " +
+                            "disc INTEGER, " +
+                            "color_depth INTEGER, " +
+                            "object_fk TEXT UNIQUE CONSTRAINT " +
+                                "object_fk_id REFERENCES Object(upnp_id) " +
+                                    "ON DELETE CASCADE);";
+
+    private const string SCHEMA_STRING =
+    "CREATE TABLE schema_info (version TEXT NOT NULL); " +
+    CREATE_META_DATA_TABLE_STRING +
+    "CREATE TABLE object (parent TEXT CONSTRAINT parent_fk_id " +
+                                "REFERENCES Object(upnp_id), " +
+                          "upnp_id TEXT PRIMARY KEY, " +
+                          "type_fk INTEGER, " +
+                          "title TEXT NOT NULL, " +
+                          "timestamp INTEGER NOT NULL, " +
+                          "uri TEXT, " +
+                          "flags TEXT);" +
+    "INSERT INTO schema_info (version) VALUES ('" +
+    SQLFactory.SCHEMA_VERSION + "'); ";
+
+    private const string CREATE_CLOSURE_TABLE =
+    "CREATE TABLE closure (ancestor TEXT, descendant TEXT, depth INTEGER)";
+
+    private const string CREATE_CLOSURE_TRIGGER_STRING =
+    "CREATE TRIGGER trgr_update_closure " +
+    "AFTER INSERT ON Object " +
+    "FOR EACH ROW BEGIN " +
+        "SELECT RAISE(IGNORE) WHERE (SELECT COUNT(*) FROM Closure " +
+            "WHERE ancestor = NEW.upnp_id " +
+                  "AND descendant = NEW.upnp_id " +
+                  "AND depth = 0) != 0;" +
+        "INSERT INTO Closure (ancestor, descendant, depth) " +
+            "VALUES (NEW.upnp_id, NEW.upnp_id, 0); " +
+        "INSERT INTO Closure (ancestor, descendant, depth) " +
+            "SELECT ancestor, NEW.upnp_id, depth + 1 FROM Closure " +
+                "WHERE descendant = NEW.parent;" +
+    "END;" +
+
+    "CREATE TRIGGER trgr_delete_closure " +
+    "AFTER DELETE ON Object " +
+    "FOR EACH ROW BEGIN " +
+        "DELETE FROM Closure WHERE descendant = OLD.upnp_id;" +
+    "END;";
+
+    // these triggers emulate ON DELETE CASCADE
+    private const string CREATE_TRIGGER_STRING =
+    "CREATE TRIGGER trgr_delete_metadata " +
+    "BEFORE DELETE ON Object " +
+    "FOR EACH ROW BEGIN " +
+        "DELETE FROM meta_data WHERE meta_data.object_fk = OLD.upnp_id; "+
+    "END;";
+
+    private const string CREATE_INDICES_STRING =
+    "CREATE INDEX IF NOT EXISTS idx_parent on Object(parent);" +
+    "CREATE INDEX IF NOT EXISTS idx_object_upnp_id on Object(upnp_id);" +
+    "CREATE INDEX IF NOT EXISTS idx_meta_data_fk on meta_data(object_fk);" +
+    "CREATE INDEX IF NOT EXISTS idx_closure on Closure(descendant,depth);" +
+    "CREATE INDEX IF NOT EXISTS idx_closure_descendant on Closure(descendant);" +
+    "CREATE INDEX IF NOT EXISTS idx_closure_ancestor on Closure(ancestor);" +
+    "CREATE INDEX IF NOT EXISTS idx_uri on Object(uri);" +
+    "CREATE INDEX IF NOT EXISTS idx_meta_data_date on meta_data(date);" +
+    "CREATE INDEX IF NOT EXISTS idx_meta_data_genre on meta_data(genre);" +
+    "CREATE INDEX IF NOT EXISTS idx_meta_data_album on meta_data(album);" +
+    "CREATE INDEX IF NOT EXISTS idx_meta_data_artist_album on " +
+                                "meta_data(author, album);";
+
+
+
+    private const string EXISTS_CACHE_STRING =
+    "SELECT m.size, o.timestamp, o.uri FROM Object o " +
+        "JOIN meta_data m ON o.upnp_id = m.object_fk";
+
+    private const string STATISTICS_STRING =
+    "SELECT class, count(1) FROM meta_data GROUP BY class";
+
+    public unowned string make (SQLString query) {
+        switch (query) {
+            case SQLString.SAVE_METADATA:
+                return SAVE_META_DATA_STRING;
+            case SQLString.INSERT:
+                return INSERT_OBJECT_STRING;
+            case SQLString.DELETE:
+                return DELETE_BY_ID_STRING;
+            case SQLString.GET_OBJECT:
+                return GET_OBJECT_WITH_PATH;
+            case SQLString.GET_CHILDREN:
+                return GET_CHILDREN_STRING;
+            case SQLString.GET_OBJECTS_BY_FILTER:
+                return GET_OBJECTS_BY_FILTER_STRING;
+            case SQLString.GET_OBJECTS_BY_FILTER_WITH_ANCESTOR:
+                return GET_OBJECTS_BY_FILTER_STRING_WITH_ANCESTOR;
+            case SQLString.GET_OBJECT_COUNT_BY_FILTER:
+                return GET_OBJECT_COUNT_BY_FILTER_STRING;
+            case SQLString.GET_OBJECT_COUNT_BY_FILTER_WITH_ANCESTOR:
+                return GET_OBJECT_COUNT_BY_FILTER_STRING_WITH_ANCESTOR;
+            case SQLString.GET_META_DATA_COLUMN:
+                return GET_META_DATA_COLUMN_STRING;
+            case SQLString.CHILD_COUNT:
+                return CHILDREN_COUNT_STRING;
+            case SQLString.EXISTS:
+                return OBJECT_EXISTS_STRING;
+            case SQLString.CHILD_IDS:
+                return GET_CHILD_ID_STRING;
+            case SQLString.TABLE_METADATA:
+                return CREATE_META_DATA_TABLE_STRING;
+            case SQLString.TRIGGER_COMMON:
+                return CREATE_TRIGGER_STRING;
+            case SQLString.TRIGGER_CLOSURE:
+                return CREATE_CLOSURE_TRIGGER_STRING;
+            case SQLString.INDEX_COMMON:
+                return CREATE_INDICES_STRING;
+            case SQLString.SCHEMA:
+                return SCHEMA_STRING;
+            case SQLString.EXISTS_CACHE:
+                return EXISTS_CACHE_STRING;
+            case SQLString.TABLE_CLOSURE:
+                return CREATE_CLOSURE_TABLE;
+            case SQLString.STATISTICS:
+                return STATISTICS_STRING;
+            default:
+                assert_not_reached ();
+        }
+    }
+}
diff --git a/src/rygel-wmv-transcoder.vala b/src/media-export/rygel-media-export-sql-function.vala
similarity index 60%
rename from src/rygel-wmv-transcoder.vala
rename to src/media-export/rygel-media-export-sql-function.vala
index 947ee04..0ff0a80 100644
--- a/src/rygel-wmv-transcoder.vala
+++ b/src/media-export/rygel-media-export-sql-function.vala
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2009 Jens Georg <mail jensge org>.
+ * Copyright (C) 2010 Jens Georg <mail jensge org>.
  *
  * Author: Jens Georg <mail jensge org>
  *
@@ -19,21 +19,13 @@
  * along with this program; if not, write to the Free Software Foundation,
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
  */
-using Gst;
-using GUPnP;
 
-internal class Rygel.WMVTranscoder : Rygel.VideoTranscoder {
-    private const int VIDEO_BITRATE = 1200;
-    private const int AUDIO_BITRATE = 64;
+internal class Rygel.MediaExport.SqlFunction : SqlOperator {
+    public SqlFunction (string name, string arg) {
+        base (name, arg);
+    }
 
-    public WMVTranscoder () {
-        base ("video/x-ms-wmv",
-              "WMVHIGH_FULL",
-              AUDIO_BITRATE,
-              VIDEO_BITRATE,
-              "video/x-ms-asf,parsed=true",
-              "audio/x-wma,channels=2,wmaversion=1",
-              "video/x-wmv,wmvversion=1",
-              "wmv");
+    public override string to_string () {
+        return "%s(%s,?)".printf (name, arg);
     }
 }
diff --git a/src/media-export/rygel-media-export-sql-operator.vala b/src/media-export/rygel-media-export-sql-operator.vala
new file mode 100644
index 0000000..3620091
--- /dev/null
+++ b/src/media-export/rygel-media-export-sql-operator.vala
@@ -0,0 +1,73 @@
+/*
+ * Copyright (C) 2010 Jens Georg <mail jensge org>.
+ *
+ * Author: Jens Georg <mail jensge org>
+ *
+ * This file is part of Rygel.
+ *
+ * Rygel is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * Rygel 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 Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+
+using GUPnP;
+
+internal class Rygel.MediaExport.SqlOperator : GLib.Object {
+    protected string name;
+    protected string arg;
+    protected string collate;
+
+    public SqlOperator (string name,
+                        string arg,
+                        string collate = "") {
+        this.name = name;
+        this.arg = arg;
+        this.collate = collate;
+    }
+
+    public SqlOperator.from_search_criteria_op (SearchCriteriaOp op,
+                                                string           arg,
+                                                string           collate) {
+        string sql = null;
+        switch (op) {
+            case SearchCriteriaOp.EQ:
+                sql = "=";
+                break;
+            case SearchCriteriaOp.NEQ:
+                sql = "!=";
+                break;
+            case SearchCriteriaOp.LESS:
+                sql = "<";
+                break;
+            case SearchCriteriaOp.LEQ:
+                sql = "<=";
+                break;
+            case SearchCriteriaOp.GREATER:
+                sql = ">";
+                break;
+            case SearchCriteriaOp.GEQ:
+                sql = ">=";
+                break;
+            default:
+                assert_not_reached ();
+        }
+
+        this (sql, arg, collate);
+    }
+
+    public virtual string to_string () {
+        return "(%s %s ? %s)".printf (arg, name, collate);
+    }
+}
+
+
diff --git a/src/media-export/rygel-media-export-sqlite-wrapper.vala b/src/media-export/rygel-media-export-sqlite-wrapper.vala
new file mode 100644
index 0000000..84a288b
--- /dev/null
+++ b/src/media-export/rygel-media-export-sqlite-wrapper.vala
@@ -0,0 +1,80 @@
+/*
+ * Copyright (C) 2011 Jens Georg <mail jensge org>.
+ *
+ * Author: Jens Georg <mail jensge org>
+ *
+ * This file is part of Rygel.
+ *
+ * Rygel is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * Rygel 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 Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+
+using Sqlite;
+
+internal class Rygel.MediaExport.SqliteWrapper : Object {
+    private Sqlite.Database database = null;
+    private Sqlite.Database *reference = null;
+
+    /**
+     * Property to access the wrapped database
+     */
+    protected unowned Sqlite.Database db {
+        get { return reference; }
+    }
+
+    /**
+     * Wrap an existing SQLite Database object.
+     *
+     * The SqliteWrapper doesn't take ownership of the passed db
+     */
+    public SqliteWrapper.wrap (Sqlite.Database db) {
+        this.reference = db;
+    }
+
+    /**
+     * Create or open a new SQLite database in path.
+     *
+     * @note: Path may also be ":memory:" for temporary databases
+     */
+    public SqliteWrapper (string path) throws DatabaseError {
+        Sqlite.Database.open (path, out this.database);
+        this.reference = this.database;
+        this.throw_if_db_has_error ();
+    }
+
+    /**
+     * Convert a SQLite return code to a DatabaseError
+     */
+    protected void throw_if_code_is_error (int sqlite_error)
+                                           throws DatabaseError {
+        switch (sqlite_error) {
+            case Sqlite.OK:
+            case Sqlite.DONE:
+            case Sqlite.ROW:
+                return;
+            default:
+                throw new DatabaseError.SQLITE_ERROR
+                                        ("SQLite error %d: %s",
+                                         sqlite_error,
+                                         this.reference->errmsg ());
+        }
+    }
+
+    /**
+     * Check if the last operation on the database was an error
+     */
+    protected void throw_if_db_has_error () throws DatabaseError {
+        this.throw_if_code_is_error (this.reference->errcode ());
+    }
+}
diff --git a/src/rygel-mp3-transcoder.vala b/src/media-export/rygel-media-export-video-item.vala
similarity index 56%
rename from src/rygel-mp3-transcoder.vala
rename to src/media-export/rygel-media-export-video-item.vala
index ecbdd45..e22213d 100644
--- a/src/rygel-mp3-transcoder.vala
+++ b/src/media-export/rygel-media-export-video-item.vala
@@ -1,8 +1,7 @@
 /*
- * Copyright (C) 2009 Nokia Corporation.
+ * Copyright (C) 2012 Intel Corporation.
  *
- * Author: Zeeshan Ali (Khattak) <zeeshanak gnome org>
- *                               <zeeshan ali nokia com>
+ * Author: Jens Georg <jensg openismus com>
  *
  * This file is part of Rygel.
  *
@@ -20,23 +19,19 @@
  * along with this program; if not, write to the Free Software Foundation,
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
  */
-using Gst;
-using GUPnP;
-using Gee;
 
-/**
- * Transcoder for mpeg 1 layer 3 audio.
- */
-internal class Rygel.MP3Transcoder : Rygel.AudioTranscoder {
-    public const int BITRATE = 128;
-    private const string FORMAT = "audio/mpeg,mpegversion=1,layer=3";
+internal class Rygel.MediaExport.VideoItem : Rygel.VideoItem,
+                                             Rygel.UpdatableObject {
+    public VideoItem (string         id,
+                      MediaContainer parent,
+                      string         title,
+                      string         upnp_class = Rygel.VideoItem.UPNP_CLASS) {
+        base (id, parent, title, upnp_class);
+    }
 
-    public MP3Transcoder () {
-        base ("audio/mpeg",
-              "MP3",
-              BITRATE,
-              AudioTranscoder.NO_CONTAINER,
-              FORMAT,
-              "mp3");
+    public async void commit () throws Error {
+        var cache = MediaCache.get_default ();
+        cache.save_item (this);
     }
+
 }
diff --git a/src/media-export/rygel-media-export-writable-db-container.vala b/src/media-export/rygel-media-export-writable-db-container.vala
new file mode 100644
index 0000000..f3c3073
--- /dev/null
+++ b/src/media-export/rygel-media-export-writable-db-container.vala
@@ -0,0 +1,55 @@
+/*
+ * Copyright (C) 2010 Jens Georg <mail jensge org>.
+ *
+ * Author: Jens Georg <mail jensge org>
+ *
+ * This file is part of Rygel.
+ *
+ * Rygel is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * Rygel 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 Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+using Gee;
+
+internal class Rygel.MediaExport.WritableDbContainer : DBContainer,
+                                                    Rygel.WritableContainer {
+    public ArrayList<string> create_classes { get; set; }
+
+    public WritableDbContainer (MediaCache media_db, string id, string title) {
+        base (media_db, id, title);
+
+        this.create_classes = new ArrayList<string> ();
+        this.create_classes.add (Rygel.ImageItem.UPNP_CLASS);
+        this.create_classes.add (Rygel.PhotoItem.UPNP_CLASS);
+        this.create_classes.add (Rygel.VideoItem.UPNP_CLASS);
+        this.create_classes.add (Rygel.AudioItem.UPNP_CLASS);
+        this.create_classes.add (Rygel.MusicItem.UPNP_CLASS);
+    }
+
+    public async void add_item (Rygel.MediaItem item, Cancellable? cancellable)
+                                throws Error {
+        item.parent = this;
+        var file = File.new_for_uri (item.uris[0]);
+        // TODO: Mark as place-holder. Make this proper some time.
+        if (file.is_native ()) {
+            item.modified = int64.MAX;
+        }
+        item.id = MediaCache.get_id (file);
+        this.media_db.save_item (item);
+    }
+
+    public async void remove_item (string id, Cancellable? cancellable)
+                                   throws Error {
+        this.media_db.remove_by_id (id);
+    }
+}



[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]