[gnome-music/wip/mschraal/artrework: 2/7] albumartcache: Rewrite
- From: Marinus Schraal <mschraal src gnome org>
- To: commits-list gnome org
- Cc:
- Subject: [gnome-music/wip/mschraal/artrework: 2/7] albumartcache: Rewrite
- Date: Mon, 5 Feb 2018 00:01:34 +0000 (UTC)
commit 3c44d622ca8c8361c42e05d7011453236d460176
Author: Marinus Schraal <mschraal gnome org>
Date: Fri Jan 5 13:54:49 2018 +0100
albumartcache: Rewrite
The former artworkcache was a monolithic method filled with callbacks,
hard to debug and comprehend. It also left quite a bit for the caller to
take care of.
The current design is that Music has an Art object that is specific to
one cairo.Surface or Gtk.Image (ArtImage) as requested. The Art object
takes care of retrieving the correct image and emits a signal
(cairo.Surface) or updates the Gtk.Image when done. This leads to less
art related code in the views and widgets.
The lookup process itself is now clearly divided into several steps:
1. (Cache) libmediaart cache lookup
2. (EmbeddedArt) local lookup
1. tags (gstreamer)
2. coverart in the directory (libmediaart)
3. (RemoteArt) remote lookup through Grilo coverart providers
Using a cairo.Surface in the Gtk.TreeView pixbuf renderer also allows
for HiDPI art in SearchView.
For simplicity and cleanliness, all art related calls have been removed
from BaseView as they were unused and there is no plan to bring it back
to BaseView.
Closes: #65
gnomemusic/albumartcache.py | 754 ++++++++++++++++++++------------
gnomemusic/player.py | 17 +-
gnomemusic/views/albumsview.py | 15 +-
gnomemusic/views/baseview.py | 27 +-
gnomemusic/views/initialstateview.py | 4 +-
gnomemusic/views/searchview.py | 84 ++--
gnomemusic/widgets/albumwidget.py | 23 +-
gnomemusic/widgets/artistalbumwidget.py | 22 +-
8 files changed, 541 insertions(+), 405 deletions(-)
---
diff --git a/gnomemusic/albumartcache.py b/gnomemusic/albumartcache.py
index f9954ca..a9cdb77 100644
--- a/gnomemusic/albumartcache.py
+++ b/gnomemusic/albumartcache.py
@@ -1,9 +1,4 @@
-# Copyright (c) 2013 Vadim Rutkovsky <vrutkovs redhat com>
-# Copyright (c) 2013 Arnel A. Borja <kyoushuu yahoo com>
-# Copyright (c) 2013 Seif Lotfy <seif lotfy com>
-# Copyright (c) 2013 Guillaume Quintard <guillaume quintard gmail com>
-# Copyright (c) 2013 Lubosz Sarnecki <lubosz gmail com>
-# Copyright (c) 2013 Sai Suman Prayaga <suman sai14 gmail com>
+# Copyright © 2018 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
@@ -46,41 +41,6 @@ import gnomemusic.utils as utils
logger = logging.getLogger(__name__)
-class LookupQueue(object):
- """A queue for IO operations"""
-
- _max_simultaneous_lookups = 12
- _lookup_queue = []
- _n_lookups = 0
-
- @classmethod
- @log
- def push(cls, cache, item, art_size, callback, itr):
- """Push a lookup counter or queue the lookup if needed"""
-
- # If reached the limit, queue the operation.
- if cls._n_lookups >= cls._max_simultaneous_lookups:
- cls._lookup_queue.append((cache, item, art_size, callback, itr))
- return False
- else:
- cls._n_lookups += 1
- return True
-
- @classmethod
- @log
- def pop(cls):
- """Pops a lookup counter and consume the lookup queue if needed"""
-
- cls._n_lookups -= 1
-
- # An available lookup slot appeared! Let's continue looking up
- # artwork then.
- if (cls._n_lookups < cls._max_simultaneous_lookups
- and cls._lookup_queue):
- (cache, item, art_size, callback, itr) = cls._lookup_queue.pop(0)
- cache.lookup(item, art_size, callback, itr)
-
-
@log
def _make_icon_frame(pixbuf, art_size=None, scale=1):
border = 3 * scale
@@ -136,20 +96,6 @@ def _make_icon_frame(pixbuf, art_size=None, scale=1):
return surface
-class ArtSize(Enum):
- """Enum for icon sizes"""
- XSMALL = (34, 34)
- SMALL = (48, 48)
- MEDIUM = (128, 128)
- LARGE = (256, 256)
- XLARGE = (512, 512)
-
- def __init__(self, width, height):
- """Intialize width and height"""
- self.width = width
- self.height = height
-
-
class DefaultIcon(GObject.GObject):
"""Provides the symbolic fallback and loading icons."""
@@ -158,21 +104,18 @@ class DefaultIcon(GObject.GObject):
music = 'folder-music-symbolic'
_cache = {}
- _scale = 1
def __repr__(self):
return '<DefaultIcon>'
@log
- def __init__(self, scale=1):
+ def __init__(self):
super().__init__()
- self._scale = scale
-
@log
- def _make_default_icon(self, icon_type, art_size=None):
- width = art_size.width * self._scale
- height = art_size.height * self._scale
+ def _make_default_icon(self, icon_type, art_size, scale):
+ width = art_size.width * scale
+ height = art_size.height * scale
icon = Gtk.IconTheme.get_default().load_icon(icon_type.value,
max(width, height) / 4,
@@ -195,177 +138,387 @@ class DefaultIcon(GObject.GObject):
icon.get_height() * 3 / 2,
1, 1, GdkPixbuf.InterpType.HYPER, 0x33)
- icon_surface = _make_icon_frame(result, art_size, self._scale)
+ icon_surface = _make_icon_frame(result, art_size, scale)
return icon_surface
@log
- def get(self, icon_type, art_size):
+ def get(self, icon_type, art_size, scale=1):
"""Returns the requested symbolic icon
- Returns a GdkPixbuf of the requested symbolic icon
- in the given size.
+ 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 ArtSize requested
+ :param enum art_size: The Art.Size requested
:return: The symbolic icon
- :rtype: GdkPixbuf
+ :rtype: cairo.Surface
"""
- if (icon_type, art_size) not in self._cache.keys():
- new_icon = self._make_default_icon(icon_type, art_size)
- self._cache[(icon_type, art_size)] = new_icon
+ 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)]
+ return self._cache[(icon_type, art_size, scale)]
-class AlbumArtCache(GObject.GObject):
- """Album art retrieval class
+class Art(GObject.GObject):
+ """Retrieves art for an album or song
- On basis of a given media item looks up album art in the following order:
- 1) already existing in cache
- 2) from embedded images
- 3) from local images
- 3) remotely
+ This is the control class for retrieving art.
+ It looks for art in
+ 1. The MediaArt cache
+ 2. Embedded or in the directory
+ 3. Remotely
"""
- _instance = None
- blacklist = {}
- _scale = 1
+
+ __gsignals__ = {
+ 'finished': (GObject.SignalFlags.RUN_FIRST, None, ())
+ }
+
+ _blacklist = {}
+
+ class Size(Enum):
+ """Enum for icon sizes"""
+ XSMALL = (34, 34)
+ SMALL = (48, 48)
+ MEDIUM = (128, 128)
+ LARGE = (256, 256)
+ XLARGE = (512, 512)
+
+ def __init__(self, width, height):
+ """Intialize width and height"""
+ self.width = width
+ self.height = height
def __repr__(self):
- return '<AlbumArtCache>'
+ return '<Art>'
@log
- def __init__(self, scale=1):
+ def __init__(self, size, media, scale=1):
super().__init__()
+ self._size = size
+ self._media = media
+ self._media_url = self._media.get_url()
+ self._surface = None
self._scale = scale
+ @log
+ def lookup(self):
+ """Starts the art lookup sequence"""
+ if self._in_blacklist():
+ self._no_art_available()
+ return
+
+ cache = Cache()
+ cache.connect('miss', self._cache_miss)
+ cache.connect('hit', self._cache_hit)
+ cache.query(self._media)
+
+ @log
+ def _cache_miss(self, klass):
+ embedded_art = EmbeddedArt()
+ embedded_art.connect('found', self._embedded_art_found)
+ embedded_art.connect('unavailable', self._embedded_art_unavailable)
+ embedded_art.query(self._media)
+
+ @log
+ def _cache_hit(self, klass, pixbuf):
+ surface = _make_icon_frame(pixbuf, self._size, self._scale)
+ self._surface = surface
+
+ self.emit('finished')
+
+ @log
+ def _embedded_art_found(self, klass):
+ cache = Cache()
+ cache.connect('miss', self._cache_miss)
+ cache.connect('hit', self._cache_hit)
+ cache.query(self._media)
+
+ @log
+ def _embedded_art_unavailable(self, klass):
+ remote_art = RemoteArt()
+ remote_art.connect('retrieved', self._remote_art_retrieved)
+ remote_art.connect('unavailable', self._remote_art_unavailable)
+ remote_art.query(self._media)
+
+ @log
+ def _remote_art_retrieved(self, klass):
+ cache = Cache()
+ cache.connect('miss', self._cache_miss)
+ cache.connect('hit', self._cache_hit)
+ cache.query(self._media)
+
+ @log
+ def _remote_art_unavailable(self, klass):
+ self._add_to_blacklist()
+ self._no_art_available()
+
+ @log
+ def _no_art_available(self):
+ self._surface = DefaultIcon().get(
+ DefaultIcon.Type.music, self._size, self._scale)
+
+ self.emit('finished')
+
+ @log
+ def _add_to_blacklist(self):
+ album = utils.get_album_title(self._media)
+ artist = utils.get_artist_name(self._media)
+
+ if artist not in self._blacklist:
+ self._blacklist[artist] = []
+
+ album_stripped = MediaArt.strip_invalid_entities(album)
+ self._blacklist[artist].append(album_stripped)
+
+ @log
+ def _in_blacklist(self):
+ album = utils.get_album_title(self._media)
+ artist = utils.get_artist_name(self._media)
+ album_stripped = MediaArt.strip_invalid_entities(album)
+
+ if artist in self._blacklist:
+ if album_stripped in self._blacklist[artist]:
+ return True
+
+ return False
+
+ @GObject.Property
+ @log
+ def surface(self):
+ if self._surface is None:
+ self._surface = DefaultIcon().get(
+ DefaultIcon.Type.loading, self._size, self._scale)
+
+ return self._surface
+
+
+class ArtImage(Art):
+ """Extends Art class to support Gtk.Image specifically"""
+
+ def __repr__(self):
+ return '<ArtImage>'
+
+ @log
+ def __init__(self, size, media):
+ super().__init__(size, media)
+
+ self._image = None
+
+ @log
+ def _cache_hit(self, klass, pixbuf):
+ super()._cache_hit(klass, pixbuf)
+
+ self._image.set_from_surface(self._surface)
+
+ @log
+ def _no_art_available(self):
+ super()._no_art_available()
+
+ self._image.set_from_surface(self._surface)
+
+ @GObject.Property
+ @log
+ def image(self):
+ """Returns the image object of the ArtImage class
+
+ :returns: The current image available in the class
+ :rtype: Gtk.Image
+ """
+
+ return self._image.set_from_surface(self._surface)
+
+ @image.setter
+ @log
+ def image(self, image):
+ """Set the image of the Art class instance""
+
+ And starts the lookup process, automatically updating the image
+ when found.
+ :param Gtk.Image image: An Gtk.Image object
+ """
+
+ self._image = image
+
+ self._image.set_property("width-request", self._size.width)
+ self._image.set_property("height-request", self._size.height)
+
+ self._scale = self._image.get_scale_factor()
+
+ self._surface = DefaultIcon().get(
+ DefaultIcon.Type.loading, self._size, self._scale)
+
+ self._image.set_from_surface(self._surface)
+
+ self.lookup()
+
+
+class Cache(GObject.GObject):
+ """Handles retrieval of MediaArt cache art
+
+ Uses signals to indicate success or failure.
+ """
+
+ __gsignals__ = {
+ 'miss': (GObject.SignalFlags.RUN_FIRST, None, ()),
+ 'hit': (GObject.SignalFlags.RUN_FIRST, None, (GObject.GObject, ))
+ }
+
+ def __repr__(self):
+ return '<Cache>'
+
+ @log
+ def __init__(self):
+ super().__init__()
+
+ self._media_art = MediaArt.Process.new()
+
+ # 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 Exception as err:
- logger.warn("Error: %s, %s", err.__class__, err)
+ except GLib.Error as error:
+ logger.warn(
+ "Error: {}, {}".format(error.domain, error.message))
return
- Gst.init(None)
- self._discoverer = GstPbutils.Discoverer.new(Gst.SECOND)
- self._discoverer.connect('discovered', self._discovered_cb)
- self._discoverer.start()
+ @log
+ def query(self, media):
+ """Start the cache query
- self._discoverer_items = {}
+ :param Grl.Media media: The media object to search art for
+ """
+ album = utils.get_album_title(media)
+ artist = utils.get_artist_name(media)
- self._media_art = None
- try:
- self._media_art = MediaArt.Process.new()
- except Exception as err:
- logger.warn("Error: %s, %s", err.__class__, err)
+ success, thumb_file = MediaArt.get_file(artist, album, "album")
+
+ if (success
+ and thumb_file.query_exists()):
+ thumb_file.read_async(
+ GLib.PRIORITY_LOW, None, self._open_stream, None)
+ return
+
+ self.emit('miss')
@log
- def lookup(self, item, art_size, callback, itr):
- """Find art for the given item
+ def _open_stream(self, thumb_file, result, arguments):
+ try:
+ stream = thumb_file.read_finish(result)
+ except GLib.Error as error:
+ logger.warn("Error: {}, {}".format(error.domain, error.message))
+ stream.close_async(
+ GLib.PRIORITY_LOW, None, self._close_stream, None)
+ self.emit('miss')
+ return
- :param item: Grilo media item
- :param ArtSize art_size: Size of the icon
- :param callback: Callback function when retrieved
- :param itr: Iter to return with callback
- """
- if LookupQueue.push(self, item, art_size, callback, itr):
- self._lookup_local(item, art_size, callback, itr)
+ GdkPixbuf.Pixbuf.new_from_stream_async(
+ stream, None, self._pixbuf_loaded, None)
@log
- def _lookup_local(self, item, art_size, callback, itr):
- """Checks if there is already a local art file, if not calls
- the embedded lookup function"""
- album = utils.get_album_title(item)
- artist = utils.get_artist_name(item)
+ def _pixbuf_loaded(self, stream, result, data):
+ try:
+ pixbuf = GdkPixbuf.Pixbuf.new_from_stream_finish(result)
+ except GLib.Error as error:
+ logger.warn("Error: {}, {}".format(error.domain, error.message))
+ stream.close_async(
+ GLib.PRIORITY_LOW, None, self._close_stream, None)
+ self.emit('miss')
+ return
- def stream_open(thumb_file, result, arguments):
- try:
- stream = thumb_file.read_finish(result)
- except Exception as err:
- logger.warn("Error: %s, %s", err.__class__, err)
- do_callback(None)
- return
+ stream.close_async(GLib.PRIORITY_LOW, None, self._close_stream, None)
+ self.emit('hit', pixbuf)
- GdkPixbuf.Pixbuf.new_from_stream_async(stream,
- None,
- pixbuf_loaded,
- None)
+ @log
+ def _close_stream(self, stream, result, data):
+ try:
+ stream.close_finish(result)
+ except GLib.Error as error:
+ logger.warn("Error: {}, {}".format(error.domain, error.message))
- def pixbuf_loaded(stream, result, data):
- try:
- pixbuf = GdkPixbuf.Pixbuf.new_from_stream_finish(result)
- except Exception as err:
- logger.warn("Error: %s, %s", err.__class__, err)
- do_callback(None)
- return
- do_callback(pixbuf)
- return
+class EmbeddedArt(GObject.GObject):
+ """Lookup local art
- def do_callback(pixbuf):
+ 1. Embedded through Gstreamer
+ 2. Available in the directory through MediaArt
+ """
- # Lookup finished, decrease the counter
- LookupQueue.pop()
+ __gsignals__ = {
+ 'found': (GObject.SignalFlags.RUN_FIRST, None, ()),
+ 'unavailable': (GObject.SignalFlags.RUN_FIRST, None, ())
+ }
- if not pixbuf:
- surface = DefaultIcon(self._scale).get(DefaultIcon.Type.music,
- art_size)
- else:
- surface = _make_icon_frame(pixbuf, art_size, self._scale)
+ def __repr__(self):
+ return '<EmbeddedArt>'
- # Sets the thumbnail location for MPRIS to use.
- item.set_thumbnail(GLib.filename_to_uri(thumb_file.get_path(),
- None))
+ @log
+ def __init__(self):
+ super().__init__()
- GLib.idle_add(callback, surface, itr)
+ try:
+ Gst.init(None)
+ GstPbutils.pb_utils_init()
+ except GLib.Error as error:
+ logger.warn("Error: {}, {}".format(error.domain, error.message))
return
- success, thumb_file = MediaArt.get_file(artist, album, "album")
+ self._media_art = MediaArt.Process.new()
- if (success
- and thumb_file.query_exists()):
- thumb_file.read_async(GLib.PRIORITY_LOW,
- None,
- stream_open,
- None)
+ self._album = None
+ self._artist = None
+ self._media = None
+ self._path = None
+
+ @log
+ def query(self, media):
+ """Start the local query
+
+ :param Grl.Media media: The media object to search art for
+ """
+ if media.get_url() is None:
+ self.emit('unavailable')
return
- stripped_album = MediaArt.strip_invalid_entities(album)
- if (artist in self.blacklist
- and stripped_album in self.blacklist[artist]):
- do_callback(None)
+ self._album = utils.get_album_title(media)
+ self._artist = utils.get_artist_name(media)
+ self._media = media
+
+ try:
+ discoverer = GstPbutils.Discoverer.new(Gst.SECOND)
+ except GLib.Error as error:
+ logger.warn("Error: {}, {}".format(error.domain, error.message))
+ self._lookup_cover_in_directory()
return
- # When we reach here because it fails to retrieve the artwork,
- # do a long round trip (either through _lookup_embedded or
- # _lookup_remote) and call self.lookup() again. Thus, decrease
- # global lookup counter.
- LookupQueue.pop()
+ discoverer.connect('discovered', self._discovered)
+ discoverer.start()
- self._lookup_embedded(item, art_size, callback, itr)
+ success, path = MediaArt.get_path(self._artist, self._album, "album")
- @log
- def _discovered_cb(self, discoverer, info, error):
- item, art_size, callback, itr, cache_path = \
- self._discoverer_items[info.get_uri()]
+ if not success:
+ self.emit('unavailable')
+ discoverer.stop()
+ return
- album = utils.get_album_title(item)
- artist = utils.get_artist_name(item)
- tags = info.get_tags()
- index = 0
+ self._path = path
- def art_retrieved(result):
- if not result:
- if artist not in self.blacklist:
- self.blacklist[artist] = []
+ success = discoverer.discover_uri_async(self._media.get_url())
- album_stripped = MediaArt.strip_invalid_entities(album)
- self.blacklist[artist].append(album_stripped)
+ if not success:
+ logger.warn("Could not add url to discoverer.")
+ self.emit('unavailable')
+ discoverer.stop()
+ return
- self.lookup(item, art_size, callback, itr)
+ @log
+ def _discovered(self, discoverer, info, error):
+ tags = info.get_tags()
+ index = 0
# FIXME: tags should not return as None, but it sometimes is.
# So as a workaround until we figure out what is wrong check
@@ -373,7 +526,11 @@ class AlbumArtCache(GObject.GObject):
# https://bugzilla.gnome.org/show_bug.cgi?id=780980
if (error is not None
or tags is None):
- art_retrieved(False)
+ if error:
+ logger.warn(
+ "Discoverer error: {}, {}", error.domain, error.message)
+ discoverer.stop()
+ self.emit('unavailable')
return
while True:
@@ -382,8 +539,8 @@ class AlbumArtCache(GObject.GObject):
break
index += 1
struct = sample.get_info()
- success, image_type = struct.get_enum('image-type',
- GstTag.TagImageType)
+ success, image_type = struct.get_enum(
+ 'image-type', GstTag.TagImageType)
if not success:
continue
if image_type != GstTag.TagImageType.FRONT_COVER:
@@ -396,143 +553,164 @@ class AlbumArtCache(GObject.GObject):
try:
mime = sample.get_caps().get_structure(0).get_name()
- MediaArt.buffer_to_jpeg(map_info.data, mime, cache_path)
- art_retrieved(True)
+ MediaArt.buffer_to_jpeg(map_info.data, mime, self._path)
+ self.emit('found')
+ discoverer.stop()
return
- except Exception as err:
- logger.warn("Error: %s, %s", err.__class__, err)
+ except GLib.Error as error:
+ logger.warn("Error: {}, {}".format(
+ MediaArt.Error(error.code), error.message))
+
+ discoverer.stop()
+ self._lookup_cover_in_directory()
+
+ @log
+ def _lookup_cover_in_directory(self):
+ # Find local art in cover.jpeg files.
+ self._media_art.uri_async(
+ MediaArt.Type.ALBUM, MediaArt.ProcessFlags.NONE,
+ self._media.get_url(), self._artist, self._album,
+ GLib.PRIORITY_LOW, None, self._uri_async_cb, None)
+
+ @log
+ def _uri_async_cb(self, src, result, data):
try:
- self._media_art.uri(MediaArt.Type.ALBUM,
- MediaArt.ProcessFlags.NONE, item.get_url(),
- artist, album, None)
- if os.path.exists(cache_path):
- art_retrieved(True)
+ success = self._media_art.uri_finish(result)
+ if success:
+ self.emit('found')
return
- except Exception as err:
- logger.warn("Trying to process misc albumart: %s, %s",
- err.__class__, err)
+ except GLib.Error as error:
+ if MediaArt.Error(error.code) == MediaArt.Error.SYMLINK_FAILED:
+ # This error indicates that the coverart has already
+ # been linked by another concurrent lookup.
+ self.emit('found')
+ else:
+ logger.warning("Error: {}, {}".format(
+ MediaArt.Error(error.code), error.message))
- self._lookup_remote(item, art_size, callback, itr)
+ self.emit('unavailable')
- @log
- def _lookup_embedded(self, item, art_size, callback, itr):
- """Lookup embedded cover
- Lookup embedded art through Gst.Discoverer. If found
- copy locally and call _lookup_local to finish retrieving
- suitable art, otherwise follow up with _lookup_remote.
- """
- album = utils.get_album_title(item)
- artist = utils.get_artist_name(item)
+class RemoteArt(GObject.GObject):
+ """Looks for remote art through Grilo
- success, cache_path = MediaArt.get_path(artist, album, "album")
- if not success:
- self._lookup_remote(item, art_size, callback, itr)
+ Uses Grilo coverart providers to retrieve art.
+ """
+
+ __gsignals__ = {
+ 'retrieved': (GObject.SignalFlags.RUN_FIRST, None, ()),
+ 'unavailable': (GObject.SignalFlags.RUN_FIRST, None, ())
+ }
- self._discoverer_items[item.get_url()] = [item, art_size, callback,
- itr, cache_path]
- self._discoverer.discover_uri_async(item.get_url())
+ def __repr__(self):
+ return '<RemoteArt>'
@log
- def _lookup_remote(self, item, art_size, callback, itr):
- """Lookup remote art
+ def __init__(self):
+ super().__init__()
+
+ self._artist = None
+ self._album = None
- Lookup remote art through Grilo and if found copy locally. Call
- _lookup_local to finish retrieving suitable art.
+ @log
+ def query(self, media):
+ """Start the remote query
+
+ :param Grl.Media media: The media object to search art for
"""
- album = utils.get_album_title(item)
- artist = utils.get_artist_name(item)
+ self._album = utils.get_album_title(media)
+ self._artist = utils.get_artist_name(media)
- @log
- def delete_cb(src, result, data):
- try:
- src.delete_finish(result)
- except Exception as err:
- logger.warn("Error: %s, %s", err.__class__, err)
+ # FIXME: It seems this Grilo query does not always return,
+ # especially on queries with little info.
+ grilo.get_album_art_for_item(media, self._remote_album_art)
- @log
- def splice_cb(src, result, data):
- tmp_file, iostream = data
+ @log
+ def _delete_callback(self, src, result, data):
+ try:
+ src.delete_finish(result)
+ except GLib.Error as error:
+ logger.warn("Error: {}, {}".format(error.domain, error.message))
- try:
- src.splice_finish(result)
- except Exception as err:
- logger.warn("Error: %s, %s", err.__class__, err)
- art_retrieved(False)
- return
+ @log
+ def _splice_callback(self, src, result, data):
+ tmp_file, iostream = data
- success, cache_path = MediaArt.get_path(artist, album, "album")
- try:
- # FIXME: I/O blocking
- MediaArt.file_to_jpeg(tmp_file.get_path(), cache_path)
- except Exception as err:
- logger.warn("Error: %s, %s", err.__class__, err)
- art_retrieved(False)
- return
+ iostream.close_async(
+ GLib.PRIORITY_LOW, None, self._close_iostream_callback, None)
+
+ try:
+ src.splice_finish(result)
+ except GLib.Error as error:
+ logger.warn("Error: {}, {}".format(error.domain, error.message))
+ self.emit('unavailable')
+ return
- art_retrieved(True)
+ success, cache_path = MediaArt.get_path(
+ self._artist, self._album, "album")
- tmp_file.delete_async(GLib.PRIORITY_LOW,
- None,
- delete_cb,
- None)
+ if not success:
+ self.emit('unavailable')
+ return
- @log
- def async_read_cb(src, result, data):
- try:
- istream = src.read_finish(result)
- except Exception as err:
- logger.warn("Error: %s, %s", err.__class__, err)
- art_retrieved(False)
- return
+ try:
+ # FIXME: I/O blocking
+ MediaArt.file_to_jpeg(tmp_file.get_path(), cache_path)
+ except GLib.Error as error:
+ logger.warn("Error: {}, {}".format(error.domain, error.message))
+ self.emit('unavailable')
+ return
- try:
- [tmp_file, iostream] = Gio.File.new_tmp()
- except Exception as err:
- logger.warn("Error: %s, %s", err.__class__, err)
- art_retrieved(False)
- return
+ self.emit('retrieved')
- 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,
- splice_cb,
- [tmp_file, iostream])
-
- @log
- def album_art_for_item_cb(source, param, item, count, error):
- if error:
- logger.warn("Grilo error %s", error)
- art_retrieved(False)
- return
+ tmp_file.delete_async(
+ GLib.PRIORITY_LOW, None, self._delete_callback, None)
- thumb_uri = item.get_thumbnail()
+ @log
+ def _close_iostream_callback(self, src, result, data):
+ try:
+ src.close_finish(result)
+ except GLib.Error as error:
+ logger.warn("Error: {}, {}".format(error.domain, error.message))
- if thumb_uri is None:
- art_retrieved(False)
- return
+ @log
+ def _read_callback(self, src, result, data):
+ try:
+ istream = src.read_finish(result)
+ except GLib.Error as error:
+ logger.warn("Error: {}, {}".format(error.domain, error.message))
+ self.emit('unavailable')
+ return
- src = Gio.File.new_for_uri(thumb_uri)
- src.read_async(GLib.PRIORITY_LOW,
- None,
- async_read_cb,
- None)
+ try:
+ [tmp_file, iostream] = Gio.File.new_tmp()
+ except GLib.Error as error:
+ logger.warn("Error: {}, {}".format(error.domain, error.message))
+ self.emit('unavailable')
+ return
- @log
- def art_retrieved(result):
- if not result:
- if artist not in self.blacklist:
- self.blacklist[artist] = []
+ 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])
- album_stripped = MediaArt.strip_invalid_entities(album)
- self.blacklist[artist].append(album_stripped)
+ @log
+ def _remote_album_art(self, source, param, item, count, error):
+ if error:
+ logger.warn("Grilo error {}".format(error))
+ self.emit('unavailable')
+ return
- self.lookup(item, art_size, callback, itr)
+ thumb_uri = item.get_thumbnail()
+
+ if thumb_uri is None:
+ self.emit('unavailable')
+ return
- grilo.get_album_art_for_item(item, album_art_for_item_cb)
+ src = Gio.File.new_for_uri(thumb_uri)
+ src.read_async(
+ GLib.PRIORITY_LOW, None, self._read_callback, None)
diff --git a/gnomemusic/player.py b/gnomemusic/player.py
index e55ef09..6a5a800 100644
--- a/gnomemusic/player.py
+++ b/gnomemusic/player.py
@@ -46,7 +46,7 @@ from gi.repository import Gtk, GLib, Gio, GObject, Gst, GstAudio, GstPbutils
from gettext import gettext as _, ngettext
from gnomemusic import log
-from gnomemusic.albumartcache import AlbumArtCache, DefaultIcon, ArtSize
+from gnomemusic.albumartcache import Art, ArtImage
from gnomemusic.grilo import grilo
from gnomemusic.playlists import Playlists
from gnomemusic.scrobbler import LastFmScrobbler
@@ -109,11 +109,6 @@ class Player(GObject.GObject):
self.playlistField = None
self.currentTrack = None
self.currentTrackUri = None
- scale = parent_window.get_scale_factor()
- self.cache = AlbumArtCache(scale)
- self._loading_icon_surface = DefaultIcon(scale).get(
- DefaultIcon.Type.loading,
- ArtSize.XSMALL)
self._missingPluginMessages = []
Gst.init(None)
@@ -584,8 +579,8 @@ class Player(GObject.GObject):
artist = utils.get_artist_name(media)
self.artistLabel.set_label(artist)
- self.coverImg.set_from_surface(self._loading_icon_surface)
- self.cache.lookup(media, ArtSize.XSMALL, self._on_cache_lookup, None)
+ art = ArtImage(Art.Size.XSMALL, media)
+ art.image = self._image
title = utils.get_media_title(media)
self.titleLabel.set_label(title)
@@ -639,7 +634,7 @@ class Player(GObject.GObject):
@log
def _on_cache_lookup(self, surface, data=None):
- self.coverImg.set_from_surface(surface)
+ # FIXME: Need this for mpris
self.emit('thumbnail-updated')
@log
@@ -780,9 +775,7 @@ class Player(GObject.GObject):
self.songTotalTimeLabel = self._ui.get_object('duration')
self.titleLabel = self._ui.get_object('title')
self.artistLabel = self._ui.get_object('artist')
- self.coverImg = self._ui.get_object('cover')
- self.coverImg.set_property("width-request", ArtSize.XSMALL.width)
- self.coverImg.set_property("height-request", ArtSize.XSMALL.height)
+ self._image = self._ui.get_object('cover')
self.duration = self._ui.get_object('duration')
self.repeatBtnImage = self._ui.get_object('playlistRepeat')
diff --git a/gnomemusic/views/albumsview.py b/gnomemusic/views/albumsview.py
index 1538a59..3d2936e 100644
--- a/gnomemusic/views/albumsview.py
+++ b/gnomemusic/views/albumsview.py
@@ -26,7 +26,7 @@ from gettext import gettext as _
from gi.repository import GLib, GObject, Gtk, Gdk
from gnomemusic import log
-from gnomemusic.albumartcache import ArtSize
+from gnomemusic.albumartcache import Art, ArtImage
from gnomemusic.grilo import grilo
from gnomemusic.toolbar import ToolbarState
from gnomemusic.views.baseview import BaseView
@@ -167,13 +167,6 @@ class AlbumsView(BaseView):
child.title.set_label(title)
child.subtitle.set_label(artist)
- child.image.set_from_surface(self._loading_icon_surface)
- # In the case of off-sized icons (eg. provided in the soundfile)
- # keep the size request equal to all other icons to get proper
- # alignment with GtkFlowBox.
- child.image.set_property("width-request", ArtSize.MEDIUM.width)
- child.image.set_property("height-request", ArtSize.MEDIUM.height)
-
child.events.add_events(Gdk.EventMask.TOUCH_MASK)
child.events.connect('button-release-event',
@@ -190,7 +183,8 @@ class AlbumsView(BaseView):
child.add(builder.get_object('main_box'))
child.show()
- self._cache.lookup(item, ArtSize.MEDIUM, self._on_lookup_ready, child)
+ art = ArtImage(Art.Size.MEDIUM, item)
+ art.image = child.image
return child
@@ -201,9 +195,6 @@ class AlbumsView(BaseView):
if self.selection_mode:
child.check.set_active(True)
- def _on_lookup_ready(self, icon, child):
- child.image.set_from_surface(icon)
-
@log
def _on_child_toggled(self, check, pspec, child):
if (check.get_active()
diff --git a/gnomemusic/views/baseview.py b/gnomemusic/views/baseview.py
index 8048bf9..a7afeb7 100644
--- a/gnomemusic/views/baseview.py
+++ b/gnomemusic/views/baseview.py
@@ -23,10 +23,9 @@
# delete this exception statement from your version.
from gettext import gettext as _, ngettext
-from gi.repository import Gd, Gdk, GdkPixbuf, GObject, Gtk
+from gi.repository import Gd, GdkPixbuf, GObject, Gtk
from gnomemusic import log
-from gnomemusic.albumartcache import AlbumArtCache, DefaultIcon, ArtSize
from gnomemusic.grilo import grilo
from gnomemusic.widgets.starhandlerwidget import StarHandlerWidget
import gnomemusic.utils as utils
@@ -107,11 +106,6 @@ class BaseView(Gtk.Stack):
self.show_all()
self._view.hide()
- scale = self.get_scale_factor()
- self._cache = AlbumArtCache(scale)
- self._loading_icon_surface = DefaultIcon(scale).get(
- DefaultIcon.Type.loading, ArtSize.MEDIUM)
-
self._init = False
grilo.connect('ready', self._on_grilo_ready)
self._header_bar.connect('selection-mode-changed',
@@ -211,6 +205,10 @@ class BaseView(Gtk.Stack):
def populate(self):
pass
+ @log
+ def _retrieval_finished(self, klass):
+ self.model[klass.iter][4] = klass.pixbuf
+
@log
def _add_item(self, source, param, item, remaining=0, data=None):
if not item:
@@ -224,30 +222,17 @@ class BaseView(Gtk.Stack):
title = utils.get_media_title(item)
itr = self.model.append(None)
- loading_icon = Gdk.pixbuf_get_from_surface(
- self._loadin_icon_surface, 0, 0,
- self._loading_icon_surface.get_width(),
- self._loading_icon_surface.get_height())
- self.model[itr][0, 1, 2, 3, 4, 5, 7, 9] = [
+ self.model[itr][0, 1, 2, 3, 5, 7, 9] = [
str(item.get_id()),
'',
title,
artist,
- loading_icon,
item,
0,
False
]
- @log
- def _on_lookup_ready(self, surface, itr):
- if surface:
- pixbuf = Gdk.pixbuf_get_from_surface(surface, 0, 0,
- surface.get_width(),
- surface.get_height())
- self.model[itr][4] = pixbuf
-
@log
def _add_list_renderers(self):
pass
diff --git a/gnomemusic/views/initialstateview.py b/gnomemusic/views/initialstateview.py
index d005537..e06d065 100644
--- a/gnomemusic/views/initialstateview.py
+++ b/gnomemusic/views/initialstateview.py
@@ -25,7 +25,7 @@
from gettext import gettext as _
from gnomemusic import log
-from gnomemusic.albumartcache import ArtSize
+from gnomemusic.albumartcache import Art
from gnomemusic.views.emptyview import EmptyView
@@ -43,7 +43,7 @@ class InitialStateView(EmptyView):
icon.set_margin_bottom(32)
icon.set_opacity(1)
icon.set_from_resource('/org/gnome/Music/initial-state.png')
- icon.set_size_request(ArtSize.LARGE.width, ArtSize.LARGE.height)
+ icon.set_size_request(Art.Size.LARGE.width, Art.Size.LARGE.height)
# Update label
label = self.builder.get_object('label')
diff --git a/gnomemusic/views/searchview.py b/gnomemusic/views/searchview.py
index 592ed06..ae97518 100644
--- a/gnomemusic/views/searchview.py
+++ b/gnomemusic/views/searchview.py
@@ -23,9 +23,9 @@
# delete this exception statement from your version.
from gettext import gettext as _
-from gi.repository import Gd, Gdk, GdkPixbuf, GObject, Grl, Gtk, Pango
+from gi.repository import Gd, GdkPixbuf, GObject, Grl, Gtk, Pango
-from gnomemusic.albumartcache import DefaultIcon, ArtSize
+from gnomemusic.albumartcache import Art
from gnomemusic.grilo import grilo
from gnomemusic import log
from gnomemusic.player import DiscoveryStatus
@@ -54,13 +54,6 @@ class SearchView(BaseView):
def __init__(self, window, player):
super().__init__('search', None, window, Gd.MainViewType.LIST)
- scale = self.get_scale_factor()
- loading_icon_surface = DefaultIcon(scale).get(
- DefaultIcon.Type.loading, ArtSize.SMALL)
- self._loading_icon = Gdk.pixbuf_get_from_surface(
- loading_icon_surface, 0, 0, loading_icon_surface.get_width(),
- loading_icon_surface.get_height())
-
self._add_list_renderers()
self.player = player
self._head_iters = [None, None, None, None]
@@ -194,6 +187,13 @@ class SearchView(BaseView):
self._albums[key].songs.append(item)
self._add_item(source, None, item, 0, [self.model, 'song'])
+ @log
+ def _retrieval_finished(self, klass, model, _iter):
+ if not model[_iter][13]:
+ return
+
+ model[_iter][13] = klass.surface
+
@log
def _add_item(self, source, param, item, remaining=0, data=None):
if data is None:
@@ -238,17 +238,12 @@ class SearchView(BaseView):
except:
pass
- # FIXME: HiDPI icon lookups return a surface that can't be
- # scaled by GdkPixbuf, so it results in a * scale factor sized
- # icon for the search view.
_iter = None
if category == 'album':
_iter = self.model.insert_with_values(
- self._head_iters[group], -1, [0, 2, 3, 4, 5, 9, 11],
- [str(item.get_id()), title, artist, self._loading_icon, item,
- 2, category])
- self._cache.lookup(
- item, ArtSize.SMALL, self._on_lookup_ready, _iter)
+ self._head_iters[group], -1, [0, 2, 3, 5, 9, 11],
+ [str(item.get_id()), title, artist, item, 2,
+ category])
elif category == 'song':
# FIXME: source specific hack
if source.get_id() != 'grl-tracker-source':
@@ -256,26 +251,30 @@ class SearchView(BaseView):
else:
fav = item.get_favourite()
_iter = self.model.insert_with_values(
- self._head_iters[group], -1, [0, 2, 3, 4, 5, 9, 11],
- [str(item.get_id()), title, artist, self._loading_icon, item,
- fav, category])
- self._cache.lookup(
- item, ArtSize.SMALL, self._on_lookup_ready, _iter)
+ self._head_iters[group], -1, [0, 2, 3, 5, 9, 11],
+ [str(item.get_id()), title, artist, item, fav,
+ category])
else:
if not artist.casefold() in self._artists:
_iter = self.model.insert_with_values(
- self._head_iters[group], -1, [0, 2, 4, 5, 9, 11],
- [str(item.get_id()), artist, self._loading_icon, item, 2,
+ self._head_iters[group], -1, [0, 2, 5, 9, 11],
+ [str(item.get_id()), artist, item, 2,
category])
- self._cache.lookup(
- item, ArtSize.SMALL, self._on_lookup_ready, _iter)
self._artists[artist.casefold()] = {
'iter': _iter,
'albums': []
}
-
self._artists[artist.casefold()]['albums'].append(item)
+ # FIXME: Figure out why iter can be None here, seems illogical.
+ if _iter is not None:
+ scale = self._view.get_scale_factor()
+ art = Art(Art.Size.SMALL, item, scale)
+ self.model[_iter][13] = art.surface
+ art.connect(
+ 'finished', self._retrieval_finished, self.model, _iter)
+ art.lookup()
+
if self.model.iter_n_children(self._head_iters[group]) == 1:
path = self.model.get_path(self._head_iters[group])
path = self._filter_model.convert_child_path_to_path(path)
@@ -295,13 +294,31 @@ class SearchView(BaseView):
title_renderer, self._on_list_widget_title_render, None)
cols[0].add_attribute(title_renderer, 'text', 2)
- self._star_handler.add_star_renderers(list_widget, cols[0])
+ # Add our own surface renderer, instead of the one provided by
+ # Gd. This avoids us having to set the model to a cairo.Surface
+ # which is currently not a working solution in pygobject.
+ # https://gitlab.gnome.org/GNOME/pygobject/issues/155
+ pixbuf_renderer = Gtk.CellRendererPixbuf(
+ xalign=0.5, yalign=0.5, xpad=12, ypad=2)
+ list_widget.add_renderer(
+ pixbuf_renderer, self._on_list_widget_pixbuf_renderer, None)
+ cols[0].add_attribute(pixbuf_renderer, 'surface', 13)
+ self._star_handler.add_star_renderers(list_widget, cols[0])
cells = cols[0].get_cells()
cols[0].reorder(cells[0], -1)
+ cols[0].reorder(cells[4], 1)
cols[0].set_cell_data_func(
cells[0], self._on_list_widget_selection_render, None)
+ @log
+ def _on_list_widget_pixbuf_renderer(self, col, cell, model, _iter, data):
+ if not model[_iter][13]:
+ return
+
+ cell.set_property("surface", model[_iter][13])
+
+ @log
def _on_list_widget_selection_render(self, col, cell, model, _iter, data):
if (self._view.get_selection_mode()
and model.iter_parent(_iter) is not None):
@@ -309,11 +326,13 @@ class SearchView(BaseView):
else:
cell.set_visible(False)
+ @log
def _on_list_widget_title_render(self, col, cell, model, _iter, data):
cells = col.get_cells()
- cells[0].set_visible(model.iter_parent(_iter) is not None)
+ cells[0].set_visible(False)
cells[1].set_visible(model.iter_parent(_iter) is not None)
- cells[2].set_visible(model.iter_parent(_iter) is None)
+ cells[2].set_visible(model.iter_parent(_iter) is not None)
+ cells[3].set_visible(model.iter_parent(_iter) is None)
@log
def populate(self):
@@ -456,7 +475,7 @@ class SearchView(BaseView):
GObject.TYPE_STRING,
GObject.TYPE_STRING, # item title or header text
GObject.TYPE_STRING, # artist for albums and songs
- GdkPixbuf.Pixbuf, # album art
+ GdkPixbuf.Pixbuf, # Gd placeholder album art
GObject.TYPE_OBJECT, # item
GObject.TYPE_BOOLEAN,
GObject.TYPE_INT,
@@ -464,7 +483,8 @@ class SearchView(BaseView):
GObject.TYPE_INT,
GObject.TYPE_BOOLEAN,
GObject.TYPE_STRING, # type
- GObject.TYPE_INT
+ GObject.TYPE_INT,
+ object # album art surface
)
self._filter_model = self.model.filter_new(None)
diff --git a/gnomemusic/widgets/albumwidget.py b/gnomemusic/widgets/albumwidget.py
index 5f6841c..97aaaa1 100644
--- a/gnomemusic/widgets/albumwidget.py
+++ b/gnomemusic/widgets/albumwidget.py
@@ -26,7 +26,7 @@ from gettext import gettext as _, ngettext
from gi.repository import GdkPixbuf, GLib, GObject, Gtk
from gnomemusic import log
-from gnomemusic.albumartcache import AlbumArtCache, DefaultIcon, ArtSize
+from gnomemusic.albumartcache import Art, ArtImage
from gnomemusic.grilo import grilo
from gnomemusic.widgets.disclistboxwidget import DiscBox, DiscListBox
import gnomemusic.utils as utils
@@ -55,12 +55,6 @@ class AlbumWidget(Gtk.EventBox):
self._songs = []
- scale = self.get_scale_factor()
- self._cache = AlbumArtCache(scale)
- self._loading_icon_surface = DefaultIcon(scale).get(
- DefaultIcon.Type.loading,
- ArtSize.LARGE)
-
self._player = player
self._iter_to_clean = None
@@ -143,10 +137,9 @@ class AlbumWidget(Gtk.EventBox):
self.selection_toolbar = selection_toolbar
self._header_bar = header_bar
self._album = album
- self._builder.get_object('cover').set_from_surface(
- self._loading_icon_surface)
- self._cache.lookup(item, ArtSize.LARGE, self._on_lookup, None)
self._duration = 0
+ art = ArtImage(Art.Size.LARGE, item)
+ art.image = self._builder.get_object('cover')
GLib.idle_add(grilo.populate_album_songs, item, self.add_item)
header_bar._select_button.connect(
@@ -289,16 +282,6 @@ class AlbumWidget(Gtk.EventBox):
self.show_all()
- @log
- def _on_lookup(self, surface, data=None):
- """Albumart retrieved callback.
-
- :param surface: The Cairo surface retrieved
- :param path: The filesystem location the pixbuf
- :param data: User data
- """
- self._builder.get_object('cover').set_from_surface(surface)
-
@log
def _update_model(self, player, playlist, current_iter):
"""Player changed callback.
diff --git a/gnomemusic/widgets/artistalbumwidget.py b/gnomemusic/widgets/artistalbumwidget.py
index 71bc01b..a86eca2 100644
--- a/gnomemusic/widgets/artistalbumwidget.py
+++ b/gnomemusic/widgets/artistalbumwidget.py
@@ -22,10 +22,10 @@
# 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 gi.repository import GObject, Gtk
from gnomemusic import log
-from gnomemusic.albumartcache import AlbumArtCache, DefaultIcon, ArtSize
+from gnomemusic.albumartcache import Art, ArtImage
from gnomemusic.grilo import grilo
from gnomemusic.widgets.disclistboxwidget import DiscBox
import gnomemusic.utils as utils
@@ -48,11 +48,6 @@ class ArtistAlbumWidget(Gtk.Box):
self._size_group = size_group
self._cover_size_group = cover_size_group
- scale = self.get_scale_factor()
- self._cache = AlbumArtCache(scale)
- self._loading_icon_surface = DefaultIcon(scale).get(
- DefaultIcon.Type.loading,
- ArtSize.MEDIUM)
self._media = media
self._player = player
@@ -72,7 +67,8 @@ class ArtistAlbumWidget(Gtk.Box):
ui.add_from_resource('/org/gnome/Music/ArtistAlbumWidget.ui')
self.cover = ui.get_object('cover')
- self.cover.set_from_surface(self._loading_icon_surface)
+ art = ArtImage(Art.Size.MEDIUM, self._media)
+ art.image = self.cover
self._disc_listbox = ui.get_object('disclistbox')
self._disc_listbox.set_selection_mode_allowed(
@@ -93,7 +89,6 @@ class ArtistAlbumWidget(Gtk.Box):
self.pack_start(ui.get_object('ArtistAlbumWidget'), True, True, 0)
- GLib.idle_add(self._update_album_art)
grilo.populate_album_songs(self._media, self._add_item)
def create_disc_box(self, disc_nr, disc_songs):
@@ -159,15 +154,6 @@ class ArtistAlbumWidget(Gtk.Box):
if remaining == 0:
self.emit("songs-loaded")
- @log
- def _update_album_art(self):
- self._cache.lookup(self._media, ArtSize.MEDIUM, self._get_album_cover,
- None)
-
- @log
- def _get_album_cover(self, surface, data=None):
- self.cover.set_from_surface(surface)
-
@log
def _song_activated(self, widget, song_widget):
if (not song_widget.can_be_played
[
Date Prev][
Date Next] [
Thread Prev][
Thread Next]
[
Thread Index]
[
Date Index]
[
Author Index]