[gnome-music/wip/mschraal/coresong-thumbnail-prop: 5/8] bit messy album art thumbnail retrieval



commit b51e368d69d8d3f50fd9ace633c736e01b951dfe
Author: Marinus Schraal <mschraal gnome org>
Date:   Thu Nov 28 17:47:26 2019 +0100

    bit messy album art thumbnail retrieval

 gnomemusic/albumart.py                        | 334 ++++++++++++++++++++++++++
 gnomemusic/corealbum.py                       |  14 +-
 gnomemusic/coregrilo.py                       |   8 +
 gnomemusic/grilowrappers/grltrackerwrapper.py |  24 ++
 gnomemusic/storealbumart.py                   |  98 ++++++++
 5 files changed, 466 insertions(+), 12 deletions(-)
---
diff --git a/gnomemusic/albumart.py b/gnomemusic/albumart.py
new file mode 100644
index 00000000..ff809f9d
--- /dev/null
+++ b/gnomemusic/albumart.py
@@ -0,0 +1,334 @@
+# Copyright 2019 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.
+
+from enum import Enum
+import logging
+from math import pi
+
+import cairo
+import gi
+gi.require_version("MediaArt", "2.0")
+from gi.repository import Gdk, GdkPixbuf, Gio, GLib, GObject, Gtk, MediaArt
+
+
+logger = logging.getLogger(__name__)
+
+
+def _make_icon_frame(icon_surface, art_size=None, scale=1, default_icon=False):
+    icon_w = icon_surface.get_width()
+    icon_h = icon_surface.get_height()
+    ratio = icon_h / icon_w
+
+    # Scale down the image according to the biggest axis
+    if ratio > 1:
+        w = int(art_size.width / ratio)
+        h = art_size.height
+    else:
+        w = art_size.width
+        h = int(art_size.height * ratio)
+
+    surface = cairo.ImageSurface(cairo.FORMAT_ARGB32, w * scale, h * scale)
+    surface.set_device_scale(scale, scale)
+    ctx = cairo.Context(surface)
+
+    matrix = cairo.Matrix()
+
+    line_width = 0.6
+    ctx.new_sub_path()
+    ctx.arc(w / 2, h / 2, (w / 2) - line_width, 0, 2 * pi)
+    ctx.set_source_rgba(0, 0, 0, 0.7)
+    ctx.set_line_width(line_width)
+    ctx.stroke_preserve()
+
+    if default_icon:
+        ctx.set_source_rgb(1, 1, 1)
+        ctx.fill()
+        ctx.set_source_rgba(0, 0, 0, 0.3)
+        ctx.mask_surface(icon_surface, w / 3, h / 3)
+        ctx.fill()
+    else:
+        matrix.scale(icon_w / (w * scale), icon_h / (h * scale))
+        ctx.set_source_surface(icon_surface, 0, 0)
+
+        pattern = ctx.get_source()
+        pattern.set_matrix(matrix)
+        ctx.fill()
+
+    ctx.arc(w / 2, h / 2, w / 2, 0, 2 * pi)
+    ctx.clip()
+
+    return surface
+
+
+class DefaultIcon(GObject.GObject):
+    """Provides the symbolic fallback and loading icons."""
+
+    class Type(Enum):
+        LOADING = "content-loading-symbolic"
+        ARTIST = "avatar-default-symbolic"
+
+    _cache = {}
+    _default_theme = Gtk.IconTheme.get_default()
+
+    def __repr__(self):
+        return "<DefaultIcon>"
+
+    def __init__(self):
+        super().__init__()
+
+    def _make_default_icon(self, icon_type, art_size, scale):
+        icon_info = self._default_theme.lookup_icon_for_scale(
+            icon_type.value, art_size.width / 3, scale, 0)
+        icon = icon_info.load_surface()
+
+        icon_surface = _make_icon_frame(icon, art_size, scale, True)
+
+        return icon_surface
+
+    def get(self, icon_type, art_size, scale=1):
+        """Returns the requested symbolic icon
+
+        Returns a cairo surface of the requested symbolic icon in the
+        given size.
+
+        :param enum icon_type: The DefaultIcon.Type of the icon
+        :param enum art_size: The Art.Size requested
+
+        :return: The symbolic icon
+        :rtype: cairo.Surface
+        """
+        if (icon_type, art_size, scale) not in self._cache.keys():
+            new_icon = self._make_default_icon(icon_type, art_size, scale)
+            self._cache[(icon_type, art_size, scale)] = new_icon
+
+        return self._cache[(icon_type, art_size, scale)]
+
+
+class AlbumArt(GObject.GObject):
+
+    def __init__(self, corealbum, coremodel):
+        """Initialize the Album Art retrieval object
+
+        :param CoreAlbum corealbum: The CoreALbum to use
+        :param CoreModel coremodel: The CoreModel object
+        """
+        super().__init__()
+
+        self._corealbum = corealbum
+        self._artist = corealbum.props.artist
+        self._title = corealbum.props.title
+
+        # if self._in_cache():
+        #     return
+
+        coremodel.props.grilo.get_album_art(corealbum)
+
+    def _in_cache(self):
+        success, thumb_file = MediaArt.get_file(
+            self._artist, self._title, "album")
+
+        # FIXME: Make async.
+        if (not success
+                or not thumb_file.query_exists()):
+            return False
+
+        self._corealbum.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
+
+        src = Gio.File.new_for_uri(uri)
+        src.read_async(
+            GLib.PRIORITY_LOW, None, self._read_callback, None)
+
+    def _read_callback(self, src, result, data):
+        try:
+            istream = src.read_finish(result)
+        except GLib.Error as error:
+            logger.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:
+            logger.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:
+            logger.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:
+            logger.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:
+            logger.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:
+            logger.warning("Error: {}, {}".format(error.domain, error.message))
+
+
+# class ArtistCache(GObject.GObject):
+#     """Handles retrieval of MediaArt cache art
+
+#     Uses signals to indicate success or failure.
+#     """
+
+#     __gtype_name__ = "ArtistCache"
+
+#     __gsignals__ = {
+#         "result": (GObject.SignalFlags.RUN_FIRST, None, (object, ))
+#     }
+
+#     def __repr__(self):
+#         return "<ArtistCache>"
+
+#     def __init__(self, size, scale):
+#         super().__init__()
+
+#         self._size = size
+#         self._scale = scale
+
+#         self._default_icon = DefaultIcon().get(
+#             DefaultIcon.Type.ARTIST, self._size, self._scale)
+
+#         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, None)
+
+#     def _cache_dir_info_read(self, cache_dir_file, res, data):
+#         try:
+#             cache_dir_file.query_info_finish(res)
+#             return
+#         except GLib.Error:
+            # directory does not exist yet
+#             try:
+#                 cache_dir_file.make_directory(None)
+#             except GLib.Error as error:
+#                 logger.warning(
+#                     "Error: {}, {}".format(error.domain, error.message))
+
+#     def query(self, coreartist):
+#         """Start the cache query
+
+#         :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)
+#             return
+#         elif thumbnail_uri is None:
+#             return
+
+#         thumb_file = Gio.File.new_for_path(thumbnail_uri)
+#         if thumb_file:
+#             thumb_file.read_async(
+#                 GLib.PRIORITY_LOW, None, self._open_stream, None)
+#             return
+
+#         self.emit("result", self._default_icon)
+
+#     def _open_stream(self, thumb_file, result, arguments):
+#         try:
+#             stream = thumb_file.read_finish(result)
+#         except GLib.Error as error:
+#             logger.warning("Error: {}, {}".format(error.domain, error.message))
+#             self.emit("result", self._default_icon)
+#             return
+
+#         GdkPixbuf.Pixbuf.new_from_stream_async(
+#             stream, None, self._pixbuf_loaded, None)
+
+#     def _pixbuf_loaded(self, stream, result, data):
+#         try:
+#             pixbuf = GdkPixbuf.Pixbuf.new_from_stream_finish(result)
+#         except GLib.Error as error:
+#             logger.warning("Error: {}, {}".format(error.domain, error.message))
+#             self.emit("result", self._default_icon)
+#             return
+
+#         stream.close_async(GLib.PRIORITY_LOW, None, self._close_stream, None)
+
+#         surface = Gdk.cairo_surface_create_from_pixbuf(
+#             pixbuf, self._scale, None)
+#         surface = _make_icon_frame(surface, self._size, self._scale)
+
+#         self.emit("result", surface)
+
+#     def _close_stream(self, stream, result, data):
+#         try:
+#             stream.close_finish(result)
+#         except GLib.Error as error:
+#             logger.warning("Error: {}, {}".format(error.domain, error.message))
diff --git a/gnomemusic/corealbum.py b/gnomemusic/corealbum.py
index 5b2cc481..96d30107 100644
--- a/gnomemusic/corealbum.py
+++ b/gnomemusic/corealbum.py
@@ -27,7 +27,7 @@ gi.require_versions({"Grl": "0.3", "MediaArt": "2.0"})
 from gi.repository import Gio, Grl, GObject, MediaArt
 
 import gnomemusic.utils as utils
