[grilo-plugins] tmdb: Add TMDb plugin



commit e27961064c6ea88cc5a0bdbab010b33af4591329
Author: Jens Georg <jensg openismus com>
Date:   Tue Oct 2 09:34:33 2012 +0200

    tmdb: Add TMDb plugin
    
    Retrieves movies information from TMDb.
    
    TMDb, or themoviedb.org, is a free and community maintained movie
    database.
    
    Signed-off-by: Juan A. Suarez Romero <jasuarez igalia com>

 configure.ac                |   32 ++
 src/Makefile.am             |    6 +-
 src/tmdb/Makefile.am        |   41 ++
 src/tmdb/grl-tmdb-request.c |  585 ++++++++++++++++++++++
 src/tmdb/grl-tmdb-request.h |  126 +++++
 src/tmdb/grl-tmdb.c         | 1161 +++++++++++++++++++++++++++++++++++++++++++
 src/tmdb/grl-tmdb.h         |   74 +++
 src/tmdb/grl-tmdb.xml       |   10 +
 8 files changed, 2034 insertions(+), 1 deletions(-)
---
diff --git a/configure.ac b/configure.ac
index ae897f0..69e5a49 100644
--- a/configure.ac
+++ b/configure.ac
@@ -131,6 +131,8 @@ PKG_CHECK_MODULES(GTHREAD, gthread-2.0, HAVE_GTHREAD=yes, HAVE_GTHREAD=no)
 
 PKG_CHECK_MODULES(TOTEM_PL_PARSER, totem-plparser >= 3.4.1, HAVE_TOTEM_PL_PARSER=yes, HAVE_TOTEM_PL_PARSER=no)
 
