[sushi] audio: load cover art from JS



commit 84f6301b984ef94fadcf80da5ad98c578b5fc3f6
Author: Cosimo Cecchi <cosimoc gnome org>
Date:   Tue Jun 18 17:48:47 2019 -0700

    audio: load cover art from JS
    
    Instead of requiring a GObject class for this.
    Fetching cover art from Amazon was broken already, so this commit
    fixes that too.

 src/libsushi/meson.build       |   2 -
 src/libsushi/sushi-cover-art.c | 688 -----------------------------------------
 src/libsushi/sushi-cover-art.h |  62 ----
 src/libsushi/sushi-utils.c     | 152 +++++++++
 src/libsushi/sushi-utils.h     |  11 +
 src/viewers/audio.js           | 189 ++++++++++-
 6 files changed, 342 insertions(+), 762 deletions(-)
---
diff --git a/src/libsushi/meson.build b/src/libsushi/meson.build
index b9d9d97..a1b08ae 100644
--- a/src/libsushi/meson.build
+++ b/src/libsushi/meson.build
@@ -1,5 +1,4 @@
 libsushi_c = [
-  'sushi-cover-art.c',
   'sushi-font-loader.c',
   'sushi-font-widget.c',
   'sushi-media-bin.c',
@@ -8,7 +7,6 @@ libsushi_c = [
 ]
 
 libsushi_headers = [
-  'sushi-cover-art.h',
   'sushi-font-loader.h',
   'sushi-font-widget.h',
   'sushi-media-bin.h',
diff --git a/src/libsushi/sushi-utils.c b/src/libsushi/sushi-utils.c
index dc69278..2aa35d1 100644
--- a/src/libsushi/sushi-utils.c
+++ b/src/libsushi/sushi-utils.c
@@ -27,6 +27,7 @@
 
 #include <glib/gstdio.h>
 #include <gtk/gtk.h>
+#include <musicbrainz5/mb5_c.h>
 
 #ifdef GDK_WINDOWING_X11
 #include <gdk/gdkx.h>
@@ -375,3 +376,154 @@ sushi_convert_libreoffice_finish (GAsyncResult *result,
 {
   return g_task_propagate_pointer (G_TASK (result), error);
 }
+
+/**
+ * sushi_pixbuf_from_gst_sample:
+ * @sample:
+ * @error:
+ *
+ * Returns: (transfer full):
+ */
+GdkPixbuf *
+sushi_pixbuf_from_gst_sample (GstSample *sample,
+                              GError   **error)
+{
+  GstBuffer *buffer = gst_sample_get_buffer (sample);
+  GdkPixbuf *pixbuf = NULL;
+  GdkPixbufLoader *loader;
+  GstMapInfo info;
+
+  if (!gst_buffer_map (buffer, &info, GST_MAP_READ)) {
+    g_set_error_literal (error, G_IO_ERROR, G_IO_ERROR_FAILED,
+                         "Failed to map GstBuffer");
+    return NULL;
+  }
+
+  loader = gdk_pixbuf_loader_new ();
+  if (!gdk_pixbuf_loader_write (loader, info.data, info.size, error) ||
+      !gdk_pixbuf_loader_close (loader, error))
+    return NULL;
+
+  pixbuf = gdk_pixbuf_loader_get_pixbuf (loader);
+  if (pixbuf)
+    g_object_ref (pixbuf);
+
+  g_object_unref (loader);
+  gst_buffer_unmap (buffer, &info);
+
+  return pixbuf;
+}
+
+typedef struct {
+  gchar *artist;
+  gchar *album;
+} FetchUriTaskData;
+
+static void
+fetch_uri_task_data_free (gpointer user_data)
+{
+  FetchUriTaskData *data = user_data;
+
+  g_free (data->artist);
+  g_free (data->album);
+
+  g_slice_free (FetchUriTaskData, data);
+}
+
+static FetchUriTaskData *
+fetch_uri_task_data_new (const gchar *artist,
+                         const gchar *album)
+{
+  FetchUriTaskData *retval;
+
+  retval = g_slice_new0 (FetchUriTaskData);
+  retval->artist = g_strdup (artist);
+  retval->album = g_strdup (album);
+
+  return retval;
+}
+
+static void
+fetch_uri_job (GTask *task,
+               gpointer source_object,
+               gpointer task_data,
+               GCancellable *cancellable)
+{
+  FetchUriTaskData *data = task_data;
+  Mb5Metadata metadata;
+  Mb5Query query;
+  Mb5Release release;
+  Mb5ReleaseList release_list;
+  gchar *retval = NULL;
+  gchar **param_names = NULL;
+  gchar **param_values = NULL;
+
+  query = mb5_query_new ("sushi", NULL, 0);
+
+  param_names = g_new (gchar*, 3);
+  param_values = g_new (gchar*, 3);
+
+  param_names[0] = g_strdup ("query");
+  param_values[0] = g_strdup_printf ("artist:\"%s\" AND release:\"%s\"", data->artist, data->album);
+
+  param_names[1] = g_strdup ("limit");
+  param_values[1] = g_strdup ("10");
+
+  param_names[2] = NULL;
+  param_values[2] = NULL;
+
+  metadata = mb5_query_query (query, "release", "", "",
+                              2, param_names, param_values);
+
+  mb5_query_delete (query);
+
+  if (metadata) {
+    release_list = mb5_metadata_get_releaselist (metadata);
+    int i;
+    int release_list_length = mb5_release_list_size (release_list);
+    for (i = 0; i < release_list_length; i++) {
+      gchar asin[255];
+
+      release = mb5_release_list_item (release_list, i);
+      mb5_release_get_asin (release, asin, 255);
+
+      if (asin != NULL && asin[0] != '\0') {
+        retval = g_strdup (asin);
+        break;
+      }
+    }
+  }
+  mb5_metadata_delete (metadata);
+
+  if (retval == NULL)
+    g_task_return_new_error (task,
+                             G_IO_ERROR,
+                             G_IO_ERROR_FAILED, "%s",
+                             "Error getting the ASIN from MusicBrainz");
+  else
+    g_task_return_pointer (task, retval, g_free);
+
+  g_strfreev (param_names);
+  g_strfreev (param_values);
+}
+
+gchar *
+sushi_get_asin_for_track_finish (GAsyncResult *result,
+                                 GError **error)
+{
+  return g_task_propagate_pointer (G_TASK (result), error);
+}
+
+void
+sushi_get_asin_for_track (const gchar *artist,
+                          const gchar *album,
+                          GAsyncReadyCallback callback,
+                          gpointer user_data)
+{
+  FetchUriTaskData *data = fetch_uri_task_data_new (artist, album);
+  GTask *task = g_task_new (NULL, NULL, callback, user_data);
+  g_task_set_task_data (task, data, fetch_uri_task_data_free);
+
+  g_task_run_in_thread (task, fetch_uri_job);
+  g_object_unref (task);
+}
diff --git a/src/libsushi/sushi-utils.h b/src/libsushi/sushi-utils.h
index c2bb721..96af3cc 100644
--- a/src/libsushi/sushi-utils.h
+++ b/src/libsushi/sushi-utils.h
@@ -30,6 +30,7 @@
 #include <evince-view.h>
 #include <gdk/gdk.h>
 #include <gio/gio.h>
+#include <gst/gst.h>
 
 G_BEGIN_DECLS
 
@@ -43,6 +44,16 @@ void           sushi_convert_libreoffice (GFile *file,
 GFile *        sushi_convert_libreoffice_finish (GAsyncResult *result,
                                                  GError **error);
 
+void           sushi_get_asin_for_track (const gchar *artist,
+                                         const gchar *album,
+                                         GAsyncReadyCallback callback,
+                                         gpointer user_data);
+gchar *        sushi_get_asin_for_track_finish (GAsyncResult *result,
+                                                GError **error);
+
+GdkPixbuf *    sushi_pixbuf_from_gst_sample (GstSample *sample,
+                                             GError   **error);
+
 G_END_DECLS
 
 #endif /* __SUSHI_UTILS_H__ */
diff --git a/src/viewers/audio.js b/src/viewers/audio.js
index 68582f9..1ac19f2 100644
--- a/src/viewers/audio.js
+++ b/src/viewers/audio.js
@@ -23,7 +23,7 @@
  *
  */
 
-const {GdkPixbuf, Gio, GObject, Gst, Gtk, Sushi} = imports.gi;
+const {GdkPixbuf, Gio, GLib, GObject, Gst, GstTag, Gtk, Soup, Sushi} = imports.gi;
 
 const Constants = imports.util.constants;
 const Renderer = imports.ui.renderer;
@@ -47,6 +47,178 @@ function _formatTimeString(timeVal) {
     return str;
 }
 
+const AMAZON_IMAGE_FORMAT = "http://images.amazon.com/images/P/%s.01.LZZZZZZZ.jpg";;
+const fetchCoverArt = function(_tagList, _callback) {
+    function _fetchFromTags() {
+        let coverSample = null;
+        let idx = 0;
+
+        while (true) {
+            let [res, sample] = _tagList.get_sample_index(Gst.TAG_IMAGE, idx);
+            if (!res)
+                break;
+
+            idx++;
+
+            let caps = sample.get_caps();
+            let capsStruct = caps.get_structure(0);
+            let [r, type] = capsStruct.get_enum('image-type', GstTag.TagImageType.$gtype);
+            if (type == GstTag.TagImageType.UNDEFINED) {
+                coverSample = sample;
+            } else if (type == GstTag.TagImageType.FRONT_COVER) {
+                coverSample = sample;
+                break;
+            }
+        }
+
+        // Fallback to preview
+        if (!coverSample)
+            coverSample = _tagList.get_sample_index(Gst.TAG_PREVIEW_IMAGE, 0)[1];
+
+        if (coverSample) {
+            try {
+                return Sushi.pixbuf_from_gst_sample(coverSample)
+            } catch (e) {
+                logError(e, 'Unable to fetch cover art from GstSample');
+            }
+        }
+        return null;
+    }
+
+    function _getCacheFile(asin) {
+        let cachePath = GLib.build_filenamev([GLib.get_user_cache_dir(), 'sushi']);
+        return Gio.File.new_for_path(GLib.build_filenamev([cachePath, `${asin}.jpg`]));
+    }
+
+    function _fetchFromStream(stream, done) {
+        GdkPixbuf.Pixbuf.new_from_stream_async(stream, null, (o, res) => {
+            let cover;
+            try {
+                cover = GdkPixbuf.Pixbuf.new_from_stream_finish(res);
+            } catch (e) {
+                done(e, null);
+                return;
+            }
+
+            done(null, cover);
+        });
+    }
+
+    function _fetchFromCache(asin, done) {
+        let file = _getCacheFile(asin);
+        file.query_info_async(Gio.FILE_ATTRIBUTE_STANDARD_TYPE, 0, 0, null, (f, res) => {
+            try {
+                file.query_info_finish(res);
+            } catch (e) {
+                done(e, null);
+                return;
+            }
+
+            file.read_async(0, null, (f, res) => {
+                let stream;
+                try {
+                    stream = file.read_finish(res);
+                } catch (e) {
+                    done(e, null);
+                    return;
+                }
+
+                _fetchFromStream(stream, done);
+            });
+        });
+    }
+
+    function _saveToCache(asin, stream, done) {
+        let cacheFile = _getCacheFile(asin);
+        let cachePath = cacheFile.get_parent().get_path();
+        GLib.mkdir_with_parents(cachePath, 448);
+
+        cacheFile.replace_async(null, false, Gio.FileCreateFlags.PRIVATE, 0, null, (f, res) => {
+            let outStream;
+            try {
+                outStream = cacheFile.replace_finish(res);
+            } catch (e) {
+                done(e);
+                return;
+            }
+
+            outStream.splice_async(
+                stream,
+                Gio.OutputStreamSpliceFlags.CLOSE_SOURCE |
+                Gio.OutputStreamSpliceFlags.CLOSE_TARGET,
+                0, null, (s, res) => {
+                    try {
+                        outStream.splice_finish(res);
+                    } catch (e) {
+                        done(e);
+                        return;
+                    }
+
+                    done();
+                });
+        });
+    }
+
+    function _fetchFromAmazon(asin, done) {
+        let uri = AMAZON_IMAGE_FORMAT.format(asin);
+        let session = new Soup.Session();
+
+        let request;
+        try {
+            request = session.request(uri);
+        } catch (e) {
+            done(e, null);
+            return;
+        }
+
+        request.send_async(null, (r, res) => {
+            let stream;
+            try {
+                stream = request.send_finish(res);
+            } catch (e) {
+                done(e, null);
+                return;
+            }
+
+            _saveToCache(asin, stream, (err) => {
+                if (err)
+                    logError(err, 'Unable to save cover to cache');
+                _fetchFromCache(asin, done);
+            });
+        });
+    }
+
+    function _fetchFromASIN(done) {
+        let artist = _tagList.get_string('artist')[1];
+        let album = _tagList.get_string('album')[1];
+
+        Sushi.get_asin_for_track(artist, album, (o, res) => {
+            let asin
+            try {
+                asin = Sushi.get_asin_for_track_finish(res);
+            } catch (e) {
+                done(e, null);
+                return;
+            }
+
+            _fetchFromCache(asin, (err, cover) => {
+                if (cover)
+                    done(null, cover);
+                else
+                    _fetchFromAmazon(asin, done);
+            });
+        });
+    }
+
+   let cover = _fetchFromTags();
+   if (cover) {
+       _callback(null, cover);
+       return;
+   }
+
+    _fetchFromASIN(_callback);
+}
+
 var Klass = GObject.registerClass({
     Implements: [Renderer.Renderer],
     Properties: {
@@ -105,8 +277,6 @@ var Klass = GObject.registerClass({
             this._player.connect('notify::state', this._onPlayerStateChanged.bind(this)));
         this._playerNotifies.push(
             this._player.connect('notify::taglist', this._onTagListChanged.bind(this)));
-        this._playerNotifies.push(
-            this._player.connect('notify::cover', this._onCoverArtChanged.bind(this)));
     }
 
     _onDestroy() {
@@ -138,13 +308,13 @@ var Klass = GObject.registerClass({
         }
     }
 
-    _onCoverArtChanged() {
-        if (!this._artFetcher.cover) {
-            this._image.set_from_icon_name('media-optical-symbolic');
+    _onCoverArtFetched(err, cover) {
+        if (err) {
+            logError(err, 'Unable to fetch cover art');
             return;
         }
 
-        this._ensurePixbufSize(this._artFetcher.cover);
+        this._ensurePixbufSize(cover);
         this._image.set_from_pixbuf(this._coverArt);
     }
 
@@ -166,9 +336,8 @@ var Klass = GObject.registerClass({
 
         this._titleLabel.set_markup('<b>' + titleName + '</b>');
 
-        this._artFetcher = new Sushi.CoverArtFetcher();
-        this._artFetcher.connect('notify::cover', this._onCoverArtChanged.bind(this));
-        this._artFetcher.taglist = tags;
+        if (artistName && albumName)
+            fetchCoverArt(tags, this._onCoverArtFetched.bind(this));
     }
 
     _updateProgressBar() {


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