[gnome-music/wip/mschraal/core-thumb-property: 1/18] coreartist: Rework retrieval



commit 5bc9a5e55aa4907a3c78ee21dd11fc47725a9346
Author: Marinus Schraal <mschraal gnome org>
Date:   Sun Apr 5 15:43:16 2020 +0200

    coreartist: Rework retrieval
    
    * Remove the use of the cached_thumbnail_uri property, just have this
    intermediate uri directly passed to StoreArtistArt.
    * Add StoreArtistArt class, which retrieves art from a remote uri and
    stores it in the media art cache.
    * Listen to thumbnail property changes directly to update
    ArtistArtStack.
    * Add error logic to TrackerWrapper Grilo resolve callback.

 gnomemusic/artistart.py                       | 108 ++-----------------
 gnomemusic/coreartist.py                      |  20 ++--
 gnomemusic/coregrilo.py                       |   4 +
 gnomemusic/grilowrappers/grltrackerwrapper.py |  20 ++--
 gnomemusic/storeartistart.py                  | 147 ++++++++++++++++++++++++++
 gnomemusic/widgets/artistartstack.py          |  31 +++---
 6 files changed, 203 insertions(+), 127 deletions(-)
---
diff --git a/gnomemusic/artistart.py b/gnomemusic/artistart.py
index 973c5369..7edfae42 100644
--- a/gnomemusic/artistart.py
+++ b/gnomemusic/artistart.py
@@ -141,9 +141,6 @@ class ArtistArt(GObject.GObject):
         if self._in_cache():
             return
 
-        self._coreartist.connect(
-            "notify::thumbnail", self._on_thumbnail_changed)
-
         application.props.coregrilo.get_artist_art(self._coreartist)
 
     def _in_cache(self):
@@ -151,103 +148,13 @@ class ArtistArt(GObject.GObject):
             self._artist, None, "artist")
         if (not success
                 or not thumb_file.query_exists()):
+            self._coreartist.props.thumbnail = "loading"
             return False
 
-        self._coreartist.props.cached_thumbnail_uri = thumb_file.get_path()
+        self._coreartist.props.thumbnail = thumb_file.get_path()
 
         return True
 
