[gnome-music/wip/jfelder/playlists-fixes: 7/8] grltrackerplaylists: Restore smart playlists update



commit 8733c9e12ce00d18b3ef40ff62ac6e9cb53cd038
Author: Jean Felder <jfelder src gnome org>
Date:   Fri Sep 6 12:54:07 2019 +0200

    grltrackerplaylists: Restore smart playlists update
    
    This feature was lost during the core rewrite.
    
    A SmartPlaylist is updated when its associated property of a song is
    changed (for example the favorties property for the Favorites
    SmartPlaylist).
    
    Once the property is updated, the model of the SmartPlaylist is
    queried and compared with the previous one to perform the correct
    insert and remove operations.

 gnomemusic/coregrilo.py                         | 33 ++++++++++---
 gnomemusic/coresong.py                          | 30 +++++++++--
 gnomemusic/grilowrappers/grltrackerplaylists.py | 66 ++++++++++++++++++++++++-
 gnomemusic/grilowrappers/grltrackerwrapper.py   |  8 +++
 4 files changed, 125 insertions(+), 12 deletions(-)
---
diff --git a/gnomemusic/coregrilo.py b/gnomemusic/coregrilo.py
index 47b3151e..ba69b702 100644
--- a/gnomemusic/coregrilo.py
+++ b/gnomemusic/coregrilo.py
@@ -157,13 +157,14 @@ class CoreGrilo(GObject.GObject):
         for wrapper in self._wrappers.values():
             wrapper.populate_album_disc_songs(media, discnr, callback)
 
-    def _store_metadata(self, source, media, key):
+    def _store_metadata(self, source, media, key, callback):
         """Convenience function to store metadata
 
         Wrap the metadata store call in a idle_add compatible form.
-        :param source: A Grilo source object
-        :param media: A Grilo media item
-        :param key: A Grilo metadata key
+        :param Grl.Source source: A Grilo source object
+        :param Grl.Media: A Grilo media item
+        :param int key: A Grilo metadata key
+        :param function callback: function to call once finished
         """
         # FIXME: Doing this async crashes.
         try:
@@ -173,13 +174,23 @@ class CoreGrilo(GObject.GObject):
             # FIXME: Do not print.
             print("Error {}: {}".format(error.domain, error.message))
 
+        if callback:
+            callback()
+
         return GLib.SOURCE_REMOVE
 
-    def writeback(self, media, key):
+    def writeback(self, media, key, callback=None):
+        """Updates the property (associated with key) of a song.
+
+        :param Grl.Media media: song to update
+        :param int key: Grilo metadata key
+        :param function callback: function to call once finished
+        """
         for wrapper in self._wrappers.values():
             if media.get_source() == wrapper.source.props.source_id:
                 GLib.idle_add(
-                    self._store_metadata, wrapper.props.source, media, key)
+                    self._store_metadata, wrapper.props.source, media, key,
+                    callback)
                 break
 
     def search(self, text):
@@ -229,3 +240,13 @@ class CoreGrilo(GObject.GObject):
             if wrapper.source.props.source_id == "grl-tracker-source":
                 wrapper.create_playlist(playlist_title, callback)
                 break
+
+    def update_smart_playlist(self, title):
+        """Updates a smart playlist.
+
+        :param str title: playlist title
+        """
+        for wrapper in self._wrappers.values():
+            if wrapper.source.props.source_id == "grl-tracker-source":
+                wrapper.update_smart_playlist(title)
+                break
diff --git a/gnomemusic/coresong.py b/gnomemusic/coresong.py
index c2befcb6..9575959a 100644
--- a/gnomemusic/coresong.py
+++ b/gnomemusic/coresong.py
@@ -93,8 +93,14 @@ class CoreSong(GObject.GObject):
         if old_fav == self._favorite:
             return
 
+        def _update_smart_playlist():
+            self._grilo.update_smart_playlist("Favorites")
+            return
+
         self.props.media.set_favourite(self._favorite)
-        self._grilo.writeback(self.props.media, Grl.METADATA_KEY_FAVOURITE)
+        self._grilo.writeback(
+            self.props.media, Grl.METADATA_KEY_FAVOURITE,
+            _update_smart_playlist)
 
     @GObject.Property(type=bool, default=False)
     def selected(self):
@@ -127,12 +133,28 @@ class CoreSong(GObject.GObject):
         if not self._is_tracker:
             return
 
-        self.props.media.set_play_count(self.props.play_count + 1)
-        self._grilo.writeback(self.props.media, Grl.METADATA_KEY_PLAY_COUNT)
+        play_count = self.props.play_count + 1
+
+        def _update_smart_playlists():
+            self._grilo.update_smart_playlist("MostPlayed")
+            if play_count == 1:
+                self._grilo.update_smart_playlist("NeverPlayed")
+            return
+
+        self.props.media.set_play_count(play_count)
+        self._grilo.writeback(
+            self.props.media, Grl.METADATA_KEY_PLAY_COUNT,
+            _update_smart_playlists)
 
     def set_last_played(self):
         if not self._is_tracker:
             return
 
