diff options
Diffstat (limited to 'meta-agl/recipes-connectivity/rygel/files/0001-Add-LightMediaScanner-plugin.patch')
-rw-r--r-- | meta-agl/recipes-connectivity/rygel/files/0001-Add-LightMediaScanner-plugin.patch | 2541 |
1 files changed, 2541 insertions, 0 deletions
diff --git a/meta-agl/recipes-connectivity/rygel/files/0001-Add-LightMediaScanner-plugin.patch b/meta-agl/recipes-connectivity/rygel/files/0001-Add-LightMediaScanner-plugin.patch new file mode 100644 index 000000000..eca08ee84 --- /dev/null +++ b/meta-agl/recipes-connectivity/rygel/files/0001-Add-LightMediaScanner-plugin.patch @@ -0,0 +1,2541 @@ +From 8bb9ae73464dd76f5fa94f2e9ba76b0bd88114df Mon Sep 17 00:00:00 2001 +From: Manuel Bachmann <manuel.bachmann@iot.bzh> +Date: Mon, 26 Oct 2015 04:18:33 +0000 +Subject: [PATCH] Add LightMediaScanner plugin + +Add a new plugin based on LightMediaScanner : +https://github.com/profusion/lightmediascanner + +Shorty put, this plugin does not do the indexing itself as +"media-export" does, but defers this task to the +"lightmediascannerd" daemon, with which it communicates +via a D-Bus interface. +The remote indexing daemon installs itself as a separate +package, and is designed to be ultra-lightweight and fast. + +This commit is the rebase and fusion of all work done +first on Maemo (lms.garage.maemo.org), then on Tizen IVI +(review.tizen.org/git/?p=profile/ivi/rygel.git) and right +now on AGL (automotivelinux.org) where several components +depend on LightMediaScanner. +A splitted version (13 commits) can also be seen on : +https://github.com/Tarnyko/rygel + +It difffers mostly in that it lets "media-export" plugin +as the default, just adding itself in "rygel.conf" as an +alternative. + +(note : reporter is not code Author, see patch for details) +(note 2 : rebased on top of 0.26.1) + +Author: Jussi Kukkonen <jussi.kukkonen@intel.com> +Author: Alexander Kanavin <alex.kanavin@gmail.com> +Signed-off-by: Manuel Bachmann <manuel.bachmann@iot.bzh> +--- + configure.ac | 18 + + data/rygel.conf | 6 +- + src/plugins/Makefile.am | 5 + + src/plugins/lms/Makefile.am | 46 +++ + src/plugins/lms/README | 17 + + src/plugins/lms/lms.plugin.in | 7 + + src/plugins/lms/rygel-lms-album.vala | 173 +++++++++ + src/plugins/lms/rygel-lms-albums.vala | 175 +++++++++ + src/plugins/lms/rygel-lms-all-images.vala | 95 +++++ + src/plugins/lms/rygel-lms-all-music.vala | 169 ++++++++ + src/plugins/lms/rygel-lms-all-videos.vala | 123 ++++++ + src/plugins/lms/rygel-lms-artist.vala | 75 ++++ + src/plugins/lms/rygel-lms-artists.vala | 62 +++ + src/plugins/lms/rygel-lms-category-container.vala | 428 +++++++++++++++++++++ + src/plugins/lms/rygel-lms-collate.c | 49 +++ + src/plugins/lms/rygel-lms-database.vala | 294 ++++++++++++++ + src/plugins/lms/rygel-lms-dbus-interfaces.vala | 30 ++ + src/plugins/lms/rygel-lms-image-root.vala | 35 ++ + src/plugins/lms/rygel-lms-image-year.vala | 114 ++++++ + src/plugins/lms/rygel-lms-image-years.vala | 59 +++ + src/plugins/lms/rygel-lms-music-root.vala | 36 ++ + src/plugins/lms/rygel-lms-plugin-factory.vala | 40 ++ + src/plugins/lms/rygel-lms-plugin.vala | 35 ++ + src/plugins/lms/rygel-lms-root-container.vala | 58 +++ + src/plugins/lms/rygel-lms-sql-function.vala | 31 ++ + src/plugins/lms/rygel-lms-sql-operator.vala | 73 ++++ + 26 files changed, 2252 insertions(+), 1 deletion(-) + create mode 100644 src/plugins/lms/Makefile.am + create mode 100644 src/plugins/lms/README + create mode 100644 src/plugins/lms/lms.plugin.in + create mode 100644 src/plugins/lms/rygel-lms-album.vala + create mode 100644 src/plugins/lms/rygel-lms-albums.vala + create mode 100644 src/plugins/lms/rygel-lms-all-images.vala + create mode 100644 src/plugins/lms/rygel-lms-all-music.vala + create mode 100644 src/plugins/lms/rygel-lms-all-videos.vala + create mode 100644 src/plugins/lms/rygel-lms-artist.vala + create mode 100644 src/plugins/lms/rygel-lms-artists.vala + create mode 100644 src/plugins/lms/rygel-lms-category-container.vala + create mode 100644 src/plugins/lms/rygel-lms-collate.c + create mode 100644 src/plugins/lms/rygel-lms-database.vala + create mode 100644 src/plugins/lms/rygel-lms-dbus-interfaces.vala + create mode 100644 src/plugins/lms/rygel-lms-image-root.vala + create mode 100644 src/plugins/lms/rygel-lms-image-year.vala + create mode 100644 src/plugins/lms/rygel-lms-image-years.vala + create mode 100644 src/plugins/lms/rygel-lms-music-root.vala + create mode 100644 src/plugins/lms/rygel-lms-plugin-factory.vala + create mode 100644 src/plugins/lms/rygel-lms-plugin.vala + create mode 100644 src/plugins/lms/rygel-lms-root-container.vala + create mode 100644 src/plugins/lms/rygel-lms-sql-function.vala + create mode 100644 src/plugins/lms/rygel-lms-sql-operator.vala + +diff --git a/configure.ac b/configure.ac +index 7ae1105..275fd99 100644 +--- a/configure.ac ++++ b/configure.ac +@@ -170,6 +170,18 @@ AS_IF([test "x$enable_ruih_plugin" = "xyes"], + libxml-2.0 >= $LIBXML_REQUIRED]) + ]) + ++ ++RYGEL_ADD_PLUGIN([lms],[LightMediaScanner],[yes]) ++AS_IF([test "x$enable_lms_plugin" = "xyes"], ++ [ ++ PKG_CHECK_MODULES([RYGEL_PLUGIN_LMS_DEPS], ++ [$RYGEL_COMMON_MODULES ++ gio-2.0 >= $GIO_REQUIRED ++ sqlite3 >= $LIBSQLITE3_REQUIRED]) ++ RYGEL_PLUGIN_LMS_DEPS_VALAFLAGS="$RYGEL_COMMON_MODULES_VALAFLAGS --pkg gio-2.0 --pkg gee-0.8 --pkg sqlite3" ++ AC_SUBST([RYGEL_PLUGIN_LMS_DEPS_VALAFLAGS]) ++ ]) ++ + AS_IF([test "x$with_media_engine" = "xgstreamer"], + [ + RYGEL_ADD_PLUGIN([playbin],[GStreamer playbin],[yes]) +@@ -332,6 +344,11 @@ then + fi + fi + ++dnl Check additional requirements for LMS plugin ++if test "x$enable_lms_plugin" = "xyes"; ++then ++ RYGEL_CHECK_PACKAGES([sqlite3]) ++fi + + RYGEL_ADD_PLUGIN([tracker],[Tracker],[yes]) + AS_IF([test "x$enable_tracker_plugin" = "xyes"], +@@ -513,6 +530,7 @@ echo " + version: ${tracker_api_version} + mediathek: ${enable_mediathek_plugin} + media-export ${enable_media_export_plugin} ++ lightmediascanner ${enable_lms_plugin} + external: ${enable_external_plugin} + MPRIS2: ${enable_mpris_plugin} + gst-launch: ${enable_gst_launch_plugin} +diff --git a/data/rygel.conf b/data/rygel.conf +index 6b1c1c4..8677a0d 100644 +--- a/data/rygel.conf ++++ b/data/rygel.conf +@@ -99,7 +99,7 @@ strict-sharing=false + title=@REALNAME@'s media on @PRETTY_HOSTNAME@ + + [MediaExport] +-enabled=true ++enabled=false + title=@REALNAME@'s media on @PRETTY_HOSTNAME@ + # List of URIs to export. Following variables are automatically substituted by + # the appropriate XDG standard media folders by Rygel for you. +@@ -114,6 +114,10 @@ monitor-changes=true + monitor-grace-timeout=5 + virtual-folders=true + ++[LightMediaScanner] ++enabled=true ++title=My Media ++ + [Playbin] + enabled=true + title=Audio/Video playback on @PRETTY_HOSTNAME@ +diff --git a/src/plugins/Makefile.am b/src/plugins/Makefile.am +index d116f09..40791f0 100644 +--- a/src/plugins/Makefile.am ++++ b/src/plugins/Makefile.am +@@ -10,6 +10,10 @@ if BUILD_MEDIA_EXPORT_PLUGIN + MEDIA_EXPORT_PLUGIN = media-export + endif + ++if BUILD_LMS_PLUGIN ++LMS_PLUGIN = lms ++endif ++ + if BUILD_EXTERNAL_PLUGIN + EXTERNAL_PLUGIN = external + endif +@@ -33,6 +37,7 @@ endif + SUBDIRS = $(TRACKER_PLUGIN) \ + $(MEDIATHEK_PLUGIN) \ + $(MEDIA_EXPORT_PLUGIN) \ ++ $(LMS_PLUGIN) \ + $(EXTERNAL_PLUGIN) \ + $(MPRIS_PLUGIN) \ + $(GST_LAUNCH_PLUGIN) \ +diff --git a/src/plugins/lms/Makefile.am b/src/plugins/lms/Makefile.am +new file mode 100644 +index 0000000..f96a2ab +--- /dev/null ++++ b/src/plugins/lms/Makefile.am +@@ -0,0 +1,46 @@ ++include $(top_srcdir)/common.am ++ ++plugin_LTLIBRARIES = librygel-lms.la ++plugin_DATA = lms.plugin ++ ++librygel_lms_la_SOURCES = \ ++ rygel-lms-plugin.vala \ ++ rygel-lms-plugin-factory.vala \ ++ rygel-lms-root-container.vala \ ++ rygel-lms-music-root.vala \ ++ rygel-lms-image-root.vala \ ++ rygel-lms-category-container.vala \ ++ rygel-lms-all-music.vala \ ++ rygel-lms-album.vala \ ++ rygel-lms-albums.vala \ ++ rygel-lms-artist.vala \ ++ rygel-lms-artists.vala \ ++ rygel-lms-all-videos.vala \ ++ rygel-lms-database.vala \ ++ rygel-lms-all-images.vala \ ++ rygel-lms-image-years.vala \ ++ rygel-lms-image-year.vala \ ++ rygel-lms-sql-function.vala \ ++ rygel-lms-sql-operator.vala \ ++ rygel-lms-collate.c \ ++ rygel-lms-dbus-interfaces.vala ++ ++librygel_lms_la_VALAFLAGS = \ ++ --enable-experimental \ ++ $(RYGEL_PLUGIN_LMS_DEPS_VALAFLAGS) \ ++ $(RYGEL_COMMON_LIBRYGEL_SERVER_VALAFLAGS) \ ++ $(RYGEL_COMMON_VALAFLAGS) ++ ++librygel_lms_la_CFLAGS = \ ++ $(RYGEL_PLUGIN_LMS_DEPS_CFLAGS) \ ++ $(RYGEL_COMMON_LIBRYGEL_SERVER_CFLAGS) \ ++ -DG_LOG_DOMAIN='"Lms"' ++ ++librygel_lms_la_LIBADD = \ ++ $(RYGEL_PLUGIN_LMS_DEPS_LIBS) \ ++ $(RYGEL_COMMON_LIBRYGEL_SERVER_LIBS) ++ ++librygel_lms_la_LDFLAGS = \ ++ $(RYGEL_PLUGIN_LINKER_FLAGS) ++ ++EXTRA_DIST = lms.plugin.in +diff --git a/src/plugins/lms/README b/src/plugins/lms/README +new file mode 100644 +index 0000000..b741806 +--- /dev/null ++++ b/src/plugins/lms/README +@@ -0,0 +1,17 @@ ++rygel-lms ++========= ++ ++A rygel mediaserver plugin that exposes a lightmediascanner database ++as a Mediaserver. ++ ++Configuration in rygel.conf: ++ ++ [LightMediaScanner] ++ db-path=/path/to/lightmediascannerd.sqlite3 ++ title=My Media ++ ++* Supports browsing and searching (but in many cases searches will ++ still fall back to the inefficient simple_search()). ++* UpdateIDs are not yet supported as lightmediascanner seems to have ++ not change signal support yet ++* No real DLNA CTT testing has been done so far +diff --git a/src/plugins/lms/lms.plugin.in b/src/plugins/lms/lms.plugin.in +new file mode 100644 +index 0000000..9db9895 +--- /dev/null ++++ b/src/plugins/lms/lms.plugin.in +@@ -0,0 +1,7 @@ ++[Plugin] ++Version = @VERSION@ ++Module = lms ++Name = LMS ++License = LGPL ++Description = LMS DMS plugin for Rygel ++Copyright = Copyright © Intel +diff --git a/src/plugins/lms/rygel-lms-album.vala b/src/plugins/lms/rygel-lms-album.vala +new file mode 100644 +index 0000000..4fea17a +--- /dev/null ++++ b/src/plugins/lms/rygel-lms-album.vala +@@ -0,0 +1,173 @@ ++/* ++ * Copyright (C) 2013 Intel Corporation. ++ * ++ * Author: Jussi Kukkonen <jussi.kukkonen@intel.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 Rygel; ++using Sqlite; ++ ++public class Rygel.LMS.Album : Rygel.LMS.CategoryContainer { ++ private static const string SQL_ALL_TEMPLATE = ++ "SELECT files.id, files.path, files.size, " + ++ "audios.title as title, audios.trackno, audios.length, audios.channels, audios.sampling_rate, audios.bitrate, audios.dlna_profile, audios.dlna_mime, " + ++ "audio_artists.name as artist, " + ++ "audio_albums.name " + ++ "FROM audios, files " + ++ "LEFT JOIN audio_artists " + ++ "ON audios.artist_id = audio_artists.id " + ++ "LEFT JOIN audio_albums " + ++ "ON audios.album_id = audio_albums.id " + ++ "WHERE dtime = 0 AND audios.id = files.id AND audios.album_id = %s " + ++ "LIMIT ? OFFSET ?;"; ++ ++ private static const string SQL_COUNT_TEMPLATE = ++ "SELECT COUNT(audios.id) " + ++ "FROM audios, files " + ++ "WHERE dtime = 0 AND audios.id = files.id AND audios.album_id = %s;"; ++ ++ private static const string SQL_COUNT_WITH_FILTER_TEMPLATE = ++ "SELECT COUNT(audios.id), audios.title as title, " + ++ "audio_artists.name as artist, " + ++ "audio_albums.name " + ++ "FROM audios, files " + ++ "LEFT JOIN audio_artists " + ++ "ON audios.artist_id = audio_artists.id " + ++ "LEFT JOIN audio_albums " + ++ "ON audios.album_id = audio_albums.id " + ++ "WHERE dtime = 0 AND audios.id = files.id AND audios.album_id = %s;"; ++ ++ private static const string SQL_FIND_OBJECT_TEMPLATE = ++ "SELECT files.id, files.path, files.size, " + ++ "audios.title, audios.trackno, audios.length, audios.channels, audios.sampling_rate, audios.bitrate, audios.dlna_profile, audios.dlna_mime, " + ++ "audio_artists.name, " + ++ "audio_albums.name " + ++ "FROM audios, files " + ++ "LEFT JOIN audio_artists " + ++ "ON audios.artist_id = audio_artists.id " + ++ "LEFT JOIN audio_albums " + ++ "ON audios.album_id = audio_albums.id " + ++ "WHERE dtime = 0 AND files.id = ? AND audios.id = files.id AND audios.album_id = %s;"; ++ ++ private static const string SQL_ADDED_TEMPLATE = ++ "SELECT files.id, files.path, files.size, " + ++ "audios.title as title, audios.trackno, audios.length, audios.channels, audios.sampling_rate, audios.bitrate, audios.dlna_profile, audios.dlna_mime, " + ++ "audio_artists.name as artist, " + ++ "audio_albums.name " + ++ "FROM audios, files " + ++ "LEFT JOIN audio_artists " + ++ "ON audios.artist_id = audio_artists.id " + ++ "LEFT JOIN audio_albums " + ++ "ON audios.album_id = audio_albums.id " + ++ "WHERE dtime = 0 AND audios.id = files.id AND audios.album_id = %s " + ++ "AND update_id > ? AND update_id <= ?;"; ++ ++ private static const string SQL_REMOVED_TEMPLATE = ++ "SELECT files.id, files.path, files.size, " + ++ "audios.title as title, audios.trackno, audios.length, audios.channels, audios.sampling_rate, audios.bitrate, audios.dlna_profile, audios.dlna_mime, " + ++ "audio_artists.name as artist, " + ++ "audio_albums.name " + ++ "FROM audios, files " + ++ "LEFT JOIN audio_artists " + ++ "ON audios.artist_id = audio_artists.id " + ++ "LEFT JOIN audio_albums " + ++ "ON audios.album_id = audio_albums.id " + ++ "WHERE dtime <> 0 AND audios.id = files.id AND audios.album_id = %s " + ++ "AND update_id > ? AND update_id <= ?;"; ++ ++ protected override MediaObject? object_from_statement (Statement statement) { ++ var id = statement.column_int (0); ++ var path = statement.column_text (1); ++ var mime_type = statement.column_text(10); ++ ++ if (mime_type == null || mime_type.length == 0) { ++ /* TODO is this correct? */ ++ debug ("Music item %d (%s) has no MIME type", ++ id, ++ path); ++ } ++ ++ var title = statement.column_text(3); ++ var song_id = this.build_child_id (id); ++ var song = new MusicItem (song_id, this, title); ++ song.ref_id = this.build_reference_id (id); ++ song.size = statement.column_int(2); ++ song.track_number = statement.column_int(4); ++ song.duration = statement.column_int(5); ++ song.channels = statement.column_int(6); ++ song.sample_freq = statement.column_int(7); ++ song.bitrate = statement.column_int(8); ++ song.dlna_profile = statement.column_text(9); ++ song.mime_type = mime_type; ++ song.artist = statement.column_text(11); ++ song.album = statement.column_text(12); ++ File file = File.new_for_path (path); ++ song.add_uri (file.get_uri ()); ++ ++ return song; ++ } ++ ++ private static string get_sql_all (string db_id) { ++ return (SQL_ALL_TEMPLATE.printf (db_id)); ++ } ++ private static string get_sql_find_object (string db_id) { ++ return (SQL_FIND_OBJECT_TEMPLATE.printf (db_id)); ++ } ++ private static string get_sql_count (string db_id) { ++ return (SQL_COUNT_TEMPLATE.printf (db_id)); ++ } ++ private static string get_sql_added (string db_id) { ++ return (SQL_ADDED_TEMPLATE.printf (db_id)); ++ } ++ private static string get_sql_removed (string db_id) { ++ return (SQL_REMOVED_TEMPLATE.printf (db_id)); ++ } ++ ++ protected override string get_sql_all_with_filter (string filter) { ++ if (filter.length == 0) { ++ return this.sql_all; ++ } ++ var filter_str = "%s AND %s".printf (this.db_id, filter); ++ return (SQL_ALL_TEMPLATE.printf (filter_str)); ++ } ++ ++ protected override string get_sql_count_with_filter (string filter) { ++ if (filter.length == 0) { ++ return this.sql_count; ++ } ++ var filter_str = "%s AND %s".printf (this.db_id, filter); ++ return (SQL_COUNT_WITH_FILTER_TEMPLATE.printf (filter_str)); ++ } ++ ++ public Album (string db_id, ++ MediaContainer parent, ++ string title, ++ LMS.Database lms_db) { ++ base (db_id, ++ parent, ++ title, ++ lms_db, ++ get_sql_all (db_id), ++ get_sql_find_object (db_id), ++ get_sql_count (db_id), ++ get_sql_added (db_id), ++ get_sql_removed (db_id) ++ ); ++ } ++} +diff --git a/src/plugins/lms/rygel-lms-albums.vala b/src/plugins/lms/rygel-lms-albums.vala +new file mode 100644 +index 0000000..309a352 +--- /dev/null ++++ b/src/plugins/lms/rygel-lms-albums.vala +@@ -0,0 +1,175 @@ ++/* ++ * Copyright (C) 2013 Intel Corporation. ++ * ++ * Author: Jussi Kukkonen <jussi.kukkonen@intel.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 Rygel; ++using Sqlite; ++ ++public class Rygel.LMS.Albums : Rygel.LMS.CategoryContainer { ++ private static const string SQL_ALL = ++ "SELECT audio_albums.id, audio_albums.name as title, " + ++ "audio_artists.name as artist " + ++ "FROM audio_albums " + ++ "LEFT JOIN audio_artists " + ++ "ON audio_albums.artist_id = audio_artists.id " + ++ "LIMIT ? OFFSET ?;"; ++ ++ private static const string SQL_ALL_WITH_FILTER_TEMPLATE = ++ "SELECT audio_albums.id, audio_albums.name as title, " + ++ "audio_artists.name as artist " + ++ "FROM audio_albums " + ++ "LEFT JOIN audio_artists " + ++ "ON audio_albums.artist_id = audio_artists.id " + ++ "WHERE %s " + ++ "LIMIT ? OFFSET ?;"; ++ ++ private static const string SQL_COUNT = ++ "SELECT COUNT(audio_albums.id) " + ++ "FROM audio_albums;"; ++ ++ private static const string SQL_COUNT_WITH_FILTER_TEMPLATE = ++ "SELECT COUNT(audio_albums.id), audio_albums.name as title, " + ++ "audio_artists.name as artist " + ++ "FROM audio_albums " + ++ "LEFT JOIN audio_artists " + ++ "ON audio_albums.artist_id = audio_artists.id " + ++ "WHERE %s;"; ++ ++ /* count songs inside albums */ ++ private static const string SQL_CHILD_COUNT_WITH_FILTER_TEMPLATE = ++ "SELECT COUNT(audios.id), audios.title as title, " + ++ "audio_artists.name as artist " + ++ "FROM audios, files, audio_albums " + ++ "LEFT JOIN audio_artists " + ++ "ON audios.artist_id = audio_artists.id " + ++ "WHERE dtime = 0 AND audios.id = files.id AND audios.album_id = audio_albums.id %s;"; ++ ++ /* select songs inside albums */ ++ private static const string SQL_CHILD_ALL_WITH_FILTER_TEMPLATE = ++ "SELECT files.id, files.path, files.size, " + ++ "audios.title as title, audios.trackno, audios.length, audios.channels, audios.sampling_rate, audios.bitrate, audios.dlna_profile, audios.dlna_mime, " + ++ "audio_artists.name as artist, " + ++ "audio_albums.name, audio_albums.id " + ++ "FROM audios, files, audio_albums " + ++ "LEFT JOIN audio_artists " + ++ "ON audios.artist_id = audio_artists.id " + ++ "WHERE dtime = 0 AND audios.id = files.id AND audios.album_id = audio_albums.id %s " + ++ "LIMIT ? OFFSET ?;"; ++ ++ ++ private static const string SQL_FIND_OBJECT = ++ "SELECT audio_albums.id, audio_albums.name " + ++ "FROM audio_albums " + ++ "WHERE audio_albums.id = ?;"; ++ ++ protected override string get_sql_all_with_filter (string filter) { ++ if (filter.length == 0) { ++ return Albums.SQL_ALL; ++ } ++ return (Albums.SQL_ALL_WITH_FILTER_TEMPLATE.printf (filter)); ++ } ++ ++ protected override string get_sql_count_with_filter (string filter) { ++ if (filter.length == 0) { ++ return Albums.SQL_COUNT; ++ } ++ return (Albums.SQL_COUNT_WITH_FILTER_TEMPLATE.printf (filter)); ++ } ++ ++ protected override uint get_child_count_with_filter (string where_filter, ++ ValueArray args) ++ { ++ ++ /* search the children (albums) as usual */ ++ var count = base.get_child_count_with_filter (where_filter, args); ++ ++ /* now search the album contents */ ++ var filter = ""; ++ if (where_filter.length > 0) { ++ filter = "AND %s".printf (where_filter); ++ } ++ var query = Albums.SQL_CHILD_COUNT_WITH_FILTER_TEMPLATE.printf (filter); ++ try { ++ var stmt = this.lms_db.prepare_and_init (query, args.values); ++ if (stmt.step () == Sqlite.ROW) { ++ count += stmt.column_int (0); ++ } ++ } catch (DatabaseError e) { ++ warning ("Query failed: %s", e.message); ++ } ++ ++ return count; ++ } ++ ++ protected override MediaObjects? get_children_with_filter (string where_filter, ++ ValueArray args, ++ string sort_criteria, ++ uint offset, ++ uint max_count) { ++ var children = base. get_children_with_filter (where_filter, ++ args, ++ sort_criteria, ++ offset, ++ max_count); ++ var filter = ""; ++ if (where_filter.length > 0) { ++ filter = "AND %s".printf (where_filter); ++ } ++ var query = Albums.SQL_CHILD_ALL_WITH_FILTER_TEMPLATE.printf (filter); ++ try { ++ var stmt = this.lms_db.prepare_and_init (query, args.values); ++ while (Database.get_children_step (stmt)) { ++ var album_id = stmt.column_text (13); ++ var album = new Album (album_id, this, "", this.lms_db); ++ ++ var song = album.object_from_statement (stmt); ++ song.parent_ref = song.parent; ++ children.add (song); ++ ++ } ++ } catch (DatabaseError e) { ++ warning ("Query failed: %s", e.message); ++ } ++ ++ return children; ++ } ++ ++ protected override MediaObject? object_from_statement (Statement statement) { ++ var id = "%d".printf (statement.column_int (0)); ++ LMS.Album album = new LMS.Album (id, ++ this, ++ statement.column_text (1), ++ this.lms_db); ++ return album; ++ } ++ ++ public Albums (MediaContainer parent, ++ LMS.Database lms_db) { ++ base ("albums", ++ parent, ++ _("Albums"), ++ lms_db, ++ Albums.SQL_ALL, ++ Albums.SQL_FIND_OBJECT, ++ Albums.SQL_COUNT, ++ null, null); ++ } ++} +diff --git a/src/plugins/lms/rygel-lms-all-images.vala b/src/plugins/lms/rygel-lms-all-images.vala +new file mode 100644 +index 0000000..0b54c7f +--- /dev/null ++++ b/src/plugins/lms/rygel-lms-all-images.vala +@@ -0,0 +1,95 @@ ++/* ++ * Copyright (C) 2013 Intel Corporation. ++ * ++ * Author: Jussi Kukkonen <jussi.kukkonen@intel.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 Rygel; ++using Sqlite; ++ ++public class Rygel.LMS.AllImages : Rygel.LMS.CategoryContainer { ++ private static const string SQL_ALL = ++ "SELECT images.id, title, artist, date, width, height, path, size, dlna_profile, dlna_mime " + ++ "FROM images, files " + ++ "WHERE dtime = 0 AND images.id = files.id " + ++ "LIMIT ? OFFSET ?;"; ++ ++ private static const string SQL_COUNT = ++ "SELECT count(images.id) " + ++ "FROM images, files " + ++ "WHERE dtime = 0 AND images.id = files.id;"; ++ ++ private static const string SQL_FIND_OBJECT = ++ "SELECT images.id, title, artist, date, width, height, path, size, dlna_profile, dlna_mime " + ++ "FROM images, files " + ++ "WHERE dtime = 0 AND files.id = ? AND images.id = files.id;"; ++ ++ private static const string SQL_ADDED = ++ "SELECT images.id, title, artist, date, width, height, path, size, dlna_profile, dlna_mime " + ++ "FROM images, files " + ++ "WHERE dtime = 0 AND images.id = files.id " + ++ "AND update_id > ? AND update_id <= ?;"; ++ ++ private static const string SQL_REMOVED = ++ "SELECT images.id, title, artist, date, width, height, path, size, dlna_profile, dlna_mime " + ++ "FROM images, files " + ++ "WHERE dtime <> 0 AND images.id = files.id " + ++ "AND update_id > ? AND update_id <= ?;"; ++ ++ protected override MediaObject? object_from_statement (Statement statement) { ++ var id = statement.column_int(0); ++ var path = statement.column_text(6); ++ var mime_type = statement.column_text(9); ++ ++ if (mime_type == null || mime_type.length == 0){ ++ /* TODO is this correct? */ ++ debug ("Image item %d (%s) has no MIME type", ++ id, ++ path); ++ } ++ ++ var title = statement.column_text(1); ++ var image = new ImageItem(this.build_child_id (id), this, title); ++ image.creator = statement.column_text(2); ++ TimeVal tv = { (long) statement.column_int(3), (long) 0 }; ++ image.date = tv.to_iso8601 (); ++ image.width = statement.column_int(4); ++ image.height = statement.column_int(5); ++ image.size = statement.column_int(7); ++ image.mime_type = mime_type; ++ image.dlna_profile = statement.column_text(8); ++ File file = File.new_for_path(path); ++ image.add_uri (file.get_uri ()); ++ ++ return image; ++ } ++ ++ public AllImages (MediaContainer parent, LMS.Database lms_db) { ++ base ("all", ++ parent, ++ _("All"), ++ lms_db, ++ AllImages.SQL_ALL, ++ AllImages.SQL_FIND_OBJECT, ++ AllImages.SQL_COUNT, ++ AllImages.SQL_ADDED, ++ AllImages.SQL_REMOVED ++ ); ++ } ++} +diff --git a/src/plugins/lms/rygel-lms-all-music.vala b/src/plugins/lms/rygel-lms-all-music.vala +new file mode 100644 +index 0000000..2a7226f +--- /dev/null ++++ b/src/plugins/lms/rygel-lms-all-music.vala +@@ -0,0 +1,169 @@ ++/* ++ * Copyright (C) 2013 Intel Corporation. ++ * ++ * Author: Jussi Kukkonen <jussi.kukkonen@intel.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 Rygel; ++using Sqlite; ++ ++public class Rygel.LMS.AllMusic : Rygel.LMS.CategoryContainer { ++ private static const string SQL_ALL_TEMPLATE = ++ "SELECT files.id, files.path, files.size, " + ++ "audios.title as title, audios.trackno, audios.length, audios.channels, audios.sampling_rate, audios.bitrate, audios.dlna_profile, audios.dlna_mime, " + ++ "audio_artists.name as artist, " + ++ "audio_albums.name, " + ++ "files.mtime, " + ++ "audio_genres.name " + ++ "FROM audios, files " + ++ "LEFT JOIN audio_artists " + ++ "ON audios.artist_id = audio_artists.id " + ++ "LEFT JOIN audio_albums " + ++ "ON audios.album_id = audio_albums.id " + ++ "LEFT JOIN audio_genres " + ++ "ON audios.genre_id = audio_genres.id " + ++ "WHERE dtime = 0 AND audios.id = files.id %s " + ++ "LIMIT ? OFFSET ?;"; ++ ++ private static const string SQL_COUNT = ++ "SELECT COUNT(audios.id) " + ++ "FROM audios, files " + ++ "WHERE dtime = 0 AND audios.id = files.id;"; ++ ++ private static const string SQL_COUNT_WITH_FILTER_TEMPLATE = ++ "SELECT COUNT(audios.id), audios.title as title, " + ++ "audio_artists.name as artist " + ++ "FROM audios, files " + ++ "LEFT JOIN audio_artists " + ++ "ON audios.artist_id = audio_artists.id " + ++ "WHERE dtime = 0 AND audios.id = files.id %s;"; ++ ++ private static const string SQL_FIND_OBJECT = ++ "SELECT files.id, files.path, files.size, " + ++ "audios.title, audios.trackno, audios.length, audios.channels, audios.sampling_rate, audios.bitrate, audios.dlna_profile, audios.dlna_mime, " + ++ "audio_artists.name, " + ++ "audio_albums.name, " + ++ "files.mtime, " + ++ "audio_genres.name " + ++ "FROM audios, files " + ++ "LEFT JOIN audio_artists " + ++ "ON audios.artist_id = audio_artists.id " + ++ "LEFT JOIN audio_albums " + ++ "ON audios.album_id = audio_albums.id " + ++ "LEFT JOIN audio_genres " + ++ "ON audios.genre_id = audio_genres.id " + ++ "WHERE dtime = 0 AND files.id = ? AND audios.id = files.id;"; ++ ++ private static const string SQL_ADDED = ++ "SELECT files.id, files.path, files.size, " + ++ "audios.title as title, audios.trackno, audios.length, audios.channels, audios.sampling_rate, audios.bitrate, audios.dlna_profile, audios.dlna_mime, " + ++ "audio_artists.name as artist, " + ++ "audio_albums.name, " + ++ "files.mtime, " + ++ "audio_genres.name " + ++ "FROM audios, files " + ++ "LEFT JOIN audio_artists " + ++ "ON audios.artist_id = audio_artists.id " + ++ "LEFT JOIN audio_albums " + ++ "ON audios.album_id = audio_albums.id " + ++ "LEFT JOIN audio_genres " + ++ "ON audios.genre_id = audio_genres.id " + ++ "WHERE dtime = 0 AND audios.id = files.id " + ++ "AND update_id > ? AND update_id <= ?;"; ++ ++ private static const string SQL_REMOVED = ++ "SELECT files.id, files.path, files.size, " + ++ "audios.title as title, audios.trackno, audios.length, audios.channels, audios.sampling_rate, audios.bitrate, audios.dlna_profile, audios.dlna_mime, " + ++ "audio_artists.name as artist, " + ++ "audio_albums.name, " + ++ "files.mtime, " + ++ "audio_genres.name " + ++ "FROM audios, files " + ++ "LEFT JOIN audio_artists " + ++ "ON audios.artist_id = audio_artists.id " + ++ "LEFT JOIN audio_albums " + ++ "ON audios.album_id = audio_albums.id " + ++ "LEFT JOIN audio_genres " + ++ "ON audios.genre_id = audio_genres.id " + ++ "WHERE dtime <> 0 AND audios.id = files.id " + ++ "AND update_id > ? AND update_id <= ?;"; ++ ++ protected override string get_sql_all_with_filter (string filter) { ++ if (filter.length == 0) { ++ return this.sql_all; ++ } ++ var filter_str = "AND %s".printf (filter); ++ return (AllMusic.SQL_ALL_TEMPLATE.printf (filter_str)); ++ } ++ ++ protected override string get_sql_count_with_filter (string filter) { ++ if (filter.length == 0) { ++ return this.sql_count; ++ } ++ var filter_str = "AND %s".printf (filter); ++ return (AllMusic.SQL_COUNT_WITH_FILTER_TEMPLATE.printf (filter_str)); ++ } ++ ++ protected override MediaObject? object_from_statement (Statement statement) { ++ var id = statement.column_int (0); ++ var path = statement.column_text (1); ++ var mime_type = statement.column_text(10); ++ ++ if (mime_type == null || mime_type.length == 0) { ++ /* TODO is this correct? */ ++ debug ("Music item %d (%s) has no MIME type", ++ id, ++ path); ++ } ++ ++ var title = statement.column_text(3); ++ var song_id = this.build_child_id (id); ++ var song = new MusicItem (song_id, this, title); ++ song.size = statement.column_int(2); ++ song.track_number = statement.column_int(4); ++ song.duration = statement.column_int(5); ++ song.channels = statement.column_int(6); ++ song.sample_freq = statement.column_int(7); ++ song.bitrate = statement.column_int(8); ++ song.dlna_profile = statement.column_text(9); ++ song.mime_type = mime_type; ++ song.artist = statement.column_text(11); ++ song.album = statement.column_text(12); ++ TimeVal tv = { (long) statement.column_int(13), (long) 0 }; ++ song.date = tv.to_iso8601 (); ++ song.genre = statement.column_text(14); ++ File file = File.new_for_path (path); ++ song.add_uri (file.get_uri ()); ++ ++ return song; ++ } ++ ++ public AllMusic (MediaContainer parent, LMS.Database lms_db) { ++ base("all", ++ parent, ++ _("All"), ++ lms_db, ++ AllMusic.SQL_ALL_TEMPLATE.printf (""), ++ AllMusic.SQL_FIND_OBJECT, ++ AllMusic.SQL_COUNT, ++ AllMusic.SQL_ADDED, ++ AllMusic.SQL_REMOVED ++ ); ++ } ++} +diff --git a/src/plugins/lms/rygel-lms-all-videos.vala b/src/plugins/lms/rygel-lms-all-videos.vala +new file mode 100644 +index 0000000..dbde0db +--- /dev/null ++++ b/src/plugins/lms/rygel-lms-all-videos.vala +@@ -0,0 +1,123 @@ ++/* ++ * Copyright (C) 2013 Intel Corporation. ++ * ++ * Author: Jussi Kukkonen <jussi.kukkonen@intel.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 Rygel; ++using Sqlite; ++ ++public class Rygel.LMS.AllVideos : Rygel.LMS.CategoryContainer { ++ private static const string SQL_ALL = ++ "SELECT videos.id, title, artist, length, path, mtime, size, dlna_profile, dlna_mime " + ++ "FROM videos, files " + ++ "WHERE dtime = 0 AND videos.id = files.id " + ++ "LIMIT ? OFFSET ?;"; ++ ++ private static const string SQL_COUNT = ++ "SELECT count(videos.id) " + ++ "FROM videos, files " + ++ "WHERE dtime = 0 AND videos.id = files.id;"; ++ ++ private static const string SQL_FIND_OBJECT = ++ "SELECT videos.id, title, artist, length, path, mtime, size, dlna_profile, dlna_mime " + ++ "FROM videos, files " + ++ "WHERE dtime = 0 AND files.id = ? AND videos.id = files.id;"; ++ ++ private static const string SQL_ADDED = ++ "SELECT videos.id, title, artist, length, path, mtime, size, dlna_profile, dlna_mime " + ++ "FROM videos, files " + ++ "WHERE dtime = 0 AND videos.id = files.id " + ++ "AND update_id > ? AND update_id <= ?;"; ++ ++ private static const string SQL_REMOVED = ++ "SELECT videos.id, title, artist, length, path, mtime, size, dlna_profile, dlna_mime " + ++ "FROM videos, files " + ++ "WHERE dtime <> 0 AND videos.id = files.id " + ++ "AND update_id > ? AND update_id <= ?;"; ++ ++ protected override MediaObject? object_from_statement (Statement statement) { ++ var id = statement.column_int(0); ++ var mime_type = statement.column_text(8); ++ var path = statement.column_text(4); ++ var file = File.new_for_path(path); ++ ++ /* TODO: Temporary code to extract the MIME TYPE. LMS does not seem ++ to compute the mime type of videos. Don't know why. */ ++ ++/* if (mime_type == null || mime_type.length == 0) { ++ try { ++ FileInfo info = file.query_info(FileAttribute.STANDARD_CONTENT_TYPE, ++ FileQueryInfoFlags.NONE, null); ++ mime_type = info.get_content_type(); ++ } catch {} ++ } ++*/ ++ ++ if (mime_type == null || mime_type.length == 0) { ++ /* TODO is this correct? */ ++ debug ("Video item %d (%s) has no MIME type", ++ id, ++ path); ++ } ++ ++ var title = statement.column_text(1); ++ var video = new VideoItem(this.build_child_id (id), this, title); ++ video.creator = statement.column_text(2); ++ video.duration = statement.column_int(3); ++ TimeVal tv = { (long) statement.column_int(5), (long) 0 }; ++ video.date = tv.to_iso8601 (); ++ video.size = statement.column_int(6); ++ video.dlna_profile = statement.column_text(7); ++ video.mime_type = mime_type; ++ video.add_uri (file.get_uri ()); ++ ++ // Rygel does not support multiple video and audio tracks in a single file, ++ // so we just take the first one ++ var video_data = "select videos_videos.bitrate + videos_audios.bitrate, width, height, channels, sampling_rate " + ++ "from videos, videos_audios, videos_videos where videos.id = ? " + ++ "and videos.id = videos_audios.video_id and videos.id = videos_videos.video_id;"; ++ try { ++ var stmt = this.lms_db.prepare(video_data); ++ Rygel.LMS.Database.find_object("%d".printf(id), stmt); ++ video.bitrate = stmt.column_int(0) / 8; //convert bits per second into bytes per second ++ video.width = stmt.column_int(1); ++ video.height = stmt.column_int(2); ++ video.channels = stmt.column_int(3); ++ video.sample_freq = stmt.column_int(4); ++ } catch (DatabaseError e) { ++ warning ("Query failed: %s", e.message); ++ } ++ ++ return video; ++ } ++ ++ public AllVideos (string id, MediaContainer parent, string title, LMS.Database lms_db){ ++ base (id, ++ parent, ++ title, ++ lms_db, ++ AllVideos.SQL_ALL, ++ AllVideos.SQL_FIND_OBJECT, ++ AllVideos.SQL_COUNT, ++ AllVideos.SQL_ADDED, ++ AllVideos.SQL_REMOVED ++ ); ++ } ++} +diff --git a/src/plugins/lms/rygel-lms-artist.vala b/src/plugins/lms/rygel-lms-artist.vala +new file mode 100644 +index 0000000..31e9070 +--- /dev/null ++++ b/src/plugins/lms/rygel-lms-artist.vala +@@ -0,0 +1,75 @@ ++/* ++ * Copyright (C) 2013 Intel Corporation. ++ * ++ * Author: Jussi Kukkonen <jussi.kukkonen@intel.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 Rygel; ++using Sqlite; ++ ++public class Rygel.LMS.Artist : Rygel.LMS.CategoryContainer { ++ private static const string SQL_ALL_TEMPLATE = ++ "SELECT audio_albums.id, audio_albums.name " + ++ "FROM audio_albums " + ++ "WHERE audio_albums.artist_id = %s " + ++ "LIMIT ? OFFSET ?;"; ++ ++ private static const string SQL_COUNT_TEMPLATE = ++ "SELECT COUNT(audio_albums.id) " + ++ "FROM audio_albums " + ++ "WHERE audio_albums.artist_id = %s"; ++ ++ private static const string SQL_FIND_OBJECT_TEMPLATE = ++ "SELECT audio_albums.id, audio_albums.name " + ++ "FROM audio_albums " + ++ "WHERE audio_albums.id = ? AND audio_albums.artist_id = %s;"; ++ ++ private static string get_sql_all (string id) { ++ return (SQL_ALL_TEMPLATE.printf (id)); ++ } ++ private static string get_sql_find_object (string id) { ++ return (SQL_FIND_OBJECT_TEMPLATE.printf (id)); ++ } ++ private static string get_sql_count (string id) { ++ return (SQL_COUNT_TEMPLATE.printf (id)); ++ } ++ ++ protected override MediaObject? object_from_statement (Statement statement) { ++ var db_id = "%d".printf (statement.column_int (0)); ++ var title = statement.column_text (1); ++ return new LMS.Album (db_id, this, title, this.lms_db); ++ } ++ ++ public Artist (string id, ++ MediaContainer parent, ++ string title, ++ LMS.Database lms_db) { ++ ++ base (id, ++ parent, ++ title, ++ lms_db, ++ get_sql_all (id), ++ get_sql_find_object (id), ++ get_sql_count (id), ++ null, // LMS does not track adding or removing albums ++ null ++ ); ++ } ++} +diff --git a/src/plugins/lms/rygel-lms-artists.vala b/src/plugins/lms/rygel-lms-artists.vala +new file mode 100644 +index 0000000..a00b2ce +--- /dev/null ++++ b/src/plugins/lms/rygel-lms-artists.vala +@@ -0,0 +1,62 @@ ++/* ++ * Copyright (C) 2013 Intel Corporation. ++ * ++ * Author: Jussi Kukkonen <jussi.kukkonen@intel.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 Rygel; ++using Sqlite; ++ ++public class Rygel.LMS.Artists : Rygel.LMS.CategoryContainer { ++ private static const string SQL_ALL = ++ "SELECT audio_artists.id, audio_artists.name " + ++ "FROM audio_artists " + ++ "LIMIT ? OFFSET ?;"; ++ ++ private static const string SQL_COUNT = ++ "SELECT COUNT(audio_artists.id) " + ++ "FROM audio_artists;"; ++ ++ private static const string SQL_FIND_OBJECT = ++ "SELECT audio_artists.id, audio_artists.name " + ++ "FROM audio_artists " + ++ "WHERE audio_artists.id = ?;"; ++ ++ protected override MediaObject? object_from_statement (Statement statement) { ++ var db_id = "%d".printf (statement.column_int (0)); ++ var title = statement.column_text (1); ++ ++ return new LMS.Artist (db_id, this, title, this.lms_db); ++ } ++ ++ public Artists (string id, ++ MediaContainer parent, ++ string title, ++ LMS.Database lms_db) { ++ base (id, ++ parent, ++ title, ++ lms_db, ++ Artists.SQL_ALL, ++ Artists.SQL_FIND_OBJECT, ++ Artists.SQL_COUNT, ++ null, null ++ ); ++ } ++} +diff --git a/src/plugins/lms/rygel-lms-category-container.vala b/src/plugins/lms/rygel-lms-category-container.vala +new file mode 100644 +index 0000000..e5430d1 +--- /dev/null ++++ b/src/plugins/lms/rygel-lms-category-container.vala +@@ -0,0 +1,428 @@ ++/* ++ * Copyright (C) 2009,2010 Jens Georg <mail@jensge.org>, ++ * (C) 2013 Intel Corporation. ++ * ++ * Author: Jussi Kukkonen <jussi.kukkonen@intel.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 Rygel; ++using Gee; ++using Sqlite; ++ ++public errordomain Rygel.LMS.CategoryContainerError { ++ SQLITE_ERROR, ++ GENERAL_ERROR, ++ INVALID_TYPE, ++ UNSUPPORTED_SEARCH ++} ++ ++public abstract class Rygel.LMS.CategoryContainer : Rygel.MediaContainer, ++ Rygel.TrackableContainer, ++ Rygel.SearchableContainer { ++ public ArrayList<string> search_classes { get; set; } ++ ++ public unowned LMS.Database lms_db { get; construct; } ++ ++ public string db_id { get; construct; } ++ ++ public string sql_all { get; construct; } ++ public string sql_find_object { get; construct; } ++ public string sql_count { get; construct; } ++ public string sql_added { get; construct; } ++ public string sql_removed { get; construct; } ++ ++ protected Statement stmt_all; ++ protected Statement stmt_find_object; ++ protected Statement stmt_added; ++ protected Statement stmt_removed; ++ ++ protected string child_prefix; ++ protected string ref_prefix; ++ ++ protected abstract MediaObject? object_from_statement (Statement statement); ++ ++ /* TODO these should be abstract */ ++ protected virtual string get_sql_all_with_filter (string filter) { ++ return this.sql_all; ++ } ++ protected virtual string get_sql_count_with_filter (string filter) { ++ return this.sql_count; ++ } ++ ++ private static string? map_operand_to_column (string operand, ++ out string? collate = null, ++ bool for_sort = false) ++ throws Error { ++ string column = null; ++ bool use_collation = false; ++ ++ // TODO add all used aliases to sql queries ++ switch (operand) { ++ case "dc:title": ++ column = "title"; ++ use_collation = true; ++ break; ++ case "upnp:artist": ++ column = "artist"; ++ use_collation = true; ++ break; ++ case "dc:creator": ++ column = "creator"; ++ use_collation = true; ++ break; ++ default: ++ var message = "Unsupported column %s".printf (operand); ++ ++ throw new CategoryContainerError.UNSUPPORTED_SEARCH (message); ++ } ++ ++ if (use_collation) { ++ collate = "COLLATE CASEFOLD"; ++ } else { ++ collate = ""; ++ } ++ ++ return column; ++ } ++ ++ private static string? relational_expression_to_sql ++ (RelationalExpression exp, ++ GLib.ValueArray args) ++ throws Error { ++ GLib.Value? v = null; ++ string collate = null; ++ ++ string column = CategoryContainer.map_operand_to_column (exp.operand1, ++ out collate); ++ SqlOperator operator; ++ ++ switch (exp.op) { ++ case GUPnP.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 GUPnP.SearchCriteriaOp.EQ: ++ case GUPnP.SearchCriteriaOp.NEQ: ++ case GUPnP.SearchCriteriaOp.LESS: ++ case GUPnP.SearchCriteriaOp.LEQ: ++ case GUPnP.SearchCriteriaOp.GREATER: ++ case GUPnP.SearchCriteriaOp.GEQ: ++ v = exp.operand2; ++ operator = new SqlOperator.from_search_criteria_op ++ (exp.op, column, collate); ++ break; ++ case GUPnP.SearchCriteriaOp.CONTAINS: ++ operator = new SqlFunction ("contains", column); ++ v = exp.operand2; ++ break; ++ case GUPnP.SearchCriteriaOp.DOES_NOT_CONTAIN: ++ operator = new SqlFunction ("NOT contains", column); ++ v = exp.operand2; ++ break; ++ case GUPnP.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 static string logical_expression_to_sql ++ (LogicalExpression expression, ++ GLib.ValueArray args) ++ throws Error { ++ string left_sql_string = CategoryContainer.search_expression_to_sql ++ (expression.operand1, ++ args); ++ string right_sql_string = CategoryContainer.search_expression_to_sql ++ (expression.operand2, ++ args); ++ unowned 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 static string? search_expression_to_sql ++ (SearchExpression? expression, ++ GLib.ValueArray args) ++ throws Error { ++ if (expression == null) { ++ return ""; ++ } ++ ++ if (expression is LogicalExpression) { ++ return CategoryContainer.logical_expression_to_sql ++ (expression as LogicalExpression, args); ++ } else { ++ return CategoryContainer.relational_expression_to_sql ++ (expression as RelationalExpression, ++ args); ++ } ++ } ++ ++ protected virtual uint get_child_count_with_filter (string where_filter, ++ ValueArray args) ++ { ++ var query = this.get_sql_count_with_filter (where_filter); ++ try { ++ var stmt = this.lms_db.prepare_and_init (query, args.values); ++ if (stmt.step () != Sqlite.ROW) { ++ return 0; ++ } ++ return stmt.column_int (0); ++ } catch (DatabaseError e) { ++ warning ("Query failed: %s", e.message); ++ return 0; ++ } ++ } ++ ++ protected virtual MediaObjects? get_children_with_filter (string where_filter, ++ ValueArray args, ++ string sort_criteria, ++ uint offset, ++ uint max_count) { ++ var children = new MediaObjects (); ++ GLib.Value v = max_count; ++ args.append (v); ++ v = offset; ++ args.append (v); ++ ++ var query = this.get_sql_all_with_filter (where_filter); ++ try { ++ var stmt = this.lms_db.prepare_and_init (query, args.values); ++ while (Database.get_children_step (stmt)) { ++ children.add (this.object_from_statement (stmt)); ++ } ++ } catch (DatabaseError e) { ++ warning ("Query failed: %s", e.message); ++ } ++ ++ return children; ++ } ++ ++ public async MediaObjects? search (SearchExpression? expression, ++ uint offset, ++ uint max_count, ++ out uint total_matches, ++ string sort_criteria, ++ Cancellable? cancellable) ++ throws Error { ++ debug ("search()"); ++ try { ++ var args = new GLib.ValueArray (0); ++ var filter = CategoryContainer.search_expression_to_sql (expression, ++ args); ++ total_matches = this.get_child_count_with_filter (filter, args); ++ ++ if (expression != null) { ++ debug (" Original search: %s", expression.to_string ()); ++ debug (" Parsed search expression: %s", filter); ++ debug (" Filtered cild count is %u", total_matches); ++ } ++ ++ if (max_count == 0) { ++ max_count = uint.MAX; ++ } ++ return this.get_children_with_filter (filter, ++ args, ++ sort_criteria, ++ offset, ++ max_count); ++ } catch (Error e) { ++ debug (" Falling back to simple_search(): %s", e.message); ++ return yield this.simple_search (expression, ++ offset, ++ max_count, ++ out total_matches, ++ sort_criteria, ++ cancellable); ++ } ++ } ++ ++ public async override MediaObjects? get_children (uint offset, ++ uint max_count, ++ string sort_criteria, ++ Cancellable? cancellable) ++ throws Error { ++ MediaObjects retval = new MediaObjects (); ++ ++ Database.get_children_init (this.stmt_all, ++ offset, ++ max_count, ++ sort_criteria); ++ while (Database.get_children_step (this.stmt_all)) { ++ retval.add (this.object_from_statement (this.stmt_all)); ++ } ++ ++ return retval; ++ } ++ ++ public async override MediaObject? find_object (string id, ++ Cancellable? cancellable) ++ throws Error { ++ if (!id.has_prefix (this.child_prefix)) { ++ /* can't match anything in this container */ ++ return null; ++ } ++ ++ MediaObject object = null; ++ ++ /* remove parent section from id */ ++ var real_id = id.substring (this.child_prefix.length); ++ /* remove grandchildren from id */ ++ var index = real_id.index_of_char (':'); ++ if (index > 0) { ++ real_id = real_id.slice (0, index); ++ } ++ ++ try { ++ Database.find_object (real_id, this.stmt_find_object); ++ var child = this.object_from_statement (this.stmt_find_object); ++ if (index < 0) { ++ object = child; ++ } else { ++ /* try grandchildren */ ++ var container = child as CategoryContainer; ++ object = yield container.find_object (id, cancellable); ++ ++ /* tell object to keep a reference to the parent -- ++ * otherwise parent is freed before object is serialized */ ++ object.parent_ref = object.parent; ++ } ++ } catch (DatabaseError e) { ++ debug ("find_object %s in %s: %s", id, this.id, e.message); ++ /* Happens e.g. if id is not an integer */ ++ } ++ ++ return object; ++ } ++ ++ protected string build_child_id (int db_id) { ++ return "%s%d".printf (this.child_prefix, db_id); ++ } ++ ++ protected string build_reference_id (int db_id) { ++ return "%s%d".printf (this.ref_prefix, db_id); ++ } ++ ++ protected async void add_child (MediaObject object) { ++ } ++ ++ protected async void remove_child (MediaObject object) { ++ } ++ ++ private void on_db_updated(uint64 old_id, uint64 new_id) { ++ try { ++ var stmt_count = this.lms_db.prepare (this.sql_count); ++ ++ if (stmt_count.step () == Sqlite.ROW) { ++ this.child_count = stmt_count.column_int (0); ++ } ++ ++ Database.get_children_with_update_id_init (this.stmt_added, ++ old_id, ++ new_id); ++ while (Database.get_children_step (this.stmt_added)) { ++ this.add_child_tracked.begin(this.object_from_statement (this.stmt_added)); ++ } ++ ++ Database.get_children_with_update_id_init (this.stmt_removed, ++ old_id, ++ new_id); ++ while (Database.get_children_step (this.stmt_removed)) { ++ this.remove_child_tracked.begin(this.object_from_statement (this.stmt_removed)); ++ } ++ ++ } catch (DatabaseError e) { ++ warning ("Can't perform container update: %s", e.message); ++ } ++ ++ } ++ ++ public CategoryContainer (string db_id, ++ MediaContainer parent, ++ string title, ++ LMS.Database lms_db, ++ string sql_all, ++ string sql_find_object, ++ string sql_count, ++ string? sql_added, ++ string? sql_removed ++ ) { ++ Object (id : "%s:%s".printf (parent.id, db_id), ++ db_id : db_id, ++ parent : parent, ++ title : title, ++ lms_db : lms_db, ++ sql_all : sql_all, ++ sql_find_object : sql_find_object, ++ sql_count : sql_count, ++ sql_added : sql_added, ++ sql_removed: sql_removed ++ ); ++ } ++ ++ construct { ++ this.search_classes = new ArrayList<string> (); ++ ++ this.child_prefix = "%s:".printf (this.id); ++ ++ var index = this.id.index_of_char (':'); ++ this.ref_prefix = this.id.slice (0, index) + ":all:"; ++ ++ try { ++ this.stmt_all = this.lms_db.prepare (this.sql_all); ++ this.stmt_find_object = this.lms_db.prepare (this.sql_find_object); ++ var stmt_count = this.lms_db.prepare (this.sql_count); ++ ++ if (stmt_count.step () == Sqlite.ROW) { ++ this.child_count = stmt_count.column_int (0); ++ } ++ // some container implementations don't have a reasonable way to provide ++ // id-based statements to fetch added or removed items ++ if (this.sql_added != null && this.sql_removed != null) { ++ this.stmt_added = this.lms_db.prepare (this.sql_added); ++ this.stmt_removed = this.lms_db.prepare (this.sql_removed); ++ lms_db.db_updated.connect(this.on_db_updated); ++ } ++ } catch (DatabaseError e) { ++ warning ("Container %s: %s", this.title, e.message); ++ } ++ ++ } ++} +diff --git a/src/plugins/lms/rygel-lms-collate.c b/src/plugins/lms/rygel-lms-collate.c +new file mode 100644 +index 0000000..8eee80b +--- /dev/null ++++ b/src/plugins/lms/rygel-lms-collate.c +@@ -0,0 +1,49 @@ ++/* ++ * Copyright (C) 2012 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. ++ */ ++ ++#include <glib.h> ++ ++#ifdef HAVE_UNISTRING ++# include <unistr.h> ++#endif ++ ++gint rygel_lms_utf8_collate_str (const char *a, gsize alen, ++ const char *b, gsize blen) ++{ ++ char *a_str, *b_str; ++ gint result; ++ ++ /* Make sure the passed strings are null terminated */ ++ a_str = g_strndup (a, alen); ++ b_str = g_strndup (b, blen); ++ ++#ifdef HAVE_UNISTRING ++ result = u8_strcoll (a_str, b_str); ++#else ++ return g_utf8_collate (a_str, b_str); ++#endif ++ ++ g_free (a_str); ++ g_free (b_str); ++ ++ return result; ++} +diff --git a/src/plugins/lms/rygel-lms-database.vala b/src/plugins/lms/rygel-lms-database.vala +new file mode 100644 +index 0000000..e898d66 +--- /dev/null ++++ b/src/plugins/lms/rygel-lms-database.vala +@@ -0,0 +1,294 @@ ++/* ++ * Copyright (C) 2009,2011 Jens Georg <mail@jensge.org>, ++ * (C) 2013 Intel Corporation. ++ * ++ * Author: Jussi Kukkonen <jussi.kukkonen@intel.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 Rygel; ++using Gee; ++using Sqlite; ++ ++public errordomain Rygel.LMS.DatabaseError { ++ OPEN, ++ PREPARE, ++ BIND, ++ STEP, ++ NOT_FOUND ++} ++ ++namespace Rygel.LMS { ++ extern static int utf8_collate_str (uint8[] a, uint8[] b); ++} ++ ++public class Rygel.LMS.Database { ++ ++ public signal void db_updated(uint64 old_update_id, uint64 new_update_id); ++ ++ private Sqlite.Database db; ++ private LMS.DBus lms_proxy; ++ private uint64 update_id; ++ ++ /** ++ * 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[0].to_text () == null || ++ 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; ++ ++ return LMS.utf8_collate_str (_a, _b); ++ } ++ ++ public Database () throws DatabaseError { ++ string db_path; ++ try { ++ lms_proxy = Bus.get_proxy_sync (BusType.SESSION, ++ "org.lightmediascanner", ++ "/org/lightmediascanner/Scanner1"); ++ db_path = lms_proxy.data_base_path; ++ debug ("Got db path %s from LMS over dbus", db_path); ++ update_id = lms_proxy.update_id; ++ debug ("Got updated id %lld from LMS over dbus", update_id); ++ lms_proxy.g_properties_changed.connect (this.on_lms_properties_changed); ++ ++ } catch (IOError e) { ++ warning("Couldn't get LMS Dbus proxy: %s", e.message); ++ db_path = Environment.get_user_config_dir() + ++ "/lightmediascannerd/db.sqlite3"; ++ debug ("Using default sqlite database location %s", db_path); ++ } ++ ++ Sqlite.Database.open (db_path, out this.db); ++ if (this.db.errcode () != Sqlite.OK) { ++ throw new DatabaseError.OPEN ("Failed to open '%s': %d", ++ db_path, ++ this.db.errcode () ); ++ } ++ ++ this.db.create_function ("contains", ++ 2, ++ Sqlite.UTF8, ++ null, ++ LMS.Database.utf8_contains, ++ null, ++ null); ++ ++ this.db.create_collation ("CASEFOLD", ++ Sqlite.UTF8, ++ LMS.Database.utf8_collate); ++ ++ } ++ ++ private void on_lms_properties_changed (DBusProxy lms_proxy, ++ Variant changed, ++ string[] invalidated) { ++ if (!changed.get_type().equal (VariantType.VARDICT)) { ++ return; ++ } ++ ++ foreach (var changed_prop in changed) { ++ var key = (string) changed_prop.get_child_value (0); ++ var value = changed_prop.get_child_value (1).get_child_value (0); ++ ++ debug ("LMS property %s changed value to %s", key, value.print(true)); ++ ++ switch (key) { ++ case "UpdateID": ++ db_updated(update_id, (uint64)value); ++ update_id = (uint64)value; ++ break; ++ } ++ } ++ } ++ ++ ++ public Statement prepare (string query_string) throws DatabaseError { ++ Statement statement; ++ ++ var err = this.db.prepare_v2 (query_string, -1, out statement); ++ if (err != Sqlite.OK) ++ throw new DatabaseError.PREPARE ("Unable to create statement '%s': %d", ++ query_string, ++ err); ++ return statement; ++ } ++ ++ ++ public Statement prepare_and_init (string query, ++ GLib.Value[]? arguments) ++ throws DatabaseError { ++ ++ Statement statement; ++ ++ var err = this.db.prepare_v2 (query, -1, out statement); ++ if (err != Sqlite.OK) ++ throw new DatabaseError.PREPARE ("Unable to create statement '%s': %d", ++ query, ++ err); ++ ++ for (var i = 1; i <= arguments.length; ++i) { ++ int sqlite_err; ++ unowned GLib.Value current_value = arguments[i - 1]; ++ ++ if (current_value.holds (typeof (int))) { ++ sqlite_err = statement.bind_int (i, current_value.get_int ()); ++ if (sqlite_err != Sqlite.OK) ++ throw new DatabaseError.BIND("Unable to bind value %d", ++ sqlite_err); ++ } else if (current_value.holds (typeof (int64))) { ++ sqlite_err = statement.bind_int64 (i, current_value.get_int64 ()); ++ if (sqlite_err != Sqlite.OK) ++ throw new DatabaseError.BIND("Unable to bind value %d", ++ sqlite_err); ++ } else if (current_value.holds (typeof (uint64))) { ++ sqlite_err = statement.bind_int64 (i, (int64) current_value.get_uint64 ()); ++ if (sqlite_err != Sqlite.OK) ++ throw new DatabaseError.BIND("Unable to bind value %d", ++ sqlite_err); ++ } else if (current_value.holds (typeof (long))) { ++ sqlite_err = statement.bind_int64 (i, current_value.get_long ()); ++ if (sqlite_err != Sqlite.OK) ++ throw new DatabaseError.BIND("Unable to bind value %d", ++ sqlite_err); ++ } else if (current_value.holds (typeof (uint))) { ++ sqlite_err = statement.bind_int64 (i, current_value.get_uint ()); ++ if (sqlite_err != Sqlite.OK) ++ throw new DatabaseError.BIND("Unable to bind value %d", ++ sqlite_err); ++ } else if (current_value.holds (typeof (string))) { ++ sqlite_err = statement.bind_text (i, current_value.get_string ()); ++ if (sqlite_err != Sqlite.OK) ++ throw new DatabaseError.BIND("Unable to bind value %d", ++ sqlite_err); ++ } else if (current_value.holds (typeof (void *))) { ++ if (current_value.peek_pointer () == null) { ++ sqlite_err = statement.bind_null (i); ++ if (sqlite_err != Sqlite.OK) ++ throw new DatabaseError.BIND("Unable to bind value %d", ++ sqlite_err); ++ } else { ++ assert_not_reached (); ++ } ++ } else { ++ var type = current_value.type (); ++ warning (_("Unsupported type %s"), type.name ()); ++ assert_not_reached (); ++ } ++ } ++ ++ return statement; ++ } ++ ++ public static void find_object(string id, Statement stmt) throws DatabaseError { ++ ++ (void) stmt.reset(); ++ ++ int integer_id = int.parse(id); ++ int sqlite_err = stmt.bind_int(1, integer_id); ++ if (sqlite_err != Sqlite.OK) ++ throw new DatabaseError.BIND("Unable to bind id %d", sqlite_err); ++ ++ sqlite_err = stmt.step(); ++ if (sqlite_err != Sqlite.ROW) ++ throw new DatabaseError.STEP("Unable to find id %s", id); ++ } ++ ++ public static void get_children_init (Statement stmt, ++ uint offset, uint max_count, string sort_criteria) throws DatabaseError { ++ ++ int sqlite_err; ++ ++ (void) stmt.reset(); ++ ++ sqlite_err = stmt.bind_int(1, (int) max_count); ++ if (sqlite_err != Sqlite.OK) ++ throw new DatabaseError.BIND("Unable to bind max_count %d", ++ sqlite_err); ++ ++ sqlite_err = stmt.bind_int(2, (int) offset); ++ if (sqlite_err != Sqlite.OK) ++ throw new DatabaseError.BIND("Unable to bind offset %d", ++ sqlite_err); ++ } ++ ++ public static void get_children_with_update_id_init (Statement stmt, ++ uint64 old_id, uint64 new_id) throws DatabaseError { ++ ++ int sqlite_err; ++ ++ (void) stmt.reset(); ++ ++ if (new_id < old_id) // id value wrapped over ++ sqlite_err = stmt.bind_int64(1, 0); ++ else ++ sqlite_err = stmt.bind_int64(1, (int64)old_id); ++ if (sqlite_err != Sqlite.OK) ++ throw new DatabaseError.BIND("Unable to bind old_id %d", ++ sqlite_err); ++ ++ sqlite_err = stmt.bind_int64(2, (int64)new_id); ++ if (sqlite_err != Sqlite.OK) ++ throw new DatabaseError.BIND("Unable to bind new_id %d", ++ sqlite_err); ++ } ++ ++ public static bool get_children_step(Statement stmt) throws DatabaseError { ++ ++ bool retval; ++ int sqlite_err; ++ ++ sqlite_err = stmt.step(); ++ retval = sqlite_err == Sqlite.ROW; ++ ++ if (!retval && (sqlite_err != Sqlite.DONE)) ++ throw new DatabaseError.STEP("Error iterating through rows %d", ++ sqlite_err); ++ ++ return retval; ++ } ++} +diff --git a/src/plugins/lms/rygel-lms-dbus-interfaces.vala b/src/plugins/lms/rygel-lms-dbus-interfaces.vala +new file mode 100644 +index 0000000..13f00cb +--- /dev/null ++++ b/src/plugins/lms/rygel-lms-dbus-interfaces.vala +@@ -0,0 +1,30 @@ ++/* ++ * Copyright (C) 2014 Intel Corporation. ++ * ++ * Author: Alexander Kanavin <alex.kanavin@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. ++ */ ++ ++[DBus (name = "org.lightmediascanner.Scanner1")] ++interface Rygel.LMS.DBus : DBusProxy { ++ public abstract string data_base_path { owned get; } ++ [DBus (name = "UpdateID")] ++ public abstract uint64 update_id { get; } ++ ++ //TODO: add all the other API items which are currently unused ++} +\ No newline at end of file +diff --git a/src/plugins/lms/rygel-lms-image-root.vala b/src/plugins/lms/rygel-lms-image-root.vala +new file mode 100644 +index 0000000..466bbe2 +--- /dev/null ++++ b/src/plugins/lms/rygel-lms-image-root.vala +@@ -0,0 +1,35 @@ ++/* ++ * Copyright (C) 2013 Intel Corporation. ++ * ++ * Author: Jussi Kukkonen <jussi.kukkonen@intel.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 Rygel; ++ ++public class Rygel.LMS.ImageRoot : Rygel.SimpleContainer { ++ public ImageRoot (string id, ++ MediaContainer parent, ++ string title, ++ LMS.Database lms_db) { ++ base (id, parent, title); ++ ++ this.add_child_container (new AllImages (this, lms_db)); ++ this.add_child_container (new ImageYears (this, lms_db)); ++ } ++} +diff --git a/src/plugins/lms/rygel-lms-image-year.vala b/src/plugins/lms/rygel-lms-image-year.vala +new file mode 100644 +index 0000000..a7768f0 +--- /dev/null ++++ b/src/plugins/lms/rygel-lms-image-year.vala +@@ -0,0 +1,114 @@ ++/* ++ * Copyright (C) 2013 Intel Corporation. ++ * ++ * Author: Jussi Kukkonen <jussi.kukkonen@intel.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 Rygel; ++using Sqlite; ++ ++public class Rygel.LMS.ImageYear : Rygel.LMS.CategoryContainer { ++ private static const string SQL_ALL_TEMPLATE = ++ "SELECT images.id, title, artist, date, width, height, path, size, dlna_profile, dlna_mime, strftime('%Y', date, 'unixepoch') as year " + ++ "FROM images, files " + ++ "WHERE dtime = 0 AND images.id = files.id AND year = '%s' " + ++ "LIMIT ? OFFSET ?;"; ++ ++ private static const string SQL_COUNT_TEMPLATE = ++ "SELECT count(images.id), strftime('%Y', date, 'unixepoch') as year " + ++ "FROM images, files " + ++ "WHERE dtime = 0 AND images.id = files.id AND year = '%s';"; ++ ++ private static const string SQL_FIND_OBJECT_TEMPLATE = ++ "SELECT images.id, title, artist, date, width, height, path, size, dlna_profile, dlna_mime, strftime('%Y', date, 'unixepoch') as year " + ++ "FROM images, files " + ++ "WHERE dtime = 0 AND files.id = ? AND images.id = files.id AND year = '%s';"; ++ ++ private static const string SQL_ADDED_TEMPLATE = ++ "SELECT images.id, title, artist, date, width, height, path, size, dlna_profile, dlna_mime, strftime('%Y', date, 'unixepoch') as year " + ++ "FROM images, files " + ++ "WHERE dtime = 0 AND images.id = files.id AND year = '%s' " + ++ "AND update_id > ? AND update_id <= ?;"; ++ ++ private static const string SQL_REMOVED_TEMPLATE = ++ "SELECT images.id, title, artist, date, width, height, path, size, dlna_profile, dlna_mime, strftime('%Y', date, 'unixepoch') as year " + ++ "FROM images, files " + ++ "WHERE dtime <> 0 AND images.id = files.id AND year = '%s' " + ++ "AND update_id > ? AND update_id <= ?;"; ++ ++ protected override MediaObject? object_from_statement (Statement statement) { ++ var id = statement.column_int(0); ++ var path = statement.column_text(6); ++ var mime_type = statement.column_text(9); ++ ++ if (mime_type == null || mime_type.length == 0){ ++ /* TODO is this correct? */ ++ debug ("Image item %d (%s) has no MIME type", ++ id, ++ path); ++ } ++ ++ var title = statement.column_text(1); ++ var image = new ImageItem(this.build_child_id (id), this, title); ++ image.ref_id = this.build_reference_id (id); ++ image.creator = statement.column_text(2); ++ TimeVal tv = { (long) statement.column_int(3), (long) 0 }; ++ image.date = tv.to_iso8601 (); ++ image.width = statement.column_int(4); ++ image.height = statement.column_int(5); ++ image.size = statement.column_int(7); ++ image.mime_type = mime_type; ++ image.dlna_profile = statement.column_text(8); ++ File file = File.new_for_path(path); ++ image.add_uri (file.get_uri ()); ++ ++ return image; ++ } ++ ++ private static string get_sql_all (string year) { ++ return (SQL_ALL_TEMPLATE.printf (year)); ++ } ++ private static string get_sql_find_object (string year) { ++ return (SQL_FIND_OBJECT_TEMPLATE.printf (year)); ++ } ++ private static string get_sql_count (string year) { ++ return (SQL_COUNT_TEMPLATE.printf (year)); ++ } ++ private static string get_sql_added (string year) { ++ return (SQL_ADDED_TEMPLATE.printf (year)); ++ } ++ private static string get_sql_removed (string year) { ++ return (SQL_REMOVED_TEMPLATE.printf (year)); ++ } ++ ++ public ImageYear (MediaContainer parent, ++ string year, ++ LMS.Database lms_db) { ++ base ("%s".printf (year), ++ parent, ++ year, ++ lms_db, ++ get_sql_all (year), ++ get_sql_find_object (year), ++ get_sql_count (year), ++ get_sql_added (year), ++ get_sql_removed (year) ++ ); ++ } ++} +diff --git a/src/plugins/lms/rygel-lms-image-years.vala b/src/plugins/lms/rygel-lms-image-years.vala +new file mode 100644 +index 0000000..636f4d1 +--- /dev/null ++++ b/src/plugins/lms/rygel-lms-image-years.vala +@@ -0,0 +1,59 @@ ++/* ++ * Copyright (C) 2013 Intel Corporation. ++ * ++ * Author: Jussi Kukkonen <jussi.kukkonen@intel.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 Rygel; ++using Sqlite; ++ ++public class Rygel.LMS.ImageYears : Rygel.LMS.CategoryContainer { ++ private static const string SQL_ALL = ++ "SELECT DISTINCT(strftime('%Y', images.date, 'unixepoch')) as year " + ++ "FROM images " + ++ "LIMIT ? OFFSET ?;"; ++ ++ private static const string SQL_COUNT = ++ "SELECT COUNT(DISTINCT(strftime('%Y', images.date, 'unixepoch'))) " + ++ "FROM images;"; ++ ++ /* actually returns multiple times the same result (because no DISTINCT) */ ++ /* Casting the year is a workaround so we can keep using ++ * Database.find_object() without making the argument a variant or something like it*/ ++ private static const string SQL_FIND_OBJECT = ++ "SELECT strftime('%Y', images.date, 'unixepoch') as year " + ++ "FROM images " + ++ "WHERE year = CAST(? AS TEXT)"; ++ ++ protected override MediaObject? object_from_statement (Statement statement) { ++ return new LMS.ImageYear (this, statement.column_text (0), this.lms_db); ++ } ++ ++ public ImageYears (MediaContainer parent, LMS.Database lms_db) { ++ base ("years", ++ parent, ++ _("Years"), ++ lms_db, ++ ImageYears.SQL_ALL, ++ ImageYears.SQL_FIND_OBJECT, ++ ImageYears.SQL_COUNT, ++ null, null ++ ); ++ } ++} +diff --git a/src/plugins/lms/rygel-lms-music-root.vala b/src/plugins/lms/rygel-lms-music-root.vala +new file mode 100644 +index 0000000..7b1eb0f +--- /dev/null ++++ b/src/plugins/lms/rygel-lms-music-root.vala +@@ -0,0 +1,36 @@ ++/* ++ * Copyright (C) 2013 Intel Corporation. ++ * ++ * Author: Jussi Kukkonen <jussi.kukkonen@intel.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 Rygel; ++ ++public class Rygel.LMS.MusicRoot : Rygel.SimpleContainer { ++ public MusicRoot (string id, ++ MediaContainer parent, ++ string title, ++ LMS.Database lms_db) { ++ base (id, parent, title); ++ ++ this.add_child_container (new AllMusic (this, lms_db)); ++ this.add_child_container (new Artists ("artists", this, _("Artists"), lms_db)); ++ this.add_child_container (new Albums (this, lms_db)); ++ } ++} +diff --git a/src/plugins/lms/rygel-lms-plugin-factory.vala b/src/plugins/lms/rygel-lms-plugin-factory.vala +new file mode 100644 +index 0000000..9fa8ccd +--- /dev/null ++++ b/src/plugins/lms/rygel-lms-plugin-factory.vala +@@ -0,0 +1,40 @@ ++/* ++ * Copyright (C) 2013 Intel Corporation. ++ * ++ * Author: Jussi Kukkonen <jussi.kukkonen@intel.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 Rygel; ++ ++private Rygel.LMS.PluginFactory plugin_factory; ++ ++public void module_init(PluginLoader loader) { ++ plugin_factory = new Rygel.LMS.PluginFactory(loader); ++} ++ ++public class Rygel.LMS.PluginFactory { ++ ++ PluginLoader loader; ++ ++ public PluginFactory(PluginLoader loader) { ++ this.loader = loader; ++ this.loader.add_plugin(new LMS.Plugin()); ++ } ++ ++} +diff --git a/src/plugins/lms/rygel-lms-plugin.vala b/src/plugins/lms/rygel-lms-plugin.vala +new file mode 100644 +index 0000000..8bf1284 +--- /dev/null ++++ b/src/plugins/lms/rygel-lms-plugin.vala +@@ -0,0 +1,35 @@ ++/* ++ * Copyright (C) 2013 Intel Corporation. ++ * ++ * Author: Jussi Kukkonen <jussi.kukkonen@intel.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 Rygel; ++ ++public class Rygel.LMS.Plugin : Rygel.MediaServerPlugin { ++ public const string NAME = "LMS"; ++ ++ private static RootContainer root; ++ ++ public Plugin() { ++ if (root == null) ++ root = new RootContainer(); ++ base(root, Plugin.NAME, null, PluginCapabilities.TRACK_CHANGES); ++ } ++} +diff --git a/src/plugins/lms/rygel-lms-root-container.vala b/src/plugins/lms/rygel-lms-root-container.vala +new file mode 100644 +index 0000000..1623fa3 +--- /dev/null ++++ b/src/plugins/lms/rygel-lms-root-container.vala +@@ -0,0 +1,58 @@ ++/* ++ * Copyright (C) 2013 Intel Corporation. ++ * ++ * Author: Jussi Kukkonen <jussi.kukkonen@intel.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 Rygel; ++ ++using Sqlite; ++ ++public class Rygel.LMS.RootContainer : Rygel.SimpleContainer { ++ ++ private LMS.Database lms_db = null; ++ ++ public RootContainer() { ++ var config = MetaConfig.get_default (); ++ ++ var title = _("Shared media"); ++ try { ++ title = config.get_string ("LightMediaScanner", "title"); ++ } catch (GLib.Error error) {} ++ ++ base.root(title); ++ ++ try { ++ this.lms_db = new LMS.Database (); ++ ++ this.add_child_container (new MusicRoot ("music", this, _("Music"), this.lms_db)); ++ this.add_child_container (new AllVideos ("all-videos", this, _("Videos"), this.lms_db)); ++ this.add_child_container (new ImageRoot ("images", this, _("Pictures"), this.lms_db)); ++ ++ } catch (DatabaseError e) { ++ warning ("%s\n", e.message); ++ ++ /* TODO if db does not exist we should ++ wait for it to be created and then add folders. Best to wait for the ++ LMS notification API. */ ++ } ++ ++ } ++ ++} +diff --git a/src/plugins/lms/rygel-lms-sql-function.vala b/src/plugins/lms/rygel-lms-sql-function.vala +new file mode 100644 +index 0000000..e8580cc +--- /dev/null ++++ b/src/plugins/lms/rygel-lms-sql-function.vala +@@ -0,0 +1,31 @@ ++/* ++ * 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.LMS.SqlFunction : SqlOperator { ++ public SqlFunction (string name, string arg) { ++ base (name, arg); ++ } ++ ++ public override string to_string () { ++ return "%s(%s,?)".printf (name, arg); ++ } ++} +diff --git a/src/plugins/lms/rygel-lms-sql-operator.vala b/src/plugins/lms/rygel-lms-sql-operator.vala +new file mode 100644 +index 0000000..fc4e907 +--- /dev/null ++++ b/src/plugins/lms/rygel-lms-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.LMS.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); ++ } ++} ++ ++ +-- +1.7.10.4 + |