[gnome-music/wip/jfelder/searchview-new-style: 10/11] start of retrieving artist art
- From: Jean Felder <jfelder src gnome org>
- To: commits-list gnome org
- Cc:
- Subject: [gnome-music/wip/jfelder/searchview-new-style: 10/11] start of retrieving artist art
- Date: Sat, 3 Aug 2019 18:16:21 +0000 (UTC)
commit c03c26d9065a5483322157c4e29d0e2709d0c8c5
Author: Marinus Schraal <mschraal gnome org>
Date: Fri Aug 2 13:03:25 2019 +0200
start of retrieving artist art
gnomemusic/albumartcache.py | 1 +
gnomemusic/artistart.py | 317 ++++++++++++++++++++++++++
gnomemusic/coreartist.py | 23 ++
gnomemusic/coregrilo.py | 4 +
gnomemusic/grilowrappers/grltrackerwrapper.py | 22 ++
gnomemusic/widgets/artistartstack.py | 180 +++++++++++++++
6 files changed, 547 insertions(+)
---
diff --git a/gnomemusic/albumartcache.py b/gnomemusic/albumartcache.py
index ec7ca541..86943a60 100644
--- a/gnomemusic/albumartcache.py
+++ b/gnomemusic/albumartcache.py
@@ -29,6 +29,7 @@ import os
import cairo
import gi
+gi.require_version("GstPbutils", "1.0")
gi.require_version('GstTag', '1.0')
gi.require_version('MediaArt', '2.0')
from gi.repository import (Gdk, GdkPixbuf, Gio, GLib, GObject, Gtk, MediaArt,
diff --git a/gnomemusic/artistart.py b/gnomemusic/artistart.py
new file mode 100644
index 00000000..e6124cf6
--- /dev/null
+++ b/gnomemusic/artistart.py
@@ -0,0 +1,317 @@
+# 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 os
+
+import cairo
+import gi
+gi.require_version("MediaArt", "2.0")
+from gi.repository import Gdk, GdkPixbuf, Gio, GLib, GObject, Gtk, MediaArt
+
+from gnomemusic.albumartcache import Art
+
+
+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 = 2
+ # 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)
+
+ ctx.arc(w / 2, h / 2, w / 2, 0, 2 * pi)
+ ctx.clip()
+
+ matrix = cairo.Matrix()
+
+ if default_icon:
+ ctx.set_source_rgb(0, 0, 0)
+ ctx.fill()
+ matrix.translate(-w * 1/3, -h * 1/3)
+ ctx.set_operator(cairo.Operator.DIFFERENCE)
+ else:
+ matrix.scale(icon_w / w, icon_h / h)
+
+ ctx.set_source_surface(icon_surface, 0, 0)
+
+ pattern = ctx.get_source()
+ pattern.set_matrix(matrix)
+ ctx.rectangle(0, 0, w, h)
+ ctx.fill()
+
+ return surface
+
+
+class DefaultIcon(GObject.GObject):
+ """Provides the symbolic fallback and loading icons."""
+
+ class Type(Enum):
+ LOADING = 'content-loading-symbolic'
+ MUSIC = 'folder-music-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 ArtistArt(GObject.GObject):
+
+ def __init__(self, coreartist):
+ super().__init__()
+
+ self._coreartist = coreartist
+ self._artist = self._coreartist.props.artist
+
+ if self._in_cache():
+ print("In cache!")
+ return
+
+ # FIXME: Ugly.
+ grilo = self._coreartist._coremodel._grilo
+
+ self._coreartist.connect(
+ "notify::thumbnail", self._on_thumbnail_changed)
+
+ grilo.get_artist_art(self._coreartist)
+
+ def _in_cache(self):
+ success, thumb_file = MediaArt.get_file(
+ self._artist, None, "artist")
+ if (not success
+ or not thumb_file.query_exists()):
+ return False
+
+ print("setting cached uri")
+ self._coreartist.props.cached_thumbnail_uri = thumb_file.get_path()
+
+ return True
+
+ def _on_thumbnail_changed(self, coreartist, thumbnail):
+ uri = coreartist.props.thumbnail
+ print("ArtistArt", uri)
+
+ if (uri is None
+ or 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))
+ return
+
+ try:
+ [tmp_file, iostream] = Gio.File.new_tmp()
+ except GLib.Error as error:
+ logger.warning("Error: {}, {}".format(error.domain, error.message))
+ 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))
+ return
+
+ success, cache_path = MediaArt.get_path(self._artist, None, "artist")
+
+ if not success:
+ 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))
+ 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__ = {
+ 'hit': (GObject.SignalFlags.RUN_FIRST, None, (object, ))
+ }
+
+ def __repr__(self):
+ return "<ArtistCache>"
+
+ def __init__(self):
+ super().__init__()
+
+ # FIXME
+ self._size = Art.Size.MEDIUM
+ self._scale = 1
+
+ self._default_icon = DefaultIcon().get(
+ DefaultIcon.Type.MUSIC, self._size, self._scale)
+
+ # FIXME: async
+ self.cache_dir = os.path.join(GLib.get_user_cache_dir(), 'media-art')
+ if not os.path.exists(self.cache_dir):
+ try:
+ Gio.file_new_for_path(self.cache_dir).make_directory(None)
+ except GLib.Error as error:
+ logger.warning(
+ "Error: {}, {}".format(error.domain, error.message))
+ return
+
+ def query(self, coreartist):
+ """Start the cache query
+
+ :param CoreSong coresong: The CoreSong object to search art for
+ """
+ print("query")
+ thumb_file = Gio.File.new_for_path(
+ coreartist.props.cached_thumbnail_uri)
+ if thumb_file:
+ thumb_file.read_async(
+ GLib.PRIORITY_LOW, None, self._open_stream, None)
+ return
+
+ self.emit("hit", 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("hit", 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("hit", 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("hit", 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))
+
+ def _cache_hit(self, klass, pixbuf):
+ surface = Gdk.cairo_surface_create_from_pixbuf(
+ pixbuf, self._scale, None)
+ surface = _make_icon_frame(surface, self._size, self._scale)
+ self._surface = surface
diff --git a/gnomemusic/coreartist.py b/gnomemusic/coreartist.py
index fcb482bc..5910e8bb 100644
--- a/gnomemusic/coreartist.py
+++ b/gnomemusic/coreartist.py
@@ -27,6 +27,7 @@ gi.require_version('Grl', '0.3')
from gi.repository import Gio, Grl, GObject
from gnomemusic import log
+from gnomemusic.artistart import ArtistArt
import gnomemusic.utils as utils
@@ -41,9 +42,11 @@ class CoreArtist(GObject.GObject):
def __init__(self, media, coremodel):
super().__init__()
+ self._cached_thumbnail_uri = None
self._coremodel = coremodel
self._model = None
self._selected = False
+ self._thumbnail = None
self.update(media)
@@ -82,3 +85,23 @@ class CoreArtist(GObject.GObject):
# is requested, it will trigger the filled model update as
# well.
self.props.model
+
+ @GObject.Property(type=str, default=None)
+ def thumbnail(self):
+ if self._thumbnail is None:
+ self._thumbnail = ""
+ ArtistArt(self)
+
+ return self._thumbnail
+
+ @thumbnail.setter
+ def thumbnail(self, value):
+ self._thumbnail = value
+
+ @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
diff --git a/gnomemusic/coregrilo.py b/gnomemusic/coregrilo.py
index ee2448c4..940fc9fc 100644
--- a/gnomemusic/coregrilo.py
+++ b/gnomemusic/coregrilo.py
@@ -194,6 +194,10 @@ class CoreGrilo(GObject.GObject):
self._wrappers["grl-tracker-source"].get_album_art_for_item(
coresong, callback)
+ def get_artist_art(self, coreartist):
+ if "grl-tracker-source" in self._wrappers:
+ self._wrappers["grl-tracker-source"].get_artist_art(coreartist)
+
def stage_playlist_deletion(self, playlist):
"""Prepares playlist deletion.
diff --git a/gnomemusic/grilowrappers/grltrackerwrapper.py b/gnomemusic/grilowrappers/grltrackerwrapper.py
index 8fa723c5..d18dea75 100644
--- a/gnomemusic/grilowrappers/grltrackerwrapper.py
+++ b/gnomemusic/grilowrappers/grltrackerwrapper.py
@@ -867,6 +867,28 @@ class GrlTrackerWrapper(GObject.GObject):
return query
+ def get_artist_art(self, coreartist):
+ media = coreartist.props.media
+
+ def _resolve_cb(source, op_id, resolved_media, data, error):
+ print("operation finished")
+ print(resolved_media.get_artist())
+ print(resolved_media.get_thumbnail())
+ if resolved_media.get_thumbnail() is None:
+ coreartist.props.thumbnail = ""
+ return
+
+ media.set_thumbnail(resolved_media.get_thumbnail())
+ coreartist.props.thumbnail = media.get_thumbnail()
+
+ full_options = Grl.OperationOptions()
+ full_options.set_resolution_flags(
+ Grl.ResolutionFlags.FULL | Grl.ResolutionFlags.IDLE_RELAY)
+
+ self._source.resolve(
+ media, [Grl.METADATA_KEY_THUMBNAIL], full_options, _resolve_cb,
+ None)
+
def stage_playlist_deletion(self, playlist):
"""Prepares playlist deletion.
diff --git a/gnomemusic/widgets/artistartstack.py b/gnomemusic/widgets/artistartstack.py
new file mode 100644
index 00000000..6024d9f8
--- /dev/null
+++ b/gnomemusic/widgets/artistartstack.py
@@ -0,0 +1,180 @@
+# 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 gi.repository import GLib, GObject, Gtk
+
+from gnomemusic import log
+from gnomemusic.albumartcache import Art
+from gnomemusic.artistart import ArtistCache, DefaultIcon
+from gnomemusic.coreartist import CoreArtist
+
+
+class ArtistArtStack(Gtk.Stack):
+ """Provides a smooth transition between image states
+
+ Uses a Gtk.Stack to provide an in-situ transition between an image
+ state. Either between the "loading" state versus the "loaded" state
+ or in between songs.
+ """
+
+ __gtype_name__ = "ArtistArtStack"
+
+ __gsignals__ = {
+ "updated": (GObject.SignalFlags.RUN_FIRST, None, ())
+ }
+
+ _default_icon = DefaultIcon()
+
+ def __repr__(self):
+ return "ArtistArtStack"
+
+ @log
+ def __init__(self, size=Art.Size.MEDIUM):
+ """Initialize the CoverStack
+
+ :param Art.Size size: The size of the art used for the cover
+ """
+ super().__init__()
+
+ self._art = None
+ self._handler_id = None
+ self._size = None
+ self._timeout = None
+
+ self._loading_cover = Gtk.Image()
+ self._cover_a = Gtk.Image()
+ self._cover_b = Gtk.Image()
+
+ self.add_named(self._loading_cover, "loading")
+ self.add_named(self._cover_a, "A")
+ self.add_named(self._cover_b, "B")
+
+ self._active_child = "loading"
+
+ self.props.size = size
+ self.props.transition_type = Gtk.StackTransitionType.CROSSFADE
+ self.props.visible_child_name = "loading"
+
+ self.show_all()
+
+ @GObject.Property(type=object, flags=GObject.ParamFlags.READWRITE)
+ def size(self):
+ """Size of the cover
+
+ :returns: The size used
+ :rtype: Art.Size
+ """
+ return self._size
+
+ @size.setter
+ def size(self, value):
+ """Set the cover size
+
+ :param Art.Size value: The size to use for the cover
+ """
+ self._size = value
+
+ icon = self._default_icon.get(
+ DefaultIcon.Type.LOADING, self.props.size, self.props.scale_factor)
+ self._loading_cover.props.surface = icon
+
+ @GObject.Property(type=CoreArtist, default=None)
+ def coreartist(self):
+ return self._coreartist
+
+ @coreartist.setter
+ def coreartist(self, coreartist):
+ self._coreartist = coreartist
+
+ print("connecting")
+ self._coreartist.connect(
+ "notify::cached-thumbnail-uri", self._on_thumbnail_changed)
+
+ if self._coreartist.props.cached_thumbnail_uri is not None:
+ self._on_thumbnail_changed(self._coreartist, None)
+
+ def _on_thumbnail_changed(self, coreartist, uri):
+ print("thumbnail changed")
+ cache = ArtistCache()
+ cache.connect("hit", self._on_cache_hit)
+
+ cache.query(coreartist)
+
+ def _on_cache_hit(self, cache, surface):
+ print("surface", surface)
+ if self._active_child == "B":
+ self._cover_a.props.surface = surface
+ self.props.visible_child_name = "A"
+ else:
+ self._cover_b.props.surface = surface
+ self.props.visible_child_name = "B"
+
+ @log
+ def update(self, coresong):
+ """Update the stack with the given CoreSong
+
+ Update the stack with the art retrieved from the given Coresong.
+ :param CoreSong coresong: The CoreSong object
+ """
+ if self._handler_id and self._art:
+ # Remove a possible dangling "finished" callback if update
+ # is called again, but it is still looking for the previous
+ # art.
+ self._art.disconnect(self._handler_id)
+ # Set the loading state only after a delay to make between
+ # song transitions smooth if loading time is negligible.
+ self._timeout = GLib.timeout_add(100, self._set_loading_child)
+
+ self._active_child = self.props.visible_child_name
+
+ self._art = Art(self.props.size, coresong, self.props.scale_factor)
+ self._handler_id = self._art.connect("finished", self._art_retrieved)
+ self._art.lookup()
+
+ @log
+ def _set_loading_child(self):
+ self.props.visible_child_name = "loading"
+ self._active_child = self.props.visible_child_name
+ self._timeout = None
+
+ return GLib.SOURCE_REMOVE
+
+ @log
+ def _art_retrieved(self, klass):
+ if self._timeout:
+ GLib.source_remove(self._timeout)
+ self._timeout = None
+
+ if self._active_child == "B":
+ self._cover_a.props.surface = klass.surface
+ self.props.visible_child_name = "A"
+ else:
+ self._cover_b.props.surface = klass.surface
+ self.props.visible_child_name = "B"
+
+ self._active_child = self.props.visible_child_name
+ self._art = None
+
+ self.emit("updated")
+<
[
Date Prev][
Date Next] [
Thread Prev][
Thread Next]
[
Thread Index]
[
Date Index]
[
Author Index]