+PKG_CHECK_MODULES([JSON], [json-glib-1.0], HAVE_JSON_GLIB=yes, HAVE_JSON_GLIB=no)
+
 PKG_CHECK_MODULES(GMIME, gmime-2.6,
                          HAVE_GMIME=yes,
                          [PKG_CHECK_MODULES(GMIME, gmime-2.4,
@@ -843,6 +845,35 @@ AC_SUBST(DMAP_PLUGIN_ID)
 AC_DEFINE_UNQUOTED([DMAP_PLUGIN_ID], ["$DMAP_PLUGIN_ID"], [DMAP plugin ID])
 
 # ----------------------------------------------------------
+# BUILD TMDB PLUGIN
+# ----------------------------------------------------------
+
+AC_ARG_ENABLE([tmdb],
+        AC_HELP_STRING([--enable-tmdb],
+                       [enable TMDb metadata plugin (default: auto)]),
+        [
+         AS_IF([test "x$enableval" = "xyes"],
+               [AS_IF([test "x$HAVE_JSON_GLIB" = "xno"],
+                      [AC_MSG_ERROR([json-glib-1.0 not found, install it or use --disable-tmdb])
+                      ])
+               ])],
+        [
+         AS_IF([test "x$HAVE_JSON_GLIB" = "xyes"],
+               [enable_tmdb=yes],
+               [enable_tmdb=no])
+        ]
+)
+
+AM_CONDITIONAL([TMDB_PLUGIN], [test "x$enable_tmdb" = "xyes"])
+GRL_PLUGINS_ALL="$GRL_PLUGINS_ALL tmdb"
+AS_IF([test "x$enable_tmdb" = "xyes"],
+      [GRL_PLUGINS_ENABLED="$GRL_PLUGINS_ENABLED tmdb"])
+
+TMDB_PLUGIN_ID="grl-tmdb"
+AC_SUBST(TMDB_PLUGIN_ID)
+AC_DEFINE_UNQUOTED([TMDB_PLUGIN_ID], ["$TMDB_PLUGIN_ID"], [TMDb plugin ID])
+
+# ----------------------------------------------------------
 # GETTEXT
 # ----------------------------------------------------------
 
@@ -881,6 +912,7 @@ AC_CONFIG_FILES([
   src/optical-media/Makefile
   src/podcasts/Makefile
   src/shoutcast/Makefile
+  src/tmdb/Makefile
   src/tracker/Makefile
   src/upnp/Makefile
   src/vimeo/Makefile
diff --git a/src/Makefile.am b/src/Makefile.am
index 4795e02..0078e85 100644
--- a/src/Makefile.am
+++ b/src/Makefile.am
@@ -68,6 +68,10 @@ if SHOUTCAST_PLUGIN
 SUBDIRS += shoutcast
 endif
 
+if TMDB_PLUGIN
+SUBDIRS += tmdb
+endif
+
 if TRACKER_PLUGIN
 SUBDIRS += tracker
 endif
@@ -87,7 +91,7 @@ endif
 DIST_SUBDIRS = \
 	apple-trailers bliptv bookmarks dmap fake-metadata filesystem flickr	\
 	gravatar jamendo lastfm-albumart local-metadata metadata-store			\
-	optical-media podcasts shoutcast tracker upnp vimeo youtube
+	optical-media podcasts shoutcast tmdb tracker upnp vimeo youtube
 
 MAINTAINERCLEANFILES =	\
         *.in		\
diff --git a/src/tmdb/Makefile.am b/src/tmdb/Makefile.am
new file mode 100644
index 0000000..d40ad98
--- /dev/null
+++ b/src/tmdb/Makefile.am
@@ -0,0 +1,41 @@
+#
+# Makefile.am
+#
+# Author: Jens Georg <jensg openismus com>
+#
+# Copyright (C) 2012 Openismus GmbH.
+
+ext_LTLIBRARIES		 = libgrltmdb.la
+
+libgrltmdb_la_CFLAGS =	\
+	$(DEPS_CFLAGS) \
+	$(LIBSOUP_CFLAGS) \
+	$(GRLNET_CFLAGS) \
+	$(JSON_CFLAGS)
+
+libgrltmdb_la_LIBADD =	\
+	$(DEPS_LIBS) \
+	$(LIBSOUP_LIBS) \
+	$(GRLNET_LIBS) \
+	$(JSON_LIBS)
+
+libgrltmdb_la_LDFLAGS = \
+	-no-undefined		\
+	-module			\
+	-avoid-version
+
+libgrltmdb_la_SOURCES = \
+	grl-tmdb.c grl-tmdb.h \
+	grl-tmdb-request.c grl-tmdb-request.h
+
+extdir					= $(GRL_PLUGINS_DIR)
+tmdbxmldir	= $(GRL_PLUGINS_DIR)
+tmdbxml_DATA	= $(TMDB_PLUGIN_ID).xml
+
+EXTRA_DIST = $(tmdbxml_DATA)
+
+MAINTAINERCLEANFILES	 = \
+	*.in \
+	*~
+
+DISTCLEANFILES = $(MAINTAINERCLEANFILES)
diff --git a/src/tmdb/grl-tmdb-request.c b/src/tmdb/grl-tmdb-request.c
new file mode 100644
index 0000000..d82c3f1
--- /dev/null
+++ b/src/tmdb/grl-tmdb-request.c
@@ -0,0 +1,585 @@
+/*
+ * Copyright (C) 2012 Canonical Ltd.
+ *
+ * Author: Jens Georg <jensg openismus com>
+ *
+ * This library 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; version 2.1 of
+ * the License, or (at your option) any later version.
+ *
+ * This library 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 Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
+ * 02110-1301 USA
+ *
+ */
+
+#ifdef HAVE_CONFIG_H
+#include "config.h"
+#endif
+
+#include <string.h>
+
+#include <grilo.h>
+#include <net/grl-net.h>
+#include <libsoup/soup.h>
+#include <json-glib/json-glib.h>
+
+#include "grl-tmdb-request.h"
+
+/* URIs for TMDb.org API V3 */
+#define TMDB_BASE_URI "http://api.themoviedb.org/3/";
+#define TMDB_API_CALL_CONFIGURATION "configuration"
+#define TMDB_API_CALL_SEARCH_MOVIE "search/movie"
+#define TMDB_API_CALL_MOVIE_INFO "movie/%"G_GUINT64_FORMAT
+#define TMDB_API_CALL_MOVIE_CAST TMDB_API_CALL_MOVIE_INFO"/casts"
+#define TMDB_API_CALL_MOVIE_IMAGES TMDB_API_CALL_MOVIE_INFO"/images"
+#define TMDB_API_CALL_MOVIE_KEYWORDS TMDB_API_CALL_MOVIE_INFO"/keywords"
+#define TMDB_API_CALL_MOVIE_RELEASE_INFO TMDB_API_CALL_MOVIE_INFO"/releases"
+
+struct _FilterClosure {
+  GrlTmdbRequestFilterFunc filter;
+  GList *list;
+};
+
+typedef struct _FilterClosure FilterClosure;
+
+/* GObject setup functions */
+static void grl_tmdb_request_class_init (GrlTmdbRequestClass * klass);
+static void grl_tmdb_request_init (GrlTmdbRequest *self);
+
+/* GObject vfuncs */
+static void grl_tmdb_request_set_property (GObject *object,
+                                           guint property_id,
+                                           const GValue *value,
+                                           GParamSpec *pspec);
+static void grl_tmdb_request_finalize (GObject *object);
+
+static void grl_tmdb_request_constructed (GObject *object);
+
+enum {
+  PROP_0,
+  PROP_URI,
+  PROP_API_KEY,
+  PROP_ARGS
+};
+
+struct _GrlTmdbRequestPrivate {
+  char *uri;
+  char *api_key;
+  GHashTable *args;
+  SoupURI *base;
+  GSimpleAsyncResult *simple;
+  JsonParser *parser;
+  GrlTmdbRequestDetail detail;
+};
+
+G_DEFINE_TYPE (GrlTmdbRequest, grl_tmdb_request, G_TYPE_OBJECT);
+
+/* Implementation */
+
+/* GObject functions */
+
+/* GObject setup functions */
+
+static void
+grl_tmdb_request_class_init (GrlTmdbRequestClass * klass)
+{
+  GObjectClass *gobject_class = G_OBJECT_CLASS (klass);
+
+  g_type_class_add_private (klass, sizeof (GrlTmdbRequestPrivate));
+
+  gobject_class->set_property = grl_tmdb_request_set_property;
+  gobject_class->finalize = grl_tmdb_request_finalize;
+  gobject_class->constructed = grl_tmdb_request_constructed;
+
+  g_object_class_install_property (gobject_class,
+                                   PROP_URI,
+                                   g_param_spec_string ("uri",
+                                                        "uri",
+                                                        "URI used for the request",
+                                                        NULL,
+                                                        G_PARAM_WRITABLE
+                                                        | G_PARAM_CONSTRUCT_ONLY
+                                                        | G_PARAM_STATIC_STRINGS));
+
+  g_object_class_install_property (gobject_class,
+                                   PROP_API_KEY,
+                                   g_param_spec_string ("api-key",
+                                                        "api-key",
+                                                        "TMDb API key",
+                                                        NULL,
+                                                        G_PARAM_WRITABLE
+                                                        | G_PARAM_CONSTRUCT_ONLY
+                                                        | G_PARAM_STATIC_STRINGS));
+  g_object_class_install_property (gobject_class,
+                                   PROP_ARGS,
+                                   g_param_spec_boxed ("args",
+                                                       "args",
+                                                       "HTTP GET arguments",
+                                                       G_TYPE_HASH_TABLE,
+                                                       G_PARAM_WRITABLE
+                                                       | G_PARAM_CONSTRUCT_ONLY
+                                                       | G_PARAM_STATIC_STRINGS));
+}
+
+static void
+grl_tmdb_request_init (GrlTmdbRequest *self)
+{
+  self->priv = G_TYPE_INSTANCE_GET_PRIVATE (self,
+                                            GRL_TMDB_REQUEST_TYPE,
+                                            GrlTmdbRequestPrivate);
+  self->priv->base = soup_uri_new (TMDB_BASE_URI);
+  self->priv->parser = json_parser_new ();
+  self->priv->detail = GRL_TMDB_REQUEST_DETAIL_COUNT;
+}
+
+/* GObject vfuncs */
+static void
+grl_tmdb_request_set_property (GObject *object,
+                               guint property_id,
+                               const GValue *value,
+                               GParamSpec *pspec)
+{
+  GrlTmdbRequest *self = GRL_TMDB_REQUEST (object);
+
+  switch (property_id) {
+    case PROP_API_KEY:
+      self->priv->api_key = g_value_dup_string (value);
+      break;
+    case PROP_URI:
+      self->priv->uri = g_value_dup_string (value);
+      break;
+    case PROP_ARGS:
+      self->priv->args = g_value_dup_boxed (value);
+      break;
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
+  }
+}
+
+static void
+grl_tmdb_request_finalize (GObject *object)
+{
+  GrlTmdbRequest *self = GRL_TMDB_REQUEST (object);
+
+  if (self->priv->api_key != NULL) {
+    g_free (self->priv->api_key);
+    self->priv->api_key = NULL;
+  }
+
+  if (self->priv->uri != NULL) {
+    g_free (self->priv->uri);
+    self->priv->uri = NULL;
+  }
+
+  if (self->priv->args != NULL) {
+    g_hash_table_unref (self->priv->args);
+    self->priv->args = NULL;
+  }
+
+  if (self->priv->base != NULL) {
+    soup_uri_free (self->priv->base);
+    self->priv->base = NULL;
+  }
+
+  if (self->priv->parser != NULL) {
+    g_object_unref (self->priv->parser);
+    self->priv->parser = NULL;
+  }
+
+  G_OBJECT_CLASS (grl_tmdb_request_parent_class)->finalize (object);
+}
+
+static void
+grl_tmdb_request_constructed (GObject *object)
+{
+  GrlTmdbRequest *self = GRL_TMDB_REQUEST (object);
+  if (self->priv->args == NULL) {
+    self->priv->args = g_hash_table_new_full (g_str_hash,
+                                              g_str_equal,
+                                              NULL,
+                                              g_free);
+  }
+  g_hash_table_insert (self->priv->args, "api_key", g_strdup (self->priv->api_key));
+
+  G_OBJECT_CLASS (grl_tmdb_request_parent_class)->constructed (object);
+}
+
+
+/* Private functions */
+static void
+fill_string_list_filtered (JsonArray *array,
+                           guint index_,
+                           JsonNode *element,
+                           gpointer user_data)
+{
+  FilterClosure *closure = (FilterClosure *) user_data;
+  char *result;
+
+  if (closure->filter == NULL) {
+    closure->list = g_list_prepend (closure->list,
+                                    g_strdup (json_node_get_string (element)));
+    return;
+  }
+
+  result = closure->filter (element);
+  if (result != NULL) {
+    closure->list = g_list_prepend (closure->list, result);
+  }
+}
+
+/* Callbacks */
+static void
+on_wc_request (GrlNetWc *wc,
+               GAsyncResult *res,
+               gpointer user_data)
+{
+  GrlTmdbRequest *self = GRL_TMDB_REQUEST (user_data);
+  char *content;
+  gsize length = 0;
+  GError *error = NULL;
+
+  if (!grl_net_wc_request_finish (wc, res, &content, &length, &error)) {
+    g_simple_async_result_set_from_error (self->priv->simple, error);
+
+    goto out;
+  }
+
+  if (!json_parser_load_from_data (self->priv->parser, content, length, &error)) {
+    GRL_WARNING ("Could not parse JSON: %s", error->message);
+    g_simple_async_result_set_from_error (self->priv->simple, error);
+
+    goto out;
+  }
+
+out:
+  g_simple_async_result_complete_in_idle (self->priv->simple);
+  g_object_unref (self->priv->simple);
+}
+
+/* Public functions */
+/**
+ * grl_tmdb_request_new:
+ * @api_key: TMDb.org API key to use for this request
+ * @uri: URI fragment for API call, i.e. /configuration or /movie/11
+ * @args: (allow-none) (element-type utf8 utf8): Optional arguments to pass to
+ * the function call
+ * Returns: (transfer full): A new instance of GrlTmdbRequest
+ *
+ * Generic constructor for the convenience class to handle the async API HTTP
+ * requests and JSON parsing of the result.
+ */
+GrlTmdbRequest *
+grl_tmdb_request_new (const char *api_key, const char *uri, GHashTable *args)
+{
+  return g_object_new (GRL_TMDB_REQUEST_TYPE,
+                       "api-key", api_key,
+                       "uri", uri,
+                       "args", args,
+                       NULL);
+}
+
+
+/**
+ * grl_tmdb_request_new_search:
+ * @api_key: TMDb.org API key to use for this request
+ * @needle: The term to search for
+ * Returns: (transfer full): A new instance of #GrlTmdbRequest
+ *
+ * Convenience function to create a #GrlTmdbRequest that performs a movie search
+ */
+GrlTmdbRequest *
+grl_tmdb_request_new_search (const char *api_key, const char *needle)
+{
+  GHashTable *args;
+  GrlTmdbRequest *result;
+
+  args = g_hash_table_new_full (g_str_hash, g_str_equal, NULL, g_free);
+  g_hash_table_insert (args, "query", g_strdup (needle));
+
+  result = g_object_new (GRL_TMDB_REQUEST_TYPE,
+                         "api-key", api_key,
+                         "uri", TMDB_API_CALL_SEARCH_MOVIE,
+                         "args", args,
+                         NULL);
+  g_hash_table_unref (args);
+
+  return result;
+}
+
+/**
+ * grl_tmdb_request_new_details:
+ * @api_key: TMDb.org API key to use for this request
+ * @detail: The detailed information to request for the movie @id
+ * @id: TMDb.org identifier of the movie.
+ * Returns: (transfer full): A new instance of #GrlTmdbRequest
+ *
+ * Convenience function to create a #GrlTmdbRequest that gets detailed
+ * information about a movie.
+ */
+GrlTmdbRequest *
+grl_tmdb_request_new_details (const char *api_key,
+                              GrlTmdbRequestDetail detail,
+                              guint64 id)
+{
+  GrlTmdbRequest *result;
+  char *uri;
+  const char *template;
+
+  switch (detail) {
+    case GRL_TMDB_REQUEST_DETAIL_MOVIE:
+      template = TMDB_API_CALL_MOVIE_INFO;
+      break;
+    case GRL_TMDB_REQUEST_DETAIL_MOVIE_CAST:
+      template = TMDB_API_CALL_MOVIE_CAST;
+      break;
+    case GRL_TMDB_REQUEST_DETAIL_MOVIE_IMAGES:
+      template = TMDB_API_CALL_MOVIE_IMAGES;
+      break;
+    case GRL_TMDB_REQUEST_DETAIL_MOVIE_KEYWORDS:
+      template = TMDB_API_CALL_MOVIE_KEYWORDS;
+      break;
+    case GRL_TMDB_REQUEST_DETAIL_MOVIE_RELEASE_INFO:
+      template = TMDB_API_CALL_MOVIE_RELEASE_INFO;
+      break;
+    default:
+      g_assert_not_reached ();
+      break;
+  }
+
+  uri = g_strdup_printf (template, id);
+  result = g_object_new (GRL_TMDB_REQUEST_TYPE,
+                         "api-key", api_key,
+                         "uri", uri,
+                         "args", NULL,
+                         NULL);
+  result->priv->detail = detail;
+  g_free (uri);
+
+  return result;
+}
+
+GrlTmdbRequest *
+grl_tmdb_request_new_configuration (const char *api_key)
+{
+  return g_object_new (GRL_TMDB_REQUEST_TYPE,
+                       "api-key", api_key,
+                       "uri", TMDB_API_CALL_CONFIGURATION,
+                       "args", NULL,
+                       NULL);
+}
+
+/**
+ * grl_tmdb_request_run_async:
+ * @self: Instance of GrlTmdbRequest
+ * @callback: Callback to notify after the request is complete
+ * @cancellable: (allow-none): An optional cancellable to cancel this operation
+ * @user_data: User data to pass on to @callback.
+ *
+ * Schedule the request for execution.
+ */
+void
+grl_tmdb_request_run_async (GrlTmdbRequest *self,
+                            GrlNetWc *wc,
+                            GAsyncReadyCallback callback,
+                            GCancellable *cancellable,
+                            gpointer user_data)
+{
+  SoupURI *uri;
+  char *call;
+  GHashTable *headers;
+
+  uri = soup_uri_new_with_base (self->priv->base, self->priv->uri);
+  soup_uri_set_query_from_form (uri, self->priv->args);
+  call = soup_uri_to_string (uri, FALSE);
+  soup_uri_free (uri);
+
+  self->priv->simple = g_simple_async_result_new (G_OBJECT (self),
+                                                  callback,
+                                                  user_data,
+                                                  (gpointer) grl_tmdb_request_run_async);
+
+  GRL_DEBUG ("Requesting %s", call);
+
+  headers = g_hash_table_new (g_str_hash, g_str_equal);
+  g_hash_table_insert (headers, "Accept", "application/json");
+
+  grl_net_wc_request_with_headers_hash_async (wc,
+                                              call,
+                                              headers,
+                                              cancellable,
+                                              (GAsyncReadyCallback) on_wc_request,
+                                              self);
+  g_hash_table_unref (headers);
+}
+
+/**
+ * grl_tmdb_request_run_finish:
+ * @self: Instance of GrlTmdbRequest
+ * @result: #GAsyncResult of the operation
+ * @error: (allow-none): Return location for a possible error
+ * @returns: %TRUE if the request succeeded, %FALSE otherwise.
+ *
+ * Finalize a request previously scheduled with grl_tmdb_request_run_async().
+ * Usually called from the call-back passed to this function. After this
+ * grl_tmdb_request_run_finish() returned %TRUE, grl_tmdb_request_get() should
+ * return proper data.
+ */
+gboolean
+grl_tmdb_request_run_finish (GrlTmdbRequest *self,
+                             GAsyncResult *result,
+                             GError **error)
+{
+  GSimpleAsyncResult *simple;
+
+  if (!g_simple_async_result_is_valid (result,
+                                       G_OBJECT (self),
+                                       (gpointer) grl_tmdb_request_run_async)) {
+    return FALSE;
+  }
+
+  simple = (GSimpleAsyncResult *) result;
+  if (g_simple_async_result_propagate_error (simple, error)) {
+    return FALSE;
+  }
+
+  return TRUE;
+}
+
+/**
+ * grl_tmdb_request_get:
+ * @self: Instance of GrlTmdbRequest
+ * @path: JSONPath to get
+ * @returns: (transfer full): %NULL if the key cannot be found or
+ * the request is otherwise in error or the value of the key.
+ *
+ * Get a value from the API call represented by this instance.
+ */
+GValue *
+grl_tmdb_request_get (GrlTmdbRequest *self,
+                      const char *path)
+{
+  JsonNode *node;
+  GError *error = NULL;
+  GValue *value = NULL;
+  JsonArray *values;
+
+  value = g_new0 (GValue, 1);
+
+  node = json_path_query (path,
+                          json_parser_get_root (self->priv->parser),
+                          &error);
+  if (error != NULL) {
+    GRL_DEBUG ("Failed to get %s: %s", path, error->message);
+    g_error_free (error);
+
+    return NULL;
+  }
+
+  values = json_node_get_array (node);
+  json_node_get_value (json_array_get_element (values, 0), value);
+
+  json_node_free (node);
+
+  return value;
+}
+
+/**
+ * grl_tmdb_request_get_string_list:
+ * @self: Instance of GrlTmdbRequest
+ * @path: JSONPath to get
+ * Returns: (transfer full) (element-type utf-8): %NULL if the path cannot be
+ * found or a #GList containing strings matching the path.
+ */
+GList *
+grl_tmdb_request_get_string_list (GrlTmdbRequest *self,
+                                  const char *path)
+{
+  return grl_tmdb_request_get_string_list_with_filter (self, path, NULL);
+}
+
+/**
+ * grl_tmdb_request_get_string_list_with_filter:
+ * @self: Instance of #GrlTmdbRequest
+ * @path: JSONPath to get
+ * @filter: A #GrlTmdbRequestFilterFunc to match on a #JsonNode
+ * Returns: (transfer full) (element-type utf-8): %NULL if the path cannot be
+ * found or no node matched the filter or a #GList containing strings matching
+ * the path and are accepted by the filter.
+ */
+GList *
+grl_tmdb_request_get_string_list_with_filter (GrlTmdbRequest *self,
+                                              const char *path,
+                                              GrlTmdbRequestFilterFunc filter)
+{
+  JsonNode *node, *element;
+  GError *error = NULL;
+  JsonArray *values;
+  FilterClosure closure;
+
+  node = json_path_query (path,
+                          json_parser_get_root (self->priv->parser),
+                          &error);
+  if (error != NULL) {
+    GRL_DEBUG ("Failed to get %s: %s", path, error->message);
+    g_error_free (error);
+    return NULL;
+  }
+
+  if (!JSON_NODE_HOLDS_ARRAY (node)) {
+    json_node_free (node);
+    return NULL;
+  }
+
+  values = json_node_get_array (node);
+  if (json_array_get_length (values) == 0) {
+    json_node_free (node);
+    return NULL;
+  }
+
+  /* Check if we have array in array */
+  element = json_array_get_element (values, 0);
+  if (JSON_NODE_HOLDS_ARRAY (element)) {
+    values = json_node_get_array (element);
+  }
+
+  closure.list = NULL;
+  closure.filter = filter;
+
+  json_array_foreach_element (values, fill_string_list_filtered, &closure);
+
+  json_node_free (node);
+
+  return closure.list;
+}
+
+/**
+ * grl_tmdb_request_get_detail:
+ * @self: Instance of #GrlTmdbRequest
+ * Returns: An id of #GrlTmdbRequestDetail or #GRL_TMDB_REQUEST_DETAIL_NONE if
+ * the request is not a detail request.
+ */
+GrlTmdbRequestDetail
+grl_tmdb_request_get_detail (GrlTmdbRequest *self)
+{
+  return self->priv->detail;
+}
+
+/**
+ * grl_tmdb_request_get_uri:
+ * @self: Instance of #GrlTmdbRequest
+ * Returns: The URI for the request. Mostly useful for debugging or error
+ * messages
+ */
+const char *
+grl_tmdb_request_get_uri (GrlTmdbRequest *self)
+{
+  return self->priv->uri;
+}
diff --git a/src/tmdb/grl-tmdb-request.h b/src/tmdb/grl-tmdb-request.h
new file mode 100644
index 0000000..ba96985
--- /dev/null
+++ b/src/tmdb/grl-tmdb-request.h
@@ -0,0 +1,126 @@
+/*
+ * Copyright (C) 2012 Canonical Ltd.
+ *
+ * Author: Jens Georg <jensg openismus com>
+ *
+ * This library 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; version 2.1 of
+ * the License, or (at your option) any later version.
+ *
+ * This library 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 Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
+ * 02110-1301 USA
+ *
+ */
+
+#ifndef _GRL_TMDB_REQUEST_H_
+#define _GRL_TMDB_REQUEST_H_
+
+#include <glib-object.h>
+#include <gio/gio.h>
+#include <json-glib/json-glib.h>
+
+#include <net/grl-net.h>
+
+#define GRL_TMDB_REQUEST_TYPE (grl_tmdb_request_get_type())
+#define GRL_TMDB_REQUEST(obj) \
+  (G_TYPE_CHECK_INSTANCE_CAST ((obj), \
+                               GRL_TMDB_REQUEST_TYPE, \
+                               GrlTmdbRequest))
+#define GRL_IS_TMDB_REQUEST(obj) \
+  (G_TYPE_CHECK_INSTANCE_TYPE ((obj), \
+                               GRL_TMDB_REQUEST_TYPE))
+
+#define GRL_TMDB_REQUEST_CLASS(klass) \
+  (G_TYPE_CHECK_CLASS_CAST ((klass), \
+                            GRL_TMDB_REQUEST_TYPE, \
+                            GrlTmdbRequestClass))
+#define GRL_IS_TMDB_REQUEST_CLASS(obj) \
+  (G_TYPE_CHECK_CLASS_TYPE ((obj), \
+                            GRL_TMDB_REQUEST_TYPE))
+
+#define GRL_TMDB_REQUEST_GET_CLASS(klass) \
+  (G_TYPE_INSTANCE_GET_CLASS ((klass), \
+                              GRL_TMDB_REQUEST_TYPE, \
+                              GrlTmdbRequestClass))
+
+typedef struct _GrlTmdbRequest GrlTmdbRequest;
+typedef struct _GrlTmdbRequestClass GrlTmdbRequestClass;
+typedef struct _GrlTmdbRequestPrivate GrlTmdbRequestPrivate;
+
+struct _GrlTmdbRequest {
+  GObject parent;
+
+  GrlTmdbRequestPrivate *priv;
+};
+
+struct _GrlTmdbRequestClass {
+  GObjectClass parent_class;
+};
+
+enum _GrlTmdbRequestDetail {
+  GRL_TMDB_REQUEST_DETAIL_MOVIE,
+  GRL_TMDB_REQUEST_DETAIL_MOVIE_CAST,
+  GRL_TMDB_REQUEST_DETAIL_MOVIE_IMAGES,
+  GRL_TMDB_REQUEST_DETAIL_MOVIE_KEYWORDS,
+  GRL_TMDB_REQUEST_DETAIL_MOVIE_RELEASE_INFO,
+  GRL_TMDB_REQUEST_DETAIL_COUNT
+};
+typedef enum _GrlTmdbRequestDetail GrlTmdbRequestDetail;
+
+typedef char *(*GrlTmdbRequestFilterFunc) (JsonNode *element);
+
+GType grl_tmdb_request_get_type (void);
+
+GrlTmdbRequest *
+grl_tmdb_request_new (const char *api_key, const char *uri, GHashTable *args);
+
+GrlTmdbRequest *
+grl_tmdb_request_new_search (const char *api_key, const char *needle);
+
+GrlTmdbRequest *
+grl_tmdb_request_new_details (const char *api_key,
+                              GrlTmdbRequestDetail detail,
+                              guint64 id);
+
+GrlTmdbRequest *
+grl_tmdb_request_new_configuration (const char *api_key);
+
+GrlTmdbRequestDetail
+grl_tmdb_request_get_detail (GrlTmdbRequest *request);
+
+const char *
+grl_tmdb_request_get_uri (GrlTmdbRequest *request);
+
+void
+grl_tmdb_request_run_async (GrlTmdbRequest *request,
+                            GrlNetWc *wc,
+                            GAsyncReadyCallback callback,
+                            GCancellable *cancellable,
+                            gpointer user_data);
+
+gboolean
+grl_tmdb_request_run_finish (GrlTmdbRequest *request,
+                             GAsyncResult *result,
+                             GError **error);
+
+GValue *
+grl_tmdb_request_get (GrlTmdbRequest *request,
+                      const char *key);
+
+GList *
+grl_tmdb_request_get_string_list (GrlTmdbRequest *request,
+                                  const char *path);
+
+GList *
+grl_tmdb_request_get_string_list_with_filter (GrlTmdbRequest *self,
+                                              const char *path,
+                                              GrlTmdbRequestFilterFunc filter);
+#endif /* _GRL_TMDB_REQUEST_H_ */
diff --git a/src/tmdb/grl-tmdb.c b/src/tmdb/grl-tmdb.c
new file mode 100644
index 0000000..02151eb
--- /dev/null
+++ b/src/tmdb/grl-tmdb.c
@@ -0,0 +1,1161 @@
+/*
+ * Copyright (C) 2012 Canonical Ltd.
+ *
+ * Author: Jens Georg <jensg openismus com>
+ *
+ * This library 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; version 2.1 of
+ * the License, or (at your option) any later version.
+ *
+ * This library 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 Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
+ * 02110-1301 USA
+ *
+ */
+
+#ifdef HAVE_CONFIG_H
+#include "config.h"
+#endif
+
+#include <string.h>
+
+#include <grilo.h>
+#include <net/grl-net.h>
+#include <libsoup/soup-uri.h>
+#include <json-glib/json-glib.h>
+
+#include "grl-tmdb.h"
+#include "grl-tmdb-request.h"
+
+#define GRL_LOG_DOMAIN_DEFAULT tmdb_log_domain
+GRL_LOG_DOMAIN_STATIC(tmdb_log_domain);
+
+#define PLUGIN_ID   TMDB_PLUGIN_ID
+
+#define SOURCE_ID   "grl-tmdb"
+#define SOURCE_NAME "TMDb Metadata Provider"
+#define SOURCE_DESC "A source for movie metadata from themoviedb.org"
+
+#define SHOULD_RESOLVE(key) \
+    g_hash_table_contains (closure->keys, GINT_TO_POINTER ((key)))
+
+enum {
+  PROP_0,
+  PROP_API_KEY
+};
+
+static GrlKeyID GRL_TMDB_METADATA_KEY_BACKDROPS = GRL_METADATA_KEY_INVALID;
+static GrlKeyID GRL_TMDB_METADATA_KEY_POSTERS = GRL_METADATA_KEY_INVALID;
+static GrlKeyID GRL_TMDB_METADATA_KEY_IMDB_ID = GRL_METADATA_KEY_INVALID;
+static GrlKeyID GRL_TMDB_METADATA_KEY_KEYWORDS = GRL_METADATA_KEY_INVALID;
+static GrlKeyID GRL_TMDB_METADATA_KEY_PERFORMER = GRL_METADATA_KEY_INVALID;
+static GrlKeyID GRL_TMDB_METADATA_KEY_PRODUCER = GRL_METADATA_KEY_INVALID;
+static GrlKeyID GRL_TMDB_METADATA_KEY_DIRECTOR = GRL_METADATA_KEY_INVALID;
+static GrlKeyID GRL_TMDB_METADATA_KEY_AGE_CERTIFICATES = GRL_METADATA_KEY_INVALID;
+static GrlKeyID GRL_TMDB_METADATA_KEY_ORIGINAL_TITLE = GRL_METADATA_KEY_INVALID;
+
+struct _GrlTmdbSourcePrivate {
+  char *api_key;
+  GHashTable *supported_keys;
+  GHashTable *slow_keys;
+  GrlNetWc *wc;
+  GrlTmdbRequest *configuration;
+  gboolean config_pending;
+  GQueue *pending_resolves;
+  SoupURI *image_base_uri;
+};
+
+struct _ResolveClosure {
+  GrlTmdbSource *self;
+  GrlSourceResolveSpec *rs;
+  GQueue *pending_requests;
+  guint64 id;
+  GHashTable *keys;
+  gboolean slow;
+};
+
+typedef struct _ResolveClosure ResolveClosure;
+
+static GrlTmdbSource *grl_tmdb_source_new (const char *api_key);
+
+static void grl_tmdb_source_resolve (GrlSource *source,
+                                     GrlSourceResolveSpec *rs);
+
+static const
+GList *grl_tmdb_source_supported_keys (GrlSource *source);
+
+static const GList *
+grl_tmdb_source_slow_keys (GrlSource *source);
+
+static gboolean grl_tmdb_source_may_resolve (GrlSource *source,
+                                             GrlMedia *media,
+                                             GrlKeyID key_id,
+                                             GList **missing_keys);
+
+gboolean grl_tmdb_source_plugin_init (GrlRegistry *registry,
+                                      GrlPlugin *plugin,
+                                      GList *configs);
+
+static void grl_tmdb_source_set_property (GObject *object,
+                                          guint property_id,
+                                          const GValue *value,
+                                          GParamSpec *pspec);
+
+static void grl_tmdb_source_finalize (GObject *object);
+
+static GrlKeyID
+register_metadata_key (GrlRegistry *registry,
+                       const char *name,
+                       const char *nick,
+                       const char *blurb);
+
+static void resolve_closure_free (ResolveClosure *closure);
+/* =================== GrlTmdbMetadata Plugin  =============== */
+
+
+gboolean
+grl_tmdb_source_plugin_init (GrlRegistry *registry,
+                             GrlPlugin *plugin,
+                             GList *configs)
+{
+  GrlConfig *config;
+  char *api_key;
+
+  GRL_LOG_DOMAIN_INIT (tmdb_log_domain, "tmdb");
+
+  GRL_DEBUG ("grl_tmdb_source_plugin_init");
+
+  if (!configs) {
+    GRL_INFO ("No configuration provided. Will not load plugin");
+    return FALSE;
+  }
+
+  config = GRL_CONFIG (configs->data);
+  api_key = grl_config_get_api_key (config);
+  if (!api_key) {
+    GRL_INFO ("Missing API Key, cannot load plugin");
+    return FALSE;
+  }
+
+  GRL_TMDB_METADATA_KEY_BACKDROPS =
+    register_metadata_key (registry,
+                           "tmdb-backdrops",
+                           "tmdb-backdrops",
+                           "A list of URLs for movie backdrops");
+
+  GRL_TMDB_METADATA_KEY_POSTERS =
+    register_metadata_key (registry,
+                           "tmdb-poster",
+                           "tmdb-poster",
+                           "A list of URLs for movie posters");
+
+  GRL_TMDB_METADATA_KEY_IMDB_ID =
+    register_metadata_key (registry,
+                           "tmdb-imdb-id",
+                           "tmdb-imdb-id",
+                           "ID of this movie at imdb.org");
+
+  GRL_TMDB_METADATA_KEY_KEYWORDS =
+    register_metadata_key (registry,
+                           "tmdb-keywords",
+                           "tmdb-keywords",
+                           "A list of keywords about the movie");
+
+  GRL_TMDB_METADATA_KEY_PERFORMER =
+    register_metadata_key (registry,
+                           "tmdb-performer",
+                           "tmdb-performer",
+                           "A list of actors taking part in the movie");
+
+  GRL_TMDB_METADATA_KEY_PRODUCER =
+    register_metadata_key (registry,
+                           "tmdb-producer",
+                           "tmdb-producer",
+                           "A list of producers of the movie");
+
+  GRL_TMDB_METADATA_KEY_DIRECTOR =
+    register_metadata_key (registry,
+                           "tmdb-director",
+                           "tmdb-director",
+                           "Director of the movie");
+
+  GRL_TMDB_METADATA_KEY_AGE_CERTIFICATES =
+    register_metadata_key (registry,
+                           "tmdb-age-certificates",
+                           "tmdb-age-certificates",
+                           "Semicolon-separated list of all age certificates");
+
+  GRL_TMDB_METADATA_KEY_ORIGINAL_TITLE =
+    register_metadata_key (registry,
+                           "tmdb-original-title",
+                           "tmdb-original-title",
+                           "Original title of a movie");
+
+  GrlTmdbSource *source = grl_tmdb_source_new (api_key);
+  grl_registry_register_source (registry,
+                                       plugin,
+                                       GRL_SOURCE (source),
+                                       NULL);
+  g_free (api_key);
+  return TRUE;
+}
+
+GRL_PLUGIN_REGISTER (grl_tmdb_source_plugin_init,
+                     NULL,
+                     PLUGIN_ID);
+
+/* ================== GrlTmdbMetadata GObject ================ */
+
+static GrlTmdbSource *
+grl_tmdb_source_new (const char *api_key)
+{
+  GRL_DEBUG ("grl_tmdb_source_new");
+  return g_object_new (GRL_TMDB_SOURCE_TYPE,
+                       "source-id", SOURCE_ID,
+                       "source-name", SOURCE_NAME,
+                       "source-desc", SOURCE_DESC,
+                       "api-key", api_key,
+                       NULL);
+}
+
+static void
+grl_tmdb_source_class_init (GrlTmdbSourceClass * klass)
+{
+  GObjectClass *gobject_class = G_OBJECT_CLASS (klass);
+  GrlSourceClass *metadata_class = GRL_SOURCE_CLASS (klass);
+
+  metadata_class->supported_keys = grl_tmdb_source_supported_keys;
+  metadata_class->slow_keys = grl_tmdb_source_slow_keys;
+  metadata_class->may_resolve = grl_tmdb_source_may_resolve;
+  metadata_class->resolve = grl_tmdb_source_resolve;
+
+  g_type_class_add_private (klass, sizeof (GrlTmdbSourcePrivate));
+
+  gobject_class->set_property = grl_tmdb_source_set_property;
+  gobject_class->finalize = grl_tmdb_source_finalize;
+
+  g_object_class_install_property (gobject_class,
+                                   PROP_API_KEY,
+                                   g_param_spec_string ("api-key",
+                                                        "api-key",
+                                                        "TMDb API key",
+                                                        NULL,
+                                                        G_PARAM_WRITABLE
+                                                        | G_PARAM_CONSTRUCT_ONLY
+                                                        | G_PARAM_STATIC_STRINGS));
+}
+
+static void
+grl_tmdb_source_init (GrlTmdbSource *self)
+{
+  self->priv = G_TYPE_INSTANCE_GET_PRIVATE (self,
+                                            GRL_TMDB_SOURCE_TYPE,
+                                            GrlTmdbSourcePrivate);
+  self->priv->supported_keys = g_hash_table_new (g_direct_hash, g_direct_equal);
+  self->priv->slow_keys = g_hash_table_new (g_direct_hash, g_direct_equal);
+
+  /* Fast keys */
+  g_hash_table_add (self->priv->supported_keys,
+                    GINT_TO_POINTER (GRL_TMDB_METADATA_KEY_BACKDROPS));
+  g_hash_table_add (self->priv->supported_keys,
+                    GINT_TO_POINTER (GRL_TMDB_METADATA_KEY_ORIGINAL_TITLE));
+  g_hash_table_add (self->priv->supported_keys,
+                    GINT_TO_POINTER (GRL_METADATA_KEY_RATING));
+  g_hash_table_add (self->priv->supported_keys,
+                    GINT_TO_POINTER (GRL_TMDB_METADATA_KEY_POSTERS));
+  g_hash_table_add (self->priv->supported_keys,
+                    GINT_TO_POINTER (GRL_METADATA_KEY_PUBLICATION_DATE));
+
+  /* Slow keys */
+  g_hash_table_add (self->priv->slow_keys,
+                    GINT_TO_POINTER (GRL_METADATA_KEY_SITE));
+  g_hash_table_add (self->priv->slow_keys,
+                    GINT_TO_POINTER (GRL_METADATA_KEY_GENRE));
+  g_hash_table_add (self->priv->slow_keys,
+                    GINT_TO_POINTER (GRL_METADATA_KEY_STUDIO));
+  g_hash_table_add (self->priv->slow_keys,
+                    GINT_TO_POINTER (GRL_METADATA_KEY_DESCRIPTION));
+  g_hash_table_add (self->priv->slow_keys,
+                    GINT_TO_POINTER (GRL_METADATA_KEY_CERTIFICATE));
+  g_hash_table_add (self->priv->slow_keys,
+                    GINT_TO_POINTER (GRL_TMDB_METADATA_KEY_IMDB_ID));
+  g_hash_table_add (self->priv->slow_keys,
+                    GINT_TO_POINTER (GRL_TMDB_METADATA_KEY_KEYWORDS));
+  g_hash_table_add (self->priv->slow_keys,
+                    GINT_TO_POINTER (GRL_TMDB_METADATA_KEY_PERFORMER));
+  g_hash_table_add (self->priv->slow_keys,
+                    GINT_TO_POINTER (GRL_TMDB_METADATA_KEY_PRODUCER));
+  g_hash_table_add (self->priv->slow_keys,
+                    GINT_TO_POINTER (GRL_TMDB_METADATA_KEY_DIRECTOR));
+  g_hash_table_add (self->priv->slow_keys,
+                    GINT_TO_POINTER (GRL_TMDB_METADATA_KEY_AGE_CERTIFICATES));
+
+  self->priv->wc = grl_net_wc_new ();
+  grl_net_wc_set_throttling (self->priv->wc, 1);
+
+  self->priv->config_pending = FALSE;
+  self->priv->pending_resolves = g_queue_new ();
+}
+
+G_DEFINE_TYPE (GrlTmdbSource,
+               grl_tmdb_source,
+               GRL_TYPE_SOURCE);
+
+/* GObject vfuncs */
+static void
+grl_tmdb_source_set_property (GObject *object,
+                              guint property_id,
+                              const GValue *value,
+                              GParamSpec *pspec)
+{
+  GrlTmdbSource *self = GRL_TMDB_SOURCE (object);
+
+  switch (property_id) {
+    case PROP_API_KEY:
+      self->priv->api_key = g_value_dup_string (value);
+      GRL_DEBUG ("Using API key %s", self->priv->api_key);
+      break;
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
+  }
+}
+
+static void
+grl_tmdb_source_finalize (GObject *object)
+{
+  GrlTmdbSource *self = GRL_TMDB_SOURCE (object);
+
+  if (self->priv->supported_keys != NULL) {
+    g_hash_table_unref (self->priv->supported_keys);
+    self->priv->supported_keys = NULL;
+  }
+
+  if (self->priv->slow_keys != NULL) {
+    g_hash_table_unref (self->priv->slow_keys);
+    self->priv->slow_keys = NULL;
+  }
+
+  if (self->priv->api_key != NULL) {
+    g_free (self->priv->api_key);
+    self->priv->api_key = NULL;
+  }
+
+  if (self->priv->image_base_uri != NULL) {
+    soup_uri_free (self->priv->image_base_uri);
+    self->priv->image_base_uri = NULL;
+  }
+
+  if (self->priv->configuration != NULL) {
+    g_object_unref (self->priv->configuration);
+    self->priv->configuration = NULL;
+  }
+
+  if (self->priv->wc != NULL) {
+    g_object_unref (self->priv->wc);
+    self->priv->wc = NULL;
+  }
+
+  if (self->priv->pending_resolves != NULL) {
+      g_queue_free_full (self->priv->pending_resolves,
+                         (GDestroyNotify) resolve_closure_free);
+      self->priv->pending_resolves = NULL;
+  }
+
+  G_OBJECT_CLASS (grl_tmdb_source_parent_class)->finalize (object);
+}
+
+/* Private functions */
+
+static GrlKeyID
+register_metadata_key (GrlRegistry *registry,
+                       const char *name,
+                       const char *nick,
+                       const char *blurb)
+{
+  GParamSpec *spec;
+  GrlKeyID key;
+
+  spec = g_param_spec_string (name,
+                              nick,
+                              blurb,
+                              NULL,
+                              G_PARAM_READWRITE
+                              | G_PARAM_STATIC_STRINGS);
+
+  key = grl_registry_register_metadata_key (registry, spec, NULL);
+  g_param_spec_unref (spec);
+
+  if (key == GRL_METADATA_KEY_INVALID) {
+    key = grl_registry_lookup_metadata_key (registry, name);
+    if (grl_metadata_key_get_type (key) != G_TYPE_STRING) {
+      key = GRL_METADATA_KEY_INVALID;
+    }
+  }
+
+  return key;
+}
+
+static void
+resolve_closure_callback (ResolveClosure *closure,
+                          GError *outer_error)
+{
+  GError *error = NULL;
+
+  if (outer_error && outer_error->domain != GRL_CORE_ERROR) {
+    error = g_error_new_literal (GRL_CORE_ERROR,
+                                 GRL_CORE_ERROR_RESOLVE_FAILED,
+                                 error->message);
+    g_error_free (outer_error);
+  }
+
+  closure->rs->callback (GRL_SOURCE (closure->self),
+                         closure->rs->operation_id,
+                         closure->rs->media,
+                         closure->rs->user_data,
+                         error);
+  if (error)
+    g_error_free (error);
+}
+
+static void
+resolve_closure_free (ResolveClosure *closure)
+{
+  g_object_unref (closure->self);
+  g_queue_free_full (closure->pending_requests, g_object_unref);
+  g_hash_table_destroy (closure->keys);
+  g_slice_free (ResolveClosure, closure);
+}
+
+static char *
+producer_filter (JsonNode *element)
+{
+  JsonObject *object;
+  const char *department;
+
+  if (!JSON_NODE_HOLDS_OBJECT (element)) {
+    return NULL;
+  }
+
+  object = json_node_get_object (element);
+
+  department = json_object_get_string_member (object, "department");
+  if (g_ascii_strcasecmp (department, "Production") != 0) {
+    return NULL;
+  }
+
+  return g_strdup (json_object_get_string_member (object, "name"));
+}
+
+static char *
+director_filter (JsonNode *element)
+{
+  JsonObject *object;
+  const char *department;
+
+  if (!JSON_NODE_HOLDS_OBJECT (element)) {
+    return NULL;
+  }
+
+  object = json_node_get_object (element);
+
+  department = json_object_get_string_member (object, "department");
+  if (g_ascii_strcasecmp (department, "Directing") != 0) {
+    return NULL;
+  }
+
+  return g_strdup (json_object_get_string_member (object, "name"));
+}
+
+static char *
+us_release_filter (JsonNode *node)
+{
+  JsonObject *object;
+  const char *country;
+
+  if (!JSON_NODE_HOLDS_OBJECT (node)) {
+    return NULL;
+  }
+
+  object = json_node_get_object (node);
+  country = json_object_get_string_member (object, "iso_3166_1");
+  if (g_ascii_strcasecmp (country, "US") == 0) {
+    return g_strdup (json_object_get_string_member (object, "certification"));
+  }
+
+  return NULL;
+}
+
+static char *
+all_releases_filter (JsonNode *node)
+{
+  JsonObject *object;
+  const char *country, *cert;
+
+  if (!JSON_NODE_HOLDS_OBJECT (node)) {
+    return NULL;
+  }
+
+  object = json_node_get_object (node);
+  country = json_object_get_string_member (object, "iso_3166_1");
+  cert = json_object_get_string_member (object, "certification");
+  if (cert == NULL || strlen (cert) == 0) {
+    /* This is only a release date, no age cert */
+    return NULL;
+  }
+  return g_strconcat (country, ":", cert, NULL);
+}
+
+static char *
+neutral_backdrop_filter (JsonNode *node)
+{
+  JsonObject *object;
+  const char *language;
+
+  if (!JSON_NODE_HOLDS_OBJECT (node)) {
+    return NULL;
+  }
+
+  object = json_node_get_object (node);
+  language = json_object_get_string_member (object, "iso_639_1");
+
+  /* Language-neutral backdrops only */
+  if (language == NULL) {
+    return g_strdup (json_object_get_string_member (object, "file_path"));
+  }
+
+  return NULL;
+}
+
+static void on_request_ready (GObject *source,
+                              GAsyncResult *result,
+                              gpointer user_data) {
+  ResolveClosure *closure = (ResolveClosure *) user_data;
+  GrlTmdbRequest *request = GRL_TMDB_REQUEST (source);
+  GError *error = NULL;
+  GList *values, *iter;
+  GValue *value;
+
+  GRL_DEBUG ("Detail request ready...");
+  if (!grl_tmdb_request_run_finish (GRL_TMDB_REQUEST (source),
+                                    result,
+                                    &error)) {
+    /* Just remove the request and hope the others have some data */
+    GRL_WARNING ("Failed to get %s: %s",
+                 grl_tmdb_request_get_uri (request),
+                 error->message);
+    goto out;
+  }
+
+  switch (grl_tmdb_request_get_detail (request)) {
+    case GRL_TMDB_REQUEST_DETAIL_MOVIE:
+    {
+      if (SHOULD_RESOLVE (GRL_METADATA_KEY_GENRE)) {
+        iter = values = grl_tmdb_request_get_string_list (request, "$.genres..name");
+        while (iter != NULL) {
+          grl_data_add_string (GRL_DATA (closure->rs->media),
+                               GRL_METADATA_KEY_GENRE,
+                               (char *) iter->data);
+          iter = iter->next;
+        }
+        g_list_free_full (values, g_free);
+      }
+
+      if (SHOULD_RESOLVE (GRL_METADATA_KEY_STUDIO)) {
+        iter = values = grl_tmdb_request_get_string_list (request, "$.production_companies..name");
+        while (iter != NULL) {
+          grl_data_add_string (GRL_DATA (closure->rs->media),
+                               GRL_METADATA_KEY_STUDIO,
+                               (char *) iter->data);
+          iter = iter->next;
+        }
+        g_list_free_full (values, g_free);
+      }
+
+      if (SHOULD_RESOLVE (GRL_METADATA_KEY_SITE)) {
+        value = grl_tmdb_request_get (request, "$.homepage");
+        if (value != NULL) {
+          grl_media_set_site (closure->rs->media, g_value_get_string (value));
+          g_value_unset (value);
+        }
+      }
+
+      if (SHOULD_RESOLVE (GRL_METADATA_KEY_DESCRIPTION)) {
+        value = grl_tmdb_request_get (request, "$.overview");
+        if (value != NULL) {
+          grl_media_set_description (closure->rs->media,
+                                     g_value_get_string (value));
+          g_value_unset (value);
+        }
+      }
+
+      if (SHOULD_RESOLVE (GRL_TMDB_METADATA_KEY_IMDB_ID)) {
+        value = grl_tmdb_request_get (request, "$.imdb_id");
+        if (value != NULL) {
+          grl_data_set_string (GRL_DATA (closure->rs->media),
+                               GRL_TMDB_METADATA_KEY_IMDB_ID,
+                               g_value_get_string (value));
+          g_value_unset (value);
+        }
+      }
+    }
+    break;
+    case GRL_TMDB_REQUEST_DETAIL_MOVIE_IMAGES:
+    {
+      if (SHOULD_RESOLVE (GRL_TMDB_METADATA_KEY_BACKDROPS)) {
+        iter = values = grl_tmdb_request_get_string_list_with_filter (request,
+                                                                      "$.backdrops",
+                                                                      neutral_backdrop_filter);
+        while (iter != NULL) {
+          SoupURI *backdrop_uri;
+          char *path;
+
+          path = g_strconcat ("original", (char *) iter->data, NULL);
+
+          backdrop_uri = soup_uri_new_with_base (closure->self->priv->image_base_uri,
+                                                 path);
+          g_free (path);
+          path = soup_uri_to_string (backdrop_uri, FALSE);
+
+          grl_data_add_string (GRL_DATA (closure->rs->media),
+                               GRL_TMDB_METADATA_KEY_BACKDROPS,
+                               path);
+          g_free (path);
+
+          iter = iter->next;
+        }
+        g_list_free_full (values, g_free);
+      }
+
+      if (SHOULD_RESOLVE (GRL_TMDB_METADATA_KEY_POSTERS)) {
+        iter = values = grl_tmdb_request_get_string_list_with_filter (request,
+                                                                      "$.posters",
+                                                                      neutral_backdrop_filter);
+        while (iter != NULL) {
+          SoupURI *backdrop_uri;
+          char *path;
+
+          path = g_strconcat ("original", (char *) iter->data, NULL);
+
+          backdrop_uri = soup_uri_new_with_base (closure->self->priv->image_base_uri,
+                                                 path);
+          g_free (path);
+          path = soup_uri_to_string (backdrop_uri, FALSE);
+
+          grl_data_add_string (GRL_DATA (closure->rs->media),
+                               GRL_TMDB_METADATA_KEY_POSTERS,
+                               path);
+          g_free (path);
+
+          iter = iter->next;
+        }
+        g_list_free_full (values, g_free);
+      }
+    }
+    break;
+    case GRL_TMDB_REQUEST_DETAIL_MOVIE_KEYWORDS:
+    {
+      if (SHOULD_RESOLVE (GRL_TMDB_METADATA_KEY_KEYWORDS)) {
+        iter = values = grl_tmdb_request_get_string_list (request,
+                                                          "$.keywords..name");
+        while (iter != NULL) {
+          grl_data_add_string (GRL_DATA (closure->rs->media),
+                               GRL_TMDB_METADATA_KEY_KEYWORDS,
+                               (char *) iter->data);
+          iter = iter->next;
+        }
+        g_list_free_full (values, g_free);
+      }
+    }
+    break;
+    case GRL_TMDB_REQUEST_DETAIL_MOVIE_CAST:
+    {
+      if (SHOULD_RESOLVE (GRL_TMDB_METADATA_KEY_PERFORMER)) {
+        values = grl_tmdb_request_get_string_list (request, "$.cast..name");
+        iter = values;
+        while (iter != NULL) {
+          grl_data_add_string (GRL_DATA (closure->rs->media),
+                               GRL_TMDB_METADATA_KEY_PERFORMER,
+                               (char *) iter->data);
+          iter = iter->next;
+        }
+        g_list_free_full (values, g_free);
+      }
+
+      if (SHOULD_RESOLVE (GRL_TMDB_METADATA_KEY_PRODUCER)) {
+        values = grl_tmdb_request_get_string_list_with_filter (request,
+                                                               "$.crew[*]",
+                                                               producer_filter);
+        iter = values;
+        while (iter != NULL) {
+          grl_data_add_string (GRL_DATA (closure->rs->media),
+                               GRL_TMDB_METADATA_KEY_PRODUCER,
+                               (char *) iter->data);
+          iter = iter->next;
+        }
+        g_list_free_full (values, g_free);
+      }
+
+      if (SHOULD_RESOLVE (GRL_TMDB_METADATA_KEY_DIRECTOR)) {
+        values = grl_tmdb_request_get_string_list_with_filter (request,
+                                                               "$.crew[*]",
+                                                               director_filter);
+        iter = values;
+        while (iter != NULL) {
+          grl_data_add_string (GRL_DATA (closure->rs->media),
+                               GRL_TMDB_METADATA_KEY_DIRECTOR,
+                               (char *) iter->data);
+          iter = iter->next;
+        }
+        g_list_free_full (values, g_free);
+      }
+    }
+    break;
+    case GRL_TMDB_REQUEST_DETAIL_MOVIE_RELEASE_INFO:
+    {
+      GString *string;
+
+      if (SHOULD_RESOLVE (GRL_METADATA_KEY_CERTIFICATE)) {
+        values = grl_tmdb_request_get_string_list_with_filter (request,
+                                                               "$.countries[*]",
+                                                               us_release_filter);
+        if (values != NULL) {
+          grl_media_set_certificate (closure->rs->media,
+                                     (char *) values->data);
+          g_list_free_full (values, g_free);
+        }
+      }
+
+      if (SHOULD_RESOLVE (GRL_TMDB_METADATA_KEY_AGE_CERTIFICATES)) {
+        values = grl_tmdb_request_get_string_list_with_filter (request,
+                                                               "$.countries[*]",
+                                                               all_releases_filter);
+        iter = values;
+        string = g_string_new (NULL);
+        while (iter != NULL) {
+          g_string_append (string, (char *) iter->data);
+          iter = iter->next;
+          if (iter != NULL) {
+            g_string_append_c (string, ';');
+          }
+        }
+        if (string->str != NULL) {
+          grl_data_set_string (GRL_DATA (closure->rs->media),
+                               GRL_TMDB_METADATA_KEY_AGE_CERTIFICATES,
+                               string->str);
+        }
+        g_string_free (string, TRUE);
+      }
+    }
+    break;
+    default:
+      break;
+  }
+
+out:
+  if (error != NULL) {
+    g_error_free (error);
+  }
+  g_queue_remove (closure->pending_requests, request);
+  g_object_unref (request);
+
+  if (g_queue_is_empty (closure->pending_requests)) {
+    resolve_closure_callback (closure, NULL);
+    resolve_closure_free (closure);
+  }
+}
+
+static GrlTmdbRequest *
+create_and_run_request (GrlTmdbSource *self,
+                        ResolveClosure *closure,
+                        GrlTmdbRequestDetail id)
+{
+  GrlTmdbRequest *request;
+
+  request = grl_tmdb_request_new_details (self->priv->api_key,
+                                          id,
+                                          closure->id);
+  grl_tmdb_request_run_async (request,
+                              self->priv->wc,
+                              on_request_ready,
+                              NULL,
+                              closure);
+
+  return request;
+}
+
+static void
+on_search_ready (GObject *source,
+                 GAsyncResult *result,
+                 gpointer user_data)
+{
+  ResolveClosure *closure = (ResolveClosure *) user_data;
+  GrlTmdbRequest *request = GRL_TMDB_REQUEST (source);
+  GrlTmdbSource *self = closure->self;
+  GValue *value;
+  GError *error = NULL;
+  char *padded_date;
+
+  GRL_DEBUG ("Initial search ready...");
+  if (!grl_tmdb_request_run_finish (GRL_TMDB_REQUEST (source),
+                                    result,
+                                    &error)) {
+    resolve_closure_callback (closure, error);
+    resolve_closure_free (closure);
+    return;
+  }
+
+  value = grl_tmdb_request_get (request, "$.total_results");
+  if (g_value_get_int64 (value) == 0) {
+    /* Nothing found */
+    resolve_closure_callback (closure, NULL);
+    resolve_closure_free (closure);
+    return;
+  }
+
+  value = grl_tmdb_request_get (request, "$.results[0].id");
+  if (value == NULL) {
+    /* Cannot continue without id */
+    error = g_error_new_literal (GRL_CORE_ERROR,
+                                 GRL_CORE_ERROR_RESOLVE_FAILED,
+                                 "Remote data did not contain valid ID");
+    resolve_closure_callback (closure, error);
+    resolve_closure_free (closure);
+    return;
+  }
+  closure->id = g_value_get_int64 (value);
+  g_value_unset (value);
+
+  if (SHOULD_RESOLVE (GRL_METADATA_KEY_PUBLICATION_DATE)) {
+    value = grl_tmdb_request_get (request, "$.results[0].release_date");
+    if (value != NULL) {
+      GDateTime *pubdate;
+      GTimeVal tv;
+      padded_date = g_strconcat (g_value_get_string (value), "T00:00:00Z", NULL);
+      g_time_val_from_iso8601 (padded_date, &tv);
+      pubdate = g_date_time_new_from_timeval_utc (&tv);
+
+      grl_media_set_publication_date (closure->rs->media,
+                                      pubdate);
+      g_date_time_unref (pubdate);
+      g_free (padded_date);
+      g_value_unset (value);
+    }
+  }
+
+  if (SHOULD_RESOLVE (GRL_METADATA_KEY_RATING)) {
+    value = grl_tmdb_request_get (request, "$.results[0].vote_average");
+    if (value != NULL) {
+      grl_media_set_rating (closure->rs->media,
+                            (float) g_value_get_double (value),
+                            10.0f);
+      g_value_unset (value);
+    }
+  }
+
+  if (SHOULD_RESOLVE (GRL_TMDB_METADATA_KEY_BACKDROPS)) {
+    value = grl_tmdb_request_get (request, "$.results[0].backdrop_path");
+    if (value != NULL) {
+      SoupURI *backdrop_uri;
+      char *path;
+
+      path = g_strconcat ("original", g_value_get_string (value), NULL);
+
+      backdrop_uri = soup_uri_new_with_base (closure->self->priv->image_base_uri,
+                                             path);
+      g_free (path);
+      path = soup_uri_to_string (backdrop_uri, FALSE);
+
+      grl_data_add_string (GRL_DATA (closure->rs->media),
+                           GRL_TMDB_METADATA_KEY_BACKDROPS,
+                           path);
+      g_free (path);
+      g_value_unset (value);
+    }
+  }
+
+  if (SHOULD_RESOLVE (GRL_TMDB_METADATA_KEY_POSTERS)) {
+    value = grl_tmdb_request_get (request, "$.results[0].poster_path");
+    if (value != NULL) {
+      SoupURI *backdrop_uri;
+      char *path;
+
+      path = g_strconcat ("original", g_value_get_string (value), NULL);
+
+      backdrop_uri = soup_uri_new_with_base (closure->self->priv->image_base_uri,
+                                             path);
+      g_free (path);
+      path = soup_uri_to_string (backdrop_uri, FALSE);
+
+      grl_data_add_string (GRL_DATA (closure->rs->media),
+                           GRL_TMDB_METADATA_KEY_POSTERS,
+                           path);
+      g_free (path);
+    }
+  }
+
+  if (SHOULD_RESOLVE (GRL_TMDB_METADATA_KEY_ORIGINAL_TITLE)) {
+    value = grl_tmdb_request_get (request, "$.results[0].original_title");
+    if (value != NULL) {
+      grl_data_set_string (GRL_DATA (closure->rs->media),
+                           GRL_TMDB_METADATA_KEY_ORIGINAL_TITLE,
+                           g_value_get_string (value));
+      g_value_unset (value);
+    }
+  }
+
+  g_queue_pop_head (closure->pending_requests);
+  g_object_unref (source);
+
+  /* No need to do additional requests */
+  if (!closure->slow) {
+    resolve_closure_callback (closure, NULL);
+    resolve_closure_free (closure);
+    return;
+  }
+
+  /* We need to do additional requests. Check if we should have resolved
+   * images and try to get some more */
+  if (SHOULD_RESOLVE (GRL_TMDB_METADATA_KEY_BACKDROPS) ||
+      SHOULD_RESOLVE (GRL_TMDB_METADATA_KEY_POSTERS))
+    g_queue_push_tail (closure->pending_requests,
+                       create_and_run_request (self,
+                                               closure,
+                                               GRL_TMDB_REQUEST_DETAIL_MOVIE_IMAGES));
+
+  if (SHOULD_RESOLVE (GRL_METADATA_KEY_GENRE) ||
+      SHOULD_RESOLVE (GRL_METADATA_KEY_STUDIO) ||
+      SHOULD_RESOLVE (GRL_METADATA_KEY_SITE) ||
+      SHOULD_RESOLVE (GRL_METADATA_KEY_DESCRIPTION) ||
+      SHOULD_RESOLVE (GRL_TMDB_METADATA_KEY_IMDB_ID))
+    g_queue_push_tail (closure->pending_requests,
+                       create_and_run_request (self,
+                                               closure,
+                                               GRL_TMDB_REQUEST_DETAIL_MOVIE));
+
+  if (SHOULD_RESOLVE (GRL_TMDB_METADATA_KEY_KEYWORDS))
+    g_queue_push_tail (closure->pending_requests,
+                       create_and_run_request (self,
+                                               closure,
+                                               GRL_TMDB_REQUEST_DETAIL_MOVIE_KEYWORDS));
+
+  if (SHOULD_RESOLVE (GRL_TMDB_METADATA_KEY_PERFORMER) ||
+      SHOULD_RESOLVE (GRL_TMDB_METADATA_KEY_PRODUCER) ||
+      SHOULD_RESOLVE (GRL_TMDB_METADATA_KEY_DIRECTOR))
+    g_queue_push_tail (closure->pending_requests,
+                       create_and_run_request (self,
+                                               closure,
+                                               GRL_TMDB_REQUEST_DETAIL_MOVIE_CAST));
+
+  if (SHOULD_RESOLVE (GRL_METADATA_KEY_CERTIFICATE) ||
+      SHOULD_RESOLVE (GRL_TMDB_METADATA_KEY_AGE_CERTIFICATES))
+    g_queue_push_tail (closure->pending_requests,
+                       create_and_run_request (self,
+                                               closure,
+                                               GRL_TMDB_REQUEST_DETAIL_MOVIE_RELEASE_INFO));
+}
+
+static void
+on_configuration_ready (GObject *source,
+                        GAsyncResult *result,
+                        gpointer user_data)
+{
+  ResolveClosure *closure = (ResolveClosure *) user_data;
+  GrlTmdbRequest *request = GRL_TMDB_REQUEST (source);
+  GrlTmdbSource *self = closure->self;
+  GError *error = NULL;
+  GValue *value;
+
+  if (!grl_tmdb_request_run_finish (GRL_TMDB_REQUEST (source),
+                                    result,
+                                    &error)) {
+    resolve_closure_callback (closure, error);
+    resolve_closure_free (closure);
+
+    /* Notify pending requests about failure */
+    while (!g_queue_is_empty (self->priv->pending_resolves)) {
+      ResolveClosure *pending_closure;
+
+      pending_closure = g_queue_pop_head (self->priv->pending_resolves);
+
+      resolve_closure_callback (pending_closure, error);
+      resolve_closure_free (pending_closure);
+    }
+    return;
+  }
+
+  self->priv->configuration = g_queue_pop_head (closure->pending_requests);
+
+  value = grl_tmdb_request_get (request, "$.images.base_url");
+  if (value != NULL) {
+    GRL_DEBUG ("Got TMDb configuration.");
+    self->priv->image_base_uri = soup_uri_new (g_value_get_string (value));
+  }
+
+  g_queue_push_head (self->priv->pending_resolves, closure);
+
+  /* Flush queue. GrlNetWc will take care of throttling */
+  while (!g_queue_is_empty (self->priv->pending_resolves)) {
+    ResolveClosure *pending_closure;
+
+    pending_closure = g_queue_pop_head (self->priv->pending_resolves);
+    grl_tmdb_request_run_async (g_queue_peek_head (pending_closure->pending_requests),
+                                self->priv->wc,
+                                on_search_ready,
+                                NULL,
+                                pending_closure);
+  }
+}
+
+/* ================== API Implementation ================ */
+
+/* GrlSource vfuncs */
+static const GList *
+grl_tmdb_source_supported_keys (GrlSource *source)
+{
+  GrlTmdbSource *self = GRL_TMDB_SOURCE (source);
+  static GList *supported_keys = NULL;
+
+  if (supported_keys == NULL) {
+    const GList *it;
+
+    supported_keys = g_hash_table_get_keys (self->priv->supported_keys);
+    it = grl_tmdb_source_slow_keys (source);
+    while (it) {
+      supported_keys = g_list_prepend (supported_keys, it->data);
+      it = it->next;
+    }
+  }
+
+  return supported_keys;
+}
+
+static const GList *
+grl_tmdb_source_slow_keys (GrlSource *source)
+{
+  GrlTmdbSource *self = GRL_TMDB_SOURCE (source);
+  static GList *slow_keys = NULL;
+
+  if (slow_keys == NULL) {
+    slow_keys = g_hash_table_get_keys (self->priv->slow_keys);
+  }
+
+  return slow_keys;
+}
+
+static gboolean
+grl_tmdb_source_may_resolve (GrlSource *source,
+                             GrlMedia *media,
+                             GrlKeyID key_id,
+                             GList **missing_keys)
+{
+  GrlTmdbSource *self = GRL_TMDB_SOURCE (source);
+
+  if (!g_hash_table_contains (self->priv->supported_keys,
+                             GINT_TO_POINTER (key_id)) &&
+      !g_hash_table_contains (self->priv->slow_keys,
+                             GINT_TO_POINTER (key_id)))
+    return FALSE;
+
+  /* We can only entertain videos */
+  if (media && !GRL_IS_MEDIA_VIDEO (media))
+    return FALSE;
+
+  /* Caller wants to check what's needed to resolve */
+  if (!media) {
+    if (!missing_keys)
+      *missing_keys = grl_metadata_key_list_new (GRL_METADATA_KEY_TITLE, NULL);
+
+    return FALSE;
+  }
+
+  /* We can do nothing without a title */
+  if (!grl_data_has_key (GRL_DATA (media), GRL_METADATA_KEY_TITLE)) {
+    if (!missing_keys)
+      *missing_keys = grl_metadata_key_list_new (GRL_METADATA_KEY_TITLE, NULL);
+
+    return FALSE;
+  }
+
+  return TRUE;
+}
+
+static void
+grl_tmdb_source_resolve (GrlSource *source,
+                                  GrlSourceResolveSpec *rs)
+{
+  ResolveClosure *closure;
+  GrlTmdbRequest *request;
+  GAsyncReadyCallback callback;
+  const char *title;
+  GrlTmdbSource *self = GRL_TMDB_SOURCE (source);
+  GList *it;
+
+  if (!GRL_IS_MEDIA_VIDEO (rs->media)) {
+    /* We only entertain videos */
+    rs->callback (source, rs->operation_id, rs->media, rs->user_data, NULL);
+    return;
+  }
+
+  title = grl_media_get_title (rs->media);
+  if (title == NULL) {
+    /* Can't search for anything without a title ... */
+    rs->callback (source, rs->operation_id, rs->media, rs->user_data, NULL);
+    return;
+  }
+
+  GRL_DEBUG ("grl_tmdb_source_resolve");
+
+  closure = g_slice_new0 (ResolveClosure);
+  closure->self = g_object_ref (source);
+  closure->rs = rs;
+  closure->pending_requests = g_queue_new ();
+  closure->keys = g_hash_table_new (g_direct_hash, g_direct_equal);
+  closure->slow = FALSE;
+  request = grl_tmdb_request_new_search (closure->self->priv->api_key, title);
+  g_queue_push_tail (closure->pending_requests, request);
+  it = rs->keys;
+
+  /* Copy keys to list for faster lookup */
+  while (it) {
+    g_hash_table_add (closure->keys, it->data);
+    closure->slow |= g_hash_table_contains (self->priv->slow_keys,
+                                            it->data);
+    it = it->next;
+  }
+
+  if (grl_operation_options_get_flags (rs->options) & GRL_RESOLVE_FAST_ONLY)
+    closure->slow = FALSE;
+
+  /* We did not receive the config yet, queue request. Config callback will
+   * take care of flushing the queue when ready.
+   */
+  if (self->priv->configuration == NULL && self->priv->config_pending) {
+    g_queue_push_tail (self->priv->pending_resolves, closure);
+    return;
+  }
+
+  if (self->priv->configuration == NULL) {
+    GRL_DEBUG ("Fetching TMDb configuration...");
+    /* We need to fetch TMDb's configuration for the image paths */
+    request = grl_tmdb_request_new_configuration (closure->self->priv->api_key);
+    g_queue_push_head (closure->pending_requests, request);
+    callback = on_configuration_ready;
+    self->priv->config_pending = TRUE;
+  } else {
+    GRL_DEBUG ("Running initial search...");
+    callback = on_search_ready;
+  }
+
+  grl_tmdb_request_run_async (g_queue_peek_head (closure->pending_requests),
+                              closure->self->priv->wc,
+                              callback,
+                              NULL,
+                              closure);
+}
diff --git a/src/tmdb/grl-tmdb.h b/src/tmdb/grl-tmdb.h
new file mode 100644
index 0000000..2b36a0f
--- /dev/null
+++ b/src/tmdb/grl-tmdb.h
@@ -0,0 +1,74 @@
+/*
+ * Copyright (C) 2012 Canonical Ltd.
+ *
+ * Author: Jens Georg <jensg openismus com>
+ *
+ * This library 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; version 2.1 of
+ * the License, or (at your option) any later version.
+ *
+ * This library 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 Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
+ * 02110-1301 USA
+ *
+ */
+
+#ifndef _GRL_TMDB_SOURCE_H_
+#define _GRL_TMDB_SOURCE_H_
+
+#include <grilo.h>
+
+#define GRL_TMDB_SOURCE_TYPE           \
+  (grl_tmdb_source_get_type ())
+
+#define GRL_TMDB_SOURCE(obj)                           \
+  (G_TYPE_CHECK_INSTANCE_CAST ((obj),                           \
+                               GRL_TMDB_SOURCE_TYPE,   \
+                               GrlTmdbSource))
+
+#define GRL_IS_TMDB_SOURCE(obj)                        \
+  (G_TYPE_CHECK_INSTANCE_TYPE ((obj),                           \
+                               GRL_TMDB_SOURCE_TYPE))
+
+#define GRL_TMDB_SOURCE_CLASS(klass)                   \
+  (G_TYPE_CHECK_CLASS_CAST((klass),                             \
+                           GRL_TMDB_SOURCE_TYPE,       \
+                           GrlTmdbSourceClass))
+
+#define GRL_IS_TMDB_SOURCE_CLASS(klass)                \
+  (G_TYPE_CHECK_CLASS_TYPE((klass),                             \
+                           GRL_TMDB_SOURCE_TYPE))
+
+#define GRL_TMDB_SOURCE_GET_CLASS(obj)                 \
+  (G_TYPE_INSTANCE_GET_CLASS ((obj),                            \
+                              GRL_TMDB_SOURCE_TYPE,    \
+                              GrlTmdbSourceClass))
+
+typedef struct _GrlTmdbSource GrlTmdbSource;
+typedef struct _GrlTmdbSourcePrivate GrlTmdbSourcePrivate;
+
+struct _GrlTmdbSource {
+
+  GrlSource parent;
+
+  GrlTmdbSourcePrivate *priv;
+};
+
+typedef struct _GrlTmdbSourceClass GrlTmdbSourceClass;
+
+struct _GrlTmdbSourceClass {
+
+  GrlSourceClass parent_class;
+
+};
+
+GType grl_tmdb_source_get_type (void);
+
+#endif /* _GRL_TMDB_SOURCE_H_ */
diff --git a/src/tmdb/grl-tmdb.xml b/src/tmdb/grl-tmdb.xml
new file mode 100644
index 0000000..4031bc0
--- /dev/null
+++ b/src/tmdb/grl-tmdb.xml
@@ -0,0 +1,10 @@
+<plugin>
+  <info>
+    <name>TMDb Metadata Provider</name>
+    <module>libgrltmdb</module>
+    <description>A plugin to retreive movie metadata from themoviedb.org</description>
+    <author>Canonical Ltd.</author>
+    <license>LGPL</license>
+    <site>http://www.canonical.com</site>
+  </info>
+</plugin>



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