[grilo-plugins/wip/gbsneto/spotify] spotify: implement Spotify plugin



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]