-    def _on_thumbnail_changed(self, coreartist, thumbnail):
-        uri = coreartist.props.thumbnail
-
-        if (uri is None
-                or uri == ""):
-            self._coreartist.props.cached_thumbnail_uri = ""
-            return
-
-        msg = Soup.Message.new("GET", uri)
-        self._soup_session.queue_message(msg, self._read_callback, None)
-
-    def _read_callback(self, src, result, data):
-        if result.props.status_code != 200:
-            self._log.debug(
-                "Failed to get remote art for the artist {} : {}".format(
-                    self._artist, result.props.reason_phrase))
-            return
-
-        try:
-            istream = Gio.MemoryInputStream.new_from_bytes(
-                result.props.response_body_data)
-        except GLib.Error as error:
-            self._log.warning(
-                "Error: {}, {}".format(error.domain, error.message))
-            self._coreartist.props.cached_thumbnail_uri = ""
-            return
-
-        try:
-            [tmp_file, iostream] = Gio.File.new_tmp()
-        except GLib.Error as error:
-            self._log.warning(
-                "Error: {}, {}".format(error.domain, error.message))
-            self._coreartist.props.cached_thumbnail_uri = ""
-            return
-
-        ostream = iostream.get_output_stream()
-        # FIXME: Passing the iostream here, otherwise it gets
-        # closed. PyGI specific issue?
-        ostream.splice_async(
-            istream, Gio.OutputStreamSpliceFlags.CLOSE_SOURCE
-            | Gio.OutputStreamSpliceFlags.CLOSE_TARGET, GLib.PRIORITY_LOW,
-            None, self._splice_callback, [tmp_file, iostream])
-
-    def _delete_callback(self, src, result, data):
-        try:
-            src.delete_finish(result)
-        except GLib.Error as error:
-            self._log.warning(
-                "Error: {}, {}".format(error.domain, error.message))
-
-    def _splice_callback(self, src, result, data):
-        tmp_file, iostream = data
-
-        iostream.close_async(
-            GLib.PRIORITY_LOW, None, self._close_iostream_callback, None)
-
-        try:
-            src.splice_finish(result)
-        except GLib.Error as error:
-            self._log.warning(
-                "Error: {}, {}".format(error.domain, error.message))
-            self._coreartist.props.cached_thumbnail_uri = ""
-            return
-
-        success, cache_path = MediaArt.get_path(self._artist, None, "artist")
-
-        if not success:
-            self._coreartist.props.cached_thumbnail_uri = ""
-            return
-
-        try:
-            # FIXME: I/O blocking
-            MediaArt.file_to_jpeg(tmp_file.get_path(), cache_path)
-        except GLib.Error as error:
-            self._log.warning(
-                "Error: {}, {}".format(error.domain, error.message))
-            self._coreartist.props.cached_thumbnail_uri = ""
-            return
-
-        self._in_cache()
-
-        tmp_file.delete_async(
-            GLib.PRIORITY_LOW, None, self._delete_callback, None)
-
-    def _close_iostream_callback(self, src, result, data):
-        try:
-            src.close_finish(result)
-        except GLib.Error as error:
-            self._log.warning(
-                "Error: {}, {}".format(error.domain, error.message))
-
 
 class ArtistCache(GObject.GObject):
     """Handles retrieval of MediaArt cache art
@@ -269,6 +176,8 @@ class ArtistCache(GObject.GObject):
         self._size = size
         self._scale = scale
 
+        self._loading_icon = DefaultIcon().get(
+            DefaultIcon.Type.LOADING, self._size, self._scale)
         self._default_icon = DefaultIcon().get(
             DefaultIcon.Type.ARTIST, self._size, self._scale)
 
@@ -296,11 +205,12 @@ class ArtistCache(GObject.GObject):
 
         :param CoreSong coresong: The CoreSong object to search art for
         """
-        thumbnail_uri = coreartist.props.cached_thumbnail_uri
-        if thumbnail_uri == "":
-            self.emit("result", self._default_icon)
+        thumbnail_uri = coreartist.props.thumbnail
+        if thumbnail_uri == "loading":
+            self.emit("result", self._loading_icon)
             return
-        elif thumbnail_uri is None:
+        elif thumbnail_uri == "generic":
+            self.emit("result", self._default_icon)
             return
 
         thumb_file = Gio.File.new_for_path(thumbnail_uri)
diff --git a/gnomemusic/coreartist.py b/gnomemusic/coreartist.py
index 9aeccd68..a5d954bf 100644
--- a/gnomemusic/coreartist.py
+++ b/gnomemusic/coreartist.py
@@ -46,7 +46,6 @@ class CoreArtist(GObject.GObject):
         super().__init__()
 
         self._application = application
-        self._cached_thumbnail_uri = None
         self._coregrilo = application.props.coregrilo
         self._coremodel = application.props.coremodel
         self._model = None
@@ -110,20 +109,21 @@ class CoreArtist(GObject.GObject):
 
     @GObject.Property(type=str, default=None)
     def thumbnail(self):
+        """Artist art thumbnail retrieval
+
+        :return: The artist art location or "generic" or "loading"
+        :rtype: string
+        """
         if self._thumbnail is None:
-            self._thumbnail = ""
+            self._thumbnail = "loading"
             ArtistArt(self._application, self)
 
         return self._thumbnail
 
     @thumbnail.setter
     def thumbnail(self, value):
