[grilo-plugins] thetvdb: Include The TVDB source



commit b2b937413a26a0fe2ad18891a27a30a35b12b26b
Author: Victor Toso <me victortoso com>
Date:   Sat May 24 20:08:48 2014 -0300

    thetvdb: Include The TVDB source
    
    https://bugzilla.gnome.org/show_bug.cgi?id=672933

 Makefile.am                              |    1 +
 configure.ac                             |   51 +
 src/Makefile.am                          |    6 +-
 src/thetvdb/Makefile.am                  |   48 +
 src/thetvdb/grl-thetvdb.c                | 1594 ++++++++++++++++++++++++++++++
 src/thetvdb/grl-thetvdb.h                |   66 ++
 src/thetvdb/grl-thetvdb.xml              |   10 +
 src/thetvdb/thetvdb-resources-episodes.c |  349 +++++++
 src/thetvdb/thetvdb-resources-series.c   |  338 +++++++
 src/thetvdb/thetvdb-resources.h          |  155 +++
 10 files changed, 2617 insertions(+), 1 deletions(-)
---
diff --git a/Makefile.am b/Makefile.am
index 813aeac..1a36173 100644
--- a/Makefile.am
+++ b/Makefile.am
@@ -53,6 +53,7 @@ DISTCHECK_CONFIGURE_FLAGS = --enable-apple-trailers \
                             --enable-optical-media \
                             --enable-pocket \
                             --enable-podcasts \
+                            --enable-thetvdb \
                             --enable-tmdb \
                             --enable-tracker \
                             --enable-upnp \
diff --git a/configure.ac b/configure.ac
index b5ef16b..c84cdcf 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1273,6 +1273,55 @@ DEPS_DMAP_LIBS="$DEPS_LIBS $DMAP_LIBS $XML_LIBS"
 AC_SUBST(DEPS_DMAP_LIBS)
 
 # ----------------------------------------------------------
+# BUILD THETVDB PLUGIN
+# ----------------------------------------------------------
+
+AC_ARG_ENABLE(thetvdb,
+        AC_HELP_STRING([--enable-thetvdb],
+                [enable thetvdb 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-thetvdb])
+                        fi
+                        if test "x$HAVE_XML" = "xno"; then
+                           AC_MSG_ERROR([libxml-2.0 not found, install it or use --disable-thetvdb])
+                        fi
+                        if test "x$HAVE_ARCHIVE" = "xno"; then
+                           AC_MSG_ERROR([libarchive not found, install it or use --disable-thetvdb])
+                        fi
+                        if test "x$HAVE_GOM" = "xno"; then
+                           AC_MSG_ERROR([gom-1.0 not found, install it or use --disable-thetvdb])
+                        fi
+                        ;;
+                esac
+        ],
+        [
+                if test "x$HAVE_GRLNET" = "xyes" -a "x$HAVE_XML" = "xyes" -a "x$HAVE_ARCHIVE" = "xyes" -a 
"x$HAVE_GOM" = "xyes"; then
+                  enable_thetvdb=yes
+                else
+                  enable_thetvdb=no
+                fi
+        ])
+
+AM_CONDITIONAL([THETVDB_PLUGIN], [test "x$enable_thetvdb" = "xyes"])
+GRL_PLUGINS_ALL="$GRL_PLUGINS_ALL thetvdb"
+if test "x$enable_thetvdb" = "xyes"
+then
+       GRL_PLUGINS_ENABLED="$GRL_PLUGINS_ENABLED thetvdb"
+fi
+
+THETVDB_PLUGIN_ID="grl-thetvdb"
+AC_SUBST(THETVDB_PLUGIN_ID)
+AC_DEFINE_UNQUOTED([THETVDB_PLUGIN_ID], ["$THETVDB_PLUGIN_ID"], [thetvdb plugin ID])
+
+DEPS_THETVDB_CFLAGS="$DEPS_CFLAGS $ARCHIVE_CFLAGS $GRLNET_CFLAGS $XML_CFLAGS $GOM_CFLAGS"
+AC_SUBST(DEPS_THETVDB_CFLAGS)
+DEPS_THETVDB_LIBS="$DEPS_LIBS $ARCHIVE_LIBS $GRLNET_LIBS $XML_LIBS $GOM_LIBS"
+AC_SUBST(DEPS_THETVDB_LIBS)
+
+# ----------------------------------------------------------
 # BUILD TMDB PLUGIN
 # ----------------------------------------------------------
 