-
+from gnomemusic.albumart import AlbumArt
 
 class CoreAlbum(GObject.GObject):
     """Exposes a Grl.Media with relevant data as properties
@@ -122,7 +122,7 @@ class CoreAlbum(GObject.GObject):
         if self._thumbnail == None:
             self._thumbnail = "loading"
 
-            self._in_cache()
+            AlbumArt(self, self._coremodel)
 
         return self._thumbnail
 
@@ -132,13 +132,3 @@ class CoreAlbum(GObject.GObject):
             return
 
         self._thumbnail = value
-
-    def _in_cache(self):
-        success, thumb_file = MediaArt.get_file(
-            self.props.artist, self.props.title, "album")
-
-        if (not success
-                or not thumb_file.query_exists()):
-            return False
-
-        self.props.thumbnail = thumb_file.get_path()
diff --git a/gnomemusic/coregrilo.py b/gnomemusic/coregrilo.py
index ed2c9993..1dfd90fe 100644
--- a/gnomemusic/coregrilo.py
+++ b/gnomemusic/coregrilo.py
@@ -208,6 +208,14 @@ class CoreGrilo(GObject.GObject):
             self._wrappers["grl-tracker-source"].get_album_art_for_item(
                 coresong, callback)
 
+    def get_album_art(self, corealbum):
+        source = corealbum.props.media.get_source()
+
+        for wrapper_id in self._wrappers.keys():
+            if wrapper_id == source:
+                self._wrappers[wrapper_id].get_album_art(corealbum)
+                break
+
     def get_artist_art(self, coreartist):
         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 f8869659..d0ae8645 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.storealbumart import StoreAlbumArt
 from gnomemusic.trackerwrapper import TrackerWrapper
 
 
@@ -839,6 +840,29 @@ class GrlTrackerWrapper(GObject.GObject):
 
         self._source.query(query, self.METADATA_KEYS, options, songs_search_cb)
 
+    def get_album_art(self, corealbum):
+        def art_retrieved_cb(source, op_id, media, data, error):
+            if error:
+                print("ERROR", error)
+                corealbum.props.thumbnail = "generic"
+                return
+
+            StoreAlbumArt(corealbum, media)
+
+        album_id = corealbum.props.media.get_id()
+
+        query = self._get_album_for_album_id(album_id)
+
+        full_options = Grl.OperationOptions()
+        full_options.set_resolution_flags(
+            Grl.ResolutionFlags.FULL
+            | Grl.ResolutionFlags.IDLE_RELAY)
+        full_options.set_count(1)
+
+        self._source.query(
+            query, self.METADATA_THUMBNAIL_KEYS, full_options,
+            art_retrieved_cb)
+
     def get_album_art_for_item(self, coresong, callback):
         """Placeholder until we got a better solution
         """
diff --git a/gnomemusic/storealbumart.py b/gnomemusic/storealbumart.py
new file mode 100644
index 00000000..b79f5e5c
--- /dev/null
+++ b/gnomemusic/storealbumart.py
@@ -0,0 +1,98 @@
+import logging
+
+import gi
+gi.require_version("MediaArt", "2.0")
+from gi.repository import Gio, GLib, GObject, MediaArt
+
+
+logger = logging.getLogger(__name__)
+
+
+class StoreAlbumArt(GObject.GObject):
+
+    def __init__(self, corealbum, media):
+        """
+        """
+        super().__init__()
+
+        self._corealbum = corealbum
+        self._media = media
+
+        uri = media.get_thumbnail()
+        if (uri is None
+                or uri == ""):
+            self._corealbum.props.thumbnail = "generic"
+            return
+
+        src = Gio.File.new_for_uri(uri)
+        src.read_async(
+            GLib.PRIORITY_LOW, None, self._read_callback, None)
+
+    def _read_callback(self, src, result, data):
+        try:
+            istream = src.read_finish(result)
+        except GLib.Error as error:
+            logger.warning("Error: {}, {}".format(error.domain, error.message))
+            self._corealbum.props.thumbnail = "generic"
+            return
+
+        try:
+            [tmp_file, iostream] = Gio.File.new_tmp()
+        except GLib.Error as error:
+            logger.warning("Error: {}, {}".format(error.domain, error.message))
+            self._corealbum.props.thumbnail = "generic"
+            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:
+            logger.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:
+            logger.warning("Error: {}, {}".format(error.domain, error.message))
+            self._corealbum.props.thumbnail = "generic"
+            return
+
+        success, cache_path = MediaArt.get_path(
+            self._corealbum.props.artist, self._corealbum.props.title, "album")
+
+        if not success:
+            self._corealbum.props.thumbnail = "generic"
+            return
+
+        try:
+            # FIXME: I/O blocking
+            MediaArt.file_to_jpeg(tmp_file.get_path(), cache_path)
+        except GLib.Error as error:
+            logger.warning("Error: {}, {}".format(error.domain, error.message))
+            self._corealbum.props.thumbnail = "generic"
+            return
+
+        # FIXME: Also set media.
+        self._corealbum.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:
+            logger.warning("Error: {}, {}".format(error.domain, error.message))


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