-        self._thumbnail = value
+        """Artist art thumbnail setter
 
-    @GObject.Property(type=str, default=None)
-    def cached_thumbnail_uri(self):
-        return self._cached_thumbnail_uri
-
-    @cached_thumbnail_uri.setter
-    def cached_thumbnail_uri(self, value):
-        self._cached_thumbnail_uri = value
+        :param string value: path, "generic" or "loading"
+        """
+        self._thumbnail = value
diff --git a/gnomemusic/coregrilo.py b/gnomemusic/coregrilo.py
index c89b83e0..1540559d 100644
--- a/gnomemusic/coregrilo.py
+++ b/gnomemusic/coregrilo.py
@@ -222,6 +222,10 @@ class CoreGrilo(GObject.GObject):
                 coresong, callback)
 
     def get_artist_art(self, coreartist):
+        """Retrieve artist art for the given CoreArtist
+
+        :param CoreArtist coreartist: CoreArtist to retrieve art for
+        """
         if "grl-tracker-source" in self._wrappers:
             self._wrappers["grl-tracker-source"].get_artist_art(coreartist)
 
diff --git a/gnomemusic/grilowrappers/grltrackerwrapper.py b/gnomemusic/grilowrappers/grltrackerwrapper.py
index db192e2f..df0664dd 100644
--- a/gnomemusic/grilowrappers/grltrackerwrapper.py
+++ b/gnomemusic/grilowrappers/grltrackerwrapper.py
@@ -31,6 +31,7 @@ from gnomemusic.coreartist import CoreArtist
 from gnomemusic.coredisc import CoreDisc
 from gnomemusic.coresong import CoreSong
 from gnomemusic.grilowrappers.grltrackerplaylists import GrlTrackerPlaylists
+from gnomemusic.storeartistart import StoreArtistArt
 
 
 class GrlTrackerWrapper(GObject.GObject):
@@ -985,26 +986,33 @@ class GrlTrackerWrapper(GObject.GObject):
         return query
 
     def get_artist_art(self, coreartist):
-        """Get artist art for the artist
+        """Retrieve artist art for the given CoreArtist
 