@@ -1399,6 +1448,7 @@ AC_CONFIG_FILES([
   src/lua-factory/Makefile
   src/lua-factory/sources/Makefile
   src/magnatune/Makefile
+  src/thetvdb/Makefile
   src/metadata-store/Makefile
   src/optical-media/Makefile
   src/pocket/Makefile
@@ -1416,6 +1466,7 @@ AC_CONFIG_FILES([
   tests/dleyna/Makefile
   tests/dleyna/dbusmock/dleyna-server-mock.service
   tests/local-metadata/Makefile
+  tests/thetvdb/Makefile
   tests/tmdb/Makefile
   tests/vimeo/Makefile
 ])
diff --git a/src/Makefile.am b/src/Makefile.am
index 7b4f309..746ccd8 100644
--- a/src/Makefile.am
+++ b/src/Makefile.am
@@ -86,6 +86,10 @@ if SHOUTCAST_PLUGIN
 SUBDIRS += shoutcast
 endif
 
+if THETVDB_PLUGIN
+SUBDIRS += thetvdb
+endif
+
 if TMDB_PLUGIN
 SUBDIRS += tmdb
 endif
@@ -113,7 +117,7 @@ endif
 DIST_SUBDIRS = \
    apple-trailers bliptv bookmarks dleyna dmap filesystem flickr freebox gravatar jamendo \
    lastfm-albumart local-metadata lua-factory magnatune metadata-store optical-media   \
-   pocket podcasts raitv shoutcast tmdb tracker upnp vimeo youtube
+   pocket podcasts raitv shoutcast thetvdb tmdb tracker upnp vimeo youtube
 
 MAINTAINERCLEANFILES = \
         *.in           \
diff --git a/src/thetvdb/Makefile.am b/src/thetvdb/Makefile.am
new file mode 100644
index 0000000..05b0c94
--- /dev/null
+++ b/src/thetvdb/Makefile.am
@@ -0,0 +1,48 @@
+#
+# Makefile.am
+#
+# Author: Victor Toso <me victortoso com>
+#
+# Copyright (C) 2014 Victor Toso. All rights reserved.
+
+include $(top_srcdir)/gtester.mk
+
+ext_LTLIBRARIES = libgrlthetvdb.la
+
+libgrlthetvdb_la_CFLAGS =      \
+       $(DEPS_THETVDB_CFLAGS)  \
+       -DG_LOG_DOMAIN=\"GrlTheTVDB\" \
+       -DLOCALEDIR=\"$(localedir)\"
+
+libgrlthetvdb_la_LIBADD =      \
+       $(DEPS_THETVDB_LIBS)
+
+libgrlthetvdb_la_LDFLAGS = \
+       -no-undefined       \
+       -module             \
+       -avoid-version
+
+libgrlthetvdb_la_SOURCES = \
+        grl-thetvdb.c \
+        grl-thetvdb.h \
+        thetvdb-resources-series.c \
+        thetvdb-resources-episodes.c \
+        thetvdb-resources.h
+
+extdir                         = $(GRL_PLUGINS_DIR)
+thetvdbxmldir  = $(GRL_PLUGINS_DIR)
+thetvdbxml_DATA        = $(THETVDB_PLUGIN_ID).xml
+
+# Rules to enable tests
+copy-xml-to-libs-dir: libgrlthetvdb.la
+       cp -f $(srcdir)/$(thetvdbxml_DATA) $(builddir)/.libs/
+
+all-local: copy-xml-to-libs-dir
+
+EXTRA_DIST = $(thetvdbxml_DATA)
+
+MAINTAINERCLEANFILES = \
+       *.in            \
+       *~
+
+DISTCLEANFILES = $(MAINTAINERCLEANFILES)
diff --git a/src/thetvdb/grl-thetvdb.c b/src/thetvdb/grl-thetvdb.c
new file mode 100644
index 0000000..f7fff2f
--- /dev/null
+++ b/src/thetvdb/grl-thetvdb.c
@@ -0,0 +1,1594 @@
+/*
+ * Copyright (C) 2014 Victor Toso.
+ *
+ * Contact: Victor Toso <me victortoso 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 <glib.h>
+#include <glib/gstdio.h>
+#include <glib/gi18n-lib.h>
+#include <net/grl-net.h>
+#include <libxml/xmlreader.h>
+#include <archive.h>
+#include <archive_entry.h>
+
+#include "grl-thetvdb.h"
+#include "thetvdb-resources.h"
+
+#define GRL_THETVDB_GET_PRIVATE(object)                  \
+  (G_TYPE_INSTANCE_GET_PRIVATE((object),                 \
+                               GRL_THETVDB_SOURCE_TYPE,  \
+                               GrlTheTVDBPrivate))
+
+/* --------- Logging  -------- */
+
+#define GRL_LOG_DOMAIN_DEFAULT thetvdb_log_domain
+GRL_LOG_DOMAIN_STATIC (thetvdb_log_domain);
+
+/* --- URLs --- */
+
+#define THETVDB_BASE        "http://thetvdb.com/";
+#define THETVDB_BASE_API    THETVDB_BASE "api/"
+#define THETVDB_BASE_IMG    THETVDB_BASE "banners/%s"
+
+#define THETVDB_GET_SERIES    THETVDB_BASE_API "GetSeries.php?seriesname=%s"
+#define THETVDB_GET_EPISODES  THETVDB_BASE_API "%s/series/%s/all/%s.zip"
+
+/* --- Files and Path to Images --- */
+#define THETVDB_ALL_DATA_XML    "%s.xml"
+#define THETVDB_URL_TO_SCREEN   THETVDB_BASE    "banners/%s"
+
+/* --- Helpers --- */
+#define THETVDB_STR_DELIMITER   "|"
+#define THETVDB_DEFAULT_LANG    "en"
+#define GRL_SQL_DB              "grl-thetvdb.db"
+
+/* --- XML Fields --- */
+#define THETVDB_ID              BAD_CAST "id"
+#define THETVDB_SERIES_ID       BAD_CAST "SeriesID"
+#define THETVDB_GENRE           BAD_CAST "Genre"
+#define THETVDB_ACTORS          BAD_CAST "Actors"
+#define THETVDB_LANGUAGE        BAD_CAST "Language"
+#define THETVDB_STATUS          BAD_CAST "Status"
+#define THETVDB_SERIES_NAME     BAD_CAST "SeriesName"
+#define THETVDB_OVERVIEW        BAD_CAST "Overview"
+#define THETVDB_RATING          BAD_CAST "Rating"
+#define THETVDB_FIRST_AIRED     BAD_CAST "FirstAired"
+#define THETVDB_IMDB_ID         BAD_CAST "IMDB_ID"
+#define THETVDB_ZAP2IT_ID       BAD_CAST "zap2it_id"
+#define THETVDB_BANNER          BAD_CAST "banner"
+#define THETVDB_POSTER          BAD_CAST "poster"
+#define THETVDB_FANART          BAD_CAST "fanart"
+
+#define THETVDB_SEASON_NUMBER   BAD_CAST "SeasonNumber"
+#define THETVDB_EPISODE_NUMBER  BAD_CAST "EpisodeNumber"
+#define THETVDB_ABSOLUTE_NUMBER BAD_CAST "absolute_number"
+#define THETVDB_EPISODE_NAME    BAD_CAST "EpisodeName"
+#define THETVDB_DIRECTOR        BAD_CAST "Director"
+#define THETVDB_GUEST_STARS     BAD_CAST "GuestStars"
+#define THETVDB_FILENAME        BAD_CAST "filename"
+#define THETVDB_SEASON_ID       BAD_CAST "seasonid"
+#define THETVDB_SERIE_ID        BAD_CAST "seriesid"
+
+/* --- Plugin information --- */
+
+#define SOURCE_ID     "grl-thetvdb"
+#define SOURCE_NAME   "TheTVDB"
+#define SOURCE_DESC   _("A source for fetching metadata of television shows")
+
+/* --- TheTVDB keys  --- */
+static GrlKeyID GRL_THETVDB_METADATA_KEY_THETVDB_ID   = GRL_METADATA_KEY_INVALID;
+static GrlKeyID GRL_THETVDB_METADATA_KEY_IMDB_ID      = GRL_METADATA_KEY_INVALID;
+static GrlKeyID GRL_THETVDB_METADATA_KEY_ZAP2IT_ID    = GRL_METADATA_KEY_INVALID;
+static GrlKeyID GRL_THETVDB_METADATA_KEY_GUEST_STARS  = GRL_METADATA_KEY_INVALID;
+static GrlKeyID GRL_THETVDB_METADATA_KEY_FANART       = GRL_METADATA_KEY_INVALID;
+static GrlKeyID GRL_THETVDB_METADATA_KEY_BANNER       = GRL_METADATA_KEY_INVALID;
+static GrlKeyID GRL_THETVDB_METADATA_KEY_POSTER       = GRL_METADATA_KEY_INVALID;
+static GrlKeyID GRL_THETVDB_METADATA_KEY_EPISODE_SS   = GRL_METADATA_KEY_INVALID;
+
+struct _GrlTheTVDBPrivate {
+  gchar         *api_key;
+  GList         *supported_keys;
+  GomAdapter    *adapter;
+  GomRepository *repository;
+
+  /* Hash table with a string as key (the show name) and a list as its value
+   * (a list of OperationSpec *) */
+  GHashTable    *ht_wait_list;
+};
+
+typedef struct _OperationSpec {
+  GrlSource      *source;
+  guint           operation_id;
+  GList          *keys;
+  GrlMedia       *media;
+  gpointer        user_data;
+  guint           error_code;
+  gchar          *lang;
+  gboolean        fetched_web;
+  SeriesResource *serie_resource;
+  GrlSourceResolveCb callback;
+} OperationSpec;
+
+/* All supported languages from
+ * thetvdb.com/wiki/index.php?title=Multi_Language#Available_Languages */
+static struct {
+  gint         lid;
+  const gchar *name;
+} supported_languages[] = {
+  { 7, "en" },
+  { 8, "sv" },
+  { 9, "no" },
+  { 10, "da" },
+  { 11, "fi" },
+  { 13, "nl" },
+  { 14, "de" },
+  { 15, "it" },
+  { 16, "es" },
+  { 17, "fr" },
+  { 18, "pl" },
+  { 19, "hu" },
+  { 20, "el" },
+  { 21, "tr" },
+  { 22, "ru" },
+  { 24, "he" },
+  { 25, "ja" },
+  { 26, "pt" },
+  { 27, "zh" },
+  { 28, "cs" },
+  { 30, "sl" },
+  { 31, "hr" },
+  { 32, "ko" },
+};
+
+static GrlTheTVDBSource *grl_thetvdb_source_new (const gchar *api_key);
+
+static void grl_thetvdb_source_finalize (GObject *object);
+
+static void grl_thetvdb_source_resolve (GrlSource *source,
+                                        GrlSourceResolveSpec *rs);
+
+static gboolean grl_thetvdb_source_may_resolve (GrlSource *source,
+                                                GrlMedia *media,
+                                                GrlKeyID key_id,
+                                                GList **missing_keys);
+
+static const GList *grl_thetvdb_source_supported_keys (GrlSource *source);
+
+static void thetvdb_migrate_db_done (GObject *object,
+                                     GAsyncResult *result,
+                                     gpointer user_data);
+
+static void cache_find_episode (OperationSpec *os);
+
+/* ================== TheTVDB Plugin  ================= */
+
+static gboolean
+grl_thetvdb_plugin_init (GrlRegistry *registry,
+                         GrlPlugin *plugin,
+                         GList *configs)
+{
+  GrlTheTVDBSource *source;
+  GParamSpec *spec;
+  GrlConfig *config;
+  char *api_key = NULL;
+
+  GRL_LOG_DOMAIN_INIT (thetvdb_log_domain, "thetvdb");
+
+  GRL_DEBUG ("thetvdb_plugin_init");
+
+  if (configs) {
+    config = GRL_CONFIG (configs->data);
+    api_key = grl_config_get_api_key (config);
+  }
+
+  if (api_key == NULL) {
+    GRL_INFO ("Cannot load plugin: missing API Key");
+    return FALSE;
+  }
+
+  spec = g_param_spec_string ("thetvdb-id",
+                              "thetvdb-id",
+                              "TV Show or episode id for The TVDB source.",
+                              NULL,
+                              G_PARAM_STATIC_STRINGS | G_PARAM_READWRITE),
+  GRL_THETVDB_METADATA_KEY_THETVDB_ID =
+    grl_registry_register_metadata_key (registry, spec, NULL);
+  g_param_spec_unref (spec);
+
+  spec = g_param_spec_string ("thetvdb-imdb-id",
+                              "thetvdb-imdb-id",
+                              "TV Show or episode id for IMDB source.",
+                              NULL,
+                              G_PARAM_STATIC_STRINGS | G_PARAM_READWRITE),
+  GRL_THETVDB_METADATA_KEY_IMDB_ID =
+    grl_registry_register_metadata_key (registry, spec, NULL);
+  g_param_spec_unref (spec);
+
+  spec = g_param_spec_string ("thetvdb-zap2it-id",
+                              "thetvdb-zap2it-id",
+                              "TV Show or episode id for Zap2it source.",
+                              NULL,
+                              G_PARAM_STATIC_STRINGS | G_PARAM_READWRITE),
+  GRL_THETVDB_METADATA_KEY_ZAP2IT_ID =
+    grl_registry_register_metadata_key (registry, spec, NULL);
+  g_param_spec_unref (spec);
+
+  spec = g_param_spec_string ("thetvdb-guest-stars",
+                              "thetvdb-guest-stars",
+                              "Guest stars perfoming in the episode.",
+                              NULL,
+                              G_PARAM_STATIC_STRINGS | G_PARAM_READWRITE),
+  GRL_THETVDB_METADATA_KEY_GUEST_STARS =
+    grl_registry_register_metadata_key (registry, spec, NULL);
+  g_param_spec_unref (spec);
+
+  spec = g_param_spec_string ("thetvdb-fanart",
+                              "thetvdb-fanart",
+                              "The mosted voted fanart of the TV Show.",
+                              NULL,
+                              G_PARAM_STATIC_STRINGS | G_PARAM_READWRITE),
+  GRL_THETVDB_METADATA_KEY_FANART =
+    grl_registry_register_metadata_key (registry, spec, NULL);
+  g_param_spec_unref (spec);
+
+  spec = g_param_spec_string ("thetvdb-banner",
+                              "thetvdb-banner",
+                              "The most voted banner of the TV Show.",
+                              NULL,
+                              G_PARAM_STATIC_STRINGS | G_PARAM_READWRITE),
+  GRL_THETVDB_METADATA_KEY_BANNER =
+    grl_registry_register_metadata_key (registry, spec, NULL);
+  g_param_spec_unref (spec);
+
+  spec = g_param_spec_string ("thetvdb-poster",
+                              "thetvdb-poster",
+                              "The most voted poster of the TV Show.",
+                              NULL,
+                              G_PARAM_STATIC_STRINGS | G_PARAM_READWRITE),
+  GRL_THETVDB_METADATA_KEY_POSTER =
+    grl_registry_register_metadata_key (registry, spec, NULL);
+  g_param_spec_unref (spec);
+
+  spec = g_param_spec_string ("thetvdb-episode-screenshot",
+                              "thetvdb-episode-screenshot",
+                              "One screenshot of the episode.",
+                              NULL,
+                              G_PARAM_STATIC_STRINGS | G_PARAM_READWRITE),
+  GRL_THETVDB_METADATA_KEY_EPISODE_SS =
+    grl_registry_register_metadata_key (registry, spec, NULL);
+  g_param_spec_unref (spec);
+
+  source = grl_thetvdb_source_new (api_key);
+  grl_registry_register_source (registry,
+                                plugin,
+                                GRL_SOURCE (source),
+                                NULL);
+  g_free (api_key);
+  return TRUE;
+}
+
+GRL_PLUGIN_REGISTER (grl_thetvdb_plugin_init, NULL, SOURCE_ID);
+
+/* ================== TheTVDB GObject ================= */
+
+static GrlTheTVDBSource *
+grl_thetvdb_source_new (const gchar *api_key)
+{
+  GObject *object;
+  GrlTheTVDBSource *source;
+
+  GRL_DEBUG ("thetvdb_source_new");
+
+  object = g_object_new (GRL_THETVDB_SOURCE_TYPE,
+                         "source-id", SOURCE_ID,
+                         "source-name", SOURCE_NAME,
+                         "source-desc", SOURCE_DESC,
+                         "supported-media", GRL_MEDIA_TYPE_VIDEO,
+                         NULL);
+
+  source = GRL_THETVDB_SOURCE (object);
+  source->priv->api_key = g_strdup (api_key);
+  return source;
+}
+
+static void
+grl_thetvdb_source_class_init (GrlTheTVDBSourceClass * klass)
+{
+  GObjectClass *gobject_class = G_OBJECT_CLASS (klass);
+  GrlSourceClass *source_class = GRL_SOURCE_CLASS (klass);
+
+  source_class->supported_keys = grl_thetvdb_source_supported_keys;
+  source_class->may_resolve = grl_thetvdb_source_may_resolve;
+  source_class->resolve = grl_thetvdb_source_resolve;
+  gobject_class->finalize = grl_thetvdb_source_finalize;
+
+  g_type_class_add_private (klass, sizeof (GrlTheTVDBPrivate));
+}
+
+static void
+grl_thetvdb_source_init (GrlTheTVDBSource *source)
+{
+  gchar *path;
+  gchar *db_path;
+  GList *tables;
+  GError *error = NULL;
+
+  GRL_DEBUG ("thetvdb_source_init");
+
+  source->priv = GRL_THETVDB_GET_PRIVATE (source);
+
+  /* All supported keys in a GList */
+  source->priv->supported_keys =
+    grl_metadata_key_list_new (GRL_METADATA_KEY_SEASON,
+                               GRL_METADATA_KEY_EPISODE,
+                               GRL_METADATA_KEY_GENRE,
+                               GRL_METADATA_KEY_TITLE,
+                               GRL_METADATA_KEY_PERFORMER,
+                               GRL_METADATA_KEY_DIRECTOR,
+                               GRL_METADATA_KEY_PUBLICATION_DATE,
+                               GRL_METADATA_KEY_DESCRIPTION,
+                               GRL_THETVDB_METADATA_KEY_THETVDB_ID,
+                               GRL_THETVDB_METADATA_KEY_IMDB_ID,
+                               GRL_THETVDB_METADATA_KEY_ZAP2IT_ID,
+                               GRL_THETVDB_METADATA_KEY_GUEST_STARS,
+                               GRL_THETVDB_METADATA_KEY_FANART,
+                               GRL_THETVDB_METADATA_KEY_BANNER,
+                               GRL_THETVDB_METADATA_KEY_POSTER,
+                               GRL_THETVDB_METADATA_KEY_EPISODE_SS,
+                               GRL_METADATA_KEY_INVALID);
+
+  /* Get database connection */
+  path = g_build_filename (g_get_user_data_dir (), "grilo-plugins", NULL);
+  if (!g_file_test (path, G_FILE_TEST_IS_DIR))
+    g_mkdir_with_parents (path, 0775);
+
+  GRL_DEBUG ("Opening database connection...");
+  db_path = g_build_filename (path, GRL_SQL_DB, NULL);
+  g_free (path);
+
+  source->priv->adapter = gom_adapter_new ();
+  if (!gom_adapter_open_sync (source->priv->adapter, db_path, &error)) {
+    GrlRegistry *registry;
+    GRL_WARNING ("Could not open database '%s': %s", db_path, error->message);
+    g_error_free (error);
+    g_free (db_path);
+
+    /* Removes itself from list of sources */
+    registry = grl_registry_get_default ();
+    grl_registry_unregister_source (registry,
+                                    GRL_SOURCE (source),
+                                    NULL);
+    return;
+  }
+  g_free (db_path);
+
+  /* Creates our pending queue */
+  source->priv->ht_wait_list = g_hash_table_new_full (g_str_hash, g_str_equal,
+                                                      g_free, NULL);
+
+  source->priv->repository = gom_repository_new (source->priv->adapter);
+  tables = g_list_prepend (NULL, GINT_TO_POINTER (SERIES_TYPE_RESOURCE));
+  tables = g_list_prepend (tables, GINT_TO_POINTER (EPISODE_TYPE_RESOURCE));
+  gom_repository_automatic_migrate_async (source->priv->repository, 2, tables,
+                                          thetvdb_migrate_db_done, source);
+}
+
+G_DEFINE_TYPE (GrlTheTVDBSource, grl_thetvdb_source, GRL_TYPE_SOURCE);
+
+static void
+grl_thetvdb_source_finalize (GObject *object)
+{
+  GrlTheTVDBSource *source;
+
+  GRL_DEBUG ("grl_thetvdb_source_finalize");
+
+  source = GRL_THETVDB_SOURCE (object);
+
+  g_list_free (source->priv->supported_keys);
+  g_hash_table_destroy (source->priv->ht_wait_list);
+  g_clear_object (&source->priv->repository);
+  g_clear_pointer (&source->priv->api_key, g_free);
+
+  if (source->priv->adapter) {
+    gom_adapter_close_sync (source->priv->adapter, NULL);
+    g_clear_object (&source->priv->adapter);
+  }
+
+  G_OBJECT_CLASS (grl_thetvdb_source_parent_class)->finalize (object);
+}
+
+/* ======================= Utilities ==================== */
+static void
+thetvdb_migrate_db_done (GObject      *object,
+                         GAsyncResult *result,
+                         gpointer      user_data)
+{
+  gboolean ret;
+  GError *error = NULL;
+  GrlRegistry *registry;
+
+  ret = gom_repository_migrate_finish (GOM_REPOSITORY (object), result, &error);
+  if (ret == FALSE) {
+    GRL_WARNING ("Failed to migrate database: %s", error->message);
+    g_error_free (error);
+
+    /* Removes itself from list of sources */
+    registry = grl_registry_get_default ();
+    grl_registry_unregister_source (registry,
+                                    GRL_SOURCE (user_data),
+                                    NULL);
+  }
+}
+
+static void
+free_operation_spec (gpointer os_pointer)
+{
+  OperationSpec *os = (OperationSpec *) os_pointer;
+  g_free (os->lang);
+  g_list_free (os->keys);
+  g_clear_object (&os->serie_resource);
+  g_slice_free (OperationSpec, os);
+}
+
+static gchar *
+get_pref_language (GrlTheTVDBSource *tvdb_source)
+{
+  const gchar * const *strv;
+  gint strv_len, langv_len;
+  gint i, j;
+
+  strv = g_get_language_names ();
+  strv_len = g_strv_length ((gchar **) strv);
+  langv_len = G_N_ELEMENTS (supported_languages);
+  for (i = 0; i < strv_len; i++) {
+    /* We are only interested in two letter language */
+    if (strlen (strv[i]) != 2)
+      continue;
+
+    for (j = 0; j < langv_len; j++) {
+      if (g_strcmp0 (supported_languages[j].name, strv[i]) == 0)
+        return g_strdup (strv[i]);
+    }
+  }
+
+  return g_strdup (THETVDB_DEFAULT_LANG);
+}
+
+static EpisodeResource *
+xml_parse_and_save_episodes (GomRepository *repository,
+                             xmlDocPtr doc,
+                             const gchar *title,
+                             gint season_number,
+                             gint episode_number)
+{
+  xmlNodePtr node, child;
+  xmlChar *node_data = NULL;
+  EpisodeResource *episode_resource= NULL;
+
+  /* Get ptr to <data> */
+  node = xmlDocGetRootElement (doc);
+  /* Get ptr to <Serie> */
+  node = node->xmlChildrenNode;
+  /* Iterate in the <Episode> nodes */
+  for (node = node->next; node != NULL; node = node->next) {
+    GError *error = NULL;
+    gint tmp_season_number = -1;
+    gint tmp_episode_number = -1;
+    gchar *tmp_title;
+    gboolean episode_found = FALSE;
+    EpisodeResource *eres;
+
+    eres = g_object_new (EPISODE_TYPE_RESOURCE,
+                         "repository", repository,
+                         NULL);
+
+    for (child = node->xmlChildrenNode; child != NULL; child = child->next) {
+      node_data = xmlNodeListGetString (doc, child->xmlChildrenNode, 1);
+      if (node_data == NULL)
+        continue;
+
+      if (xmlStrcasecmp (child->name, THETVDB_LANGUAGE) == 0) {
+        g_object_set (G_OBJECT (eres), EPISODE_COLUMN_LANGUAGE,
+                      (gchar *) node_data, NULL);
+
+      } else if (xmlStrcmp (child->name, THETVDB_OVERVIEW) == 0) {
+        g_object_set (G_OBJECT (eres), EPISODE_COLUMN_OVERVIEW,
+                      (gchar *) node_data, NULL);
+
+      } else if (xmlStrcmp (child->name, THETVDB_FIRST_AIRED) == 0) {
+        g_object_set (G_OBJECT (eres), EPISODE_COLUMN_FIRST_AIRED,
+                      (gchar *) node_data, NULL);
+
+      } else if (xmlStrcmp (child->name, THETVDB_IMDB_ID) == 0) {
+        g_object_set (G_OBJECT (eres), EPISODE_COLUMN_IMDB_ID,
+                      (gchar *) node_data, NULL);
+
+      } else if (xmlStrcmp (child->name, THETVDB_SEASON_ID) == 0) {
+        g_object_set (G_OBJECT (eres), EPISODE_COLUMN_SEASON_ID,
+                      (gchar *) node_data, NULL);
+
+      } else if (xmlStrcmp (child->name, THETVDB_SERIE_ID) == 0) {
+        g_object_set (G_OBJECT (eres), EPISODE_COLUMN_SERIES_ID,
+                      (gchar *) node_data, NULL);
+
+      } else if (xmlStrcmp (child->name, THETVDB_DIRECTOR) == 0) {
+        g_object_set (G_OBJECT (eres), EPISODE_COLUMN_DIRECTOR_NAMES,
+                      (gchar *) node_data, NULL);
+
+      } else if (xmlStrcmp (child->name, THETVDB_GUEST_STARS) == 0) {
+        g_object_set (G_OBJECT (eres), EPISODE_COLUMN_GUEST_STARS_NAMES,
+                      (gchar *) node_data, NULL);
+
+      } else if (xmlStrcmp (child->name, THETVDB_ID) == 0) {
+        g_object_set (G_OBJECT (eres), EPISODE_COLUMN_EPISODE_ID,
+                      (gchar *) node_data, NULL);
+
+      } else if (xmlStrcmp (child->name, THETVDB_FILENAME) == 0) {
+        gchar *str = g_strdup_printf (THETVDB_BASE_IMG, (gchar *) node_data);
+        g_object_set (G_OBJECT (eres), EPISODE_COLUMN_URL_EPISODE_SCREEN,
+                      str, NULL);
+        g_free (str);
+
+      } else if (xmlStrcmp (child->name, THETVDB_EPISODE_NAME) == 0) {
+        g_object_set (G_OBJECT (eres), EPISODE_COLUMN_EPISODE_NAME,
+                      (gchar *) node_data, NULL);
+        tmp_title = g_strdup ((gchar *) node_data);
+
+      } else if (xmlStrcmp (child->name, THETVDB_RATING) == 0) {
+        gdouble num_double = g_ascii_strtod ((gchar *) node_data, NULL);
+        g_object_set (G_OBJECT (eres), EPISODE_COLUMN_RATING, num_double, NULL);
+
+      } else if (xmlStrcmp (child->name, THETVDB_SEASON_NUMBER) == 0) {
+        gint num = g_ascii_strtoull ((gchar *) node_data, NULL, 10);
+        g_object_set (G_OBJECT (eres), EPISODE_COLUMN_SEASON_NUMBER, num, NULL);
+        tmp_season_number = num;
+
+      } else if (xmlStrcmp (child->name, THETVDB_ABSOLUTE_NUMBER) == 0) {
+        gint num = g_ascii_strtoull ((gchar *) node_data, NULL, 10);
+        g_object_set (G_OBJECT (eres), EPISODE_COLUMN_ABSOLUTE_NUMBER,
+                      num, NULL);
+
+      } else if (xmlStrcmp (child->name, THETVDB_EPISODE_NUMBER) == 0) {
+        gint num = g_ascii_strtoull ((gchar *) node_data, NULL, 10);
+        g_object_set (G_OBJECT (eres), EPISODE_COLUMN_EPISODE_NUMBER,
+                      num, NULL);
+        tmp_episode_number = num;
+      }
+
+      if (node_data != NULL) {
+        xmlFree (node_data);
+        node_data = NULL;
+      }
+    }
+
+    /* Check if the episode is the same we want */
+    if (tmp_season_number == season_number
+        && tmp_episode_number == episode_number) {
+      episode_found = TRUE;
+      episode_resource = eres;
+    }
+
+    if (title != NULL && tmp_title != NULL
+        && g_ascii_strncasecmp (title, tmp_title, 0) == 0) {
+      episode_found = TRUE;
+      episode_resource = eres;
+    }
+
+    gom_resource_save_sync (GOM_RESOURCE (eres), &error);
+    if (error != NULL) {
+      GRL_DEBUG ("Failed to store episode '%s' due %s",
+                  tmp_title, error->message);
+      g_error_free (error);
+    }
+
+    g_clear_pointer (&tmp_title, g_free);
+    if (!episode_found)
+      g_object_unref (eres);
+  }
+
+  return episode_resource;
+}
+
+static gchar *
+xml_parse_get_series_id (xmlDocPtr doc)
+{
+  xmlNodePtr node;
+  xmlChar *node_data = NULL;
+  gchar *series_id = NULL;
+
+  /* Get ptr to <data> */
+  node = xmlDocGetRootElement (doc);
+  /* Get ptr to <Serie> */
+  node = node->xmlChildrenNode;
+  /* Iterate in the <Serie> */
+  for (node = node->xmlChildrenNode; node != NULL; node = node->next) {
+    node_data = xmlNodeListGetString (doc, node->xmlChildrenNode, 1);
+    if (node_data == NULL)
+      continue;
+
+    if (xmlStrcmp (node->name, THETVDB_ID) == 0) {
+        series_id = g_strdup ((gchar *) node_data);
+        xmlFree (node_data);
+        break;
+    }
+    xmlFree (node_data);
+  }
+  return series_id;
+}
+
+static SeriesResource *
+xml_parse_and_save_serie (GomRepository *repository,
+                          xmlDocPtr doc)
+{
+  xmlNodePtr node;
+  xmlChar *node_data = NULL;
+  SeriesResource *sres;
+  GError *error = NULL;
+  gchar *show = NULL;
+
+  sres = g_object_new (SERIES_TYPE_RESOURCE,
+                       "repository", repository,
+                       NULL);
+
+  /* Get ptr to <data> */
+  node = xmlDocGetRootElement (doc);
+  /* Get ptr to <Serie> */
+  node = node->xmlChildrenNode;
+  /* Iterate in the <Serie> */
+  for (node = node->xmlChildrenNode; node != NULL; node = node->next) {
+    node_data = xmlNodeListGetString (doc, node->xmlChildrenNode, 1);
+    if (node_data == NULL)
+      continue;
+
+    if (xmlStrcasecmp (node->name, THETVDB_LANGUAGE) == 0) {
+      g_object_set (G_OBJECT (sres), SERIES_COLUMN_LANGUAGE,
+                    (gchar *) node_data, NULL);
+
+    } else if (xmlStrcmp (node->name, THETVDB_STATUS) == 0) {
+      g_object_set (G_OBJECT (sres), SERIES_COLUMN_STATUS,
+                    (gchar *) node_data, NULL);
+
+    } else if (xmlStrcmp (node->name, THETVDB_SERIES_NAME) == 0) {
+      g_object_set (G_OBJECT (sres), SERIES_COLUMN_SERIES_NAME,
+                    (gchar *) node_data, NULL);
+      show = g_strdup ((gchar *) node_data);
+
+    } else if (xmlStrcmp (node->name, THETVDB_OVERVIEW) == 0) {
+      g_object_set (G_OBJECT (sres), SERIES_COLUMN_OVERVIEW,
+                    (gchar *) node_data, NULL);
+
+    } else if (xmlStrcmp (node->name, THETVDB_FIRST_AIRED) == 0) {
+      g_object_set (G_OBJECT (sres), SERIES_COLUMN_FIRST_AIRED,
+                    (gchar *) node_data, NULL);
+
+    } else if (xmlStrcmp (node->name, THETVDB_IMDB_ID) == 0) {
+      g_object_set (G_OBJECT (sres), SERIES_COLUMN_IMDB_ID,
+                    (gchar *) node_data, NULL);
+
+    } else if (xmlStrcmp (node->name, THETVDB_ZAP2IT_ID) == 0) {
+      g_object_set (G_OBJECT (sres), SERIES_COLUMN_ZAP2IT_ID,
+                    (gchar *) node_data, NULL);
+
+    } else if (xmlStrcmp (node->name, THETVDB_ACTORS) == 0) {
+      g_object_set (G_OBJECT (sres), SERIES_COLUMN_ACTOR_NAMES,
+                    (gchar *) node_data, NULL);
+
+    } else if (xmlStrcmp (node->name, THETVDB_GENRE) == 0) {
+      g_object_set (G_OBJECT (sres), SERIES_COLUMN_GENRES,
+                    (gchar *) node_data, NULL);
+
+    } else if (xmlStrcmp (node->name, THETVDB_ID) == 0) {
+      g_object_set (G_OBJECT (sres), SERIES_COLUMN_SERIES_ID,
+                    (gchar *) node_data, NULL);
+
+    } else if (xmlStrcmp (node->name, THETVDB_BANNER) == 0) {
+      gchar *str = g_strdup_printf (THETVDB_BASE_IMG, (gchar *) node_data);
+      g_object_set (G_OBJECT (sres), SERIES_COLUMN_URL_BANNER, str, NULL);
+      g_free (str);
+
+    } else if (xmlStrcmp (node->name, THETVDB_POSTER) == 0) {
+      gchar *str = g_strdup_printf (THETVDB_BASE_IMG, (gchar *) node_data);
+      g_object_set (G_OBJECT (sres), SERIES_COLUMN_URL_POSTER, str, NULL);
+      g_free (str);
+
+    } else if (xmlStrcmp (node->name, THETVDB_FANART) == 0) {
+      gchar *str = g_strdup_printf (THETVDB_BASE_IMG, (gchar *) node_data);
+      g_object_set (G_OBJECT (sres), SERIES_COLUMN_URL_FANART, str, NULL);
+      g_free (str);
+
+    } else if (xmlStrcmp (node->name, THETVDB_RATING) == 0) {
+      gdouble num_double = g_ascii_strtod ((gchar *) node_data, NULL);
+      g_object_set (G_OBJECT (sres), SERIES_COLUMN_RATING, num_double, NULL);
+    }
+
+    if (node_data != NULL) {
+      xmlFree (node_data);
+      node_data = NULL;
+    }
+  }
+
+  gom_resource_save_sync (GOM_RESOURCE (sres), &error);
+  if (error != NULL) {
+    GRL_DEBUG ("Failed to store series '%s' due %s",
+                show, error->message);
+    g_error_free (error);
+  }
+
+  g_clear_pointer (&show, g_free);
+  return sres;
+}
+
+static void
+thetvdb_add_data_string_unique (GrlData *data,
+                                GrlKeyID key_id,
+                                gchar **strv)
+{
+  guint i, j;
+  guint data_len;
+  GrlRelatedKeys *related_keys;
+
+  for (i = 0; strv[i] != NULL; i++) {
+    gboolean insert = TRUE;
+
+    /* Do not insert empty strings */
+    if (strv[i] && *strv[i] == '\0')
+      continue;
+
+    /* Check if this value already exists to this key */
+    data_len = grl_data_length (data, key_id);
+    for (j = 0; insert && j < data_len; j++) {
+      const gchar *element;
+
+      related_keys = grl_data_get_related_keys (data, key_id, j);
+      element = grl_related_keys_get_string (related_keys, key_id);
+      if (g_strcmp0 (element, strv[i]) == 0)
+        insert = FALSE;
+    }
+
+    if (insert)
+      grl_data_add_string (data, key_id, strv[i]);
+  }
+}
+
+static gchar *
+get_from_cache_episode_or_series (EpisodeResource *eres,
+                                  const gchar *ecol,
+                                  SeriesResource *sres,
+                                  const gchar *scol)
+{
+  gchar *str = NULL;
+  if (eres != NULL)
+    g_object_get (eres, ecol, &str, NULL);
+
+  if (str == NULL && sres != NULL)
+    g_object_get (sres, scol, &str, NULL);
+
+  return str;
+}
+
+/* Return a pointer to the media with updated values
+ * or NULL if no keys were set
+ *
+ * NOTE: We give preference to an episode over the tv show for those metadatas
+ * that could provide information for both. */
+static GrlMedia *
+thetvdb_update_media_from_resources (GrlMediaVideo *video,
+                                     GList *keys,
+                                     SeriesResource *sres,
+                                     EpisodeResource *eres)
+{
+  gint failed_keys = 0;
+  GList *it;
+
+  if (sres == NULL)
+    return NULL;
+
+  for (it = keys; it != NULL; it = it->next) {
+    GrlKeyID key_id = GRLPOINTER_TO_KEYID (it->data);
+    gint num = -1;
+    gchar *str = NULL;
+
+    switch (key_id) {
+    case GRL_METADATA_KEY_SEASON:
+      if (eres != NULL)
+        g_object_get (eres, EPISODE_COLUMN_SEASON_NUMBER, &num, NULL);
+
+      if (num > 0)
+        grl_media_video_set_season (video, num);
+      else
+        failed_keys++;
+      break;
+
+    case GRL_METADATA_KEY_EPISODE:
+      if (eres != NULL)
+        g_object_get (eres, EPISODE_COLUMN_EPISODE_NUMBER, &num, NULL);
+
+      if (num > 0)
+        grl_media_video_set_episode (video, num);
+      else
+        failed_keys++;
+      break;
+
+    case GRL_METADATA_KEY_TITLE:
+      if (eres != NULL)
+        g_object_get (eres, EPISODE_COLUMN_EPISODE_NAME, &str, NULL);
+
+      if (str != NULL) {
+        grl_media_set_title (GRL_MEDIA (video), str);
+        g_free (str);
+      } else
+        failed_keys++;
+      break;
+
+    case GRL_METADATA_KEY_GENRE:
+      g_object_get (sres, SERIES_COLUMN_GENRES, &str, NULL);
+      if (str != NULL) {
+        gchar **genres;
+        genres = g_strsplit (str, THETVDB_STR_DELIMITER, 0);
+        thetvdb_add_data_string_unique (GRL_DATA (video),
+                                        GRL_METADATA_KEY_GENRE,
+                                        genres);
+        g_free (str);
+        g_strfreev (genres);
+      } else
+        failed_keys++;
+      break;
+
+    case GRL_METADATA_KEY_PERFORMER:
+      g_object_get (sres, SERIES_COLUMN_ACTOR_NAMES, &str, NULL);
+      if (str != NULL) {
+        gchar **actors;
+        actors = g_strsplit (str, THETVDB_STR_DELIMITER, 0);
+        thetvdb_add_data_string_unique (GRL_DATA (video),
+                                        GRL_METADATA_KEY_PERFORMER,
+                                        actors);
+        g_free (str);
+        g_strfreev (actors);
+      } else
+        failed_keys++;
+      break;
+
+    case GRL_METADATA_KEY_DIRECTOR:
+      if (eres != NULL)
+        g_object_get (eres, EPISODE_COLUMN_DIRECTOR_NAMES, &str, NULL);
+
+      if (str != NULL) {
+        gchar **directors;
+        directors = g_strsplit (str, THETVDB_STR_DELIMITER, 0);
+        thetvdb_add_data_string_unique (GRL_DATA (video),
+                                        GRL_METADATA_KEY_DIRECTOR,
+                                        directors);
+        g_free (str);
+        g_strfreev (directors);
+      } else
+        failed_keys++;
+      break;
+
+    case GRL_METADATA_KEY_PUBLICATION_DATE:
+      str = get_from_cache_episode_or_series (eres, EPISODE_COLUMN_FIRST_AIRED,
+                                              sres, SERIES_COLUMN_FIRST_AIRED);
+      if (str != NULL) {
+        GDateTime *date;
+        gint year, month, day;
+
+        sscanf (str, "%d-%d-%d", &year, &month, &day);
+        date = g_date_time_new_local (year, month, day, 0, 0, 0);
+        grl_media_set_publication_date (GRL_MEDIA (video), date);
+        g_date_time_unref (date);
+        g_free (str);
+      } else
+        failed_keys++;
+      break;
+
+    case GRL_METADATA_KEY_DESCRIPTION:
+      str = get_from_cache_episode_or_series (eres, EPISODE_COLUMN_OVERVIEW,
+                                              sres, SERIES_COLUMN_OVERVIEW);
+      if (str != NULL) {
+        grl_media_set_description (GRL_MEDIA (video), str);
+        g_free (str);
+      } else
+        failed_keys++;
+      break;
+
+    default:
+      /* THETVDB Keys */
+      if (key_id == GRL_THETVDB_METADATA_KEY_THETVDB_ID) {
+        str = get_from_cache_episode_or_series (eres, EPISODE_COLUMN_EPISODE_ID,
+                                                sres, SERIES_COLUMN_SERIES_ID);
+        if (str != NULL) {
+          grl_data_set_string (GRL_DATA (video),
+                               GRL_THETVDB_METADATA_KEY_THETVDB_ID,
+                               str);
+          g_free (str);
+        } else
+          failed_keys++;
+
+      } else if (key_id == GRL_THETVDB_METADATA_KEY_IMDB_ID) {
+        str = get_from_cache_episode_or_series (eres, EPISODE_COLUMN_IMDB_ID,
+                                                sres, SERIES_COLUMN_IMDB_ID);
+        if (str != NULL) {
+          grl_data_set_string (GRL_DATA (video),
+                               GRL_THETVDB_METADATA_KEY_IMDB_ID,
+                               str);
+          g_free (str);
+        } else
+          failed_keys++;
+
+      } else if (key_id == GRL_THETVDB_METADATA_KEY_ZAP2IT_ID) {
+        g_object_get (sres, SERIES_COLUMN_ZAP2IT_ID, &str, NULL);
+        if (str != NULL) {
+          grl_data_set_string (GRL_DATA (video),
+                               GRL_THETVDB_METADATA_KEY_ZAP2IT_ID,
+                               str);
+          g_free (str);
+        } else
+          failed_keys++;
+
+      } else if (key_id == GRL_THETVDB_METADATA_KEY_GUEST_STARS) {
+        if (eres != NULL)
+          g_object_get (eres, EPISODE_COLUMN_GUEST_STARS_NAMES, &str, NULL);
+
+        if (str != NULL) {
+          gchar **stars;
+          stars = g_strsplit (str, THETVDB_STR_DELIMITER, 0);
+          thetvdb_add_data_string_unique (GRL_DATA (video),
+                                          GRL_THETVDB_METADATA_KEY_GUEST_STARS,
+                                          stars);
+          g_free (str);
+          g_strfreev (stars);
+        } else
+          failed_keys++;
+
+      } else if (key_id == GRL_THETVDB_METADATA_KEY_FANART) {
+        g_object_get (sres, SERIES_COLUMN_URL_FANART, &str, NULL);
+        if (str != NULL) {
+          grl_data_set_string (GRL_DATA (video),
+                               GRL_THETVDB_METADATA_KEY_FANART,
+                               str);
+          g_free (str);
+        } else
+          failed_keys++;
+
+      } else if (key_id == GRL_THETVDB_METADATA_KEY_BANNER) {
+        g_object_get (sres, SERIES_COLUMN_URL_BANNER, &str, NULL);
+        if (str != NULL) {
+          grl_data_set_string (GRL_DATA (video),
+                               GRL_THETVDB_METADATA_KEY_BANNER,
+                               str);
+          g_free (str);
+        } else
+          failed_keys++;
+
+      } else if (key_id == GRL_THETVDB_METADATA_KEY_POSTER) {
+        g_object_get (sres, SERIES_COLUMN_URL_POSTER, &str, NULL);
+        if (str != NULL) {
+          grl_data_set_string (GRL_DATA (video),
+                               GRL_THETVDB_METADATA_KEY_POSTER,
+                               str);
+          g_free (str);
+        } else
+          failed_keys++;
+
+      } else if (key_id == GRL_THETVDB_METADATA_KEY_EPISODE_SS) {
+        if (eres != NULL)
+          g_object_get (eres, EPISODE_COLUMN_URL_EPISODE_SCREEN, &str, NULL);
+
+        if (str != NULL) {
+          grl_data_set_string (GRL_DATA (video),
+                               GRL_THETVDB_METADATA_KEY_EPISODE_SS,
+                               str);
+          g_free (str);
+        } else
+          failed_keys++;
+      } else
+        failed_keys++;
+    }
+  }
+
+  return (failed_keys == g_list_length (keys)) ? NULL : GRL_MEDIA (video);
+}
+
+static gboolean
+xml_load_data (const gchar *str,
+               xmlDocPtr *doc)
+{
+  xmlDocPtr doc_ptr;
+  xmlNodePtr node;
+
+  doc_ptr = xmlReadMemory (str, strlen (str), NULL, NULL,
+                           XML_PARSE_RECOVER | XML_PARSE_NOBLANKS);
+  if (!doc_ptr)
+    goto free_resources;
+
+  node = xmlDocGetRootElement (doc_ptr);
+  if (!node)
+    goto free_resources;
+
+  *doc = doc_ptr;
+  return TRUE;
+
+free_resources:
+  xmlFreeDoc (doc_ptr);
+  return FALSE;
+}
+
+static void
+web_request_succeed (const OperationSpec *os,
+                     SeriesResource *sres)
+{
+  const gchar *show;
+  GList *wait_list, *it;
+  GrlTheTVDBSource *tvdb_source;
+
+  tvdb_source = GRL_THETVDB_SOURCE (os->source);
+  show = grl_media_video_get_show (GRL_MEDIA_VIDEO (os->media));
+
+  wait_list = g_hash_table_lookup (tvdb_source->priv->ht_wait_list, show);
+  for (it = wait_list; it != NULL; it = it->next) {
+    OperationSpec *os = (OperationSpec *) it->data;
+
+    GRL_DEBUG ("Request with id %d succeed. Show name is %s",
+               os->operation_id, show);
+
+    /* To avoid fetching again */
+    os->fetched_web = TRUE;
+    os->serie_resource = g_object_ref (sres);
+    cache_find_episode (os);
+  }
+  g_object_unref (sres);
+  g_list_free (wait_list);
+  g_hash_table_remove (tvdb_source->priv->ht_wait_list, show);
+}
+
+static void
+web_request_failed (const OperationSpec *os)
+{
+  const gchar *show;
+  GList *wait_list, *it;
+  GrlTheTVDBSource *tvdb_source;
+
+  tvdb_source = GRL_THETVDB_SOURCE (os->source);
+  show = grl_media_video_get_show (GRL_MEDIA_VIDEO (os->media));
+
+  wait_list = g_hash_table_lookup (tvdb_source->priv->ht_wait_list, show);
+  for (it = wait_list; it != NULL; it = it->next) {
+    OperationSpec *os = (OperationSpec *) it->data;
+
+    GRL_DEBUG ("Request with id %d failed. Show name is %s",
+               os->operation_id, show);
+
+    os->callback (os->source, os->operation_id, NULL, os->user_data, NULL);
+  }
+  g_list_free_full (wait_list, free_operation_spec);
+  g_hash_table_remove (tvdb_source->priv->ht_wait_list, show);
+}
+
+static gboolean
+unzip_all_series_data (gchar *zip,
+                       gsize zip_len,
+                       gchar *language,
+                       gchar **dest)
+{
+  struct archive *a;
+  struct archive_entry *entry;
+  gint r;
+  gboolean ret = FALSE;
+  gchar *target_xml;
+
+  a = archive_read_new ();
+  archive_read_support_format_zip (a);
+  r = archive_read_open_memory (a, zip, zip_len);
+  if (r != ARCHIVE_OK) {
+    GRL_DEBUG ("Fail to open archive.");
+    goto unzip_all_series_end;
+  }
+
+  target_xml = g_strdup_printf (THETVDB_ALL_DATA_XML, language);
+  while ((r = archive_read_next_header (a, &entry)) == ARCHIVE_OK) {
+    const char *name;
+
+    name = archive_entry_pathname (entry);
+    GRL_DEBUG ("[ZIP] ENTRY-NAME: '%s'", name);
+    if (g_strcmp0 (name, target_xml) == 0) {
+      size_t size = archive_entry_size (entry);
+      char *buf;
+      ssize_t read;
+
+      buf = g_malloc (size + 1);
+      buf[size] = 0;
+      read = archive_read_data (a, buf, size);
+      if (read <= 0) {
+        g_free (buf);
+        if (read < 0)
+          GRL_DEBUG ("Fatal error reading '%s' in archive: %s",
+                     name, archive_error_string (a));
+        else
+          GRL_DEBUG ("Read an empty file from the archive");
+      } else {
+        *dest = buf;
+        break;
+      }
+    }
+    archive_read_data_skip (a);
+  }
+
+  g_free (target_xml);
+  if (r != ARCHIVE_OK) {
+    if (r == ARCHIVE_FATAL) {
+      GRL_WARNING ("Fatal error handling archive: %s",
+                   archive_error_string (a));
+    }
+    goto unzip_all_series_end;
+  }
+
+  ret = TRUE;
+unzip_all_series_end:
+  archive_read_free (a);
+  return ret;
+}
+
+static void
+web_get_all_zipped_done (GObject *source_object,
+                         GAsyncResult *res,
+                         gpointer user_data)
+{
+  gchar *zip_data;
+  gchar *xml_data;
+  gsize zip_len;
+  GError *err = NULL;
+  xmlDocPtr doc;
+  SeriesResource *sres = NULL;
+  EpisodeResource *eres;
+  OperationSpec *os;
+  GrlMediaVideo *video;
+  GrlTheTVDBSource *tvdb_source;
+
+  os = (OperationSpec *) user_data;
+  video = GRL_MEDIA_VIDEO (os->media);
+  tvdb_source = GRL_THETVDB_SOURCE (os->source);
+
+  grl_net_wc_request_finish (GRL_NET_WC (source_object),
+                             res, &zip_data, &zip_len, &err);
+  if (err != NULL) {
+    GRL_DEBUG ("Resolve operation failed due '%s'", err->message);
+    g_error_free (err);
+    goto get_all_zipped_end;
+  }
+
+  if (!unzip_all_series_data (zip_data, zip_len, os->lang, &xml_data)) {
+    GRL_DEBUG ("Resolve operation failed while unzipping data");
+    goto get_all_zipped_end;
+  }
+
+  if (!xml_load_data (xml_data, &doc)) {
+    GRL_DEBUG ("Resolve operation failed while loading xml");
+    g_free (xml_data);
+    goto get_all_zipped_end;
+  }
+  g_free (xml_data);
+
+  sres = xml_parse_and_save_serie (tvdb_source->priv->repository, doc);
+  eres = xml_parse_and_save_episodes (tvdb_source->priv->repository, doc,
+                                      grl_media_get_title (os->media),
+                                      grl_media_video_get_season (video),
+                                      grl_media_video_get_episode (video));
+  xmlFreeDoc (doc);
+
+get_all_zipped_end:
+  if (sres != NULL) {
+    web_request_succeed (os, sres);
+    if (eres)
+      g_clear_object (&eres);
+  } else {
+    web_request_failed (os);
+  }
+}
+
+static void
+web_get_series_done (GObject *source_object,
+                     GAsyncResult *res,
+                     gpointer user_data)
+{
+  gchar *data;
+  gsize len;
+  gchar *url;
+  gchar *serie_id;
+  GError *err = NULL;
+  xmlDocPtr doc;
+  GrlNetWc *wc;
+  OperationSpec *os;
+  GrlTheTVDBSource *tvdb_source;
+
+  os = (OperationSpec *) user_data;
+  tvdb_source = GRL_THETVDB_SOURCE (os->source);
+
+  grl_net_wc_request_finish (GRL_NET_WC (source_object),
+                             res, &data, &len, &err);
+  if (err != NULL) {
+    GRL_WARNING ("Resolve operation failed due '%s'", err->message);
+    g_error_free (err);
+    goto get_series_done_error;
+  }
+
+  if (!xml_load_data (data, &doc)) {
+    GRL_WARNING ("Resolve operation failed while loading xml");
+    goto get_series_done_error;
+  }
+
+  serie_id = xml_parse_get_series_id (doc);
+  wc = grl_net_wc_new ();
+  url = g_strdup_printf (THETVDB_GET_EPISODES, tvdb_source->priv->api_key,
+                         serie_id, os->lang);
+  g_free (serie_id);
+
+  GRL_DEBUG ("url[2] %s", url);
+  grl_net_wc_request_async (wc, url, NULL, web_get_all_zipped_done, os);
+  g_free (url);
+  g_object_unref (wc);
+  xmlFreeDoc (doc);
+  return;
+
+get_series_done_error:
+  os->callback (os->source, os->operation_id, NULL, os->user_data, NULL);
+  web_request_failed (os);
+}
+
+static void
+thetvdb_execute_resolve_web (OperationSpec *os)
+{
+  GrlNetWc *wc;
+  gchar *url;
+  const gchar *show;
+  GrlTheTVDBSource *tvdb_source;
+  GList *wait_list;
+
+  GRL_DEBUG ("thetvdb_resolve_web");
+
+  tvdb_source = GRL_THETVDB_SOURCE (os->source);
+  show = grl_media_video_get_show (GRL_MEDIA_VIDEO (os->media));
+
+  /* If there is a request on this show already, wait. */
+  wait_list = g_hash_table_lookup (tvdb_source->priv->ht_wait_list, show);
+  if (wait_list == NULL) {
+    wait_list = g_list_append (wait_list, os);
+    g_hash_table_insert (tvdb_source->priv->ht_wait_list,
+                         g_strdup (show),
+                         wait_list);
+    wc = grl_net_wc_new ();
+    url = g_strdup_printf (THETVDB_GET_SERIES, show);
+
+    GRL_DEBUG ("url[1] %s", url);
+    grl_net_wc_request_async (wc, url, NULL, web_get_series_done, os);
+    g_free (url);
+    g_object_unref (wc);
+  } else {
+    wait_list = g_list_append (wait_list, os);
+    GRL_DEBUG ("[%s] Add to wait list: %d", show, os->operation_id);
+  }
+}
+
+static void
+cache_find_episode_done (GObject *object,
+                         GAsyncResult *res,
+                         gpointer user_data)
+{
+  const gchar *show;
+  OperationSpec *os;
+  GomResource *resource;
+  GrlMedia *media;
+  GError *err = NULL;
+
+  os = (OperationSpec *) user_data;
+  show = grl_media_video_get_show (GRL_MEDIA_VIDEO (os->media));
+
+  resource = gom_repository_find_one_finish (GOM_REPOSITORY (object),
+                                             res,
+                                             &err);
+  if (resource == NULL) {
+    GRL_DEBUG ("[Episode] Cache miss with '%s' due '%s'", show, err->message);
+    g_error_free (err);
+
+    if (os->fetched_web == FALSE) {
+      /* Fetch web API in order to update current cache */
+      thetvdb_execute_resolve_web (os);
+    }
+    return;
+  }
+
+  media = thetvdb_update_media_from_resources (GRL_MEDIA_VIDEO (os->media),
+                                               os->keys,
+                                               os->serie_resource,
+                                               EPISODE_RESOURCE (resource));
+  os->callback (os->source, os->operation_id, media, os->user_data, NULL);
+  g_object_unref (resource);
+  free_operation_spec (os);
+}
+
+static void
+cache_find_episode (OperationSpec *os)
+{
+  GrlTheTVDBSource *tvdb_source;
+  GomFilter *query, *by_series_id, *by_episode;
+  GValue value_str = { 0, };
+  GrlMedia *media = NULL;
+  gchar *series_id;
+  gchar *show;
+  const gchar *title;
+  gint season_number, episode_number;
+
+  GRL_DEBUG ("cache_find_episode");
+
+  tvdb_source = GRL_THETVDB_SOURCE (os->source);
+  title = grl_media_get_title (os->media);
+  season_number = grl_media_video_get_season (GRL_MEDIA_VIDEO (os->media));
+  episode_number = grl_media_video_get_episode (GRL_MEDIA_VIDEO (os->media));
+
+  g_object_get (os->serie_resource,
+                SERIES_COLUMN_SERIES_ID, &series_id,
+                SERIES_COLUMN_SERIES_NAME, &show,
+                NULL);
+
+  /* Check if this media is an Episode of a TV Show */
+  if (title == NULL && (season_number == 0 || episode_number == 0)) {
+    goto cache_episode_end;
+  }
+
+  /* Find the exactly episode using series-id and some episode specific info */
+  g_value_init (&value_str, G_TYPE_STRING);
+  g_value_set_string (&value_str, series_id);
+  by_series_id = gom_filter_new_eq (EPISODE_TYPE_RESOURCE,
+                                    EPISODE_COLUMN_SERIES_ID,
+                                    &value_str);
+  g_value_unset (&value_str);
+
+  if (season_number != 0 && episode_number != 0) {
+    GomFilter *filter1, *filter2;
+    GValue value_num = { 0, };
+
+    g_value_init (&value_num, G_TYPE_UINT);
+    g_value_set_uint (&value_num, season_number);
+    filter1 = gom_filter_new_eq (EPISODE_TYPE_RESOURCE,
+                                 EPISODE_COLUMN_SEASON_NUMBER,
+                                 &value_num);
+    g_value_set_uint (&value_num, episode_number);
+    filter2 = gom_filter_new_eq (EPISODE_TYPE_RESOURCE,
+                                 EPISODE_COLUMN_EPISODE_NUMBER,
+                                 &value_num);
+    g_value_unset (&value_num);
+
+    /* Find episode by season number and episode number */
+    by_episode = gom_filter_new_and (filter1, filter2);
+    g_object_unref (filter1);
+    g_object_unref (filter2);
+  } else {
+    g_value_init (&value_str, G_TYPE_STRING);
+    g_value_set_string (&value_str, title);
+
+    /* Find episode by its title */
+    by_episode = gom_filter_new_like (EPISODE_TYPE_RESOURCE,
+                                      EPISODE_COLUMN_EPISODE_NAME,
+                                      &value_str);
+    g_value_unset (&value_str);
+  }
+
+  query = gom_filter_new_and (by_series_id, by_episode);
+  g_object_unref (by_series_id);
+  g_object_unref (by_episode);
+
+  gom_repository_find_one_async (tvdb_source->priv->repository,
+                                 EPISODE_TYPE_RESOURCE,
+                                 query,
+                                 cache_find_episode_done,
+                                 os);
+  g_object_unref (query);
+  g_clear_pointer (&series_id, g_free);
+  g_clear_pointer (&show, g_free);
+  return;
+
+cache_episode_end:
+  /* This media does not specify an episode: return series metadata */
+  media = thetvdb_update_media_from_resources (GRL_MEDIA_VIDEO (os->media),
+                                               os->keys,
+                                               os->serie_resource,
+                                               NULL);
+  os->callback (os->source, os->operation_id, media, os->user_data, NULL);
+  g_clear_pointer (&series_id, g_free);
+  g_clear_pointer (&show, g_free);
+  free_operation_spec (os);
+}
+
+static void
+cache_find_serie_done (GObject *object,
+                       GAsyncResult *res,
+                       gpointer user_data)
+{
+  OperationSpec *os;
+  GomResource *resource;
+  const gchar *show;
+  GError *err = NULL;
+
+  os = (OperationSpec *) user_data;
+  show = grl_media_video_get_show (GRL_MEDIA_VIDEO (os->media));
+
+  resource = gom_repository_find_one_finish (GOM_REPOSITORY (object),
+                                             res,
+                                             &err);
+  if (resource == NULL) {
+    GRL_DEBUG ("[Series] Cache miss with '%s' due '%s'", show, err->message);
+    g_error_free (err);
+    thetvdb_execute_resolve_web (os);
+    return;
+  }
+
+  os->serie_resource = SERIES_RESOURCE (resource);
+  cache_find_episode (os);
+}
+
+static void
+thetvdb_execute_resolve_cache (OperationSpec *os)
+{
+  const gchar *show;
+  GrlTheTVDBSource *tvdb_source;
+  GomFilter *query;
+  GValue value = { 0, };
+
+  GRL_DEBUG ("thetvdb_resolve_cache");
+
+  tvdb_source = GRL_THETVDB_SOURCE (os->source);
+  show = grl_media_video_get_show (GRL_MEDIA_VIDEO (os->media));
+
+  /* Get series async */
+  g_value_init (&value, G_TYPE_STRING);
+  g_value_set_string (&value, show);
+  query = gom_filter_new_eq (SERIES_TYPE_RESOURCE,
+                             SERIES_COLUMN_SERIES_NAME,
+                             &value);
+  g_value_unset (&value);
+  gom_repository_find_one_async (tvdb_source->priv->repository,
+                                 SERIES_TYPE_RESOURCE,
+                                 query,
+                                 cache_find_serie_done,
+                                 os);
+  g_object_unref (query);
+}
+
+/* ================== API Implementation ================ */
+static void
+grl_thetvdb_source_resolve (GrlSource *source,
+                            GrlSourceResolveSpec *rs)
+{
+  OperationSpec *os = NULL;
+
+  GRL_DEBUG ("thetvdb_resolve");
+
+  os = g_slice_new0 (OperationSpec);
+  os->source = rs->source;
+  os->operation_id = rs->operation_id;
+  os->keys = g_list_copy (rs->keys);
+  os->callback = rs->callback;
+  os->media = rs->media;
+  os->user_data = rs->user_data;
+  os->error_code = GRL_CORE_ERROR_RESOLVE_FAILED;
+  os->lang = get_pref_language (GRL_THETVDB_SOURCE (source));
+  os->fetched_web = FALSE;
+
+  thetvdb_execute_resolve_cache (os);
+}
+
+static gboolean
+grl_thetvdb_source_may_resolve (GrlSource *source,
+                                GrlMedia *media,
+                                GrlKeyID key_id,
+                                GList **missing_keys)
+{
+  GrlTheTVDBSource *tvdb_source = GRL_THETVDB_SOURCE (source);
+
+  GRL_DEBUG ("thetvdb_may_resolve");
+
+  /* Check if this key is supported */
+  if (!g_list_find (tvdb_source->priv->supported_keys,
+                    GRLKEYID_TO_POINTER (key_id)))
+    return FALSE;
+
+  /* Check if resolve type and media type match */
+  if (media && !GRL_IS_MEDIA_VIDEO (media))
+    return FALSE;
+
+  /* Check if the media has a show */
+  if (!media || !grl_data_has_key (GRL_DATA (media), GRL_METADATA_KEY_SHOW)) {
+    if (missing_keys)
+      *missing_keys = grl_metadata_key_list_new (GRL_METADATA_KEY_SHOW, NULL);
+
+    return FALSE;
+  }
+
+  /* For season and episode number, we need the the title of the episode */
+  if ((key_id == GRL_METADATA_KEY_SEASON || key_id == GRL_METADATA_KEY_EPISODE)
+      && !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;
+  }
+
+  /* For the title of the episode, we need season and episode number */
+  if (key_id == GRL_METADATA_KEY_TITLE) {
+    GList *l = NULL;
+    if (!grl_data_has_key (GRL_DATA (media), GRL_METADATA_KEY_SEASON))
+        l = g_list_prepend (l, GRLKEYID_TO_POINTER (GRL_METADATA_KEY_SEASON));
+
+    if (!grl_data_has_key (GRL_DATA (media), GRL_METADATA_KEY_EPISODE))
+        l = g_list_prepend (l, GRLKEYID_TO_POINTER (GRL_METADATA_KEY_EPISODE));
+
+    if (l != NULL) {
+      if (missing_keys)
+        *missing_keys = l;
+
+      return FALSE;
+    }
+  }
+
+  /* For episode's specific metadata, we need the season
+   * and episode number or the title of the episode */
+  if ((key_id == GRL_METADATA_KEY_DIRECTOR
+       || key_id == GRL_THETVDB_METADATA_KEY_GUEST_STARS)
+      && !grl_data_has_key (GRL_DATA (media), GRL_METADATA_KEY_TITLE)) {
+    GList *l = NULL;
+    if (!grl_data_has_key (GRL_DATA (media), GRL_METADATA_KEY_SEASON))
+        l = g_list_prepend (l, GRLKEYID_TO_POINTER (GRL_METADATA_KEY_SEASON));
+
+    if (!grl_data_has_key (GRL_DATA (media), GRL_METADATA_KEY_EPISODE))
+        l = g_list_prepend (l, GRLKEYID_TO_POINTER (GRL_METADATA_KEY_EPISODE));
+
+    if (l != NULL) {
+      if (missing_keys)
+        *missing_keys = l;
+
+      return FALSE;
+    }
+  }
+
+  /* Note: The following keys could be related for both show and episode.
+   * To the may-resolve operation we consider the request of those keys as
+   * for the show because we only need the show name to retrieve that
+   * information.
+   * If the GrlMedia has Title or Episode and Season, we retrieve those keys
+   * for the episode.
+   *
+   * GRL_METADATA_KEY_PUBLICATION_DATE
+   * GRL_METADATA_KEY_DESCRIPTION
+   * GRL_THETVDB_METADATA_KEY_THETVDB_ID
+   * GRL_THETVDB_METADATA_KEY_IMDB_ID
+   */
+
+  return TRUE;
+}
+
+static const GList *
+grl_thetvdb_source_supported_keys (GrlSource *source)
+{
+  GrlTheTVDBSource *tvdb_source = GRL_THETVDB_SOURCE (source);
+
+  return tvdb_source->priv->supported_keys;
+}
diff --git a/src/thetvdb/grl-thetvdb.h b/src/thetvdb/grl-thetvdb.h
new file mode 100644
index 0000000..45a0a1f
--- /dev/null
+++ b/src/thetvdb/grl-thetvdb.h
@@ -0,0 +1,66 @@
+/*
+ * Copyright (C) 2014 Victor Toso.
+ *
+ * Contact: Victor Toso <me victortoso 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_THETVDB_SOURCE_H_
+#define _GRL_THETVDB_SOURCE_H_
+
+#include <grilo.h>
+
+#define GRL_THETVDB_SOURCE_TYPE                         \
+  (grl_thetvdb_source_get_type ())
+
+#define GRL_THETVDB_SOURCE(obj)                        \
+  (G_TYPE_CHECK_INSTANCE_CAST((obj),                   \
+                              GRL_THETVDB_SOURCE_TYPE, \
+                              GrlTheTVDBSource))
+
+#define GRL_THETVDB_SOURCE_CLASS(klass)                \
+  (G_TYPE_CHECK_CLASS_CAST((klass),                    \
+                           GRL_THETVDB_SOURCE_TYPE,    \
+                           GrlTheTVDBSourceClass))
+
+#define GRL_IS_THETVDB_SOURCE_CLASS(klass)             \
+  (G_TYPE_CHECK_CLASS_TYPE((klass),                    \
+                           GRL_THETVDB_SOURCE_TYPE))
+
+#define GRL_THETVDB_SOURCE_GET_CLASS(obj)              \
+  (G_TYPE_INSTANCE_GET_CLASS((obj),                    \
+                             GRL_THETVDB_SOURCE_TYPE,  \
+                             GrlTheTVDBSourceClass))
+
+typedef struct _GrlTheTVDBPrivate GrlTheTVDBPrivate;
+typedef struct _GrlTheTVDBSource  GrlTheTVDBSource;
+
+struct _GrlTheTVDBSource {
+  GrlSource parent;
+  GrlTheTVDBPrivate *priv;
+};
+
+typedef struct _GrlTheTVDBSourceClass GrlTheTVDBSourceClass;
+
+struct _GrlTheTVDBSourceClass {
+  GrlSourceClass parent_class;
+};
+
+GType grl_thetvdb_source_get_type (void);
+
+#endif /* _GRL_THETVDB_SOURCE_H_ */
diff --git a/src/thetvdb/grl-thetvdb.xml b/src/thetvdb/grl-thetvdb.xml
new file mode 100644
index 0000000..fa12fc5
--- /dev/null
+++ b/src/thetvdb/grl-thetvdb.xml
@@ -0,0 +1,10 @@
+<plugin>
+  <info>
+    <name>The TVDB</name>
+    <module>libgrlthetvdb</module>
+    <description>A plugin for fetching metadata of television shows</description>
+    <author>Victor Toso</author>
+    <license>LGPL</license>
+    <site>http://victortoso.com</site>
+  </info>
+</plugin>
diff --git a/src/thetvdb/thetvdb-resources-episodes.c b/src/thetvdb/thetvdb-resources-episodes.c
new file mode 100644
index 0000000..d760a01
--- /dev/null
+++ b/src/thetvdb/thetvdb-resources-episodes.c
@@ -0,0 +1,349 @@
+/*
+ * Copyright (C) 2014 Victor Toso.
+ *
+ * Contact: Victor Toso <me victortoso 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
+ *
+ */
+
+#include "thetvdb-resources.h"
+
+G_DEFINE_TYPE (EpisodeResource, episode_resource, GOM_TYPE_RESOURCE)
+
+struct _EpisodeResourcePrivate {
+  gint64      db_id;
+  gdouble     rating;
+  gchar      *series_id;
+  gchar      *overview;
+  gchar      *language;
+  gchar      *imdb_id;
+  gchar      *first_aired;
+  guint       season_number;
+  guint       episode_number;
+  guint       absolute_number;
+  gchar      *season_id;
+  gchar      *episode_id;
+  gchar      *episode_name;
+  gchar      *url_episode_screen;
+  gchar      *director_names;
+  gchar      *guest_stars_names;
+};
+
+enum {
+  PROP_0,
+  PROP_DB_ID,
+  PROP_LANGUAGE,
+  PROP_SERIES_ID,
+  PROP_OVERVIEW,
+  PROP_IMDB_ID,
+  PROP_FIRST_AIRED,
+  PROP_RATING,
+  PROP_SEASON_NUMBER,
+  PROP_EPISODE_NUMBER,
+  PROP_ABSOLUTE_NUMBER,
+  PROP_SEASON_ID,
+  PROP_EPISODE_ID,
+  PROP_EPISODE_NAME,
+  PROP_URL_EPISODE_SCREEN,
+  PROP_DIRECTOR_NAMES,
+  PROP_GUEST_STARS_NAMES,
+  LAST_PROP
+};
+
+static GParamSpec *specs[LAST_PROP];
+
+static void
+episode_resource_finalize (GObject *object)
+{
+  EpisodeResourcePrivate *priv = EPISODE_RESOURCE(object)->priv;
+
+  g_clear_pointer (&priv->language, g_free);
+  g_clear_pointer (&priv->series_id, g_free);
+  g_clear_pointer (&priv->overview, g_free);
+  g_clear_pointer (&priv->imdb_id, g_free);
+  g_clear_pointer (&priv->first_aired, g_free);
+  g_clear_pointer (&priv->season_id, g_free);
+  g_clear_pointer (&priv->episode_id, g_free);
+  g_clear_pointer (&priv->episode_name, g_free);
+  g_clear_pointer (&priv->url_episode_screen, g_free);
+  g_clear_pointer (&priv->director_names, g_free);
+  g_clear_pointer (&priv->guest_stars_names, g_free);
+
+  G_OBJECT_CLASS(episode_resource_parent_class)->finalize(object);
+}
+
+static void
+episode_resource_get_property (GObject    *object,
+                               guint       prop_id,
+                               GValue     *value,
+                               GParamSpec *pspec)
+{
+  EpisodeResource *resource = EPISODE_RESOURCE(object);
+
+  switch (prop_id) {
+  case PROP_DB_ID:
+    g_value_set_int64 (value, resource->priv->db_id);
+    break;
+  case PROP_LANGUAGE:
+    g_value_set_string (value, resource->priv->language);
+    break;
+  case PROP_SERIES_ID:
+    g_value_set_string (value, resource->priv->series_id);
+    break;
+  case PROP_OVERVIEW:
+    g_value_set_string (value, resource->priv->overview);
+    break;
+  case PROP_IMDB_ID:
+    g_value_set_string (value, resource->priv->imdb_id);
+    break;
+  case PROP_FIRST_AIRED:
+    g_value_set_string (value, resource->priv->first_aired);
+    break;
+  case PROP_RATING:
+    g_value_set_double (value, resource->priv->rating);
+    break;
+  case PROP_SEASON_NUMBER:
+    g_value_set_uint (value, resource->priv->season_number);
+    break;
+  case PROP_EPISODE_NUMBER:
+    g_value_set_uint (value, resource->priv->episode_number);
+    break;
+  case PROP_ABSOLUTE_NUMBER:
+    g_value_set_uint (value, resource->priv->absolute_number);
+    break;
+  case PROP_SEASON_ID:
+    g_value_set_string (value, resource->priv->season_id);
+    break;
+  case PROP_EPISODE_ID:
+    g_value_set_string (value, resource->priv->episode_id);
+    break;
+  case PROP_EPISODE_NAME:
+    g_value_set_string (value, resource->priv->episode_name);
+    break;
+  case PROP_URL_EPISODE_SCREEN:
+    g_value_set_string (value, resource->priv->url_episode_screen);
+    break;
+  case PROP_DIRECTOR_NAMES:
+    g_value_set_string (value, resource->priv->director_names);
+    break;
+  case PROP_GUEST_STARS_NAMES:
+    g_value_set_string (value, resource->priv->guest_stars_names);
+    break;
+  default:
+    G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec);
+  }
+}
+
+static void
+episode_resource_set_property (GObject      *object,
+                               guint         prop_id,
+                               const GValue *value,
+                               GParamSpec   *pspec)
+{
+  EpisodeResource *resource = EPISODE_RESOURCE(object);
+
+  switch (prop_id) {
+  case PROP_DB_ID:
+    resource->priv->db_id = g_value_get_int64 (value);
+    break;
+  case PROP_LANGUAGE:
+    g_clear_pointer (&resource->priv->language, g_free);
+    resource->priv->language = g_value_dup_string (value);
+    break;
+  case PROP_SERIES_ID:
+    g_clear_pointer (&resource->priv->series_id, g_free);
+    resource->priv->series_id = g_value_dup_string (value);
+    break;
+  case PROP_OVERVIEW:
+    g_clear_pointer (&resource->priv->overview, g_free);
+    resource->priv->overview = g_value_dup_string (value);
+    break;
+  case PROP_IMDB_ID:
+    g_clear_pointer (&resource->priv->imdb_id, g_free);
+    resource->priv->imdb_id = g_value_dup_string (value);
+    break;
+  case PROP_FIRST_AIRED:
+    g_clear_pointer (&resource->priv->first_aired, g_free);
+    resource->priv->first_aired = g_value_dup_string (value);
+    break;
+  case PROP_RATING:
+    resource->priv->rating = g_value_get_double (value);
+    break;
+  case PROP_SEASON_NUMBER:
+    resource->priv->season_number = g_value_get_uint (value);
+    break;
+  case PROP_EPISODE_NUMBER:
+    resource->priv->episode_number = g_value_get_uint (value);
+    break;
+  case PROP_ABSOLUTE_NUMBER:
+    resource->priv->absolute_number = g_value_get_uint (value);
+    break;
+  case PROP_SEASON_ID:
+    g_clear_pointer (&resource->priv->season_id, g_free);
+    resource->priv->season_id = g_value_dup_string (value);
+    break;
+  case PROP_EPISODE_ID:
+    g_clear_pointer (&resource->priv->episode_id, g_free);
+    resource->priv->episode_id = g_value_dup_string (value);
+    break;
+  case PROP_EPISODE_NAME:
+    g_clear_pointer (&resource->priv->episode_name, g_free);
+    resource->priv->episode_name = g_value_dup_string (value);
+    break;
+  case PROP_URL_EPISODE_SCREEN:
+    g_clear_pointer (&resource->priv->url_episode_screen, g_free);
+    resource->priv->url_episode_screen = g_value_dup_string (value);
+    break;
+  case PROP_DIRECTOR_NAMES:
+    g_clear_pointer (&resource->priv->director_names, g_free);
+    resource->priv->director_names = g_value_dup_string (value);
+    break;
+  case PROP_GUEST_STARS_NAMES:
+    g_clear_pointer (&resource->priv->guest_stars_names, g_free);
+    resource->priv->guest_stars_names = g_value_dup_string (value);
+    break;
+  default:
+    G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec);
+  }
+}
+
+static void
+episode_resource_class_init (EpisodeResourceClass *klass)
+{
+  GObjectClass *object_class;
+  GomResourceClass *resource_class;
+
+  object_class = G_OBJECT_CLASS(klass);
+  object_class->finalize = episode_resource_finalize;
+  object_class->get_property = episode_resource_get_property;
+  object_class->set_property = episode_resource_set_property;
+  g_type_class_add_private(object_class, sizeof(EpisodeResourcePrivate));
+
+  resource_class = GOM_RESOURCE_CLASS(klass);
+  gom_resource_class_set_table(resource_class, "episodes");
+
+  specs[PROP_DB_ID] = g_param_spec_int64 (EPISODE_COLUMN_ID,
+                                          NULL, NULL,
+                                          0, G_MAXINT64,
+                                          0, G_PARAM_READWRITE);
+  g_object_class_install_property (object_class, PROP_DB_ID,
+                                   specs[PROP_DB_ID]);
+  gom_resource_class_set_primary_key (resource_class, EPISODE_COLUMN_ID);
+
+  specs[PROP_LANGUAGE] = g_param_spec_string (EPISODE_COLUMN_LANGUAGE,
+                                              NULL, NULL, NULL,
+                                              G_PARAM_READWRITE);
+  g_object_class_install_property (object_class, PROP_LANGUAGE,
+                                   specs[PROP_LANGUAGE]);
+
+  specs[PROP_SERIES_ID] = g_param_spec_string (EPISODE_COLUMN_SERIES_ID,
+                                               NULL, NULL, NULL,
+                                               G_PARAM_READWRITE);
+  g_object_class_install_property (object_class, PROP_SERIES_ID,
+                                   specs[PROP_SERIES_ID]);
+
+  specs[PROP_OVERVIEW] = g_param_spec_string (EPISODE_COLUMN_OVERVIEW,
+                                              NULL, NULL, NULL,
+                                              G_PARAM_READWRITE);
+  g_object_class_install_property (object_class, PROP_OVERVIEW,
+                                   specs[PROP_OVERVIEW]);
+
+  specs[PROP_IMDB_ID] = g_param_spec_string (EPISODE_COLUMN_IMDB_ID,
+                                             NULL, NULL, NULL,
+                                             G_PARAM_READWRITE);
+  g_object_class_install_property (object_class, PROP_IMDB_ID,
+                                   specs[PROP_IMDB_ID]);
+
+  specs[PROP_FIRST_AIRED] = g_param_spec_string (EPISODE_COLUMN_FIRST_AIRED,
+                                                 NULL, NULL, NULL,
+                                                 G_PARAM_READWRITE);
+  g_object_class_install_property (object_class, PROP_FIRST_AIRED,
+                                   specs[PROP_FIRST_AIRED]);
+
+  specs[PROP_RATING] = g_param_spec_double (EPISODE_COLUMN_RATING,
+                                            NULL, NULL,
+                                            0, G_MAXDOUBLE,
+                                            0, G_PARAM_READWRITE);
+  g_object_class_install_property (object_class, PROP_RATING,
+                                   specs[PROP_RATING]);
+
+  specs[PROP_SEASON_NUMBER] = g_param_spec_uint (EPISODE_COLUMN_SEASON_NUMBER,
+                                                 NULL, NULL,
+                                                 0, G_MAXUINT,
+                                                 0, G_PARAM_READWRITE);
+  g_object_class_install_property (object_class, PROP_SEASON_NUMBER,
+                                   specs[PROP_SEASON_NUMBER]);
+
+  specs[PROP_EPISODE_NUMBER] = g_param_spec_uint (EPISODE_COLUMN_EPISODE_NUMBER,
+                                                  NULL, NULL,
+                                                  0, G_MAXUINT,
+                                                  0, G_PARAM_READWRITE);
+  g_object_class_install_property (object_class, PROP_EPISODE_NUMBER,
+                                   specs[PROP_EPISODE_NUMBER]);
+
+  specs[PROP_ABSOLUTE_NUMBER] = g_param_spec_uint (EPISODE_COLUMN_ABSOLUTE_NUMBER,
+                                                   NULL, NULL,
+                                                   0, G_MAXUINT,
+                                                   0, G_PARAM_READWRITE);
+  g_object_class_install_property (object_class, PROP_ABSOLUTE_NUMBER,
+                                   specs[PROP_ABSOLUTE_NUMBER]);
+
+  specs[PROP_SEASON_ID] = g_param_spec_string (EPISODE_COLUMN_SEASON_ID,
+                                               NULL, NULL, NULL,
+                                               G_PARAM_READWRITE);
+  g_object_class_install_property (object_class, PROP_SEASON_ID,
+                                   specs[PROP_SEASON_ID]);
+
+  specs[PROP_EPISODE_ID] = g_param_spec_string (EPISODE_COLUMN_EPISODE_ID,
+                                                NULL, NULL, NULL,
+                                                G_PARAM_READWRITE);
+  g_object_class_install_property (object_class, PROP_EPISODE_ID,
+                                   specs[PROP_EPISODE_ID]);
+  gom_resource_class_set_unique (resource_class, EPISODE_COLUMN_EPISODE_ID);
+
+  specs[PROP_EPISODE_NAME] = g_param_spec_string (EPISODE_COLUMN_EPISODE_NAME,
+                                                  NULL, NULL, NULL,
+                                                  G_PARAM_READWRITE);
+  g_object_class_install_property (object_class, PROP_EPISODE_NAME,
+                                   specs[PROP_EPISODE_NAME]);
+
+  specs[PROP_URL_EPISODE_SCREEN] = g_param_spec_string (EPISODE_COLUMN_URL_EPISODE_SCREEN,
+                                                        NULL, NULL, NULL,
+                                                        G_PARAM_READWRITE);
+  g_object_class_install_property (object_class, PROP_URL_EPISODE_SCREEN,
+                                   specs[PROP_URL_EPISODE_SCREEN]);
+
+  specs[PROP_DIRECTOR_NAMES] = g_param_spec_string (EPISODE_COLUMN_DIRECTOR_NAMES,
+                                                    NULL, NULL, NULL,
+                                                    G_PARAM_READWRITE);
+  g_object_class_install_property (object_class, PROP_DIRECTOR_NAMES,
+                                   specs[PROP_DIRECTOR_NAMES]);
+
+  specs[PROP_GUEST_STARS_NAMES] = g_param_spec_string (EPISODE_COLUMN_GUEST_STARS_NAMES,
+                                                       NULL, NULL, NULL,
+                                                       G_PARAM_READWRITE);
+  g_object_class_install_property (object_class, PROP_GUEST_STARS_NAMES,
+                                   specs[PROP_GUEST_STARS_NAMES]);
+}
+
+static void
+episode_resource_init (EpisodeResource *resource)
+{
+  resource->priv = G_TYPE_INSTANCE_GET_PRIVATE(resource,
+                                               EPISODE_TYPE_RESOURCE,
+                                               EpisodeResourcePrivate);
+}
diff --git a/src/thetvdb/thetvdb-resources-series.c b/src/thetvdb/thetvdb-resources-series.c
new file mode 100644
index 0000000..25f9e49
--- /dev/null
+++ b/src/thetvdb/thetvdb-resources-series.c
@@ -0,0 +1,338 @@
+/*
+ * Copyright (C) 2014 Victor Toso.
+ *
+ * Contact: Victor Toso <me victortoso 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
+ *
+ */
+
+#include "thetvdb-resources.h"
+
+G_DEFINE_TYPE (SeriesResource, series_resource, GOM_TYPE_RESOURCE)
+
+struct _SeriesResourcePrivate {
+  gint64      db_id;
+  gdouble     rating;
+  gchar      *series_id;
+  gchar      *overview;
+  gchar      *language;
+  gchar      *imdb_id;
+  gchar      *first_aired;
+  gchar      *series_name;
+  gchar      *status;
+  gchar      *url_banner;
+  gchar      *url_fanart;
+  gchar      *url_poster;
+  gchar      *zap2it_id;
+  gchar      *actor_names;
+  gchar      *alias_names;
+  gchar      *genres;
+};
+
+enum {
+  PROP_0,
+  PROP_DB_ID,
+  PROP_LANGUAGE,
+  PROP_SERIES_NAME,
+  PROP_SERIES_ID,
+  PROP_STATUS,
+  PROP_OVERVIEW,
+  PROP_IMDB_ID,
+  PROP_ZAP2IT_ID,
+  PROP_FIRST_AIRED,
+  PROP_RATING,
+  PROP_ACTOR_NAMES,
+  PROP_GENRES,
+  PROP_URL_BANNER,
+  PROP_URL_FANART,
+  PROP_URL_POSTER,
+  LAST_PROP
+};
+
+static GParamSpec *specs[LAST_PROP];
+
+static void
+series_resource_finalize (GObject *object)
+{
+  SeriesResourcePrivate *priv = SERIES_RESOURCE(object)->priv;
+
+  g_clear_pointer (&priv->language, g_free);
+  g_clear_pointer (&priv->series_name, g_free);
+  g_clear_pointer (&priv->series_id, g_free);
+  g_clear_pointer (&priv->status, g_free);
+  g_clear_pointer (&priv->overview, g_free);
+  g_clear_pointer (&priv->imdb_id, g_free);
+  g_clear_pointer (&priv->zap2it_id, g_free);
+  g_clear_pointer (&priv->first_aired, g_free);
+  g_clear_pointer (&priv->actor_names, g_free);
+  g_clear_pointer (&priv->alias_names, g_free);
+  g_clear_pointer (&priv->genres, g_free);
+  g_clear_pointer (&priv->url_banner, g_free);
+  g_clear_pointer (&priv->url_fanart, g_free);
+  g_clear_pointer (&priv->url_poster, g_free);
+
+  G_OBJECT_CLASS(series_resource_parent_class)->finalize(object);
+}
+
+static void
+series_resource_get_property (GObject    *object,
+                              guint       prop_id,
+                              GValue     *value,
+                              GParamSpec *pspec)
+{
+  SeriesResource *resource = SERIES_RESOURCE(object);
+
+  switch (prop_id) {
+  case PROP_DB_ID:
+    g_value_set_int64 (value, resource->priv->db_id);
+    break;
+  case PROP_LANGUAGE:
+    g_value_set_string (value, resource->priv->language);
+    break;
+  case PROP_SERIES_NAME:
+    g_value_set_string (value, resource->priv->series_name);
+    break;
+  case PROP_SERIES_ID:
+    g_value_set_string (value, resource->priv->series_id);
+    break;
+  case PROP_STATUS:
+    g_value_set_string (value, resource->priv->status);
+    break;
+  case PROP_OVERVIEW:
+    g_value_set_string (value, resource->priv->overview);
+    break;
+  case PROP_IMDB_ID:
+    g_value_set_string (value, resource->priv->imdb_id);
+    break;
+  case PROP_ZAP2IT_ID:
+    g_value_set_string (value, resource->priv->zap2it_id);
+    break;
+  case PROP_FIRST_AIRED:
+    g_value_set_string (value, resource->priv->first_aired);
+    break;
+  case PROP_RATING:
+    g_value_set_double (value, resource->priv->rating);
+    break;
+  case PROP_ACTOR_NAMES:
+    g_value_set_string (value, resource->priv->actor_names);
+    break;
+  case PROP_GENRES:
+    g_value_set_string (value, resource->priv->genres);
+    break;
+  case PROP_URL_BANNER:
+    g_value_set_string (value, resource->priv->url_banner);
+    break;
+  case PROP_URL_FANART:
+    g_value_set_string (value, resource->priv->url_fanart);
+    break;
+  case PROP_URL_POSTER:
+    g_value_set_string (value, resource->priv->url_poster);
+    break;
+  default:
+    G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec);
+  }
+}
+
+static void
+series_resource_set_property (GObject      *object,
+                              guint         prop_id,
+                              const GValue *value,
+                              GParamSpec   *pspec)
+{
+  SeriesResource *resource = SERIES_RESOURCE(object);
+
+  switch (prop_id) {
+  case PROP_DB_ID:
+    resource->priv->db_id = g_value_get_int64 (value);
+    break;
+  case PROP_LANGUAGE:
+    g_clear_pointer (&resource->priv->language, g_free);
+    resource->priv->language = g_value_dup_string (value);
+    break;
+  case PROP_SERIES_NAME:
+    g_clear_pointer (&resource->priv->series_name, g_free);
+    resource->priv->series_name = g_value_dup_string (value);
+    break;
+  case PROP_SERIES_ID:
+    g_clear_pointer (&resource->priv->series_id, g_free);
+    resource->priv->series_id = g_value_dup_string (value);
+    break;
+  case PROP_STATUS:
+    g_clear_pointer (&resource->priv->status, g_free);
+    resource->priv->status = g_value_dup_string (value);
+    break;
+  case PROP_OVERVIEW:
+    g_clear_pointer (&resource->priv->overview, g_free);
+    resource->priv->overview = g_value_dup_string (value);
+    break;
+  case PROP_IMDB_ID:
+    g_clear_pointer (&resource->priv->imdb_id, g_free);
+    resource->priv->imdb_id = g_value_dup_string (value);
+    break;
+  case PROP_ZAP2IT_ID:
+    g_clear_pointer (&resource->priv->zap2it_id, g_free);
+    resource->priv->zap2it_id = g_value_dup_string (value);
+    break;
+  case PROP_FIRST_AIRED:
+    g_clear_pointer (&resource->priv->first_aired, g_free);
+    resource->priv->first_aired = g_value_dup_string (value);
+    break;
+  case PROP_RATING:
+    resource->priv->rating = g_value_get_double (value);
+    break;
+  case PROP_ACTOR_NAMES:
+    g_clear_pointer (&resource->priv->actor_names, g_free);
+    resource->priv->actor_names = g_value_dup_string (value);
+    break;
+  case PROP_GENRES:
+    g_clear_pointer (&resource->priv->genres, g_free);
+    resource->priv->genres = g_value_dup_string (value);
+    break;
+  case PROP_URL_BANNER:
+    g_clear_pointer (&resource->priv->url_banner, g_free);
+    resource->priv->url_banner = g_value_dup_string (value);
+    break;
+  case PROP_URL_FANART:
+    g_clear_pointer (&resource->priv->url_fanart, g_free);
+    resource->priv->url_fanart = g_value_dup_string (value);
+    break;
+  case PROP_URL_POSTER:
+    g_clear_pointer (&resource->priv->url_poster, g_free);
+    resource->priv->url_poster = g_value_dup_string (value);
+    break;
+  default:
+    G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec);
+  }
+}
+
+static void
+series_resource_class_init (SeriesResourceClass *klass)
+{
+  GObjectClass *object_class;
+  GomResourceClass *resource_class;
+
+  object_class = G_OBJECT_CLASS(klass);
+  object_class->finalize = series_resource_finalize;
+  object_class->get_property = series_resource_get_property;
+  object_class->set_property = series_resource_set_property;
+  g_type_class_add_private(object_class, sizeof(SeriesResourcePrivate));
+
+  resource_class = GOM_RESOURCE_CLASS(klass);
+  gom_resource_class_set_table(resource_class, "series");
+
+  specs[PROP_DB_ID] = g_param_spec_int64 (SERIES_COLUMN_ID,
+                                          NULL, NULL,
+                                          0, G_MAXINT64,
+                                          0, G_PARAM_READWRITE);
+  g_object_class_install_property (object_class, PROP_DB_ID,
+                                   specs[PROP_DB_ID]);
+  gom_resource_class_set_primary_key (resource_class, SERIES_COLUMN_ID);
+
+  specs[PROP_LANGUAGE] = g_param_spec_string (SERIES_COLUMN_LANGUAGE,
+                                              NULL, NULL, NULL,
+                                              G_PARAM_READWRITE);
+  g_object_class_install_property (object_class, PROP_LANGUAGE,
+                                   specs[PROP_LANGUAGE]);
+
+  specs[PROP_SERIES_NAME] = g_param_spec_string (SERIES_COLUMN_SERIES_NAME,
+                                                 NULL, NULL, NULL,
+                                                 G_PARAM_READWRITE);
+  g_object_class_install_property (object_class, PROP_SERIES_NAME,
+                                   specs[PROP_SERIES_NAME]);
+
+  specs[PROP_SERIES_ID] = g_param_spec_string (SERIES_COLUMN_SERIES_ID,
+                                               NULL, NULL, NULL,
+                                               G_PARAM_READWRITE);
+  g_object_class_install_property (object_class, PROP_SERIES_ID,
+                                   specs[PROP_SERIES_ID]);
+  gom_resource_class_set_unique (resource_class, SERIES_COLUMN_SERIES_ID);
+
+  specs[PROP_STATUS] = g_param_spec_string (SERIES_COLUMN_STATUS,
+                                            NULL, NULL, NULL,
+                                            G_PARAM_READWRITE);
+  g_object_class_install_property (object_class, PROP_STATUS,
+                                   specs[PROP_STATUS]);
+
+  specs[PROP_OVERVIEW] = g_param_spec_string (SERIES_COLUMN_OVERVIEW,
+                                              NULL, NULL, NULL,
+                                              G_PARAM_READWRITE);
+  g_object_class_install_property (object_class, PROP_OVERVIEW,
+                                   specs[PROP_OVERVIEW]);
+
+  specs[PROP_IMDB_ID] = g_param_spec_string (SERIES_COLUMN_IMDB_ID,
+                                              NULL, NULL, NULL,
+                                              G_PARAM_READWRITE);
+  g_object_class_install_property (object_class, PROP_IMDB_ID,
+                                   specs[PROP_IMDB_ID]);
+
+  specs[PROP_ZAP2IT_ID] = g_param_spec_string (SERIES_COLUMN_ZAP2IT_ID,
+                                               NULL, NULL, NULL,
+                                               G_PARAM_READWRITE);
+  g_object_class_install_property (object_class, PROP_ZAP2IT_ID,
+                                   specs[PROP_ZAP2IT_ID]);
+
+  specs[PROP_FIRST_AIRED] = g_param_spec_string (SERIES_COLUMN_FIRST_AIRED,
+                                                 NULL, NULL, NULL,
+                                                 G_PARAM_READWRITE);
+  g_object_class_install_property (object_class, PROP_FIRST_AIRED,
+                                   specs[PROP_FIRST_AIRED]);
+
+  specs[PROP_RATING] = g_param_spec_double (SERIES_COLUMN_RATING,
+                                            NULL, NULL,
+                                            0, G_MAXDOUBLE,
+                                            0, G_PARAM_READWRITE);
+  g_object_class_install_property (object_class, PROP_RATING,
+                                   specs[PROP_RATING]);
+
+  specs[PROP_ACTOR_NAMES] = g_param_spec_string (SERIES_COLUMN_ACTOR_NAMES,
+                                                 NULL, NULL, NULL,
+                                                 G_PARAM_READWRITE);
+  g_object_class_install_property (object_class, PROP_ACTOR_NAMES,
+                                   specs[PROP_ACTOR_NAMES]);
+
+  specs[PROP_GENRES] = g_param_spec_string (SERIES_COLUMN_GENRES,
+                                            NULL, NULL, NULL,
+                                            G_PARAM_READWRITE);
+  g_object_class_install_property (object_class, PROP_GENRES,
+                                   specs[PROP_GENRES]);
+
+  specs[PROP_URL_BANNER] = g_param_spec_string (SERIES_COLUMN_URL_BANNER,
+                                                NULL, NULL, NULL,
+                                                G_PARAM_READWRITE);
+  g_object_class_install_property (object_class, PROP_URL_BANNER,
+                                   specs[PROP_URL_BANNER]);
+
+  specs[PROP_URL_FANART] = g_param_spec_string (SERIES_COLUMN_URL_FANART,
+                                                NULL, NULL, NULL,
+                                                G_PARAM_READWRITE);
+  g_object_class_install_property (object_class, PROP_URL_FANART,
+                                   specs[PROP_URL_FANART]);
+
+  specs[PROP_URL_POSTER] = g_param_spec_string (SERIES_COLUMN_URL_POSTER,
+                                                NULL, NULL, NULL,
+                                                G_PARAM_READWRITE);
+  g_object_class_install_property (object_class, PROP_URL_POSTER,
+                                   specs[PROP_URL_POSTER]);
+}
+
+static void
+series_resource_init (SeriesResource *resource)
+{
+  resource->priv = G_TYPE_INSTANCE_GET_PRIVATE(resource,
+                                               SERIES_TYPE_RESOURCE,
+                                               SeriesResourcePrivate);
+}
diff --git a/src/thetvdb/thetvdb-resources.h b/src/thetvdb/thetvdb-resources.h
new file mode 100644
index 0000000..81d2eb3
--- /dev/null
+++ b/src/thetvdb/thetvdb-resources.h
@@ -0,0 +1,155 @@
+/*
+ * Copyright (C) 2014 Victor Toso.
+ *
+ * Contact: Victor Toso <me victortoso 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_THETVDB_RESOURCEs_H_
+#define _GRL_THETVDB_RESOURCEs_H_
+
+#include <gom/gom.h>
+
+/*----- Series ----- */
+#define SERIES_TYPE_RESOURCE   \
+  (series_resource_get_type())
+
+#define SERIES_TYPE_TYPE  \
+  (series_type_get_type())
+
+#define SERIES_RESOURCE(obj)                         \
+  (G_TYPE_CHECK_INSTANCE_CAST ((obj),                \
+                               SERIES_TYPE_RESOURCE, \
+                               SeriesResource))
+
+#define SERIES_RESOURCE_CLASS(klass)              \
+  (G_TYPE_CHECK_CLASS_CAST ((klass),              \
+                            SERIES_TYPE_RESOURCE, \
+                            SeriesResourceClass))
+
+#define SERIES_IS_RESOURCE(obj)                       \
+  (G_TYPE_CHECK_INSTANCE_TYPE ((obj),                 \
+                               SERIES_TYPE_RESOURCE))
+
+#define SERIES_IS_RESOURCE_CLASS(klass)             \
+  (G_TYPE_CHECK_CLASS_TYPE ((klass),                \
+                            SERIES_TYPE_RESOURCE))
+
+#define SERIES_RESOURCE_GET_CLASS(obj)              \
+  (G_TYPE_INSTANCE_GET_CLASS ((obj),                \
+                              SERIES_TYPE_RESOURCE, \
+                              SeriesResourceClass))
+
+#define SERIES_COLUMN_ID            "id"
+#define SERIES_COLUMN_LANGUAGE      "language"
+#define SERIES_COLUMN_SERIES_NAME   "series-name"
+#define SERIES_COLUMN_SERIES_ID     "series-id"
+#define SERIES_COLUMN_STATUS        "status"
+#define SERIES_COLUMN_OVERVIEW      "overview"
+#define SERIES_COLUMN_IMDB_ID       "imdb-id"
+#define SERIES_COLUMN_ZAP2IT_ID     "zap2it-id"
+#define SERIES_COLUMN_FIRST_AIRED   "first-aired"
+#define SERIES_COLUMN_RATING        "rating"
+#define SERIES_COLUMN_ACTOR_NAMES   "actor-names"
+#define SERIES_COLUMN_GENRES        "genres"
+#define SERIES_COLUMN_URL_BANNER    "url-banner"
+#define SERIES_COLUMN_URL_FANART    "url-fanart"
+#define SERIES_COLUMN_URL_POSTER    "url-poster"
+
+typedef struct _SeriesResource        SeriesResource;
+typedef struct _SeriesResourceClass   SeriesResourceClass;
+typedef struct _SeriesResourcePrivate SeriesResourcePrivate;
+
+struct _SeriesResource
+{
+   GomResource parent;
+   SeriesResourcePrivate *priv;
+};
+
+struct _SeriesResourceClass
+{
+   GomResourceClass parent_class;
+};
+
+GType series_resource_get_type (void);
+
+/*----- Episodes ----- */
+#define EPISODE_TYPE_RESOURCE   \
+  (episode_resource_get_type())
+
+#define EPISODE_TYPE_TYPE  \
+  (episode_type_get_type())
+
+#define EPISODE_RESOURCE(obj)                         \
+  (G_TYPE_CHECK_INSTANCE_CAST ((obj),                 \
+                               EPISODE_TYPE_RESOURCE, \
+                               EpisodeResource))
+
+#define EPISODE_RESOURCE_CLASS(klass)              \
+  (G_TYPE_CHECK_CLASS_CAST ((klass),               \
+                            EPISODE_TYPE_RESOURCE, \
+                            EpisodeResourceClass))
+
+#define EPISODE_IS_RESOURCE(obj)                       \
+  (G_TYPE_CHECK_INSTANCE_TYPE ((obj),                  \
+                               EPISODE_TYPE_RESOURCE))
+
+#define EPISODE_IS_RESOURCE_CLASS(klass)             \
+  (G_TYPE_CHECK_CLASS_TYPE ((klass),                 \
+                            EPISODE_TYPE_RESOURCE))
+
+#define EPISODE_RESOURCE_GET_CLASS(obj)              \
+  (G_TYPE_INSTANCE_GET_CLASS ((obj),                 \
+                              EPISODE_TYPE_RESOURCE, \
+                              EpisodeResourceClass))
+
+#define EPISODE_COLUMN_ID                 "id"
+#define EPISODE_COLUMN_LANGUAGE           "language"
+#define EPISODE_COLUMN_SERIES_ID          "series-id"
+#define EPISODE_COLUMN_OVERVIEW           "overview"
+#define EPISODE_COLUMN_IMDB_ID            "imdb-id"
+#define EPISODE_COLUMN_FIRST_AIRED        "first-aired"
+#define EPISODE_COLUMN_RATING             "rating"
+#define EPISODE_COLUMN_SEASON_NUMBER      "season-number"
+#define EPISODE_COLUMN_EPISODE_NUMBER     "episode-number"
+#define EPISODE_COLUMN_ABSOLUTE_NUMBER    "absolute-number"
+#define EPISODE_COLUMN_SEASON_ID          "season-id"
+#define EPISODE_COLUMN_EPISODE_ID         "episode-id"
+#define EPISODE_COLUMN_EPISODE_NAME       "episode-name"
+#define EPISODE_COLUMN_URL_EPISODE_SCREEN "url-episode-screen"
+#define EPISODE_COLUMN_DIRECTOR_NAMES     "director-names"
+#define EPISODE_COLUMN_GUEST_STARS_NAMES  "guest-stars-names"
+
+typedef struct _EpisodeResource        EpisodeResource;
+typedef struct _EpisodeResourceClass   EpisodeResourceClass;
+typedef struct _EpisodeResourcePrivate EpisodeResourcePrivate;
+
+struct _EpisodeResource
+{
+   GomResource parent;
+   EpisodeResourcePrivate *priv;
+};
+
+struct _EpisodeResourceClass
+{
+   GomResourceClass parent_class;
+};
+
+GType episode_resource_get_type (void);
+
+#endif /* _GRL_THETVDB_RESOURCES_H_ */



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