From ccff700f30ec4dedca5611fd6cf6d0cee7ffc9db Mon Sep 17 00:00:00 2001 From: Alexander Kanavin Date: Mon, 5 May 2014 16:45:27 +0300 Subject: [PATCH] lms plugin: add trackable container support (based on lms update id signal) --- src/plugins/lms/rygel-lms-album.vala | 37 +++++++++++- src/plugins/lms/rygel-lms-albums.vala | 3 +- src/plugins/lms/rygel-lms-all-images.vala | 17 +++++- src/plugins/lms/rygel-lms-all-music.vala | 39 ++++++++++++- src/plugins/lms/rygel-lms-all-videos.vala | 17 +++++- src/plugins/lms/rygel-lms-artist.vala | 5 +- src/plugins/lms/rygel-lms-artists.vala | 4 +- src/plugins/lms/rygel-lms-category-container.vala | 56 +++++++++++++++++- src/plugins/lms/rygel-lms-database.vala | 70 ++++++++++++++++++++++- src/plugins/lms/rygel-lms-dbus-interfaces.vala | 4 +- src/plugins/lms/rygel-lms-image-year.vala | 23 +++++++- src/plugins/lms/rygel-lms-image-years.vala | 4 +- src/plugins/lms/rygel-lms-root-container.vala | 20 +------ 13 files changed, 269 insertions(+), 30 deletions(-) diff --git a/src/plugins/lms/rygel-lms-album.vala b/src/plugins/lms/rygel-lms-album.vala index 3c23dc3..4fea17a 100644 --- a/src/plugins/lms/rygel-lms-album.vala +++ b/src/plugins/lms/rygel-lms-album.vala @@ -65,6 +65,32 @@ public class Rygel.LMS.Album : Rygel.LMS.CategoryContainer { "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); @@ -106,6 +132,12 @@ public class Rygel.LMS.Album : Rygel.LMS.CategoryContainer { 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) { @@ -133,6 +165,9 @@ public class Rygel.LMS.Album : Rygel.LMS.CategoryContainer { lms_db, get_sql_all (db_id), get_sql_find_object (db_id), - get_sql_count (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 index 4f365d5..309a352 100644 --- a/src/plugins/lms/rygel-lms-albums.vala +++ b/src/plugins/lms/rygel-lms-albums.vala @@ -169,6 +169,7 @@ public class Rygel.LMS.Albums : Rygel.LMS.CategoryContainer { lms_db, Albums.SQL_ALL, Albums.SQL_FIND_OBJECT, - Albums.SQL_COUNT); + 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 index 1bf219d..0b54c7f 100644 --- a/src/plugins/lms/rygel-lms-all-images.vala +++ b/src/plugins/lms/rygel-lms-all-images.vala @@ -40,6 +40,18 @@ public class Rygel.LMS.AllImages : Rygel.LMS.CategoryContainer { "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); @@ -75,6 +87,9 @@ public class Rygel.LMS.AllImages : Rygel.LMS.CategoryContainer { lms_db, AllImages.SQL_ALL, AllImages.SQL_FIND_OBJECT, - AllImages.SQL_COUNT); + 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 index 1af8c26..2a7226f 100644 --- a/src/plugins/lms/rygel-lms-all-music.vala +++ b/src/plugins/lms/rygel-lms-all-music.vala @@ -70,6 +70,40 @@ public class Rygel.LMS.AllMusic : Rygel.LMS.CategoryContainer { "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; @@ -127,6 +161,9 @@ public class Rygel.LMS.AllMusic : Rygel.LMS.CategoryContainer { lms_db, AllMusic.SQL_ALL_TEMPLATE.printf (""), AllMusic.SQL_FIND_OBJECT, - AllMusic.SQL_COUNT); + 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 index 76084ae..dbde0db 100644 --- a/src/plugins/lms/rygel-lms-all-videos.vala +++ b/src/plugins/lms/rygel-lms-all-videos.vala @@ -40,6 +40,18 @@ public class Rygel.LMS.AllVideos : Rygel.LMS.CategoryContainer { "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); @@ -103,6 +115,9 @@ public class Rygel.LMS.AllVideos : Rygel.LMS.CategoryContainer { lms_db, AllVideos.SQL_ALL, AllVideos.SQL_FIND_OBJECT, - AllVideos.SQL_COUNT); + 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 index bcabda3..31e9070 100644 --- a/src/plugins/lms/rygel-lms-artist.vala +++ b/src/plugins/lms/rygel-lms-artist.vala @@ -67,6 +67,9 @@ public class Rygel.LMS.Artist : Rygel.LMS.CategoryContainer { lms_db, get_sql_all (id), get_sql_find_object (id), - get_sql_count (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 index ebec2c3..a00b2ce 100644 --- a/src/plugins/lms/rygel-lms-artists.vala +++ b/src/plugins/lms/rygel-lms-artists.vala @@ -55,6 +55,8 @@ public class Rygel.LMS.Artists : Rygel.LMS.CategoryContainer { lms_db, Artists.SQL_ALL, Artists.SQL_FIND_OBJECT, - Artists.SQL_COUNT); + 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 index 5b4d7ba..e5430d1 100644 --- a/src/plugins/lms/rygel-lms-category-container.vala +++ b/src/plugins/lms/rygel-lms-category-container.vala @@ -33,6 +33,7 @@ public errordomain Rygel.LMS.CategoryContainerError { } public abstract class Rygel.LMS.CategoryContainer : Rygel.MediaContainer, + Rygel.TrackableContainer, Rygel.SearchableContainer { public ArrayList search_classes { get; set; } @@ -43,9 +44,13 @@ public abstract class Rygel.LMS.CategoryContainer : Rygel.MediaContainer, 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; @@ -335,13 +340,50 @@ public abstract class Rygel.LMS.CategoryContainer : Rygel.MediaContainer, 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_count, + string? sql_added, + string? sql_removed + ) { Object (id : "%s:%s".printf (parent.id, db_id), db_id : db_id, parent : parent, @@ -349,7 +391,10 @@ public abstract class Rygel.LMS.CategoryContainer : Rygel.MediaContainer, lms_db : lms_db, sql_all : sql_all, sql_find_object : sql_find_object, - sql_count : sql_count); + sql_count : sql_count, + sql_added : sql_added, + sql_removed: sql_removed + ); } construct { @@ -368,6 +413,13 @@ public abstract class Rygel.LMS.CategoryContainer : Rygel.MediaContainer, 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-database.vala b/src/plugins/lms/rygel-lms-database.vala index 2108ae6..53eb218 100644 --- a/src/plugins/lms/rygel-lms-database.vala +++ b/src/plugins/lms/rygel-lms-database.vala @@ -38,7 +38,12 @@ namespace Rygel.LMS { } 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' @@ -79,7 +84,25 @@ public class Rygel.LMS.Database { return LMS.utf8_collate_str (_a, _b); } - public Database (string db_path) throws DatabaseError { + 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", @@ -98,8 +121,32 @@ public class Rygel.LMS.Database { 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; @@ -209,6 +256,27 @@ public class Rygel.LMS.Database { 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; diff --git a/src/plugins/lms/rygel-lms-dbus-interfaces.vala b/src/plugins/lms/rygel-lms-dbus-interfaces.vala index 6c14f5b..13f00cb 100644 --- a/src/plugins/lms/rygel-lms-dbus-interfaces.vala +++ b/src/plugins/lms/rygel-lms-dbus-interfaces.vala @@ -21,8 +21,10 @@ */ [DBus (name = "org.lightmediascanner.Scanner1")] -interface Rygel.LMS.DBus : Object { +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-year.vala b/src/plugins/lms/rygel-lms-image-year.vala index b08cacd..a7768f0 100644 --- a/src/plugins/lms/rygel-lms-image-year.vala +++ b/src/plugins/lms/rygel-lms-image-year.vala @@ -40,6 +40,18 @@ public class Rygel.LMS.ImageYear : Rygel.LMS.CategoryContainer { "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); @@ -78,6 +90,12 @@ public class Rygel.LMS.ImageYear : Rygel.LMS.CategoryContainer { 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, @@ -88,6 +106,9 @@ public class Rygel.LMS.ImageYear : Rygel.LMS.CategoryContainer { lms_db, get_sql_all (year), get_sql_find_object (year), - get_sql_count (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 index 8741539..636f4d1 100644 --- a/src/plugins/lms/rygel-lms-image-years.vala +++ b/src/plugins/lms/rygel-lms-image-years.vala @@ -52,6 +52,8 @@ public class Rygel.LMS.ImageYears : Rygel.LMS.CategoryContainer { lms_db, ImageYears.SQL_ALL, ImageYears.SQL_FIND_OBJECT, - ImageYears.SQL_COUNT); + ImageYears.SQL_COUNT, + null, null + ); } } diff --git a/src/plugins/lms/rygel-lms-root-container.vala b/src/plugins/lms/rygel-lms-root-container.vala index a7a7f98..1623fa3 100644 --- a/src/plugins/lms/rygel-lms-root-container.vala +++ b/src/plugins/lms/rygel-lms-root-container.vala @@ -38,24 +38,8 @@ public class Rygel.LMS.RootContainer : Rygel.SimpleContainer { base.root(title); - string db_path = null; try { - LMS.DBus 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); - } catch (Error e) { - warning("Using dbus to get db location failed: %s", e.message); - } - if (db_path == null) { - db_path = Environment.get_user_config_dir() + - "/lightmediascannerd/db.sqlite3"; - debug ("Using default sqlite database location %s", db_path); - } - - try { - this.lms_db = new LMS.Database (db_path); + 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)); @@ -68,5 +52,7 @@ public class Rygel.LMS.RootContainer : Rygel.SimpleContainer { wait for it to be created and then add folders. Best to wait for the LMS notification API. */ } + } + } -- 2.7.4