[gnome-music/wip/mschraal/core] Use CoreSong for AlbumCover and AlbumArtCache.



commit a764a648e68e9e488343793e8586ec5f29de102b
Author: Marinus Schraal <mschraal gnome org>
Date:   Sat Jul 13 13:45:09 2019 +0200

    Use CoreSong for AlbumCover and AlbumArtCache.
    
    This is flawed in many ways and does not actually look up anything it
    seems. However it will show cached art as usual and stops using old
    grilo.py and query.py altogether.

 gnomemusic/albumartcache.py                  | 131 +++++++++++++++++----------
 gnomemusic/coregrilo.py                      |  32 +++++++
 gnomemusic/grilowrappers/grltrackersource.py |  79 ++++++++++++++++
 gnomemusic/mpris.py                          |   2 +-
 gnomemusic/widgets/albumcover.py             |   5 +-
 gnomemusic/widgets/albumwidget.py            |   2 +-
 gnomemusic/widgets/artistalbumwidget.py      |   2 +-
 gnomemusic/widgets/coverstack.py             |  10 +-
 gnomemusic/widgets/playertoolbar.py          |   2 +-
 9 files changed, 207 insertions(+), 58 deletions(-)
---
diff --git a/gnomemusic/albumartcache.py b/gnomemusic/albumartcache.py
index e2bf0f54..ec7ca541 100644
--- a/gnomemusic/albumartcache.py
+++ b/gnomemusic/albumartcache.py
@@ -35,22 +35,24 @@ from gi.repository import (Gdk, GdkPixbuf, Gio, GLib, GObject, Gtk, MediaArt,
                            Gst, GstTag, GstPbutils)
 
 from gnomemusic import log
-import gnomemusic.utils as utils
 
 
 logger = logging.getLogger(__name__)
 
 
 @log
-def lookup_art_file_from_cache(media):
+def lookup_art_file_from_cache(coresong):
     """Lookup MediaArt cache art of an album or song.
 
-    :param Grl.Media media: song or album
+    :param CoreSong coresong: song or album
     :returns: a cache file
     :rtype: Gio.File
     """
-    album = utils.get_album_title(media)
-    artist = utils.get_artist_name(media)
+    try:
+        album = coresong.props.album
+    except AttributeError:
+        album = coresong.props.title
+    artist = coresong.props.artist
 
     success, thumb_file = MediaArt.get_file(artist, album, "album")
     if (not success
@@ -197,12 +199,16 @@ class Art(GObject.GObject):
         return '<Art>'
 
     @log
-    def __init__(self, size, media, scale=1):
+    def __init__(self, size, coresong, scale=1):
         super().__init__()
 
         self._size = size
-        self._media = media
-        self._media_url = self._media.get_url()
+        self._coresong = coresong
+        # FIXME: Albums do not have a URL.
+        try:
+            self._url = self._coresong.props.url
+        except AttributeError:
+            self._url = None
         self._surface = None
         self._scale = scale
 
@@ -216,14 +222,14 @@ class Art(GObject.GObject):
         cache = Cache()
         cache.connect('miss', self._cache_miss)
         cache.connect('hit', self._cache_hit)
-        cache.query(self._media)
+        cache.query(self._coresong)
 
     @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)
+        embedded_art.query(self._coresong)
 
     @log
     def _cache_hit(self, klass, pixbuf):
@@ -245,7 +251,7 @@ class Art(GObject.GObject):
         # chance of getting artwork.
         cache.connect('miss', self._embedded_art_unavailable)
         cache.connect('hit', self._cache_hit)
-        cache.query(self._media)
+        cache.query(self._coresong)
 
     @log
     def _embedded_art_unavailable(self, klass):
@@ -253,14 +259,14 @@ class Art(GObject.GObject):
         remote_art.connect('retrieved', self._remote_art_retrieved)
         remote_art.connect('unavailable', self._remote_art_unavailable)
         remote_art.connect('no-remote-sources', self._remote_art_no_sources)
-        remote_art.query(self._media)
+        remote_art.query(self._coresong)
 
     @log
     def _remote_art_retrieved(self, klass):
         cache = Cache()
         cache.connect('miss', self._remote_art_unavailable)
         cache.connect('hit', self._cache_hit)
