[grilo-plugins/wip/gbsneto/spotify] spotify: implement Spotify plugin
- From: Georges Basile Stavracas Neto <gbsneto src gnome org>
- To: commits-list gnome org
- Cc:
- Subject: [grilo-plugins/wip/gbsneto/spotify] spotify: implement Spotify plugin
- Date: Thu, 10 Sep 2015 03:12:45 +0000 (UTC)
commit b6ae10b013b01bcb1d9231b4bd0a4dd5e4ac0fe9
Author: Georges Basile Stavracas Neto <georges stavracas gmail com>
Date: Thu Sep 10 00:08:15 2015 -0300
spotify: implement Spotify plugin
This is an initial implementation of the Spotify plugin,
which just retrieves audio coverart with the Spotify
public API.
A more sophisticated and featureful version will come
after this is settled and GNOME 3.18 is released.
https://bugzilla.gnome.org/show_bug.cgi?id=754811
Makefile.am | 1 +
configure.ac | 44 +++++
help/C/grilo-plugins.xml | 5 +
src/Makefile.am | 6 +-
src/spotify/Makefile.am | 46 +++++
src/spotify/grl-spotify.c | 389 +++++++++++++++++++++++++++++++++++++++++++
src/spotify/grl-spotify.h | 68 ++++++++
src/spotify/grl-spotify.xml | 10 +
8 files changed, 568 insertions(+), 1 deletions(-)
---
diff --git a/Makefile.am b/Makefile.am
index 1598f5f..34eee8e 100644
--- a/Makefile.am
+++ b/Makefile.am
@@ -41,6 +41,7 @@ DISTCHECK_CONFIGURE_FLAGS = --enable-apple-trailers \
--enable-optical-media \
--enable-pocket \
--enable-podcasts \
+ --enable-spotify \
--enable-thetvdb \
--enable-tmdb \
--enable-tracker \
diff --git a/configure.ac b/configure.ac
index a0768d5..d17a60d 100644
--- a/configure.ac
+++ b/configure.ac
@@ -400,6 +400,49 @@ DEPS_LASTFM_ALBUMART_LIBS="$DEPS_LIBS $GRLNET_LIBS $XML_LIBS"
AC_SUBST(DEPS_LASTFM_ALBUMART_LIBS)
# ----------------------------------------------------------
+# BUILD SPOTIFY PLUGIN
+# ----------------------------------------------------------
+
+AC_ARG_ENABLE(spotify,
+ AC_HELP_STRING([--enable-spotify],
+ [enable Spotify plugin (default: auto)]),
+ [
+ case "$enableval" in
+ yes)
+ if test "x$HAVE_GRLNET" = "xno"; then
+ AC_MSG_ERROR([grilo-net-0.2 >= 0.2.2 not found, install it or use
--disable-spotify])
+ fi
+ if test "x$HAVE_JSON_GLIB" = "xno"; then
+ AC_MSG_ERROR([json-glib-1.0 not found, install it or use --disable-spotify])
+ fi
+ ;;
+ esac
+ ],
+ [
+ if test "x$HAVE_GRLNET" = "xyes" -a "x$HAVE_JSON_GLIB" = "xyes"; then
+ enable_spotify=yes
+ else
+ enable_spotify=no
+ fi
+ ])
+
+AM_CONDITIONAL([SPOTIFY_PLUGIN], [test "x$enable_spotify" = "xyes"])
+GRL_PLUGINS_ALL="$GRL_PLUGINS_ALL spotify"
+if test "x$enable_spotify" = "xyes"
+then
+ GRL_PLUGINS_ENABLED="$GRL_PLUGINS_ENABLED spotify"
+fi
+
+SPOTIFY_PLUGIN_ID="grl-spotify"
+AC_SUBST(SPOTIFY_PLUGIN_ID)
+AC_DEFINE_UNQUOTED([SPOTIFY_PLUGIN_ID], ["$SPOTIFY_PLUGIN_ID"], [Spotify plugin ID])
+
+DEPS_SPOTIFY_CFLAGS="$DEPS_CFLAGS $GRLNET_CFLAGS $JSON_CFLAGS"
+AC_SUBST(DEPS_SPOTIFY_CFLAGS)
+DEPS_SPOTIFY_LIBS="$DEPS_LIBS $GRLNET_LIBS $JSON_LIBS"
+AC_SUBST(DEPS_SPOTIFY_LIBS)
+
+# ----------------------------------------------------------
# BUILD YOUTUBE PLUGIN
# ----------------------------------------------------------
@@ -1313,6 +1356,7 @@ AC_CONFIG_FILES([
src/podcasts/Makefile
src/raitv/Makefile
src/shoutcast/Makefile
+ src/spotify/Makefile
src/tmdb/Makefile
src/tracker/Makefile
src/vimeo/Makefile
diff --git a/help/C/grilo-plugins.xml b/help/C/grilo-plugins.xml
index f1fdf78..632d8b6 100644
--- a/help/C/grilo-plugins.xml
+++ b/help/C/grilo-plugins.xml
@@ -100,6 +100,11 @@
<para>This is a Grilo plugin for LastFM album art. Its plugin ID is
<literal>"grl-lastfm-albumart"</literal></para>
</sect1>
+<sect1 id="sec-plugin-spotify">
+<title>Spotify</title>
+<para>This is a Grilo plugin for Spotify. Its plugin ID is <literal>"grl-spotify"</literal></para>
+</sect1>
+
<sect1 id="sec-plugin-local-metadata">
<title>Local Metadata</title>
<para>This is a Grilo plugin for Local metadata. Its plugin ID is
<literal>"grl-local-metadata"</literal></para>
diff --git a/src/Makefile.am b/src/Makefile.am
index 30e647c..d983952 100644
--- a/src/Makefile.am
+++ b/src/Makefile.am
@@ -74,6 +74,10 @@ if SHOUTCAST_PLUGIN
SUBDIRS += shoutcast
endif
+if SPOTIFY_PLUGIN
+SUBDIRS += spotify
+endif
+
if THETVDB_PLUGIN
SUBDIRS += thetvdb
endif
@@ -105,6 +109,6 @@ endif
DIST_SUBDIRS = \
bookmarks dleyna dmap filesystem flickr freebox gravatar jamendo \
lastfm-albumart local-metadata lua-factory magnatune metadata-store opensubtitles \
- optical-media podcasts raitv shoutcast thetvdb tmdb tracker vimeo youtube
+ optical-media podcasts raitv shoutcast spotify thetvdb tmdb tracker vimeo youtube
-include $(top_srcdir)/git.mk
diff --git a/src/spotify/Makefile.am b/src/spotify/Makefile.am
new file mode 100644
index 0000000..8ce5b78
--- /dev/null
+++ b/src/spotify/Makefile.am
@@ -0,0 +1,46 @@
+#
+# Makefile.am
+#
+# Author: Georges Basile Stavracas Neto <georges stavracas gmail com>
+#
+# Copyright (C) 2015
+
+include $(top_srcdir)/gtester.mk
+
+ext_LTLIBRARIES = libgrlspotify.la
+
+libgrlspotify_la_CFLAGS = \
+ $(DEPS_SPOTIFY_CFLAGS) \
+ -DG_LOG_DOMAIN=\"GrlSpotify\" \
+ -DLOCALEDIR=\"$(localedir)\"
+
+libgrlspotify_la_LIBADD = \
+ $(DEPS_SPOTIFY_LIBS)
+
+libgrlspotify_la_LDFLAGS = \
+ -no-undefined \
+ -module \
+ -avoid-version
+
+libgrlspotify_la_SOURCES = grl-spotify.c grl-spotify.h
+
+extdir = \
+ $(GRL_PLUGINS_DIR)
+
+spotifyxmldir = \
+ $(GRL_PLUGINS_DIR)
+spotifyxml_DATA = \
+ $(SPOTIFY_PLUGIN_ID).xml
+
+# This lets us test the plugin without installing it,
+# because grilo expects the .so and .xml files to be in
+# the same directory:
+copy-xml-to-libs-dir: libgrlspotify.la
+ cp -f $(srcdir)/$(spotifyxml_DATA) $(builddir)/.libs/
+
+all-local: copy-xml-to-libs-dir
+
+
+EXTRA_DIST += $(spotifyxml_DATA)
+
+-include $(top_srcdir)/git.mk
diff --git a/src/spotify/grl-spotify.c b/src/spotify/grl-spotify.c
new file mode 100644
index 0000000..a9c562b
--- /dev/null
+++ b/src/spotify/grl-spotify.c
@@ -0,0 +1,389 @@
+/* grl-spotify.c
+ *
+ * Copyright (C) 2015 Georges Basile Stavracas Neto <georges stavracas gmail com>
+ *
+ * This file is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as
+ * published by the Free Software Foundation; either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This file 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
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifdef HAVE_CONFIG_H
+#include "config.h"
+#endif
+
+#include <net/grl-net.h>
+#include <glib/gi18n-lib.h>
+#include <json-glib/json-glib.h>
+
+#include "grl-spotify.h"
+
+/* ---------- Logging ---------- */
+
+#define GRL_LOG_DOMAIN_DEFAULT spotify_log_domain
+GRL_LOG_DOMAIN_STATIC(spotify_log_domain);
+
+/* -------- Spotify API -------- */
+
+#define SPOTIFY_SEARCH_ALBUM "https://api.spotify.com/v1/search?q=album:%s+artist:%s&type=album&limit=1"
+
+/* ------- Pluging Info -------- */
+
+#define PLUGIN_ID SPOTIFY_PLUGIN_ID
+
+#define SOURCE_ID "grl-spotify"
+#define SOURCE_NAME _("Album art Provider from Spotify")
+#define SOURCE_DESC _("A plugin for getting album arts using Spotify as backend")
+
+static GrlNetWc *wc;
+
+static GrlSpotifySource *grl_spotify_source_new (void);
+
+static void grl_spotify_source_finalize (GObject *object);
+
+static void grl_spotify_source_resolve (GrlSource *source,
+ GrlSourceResolveSpec *rs);
+
+static const GList *grl_spotify_source_supported_keys (GrlSource *source);
+
+static gboolean grl_spotify_source_may_resolve (GrlSource *source,
+ GrlMedia *media,
+ GrlKeyID key_id,
+ GList **missing_keys);
+
+static void grl_spotify_source_cancel (GrlSource *source,
+ guint operation_id);
+
+gboolean grl_spotify_source_plugin_init (GrlRegistry *registry,
+ GrlPlugin *plugin,
+ GList *configs);
+
+
+/* =================== Spotify-AlbumArt Plugin =============== */
+
+gboolean
+grl_spotify_source_plugin_init (GrlRegistry *registry,
+ GrlPlugin *plugin,
+ GList *configs)
+{
+ GRL_LOG_DOMAIN_INIT (spotify_log_domain, "spotify");
+
+ GRL_DEBUG ("grl_spotify_source_plugin_init");
+
+ /* Initialize i18n */
+ bindtextdomain (GETTEXT_PACKAGE, LOCALEDIR);
+ bind_textdomain_codeset (GETTEXT_PACKAGE, "UTF-8");
+
+ GrlSpotifySource *source = grl_spotify_source_new ();
+ grl_registry_register_source (registry,
+ plugin,
+ GRL_SOURCE (source),
+ NULL);
+ return TRUE;
+}
+
+GRL_PLUGIN_REGISTER (grl_spotify_source_plugin_init,
+ NULL,
+ PLUGIN_ID);
+
+/* ================== Spotify-AlbumArt GObject ================ */
+
+static GrlSpotifySource *
+grl_spotify_source_new (void)
+{
+ const char *tags[] = {
+ "net:internet",
+ NULL
+ };
+ GRL_DEBUG ("grl_spotify_source_new");
+ return g_object_new (GRL_SPOTIFY_SOURCE_TYPE,
+ "source-id", SOURCE_ID,
+ "source-name", SOURCE_NAME,
+ "source-desc", SOURCE_DESC,
+ "source-tags", tags,
+ NULL);
+}
+
+static void
+grl_spotify_source_class_init (GrlSpotifySourceClass * klass)
+{
+ GObjectClass *gobject_class = G_OBJECT_CLASS (klass);
+ GrlSourceClass *source_class = GRL_SOURCE_CLASS (klass);
+
+ source_class->supported_keys = grl_spotify_source_supported_keys;
+ source_class->cancel = grl_spotify_source_cancel;
+ source_class->may_resolve = grl_spotify_source_may_resolve;
+ source_class->resolve = grl_spotify_source_resolve;
+
+ gobject_class->finalize = grl_spotify_source_finalize;
+}
+
+static void
+grl_spotify_source_init (GrlSpotifySource *source)
+{
+}
+
+G_DEFINE_TYPE (GrlSpotifySource,
+ grl_spotify_source,
+ GRL_TYPE_SOURCE);
+
+static void
+grl_spotify_source_finalize (GObject *object)
+{
+ g_clear_object (&wc);
+
+ G_OBJECT_CLASS (grl_spotify_source_parent_class)->finalize (object);
+}
+
+/* ======================= Utilities ==================== */
+
+static gchar**
+get_images (JsonNode *root,
+ gint *n_covers)
+{
+ JsonNode *fetched_node;
+ gchar **cover_urls;
+ gint number_of_covers;
+
+ number_of_covers = 0;
+ cover_urls = NULL;
+ fetched_node = json_path_query ("$..images[*].url",
+ root,
+ NULL);
+
+ if (fetched_node) {
+ JsonArray *images_array;
+ gint i;
+
+ images_array = json_node_get_array (fetched_node);
+ number_of_covers = json_array_get_length (images_array);
+ cover_urls = g_new0 (gchar*, number_of_covers);
+
+ for (i = 0; i < number_of_covers; i++) {
+ JsonNode *node;
+
+ node = json_array_get_element (images_array, i);
+ cover_urls[i] = json_node_dup_string (node);
+ }
+
+ json_node_free (fetched_node);
+ }
+
+ if (n_covers)
+ *n_covers = number_of_covers;
+
+ return cover_urls;
+}
+
+static void
+search_finished_cb (GObject *source_object,
+ GAsyncResult *res,
+ gpointer user_data)
+{
+ GrlSourceResolveSpec *rs = (GrlSourceResolveSpec *) user_data;
+ GCancellable *cancellable;
+ GError *error = NULL;
+ GError *wc_error = NULL;
+ GrlRelatedKeys *relkeys;
+ JsonParser *parser;
+ gchar *content = NULL;
+ gchar **images = NULL;
+ gint n_covers;
+ gint i;
+
+ GRL_DEBUG (__FUNCTION__);
+
+ /* Get rid of stored operation data */
+ cancellable = grl_operation_get_data (rs->operation_id);
+ g_clear_object (&cancellable);
+
+ if (!grl_net_wc_request_finish (GRL_NET_WC (source_object),
+ res,
+ &content,
+ NULL,
+ &wc_error)) {
+ if (wc_error->code == GRL_NET_WC_ERROR_CANCELLED) {
+ g_propagate_error (&error, wc_error);
+ } else {
+ error = g_error_new (GRL_CORE_ERROR,
+ GRL_CORE_ERROR_RESOLVE_FAILED,
+ _("Failed to connect: %s"),
+ wc_error->message);
+ g_error_free (wc_error);
+ }
+ rs->callback (rs->source, rs->operation_id, rs->media, rs->user_data, error);
+ g_error_free (error);
+
+ return;
+ }
+
+ parser = json_parser_new ();
+ json_parser_load_from_data (parser,
+ content,
+ -1,
+ &error);
+
+ /* Failed to parse JSON response */
+ if (error) {
+ rs->callback (rs->source, rs->operation_id, rs->media, rs->user_data, error);
+ g_error_free (error);
+
+ return;
+ }
+
+ images = get_images (json_parser_get_root (parser), &n_covers);
+
+ GRL_DEBUG ("Found %d covers", n_covers);
+
+ for (i = 0; i < n_covers; i++) {
+ if (images[i]) {
+ relkeys = grl_related_keys_new_with_keys (GRL_METADATA_KEY_THUMBNAIL,
+ images[i],
+ NULL);
+ grl_data_add_related_keys (GRL_DATA (rs->media), relkeys);
+ g_free (images[i]);
+ }
+ }
+
+ rs->callback (rs->source, rs->operation_id, rs->media, rs->user_data, NULL);
+}
+
+static void
+search_album_async (GrlSource *source,
+ const gchar *url,
+ GrlSourceResolveSpec *rs)
+{
+ GCancellable *cancellable;
+
+ if (!wc)
+ wc = grl_net_wc_new ();
+
+ cancellable = g_cancellable_new ();
+ grl_operation_set_data (rs->operation_id, cancellable);
+
+ GRL_DEBUG ("Opening '%s'", url);
+ grl_net_wc_request_async (wc, url, cancellable, search_finished_cb, rs);
+}
+
+/* ================== API Implementation ================ */
+
+static const GList *
+grl_spotify_source_supported_keys (GrlSource *source)
+{
+ static GList *keys = NULL;
+
+ if (!keys) {
+ keys = grl_metadata_key_list_new (GRL_METADATA_KEY_THUMBNAIL,
+ NULL);
+ }
+
+ return keys;
+}
+
+static gboolean
+grl_spotify_source_may_resolve (GrlSource *source,
+ GrlMedia *media,
+ GrlKeyID key_id,
+ GList **missing_keys)
+{
+ gboolean have_artist = FALSE, have_album = FALSE;
+
+ if (key_id != GRL_METADATA_KEY_THUMBNAIL)
+ return FALSE;
+
+ if (media) {
+ if (grl_data_has_key (GRL_DATA (media), GRL_METADATA_KEY_ARTIST))
+ have_artist = TRUE;
+ if (grl_data_has_key (GRL_DATA (media), GRL_METADATA_KEY_ALBUM))
+ have_album = TRUE;
+ }
+
+ if (have_artist && have_album)
+ return TRUE;
+
+ if (missing_keys) {
+ GList *result = NULL;
+ if (!have_artist)
+ result =
+ g_list_append (result, GRLKEYID_TO_POINTER (GRL_METADATA_KEY_ARTIST));
+ if (!have_album)
+ result =
+ g_list_append (result, GRLKEYID_TO_POINTER (GRL_METADATA_KEY_ALBUM));
+
+ if (result)
+ *missing_keys = result;
+ }
+
+ return FALSE;
+}
+
+static void
+grl_spotify_source_resolve (GrlSource *source,
+ GrlSourceResolveSpec *rs)
+{
+ const gchar *artist = NULL;
+ const gchar *album = NULL;
+ gchar *esc_artist = NULL;
+ gchar *esc_album = NULL;
+ gchar *url = NULL;
+
+ GRL_DEBUG (__FUNCTION__);
+
+ GList *iter;
+
+ /* Check that albumart is requested */
+ iter = rs->keys;
+ while (iter) {
+ GrlKeyID key = GRLPOINTER_TO_KEYID (iter->data);
+ if (key == GRL_METADATA_KEY_THUMBNAIL) {
+ break;
+ } else {
+ iter = g_list_next (iter);
+ }
+ }
+
+ if (iter == NULL) {
+ GRL_DEBUG ("No supported key was requested");
+ rs->callback (source, rs->operation_id, rs->media, rs->user_data, NULL);
+ } else {
+ artist = grl_data_get_string (GRL_DATA (rs->media),
+ GRL_METADATA_KEY_ARTIST);
+
+ album = grl_data_get_string (GRL_DATA (rs->media),
+ GRL_METADATA_KEY_ALBUM);
+
+ if (!artist || !album) {
+ GRL_DEBUG ("Missing dependencies");
+ rs->callback (source, rs->operation_id, rs->media, rs->user_data, NULL);
+ } else {
+ esc_artist = g_uri_escape_string (artist, NULL, TRUE);
+ esc_album = g_uri_escape_string (album, NULL, TRUE);
+ url = g_strdup_printf (SPOTIFY_SEARCH_ALBUM, esc_album, esc_artist);
+ GRL_DEBUG ("Searching covers for %s - %s", artist, album);
+ search_album_async (source, url, rs);
+ g_free (esc_artist);
+ g_free (esc_album);
+ g_free (url);
+ }
+ }
+}
+
+static void
+grl_spotify_source_cancel (GrlSource *source,
+ guint operation_id)
+{
+ GCancellable *cancellable =
+ (GCancellable *) grl_operation_get_data (operation_id);
+
+ if (cancellable) {
+ g_cancellable_cancel (cancellable);
+ }
+}
diff --git a/src/spotify/grl-spotify.h b/src/spotify/grl-spotify.h
new file mode 100644
index 0000000..7592273
--- /dev/null
+++ b/src/spotify/grl-spotify.h
@@ -0,0 +1,68 @@
+/* grl-spotify.h
+ *
+ * Copyright (C) 2015 Georges Basile Stavracas Neto <georges stavracas gmail com>
+ *
+ * This file is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as
+ * published by the Free Software Foundation; either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This file 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
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef _GRL_SPOTIFY_SOURCE_H_
+#define _GRL_SPOTIFY_SOURCE_H_
+
+#include <grilo.h>
+
+#define GRL_SPOTIFY_SOURCE_TYPE \
+ (grl_spotify_source_get_type ())
+
+#define GRL_SPOTIFY_SOURCE(obj) \
+ (G_TYPE_CHECK_INSTANCE_CAST ((obj), \
+ GRL_SPOTIFY_SOURCE_TYPE, \
+ GrlSpotifySource))
+
+#define GRL_IS_SPOTIFY_SOURCE(obj) \
+ (G_TYPE_CHECK_INSTANCE_TYPE ((obj), \
+ GRL_SPOTIFY_SOURCE_TYPE))
+
+#define GRL_SPOTIFY_SOURCE_CLASS(klass) \
+ (G_TYPE_CHECK_CLASS_CAST((klass), \
+ GRL_SPOTIFY_SOURCE_TYPE, \
+ GrlSpotifySourceClass))
+
+#define GRL_IS_SPOTIFY_SOURCE_CLASS(klass) \
+ (G_TYPE_CHECK_CLASS_TYPE((klass), \
+ GRL_SPOTIFY_SOURCE_TYPE))
+
+#define GRL_SPOTIFY_SOURCE_GET_CLASS(obj) \
+ (G_TYPE_INSTANCE_GET_CLASS ((obj), \
+ GRL_SPOTIFY_SOURCE_TYPE, \
+ GrlSpotifySourceClass))
+
+typedef struct _GrlSpotifySource GrlSpotifySource;
+
+struct _GrlSpotifySource {
+
+ GrlSource parent;
+
+};
+
+typedef struct _GrlSpotifySourceClass GrlSpotifySourceClass;
+
+struct _GrlSpotifySourceClass {
+
+ GrlSourceClass parent_class;
+
+};
+
+GType grl_spotify_source_get_type (void);
+
+#endif /* _GRL_SPOTIFY_SOURCE_H_ */
diff --git a/src/spotify/grl-spotify.xml b/src/spotify/grl-spotify.xml
new file mode 100644
index 0000000..dc747fa
--- /dev/null
+++ b/src/spotify/grl-spotify.xml
@@ -0,0 +1,10 @@
+<plugin>
+ <info>
+ <name>Provider for Spotify services</name>
+ <module>libgrlspotify</module>
+ <description>A plugin for using Spotify services</description>
+ <author>Georges Basile Stavracas Neto.</author>
+ <license>LGPL</license>
+ <site>http://www.feaneron.com</site>
+ </info>
+</plugin>
[
Date Prev][
Date Next] [
Thread Prev][
Thread Next]
[
Thread Index]
[
Date Index]
[
Author Index]