[sushi] audio: load cover art from JS
- From: Cosimo Cecchi <cosimoc src gnome org>
- To: commits-list gnome org
- Cc:
- Subject: [sushi] audio: load cover art from JS
- Date: Wed, 19 Jun 2019 01:25:26 +0000 (UTC)
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]