-        cache.query(self._media)
+        cache.query(self._coresong)
 
     @log
     def _remote_art_unavailable(self, klass):
@@ -280,8 +286,12 @@ class Art(GObject.GObject):
 
     @log
     def _add_to_blacklist(self):
-        album = utils.get_album_title(self._media)
-        artist = utils.get_artist_name(self._media)
+        # FIXME: coresong can be a CoreAlbum
+        try:
+            album = self._coresong.props.album
+        except AttributeError:
+            album = self._coresong.props.title
+        artist = self._coresong.props.artist
 
         if artist not in self._blacklist:
             self._blacklist[artist] = []
@@ -291,8 +301,12 @@ class Art(GObject.GObject):
 
     @log
     def _in_blacklist(self):
-        album = utils.get_album_title(self._media)
-        artist = utils.get_artist_name(self._media)
+        # FIXME: coresong can be a CoreAlbum
+        try:
+            album = self._coresong.props.album
+        except AttributeError:
+            album = self._coresong.props.title
+        artist = self._coresong.props.artist
         album_stripped = MediaArt.strip_invalid_entities(album)
 
         if artist in self._blacklist:
@@ -339,12 +353,12 @@ class Cache(GObject.GObject):
                 return
 
     @log
-    def query(self, media):
+    def query(self, coresong):
         """Start the cache query
 
-        :param Grl.Media media: The media object to search art for
+        :param CoreSong coresong: The CoreSong object to search art for
         """
-        thumb_file = lookup_art_file_from_cache(media)
+        thumb_file = lookup_art_file_from_cache(coresong)
         if thumb_file:
             thumb_file.read_async(
                 GLib.PRIORITY_LOW, None, self._open_stream, None)
@@ -414,22 +428,30 @@ class EmbeddedArt(GObject.GObject):
 
         self._album = None
         self._artist = None
-        self._media = None
+        self._coresong = None
         self._path = None
 
     @log
-    def query(self, media):
+    def query(self, coresong):
         """Start the local query
 
-        :param Grl.Media media: The media object to search art for
+        :param CoreSong coresong: The CoreSong object to search art for
         """
-        if media.get_url() is None:
+        try:
+            if coresong.props.url is None:
+                self.emit('unavailable')
+                return
+        except AttributeError:
             self.emit('unavailable')
             return
 
-        self._album = utils.get_album_title(media)
-        self._artist = utils.get_artist_name(media)
-        self._media = media
+        # FIXME: coresong can be a CoreAlbum
+        try:
+            self._album = coresong.props.album
+        except AttributeError:
+            self._album = coresong.props.title
+        self._artist = coresong.props.artist
+        self._coresong = coresong
 
         try:
             discoverer = GstPbutils.Discoverer.new(Gst.SECOND)
@@ -450,7 +472,7 @@ class EmbeddedArt(GObject.GObject):
 
         self._path = path
 
-        success = discoverer.discover_uri_async(self._media.get_url())
+        success = discoverer.discover_uri_async(self._coresong.props.url)
 
         if not success:
             logger.warning("Could not add url to discoverer.")
@@ -509,7 +531,7 @@ class EmbeddedArt(GObject.GObject):
         # 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,
+            self._coresong.props.url, self._artist, self._album,
             GLib.PRIORITY_LOW, None, self._uri_async_cb, None)
 
     @log
@@ -553,30 +575,47 @@ class RemoteArt(GObject.GObject):
 
         self._artist = None
         self._album = None
+        self._coresong = None
+        self._grilo = None
 
     @log
-    def query(self, media):
+    def query(self, coresong):
         """Start the remote query
 
-        :param Grl.Media media: The media object to search art for
+        :param CoreSong coresong: The CoreSong object to search art for
         """
-        self._album = utils.get_album_title(media)
-        self._artist = utils.get_artist_name(media)
-        self._media = media
+        # FIXME: coresong can be a CoreAlbum
+        try:
+            self._album = coresong.props.album
+        except AttributeError:
+            self._album = coresong.props.title
+        self._artist = coresong.props.artist
+        self._coresong = coresong
 
         self.emit('no-remote-sources')
