[rygel-gst-0-10-plugins] Intial media-export plugin for gst-0.10
- From: Murray Cumming <murrayc src gnome org>
- To: commits-list gnome org
- Cc:
- Subject: [rygel-gst-0-10-plugins] Intial media-export plugin for gst-0.10
- Date: Tue, 11 Dec 2012 10:52:34 +0000 (UTC)
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]