+        def _update_smart_playlist():
+            self._grilo.update_smart_playlist("RecentlyPlayed")
+            return
+
         self.props.media.set_last_played(GLib.DateTime.new_now_utc())
-        self._grilo.writeback(self.props.media, Grl.METADATA_KEY_LAST_PLAYED)
+        self._grilo.writeback(
+            self.props.media, Grl.METADATA_KEY_LAST_PLAYED,
+            _update_smart_playlist)
diff --git a/gnomemusic/grilowrappers/grltrackerplaylists.py b/gnomemusic/grilowrappers/grltrackerplaylists.py
index c12fc7cd..6a8be605 100644
--- a/gnomemusic/grilowrappers/grltrackerplaylists.py
+++ b/gnomemusic/grilowrappers/grltrackerplaylists.py
@@ -22,6 +22,7 @@
 # code, but you are not obligated to do so.  If you do not wish to do so,
 # delete this exception statement from your version.
 
+import logging
 import time
 
 from gettext import gettext as _
@@ -34,6 +35,9 @@ from gnomemusic.coresong import CoreSong
 import gnomemusic.utils as utils
 
 
+logger = logging.getLogger(__name__)
+
+
 class GrlTrackerPlaylists(GObject.GObject):
 
     METADATA_KEYS = [
@@ -95,7 +99,7 @@ class GrlTrackerPlaylists(GObject.GObject):
             "tracker": self._tracker
         }
 
-        smart_playlists = {
+        self._smart_playlists = {
             "MostPlayed": MostPlayed(**args),
             "NeverPlayed": NeverPlayed(**args),
             "RecentlyPlayed": RecentlyPlayed(**args),
@@ -103,7 +107,7 @@ class GrlTrackerPlaylists(GObject.GObject):
             "Favorites": Favorites(**args)
         }
 
-        for playlist in smart_playlists.values():
+        for playlist in self._smart_playlists.values():
             self._model.append(playlist)
 
         query = """
@@ -244,6 +248,19 @@ class GrlTrackerPlaylists(GObject.GObject):
         self._tracker.update_blank_async(
             query, GLib.PRIORITY_LOW, None, _create_cb, None)
 
+    def update_smart_playlist(self, title):
+        """Updates a smart playlist.
+
+        :param str title: playlist title
+        """
+        try:
+            smart_playlist = self._smart_playlists[title]
+        except KeyError:
+            logger.warning("SmartPlaylist {} does not exist".format(title))
+            return
+
+        smart_playlist.update()
+
 
 class Playlist(GObject.GObject):
     """ Base class of all playlists """
@@ -673,6 +690,51 @@ class SmartPlaylist(Playlist):
 
         return self._model
 
+    def update(self):
+        """Updates playlist model."""
+        if self._model is None:
+            return
+
+        new_model_medias = []
+
+        def _fill_new_model(source, op_id, media, remaining, error):
+            if error:
+                return
+
+            if not media:
+                self._finish_update(new_model_medias)
+                return
+
+            new_model_medias.append(media)
+
+        options = self._fast_options.copy()
+        self._source.query(
+            self.props.query, self.METADATA_KEYS, options, _fill_new_model)
+
+    def _finish_update(self, new_model_medias):
+        if not new_model_medias:
+            self._model.remove_all()
+            return
+
+        current_models_ids = [coresong.props.media.get_id()
+                              for coresong in self._model]
+        new_model_ids = [media.get_id() for media in new_model_medias]
+
+        idx_to_delete = []
+        for idx, media_id in enumerate(current_models_ids):
+            if media_id not in new_model_ids:
+                idx_to_delete.insert(0, idx)
+
+        for idx in idx_to_delete:
+            self._model.remove(idx)
+            self.props.count -= 1
+
+        for idx, media in enumerate(new_model_medias):
+            if media.get_id() not in current_models_ids:
+                coresong = CoreSong(media, self._coreselection, self._grilo)
+                self._model.append(coresong)
+                self.props.count += 1
+
 
 class MostPlayed(SmartPlaylist):
     """Most Played smart playlist"""
diff --git a/gnomemusic/grilowrappers/grltrackerwrapper.py b/gnomemusic/grilowrappers/grltrackerwrapper.py
index 4aa57609..1aaff47f 100644
--- a/gnomemusic/grilowrappers/grltrackerwrapper.py
+++ b/gnomemusic/grilowrappers/grltrackerwrapper.py
@@ -342,6 +342,7 @@ class GrlTrackerWrapper(GObject.GObject):
                 song = CoreSong(media, self._coreselection, self._grilo)
                 self._model.append(song)
                 self._hash[media.get_id()] = song
+                self.update_smart_playlist("RecentlyAdded")
             else:
                 self._hash[media.get_id()].update(media)
 
@@ -914,3 +915,10 @@ class GrlTrackerWrapper(GObject.GObject):
         :param callback: function to perform once, the playlist is created
         """
         self._tracker_playlists.create_playlist(playlist_title, callback)
+
+    def update_smart_playlist(self, title):
+        """Updates a smart playlist.
+
+        :param str title: playlist title
+        """
+        self._tracker_playlists.update_smart_playlist(title)


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