-    #     if not grilo.props.cover_sources:
-    #         self.emit('no-remote-sources')
-    #         grilo.connect(
-    #         'notify::cover-sources', self._on_grilo_cover_sources_changed)
-    #     else:
-    # 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)
-
-    # def _on_grilo_cover_sources_changed(self, klass, data):
-    #     if grilo.props.cover_sources:
-    #         grilo.get_album_art_for_item(self._media, self._remote_album_art)
+
+        # FIXME: This is a total hack. It gets CoreModel from the
+        # CoreAlbum or CoreSong about and then retrieves the CoreGrilo
+        # instance.
+        try:
+            self._grilo = self._coresong._coremodel._grilo
+        except AttributeError:
+            self._grilo = self._coresong._grilo
+
+        if not self._grilo.props.cover_sources:
+            self.emit('no-remote-sources')
+            self._grilo.connect(
+                'notify::cover-sources', self._on_grilo_cover_sources_changed)
+        else:
+            # FIXME: It seems this Grilo query does not always return,
+            # especially on queries with little info.
+            self._grilo.get_album_art_for_item(
+                self._coresong, self._remote_album_art)
+
+    def _on_grilo_cover_sources_changed(self, klass, data):
+        if self._grilo.props.cover_sources:
+            self._grilo.get_album_art_for_item(
+                self._coresong, self._remote_album_art)
 
     @log
     def _delete_callback(self, src, result, data):
diff --git a/gnomemusic/coregrilo.py b/gnomemusic/coregrilo.py
index 7ca7a9e8..9441df23 100644
--- a/gnomemusic/coregrilo.py
+++ b/gnomemusic/coregrilo.py
@@ -18,6 +18,10 @@ class CoreGrilo(GObject.GObject):
         'grl-spotify-cover'
     ]
 
+    _theaudiodb_api_key = "195003"
+
+    cover_sources = GObject.Property(type=bool, default=False)
+
     def __repr__(self):
         return "<CoreGrilo>"
 
@@ -27,17 +31,31 @@ class CoreGrilo(GObject.GObject):
         self._coremodel = coremodel
         self._coreselection = coreselection
         self._search_wrappers = {}
+        self._thumbnail_sources = []
+        self._thumbnail_sources_timeout = None
         self._wrappers = {}
 
         Grl.init(None)
 
         self._registry = Grl.Registry.get_default()
+        config = Grl.Config.new("grl-lua-factory", "grl-theaudiodb-cover")
+        config.set_api_key(self._theaudiodb_api_key)
+        self._registry.add_config(config)
+
         self._registry.connect('source-added', self._on_source_added)
         self._registry.connect('source-removed', self._on_source_removed)
 
         self._registry.load_all_plugins(True)
 
     def _on_source_added(self, registry, source):
+
+        def _trigger_art_update():
+            self._thumbnail_sources_timeout = None
+            if len(self._thumbnail_sources) > 0:
+                self.props.cover_sources = True
+
+            return GLib.SOURCE_REMOVE
+
         if ("net:plaintext" in source.get_tags()
                 or source.props.source_id in self._blacklist):
             try:
@@ -47,6 +65,14 @@ class CoreGrilo(GObject.GObject):
                     source.props.source_id))
             return
 
+        if Grl.METADATA_KEY_THUMBNAIL in source.supported_keys():
+            self._thumbnail_sources.append(source)
+            if not self._thumbnail_sources_timeout:
+                # Aggregate sources being added, for example when the
+                # network comes online.
+                self._thumbnail_sources_timeout = GLib.timeout_add_seconds(
+                    5, _trigger_art_update)
+
         new_wrapper = None
 
         if (source.props.source_id == "grl-tracker-source"
@@ -119,3 +145,9 @@ class CoreGrilo(GObject.GObject):
             wrapper.search(text)
         for wrapper in self._search_wrappers.values():
             wrapper.search(text)
+
+    def get_album_art_for_item(self, coresong, callback):
+        # Tracker not (yet) loaded.
+        if "grl-tracker-source" not in self._wrappers:
+            self._wrappers["grl-tracker-source"].get_album_art_for_item(
+                coresong, callback)
diff --git a/gnomemusic/grilowrappers/grltrackersource.py b/gnomemusic/grilowrappers/grltrackersource.py
index 008ab5c2..07512215 100644
--- a/gnomemusic/grilowrappers/grltrackersource.py
+++ b/gnomemusic/grilowrappers/grltrackersource.py
@@ -28,6 +28,11 @@ class GrlTrackerSource(GObject.GObject):
         Grl.METADATA_KEY_URL
     ]
 