-        :param CoreArtist coreartist: coreartist
+        This retrieves art through Grilo online services only.
+
+        :param CoreArtist coreartist: CoreArtist to get art for
         """
         media = coreartist.props.media
 
-        def _resolve_cb(source, op_id, resolved_media, data, error):
+        def art_resolved_cb(source, op_id, resolved_media, data, error):
+            if error:
+                self._log.warning("Error: {}".format(error))
+                coreartist.props.thumbnail = "generic"
+                return
+
             if resolved_media.get_thumbnail() is None:
-                coreartist.props.thumbnail = ""
+                coreartist.props.thumbnail = "generic"
                 return
 
             media.set_thumbnail(resolved_media.get_thumbnail())
-            coreartist.props.thumbnail = media.get_thumbnail()
+            StoreArtistArt(coreartist)
 
         full_options = Grl.OperationOptions()
         full_options.set_resolution_flags(
             Grl.ResolutionFlags.FULL | Grl.ResolutionFlags.IDLE_RELAY)
 
         self.props.source.resolve(
-            media, [Grl.METADATA_KEY_THUMBNAIL], full_options, _resolve_cb,
+            media, [Grl.METADATA_KEY_THUMBNAIL], full_options, art_resolved_cb,
             None)
 
     def stage_playlist_deletion(self, playlist):
diff --git a/gnomemusic/storeartistart.py b/gnomemusic/storeartistart.py
new file mode 100644
index 00000000..95aaecd3
--- /dev/null
+++ b/gnomemusic/storeartistart.py
@@ -0,0 +1,147 @@
+# Copyright 2020 The GNOME Music developers
+#
+# GNOME Music is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# GNOME Music 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 General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License along
+# with GNOME Music; if not, write to the Free Software Foundation, Inc.,
+# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+#
+# The GNOME Music authors hereby grant permission for non-GPL compatible
+# GStreamer plugins to be used and distributed together with GStreamer
+# and GNOME Music.  This permission is above and beyond the permissions
+# granted by the GPL license by which GNOME Music is covered.  If you
+# modify this code, you may extend this exception to your version of the
+# 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 gi
+gi.require_versions({"MediaArt": "2.0", "Soup": "2.4"})
+from gi.repository import Gio, GLib, GObject, MediaArt, Soup
+
+from gnomemusic.musiclogger import MusicLogger
+
+
+class StoreArtistArt(GObject.Object):
+    """Stores Art in the MediaArt cache.
+    """
+
+    def __init__(self, coreartist):
+        """Initialize StoreArtistArt
+
+        :param CoreArtist coreartist: The core artist to store art for
+        """
+        self._coreartist = coreartist
+        self._log = MusicLogger()
+        self._soup_session = Soup.Session.new()
+
+        uri = coreartist.props.media.get_thumbnail()
+        if (uri is None
+                or uri == ""):
+            self._coreartist.props.thumbnail = "generic"
+            return
+
+        cache_dir = GLib.build_filenamev(
+            [GLib.get_user_cache_dir(), "media-art"])
+        cache_dir_file = Gio.File.new_for_path(cache_dir)
+        cache_dir_file.query_info_async(
+            Gio.FILE_ATTRIBUTE_ACCESS_CAN_READ, Gio.FileQueryInfoFlags.NONE,
+            GLib.PRIORITY_LOW, None, self._cache_dir_info_read, uri)
+
+    def _cache_dir_info_read(self, cache_dir_file, res, uri):
+        try:
+            cache_dir_file.query_info_finish(res)
+        except GLib.Error:
+            # directory does not exist yet
+            try:
+                cache_dir_file.make_directory(None)
+            except GLib.Error as error:
+                self._log.warning(
+                    "Error: {}, {}".format(error.domain, error.message))
+                self._coreartist.props.thumbnail = "generic"
+                return
+
+        msg = Soup.Message.new("GET", uri)
+        self._soup_session.queue_message(msg, self._read_callback, None)
+
+    def _read_callback(self, src, result, data):
+        if result.props.status_code != 200:
+            self._log.debug(
+                "Failed to get remote art: {}".format(
+                    result.props.reason_phrase))
+            return
+
+        try:
+            [tmp_file, iostream] = Gio.File.new_tmp()
+        except GLib.Error as error:
+            self._log.warning(
+                "Error: {}, {}".format(error.domain, error.message))
+            self._coreartist.props.thumbnail = "generic"
+            return
+
+        istream = Gio.MemoryInputStream.new_from_bytes(
+            result.props.response_body_data)
+        ostream = iostream.get_output_stream()
+        # FIXME: Passing the iostream here, otherwise it gets
+        # closed. PyGI specific issue?
+        ostream.splice_async(
+            istream, Gio.OutputStreamSpliceFlags.CLOSE_SOURCE
+            | Gio.OutputStreamSpliceFlags.CLOSE_TARGET, GLib.PRIORITY_LOW,
+            None, self._splice_callback, [tmp_file, iostream])
+
+    def _delete_callback(self, src, result, data):
+        try:
+            src.delete_finish(result)
+        except GLib.Error as error:
+            self._log.warning(
+                "Error: {}, {}".format(error.domain, error.message))
+
+    def _splice_callback(self, src, result, data):
+        tmp_file, iostream = data
+
+        iostream.close_async(
+            GLib.PRIORITY_LOW, None, self._close_iostream_callback, None)
+
+        try:
+            src.splice_finish(result)
+        except GLib.Error as error:
+            self._log.warning(
+                "Error: {}, {}".format(error.domain, error.message))
+            self._coreartist.props.thumbnail = "generic"
+            return
+
+        success, cache_path = MediaArt.get_path(
+            self._coreartist.props.artist, None, "artist")
+
+        if not success:
+            self._coreartist.props.thumbnail = "generic"
+            return
+
+        try:
+            # FIXME: I/O blocking
+            MediaArt.file_to_jpeg(tmp_file.get_path(), cache_path)
+        except GLib.Error as error:
+            self._log.warning(
+                "Error: {}, {}".format(error.domain, error.message))
+            self._coreartist.props.thumbnail = "generic"
+            return
+
+        self._coreartist.props.media.set_thumbnail(cache_path)
+        self._coreartist.props.thumbnail = cache_path
+
+        tmp_file.delete_async(
+            GLib.PRIORITY_LOW, None, self._delete_callback, None)
+
+    def _close_iostream_callback(self, src, result, data):
+        try:
+            src.close_finish(result)
+        except GLib.Error as error:
+            self._log.warning(
+                "Error: {}, {}".format(error.domain, error.message))
diff --git a/gnomemusic/widgets/artistartstack.py b/gnomemusic/widgets/artistartstack.py
index 5cf3bb9a..35306774 100644
--- a/gnomemusic/widgets/artistartstack.py
+++ b/gnomemusic/widgets/artistartstack.py
@@ -1,4 +1,4 @@
-# Copyright 2019 The GNOME Music developers
+# Copyright 2020 The GNOME Music developers
 #
 # GNOME Music is free software; you can redistribute it and/or modify
 # it under the terms of the GNU General Public License as published by
@@ -42,20 +42,22 @@ class ArtistArtStack(Gtk.Stack):
     _default_icon = DefaultIcon()
 
     def __init__(self, size=Art.Size.MEDIUM):
-        """Initialize the CoverStack
+        """Initialize the ArtStack
 
         :param Art.Size size: The size of the art used for the cover
         """
         super().__init__()
 