+    METADATA_THUMBNAIL_KEYS = [
+        Grl.METADATA_KEY_ID,
+        Grl.METADATA_KEY_THUMBNAIL,
+    ]
+
     def __repr__(self):
         return "<GrlTrackerSource>"
 
@@ -645,3 +650,77 @@ class GrlTrackerSource(GObject.GObject):
 
         self._source.query(
             query, self.METADATA_KEYS, options, artist_search_cb)
+
+    def get_album_art_for_item(self, coresong, callback):
+        item_id = coresong.props.media.get_id()
+
+        if coresong.props.media.is_audio():
+            query = self._get_album_for_song_id(item_id)
+        else:
+            query = self._get_album_for_album_id(item_id)
+
+        full_options = Grl.OperationOptions()
+        full_options.set_resolution_flags(
+            Grl.ResolutionFlags.FULL
+            | Grl.ResolutionFlags.IDLE_RELAY)
+        full_options.set_count(1)
+
+        self.search_source.query(
+            query, self.METADATA_THUMBNAIL_KEYS, full_options, callback)
+
+    @staticmethod
+    def _get_album_for_album_id(album_id):
+        # Even though we check for the album_artist, we fill
+        # the artist key, since Grilo coverart plugins use
+        # only that key for retrieval.
+        query = """
+        SELECT DISTINCT
+            rdf:type(?album)
+            tracker:id(?album) AS ?id
+            tracker:coalesce(nmm:artistName(?album_artist),
+                             nmm:artistName(?song_artist)) AS ?artist
+            nie:title(?album) AS ?album
+        WHERE {
+            ?album a nmm:MusicAlbum .
+            ?song a nmm:MusicPiece ;
+                    nmm:musicAlbum ?album ;
+                    nmm:performer ?song_artist .
+            OPTIONAL { ?album nmm:albumArtist ?album_artist . }
+            FILTER (
+                tracker:id(?album) = %(album_id)s
+            )
+        }
+        """.replace("\n", " ").strip() % {
+                'album_id': album_id,
+        }
+
+        return query
+
+    @staticmethod
+    def _get_album_for_song_id(song_id):
+        # See get_album_for_album_id comment.
+        query = """
+        SELECT DISTINCT
+            rdf:type(?album)
+            tracker:id(?album) AS ?id
+            tracker:coalesce(nmm:artistName(?album_artist),
+                             nmm:artistName(?song_artist)) AS ?artist
+            nie:title(?album) AS ?album
+        WHERE {
+            ?song a nmm:MusicPiece ;
+                    nmm:musicAlbum ?album ;
+                    nmm:performer ?song_artist .
+            OPTIONAL { ?album nmm:albumArtist ?album_artist . }
+            FILTER (
+                tracker:id(?song) = %(song_id)s
+            )
+            FILTER (
+                NOT EXISTS { ?song a nmm:Video }
+                && NOT EXISTS { ?song a nmm:Playlist }
+            )
+        }
+        """.replace("\n", " ").strip() % {
+                'song_id': song_id
+        }
+
+        return query
diff --git a/gnomemusic/mpris.py b/gnomemusic/mpris.py
index 5c29f28e..11cb3c9d 100644
--- a/gnomemusic/mpris.py
+++ b/gnomemusic/mpris.py
@@ -374,7 +374,7 @@ class MPRIS(DBusInterface):
         # player.
         art_url = coresong.props.media.get_thumbnail()
         if not art_url:
-            thumb_file = lookup_art_file_from_cache(coresong.props.media)
+            thumb_file = lookup_art_file_from_cache(coresong)
             if thumb_file:
                 art_url = GLib.filename_to_uri(thumb_file.get_path())
                 coresong.props.media.set_thumbnail(art_url)
diff --git a/gnomemusic/widgets/albumcover.py b/gnomemusic/widgets/albumcover.py
index f388fe71..5dc55a40 100644
--- a/gnomemusic/widgets/albumcover.py
+++ b/gnomemusic/widgets/albumcover.py
@@ -101,10 +101,9 @@ class AlbumCover(Gtk.FlowBoxChild):
         # quick first show with a placeholder cover and then a
         # reasonably responsive view while loading the actual
         # covers.
-        # FIXME: Pass CoreAlbum to CoverStack.
         GLib.timeout_add(
-            50 * self._nr_albums, self._cover_stack.update,
-            self._corealbum.props.media, priority=GLib.PRIORITY_LOW)
+            50 * self._nr_albums, self._cover_stack.update, self._corealbum,
+            priority=GLib.PRIORITY_LOW)
 
     @GObject.Property(type=CoreAlbum, flags=GObject.ParamFlags.READABLE)
     def corealbum(self):
diff --git a/gnomemusic/widgets/albumwidget.py b/gnomemusic/widgets/albumwidget.py
index 6e682cc0..ee665db8 100644
--- a/gnomemusic/widgets/albumwidget.py
+++ b/gnomemusic/widgets/albumwidget.py
@@ -63,7 +63,7 @@ class AlbumWidget(Gtk.EventBox):
         if self._signal_id:
             self._album_model.disconnect(self._signal_id)
 
-        self._cover_stack.update(corealbum.props.media)
+        self._cover_stack.update(corealbum)
 
         self._duration = 0
 
diff --git a/gnomemusic/widgets/artistalbumwidget.py b/gnomemusic/widgets/artistalbumwidget.py
index 52c70b13..db03de44 100644
--- a/gnomemusic/widgets/artistalbumwidget.py
+++ b/gnomemusic/widgets/artistalbumwidget.py
@@ -64,7 +64,7 @@ class ArtistAlbumWidget(Gtk.Box):
         self._selection_mode_allowed = selection_mode_allowed
 
         self._cover_stack.props.size = Art.Size.MEDIUM
-        self._cover_stack.update(corealbum.props.media)
+        self._cover_stack.update(corealbum)
 
         allowed = self._selection_mode_allowed
         self._disc_list_box.props.selection_mode_allowed = allowed
diff --git a/gnomemusic/widgets/coverstack.py b/gnomemusic/widgets/coverstack.py
index 4a981193..ae9eff81 100644
--- a/gnomemusic/widgets/coverstack.py
+++ b/gnomemusic/widgets/coverstack.py
@@ -96,11 +96,11 @@ class CoverStack(Gtk.Stack):
         self._loading_cover.props.surface = icon
 
     @log
-    def update(self, media):
-        """Update the stack with the given media
+    def update(self, coresong):
+        """Update the stack with the given CoreSong
 
-        Update the stack with the art retrieved from the given media.
-        :param Grl.Media media: The media object
+        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
@@ -113,7 +113,7 @@ class CoverStack(Gtk.Stack):
 
         self._active_child = self.props.visible_child_name
 
-        self._art = Art(self.props.size, media, self.props.scale_factor)
+        self._art = Art(self.props.size, coresong, self.props.scale_factor)
         self._handler_id = self._art.connect('finished', self._art_retrieved)
         self._art.lookup()
 
diff --git a/gnomemusic/widgets/playertoolbar.py b/gnomemusic/widgets/playertoolbar.py
index 17e75807..d094597f 100644
--- a/gnomemusic/widgets/playertoolbar.py
+++ b/gnomemusic/widgets/playertoolbar.py
@@ -185,7 +185,7 @@ class PlayerToolbar(Gtk.ActionBar):
         self._tooltip.props.title = title
         self._tooltip.props.subtitle = artist
 
-        self._cover_stack.update(coresong.props.media)
+        self._cover_stack.update(coresong)
 
     @Gtk.Template.Callback()
     @log


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