-        self._art = None
+        self._cache = None
         self._handler_id = None
         self._size = None
         self._timeout = None
 
         self._loading_cover = Gtk.Image()
         self._cover_a = Gtk.Image()
+        self._cover_a.props.visible = True
         self._cover_b = Gtk.Image()
+        self._cover_b.props.visible = True
 
         self.add_named(self._loading_cover, "loading")
         self.add_named(self._cover_a, "A")
@@ -67,11 +69,7 @@ class ArtistArtStack(Gtk.Stack):
         self.props.transition_type = Gtk.StackTransitionType.CROSSFADE
         self.props.visible_child_name = "loading"
 
-        self._loading_cover.props.visible = True
-        self._cover_a.props.visible = True
-        self._cover_b.props.visible = True
-
-        self.props.visible = True
+        self.connect("destroy", self._on_destroy)
 
     @GObject.Property(type=object, flags=GObject.ParamFlags.READWRITE)
     def size(self):
@@ -103,16 +101,16 @@ class ArtistArtStack(Gtk.Stack):
         self._coreartist = coreartist
 
         self._coreartist.connect(
-            "notify::cached-thumbnail-uri", self._on_thumbnail_changed)
+            "notify::thumbnail", self._on_thumbnail_changed)
 
         if self._coreartist.props.thumbnail is not None:
             self._on_thumbnail_changed(self._coreartist, None)
 
     def _on_thumbnail_changed(self, coreartist, uri):
-        cache = ArtistCache(self.props.size, self.props.scale_factor)
-        cache.connect("result", self._on_cache_result)
+        self._cache = ArtistCache(self.props.size, self.props.scale_factor)
+        self._handler_id = self._cache.connect("result", self._on_cache_result)
 
-        cache.query(coreartist)
+        self._cache.query(coreartist)
 
     def _on_cache_result(self, cache, surface):
         if self._active_child == "B":
@@ -121,3 +119,12 @@ class ArtistArtStack(Gtk.Stack):
         else:
             self._cover_b.props.surface = surface
             self.props.visible_child_name = "B"
+
+    def _on_destroy(self, widget):
+        # If the stack is destroyed while the art is updated, an error
+        # can occur once the art is retrieved because the ArtStack does
+        # not have children anymore.
+        if (self._cache is not None
+                and self._handler_id is not None):
+            self._cache.disconnect(self._handler_id)
+            self._handler_id = None


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