[PATCH 1/3] split src/ content into src/media and src/metadata



---
 src/apple-trailers/Makefile.am                     |   38 -
 src/apple-trailers/grl-apple-trailers.c            |  611 --------
 src/apple-trailers/grl-apple-trailers.h            |   78 -
 src/apple-trailers/grl-apple-trailers.xml          |    9 -
 src/bookmarks/Makefile.am                          |   34 -
 src/bookmarks/grl-bookmarks.c                      |  888 -----------
 src/bookmarks/grl-bookmarks.h                      |   75 -
 src/bookmarks/grl-bookmarks.xml                    |    9 -
 src/fake-metadata/Makefile.am                      |   32 -
 src/fake-metadata/grl-fake-metadata.c              |  231 ---
 src/fake-metadata/grl-fake-metadata.h              |   72 -
 src/fake-metadata/grl-fake-metadata.xml            |    9 -
 src/filesystem/Makefile.am                         |   34 -
 src/filesystem/TODO                                |    2 -
 src/filesystem/grl-filesystem.c                    | 1343 ----------------
 src/filesystem/grl-filesystem.h                    |   81 -
 src/filesystem/grl-filesystem.xml                  |    9 -
 src/flickr/Makefile.am                             |   40 -
 src/flickr/gflickr.c                               | 1196 --------------
 src/flickr/gflickr.h                               |  155 --
 src/flickr/grl-flickr.c                            |  755 ---------
 src/flickr/grl-flickr.h                            |   80 -
 src/flickr/grl-flickr.xml                          |    9 -
 src/gravatar/Makefile.am                           |   32 -
 src/gravatar/grl-gravatar.c                        |  301 ----
 src/gravatar/grl-gravatar.h                        |   74 -
 src/gravatar/grl-gravatar.xml                      |    9 -
 src/jamendo/Makefile.am                            |   38 -
 src/jamendo/TODO                                   |   30 -
 src/jamendo/grl-jamendo.c                          | 1365 ----------------
 src/jamendo/grl-jamendo.h                          |   78 -
 src/jamendo/grl-jamendo.xml                        |    9 -
 src/lastfm-albumart/Makefile.am                    |   36 -
 src/lastfm-albumart/grl-lastfm-albumart.c          |  316 ----
 src/lastfm-albumart/grl-lastfm-albumart.h          |   74 -
 src/lastfm-albumart/grl-lastfm-albumart.xml        |    9 -
 src/local-metadata/Makefile.am                     |   34 -
 src/local-metadata/grl-local-metadata.c            |  262 ----
 src/local-metadata/grl-local-metadata.h            |   72 -
 src/local-metadata/grl-local-metadata.xml          |    9 -
 src/media/apple-trailers/Makefile.am               |   38 +
 src/media/apple-trailers/grl-apple-trailers.c      |  611 ++++++++
 src/media/apple-trailers/grl-apple-trailers.h      |   78 +
 src/media/apple-trailers/grl-apple-trailers.xml    |    9 +
 src/media/bookmarks/Makefile.am                    |   34 +
 src/media/bookmarks/grl-bookmarks.c                |  888 +++++++++++
 src/media/bookmarks/grl-bookmarks.h                |   75 +
 src/media/bookmarks/grl-bookmarks.xml              |    9 +
 src/media/filesystem/Makefile.am                   |   34 +
 src/media/filesystem/TODO                          |    2 +
 src/media/filesystem/grl-filesystem.c              | 1343 ++++++++++++++++
 src/media/filesystem/grl-filesystem.h              |   81 +
 src/media/filesystem/grl-filesystem.xml            |    9 +
 src/media/flickr/Makefile.am                       |   40 +
 src/media/flickr/gflickr.c                         | 1196 ++++++++++++++
 src/media/flickr/gflickr.h                         |  155 ++
 src/media/flickr/grl-flickr.c                      |  755 +++++++++
 src/media/flickr/grl-flickr.h                      |   80 +
 src/media/flickr/grl-flickr.xml                    |    9 +
 src/media/jamendo/Makefile.am                      |   38 +
 src/media/jamendo/TODO                             |   30 +
 src/media/jamendo/grl-jamendo.c                    | 1365 ++++++++++++++++
 src/media/jamendo/grl-jamendo.h                    |   78 +
 src/media/jamendo/grl-jamendo.xml                  |    9 +
 src/media/podcasts/Makefile.am                     |   38 +
 src/media/podcasts/TODO                            |    5 +
 src/media/podcasts/grl-podcasts.c                  | 1636 ++++++++++++++++++++
 src/media/podcasts/grl-podcasts.h                  |   75 +
 src/media/podcasts/grl-podcasts.xml                |    9 +
 src/media/shoutcast/Makefile.am                    |   37 +
 src/media/shoutcast/grl-shoutcast.c                |  727 +++++++++
 src/media/shoutcast/grl-shoutcast.h                |   74 +
 src/media/shoutcast/grl-shoutcast.xml              |    9 +
 src/media/tracker/Makefile.am                      |   36 +
 src/media/tracker/grl-tracker.c                    | 1511 ++++++++++++++++++
 src/media/tracker/grl-tracker.h                    |   78 +
 src/media/tracker/grl-tracker.xml                  |    9 +
 src/media/upnp/Makefile.am                         |   46 +
 src/media/upnp/grl-upnp.c                          | 1356 ++++++++++++++++
 src/media/upnp/grl-upnp.h                          |   75 +
 src/media/upnp/grl-upnp.xml                        |    9 +
 src/media/vimeo/Makefile.am                        |   42 +
 src/media/vimeo/grl-vimeo.c                        |  413 +++++
 src/media/vimeo/grl-vimeo.h                        |   77 +
 src/media/vimeo/grl-vimeo.xml                      |    9 +
 src/media/vimeo/gvimeo.c                           |  517 ++++++
 src/media/vimeo/gvimeo.h                           |  112 ++
 src/media/youtube/Makefile.am                      |   40 +
 src/media/youtube/TODO                             |    6 +
 src/media/youtube/grl-youtube.c                    | 1630 +++++++++++++++++++
 src/media/youtube/grl-youtube.h                    |   74 +
 src/media/youtube/grl-youtube.xml                  |    9 +
 src/metadata-store/Makefile.am                     |   34 -
 src/metadata-store/grl-metadata-store.c            |  650 --------
 src/metadata-store/grl-metadata-store.h            |   75 -
 src/metadata-store/grl-metadata-store.xml          |    9 -
 src/metadata/fake-metadata/Makefile.am             |   32 +
 src/metadata/fake-metadata/grl-fake-metadata.c     |  231 +++
 src/metadata/fake-metadata/grl-fake-metadata.h     |   72 +
 src/metadata/fake-metadata/grl-fake-metadata.xml   |    9 +
 src/metadata/gravatar/Makefile.am                  |   32 +
 src/metadata/gravatar/grl-gravatar.c               |  301 ++++
 src/metadata/gravatar/grl-gravatar.h               |   74 +
 src/metadata/gravatar/grl-gravatar.xml             |    9 +
 src/metadata/lastfm-albumart/Makefile.am           |   36 +
 src/metadata/lastfm-albumart/grl-lastfm-albumart.c |  316 ++++
 src/metadata/lastfm-albumart/grl-lastfm-albumart.h |   74 +
 .../lastfm-albumart/grl-lastfm-albumart.xml        |    9 +
 src/metadata/local-metadata/Makefile.am            |   34 +
 src/metadata/local-metadata/grl-local-metadata.c   |  262 ++++
 src/metadata/local-metadata/grl-local-metadata.h   |   72 +
 src/metadata/local-metadata/grl-local-metadata.xml |    9 +
 src/metadata/metadata-store/Makefile.am            |   34 +
 src/metadata/metadata-store/grl-metadata-store.c   |  650 ++++++++
 src/metadata/metadata-store/grl-metadata-store.h   |   75 +
 src/metadata/metadata-store/grl-metadata-store.xml |    9 +
 src/podcasts/Makefile.am                           |   38 -
 src/podcasts/TODO                                  |    5 -
 src/podcasts/grl-podcasts.c                        | 1636 --------------------
 src/podcasts/grl-podcasts.h                        |   75 -
 src/podcasts/grl-podcasts.xml                      |    9 -
 src/shoutcast/Makefile.am                          |   37 -
 src/shoutcast/grl-shoutcast.c                      |  727 ---------
 src/shoutcast/grl-shoutcast.h                      |   74 -
 src/shoutcast/grl-shoutcast.xml                    |    9 -
 src/tracker/Makefile.am                            |   36 -
 src/tracker/grl-tracker.c                          | 1511 ------------------
 src/tracker/grl-tracker.h                          |   78 -
 src/tracker/grl-tracker.xml                        |    9 -
 src/upnp/Makefile.am                               |   46 -
 src/upnp/grl-upnp.c                                | 1356 ----------------
 src/upnp/grl-upnp.h                                |   75 -
 src/upnp/grl-upnp.xml                              |    9 -
 src/vimeo/Makefile.am                              |   42 -
 src/vimeo/grl-vimeo.c                              |  413 -----
 src/vimeo/grl-vimeo.h                              |   77 -
 src/vimeo/grl-vimeo.xml                            |    9 -
 src/vimeo/gvimeo.c                                 |  517 ------
 src/vimeo/gvimeo.h                                 |  112 --
 src/youtube/Makefile.am                            |   40 -
 src/youtube/TODO                                   |    6 -
 src/youtube/grl-youtube.c                          | 1630 -------------------
 src/youtube/grl-youtube.h                          |   74 -
 src/youtube/grl-youtube.xml                        |    9 -
 144 files changed, 17965 insertions(+), 17965 deletions(-)
 delete mode 100644 src/apple-trailers/Makefile.am
 delete mode 100644 src/apple-trailers/grl-apple-trailers.c
 delete mode 100644 src/apple-trailers/grl-apple-trailers.h
 delete mode 100644 src/apple-trailers/grl-apple-trailers.xml
 delete mode 100644 src/bookmarks/Makefile.am
 delete mode 100644 src/bookmarks/grl-bookmarks.c
 delete mode 100644 src/bookmarks/grl-bookmarks.h
 delete mode 100644 src/bookmarks/grl-bookmarks.xml
 delete mode 100644 src/fake-metadata/Makefile.am
 delete mode 100644 src/fake-metadata/grl-fake-metadata.c
 delete mode 100644 src/fake-metadata/grl-fake-metadata.h
 delete mode 100644 src/fake-metadata/grl-fake-metadata.xml
 delete mode 100644 src/filesystem/Makefile.am
 delete mode 100644 src/filesystem/TODO
 delete mode 100644 src/filesystem/grl-filesystem.c
 delete mode 100644 src/filesystem/grl-filesystem.h
 delete mode 100644 src/filesystem/grl-filesystem.xml
 delete mode 100644 src/flickr/Makefile.am
 delete mode 100644 src/flickr/gflickr.c
 delete mode 100644 src/flickr/gflickr.h
 delete mode 100644 src/flickr/grl-flickr.c
 delete mode 100644 src/flickr/grl-flickr.h
 delete mode 100644 src/flickr/grl-flickr.xml
 delete mode 100644 src/gravatar/Makefile.am
 delete mode 100644 src/gravatar/grl-gravatar.c
 delete mode 100644 src/gravatar/grl-gravatar.h
 delete mode 100644 src/gravatar/grl-gravatar.xml
 delete mode 100644 src/jamendo/Makefile.am
 delete mode 100644 src/jamendo/TODO
 delete mode 100644 src/jamendo/grl-jamendo.c
 delete mode 100644 src/jamendo/grl-jamendo.h
 delete mode 100644 src/jamendo/grl-jamendo.xml
 delete mode 100644 src/lastfm-albumart/Makefile.am
 delete mode 100644 src/lastfm-albumart/grl-lastfm-albumart.c
 delete mode 100644 src/lastfm-albumart/grl-lastfm-albumart.h
 delete mode 100644 src/lastfm-albumart/grl-lastfm-albumart.xml
 delete mode 100644 src/local-metadata/Makefile.am
 delete mode 100644 src/local-metadata/grl-local-metadata.c
 delete mode 100644 src/local-metadata/grl-local-metadata.h
 delete mode 100644 src/local-metadata/grl-local-metadata.xml
 create mode 100644 src/media/apple-trailers/Makefile.am
 create mode 100644 src/media/apple-trailers/grl-apple-trailers.c
 create mode 100644 src/media/apple-trailers/grl-apple-trailers.h
 create mode 100644 src/media/apple-trailers/grl-apple-trailers.xml
 create mode 100644 src/media/bookmarks/Makefile.am
 create mode 100644 src/media/bookmarks/grl-bookmarks.c
 create mode 100644 src/media/bookmarks/grl-bookmarks.h
 create mode 100644 src/media/bookmarks/grl-bookmarks.xml
 create mode 100644 src/media/filesystem/Makefile.am
 create mode 100644 src/media/filesystem/TODO
 create mode 100644 src/media/filesystem/grl-filesystem.c
 create mode 100644 src/media/filesystem/grl-filesystem.h
 create mode 100644 src/media/filesystem/grl-filesystem.xml
 create mode 100644 src/media/flickr/Makefile.am
 create mode 100644 src/media/flickr/gflickr.c
 create mode 100644 src/media/flickr/gflickr.h
 create mode 100644 src/media/flickr/grl-flickr.c
 create mode 100644 src/media/flickr/grl-flickr.h
 create mode 100644 src/media/flickr/grl-flickr.xml
 create mode 100644 src/media/jamendo/Makefile.am
 create mode 100644 src/media/jamendo/TODO
 create mode 100644 src/media/jamendo/grl-jamendo.c
 create mode 100644 src/media/jamendo/grl-jamendo.h
 create mode 100644 src/media/jamendo/grl-jamendo.xml
 create mode 100644 src/media/podcasts/Makefile.am
 create mode 100644 src/media/podcasts/TODO
 create mode 100644 src/media/podcasts/grl-podcasts.c
 create mode 100644 src/media/podcasts/grl-podcasts.h
 create mode 100644 src/media/podcasts/grl-podcasts.xml
 create mode 100644 src/media/shoutcast/Makefile.am
 create mode 100644 src/media/shoutcast/grl-shoutcast.c
 create mode 100644 src/media/shoutcast/grl-shoutcast.h
 create mode 100644 src/media/shoutcast/grl-shoutcast.xml
 create mode 100644 src/media/tracker/Makefile.am
 create mode 100644 src/media/tracker/grl-tracker.c
 create mode 100644 src/media/tracker/grl-tracker.h
 create mode 100644 src/media/tracker/grl-tracker.xml
 create mode 100644 src/media/upnp/Makefile.am
 create mode 100644 src/media/upnp/grl-upnp.c
 create mode 100644 src/media/upnp/grl-upnp.h
 create mode 100644 src/media/upnp/grl-upnp.xml
 create mode 100644 src/media/vimeo/Makefile.am
 create mode 100644 src/media/vimeo/grl-vimeo.c
 create mode 100644 src/media/vimeo/grl-vimeo.h
 create mode 100644 src/media/vimeo/grl-vimeo.xml
 create mode 100644 src/media/vimeo/gvimeo.c
 create mode 100644 src/media/vimeo/gvimeo.h
 create mode 100644 src/media/youtube/Makefile.am
 create mode 100644 src/media/youtube/TODO
 create mode 100644 src/media/youtube/grl-youtube.c
 create mode 100644 src/media/youtube/grl-youtube.h
 create mode 100644 src/media/youtube/grl-youtube.xml
 delete mode 100644 src/metadata-store/Makefile.am
 delete mode 100644 src/metadata-store/grl-metadata-store.c
 delete mode 100644 src/metadata-store/grl-metadata-store.h
 delete mode 100644 src/metadata-store/grl-metadata-store.xml
 create mode 100644 src/metadata/fake-metadata/Makefile.am
 create mode 100644 src/metadata/fake-metadata/grl-fake-metadata.c
 create mode 100644 src/metadata/fake-metadata/grl-fake-metadata.h
 create mode 100644 src/metadata/fake-metadata/grl-fake-metadata.xml
 create mode 100644 src/metadata/gravatar/Makefile.am
 create mode 100644 src/metadata/gravatar/grl-gravatar.c
 create mode 100644 src/metadata/gravatar/grl-gravatar.h
 create mode 100644 src/metadata/gravatar/grl-gravatar.xml
 create mode 100644 src/metadata/lastfm-albumart/Makefile.am
 create mode 100644 src/metadata/lastfm-albumart/grl-lastfm-albumart.c
 create mode 100644 src/metadata/lastfm-albumart/grl-lastfm-albumart.h
 create mode 100644 src/metadata/lastfm-albumart/grl-lastfm-albumart.xml
 create mode 100644 src/metadata/local-metadata/Makefile.am
 create mode 100644 src/metadata/local-metadata/grl-local-metadata.c
 create mode 100644 src/metadata/local-metadata/grl-local-metadata.h
 create mode 100644 src/metadata/local-metadata/grl-local-metadata.xml
 create mode 100644 src/metadata/metadata-store/Makefile.am
 create mode 100644 src/metadata/metadata-store/grl-metadata-store.c
 create mode 100644 src/metadata/metadata-store/grl-metadata-store.h
 create mode 100644 src/metadata/metadata-store/grl-metadata-store.xml
 delete mode 100644 src/podcasts/Makefile.am
 delete mode 100644 src/podcasts/TODO
 delete mode 100644 src/podcasts/grl-podcasts.c
 delete mode 100644 src/podcasts/grl-podcasts.h
 delete mode 100644 src/podcasts/grl-podcasts.xml
 delete mode 100644 src/shoutcast/Makefile.am
 delete mode 100644 src/shoutcast/grl-shoutcast.c
 delete mode 100644 src/shoutcast/grl-shoutcast.h
 delete mode 100644 src/shoutcast/grl-shoutcast.xml
 delete mode 100644 src/tracker/Makefile.am
 delete mode 100644 src/tracker/grl-tracker.c
 delete mode 100644 src/tracker/grl-tracker.h
 delete mode 100644 src/tracker/grl-tracker.xml
 delete mode 100644 src/upnp/Makefile.am
 delete mode 100644 src/upnp/grl-upnp.c
 delete mode 100644 src/upnp/grl-upnp.h
 delete mode 100644 src/upnp/grl-upnp.xml
 delete mode 100644 src/vimeo/Makefile.am
 delete mode 100644 src/vimeo/grl-vimeo.c
 delete mode 100644 src/vimeo/grl-vimeo.h
 delete mode 100644 src/vimeo/grl-vimeo.xml
 delete mode 100644 src/vimeo/gvimeo.c
 delete mode 100644 src/vimeo/gvimeo.h
 delete mode 100644 src/youtube/Makefile.am
 delete mode 100644 src/youtube/TODO
 delete mode 100644 src/youtube/grl-youtube.c
 delete mode 100644 src/youtube/grl-youtube.h
 delete mode 100644 src/youtube/grl-youtube.xml

diff --git a/src/apple-trailers/Makefile.am b/src/apple-trailers/Makefile.am
deleted file mode 100644
index 0195de5..0000000
--- a/src/apple-trailers/Makefile.am
+++ /dev/null
@@ -1,38 +0,0 @@
-#
-# Makefile.am
-#
-# Author: Juan A. Suarez Romero <jasuarez igalia com>
-#
-# Copyright (C) 2010, 2011 Igalia S.L. All rights reserved.
-
-lib_LTLIBRARIES		 = libgrlappletrailers.la
-
-libgrlappletrailers_la_CFLAGS =	\
-	$(DEPS_CFLAGS)		\
-	$(GRLNET_CFLAGS)	\
-	$(XML_CFLAGS)
-
-libgrlappletrailers_la_LIBADD =	\
-	$(DEPS_LIBS)		\
-	$(GRLNET_LIBS)		\
-	$(XML_LIBS)
-
-libgrlappletrailers_la_LDFLAGS = \
-	-module			 \
-	-avoid-version
-
-libgrlappletrailers_la_SOURCES =	\
-	grl-apple-trailers.c		\
-	grl-apple-trailers.h
-
-libdir = $(GRL_PLUGINS_DIR)
-appletrailersxmldir	= $(GRL_PLUGINS_CONF_DIR)
-appletrailersxml_DATA	= $(APPLE_TRAILERS_PLUGIN_ID).xml
-
-EXTRA_DIST = $(appletrailersxml_DATA)
-
-MAINTAINERCLEANFILES =	\
-	*.in		\
-	*~
-
-DISTCLEANFILES = $(MAINTAINERCLEANFILES)
diff --git a/src/apple-trailers/grl-apple-trailers.c b/src/apple-trailers/grl-apple-trailers.c
deleted file mode 100644
index b81748b..0000000
--- a/src/apple-trailers/grl-apple-trailers.c
+++ /dev/null
@@ -1,611 +0,0 @@
-/*
- * Copyright (C) 2010 Igalia S.L.
- * Copyright (C) 2011 Intel Corporation.
- *
- * Contact: Iago Toral Quiroga <itoral igalia com>
- *
- * Authors: Juan A. Suarez Romero <jasuarez igalia 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 <grilo.h>
-#include <net/grl-net.h>
-#include <libxml/xpath.h>
-
-#include "grl-apple-trailers.h"
-
-/* --------- Logging  -------- */
-
-#define GRL_LOG_DOMAIN_DEFAULT apple_trailers_log_domain
-GRL_LOG_DOMAIN_STATIC(apple_trailers_log_domain);
-
-/* ---- Apple Trailers Service ---- */
-
-#define APPLE_TRAILERS_CURRENT_SD                               \
-  "http://trailers.apple.com/trailers/home/xml/current_480p.xml";
-
-#define APPLE_TRAILERS_CURRENT_HD                               \
-  "http://trailers.apple.com/trailers/home/xml/current_720p.xml";
-
-/* --- Plugin information --- */
-
-#define PLUGIN_ID   APPLE_TRAILERS_PLUGIN_ID
-
-#define SOURCE_ID   "grl-apple-trailers"
-#define SOURCE_NAME "Apple Movie Trailers"
-#define SOURCE_DESC "A plugin for browsing Apple Movie Trailers"
-
-#define AUTHOR      "Igalia S.L."
-#define LICENSE     "LGPL"
-#define SITE        "http://www.igalia.com";
-
-typedef struct {
-  GrlMediaSourceBrowseSpec *bs;
-  xmlDocPtr xml_doc;
-  xmlNodePtr xml_entries;
-  gboolean cancelled;
-} OperationData;
-
-enum {
-  PROP_0,
-  PROP_HD,
-  PROP_LARGE_POSTER,
-};
-
-struct _GrlAppleTrailersSourcePriv {
-  GrlNetWc *wc;
-  GCancellable *cancellable;
-  gboolean hd;
-  gboolean large_poster;
-};
-
-#define GRL_APPLE_TRAILERS_SOURCE_GET_PRIVATE(object)		\
-  (G_TYPE_INSTANCE_GET_PRIVATE((object),                        \
-                               GRL_APPLE_TRAILERS_SOURCE_TYPE,  \
-                               GrlAppleTrailersSourcePriv))
-
-static GrlAppleTrailersSource *grl_apple_trailers_source_new (gboolean hd,
-                                                              gboolean xlarge);
-
-gboolean grl_apple_trailers_plugin_init (GrlPluginRegistry *registry,
-                                         const GrlPluginInfo *plugin,
-                                         GList *configs);
-
-static const GList *grl_apple_trailers_source_supported_keys (GrlMetadataSource *source);
-
-static void grl_apple_trailers_source_browse (GrlMediaSource *source,
-                                              GrlMediaSourceBrowseSpec *bs);
-
-static void grl_apple_trailers_source_cancel (GrlMediaSource *source,
-                                              guint operation_id);
-
-/* =================== Apple Trailers Plugin  =============== */
-
-gboolean
-grl_apple_trailers_plugin_init (GrlPluginRegistry *registry,
-                                const GrlPluginInfo *plugin,
-                                GList *configs)
-{
-  GrlAppleTrailersSource *source;
-  gboolean hd = FALSE;
-  gboolean xlarge = FALSE;
-
-  GRL_LOG_DOMAIN_INIT (apple_trailers_log_domain, "apple-trailers");
-
-  GRL_DEBUG ("apple_trailers_plugin_init");
-
-  for (; configs; configs = g_list_next (configs)) {
-    GrlConfig *config;
-    gchar *definition, *poster_size;
-
-    config = GRL_CONFIG (configs->data);
-    definition = grl_config_get_string (config, "definition");
-    if (definition) {
-      if (*definition != '\0') {
-        if (g_str_equal (definition, "hd")) {
-          hd = TRUE;
-        }
-      }
-      g_free (definition);
-    }
-
-    poster_size = grl_config_get_string (config, "poster-size");
-    if (poster_size) {
-      if (*poster_size != '\0') {
-        if (g_str_equal (poster_size, "xlarge")) {
-          xlarge = TRUE;
-        }
-      }
-      g_free (poster_size);
-    }
-  }
-
-  source = grl_apple_trailers_source_new (hd, xlarge);
-  grl_plugin_registry_register_source (registry,
-                                       plugin,
-                                       GRL_MEDIA_PLUGIN (source),
-                                       NULL);
-  return TRUE;
-}
-
-GRL_PLUGIN_REGISTER (grl_apple_trailers_plugin_init,
-                     NULL,
-                     PLUGIN_ID);
-
-/* ================== AppleTrailers GObject ================ */
-
-static GrlAppleTrailersSource *
-grl_apple_trailers_source_new (gboolean high_definition,
-                               gboolean xlarge)
-{
-  GrlAppleTrailersSource *source;
-
-  GRL_DEBUG ("grl_apple_trailers_source_new%s%s",
-             high_definition ? " (HD)" : "",
-             xlarge ? " (X-large poster)" : "");
-  source = g_object_new (GRL_APPLE_TRAILERS_SOURCE_TYPE,
-                         "source-id", SOURCE_ID,
-                         "source-name", SOURCE_NAME,
-                         "source-desc", SOURCE_DESC,
-                         "high-definition", high_definition,
-			 "large-poster", xlarge,
-                         NULL);
-
-  return source;
-}
-
-G_DEFINE_TYPE (GrlAppleTrailersSource, grl_apple_trailers_source, GRL_TYPE_MEDIA_SOURCE);
-
-static void
-grl_apple_trailers_source_finalize (GObject *object)
-{
-  GrlAppleTrailersSource *self;
-
-  self = GRL_APPLE_TRAILERS_SOURCE (object);
-
-  if (self->priv->wc)
-    g_object_unref (self->priv->wc);
-
-  if (self->priv->cancellable
-      && G_IS_CANCELLABLE (self->priv->cancellable))
-    g_object_unref (self->priv->cancellable);
-
-  G_OBJECT_CLASS (grl_apple_trailers_source_parent_class)->finalize (object);
-}
-
-static void
-grl_apple_trailers_source_set_property (GObject *object,
-                                        guint propid,
-                                        const GValue *value,
-                                        GParamSpec *pspec)
-{
-  GrlAppleTrailersSource *self;
-  self = GRL_APPLE_TRAILERS_SOURCE (object);
-
-  switch (propid) {
-    case PROP_HD:
-      self->priv->hd = g_value_get_boolean (value);
-      break;
-    case PROP_LARGE_POSTER:
-      self->priv->large_poster = g_value_get_boolean (value);
-      break;
-    default:
-      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, propid, pspec);
-    }
-}
-
-static void
-grl_apple_trailers_source_class_init (GrlAppleTrailersSourceClass * klass)
-{
-  GrlMediaSourceClass *source_class = GRL_MEDIA_SOURCE_CLASS (klass);
-  GrlMetadataSourceClass *metadata_class = GRL_METADATA_SOURCE_CLASS (klass);
-  GObjectClass *g_class = G_OBJECT_CLASS (klass);
-  source_class->browse = grl_apple_trailers_source_browse;
-  source_class->cancel = grl_apple_trailers_source_cancel;
-  metadata_class->supported_keys = grl_apple_trailers_source_supported_keys;
-  g_class->finalize = grl_apple_trailers_source_finalize;
-  g_class->set_property = grl_apple_trailers_source_set_property;
-
-  g_object_class_install_property (g_class,
-                                   PROP_HD,
-                                   g_param_spec_boolean ("high-definition",
-                                                         "hd",
-                                                         "Hi/Low definition videos",
-                                                         TRUE,
-                                                         G_PARAM_WRITABLE
-                                                         | G_PARAM_CONSTRUCT_ONLY
-                                                         | G_PARAM_STATIC_NAME));
-
-  g_object_class_install_property (g_class,
-                                   PROP_LARGE_POSTER,
-                                   g_param_spec_boolean ("large-poster",
-                                                         "xlarge",
-                                                         "Pick large poster",
-                                                         TRUE,
-                                                         G_PARAM_WRITABLE
-                                                         | G_PARAM_CONSTRUCT_ONLY
-                                                         | G_PARAM_STATIC_NAME));
-
-  g_type_class_add_private (klass, sizeof (GrlAppleTrailersSourcePriv));
-}
-
-static void
-grl_apple_trailers_source_init (GrlAppleTrailersSource *source)
-{
-  source->priv = GRL_APPLE_TRAILERS_SOURCE_GET_PRIVATE (source);
-  source->priv->hd = TRUE;
-}
-
-/* ==================== Private ==================== */
-
-static gchar *
-get_node_value (xmlNodePtr node, const gchar *node_id)
-{
-  gchar *value;
-  xmlXPathContextPtr xpath_ctx;
-  xmlXPathObjectPtr xpath_res;
-
-  xpath_ctx = xmlXPathNewContext (node->doc);
-  if (!xpath_ctx) {
-    return NULL;
-  }
-
-  xpath_res = xmlXPathEvalExpression ((xmlChar *) node_id, xpath_ctx);
-  if (!xpath_res) {
-    xmlXPathFreeContext (xpath_ctx);
-    return NULL;
-  }
-
-  if (xpath_res->nodesetval->nodeTab) {
-    value =
-      (gchar *) xmlNodeListGetString (node->doc,
-                                      xpath_res->nodesetval->nodeTab[0]->xmlChildrenNode,
-                                      1);
-  } else {
-    value = NULL;
-  }
-
-  xmlXPathFreeObject (xpath_res);
-  xmlXPathFreeContext (xpath_ctx);
-
-  return value;
-}
-
-static gint
-runtime_to_seconds (const gchar *runtime)
-{
-  gchar **items;
-  gint seconds;
-
-  if (!runtime) {
-    return 0;
-  }
-
-  seconds = 0;
-  items = g_strsplit (runtime, ":", -1);
-  if (items && items[0] && items[1])
-    seconds = 3600 * atoi (items[0]) + 60 * atoi (items[1]);
-  g_strfreev (items);
-
-  return seconds;
-}
-static GrlMedia *
-build_media_from_movie (xmlNodePtr node, gboolean xlarge)
-{
-  GrlMedia * media;
-  gchar *movie_author;
-  gchar *movie_date;
-  gchar *movie_description;
-  gchar *movie_duration;
-  gchar *movie_genre;
-  gchar *movie_id;
-  gchar *movie_thumbnail;
-  gchar *movie_title;
-  gchar *movie_url;
-  gchar *movie_rating;
-  gchar *movie_studio;
-  gchar *movie_copyright;
-
-  media = grl_media_video_new ();
-
-  movie_id = (gchar *) xmlGetProp (node, (const xmlChar *) "id");
-
-  /* HACK: as get_node_value applies xpath expression from root node, but we
-     want to do from current node, dup the node and mark it as root node */
-
-  xmlDocPtr xml_doc = xmlNewDoc ((const xmlChar *) "1.0");
-  xmlNodePtr node_dup = xmlCopyNode (node, 1);
-  xmlDocSetRootElement (xml_doc, node_dup);
-  movie_author = get_node_value (node_dup, "/movieinfo/info/director");
-  movie_date = get_node_value (node_dup, "/movieinfo/info/releasedate");
-  movie_description = get_node_value (node_dup, "/movieinfo/info/description");
-  movie_duration = get_node_value (node_dup, "/movieinfo/info/runtime");
-  movie_title = get_node_value (node_dup, "/movieinfo/info/title");
-  movie_genre = get_node_value (node_dup, "/movieinfo/genre/name");
-  if (xlarge)
-    movie_thumbnail = get_node_value (node_dup, "/movieinfo/poster/xlarge");
-  else
-    movie_thumbnail = get_node_value (node_dup, "/movieinfo/poster/location");
-  movie_url = get_node_value (node_dup, "/movieinfo/preview/large");
-  movie_rating = get_node_value (node_dup, "/movieinfo/info/rating");
-  movie_studio = get_node_value (node_dup, "/movieinfo/info/studio");
-  movie_copyright = get_node_value (node_dup, "/movieinfo/info/copyright");
-  xmlFreeDoc (xml_doc);
-
-  grl_media_set_id (media, movie_id);
-  grl_media_set_author (media, movie_author);
-  grl_media_set_date (media, movie_date);
-  grl_media_set_description (media, movie_description);
-  grl_media_set_duration (media, runtime_to_seconds (movie_duration));
-  grl_media_set_title (media, movie_title);
-  grl_data_set_string (GRL_DATA (media),
-                       GRL_METADATA_KEY_GENRE,
-                       movie_genre);
-  grl_media_set_thumbnail (media, movie_thumbnail);
-  grl_media_set_url (media, movie_url);
-  grl_media_set_certificate (media, movie_rating);
-  grl_media_set_studio (media, movie_studio);
-
-  /* FIXME: Translation */
-  grl_media_set_license (media, movie_copyright);
-
-  g_free (movie_id);
-  g_free (movie_author);
-  g_free (movie_date);
-  g_free (movie_description);
-  g_free (movie_duration);
-  g_free (movie_title);
-  g_free (movie_genre);
-  g_free (movie_thumbnail);
-  g_free (movie_url);
-  g_free (movie_rating);
-  g_free (movie_studio);
-  g_free (movie_copyright);
-
-  return media;
-}
-
-static gboolean
-send_movie_info (OperationData *op_data)
-{
-  GrlMedia *media;
-  gboolean last = FALSE;
-
-  if (op_data->cancelled) {
-    op_data->bs->callback (op_data->bs->source,
-                           op_data->bs->browse_id,
-                           NULL,
-                           0,
-                           op_data->bs->user_data,
-                           NULL);
-    last = TRUE;
-  } else {
-    GrlAppleTrailersSource *source =
-      GRL_APPLE_TRAILERS_SOURCE (op_data->bs->source);
-
-    media = build_media_from_movie (op_data->xml_entries,
-                                    source->priv->large_poster);
-    last =
-      !op_data->xml_entries->next  ||
-      op_data->bs->count == 1;
-
-    op_data->bs->callback (op_data->bs->source,
-                           op_data->bs->browse_id,
-                           media,
-                           last? 0: -1,
-                           op_data->bs->user_data,
-                           NULL);
-    op_data->xml_entries = op_data->xml_entries->next;
-    if (!last)
-      op_data->bs->count--;
-  }
-
-  if (last) {
-    xmlFreeDoc (op_data->xml_doc);
-    g_slice_free (OperationData, op_data);
-  }
-
-  return !last;
-}
-
-static void
-xml_parse_result (const gchar *str, OperationData *op_data)
-{
-  GError *error = NULL;
-  xmlNodePtr node;
-
-  if (op_data->cancelled || op_data->bs->count == 0) {
-    goto finalize;
-  }
-
-  op_data->xml_doc = xmlReadMemory (str, xmlStrlen ((xmlChar*) str), NULL, NULL,
-                                    XML_PARSE_RECOVER | XML_PARSE_NOBLANKS);
-  if (!op_data->xml_doc) {
-    error = g_error_new (GRL_CORE_ERROR,
-                         GRL_CORE_ERROR_BROWSE_FAILED,
-                         "Failed to parse response");
-    goto finalize;
-  }
-
-  node = xmlDocGetRootElement (op_data->xml_doc);
-  if (!node || !node->xmlChildrenNode) {
-    error = g_error_new (GRL_CORE_ERROR,
-                         GRL_CORE_ERROR_BROWSE_FAILED,
-                         "Empty response from Apple Trailers");
-    goto finalize;
-  }
-
-  node = node->xmlChildrenNode;
-
-  /* Skip elements */
-  while (node && op_data->bs->skip > 0) {
-    node = node->next;
-    op_data->bs->skip--;
-  }
-
-  if (!node) {
-    goto finalize;
-  } else {
-    op_data->xml_entries = node;
-    g_idle_add ((GSourceFunc) send_movie_info, op_data);
-  }
-
-  return;
-
- finalize:
-  op_data->bs->callback (op_data->bs->source,
-                         op_data->bs->browse_id,
-                         NULL,
-                         0,
-                         op_data->bs->user_data,
-                         error);
-
-  if (op_data->xml_doc) {
-    xmlFreeDoc (op_data->xml_doc);
-  }
-
-  if (error) {
-    g_error_free (error);
-  }
-
-  g_slice_free (OperationData, op_data);
-}
-
-static void
-read_done_cb (GObject *source_object,
-              GAsyncResult *res,
-              gpointer user_data)
-{
-  GError *error = NULL;
-  GError *wc_error = NULL;
-  OperationData *op_data = (OperationData *) user_data;
-  gchar *content = NULL;
-
-  if (!grl_net_wc_request_finish (GRL_NET_WC (source_object),
-                              res,
-                              &content,
-                              NULL,
-                              &wc_error)) {
-    error = g_error_new (GRL_CORE_ERROR,
-                         GRL_CORE_ERROR_BROWSE_FAILED,
-                         "Failed to connect Apple Trailers: '%s'",
-                         wc_error->message);
-    op_data->bs->callback (op_data->bs->source,
-                           op_data->bs->browse_id,
-                           NULL,
-                           0,
-                           op_data->bs->user_data,
-                           error);
-    g_error_free (wc_error);
-    g_error_free (error);
-    g_slice_free (OperationData, op_data);
-
-    return;
-  }
-
-  xml_parse_result (content, op_data);
-}
-
-static void
-read_url_async (GrlAppleTrailersSource *source,
-                const gchar *url,
-                gpointer user_data)
-{
-  if (!source->priv->wc)
-    source->priv->wc = grl_net_wc_new ();
-
-  source->priv->cancellable = g_cancellable_new ();
-
-  GRL_DEBUG ("Opening '%s'", url);
-  grl_net_wc_request_async (source->priv->wc,
-                        url,
-                        source->priv->cancellable,
-                        read_done_cb,
-                        user_data);
-}
-
-/* ================== API Implementation ================ */
-
-static const GList *
-grl_apple_trailers_source_supported_keys (GrlMetadataSource *source)
-{
-  static GList *keys = NULL;
-  if (!keys) {
-    keys = grl_metadata_key_list_new (GRL_METADATA_KEY_AUTHOR,
-                                      GRL_METADATA_KEY_DATE,
-                                      GRL_METADATA_KEY_DESCRIPTION,
-                                      GRL_METADATA_KEY_DURATION,
-                                      GRL_METADATA_KEY_GENRE,
-                                      GRL_METADATA_KEY_ID,
-                                      GRL_METADATA_KEY_THUMBNAIL,
-                                      GRL_METADATA_KEY_TITLE,
-                                      GRL_METADATA_KEY_URL,
-                                      GRL_METADATA_KEY_CERTIFICATE,
-                                      GRL_METADATA_KEY_STUDIO,
-                                      GRL_METADATA_KEY_LICENSE,
-                                      NULL);
-  }
-  return keys;
-}
-
-static void
-grl_apple_trailers_source_browse (GrlMediaSource *source,
-                                  GrlMediaSourceBrowseSpec *bs)
-{
-  GrlAppleTrailersSource *at_source = GRL_APPLE_TRAILERS_SOURCE (source);
-  OperationData *op_data;
-
-  GRL_DEBUG ("grl_apple_trailers_source_browse");
-
-  op_data = g_slice_new0 (OperationData);
-  op_data->bs = bs;
-  grl_media_source_set_operation_data (source, bs->browse_id, op_data);
-
-  if (at_source->priv->hd) {
-    read_url_async (at_source, APPLE_TRAILERS_CURRENT_HD, op_data);
-  } else {
-    read_url_async (at_source, APPLE_TRAILERS_CURRENT_SD, op_data);
-  }
-}
-
-static void
-grl_apple_trailers_source_cancel (GrlMediaSource *source, guint operation_id)
-{
-  OperationData *op_data;
-  GrlAppleTrailersSourcePriv *priv;
-
-  GRL_DEBUG ("grl_apple_trailers_source_cancel");
-
-  priv = GRL_APPLE_TRAILERS_SOURCE_GET_PRIVATE (source);
-  if (priv->cancellable && G_IS_CANCELLABLE (priv->cancellable))
-    g_cancellable_cancel (priv->cancellable);
-  priv->cancellable = NULL;
-
-  if (priv->wc)
-    grl_net_wc_flush_delayed_requests (priv->wc);
-
-  op_data = (OperationData *) grl_media_source_get_operation_data (source, operation_id);
-
-  if (op_data) {
-    op_data->cancelled = TRUE;
-  }
-}
diff --git a/src/apple-trailers/grl-apple-trailers.h b/src/apple-trailers/grl-apple-trailers.h
deleted file mode 100644
index 74f8f9a..0000000
--- a/src/apple-trailers/grl-apple-trailers.h
+++ /dev/null
@@ -1,78 +0,0 @@
-/*
- * Copyright (C) 2010 Igalia S.L.
- *
- * Contact: Iago Toral Quiroga <itoral igalia com>
- *
- * Authors: Juan A. Suarez Romero <jasuarez igalia 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_APPLE_TRAILERS_SOURCE_H_
-#define _GRL_APPLE_TRAILERS_SOURCE_H_
-
-#include <grilo.h>
-
-#define GRL_APPLE_TRAILERS_SOURCE_TYPE                 \
-  (grl_apple_trailers_source_get_type ())
-
-#define GRL_APPLE_TRAILERS_SOURCE(obj)                         \
-  (G_TYPE_CHECK_INSTANCE_CAST ((obj),                   \
-                               GRL_APPLE_TRAILERS_SOURCE_TYPE, \
-                               GrlAppleTrailersSource))
-
-#define GRL_IS_APPLE_TRAILERS_SOURCE(obj)                              \
-  (G_TYPE_CHECK_INSTANCE_TYPE ((obj),                           \
-                               GRL_APPLE_TRAILERS_SOURCE_TYPE))
-
-#define GRL_APPLE_TRAILERS_SOURCE_CLASS(klass)                 \
-  (G_TYPE_CHECK_CLASS_CAST((klass),                     \
-                           GRL_APPLE_TRAILERS_SOURCE_TYPE,     \
-                           GrlAppleTrailersSourceClass))
-
-#define GRL_IS_APPLE_TRAILERS_SOURCE_CLASS(klass)              \
-  (G_TYPE_CHECK_CLASS_TYPE((klass),                     \
-                           GRL_APPLE_TRAILERS_SOURCE_TYPE))
-
-#define GRL_APPLE_TRAILERS_SOURCE_GET_CLASS(obj)               \
-  (G_TYPE_INSTANCE_GET_CLASS ((obj),                    \
-                              GRL_APPLE_TRAILERS_SOURCE_TYPE,  \
-                              GrlAppleTrailersSourceClass))
-
-typedef struct _GrlAppleTrailersSource GrlAppleTrailersSource;
-typedef struct _GrlAppleTrailersSourcePriv GrlAppleTrailersSourcePriv;
-
-struct _GrlAppleTrailersSource {
-
-  GrlMediaSource parent;
-
-  /*< private >*/
-  GrlAppleTrailersSourcePriv *priv;
-
-};
-
-typedef struct _GrlAppleTrailersSourceClass GrlAppleTrailersSourceClass;
-
-struct _GrlAppleTrailersSourceClass {
-
-  GrlMediaSourceClass parent_class;
-
-};
-
-GType grl_apple_trailers_source_get_type (void);
-
-#endif /* _GRL_APPLE_TRAILERS_SOURCE_H_ */
diff --git a/src/apple-trailers/grl-apple-trailers.xml b/src/apple-trailers/grl-apple-trailers.xml
deleted file mode 100644
index 9fd929a..0000000
--- a/src/apple-trailers/grl-apple-trailers.xml
+++ /dev/null
@@ -1,9 +0,0 @@
-<plugin>
-  <info>
-    <name>Apple Movie Trailers</name>
-    <description>A plugin for browsing Apple Movie Trailers</description>
-    <author>Igalia S.L.</author>
-    <license>LGPL</license>
-    <site>http://www.igalia.com</site>
-  </info>
-</plugin>
diff --git a/src/bookmarks/Makefile.am b/src/bookmarks/Makefile.am
deleted file mode 100644
index 0a031f5..0000000
--- a/src/bookmarks/Makefile.am
+++ /dev/null
@@ -1,34 +0,0 @@
-#
-# Makefile.am
-#
-# Author: Iago Toral Quiroga <itoral igalia com>
-#
-# Copyright (C) 2010, 2011 Igalia S.L. All rights reserved.
-
-lib_LTLIBRARIES = libgrlbookmarks.la
-
-libgrlbookmarks_la_CFLAGS =	\
-	$(DEPS_CFLAGS)		\
-	$(SQLITE_CFLAGS)
-
-libgrlbookmarks_la_LIBADD =	\
-	$(DEPS_LIBS)		\
-	$(SQLITE_LIBS)
-
-libgrlbookmarks_la_LDFLAGS = \
-	-module		     \
-	-avoid-version
-
-libgrlbookmarks_la_SOURCES = grl-bookmarks.c grl-bookmarks.h
-
-libdir=$(GRL_PLUGINS_DIR)
-bookmarksxmldir		= $(GRL_PLUGINS_CONF_DIR)
-bookmarksxml_DATA	= $(BOOKMARKS_PLUGIN_ID).xml
-
-EXTRA_DIST = $(bookmarksxml_DATA)
-
-MAINTAINERCLEANFILES =	\
-	*.in		\
-	*~
-
-DISTCLEANFILES = $(MAINTAINERCLEANFILES)
diff --git a/src/bookmarks/grl-bookmarks.c b/src/bookmarks/grl-bookmarks.c
deleted file mode 100644
index 1c7de10..0000000
--- a/src/bookmarks/grl-bookmarks.c
+++ /dev/null
@@ -1,888 +0,0 @@
-/*
- * Copyright (C) 2010 Igalia S.L.
- *
- * Contact: Iago Toral Quiroga <itoral igalia 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 <grilo.h>
-#include <sqlite3.h>
-#include <string.h>
-#include <stdlib.h>
-
-#include "grl-bookmarks.h"
-
-#define GRL_BOOKMARKS_GET_PRIVATE(object)			 \
-  (G_TYPE_INSTANCE_GET_PRIVATE((object),			 \
-                               GRL_BOOKMARKS_SOURCE_TYPE,        \
-                               GrlBookmarksPrivate))
-
-#define GRL_ROOT_TITLE "Bookmarks"
-
-/* --------- Logging  -------- */
-
-#define GRL_LOG_DOMAIN_DEFAULT bookmarks_log_domain
-GRL_LOG_DOMAIN_STATIC(bookmarks_log_domain);
-
-/* --- Database --- */
-
-#define GRL_SQL_DB        ".grl-bookmarks"
-
-#define GRL_SQL_CREATE_TABLE_BOOKMARKS			 \
-  "CREATE TABLE IF NOT EXISTS bookmarks ("		 \
-  "id     INTEGER PRIMARY KEY AUTOINCREMENT,"		 \
-  "parent INTEGER REFERENCES bookmarks (id),"		 \
-  "type   INTEGER,"					 \
-  "url    TEXT,"					 \
-  "title  TEXT,"					 \
-  "date   TEXT,"					 \
-  "mime   TEXT,"					 \
-  "desc   TEXT)"
-
-#define GRL_SQL_GET_BOOKMARKS_BY_PARENT			\
-  "SELECT b1.*, count(b2.parent <> '') "		\
-  "FROM bookmarks b1 LEFT OUTER JOIN bookmarks b2 "	\
-  "  ON b1.id = b2.parent "				\
-  "WHERE b1.parent='%s' "				\
-  "GROUP BY b1.id "					\
-  "LIMIT %u OFFSET %u"
-
-#define GRL_SQL_GET_BOOKMARK_BY_ID			\
-  "SELECT b1.*, count(b2.parent <> '') "		\
-  "FROM bookmarks b1 LEFT OUTER JOIN bookmarks b2 "	\
-  "  ON b1.id = b2.parent "				\
-  "WHERE b1.id='%s' "					\
-  "GROUP BY b1.id "					\
-  "LIMIT 1"
-
-#define GRL_SQL_STORE_BOOKMARK				  \
-  "INSERT INTO bookmarks "				  \
-  "(parent, type, url, title, date, mime, desc) "	  \
-  "VALUES (?, ?, ?, ?, ?, ?, ?)"
-
-#define GRL_SQL_REMOVE_BOOKMARK			\
-  "DELETE FROM bookmarks "			\
-  "WHERE id='%s' or parent='%s'"
-
-#define GRL_SQL_REMOVE_ORPHAN			\
-  "DELETE FROM bookmarks "			\
-  "WHERE id in ( "				\
-  "  SELECT DISTINCT id FROM bookmarks "	\
-  "  WHERE parent NOT IN ( "			\
-  "    SELECT DISTINCT id FROM bookmarks) "	\
-  "  and parent <> 0)"
-
-#define GRL_SQL_GET_BOOKMARKS_BY_TEXT				\
-  "SELECT b1.*, count(b2.parent <> '') "			\
-  "FROM bookmarks b1 LEFT OUTER JOIN bookmarks b2 "		\
-  "  ON b1.id = b2.parent "					\
-  "WHERE (b1.title LIKE '%%%s%%' OR b1.desc LIKE '%%%s%%') "	\
-  "  AND b1.type = 1 "                                          \
-  "GROUP BY b1.id "						\
-  "LIMIT %u OFFSET %u"
-
-#define GRL_SQL_GET_BOOKMARKS_BY_QUERY				\
-  "SELECT b1.*, count(b2.parent <> '') "			\
-  "FROM bookmarks b1 LEFT OUTER JOIN bookmarks b2 "		\
-  "  ON b1.id = b2.parent "					\
-  "WHERE %s "							\
-  "GROUP BY b1.id "						\
-  "LIMIT %u OFFSET %u"
-
-/* --- Plugin information --- */
-
-#define PLUGIN_ID   BOOKMARKS_PLUGIN_ID
-
-#define SOURCE_ID   "grl-bookmarks"
-#define SOURCE_NAME "Bookmarks"
-#define SOURCE_DESC "A source for organizing media bookmarks"
-
-#define AUTHOR      "Igalia S.L."
-#define LICENSE     "LGPL"
-#define SITE        "http://www.igalia.com";
-
-
-enum {
-  BOOKMARK_TYPE_CATEGORY = 0,
-  BOOKMARK_TYPE_STREAM,
-};
-
-enum {
-  BOOKMARK_ID = 0,
-  BOOKMARK_PARENT,
-  BOOKMARK_TYPE,
-  BOOKMARK_URL,
-  BOOKMARK_TITLE,
-  BOOKMARK_DATE,
-  BOOKMARK_MIME,
-  BOOKMARK_DESC,
-  BOOKMARK_CHILDCOUNT
-};
-
-struct _GrlBookmarksPrivate {
-  sqlite3 *db;
-  gboolean notify_changes;
-};
-
-typedef struct {
-  GrlMediaSource *source;
-  guint operation_id;
-  const gchar *media_id;
-  guint skip;
-  guint count;
-  GrlMediaSourceResultCb callback;
-  guint error_code;
-  gboolean is_query;
-  gpointer user_data;
-} OperationSpec;
-
-static GrlBookmarksSource *grl_bookmarks_source_new (void);
-
-static void grl_bookmarks_source_finalize (GObject *plugin);
-
-static const GList *grl_bookmarks_source_supported_keys (GrlMetadataSource *source);
-static GrlSupportedOps grl_bookmarks_source_supported_operations (GrlMetadataSource *metadata_source);
-
-static void grl_bookmarks_source_search (GrlMediaSource *source,
-					 GrlMediaSourceSearchSpec *ss);
-static void grl_bookmarks_source_query (GrlMediaSource *source,
-					GrlMediaSourceQuerySpec *qs);
-static void grl_bookmarks_source_browse (GrlMediaSource *source,
-                                        GrlMediaSourceBrowseSpec *bs);
-static void grl_bookmarks_source_metadata (GrlMediaSource *source,
-					   GrlMediaSourceMetadataSpec *ms);
-static void grl_bookmarks_source_store (GrlMediaSource *source,
-                                       GrlMediaSourceStoreSpec *ss);
-static void grl_bookmarks_source_remove (GrlMediaSource *source,
-					 GrlMediaSourceRemoveSpec *rs);
-
-static gboolean grl_bookmarks_source_notify_change_start (GrlMediaSource *source,
-                                                          GError **error);
-
-static gboolean grl_bookmarks_source_notify_change_stop (GrlMediaSource *source,
-                                                         GError **error);
-
- /* =================== Bookmarks Plugin  =============== */
-
- static gboolean
- grl_bookmarks_plugin_init (GrlPluginRegistry *registry,
-                            const GrlPluginInfo *plugin,
-                            GList *configs)
- {
-   GRL_LOG_DOMAIN_INIT (bookmarks_log_domain, "bookmarks");
-
-   GRL_DEBUG ("grl_bookmarks_plugin_init");
-
-   GrlBookmarksSource *source = grl_bookmarks_source_new ();
-   grl_plugin_registry_register_source (registry,
-                                        plugin,
-                                        GRL_MEDIA_PLUGIN (source),
-                                        NULL);
-   return TRUE;
- }
-
- GRL_PLUGIN_REGISTER (grl_bookmarks_plugin_init,
-                      NULL,
-                      PLUGIN_ID);
-
- /* ================== Bookmarks GObject ================ */
-
- static GrlBookmarksSource *
- grl_bookmarks_source_new (void)
- {
-   GRL_DEBUG ("grl_bookmarks_source_new");
-   return g_object_new (GRL_BOOKMARKS_SOURCE_TYPE,
-                        "source-id", SOURCE_ID,
-                        "source-name", SOURCE_NAME,
-                        "source-desc", SOURCE_DESC,
-                        NULL);
- }
-
- static void
- grl_bookmarks_source_class_init (GrlBookmarksSourceClass * klass)
- {
-   GObjectClass *gobject_class = G_OBJECT_CLASS (klass);
-   GrlMediaSourceClass *source_class = GRL_MEDIA_SOURCE_CLASS (klass);
-   GrlMetadataSourceClass *metadata_class = GRL_METADATA_SOURCE_CLASS (klass);
-
-   gobject_class->finalize = grl_bookmarks_source_finalize;
-
-   metadata_class->supported_operations =
-     grl_bookmarks_source_supported_operations;
-
-   source_class->browse = grl_bookmarks_source_browse;
-   source_class->search = grl_bookmarks_source_search;
-   source_class->query = grl_bookmarks_source_query;
-   source_class->store = grl_bookmarks_source_store;
-   source_class->remove = grl_bookmarks_source_remove;
-   source_class->metadata = grl_bookmarks_source_metadata;
-   source_class->notify_change_start = grl_bookmarks_source_notify_change_start;
-   source_class->notify_change_stop = grl_bookmarks_source_notify_change_stop;
-
-  metadata_class->supported_keys = grl_bookmarks_source_supported_keys;
-
-  g_type_class_add_private (klass, sizeof (GrlBookmarksPrivate));
-}
-
-static void
-grl_bookmarks_source_init (GrlBookmarksSource *source)
-{
-  gint r;
-  const gchar *home;
-  gchar *db_path;
-  gchar *sql_error = NULL;
-
-  source->priv = GRL_BOOKMARKS_GET_PRIVATE (source);
-
-  home = g_getenv ("HOME");
-  if (!home) {
-    GRL_WARNING ("$HOME not set, cannot open database");
-    return;
-  }
-
-  GRL_DEBUG ("Opening database connection...");
-  db_path = g_strconcat (home, G_DIR_SEPARATOR_S, GRL_SQL_DB, NULL);
-  r = sqlite3_open (db_path, &source->priv->db);
-  if (r) {
-    g_critical ("Failed to open database '%s': %s",
-		db_path, sqlite3_errmsg (source->priv->db));
-    sqlite3_close (source->priv->db);
-    return;
-  }
-  GRL_DEBUG ("  OK");
-
-  GRL_DEBUG ("Checking database tables...");
-  r = sqlite3_exec (source->priv->db, GRL_SQL_CREATE_TABLE_BOOKMARKS,
-		    NULL, NULL, &sql_error);
-
-  if (r) {
-    if (sql_error) {
-      GRL_WARNING ("Failed to create database tables: %s", sql_error);
-      sqlite3_free (sql_error);
-      sql_error = NULL;
-    } else {
-      GRL_WARNING ("Failed to create database tables.");
-    }
-    sqlite3_close (source->priv->db);
-    return;
-  }
-  GRL_DEBUG ("  OK");
-
-  g_free (db_path);
-}
-
-G_DEFINE_TYPE (GrlBookmarksSource, grl_bookmarks_source, GRL_TYPE_MEDIA_SOURCE);
-
-static void
-grl_bookmarks_source_finalize (GObject *object)
-{
-  GrlBookmarksSource *source;
-
-  GRL_DEBUG ("grl_bookmarks_source_finalize");
-
-  source = GRL_BOOKMARKS_SOURCE (object);
-
-  sqlite3_close (source->priv->db);
-
-  G_OBJECT_CLASS (grl_bookmarks_source_parent_class)->finalize (object);
-}
-
-/* ======================= Utilities ==================== */
-
-static gboolean
-mime_is_video (const gchar *mime)
-{
-  return mime && strstr (mime, "video") != NULL;
-}
-
-static gboolean
-mime_is_audio (const gchar *mime)
-{
-  return mime && strstr (mime, "audio") != NULL;
-}
-
-static GrlMedia *
-build_media_from_stmt (GrlMedia *content, sqlite3_stmt *sql_stmt)
-{
-  GrlMedia *media = NULL;
-  gchar *id;
-  gchar *title;
-  gchar *url;
-  gchar *desc;
-  gchar *date;
-  gchar *mime;
-  guint type;
-  guint childcount;
-
-  if (content) {
-    media = content;
-  }
-
-  id = (gchar *) sqlite3_column_text (sql_stmt, BOOKMARK_ID);
-  title = (gchar *) sqlite3_column_text (sql_stmt, BOOKMARK_TITLE);
-  url = (gchar *) sqlite3_column_text (sql_stmt, BOOKMARK_URL);
-  desc = (gchar *) sqlite3_column_text (sql_stmt, BOOKMARK_DESC);
-  date = (gchar *) sqlite3_column_text (sql_stmt, BOOKMARK_DATE);
-  mime = (gchar *) sqlite3_column_text (sql_stmt, BOOKMARK_MIME);
-  type = (guint) sqlite3_column_int (sql_stmt, BOOKMARK_TYPE);
-  childcount = (guint) sqlite3_column_int (sql_stmt, BOOKMARK_CHILDCOUNT);
-
-  if (!media) {
-    if (type == BOOKMARK_TYPE_CATEGORY) {
-      media = GRL_MEDIA (grl_media_box_new ());
-    } else if (mime_is_audio (mime)) {
-      media = GRL_MEDIA (grl_media_new ());
-    } else if (mime_is_video (mime)) {
-      media = GRL_MEDIA (grl_media_new ());
-    } else {
-      media = GRL_MEDIA (grl_media_new ());
-    }
-  }
-
-  grl_media_set_id (media, id);
-  grl_media_set_title (media, title);
-  if (url) {
-    grl_media_set_url (media, url);
-  }
-  if (desc) {
-    grl_media_set_description (media, desc);
-  }
-  if (date) {
-    grl_media_set_date (media, date);
-  }
-
-  if (type == BOOKMARK_TYPE_CATEGORY) {
-    grl_media_box_set_childcount (GRL_MEDIA_BOX (media), childcount);
-  }
-
-  return media;
-}
-
-static void
-bookmark_metadata (GrlMediaSourceMetadataSpec *ms)
-{
-  gint r;
-  sqlite3_stmt *sql_stmt = NULL;
-  sqlite3 *db;
-  GError *error = NULL;
-  gchar *sql;
-  const gchar *id;
-  
-  GRL_DEBUG ("bookmark_metadata");
-
-  db = GRL_BOOKMARKS_SOURCE (ms->source)->priv->db;
-
-  id = grl_media_get_id (ms->media);
-  if (!id) {
-    /* Root category: special case */
-    grl_media_set_title (ms->media, GRL_ROOT_TITLE);
-    ms->callback (ms->source, ms->media, ms->user_data, NULL);
-    return;
-  }
-
-  sql = g_strdup_printf (GRL_SQL_GET_BOOKMARK_BY_ID, id);
-  GRL_DEBUG ("%s", sql);
-  r = sqlite3_prepare_v2 (db, sql, strlen (sql), &sql_stmt, NULL);
-  g_free (sql);
-
-  if (r != SQLITE_OK) {
-    GRL_WARNING ("Failed to get bookmark: %s", sqlite3_errmsg (db));
-    error = g_error_new (GRL_CORE_ERROR,
-			 GRL_CORE_ERROR_METADATA_FAILED,
-			 "Failed to get bookmark metadata");
-    ms->callback (ms->source, ms->media, ms->user_data, error);
-    g_error_free (error);
-    return;
-  }
-
-  while ((r = sqlite3_step (sql_stmt)) == SQLITE_BUSY);
-
-  if (r == SQLITE_ROW) {
-    build_media_from_stmt (ms->media, sql_stmt);
-    ms->callback (ms->source, ms->media, ms->user_data, NULL);
-  } else {
-    GRL_WARNING ("Failed to get bookmark: %s", sqlite3_errmsg (db));
-    error = g_error_new (GRL_CORE_ERROR,
-			 GRL_CORE_ERROR_METADATA_FAILED,
-			 "Failed to get bookmark metadata");
-    ms->callback (ms->source, ms->media, ms->user_data, error);
-    g_error_free (error);
-  }
-
-  sqlite3_finalize (sql_stmt);
-}
-
-static void
-produce_bookmarks_from_sql (OperationSpec *os, const gchar *sql)
-{
-  gint r;
-  sqlite3_stmt *sql_stmt = NULL;
-  sqlite3 *db;
-  GrlMedia *media;
-  GError *error = NULL;
-  GList *medias = NULL;
-  guint count = 0;
-  GList *iter;
-
-  GRL_DEBUG ("produce_bookmarks_from_sql");
-
-  GRL_DEBUG ("%s", sql);
-  db = GRL_BOOKMARKS_SOURCE (os->source)->priv->db;
-  r = sqlite3_prepare_v2 (db, sql, strlen (sql), &sql_stmt, NULL);
-
-  if (r != SQLITE_OK) {
-    GRL_WARNING ("Failed to retrieve bookmarks: %s", sqlite3_errmsg (db));
-    error = g_error_new (GRL_CORE_ERROR,
-			 os->error_code,
-			 "Failed to retrieve bookmarks list");
-    os->callback (os->source, os->operation_id, NULL, 0, os->user_data, error);
-    g_error_free (error);
-    goto free_resources;
-  }
-
-  while ((r = sqlite3_step (sql_stmt)) == SQLITE_BUSY);
-
-  while (r == SQLITE_ROW) {
-    media = build_media_from_stmt (NULL, sql_stmt);
-    medias = g_list_prepend (medias, media);
-    count++;
-    r = sqlite3_step (sql_stmt);
-  }
-
-  if (r != SQLITE_DONE) {
-    GRL_WARNING ("Failed to retrieve bookmarks: %s", sqlite3_errmsg (db));
-    error = g_error_new (GRL_CORE_ERROR,
-			 os->error_code,
-			 "Failed to retrieve bookmarks list");
-    os->callback (os->source, os->operation_id, NULL, 0, os->user_data, error);
-    g_error_free (error);
-    goto free_resources;
-  }
-
-  if (count > 0) {
-    medias = g_list_reverse (medias);
-    iter = medias;
-    while (iter) {
-      media = GRL_MEDIA (iter->data);
-      os->callback (os->source,
-		    os->operation_id,
-		    media,
-		    --count,
-		    os->user_data,
-		    NULL);
-      iter = g_list_next (iter);
-    }
-    g_list_free (medias);
-  } else {
-    os->callback (os->source, os->operation_id, NULL, 0, os->user_data, NULL);
-  }
-
- free_resources:
-  if (sql_stmt)
-    sqlite3_finalize (sql_stmt);
-}
-
-static void
-produce_bookmarks_by_query (OperationSpec *os, const gchar *query)
-{
-  gchar *sql;
-  GRL_DEBUG ("produce_bookmarks_by_query");
-  sql = g_strdup_printf (GRL_SQL_GET_BOOKMARKS_BY_QUERY,
-			 query, os->count, os->skip);    
-  produce_bookmarks_from_sql (os, sql);
-  g_free (sql);
-}
-
-static void
-produce_bookmarks_by_text (OperationSpec *os, const gchar *text)
-{
-  gchar *sql;
-  GRL_DEBUG ("produce_bookmarks_by_text");
-  sql = g_strdup_printf (GRL_SQL_GET_BOOKMARKS_BY_TEXT,
-			 text? text: "",
-                         text? text: "",
-                         os->count,
-                         os->skip);
-  produce_bookmarks_from_sql (os, sql);
-  g_free (sql);
-}
-
-static void
-produce_bookmarks_from_category (OperationSpec *os, const gchar *category_id)
-{
-  gchar *sql;
-  GRL_DEBUG ("produce_bookmarks_from_category");
-  sql = g_strdup_printf (GRL_SQL_GET_BOOKMARKS_BY_PARENT,
-			 category_id, os->count, os->skip);    
-  produce_bookmarks_from_sql (os, sql);
-  g_free (sql);
-}
-
-static void
-remove_bookmark (GrlBookmarksSource *bookmarks_source,
-                 const gchar *bookmark_id,
-                 GError **error)
-{
-  gint r;
-  gchar *sql_error;
-  gchar *sql;
-
-  GRL_DEBUG ("remove_bookmark");
-
-  sql = g_strdup_printf (GRL_SQL_REMOVE_BOOKMARK, bookmark_id, bookmark_id);
-  GRL_DEBUG ("%s", sql);
-  r = sqlite3_exec (bookmarks_source->priv->db, sql, NULL, NULL, &sql_error);
-  g_free (sql);
-
-  if (r != SQLITE_OK) {
-    GRL_WARNING ("Failed to remove bookmark '%s': %s", bookmark_id, sql_error);
-    *error = g_error_new (GRL_CORE_ERROR,
-			  GRL_CORE_ERROR_REMOVE_FAILED,
-			  "Failed to remove bookmark");
-    sqlite3_free (sql_error);
-  }
-
-  /* Remove orphan nodes from database */
-  GRL_DEBUG ("%s", GRL_SQL_REMOVE_ORPHAN);
-  r = sqlite3_exec (bookmarks_source->priv->db,
-                    GRL_SQL_REMOVE_ORPHAN,
-                    NULL, NULL, NULL);
-
-  if (bookmarks_source->priv->notify_changes) {
-    /* We can improve accuracy computing the parent container of removed
-       element */
-    grl_media_source_notify_change (GRL_MEDIA_SOURCE (bookmarks_source),
-                                    NULL,
-                                    GRL_CONTENT_REMOVED,
-                                    TRUE);
-  }
-}
-
-static void
-store_bookmark (GrlBookmarksSource *bookmarks_source,
-		GrlMediaBox *parent,
-		GrlMedia *bookmark,
-		GError **error)
-{
-  gint r;
-  sqlite3_stmt *sql_stmt = NULL;
-  const gchar *title;
-  const gchar *url;
-  const gchar *desc;
-  GTimeVal now;
-  const gchar *parent_id;
-  const gchar *mime;
-  gchar *date;
-  guint type;
-  gchar *id;
-
-  GRL_DEBUG ("store_bookmark");
-
-  title = grl_media_get_title (bookmark);
-  url = grl_media_get_url (bookmark);
-  desc = grl_media_get_description (bookmark);
-  mime = grl_media_get_mime (bookmark);
-  g_get_current_time (&now);
-  date = g_time_val_to_iso8601 (&now);
-
-  if (!parent) {
-    parent_id = "0";
-  } else {
-    parent_id = grl_media_get_id (GRL_MEDIA (parent));
-  }
-  if (!parent_id) {
-    parent_id = "0";
-  }
-
-  GRL_DEBUG ("%s", GRL_SQL_STORE_BOOKMARK);
-  r = sqlite3_prepare_v2 (bookmarks_source->priv->db,
-			  GRL_SQL_STORE_BOOKMARK,
-			  strlen (GRL_SQL_STORE_BOOKMARK),
-			  &sql_stmt, NULL);
-  if (r != SQLITE_OK) {
-    GRL_WARNING ("Failed to store bookmark '%s': %s", title,
-                 sqlite3_errmsg (bookmarks_source->priv->db));
-    *error = g_error_new (GRL_CORE_ERROR,
-			  GRL_CORE_ERROR_STORE_FAILED,
-			  "Failed to store bookmark '%s'", title);
-    return;
-  }
-
-  GRL_DEBUG ("URL: '%s'", url);
-
-  if (GRL_IS_MEDIA_BOX (bookmark)) {
-    type = BOOKMARK_TYPE_CATEGORY;
-  } else {
-    type = BOOKMARK_TYPE_STREAM;
-  }
-
-  sqlite3_bind_text (sql_stmt, BOOKMARK_PARENT, parent_id, -1, SQLITE_STATIC);
-  sqlite3_bind_int (sql_stmt, BOOKMARK_TYPE, type);
-  if (type == BOOKMARK_TYPE_STREAM) {
-    sqlite3_bind_text (sql_stmt, BOOKMARK_URL, url, -1, SQLITE_STATIC);
-  } else {
-    sqlite3_bind_null (sql_stmt, BOOKMARK_URL);
-  }
-  sqlite3_bind_text (sql_stmt, BOOKMARK_TITLE, title, -1, SQLITE_STATIC);
-  if (date) {
-    sqlite3_bind_text (sql_stmt, BOOKMARK_DATE, date, -1, SQLITE_STATIC);
-  } else {
-    sqlite3_bind_null (sql_stmt, BOOKMARK_DATE);
-  }
-  if (mime) {
-    sqlite3_bind_text (sql_stmt, BOOKMARK_MIME, mime, -1, SQLITE_STATIC);
-  } else {
-    sqlite3_bind_null (sql_stmt, BOOKMARK_MIME);
-  }
-  if (desc) {
-    sqlite3_bind_text (sql_stmt, BOOKMARK_DESC, desc, -1, SQLITE_STATIC);
-  } else {
-    sqlite3_bind_null (sql_stmt, BOOKMARK_DESC);
-  }
-
-  while ((r = sqlite3_step (sql_stmt)) == SQLITE_BUSY);
-
-  if (r != SQLITE_DONE) {
-    GRL_WARNING ("Failed to store bookmark '%s': %s", title,
-                 sqlite3_errmsg (bookmarks_source->priv->db));
-    *error = g_error_new (GRL_CORE_ERROR,
-			  GRL_CORE_ERROR_STORE_FAILED,
-			  "Failed to store bookmark '%s'", title);
-    sqlite3_finalize (sql_stmt);
-    return;
-  }
-
-  sqlite3_finalize (sql_stmt);
-
-  id = g_strdup_printf ("%llu",
-                        sqlite3_last_insert_rowid (bookmarks_source->priv->db));
-  grl_media_set_id (bookmark, id);
-  g_free (id);
-
-  if (bookmarks_source->priv->notify_changes) {
-    grl_media_source_notify_change (GRL_MEDIA_SOURCE (bookmarks_source),
-                                    GRL_MEDIA (parent),
-                                    GRL_CONTENT_ADDED,
-                                    FALSE);
-  }
-}
-
-/* ================== API Implementation ================ */
-
-static const GList *
-grl_bookmarks_source_supported_keys (GrlMetadataSource *source)
-{
-  static GList *keys = NULL;
-  if (!keys) {
-    keys = grl_metadata_key_list_new (GRL_METADATA_KEY_ID,
-                                      GRL_METADATA_KEY_TITLE,
-                                      GRL_METADATA_KEY_URL,
-                                      GRL_METADATA_KEY_CHILDCOUNT,
-                                      GRL_METADATA_KEY_DESCRIPTION,
-                                      GRL_METADATA_KEY_DATE,
-                                      NULL);
-  }
-  return keys;
-}
-
-static void
-grl_bookmarks_source_browse (GrlMediaSource *source,
-                            GrlMediaSourceBrowseSpec *bs)
-{
-  GRL_DEBUG ("grl_bookmarks_source_browse");
-
-  OperationSpec *os;
-  GrlBookmarksSource *bookmarks_source;
-  GError *error = NULL;
-
-  bookmarks_source = GRL_BOOKMARKS_SOURCE (source);
-  if (!bookmarks_source->priv->db) {
-    GRL_WARNING ("Can't execute operation: no database connection.");
-    error = g_error_new (GRL_CORE_ERROR,
-			 GRL_CORE_ERROR_BROWSE_FAILED,
-			 "No database connection");
-    bs->callback (bs->source, bs->browse_id, NULL, 0, bs->user_data, error);
-    g_error_free (error);
-  }
-
-  /* Configure browse operation */
-  os = g_slice_new0 (OperationSpec);
-  os->source = bs->source;
-  os->operation_id = bs->browse_id;
-  os->media_id = grl_media_get_id (bs->container);
-  os->count = bs->count;
-  os->skip = bs->skip;
-  os->callback = bs->callback;
-  os->user_data = bs->user_data;
-  os->error_code = GRL_CORE_ERROR_BROWSE_FAILED;
-
-  produce_bookmarks_from_category (os, os->media_id ? os->media_id : "0");
-  g_slice_free (OperationSpec, os);
-}
-
-static void
-grl_bookmarks_source_search (GrlMediaSource *source,
-			     GrlMediaSourceSearchSpec *ss)
-{
-  GRL_DEBUG ("grl_bookmarks_source_search");
-
-  GrlBookmarksSource *bookmarks_source;
-  OperationSpec *os;
-  GError *error = NULL;
-
-  bookmarks_source = GRL_BOOKMARKS_SOURCE (source);
-  if (!bookmarks_source->priv->db) {
-    GRL_WARNING ("Can't execute operation: no database connection.");
-    error = g_error_new (GRL_CORE_ERROR,
-			 GRL_CORE_ERROR_QUERY_FAILED,
-			 "No database connection");
-    ss->callback (ss->source, ss->search_id, NULL, 0, ss->user_data, error);
-    g_error_free (error);
-  }
-
-  os = g_slice_new0 (OperationSpec);
-  os->source = ss->source;
-  os->operation_id = ss->search_id;
-  os->count = ss->count;
-  os->skip = ss->skip;
-  os->callback = ss->callback;
-  os->user_data = ss->user_data;
-  os->error_code = GRL_CORE_ERROR_SEARCH_FAILED;
-  produce_bookmarks_by_text (os, ss->text);
-  g_slice_free (OperationSpec, os);
-}
-
-static void
-grl_bookmarks_source_query (GrlMediaSource *source,
-			    GrlMediaSourceQuerySpec *qs)
-{
-  GRL_DEBUG ("grl_bookmarks_source_query");
-
-  GrlBookmarksSource *bookmarks_source;
-  OperationSpec *os;
-  GError *error = NULL;
-
-  bookmarks_source = GRL_BOOKMARKS_SOURCE (source);
-  if (!bookmarks_source->priv->db) {
-    GRL_WARNING ("Can't execute operation: no database connection.");
-    error = g_error_new (GRL_CORE_ERROR,
-			 GRL_CORE_ERROR_QUERY_FAILED,
-			 "No database connection");
-    qs->callback (qs->source, qs->query_id, NULL, 0, qs->user_data, error);
-    g_error_free (error);
-  }
-
-  os = g_slice_new0 (OperationSpec);
-  os->source = qs->source;
-  os->operation_id = qs->query_id;
-  os->count = qs->count;
-  os->skip = qs->skip;
-  os->callback = qs->callback;
-  os->user_data = qs->user_data;
-  os->error_code = GRL_CORE_ERROR_SEARCH_FAILED;
-  produce_bookmarks_by_query (os, qs->query);
-  g_slice_free (OperationSpec, os);
-}
-
-static void
-grl_bookmarks_source_store (GrlMediaSource *source, GrlMediaSourceStoreSpec *ss)
-{
-  GRL_DEBUG ("grl_bookmarks_source_store");
-  /* FIXME: Try to guess bookmark mime somehow */
-  GError *error = NULL;
-  store_bookmark (GRL_BOOKMARKS_SOURCE (ss->source),
-		  ss->parent, ss->media, &error);
-  ss->callback (ss->source, ss->parent, ss->media, ss->user_data, error);
-  if (error) {
-    g_error_free (error);
-  }
-}
-
-static void grl_bookmarks_source_remove (GrlMediaSource *source,
-					 GrlMediaSourceRemoveSpec *rs)
-{
-  GRL_DEBUG ("grl_bookmarks_source_remove");
-  GError *error = NULL;
-  remove_bookmark (GRL_BOOKMARKS_SOURCE (rs->source),
-		   rs->media_id, &error);
-  rs->callback (rs->source, rs->media, rs->user_data, error);
-  if (error) {
-    g_error_free (error);
-  }
-}
-
-static void
-grl_bookmarks_source_metadata (GrlMediaSource *source,
-			       GrlMediaSourceMetadataSpec *ms)
-{
-  GRL_DEBUG ("grl_bookmarks_source_metadata");
-
-  GrlBookmarksSource *bookmarks_source;
-  GError *error = NULL;
-
-  bookmarks_source = GRL_BOOKMARKS_SOURCE (source);
-  if (!bookmarks_source->priv->db) {
-    GRL_WARNING ("Can't execute operation: no database connection.");
-    error = g_error_new (GRL_CORE_ERROR,
-			 GRL_CORE_ERROR_METADATA_FAILED,
-			 "No database connection");
-    ms->callback (ms->source, ms->media, ms->user_data, error);
-    g_error_free (error);
-  }
-
-  bookmark_metadata (ms);
-}
-
-static GrlSupportedOps
-grl_bookmarks_source_supported_operations (GrlMetadataSource *metadata_source)
-{
-  GrlSupportedOps caps;
-  GrlBookmarksSource *source;
-
-  source = GRL_BOOKMARKS_SOURCE (metadata_source);
-  caps = GRL_OP_BROWSE | GRL_OP_METADATA | GRL_OP_SEARCH | GRL_OP_QUERY |
-    GRL_OP_STORE | GRL_OP_STORE_PARENT | GRL_OP_REMOVE | GRL_OP_NOTIFY_CHANGE;
-
-  return caps;
-}
-
-static gboolean
-grl_bookmarks_source_notify_change_start (GrlMediaSource *source,
-                                          GError **error)
-{
-  GrlBookmarksSource *bookmarks_source = GRL_BOOKMARKS_SOURCE (source);
-
-  bookmarks_source->priv->notify_changes = TRUE;
-
-  return TRUE;
-}
-
-static gboolean
-grl_bookmarks_source_notify_change_stop (GrlMediaSource *source,
-                                         GError **error)
-{
-  GrlBookmarksSource *bookmarks_source = GRL_BOOKMARKS_SOURCE (source);
-
-  bookmarks_source->priv->notify_changes = FALSE;
-
-  return TRUE;
-}
diff --git a/src/bookmarks/grl-bookmarks.h b/src/bookmarks/grl-bookmarks.h
deleted file mode 100644
index b5529b1..0000000
--- a/src/bookmarks/grl-bookmarks.h
+++ /dev/null
@@ -1,75 +0,0 @@
-/*
- * Copyright (C) 2010 Igalia S.L.
- *
- * Contact: Iago Toral Quiroga <itoral igalia 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_BOOKMARKS_SOURCE_H_
-#define _GRL_BOOKMARKS_SOURCE_H_
-
-#include <grilo.h>
-
-#define GRL_BOOKMARKS_SOURCE_TYPE                \
-  (grl_bookmarks_source_get_type ())
-
-#define GRL_BOOKMARKS_SOURCE(obj)                                \
-  (G_TYPE_CHECK_INSTANCE_CAST ((obj),                           \
-                               GRL_BOOKMARKS_SOURCE_TYPE,        \
-                               GrlBookmarksSource))
-
-#define GRL_IS_BOOKMARKS_SOURCE(obj)                             \
-  (G_TYPE_CHECK_INSTANCE_TYPE ((obj),                           \
-                               GRL_BOOKMARKS_SOURCE_TYPE))
-
-#define GRL_BOOKMARKS_SOURCE_CLASS(klass)                \
-  (G_TYPE_CHECK_CLASS_CAST((klass),                     \
-                           GRL_BOOKMARKS_SOURCE_TYPE,    \
-                           GrlBookmarksSourceClass))
-
-#define GRL_IS_BOOKMARKS_SOURCE_CLASS(klass)             \
-  (G_TYPE_CHECK_CLASS_TYPE((klass),                     \
-                           GRL_BOOKMARKS_SOURCE_TYPE))
-
-#define GRL_BOOKMARKS_SOURCE_GET_CLASS(obj)              \
-  (G_TYPE_INSTANCE_GET_CLASS ((obj),                    \
-                              GRL_BOOKMARKS_SOURCE_TYPE, \
-                              GrlBookmarksSourceClass))
-
-typedef struct _GrlBookmarksPrivate GrlBookmarksPrivate;
-typedef struct _GrlBookmarksSource  GrlBookmarksSource;
-
-struct _GrlBookmarksSource {
-
-  GrlMediaSource parent;
-
-  /*< private >*/
-  GrlBookmarksPrivate *priv;
-};
-
-typedef struct _GrlBookmarksSourceClass GrlBookmarksSourceClass;
-
-struct _GrlBookmarksSourceClass {
-
-  GrlMediaSourceClass parent_class;
-
-};
-
-GType grl_bookmarks_source_get_type (void);
-
-#endif /* _GRL_BOOKMARKS_SOURCE_H_ */
diff --git a/src/bookmarks/grl-bookmarks.xml b/src/bookmarks/grl-bookmarks.xml
deleted file mode 100644
index 74ef3ee..0000000
--- a/src/bookmarks/grl-bookmarks.xml
+++ /dev/null
@@ -1,9 +0,0 @@
-<plugin>
-  <info>
-    <name>Bookmarks</name>
-    <description>A plugin for organizing media bookmarks</description>
-    <author>Igalia S.L.</author>
-    <license>LGPL</license>
-    <site>http://www.igalia.com</site>
-  </info>
-</plugin>
diff --git a/src/fake-metadata/Makefile.am b/src/fake-metadata/Makefile.am
deleted file mode 100644
index e0a1b81..0000000
--- a/src/fake-metadata/Makefile.am
+++ /dev/null
@@ -1,32 +0,0 @@
-#
-# Makefile.am
-#
-# Author: Iago Toral Quiroga <itoral igalia com>
-#
-# Copyright (C) 2010, 2011 Igalia S.L. All rights reserved.
-
-lib_LTLIBRARIES		 = libgrlfakemetadata.la
-
-libgrlfakemetadata_la_CFLAGS =	\
-	$(DEPS_CFLAGS)
-
-libgrlfakemetadata_la_LIBADD =	\
-	$(DEPS_LIBS)
-
-libgrlfakemetadata_la_LDFLAGS = \
-	-module			\
-	-avoid-version
-
-libgrlfakemetadata_la_SOURCES = grl-fake-metadata.c grl-fake-metadata.h
-
-libdir=$(GRL_PLUGINS_DIR)
-fakemetadataxmldir	= $(GRL_PLUGINS_CONF_DIR)
-fakemetadataxml_DATA	= $(FAKEMETADATA_PLUGIN_ID).xml
-
-EXTRA_DIST = $(fakemetadataxml_DATA)
-
-MAINTAINERCLEANFILES	 = \
-	*.in \
-	*~
-
-DISTCLEANFILES = $(MAINTAINERCLEANFILES)
diff --git a/src/fake-metadata/grl-fake-metadata.c b/src/fake-metadata/grl-fake-metadata.c
deleted file mode 100644
index 463b284..0000000
--- a/src/fake-metadata/grl-fake-metadata.c
+++ /dev/null
@@ -1,231 +0,0 @@
-/*
- * Copyright (C) 2010 Igalia S.L.
- *
- * Contact: Iago Toral Quiroga <itoral igalia 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 <grilo.h>
-
-#include "grl-fake-metadata.h"
-
-#define GRL_LOG_DOMAIN_DEFAULT fake_metadata_log_domain
-GRL_LOG_DOMAIN_STATIC(fake_metadata_log_domain);
-
-#define PLUGIN_ID   FAKEMETADATA_PLUGIN_ID
-
-#define SOURCE_ID   "grl-fake-metadata"
-#define SOURCE_NAME "Fake Metadata Provider"
-#define SOURCE_DESC "A source for faking metadata resolution"
-
-#define AUTHOR      "Igalia S.L."
-#define LICENSE     "LGPL"
-#define SITE        "http://www.igalia.com";
-
-
-static GrlFakeMetadataSource *grl_fake_metadata_source_new (void);
-
-static void grl_fake_metadata_source_resolve (GrlMetadataSource *source,
-                                              GrlMetadataSourceResolveSpec *rs);
-
-static void grl_fake_metadata_source_set_metadata (GrlMetadataSource *source,
-						   GrlMetadataSourceSetMetadataSpec *sms);
-
-static const GList *grl_fake_metadata_source_supported_keys (GrlMetadataSource *source);
-
-static const GList *grl_fake_metadata_source_key_depends (GrlMetadataSource *source,
-                                                          GrlKeyID key_id);
-
-static const GList *grl_fake_metadata_source_writable_keys (GrlMetadataSource *source);
-
-gboolean grl_fake_metadata_source_plugin_init (GrlPluginRegistry *registry,
-                                               const GrlPluginInfo *plugin,
-                                               GList *configs);
-
-
-/* =================== GrlFakeMetadata Plugin  =============== */
-
-gboolean
-grl_fake_metadata_source_plugin_init (GrlPluginRegistry *registry,
-                                      const GrlPluginInfo *plugin,
-                                      GList *configs)
-{
-  GRL_LOG_DOMAIN_INIT (fake_metadata_log_domain, "fake-metadata");
-
-  GRL_DEBUG ("grl_fake_metadata_source_plugin_init");
-
-  GrlFakeMetadataSource *source = grl_fake_metadata_source_new ();
-  grl_plugin_registry_register_source (registry,
-                                       plugin,
-                                       GRL_MEDIA_PLUGIN (source),
-                                       NULL);
-  return TRUE;
-}
-
-GRL_PLUGIN_REGISTER (grl_fake_metadata_source_plugin_init,
-                     NULL,
-                     PLUGIN_ID);
-
-/* ================== GrlFakeMetadata GObject ================ */
-
-static GrlFakeMetadataSource *
-grl_fake_metadata_source_new (void)
-{
-  GRL_DEBUG ("grl_fake_metadata_source_new");
-  return g_object_new (GRL_FAKE_METADATA_SOURCE_TYPE,
-		       "source-id", SOURCE_ID,
-		       "source-name", SOURCE_NAME,
-		       "source-desc", SOURCE_DESC,
-		       NULL);
-}
-
-static void
-grl_fake_metadata_source_class_init (GrlFakeMetadataSourceClass * klass)
-{
-  GrlMetadataSourceClass *metadata_class = GRL_METADATA_SOURCE_CLASS (klass);
-  metadata_class->supported_keys = grl_fake_metadata_source_supported_keys;
-  metadata_class->key_depends = grl_fake_metadata_source_key_depends;
-  metadata_class->resolve = grl_fake_metadata_source_resolve;
-  metadata_class->set_metadata = grl_fake_metadata_source_set_metadata;
-  metadata_class->writable_keys = grl_fake_metadata_source_writable_keys;
-}
-
-static void
-grl_fake_metadata_source_init (GrlFakeMetadataSource *source)
-{
-}
-
-G_DEFINE_TYPE (GrlFakeMetadataSource,
-               grl_fake_metadata_source,
-               GRL_TYPE_METADATA_SOURCE);
-
-/* ======================= Utilities ==================== */
-
-static void
-fill_metadata (GrlMedia *media, GrlKeyID key_id)
-{
-  if (key_id == GRL_METADATA_KEY_AUTHOR) {
-    grl_media_set_author (media, "fake author");
-  } else if (key_id == GRL_METADATA_KEY_ARTIST) {
-    grl_data_set_string (GRL_DATA (media),
-                         GRL_METADATA_KEY_ARTIST, "fake artist");
-  } else if (key_id == GRL_METADATA_KEY_ALBUM) {
-    grl_data_set_string (GRL_DATA (media),
-                         GRL_METADATA_KEY_ALBUM, "fake album");
-  } else if (key_id == GRL_METADATA_KEY_GENRE) {
-    grl_data_set_string (GRL_DATA (media),
-                         GRL_METADATA_KEY_GENRE, "fake genre");
-  } else if (key_id == GRL_METADATA_KEY_DESCRIPTION) {
-    grl_media_set_description (media, "fake description");
-  } else if (key_id == GRL_METADATA_KEY_DURATION) {
-    grl_media_set_duration (media, 99);
-  } else if (key_id == GRL_METADATA_KEY_DATE) {
-    grl_data_set_string (GRL_DATA (media),
-                         GRL_METADATA_KEY_DATE, "01/01/1970");
-  } else if (key_id == GRL_METADATA_KEY_THUMBNAIL) {
-    grl_media_set_thumbnail (media,
-                                  "http://fake.thumbnail.com/fake-image.jpg";);
-  }
-}
-
-
-/* ================== API Implementation ================ */
-
-static const GList *
-grl_fake_metadata_source_supported_keys (GrlMetadataSource *source)
-{
-  static GList *keys = NULL;
-  if (!keys) {
-    keys = grl_metadata_key_list_new (GRL_METADATA_KEY_AUTHOR,
-                                      GRL_METADATA_KEY_ARTIST,
-                                      GRL_METADATA_KEY_ALBUM,
-                                      GRL_METADATA_KEY_GENRE,
-                                      GRL_METADATA_KEY_DESCRIPTION,
-                                      GRL_METADATA_KEY_DURATION,
-                                      GRL_METADATA_KEY_DATE,
-                                      GRL_METADATA_KEY_THUMBNAIL,
-                                      NULL);
-  }
-  return keys;
-}
-
-static const GList *
-grl_fake_metadata_source_key_depends (GrlMetadataSource *source,
-                                      GrlKeyID key_id)
-{
-  static GList *deps = NULL;
-  if (!deps) {
-    deps = grl_metadata_key_list_new (GRL_METADATA_KEY_TITLE, NULL);
-  }
-
-  if (key_id == GRL_METADATA_KEY_AUTHOR ||
-      key_id == GRL_METADATA_KEY_ARTIST ||
-      key_id == GRL_METADATA_KEY_ALBUM ||
-      key_id == GRL_METADATA_KEY_GENRE ||
-      key_id == GRL_METADATA_KEY_DESCRIPTION ||
-      key_id == GRL_METADATA_KEY_DURATION ||
-      key_id == GRL_METADATA_KEY_DATE ||
-      key_id == GRL_METADATA_KEY_THUMBNAIL) {
-    return deps;
-  }
-
-  return  NULL;
-}
-
-static const GList *
-grl_fake_metadata_source_writable_keys (GrlMetadataSource *source)
-{
-  static GList *keys = NULL;
-  if (!keys) {
-    keys = grl_metadata_key_list_new (GRL_METADATA_KEY_ALBUM,
-                                      GRL_METADATA_KEY_ARTIST,
-                                      GRL_METADATA_KEY_GENRE,
-                                      NULL);
-  }
-  return keys;
-}
-
-static void
-grl_fake_metadata_source_resolve (GrlMetadataSource *source,
-                                  GrlMetadataSourceResolveSpec *rs)
-{
-  GRL_DEBUG ("grl_fake_metadata_source_resolve");
-
-  GList *iter;
-
-  iter = rs->keys;
-  while (iter) {
-    fill_metadata (GRL_MEDIA (rs->media), iter->data);
-    iter = g_list_next (iter);
-  }
-
-  rs->callback (source, rs->media, rs->user_data, NULL);
-}
-
-static void
-grl_fake_metadata_source_set_metadata (GrlMetadataSource *source,
-				       GrlMetadataSourceSetMetadataSpec *sms)
-{
-  GRL_DEBUG ("grl_fake_metadata_source_set_metadata");
-  GRL_DEBUG ("  Faking set metadata for %d keys!", g_list_length (sms->keys));
-  sms->callback (sms->source, sms->media, NULL, sms->user_data, NULL);
-}
diff --git a/src/fake-metadata/grl-fake-metadata.h b/src/fake-metadata/grl-fake-metadata.h
deleted file mode 100644
index 5d80453..0000000
--- a/src/fake-metadata/grl-fake-metadata.h
+++ /dev/null
@@ -1,72 +0,0 @@
-/*
- * Copyright (C) 2010 Igalia S.L.
- *
- * Contact: Iago Toral Quiroga <itoral igalia 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_FAKE_METADATA_SOURCE_H_
-#define _GRL_FAKE_METADATA_SOURCE_H_
-
-#include <grilo.h>
-
-#define GRL_FAKE_METADATA_SOURCE_TYPE           \
-  (grl_fake_metadata_source_get_type ())
-
-#define GRL_FAKE_METADATA_SOURCE(obj)                           \
-  (G_TYPE_CHECK_INSTANCE_CAST ((obj),                           \
-                               GRL_FAKE_METADATA_SOURCE_TYPE,   \
-                               GrlFakeMetadataSource))
-
-#define GRL_IS_FAKE_METADATA_SOURCE(obj)                        \
-  (G_TYPE_CHECK_INSTANCE_TYPE ((obj),                           \
-                               GRL_FAKE_METADATA_SOURCE_TYPE))
-
-#define GRL_FAKE_METADATA_SOURCE_CLASS(klass)                   \
-  (G_TYPE_CHECK_CLASS_CAST((klass),                             \
-                           GRL_FAKE_METADATA_SOURCE_TYPE,       \
-                           GrlFakeMetadataSourceClass))
-
-#define GRL_IS_FAKE_METADATA_SOURCE_CLASS(klass)                \
-  (G_TYPE_CHECK_CLASS_TYPE((klass),                             \
-                           GRL_FAKE_METADATA_SOURCE_TYPE))
-
-#define GRL_FAKE_METADATA_SOURCE_GET_CLASS(obj)                 \
-  (G_TYPE_INSTANCE_GET_CLASS ((obj),                            \
-                              GRL_FAKE_METADATA_SOURCE_TYPE,    \
-                              GrlFakeMetadataSourceClass))
-
-typedef struct _GrlFakeMetadataSource GrlFakeMetadataSource;
-
-struct _GrlFakeMetadataSource {
-
-  GrlMetadataSource parent;
-
-};
-
-typedef struct _GrlFakeMetadataSourceClass GrlFakeMetadataSourceClass;
-
-struct _GrlFakeMetadataSourceClass {
-
-  GrlMetadataSourceClass parent_class;
-
-};
-
-GType grl_fake_metadata_source_get_type (void);
-
-#endif /* _GRL_FAKE_METADATA_SOURCE_H_ */
diff --git a/src/fake-metadata/grl-fake-metadata.xml b/src/fake-metadata/grl-fake-metadata.xml
deleted file mode 100644
index 11e4d32..0000000
--- a/src/fake-metadata/grl-fake-metadata.xml
+++ /dev/null
@@ -1,9 +0,0 @@
-<plugin>
-  <info>
-    <name>Fake Metadata Provider</name>
-    <description>A plugin for faking metadata resolution</description>
-    <author>Igalia S.L.</author>
-    <license>LGPL</license>
-    <site>http://www.igalia.com</site>
-  </info>
-</plugin>
diff --git a/src/filesystem/Makefile.am b/src/filesystem/Makefile.am
deleted file mode 100644
index c685b85..0000000
--- a/src/filesystem/Makefile.am
+++ /dev/null
@@ -1,34 +0,0 @@
-#
-# Makefile.am
-#
-# Author: Iago Toral Quiroga <itoral igalia com>
-#
-# Copyright (C) 2010, 2011 Igalia S.L. All rights reserved.
-
-lib_LTLIBRARIES		 = libgrlfilesystem.la
-
-libgrlfilesystem_la_CFLAGS =	\
-	$(DEPS_CFLAGS)		\
-	$(GIO_CFLAGS)
-
-libgrlfilesystem_la_LIBADD =	\
-	$(DEPS_LIBS)		\
-	$(GIO_LIBS)
-
-libgrlfilesystem_la_LDFLAGS = \
-	-module		      \
-	-avoid-version
-
-libgrlfilesystem_la_SOURCES = grl-filesystem.c grl-filesystem.h
-
-libdir=$(GRL_PLUGINS_DIR)
-filesystemxmldir	= $(GRL_PLUGINS_CONF_DIR)
-filesystemxml_DATA	= $(FILESYSTEM_PLUGIN_ID).xml
-
-EXTRA_DIST = $(filesystemxml_DATA)
-
-MAINTAINERCLEANFILES =	\
-	*.in		\
-	*~
-
-DISTCLEANFILES = $(MAINTAINERCLEANFILES)
diff --git a/src/filesystem/TODO b/src/filesystem/TODO
deleted file mode 100644
index fb6c8ed..0000000
--- a/src/filesystem/TODO
+++ /dev/null
@@ -1,2 +0,0 @@
-- childcount for categories does not filter media-only files
-  - maybe this is acceptable (since this way it has a performance benefot)
diff --git a/src/filesystem/grl-filesystem.c b/src/filesystem/grl-filesystem.c
deleted file mode 100644
index d4fe60c..0000000
--- a/src/filesystem/grl-filesystem.c
+++ /dev/null
@@ -1,1343 +0,0 @@
-/*
- * Copyright (C) 2010, 2011 Igalia S.L.
- * Copyright (C) 2011 Intel Corporation.
- *
- * Contact: Iago Toral Quiroga <itoral igalia 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 <grilo.h>
-#include <gio/gio.h>
-#include <string.h>
-#include <stdlib.h>
-
-#include "grl-filesystem.h"
-
-/* --------- Logging  -------- */
-
-#define GRL_LOG_DOMAIN_DEFAULT filesystem_log_domain
-GRL_LOG_DOMAIN_STATIC(filesystem_log_domain);
-
-/* -------- File info ------- */
-
-#define FILE_ATTRIBUTES                         \
-  G_FILE_ATTRIBUTE_STANDARD_DISPLAY_NAME ","    \
-  G_FILE_ATTRIBUTE_STANDARD_CONTENT_TYPE ","    \
-  G_FILE_ATTRIBUTE_STANDARD_TYPE ","            \
-  G_FILE_ATTRIBUTE_STANDARD_IS_HIDDEN ","       \
-  G_FILE_ATTRIBUTE_TIME_MODIFIED ","            \
-  G_FILE_ATTRIBUTE_THUMBNAIL_PATH ","           \
-  G_FILE_ATTRIBUTE_THUMBNAILING_FAILED
-
-#define FILE_ATTRIBUTES_FAST                    \
-  G_FILE_ATTRIBUTE_STANDARD_IS_HIDDEN
-
-/* ---- Emission chunks ----- */
-
-#define BROWSE_IDLE_CHUNK_SIZE 5
-
-/* --- Plugin information --- */
-
-#define PLUGIN_ID   FILESYSTEM_PLUGIN_ID
-
-#define SOURCE_ID   "grl-filesystem"
-#define SOURCE_NAME "Filesystem"
-#define SOURCE_DESC "A source for browsing the filesystem"
-
-#define AUTHOR      "Igalia S.L."
-#define LICENSE     "LGPL"
-#define SITE        "http://www.igalia.com";
-
-/* --- Grilo Filesystem Private --- */
-
-#define GRL_FILESYSTEM_SOURCE_GET_PRIVATE(object)         \
-  (G_TYPE_INSTANCE_GET_PRIVATE((object),                  \
-			     GRL_FILESYSTEM_SOURCE_TYPE,  \
-			     GrlFilesystemSourcePrivate))
-
-struct _GrlFilesystemSourcePrivate {
-  GList *chosen_paths;
-  guint max_search_depth;
-  /* a mapping operation_id -> GCancellable to cancel this operation */
-  GHashTable *cancellables;
-  /* Monitors for changes in directories */
-  GList *monitors;
-  GCancellable *cancellable_monitors;
-};
-
-/* --- Data types --- */
-
-typedef struct _RecursiveOperation RecursiveOperation;
-
-typedef gboolean (*RecursiveOperationCb) (GFileInfo *file_info,
-                                          RecursiveOperation *operation);
-
-typedef struct {
-  GrlMediaSourceBrowseSpec *spec;
-  GList *entries;
-  GList *current;
-  const gchar *path;
-  guint remaining;
-  GCancellable *cancellable;
-  guint id;
-}  BrowseIdleData;
-
-struct _RecursiveOperation {
-  RecursiveOperationCb on_cancel;
-  RecursiveOperationCb on_finish;
-  RecursiveOperationCb on_dir;
-  RecursiveOperationCb on_file;
-  gpointer on_dir_data;
-  gpointer on_file_data;
-  GCancellable *cancellable;
-  GQueue *directories;
-  guint max_depth;
-};
-
-typedef struct {
-  guint depth;
-  GFile *directory;
-} RecursiveEntry;
-
-
-static GrlFilesystemSource *grl_filesystem_source_new (void);
-
-static void grl_filesystem_source_finalize (GObject *object);
-
-gboolean grl_filesystem_plugin_init (GrlPluginRegistry *registry,
-                                     const GrlPluginInfo *plugin,
-                                     GList *configs);
-
-static const GList *grl_filesystem_source_supported_keys (GrlMetadataSource *source);
-
-static void grl_filesystem_source_metadata (GrlMediaSource *source,
-                                            GrlMediaSourceMetadataSpec *ms);
-
-static void grl_filesystem_source_browse (GrlMediaSource *source,
-                                          GrlMediaSourceBrowseSpec *bs);
-
-static void grl_filesystem_source_search (GrlMediaSource *source,
-                                          GrlMediaSourceSearchSpec *ss);
-
-
-static gboolean grl_filesystem_test_media_from_uri (GrlMediaSource *source,
-                                                    const gchar *uri);
-
-static void grl_filesystem_get_media_from_uri (GrlMediaSource *source,
-                                               GrlMediaSourceMediaFromUriSpec *mfus);
-
-static void grl_filesystem_source_cancel (GrlMediaSource *source,
-                                          guint operation_id);
-
-static gboolean grl_filesystem_source_notify_change_start (GrlMediaSource *source,
-                                                           GError **error);
-
-static gboolean grl_filesystem_source_notify_change_stop (GrlMediaSource *source,
-                                                          GError **error);
-
-/* =================== Filesystem Plugin  =============== */
-
-gboolean
-grl_filesystem_plugin_init (GrlPluginRegistry *registry,
-                            const GrlPluginInfo *plugin,
-                            GList *configs)
-{
-  GrlConfig *config;
-  GList *chosen_paths = NULL;
-  guint max_search_depth = GRILO_CONF_MAX_SEARCH_DEPTH_DEFAULT;
-
-  GRL_LOG_DOMAIN_INIT (filesystem_log_domain, "filesystem");
-
-  GRL_DEBUG ("filesystem_plugin_init");
-
-  GrlFilesystemSource *source = grl_filesystem_source_new ();
-
-  for (; configs; configs = g_list_next (configs)) {
-    gchar *path;
-    config = GRL_CONFIG (configs->data);
-    path = grl_config_get_string (config, GRILO_CONF_CHOSEN_PATH);
-    if (path) {
-      chosen_paths = g_list_append (chosen_paths, path);
-    }
-    if (grl_config_has_param (config, GRILO_CONF_MAX_SEARCH_DEPTH)) {
-      max_search_depth = (guint)grl_config_get_int (config, GRILO_CONF_MAX_SEARCH_DEPTH);
-    }
-  }
-  source->priv->chosen_paths = chosen_paths;
-  source->priv->max_search_depth = max_search_depth;
-
-  grl_plugin_registry_register_source (registry,
-                                       plugin,
-                                       GRL_MEDIA_PLUGIN (source),
-                                       NULL);
-
-  return TRUE;
-}
-
-GRL_PLUGIN_REGISTER (grl_filesystem_plugin_init,
-                     NULL,
-                     PLUGIN_ID);
-
-/* ================== Filesystem GObject ================ */
-
-
-G_DEFINE_TYPE (GrlFilesystemSource,
-               grl_filesystem_source,
-               GRL_TYPE_MEDIA_SOURCE);
-
-static GrlFilesystemSource *
-grl_filesystem_source_new (void)
-{
-  GRL_DEBUG ("grl_filesystem_source_new");
-  return g_object_new (GRL_FILESYSTEM_SOURCE_TYPE,
-		       "source-id", SOURCE_ID,
-		       "source-name", SOURCE_NAME,
-		       "source-desc", SOURCE_DESC,
-		       NULL);
-}
-
-static void
-grl_filesystem_source_class_init (GrlFilesystemSourceClass * klass)
-{
-  GrlMediaSourceClass *source_class = GRL_MEDIA_SOURCE_CLASS (klass);
-  GrlMetadataSourceClass *metadata_class = GRL_METADATA_SOURCE_CLASS (klass);
-  source_class->browse = grl_filesystem_source_browse;
-  source_class->search = grl_filesystem_source_search;
-  source_class->cancel = grl_filesystem_source_cancel;
-  source_class->notify_change_start = grl_filesystem_source_notify_change_start;
-  source_class->notify_change_stop = grl_filesystem_source_notify_change_stop;
-  source_class->metadata = grl_filesystem_source_metadata;
-  source_class->test_media_from_uri = grl_filesystem_test_media_from_uri;
-  source_class->media_from_uri = grl_filesystem_get_media_from_uri;
-  G_OBJECT_CLASS (source_class)->finalize = grl_filesystem_source_finalize;
-  metadata_class->supported_keys = grl_filesystem_source_supported_keys;
-  g_type_class_add_private (klass, sizeof (GrlFilesystemSourcePrivate));
-}
-
-static void
-grl_filesystem_source_init (GrlFilesystemSource *source)
-{
-  source->priv = GRL_FILESYSTEM_SOURCE_GET_PRIVATE (source);
-  source->priv->cancellables = g_hash_table_new (NULL, NULL);
-}
-
-static void
-grl_filesystem_source_finalize (GObject *object)
-{
-  GrlFilesystemSource *filesystem_source = GRL_FILESYSTEM_SOURCE (object);
-  g_list_foreach (filesystem_source->priv->chosen_paths, (GFunc) g_free, NULL);
-  g_list_free (filesystem_source->priv->chosen_paths);
-  g_hash_table_unref (filesystem_source->priv->cancellables);
-  G_OBJECT_CLASS (grl_filesystem_source_parent_class)->finalize (object);
-}
-
-/* ======================= Utilities ==================== */
-
-static void recursive_operation_next_entry (RecursiveOperation *operation);
-static void add_monitor (GrlFilesystemSource *fs_source, GFile *dir);
-static void cancel_monitors (GrlFilesystemSource *fs_source);
-
-static gboolean
-mime_is_video (const gchar *mime)
-{
-  return strstr (mime, "video") != NULL;
-}
-
-static gboolean
-mime_is_audio (const gchar *mime)
-{
-  return strstr (mime, "audio") != NULL;
-}
-
-static gboolean
-mime_is_image (const gchar *mime)
-{
-  return strstr (mime, "image") != NULL;
-}
-
-static gboolean
-mime_is_media (const gchar *mime)
-{
-  if (!mime)
-    return FALSE;
-  if (!strcmp (mime, "inode/directory"))
-    return TRUE;
-  if (mime_is_audio (mime))
-    return TRUE;
-  if (mime_is_video (mime))
-    return TRUE;
-  if (mime_is_image (mime))
-    return TRUE;
-  return FALSE;
-}
-
-static gboolean
-file_is_valid_content (const gchar *path, gboolean fast)
-{
-  const gchar *mime;
-  GError *error = NULL;
-  gboolean is_media;
-  GFile *file;
-  GFileInfo *info;
-  GFileType type;
-  const gchar *spec;
-
-  if (fast) {
-    spec = FILE_ATTRIBUTES_FAST;
-  } else {
-    spec = FILE_ATTRIBUTES;
-  }
-
-  file = g_file_new_for_path (path);
-  info = g_file_query_info (file, spec, 0, NULL, &error);
-  if (error) {
-    GRL_WARNING ("Failed to get attributes for file '%s': %s",
-                 path, error->message);
-    g_error_free (error);
-    g_object_unref (file);
-    return FALSE;
-  } else {
-    if (g_file_info_get_is_hidden (info)) {
-      is_media = FALSE;
-    } else {
-      if (fast) {
-	/* In fast mode we do not check mime-types,
-	   any non-hidden file is accepted */
-	is_media = TRUE;
-      } else {
-	type = g_file_info_get_file_type (info);
-	mime = g_file_info_get_content_type (info);
-        if (type == G_FILE_TYPE_DIRECTORY || mime_is_media (mime)) {
-          is_media = TRUE;
-        } else {
-          is_media = FALSE;
-        }
-      }
-    }
-    g_object_unref (info);
-    g_object_unref (file);
-    return is_media;
-  }
-}
-
-static void
-set_container_childcount (const gchar *path,
-			  GrlMedia *media,
-			  gboolean fast)
-{
-  GDir *dir;
-  GError *error = NULL;
-  gint count;
-  const gchar *entry_name;
-
-  /* Open directory */
-  GRL_DEBUG ("Opening directory '%s' for childcount", path);
-  dir = g_dir_open (path, 0, &error);
-  if (error) {
-    GRL_WARNING ("Failed to open directory '%s': %s", path, error->message);
-    g_error_free (error);
-    return;
-  }
-
-  /* Count valid entries */
-  count = 0;
-  while ((entry_name = g_dir_read_name (dir)) != NULL) {
-    gchar *entry_path;
-    if (strcmp (path, G_DIR_SEPARATOR_S)) {
-      entry_path = g_strconcat (path, G_DIR_SEPARATOR_S, entry_name, NULL);
-    } else {
-      entry_path = g_strconcat (path, entry_name, NULL);
-    }
-    if (file_is_valid_content (entry_path, fast)) {
-      if (fast) {
-        /* in fast mode we don't compute  mime-types because it is slow,
-           so we can only check if the directory is totally empty (no subdirs,
-           and no files), otherwise we just say we do not know the actual
-           childcount */
-        count = GRL_METADATA_KEY_CHILDCOUNT_UNKNOWN;
-        break;
-      }
-      count++;
-    }
-    g_free (entry_path);
-  }
-
-  g_dir_close (dir);
-
-  grl_media_box_set_childcount (GRL_MEDIA_BOX (media), count);
-}
-
-static GrlMedia *
-create_content (GrlMedia *content,
-                const gchar *path,
-                gboolean only_fast,
-		gboolean root_dir)
-{
-  GrlMedia *media = NULL;
-  gchar *str;
-  const gchar *mime;
-  GError *error = NULL;
-
-  GFile *file = g_file_new_for_path (path);
-  GFileInfo *info = g_file_query_info (file,
-				       FILE_ATTRIBUTES,
-				       0,
-				       NULL,
-				       &error);
-
-  /* Update mode */
-  if (content) {
-    media = content;
-  }
-
-  if (error) {
-    GRL_WARNING ("Failed to get info for file '%s': %s", path,
-                 error->message);
-    if (!media) {
-      media = grl_media_new ();
-    }
-
-    /* Title */
-    str = g_strrstr (path, G_DIR_SEPARATOR_S);
-    if (!str) {
-      str = (gchar *) path;
-    }
-    grl_media_set_title (media, str);
-    g_error_free (error);
-  } else {
-    mime = g_file_info_get_content_type (info);
-
-    if (!media) {
-      if (g_file_info_get_file_type (info) == G_FILE_TYPE_DIRECTORY) {
-	media = GRL_MEDIA (grl_media_box_new ());
-      } else {
-	if (mime_is_video (mime)) {
-	  media = grl_media_video_new ();
-	} else if (mime_is_audio (mime)) {
-	  media = grl_media_audio_new ();
-	} else if (mime_is_image (mime)) {
-	  media = grl_media_image_new ();
-	} else {
-	  media = grl_media_new ();
-	}
-      }
-    }
-
-    if (!GRL_IS_MEDIA_BOX (media)) {
-      grl_media_set_mime (media, mime);
-    }
-
-    /* Title */
-    str = (gchar *) g_file_info_get_display_name (info);
-    grl_media_set_title (media, str);
-
-    /* Date */
-    GTimeVal time;
-    gchar *time_str;
-    g_file_info_get_modification_time (info, &time);
-    time_str = g_time_val_to_iso8601 (&time);
-    grl_media_set_date (media, time_str);
-    g_free (time_str);
-
-    /* Thumbnail */
-    gboolean thumb_failed =
-      g_file_info_get_attribute_boolean (info,
-                                         G_FILE_ATTRIBUTE_THUMBNAILING_FAILED);
-    if (!thumb_failed) {
-      const gchar *thumb =
-        g_file_info_get_attribute_byte_string (info,
-                                               G_FILE_ATTRIBUTE_THUMBNAIL_PATH);
-      if (thumb) {
-	gchar *thumb_uri = g_filename_to_uri (thumb, NULL, NULL);
-	if (thumb_uri) {
-	  grl_media_set_thumbnail (media, thumb_uri);
-	  g_free (thumb_uri);
-	}
-      }
-    }
-
-    g_object_unref (info);
-  }
-
-  grl_media_set_id (media,  root_dir ? NULL : path);
-
-  /* URL */
-  str = g_strconcat ("file://", path, NULL);
-  grl_media_set_url (media, str);
-  g_free (str);
-
-  /* Childcount */
-  if (GRL_IS_MEDIA_BOX (media)) {
-    set_container_childcount (path, media, only_fast);
-  }
-
-  g_object_unref (file);
-
-  return media;
-}
-
-static gboolean
-browse_emit_idle (gpointer user_data)
-{
-  BrowseIdleData *idle_data;
-  guint count;
-  GrlFilesystemSource *fs_source;
-
-  GRL_DEBUG ("browse_emit_idle");
-
-  idle_data = (BrowseIdleData *) user_data;
-  fs_source = GRL_FILESYSTEM_SOURCE (idle_data->spec->source);
-
-  if (g_cancellable_is_cancelled (idle_data->cancellable)) {
-    GRL_DEBUG ("Browse operation %d (\"%s\") has been cancelled",
-               idle_data->id, idle_data->path);
-    idle_data->spec->callback(idle_data->spec->source,
-                              idle_data->id, NULL, 0,
-                              idle_data->spec->user_data, NULL);
-    goto finish;
-  }
-
-  count = 0;
-  do {
-    gchar *entry_path;
-    GrlMedia *content;
-
-    entry_path = (gchar *) idle_data->current->data;
-    content = create_content (NULL,
-			      entry_path,
-			      idle_data->spec->flags & GRL_RESOLVE_FAST_ONLY,
-			      FALSE);
-    g_free (idle_data->current->data);
-
-    idle_data->spec->callback (idle_data->spec->source,
-			       idle_data->spec->browse_id,
-			       content,
-			       idle_data->remaining--,
-			       idle_data->spec->user_data,
-			       NULL);
-
-    idle_data->current = g_list_next (idle_data->current);
-    count++;
-  } while (count < BROWSE_IDLE_CHUNK_SIZE && idle_data->current);
-
-  if (!idle_data->current)
-    goto finish;
-
-  return TRUE;
-
-finish:
-    g_list_free (idle_data->entries);
-    g_hash_table_remove (fs_source->priv->cancellables,
-                         GUINT_TO_POINTER (idle_data->id));
-    g_object_unref (idle_data->cancellable);
-    g_slice_free (BrowseIdleData, idle_data);
-    return FALSE;
-}
-
-static void
-produce_from_path (GrlMediaSourceBrowseSpec *bs, const gchar *path)
-{
-  GDir *dir;
-  GError *error = NULL;
-  const gchar *entry;
-  guint skip, count;
-  GList *entries = NULL;
-  GList *iter;
-
-  /* Open directory */
-  GRL_DEBUG ("Opening directory '%s'", path);
-  dir = g_dir_open (path, 0, &error);
-  if (error) {
-    GRL_WARNING ("Failed to open directory '%s': %s", path, error->message);
-    bs->callback (bs->source, bs->browse_id, NULL, 0, bs->user_data, error);
-    g_error_free (error);
-    return;
-  }
-
-  /* Filter out media and directories */
-  while ((entry = g_dir_read_name (dir)) != NULL) {
-    gchar *file;
-    if (strcmp (path, G_DIR_SEPARATOR_S)) {
-      file = g_strconcat (path, G_DIR_SEPARATOR_S, entry, NULL);
-    } else {
-      file = g_strconcat (path, entry, NULL);
-    }
-    if (file_is_valid_content (file, FALSE)) {
-      entries = g_list_prepend (entries, file);
-    }
-  }
-
-  /* Apply skip and count */
-  skip = bs->skip;
-  count = bs->count;
-  iter = entries;
-  while (iter) {
-    gboolean remove;
-    GList *tmp;
-    if (skip > 0)  {
-      skip--;
-      remove = TRUE;
-    } else if (count > 0) {
-      count--;
-      remove = FALSE;
-    } else {
-      remove = TRUE;
-    }
-    if (remove) {
-      tmp = iter;
-      iter = g_list_next (iter);
-      g_free (tmp->data);
-      entries = g_list_delete_link (entries, tmp);
-    } else {
-      iter = g_list_next (iter);
-    }
-  }
-
-  /* Emit results */
-  if (entries) {
-    /* Use the idle loop to avoid blocking for too long */
-    BrowseIdleData *idle_data = g_slice_new (BrowseIdleData);
-    idle_data->spec = bs;
-    idle_data->remaining = bs->count - count - 1;
-    idle_data->path = path;
-    idle_data->entries = entries;
-    idle_data->current = entries;
-    idle_data->cancellable = g_cancellable_new ();
-    idle_data->id = bs->browse_id;
-    g_hash_table_insert (GRL_FILESYSTEM_SOURCE (bs->source)->priv->cancellables,
-                         GUINT_TO_POINTER (bs->browse_id),
-                         idle_data->cancellable);
-
-    g_idle_add (browse_emit_idle, idle_data);
-  } else {
-    /* No results */
-    bs->callback (bs->source,
-		  bs->browse_id,
-		  NULL,
-		  0,
-		  bs->user_data,
-		  NULL);
-  }
-
-  g_dir_close (dir);
-}
-
-static RecursiveEntry *
-recursive_entry_new (guint depth, GFile *directory)
-{
-  RecursiveEntry *entry;
-
-  entry = g_slice_new(RecursiveEntry);
-  entry->depth = depth;
-  entry->directory = g_object_ref (directory);
-
-  return entry;
-}
-
-static void
-recursive_entry_free (RecursiveEntry *entry)
-{
-  g_object_unref (entry->directory);
-  g_slice_free (RecursiveEntry, entry);
-}
-
-static RecursiveOperation *
-recursive_operation_new ()
-{
-  RecursiveOperation *operation;
-
-  operation = g_slice_new0 (RecursiveOperation);
-  operation->directories = g_queue_new ();
-  operation->cancellable = g_cancellable_new ();
-
-  return operation;
-}
-
-static void
-recursive_operation_free (RecursiveOperation *operation)
-{
-  g_queue_foreach (operation->directories, (GFunc) recursive_entry_free, NULL);
-  g_queue_free (operation->directories);
-  g_object_unref (operation->cancellable);
-  g_slice_free (RecursiveOperation, operation);
-}
-
-static void
-recursive_operation_got_file (GFileEnumerator *enumerator, GAsyncResult *res, RecursiveOperation *operation)
-{
-  GList *files;
-  GError *error = NULL;
-  gboolean continue_operation = TRUE;
-
-  GRL_DEBUG (__func__);
-
-  files = g_file_enumerator_next_files_finish (enumerator, res, &error);
-  if (error) {
-    if (!g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED))
-      GRL_WARNING ("Got error: %s", error->message);
-    g_error_free (error);
-    goto finished;
-  }
-
-  if (files) {
-    GFileInfo *file_info;
-    RecursiveEntry *entry;
-
-    /* we assume there is only one GFileInfo in the list since that's what we ask
-     * for when calling g_file_enumerator_next_files_async() */
-    file_info = (GFileInfo *)files->data;
-    g_list_free (files);
-    /* Get the entry we are running now */
-    entry = g_queue_peek_head (operation->directories);
-    switch (g_file_info_get_file_type (file_info)) {
-    case G_FILE_TYPE_SYMBOLIC_LINK:
-      /* we're too afraid of infinite recursion to touch this for now */
-      break;
-    case G_FILE_TYPE_DIRECTORY:
-        {
-          if (entry->depth < operation->max_depth) {
-            GFile *subdir;
-            RecursiveEntry *subentry;
-
-            if (operation->on_dir) {
-              continue_operation = operation->on_dir(file_info, operation);
-            }
-
-            if (continue_operation) {
-              subdir = g_file_get_child (entry->directory,
-                                         g_file_info_get_name (file_info));
-              subentry = recursive_entry_new (entry->depth + 1, subdir);
-              g_queue_push_tail (operation->directories, subentry);
-              g_object_unref (subdir);
-            } else {
-              g_object_unref (file_info);
-              goto finished;
-            }
-          }
-        }
-      break;
-    case G_FILE_TYPE_REGULAR:
-      if (operation->on_file) {
-        continue_operation = operation->on_file(file_info, operation);
-        if (!continue_operation) {
-          g_object_unref (file_info);
-          goto finished;
-        }
-      }
-      break;
-    default:
-      /* this file is a weirdo, we ignore it */
-      break;
-    }
-    g_object_unref (file_info);
-  } else {    /* end of enumerator */
-    goto finished;
-  }
-
-  g_file_enumerator_next_files_async (enumerator, 1, G_PRIORITY_DEFAULT,
-                                      operation->cancellable,
-                                      (GAsyncReadyCallback)recursive_operation_got_file,
-                                      operation);
-
-  return;
-
-finished:
-  /* we're done with this dir/enumerator, let's treat the next one */
-  g_object_unref (enumerator);
-  recursive_entry_free (g_queue_pop_head (operation->directories));
-  if (continue_operation) {
-    recursive_operation_next_entry (operation);
-  } else {
-    recursive_operation_free (operation);
-  }
-}
-
-static void
-recursive_operation_got_entry (GFile *directory, GAsyncResult *res, RecursiveOperation *operation)
-{
-  GError *error = NULL;
-  GFileEnumerator *enumerator;
-
-  GRL_DEBUG (__func__);
-
-  enumerator = g_file_enumerate_children_finish (directory, res, &error);
-  if (error) {
-    GRL_WARNING ("Got error: %s", error->message);
-    g_error_free (error);
-    g_object_unref (enumerator);
-    /* we couldn't get the children of this directory, but we probably have
-     * other directories to try */
-    recursive_entry_free (g_queue_pop_head (operation->directories));
-    recursive_operation_next_entry (operation);
-    return;
-  }
-
-  g_file_enumerator_next_files_async (enumerator, 1, G_PRIORITY_DEFAULT,
-                                      operation->cancellable,
-                                      (GAsyncReadyCallback)recursive_operation_got_file,
-                                      operation);
-}
-
-static void
-recursive_operation_next_entry (RecursiveOperation *operation)
-{
-  RecursiveEntry *entry;
-
-  GRL_DEBUG (__func__);
-
-  if (g_cancellable_is_cancelled (operation->cancellable)) {
-    /* We've been cancelled! */
-    GRL_DEBUG ("Operation has been cancelled");
-    if (operation->on_cancel) {
-      operation->on_cancel(NULL, operation);
-    }
-    goto finished;
-  }
-
-  entry = g_queue_peek_head (operation->directories);
-  if (!entry) { /* We've crawled everything, before reaching count */
-    if (operation->on_finish) {
-      operation->on_finish (NULL, operation);
-    }
-    goto finished;
-  }
-
-  g_file_enumerate_children_async (entry->directory, G_FILE_ATTRIBUTE_STANDARD_TYPE ","
-                                   G_FILE_ATTRIBUTE_STANDARD_NAME ","
-                                   G_FILE_ATTRIBUTE_STANDARD_DISPLAY_NAME,
-                                   G_FILE_QUERY_INFO_NOFOLLOW_SYMLINKS,
-                                   G_PRIORITY_DEFAULT,
-                                   operation->cancellable,
-                                   (GAsyncReadyCallback)recursive_operation_got_entry,
-                                   operation);
-
-  return;
-
-finished:
-  recursive_operation_free (operation);
-}
-
-static void
-recursive_operation_initialize (RecursiveOperation *operation, GrlFilesystemSource *source)
-{
-  GList *chosen_paths, *path;
-
-  chosen_paths = source->priv->chosen_paths;
-  if (chosen_paths) {
-    for (path = chosen_paths; path; path = g_list_next (path)) {
-      GFile *directory = g_file_new_for_path (path->data);
-      g_queue_push_tail (operation->directories,
-                         recursive_entry_new (0, directory));
-      g_object_unref (directory);
-    }
-  } else {
-    const gchar *home;
-    GFile *directory;
-
-    home = g_getenv ("HOME");
-    directory = g_file_new_for_path (home);
-    g_queue_push_tail (operation->directories,
-                       recursive_entry_new (0, directory));
-    g_object_unref (directory);
-  }
-}
-
-static gboolean
-cancel_cb (GFileInfo *file_info, RecursiveOperation *operation)
-{
-  GrlFilesystemSource *fs_source;
-
-  if (operation->on_file_data) {
-    GrlMediaSourceSearchSpec *ss =
-      (GrlMediaSourceSearchSpec *) operation->on_file_data;
-    fs_source = GRL_FILESYSTEM_SOURCE (ss->source);
-    g_hash_table_remove (fs_source->priv->cancellables,
-                         GUINT_TO_POINTER (ss->search_id));
-    ss->callback (ss->source, ss->search_id, NULL, 0, ss->user_data, NULL);
-  }
-
-  if (operation->on_dir_data) {
-    /* Remove all monitors */
-    fs_source = GRL_FILESYSTEM_SOURCE (operation->on_dir_data);
-    cancel_monitors (fs_source);
-  }
-  return FALSE;
-}
-
-static gboolean
-finish_cb (GFileInfo *file_info, RecursiveOperation *operation)
-{
-  if (operation->on_file_data) {
-    GrlMediaSourceSearchSpec *ss =
-      (GrlMediaSourceSearchSpec *) operation->on_file_data;
-    g_hash_table_remove (GRL_FILESYSTEM_SOURCE (ss->source)->priv->cancellables,
-                         GUINT_TO_POINTER (ss->search_id));
-    ss->callback (ss->source, ss->search_id, NULL, 0, ss->user_data, NULL);
-  }
-
-  if (operation->on_dir_data) {
-    GRL_FILESYSTEM_SOURCE (operation->on_dir_data)->priv->cancellable_monitors = NULL;
-  }
-
-  return FALSE;
-}
-
-/* return TRUE if more files need to be returned, FALSE if we sent the count */
-static gboolean
-file_cb (GFileInfo *file_info, RecursiveOperation *operation)
-{
-  gchar *needle = NULL;
-  gchar *haystack = NULL;
-  gchar *normalized_needle = NULL;
-  gchar *normalized_haystack = NULL;
-  GrlMediaSourceSearchSpec *ss = operation->on_file_data;
-  gint remaining = -1;
-
-  GRL_DEBUG (__func__);
-
-  if (ss == NULL)
-    return FALSE;
-
-  if (ss->text) {
-    haystack = g_utf8_casefold (g_file_info_get_display_name (file_info), -1);
-    normalized_haystack = g_utf8_normalize (haystack, -1, G_NORMALIZE_ALL);
-
-    needle = g_utf8_casefold (ss->text, -1);
-    normalized_needle = g_utf8_normalize (needle, -1, G_NORMALIZE_ALL);
-  }
-
-  if (!ss->text ||
-      strstr (normalized_haystack, normalized_needle)) {
-    GrlMedia *media = NULL;
-    RecursiveEntry *entry;
-    GFile *file;
-    gchar *path;
-
-    entry = g_queue_peek_head (operation->directories);
-    file = g_file_get_child (entry->directory,
-                             g_file_info_get_name (file_info));
-    path = g_file_get_path (file);
-
-    /* FIXME: both file_is_valid_content() and create_content() are likely to block */
-    if (file_is_valid_content (path, FALSE)) {
-      if (ss->skip) {
-        ss->skip--;
-      } else {
-        media = create_content (NULL, path, ss->flags & GRL_RESOLVE_FAST_ONLY, FALSE);
-      }
-    }
-
-    g_free (path);
-    g_object_unref (file);
-
-    if (media) {
-      ss->count--;
-      if (ss->count == 0) {
-        remaining = 0;
-      }
-      ss->callback (ss->source, ss->search_id, media, remaining, ss->user_data, NULL);
-    }
-  }
-
-  g_free (haystack);
-  g_free (normalized_haystack);
-  g_free (needle);
-  g_free (normalized_needle);
-  return remaining == -1;
-}
-
-static void
-notify_parent_change (GrlMediaSource *source, GFile *child, GrlMediaSourceChangeType change)
-{
-  GFile *parent;
-  GrlMedia *media;
-  gchar *parent_path;
-
-  parent = g_file_get_parent (child);
-  if (parent) {
-    parent_path = g_file_get_path (parent);
-  } else {
-    parent_path = g_strdup ("/");
-  }
-
-  media = create_content (NULL, parent_path, GRL_RESOLVE_FAST_ONLY, parent == NULL);
-  grl_media_source_notify_change (source, media, change, FALSE);
-  g_object_unref (media);
-
-  if (parent) {
-    g_object_unref (parent);
-  }
-  g_free (parent_path);
-}
-
-static void
-directory_changed (GFileMonitor *monitor,
-                   GFile *file,
-                   GFile *other_file,
-                   GFileMonitorEvent event,
-                   gpointer data)
-{
-  GrlMediaSource *source = GRL_MEDIA_SOURCE (data);
-  gchar *file_path, *other_file_path;
-  gchar *file_parent_path = NULL;
-  gchar *other_file_parent_path = NULL;
-  GFile *file_parent, *other_file_parent;
-  GFileInfo *file_info;
-
-  if (event == G_FILE_MONITOR_EVENT_CHANGES_DONE_HINT ||
-      event == G_FILE_MONITOR_EVENT_CREATED) {
-    file_path = g_file_get_path (file);
-    if (file_is_valid_content (file_path, TRUE)) {
-      notify_parent_change (source,
-                            file,
-                            (event == G_FILE_MONITOR_EVENT_CREATED)? GRL_CONTENT_ADDED: GRL_CONTENT_CHANGED);
-      if (event == G_FILE_MONITOR_EVENT_CREATED) {
-        file_info = g_file_query_info (file,
-                                       G_FILE_ATTRIBUTE_STANDARD_TYPE,
-                                       G_FILE_QUERY_INFO_NOFOLLOW_SYMLINKS,
-                                       NULL,
-                                       NULL);
-        if (file_info) {
-          if (g_file_info_get_file_type (file_info) == G_FILE_TYPE_DIRECTORY) {
-            add_monitor (GRL_FILESYSTEM_SOURCE (source), file);
-          }
-          g_object_unref (file_info);
-        }
-      }
-    }
-    g_free (file_path);
-  } else if (event == G_FILE_MONITOR_EVENT_DELETED) {
-    notify_parent_change (source, file, GRL_CONTENT_REMOVED);
-  } else if (event == G_FILE_MONITOR_EVENT_MOVED) {
-    other_file_path = g_file_get_path (other_file);
-    if (file_is_valid_content (other_file_path, TRUE)) {
-      file_parent = g_file_get_parent (file);
-      if (file_parent) {
-        file_parent_path = g_file_get_path (file_parent);
-        g_object_unref (file_parent);
-      } else {
-        file_parent_path = NULL;
-      }
-      other_file_parent = g_file_get_parent (other_file);
-      if (other_file_parent) {
-        other_file_parent_path = g_file_get_path (other_file_parent);
-        g_object_unref (other_file_parent);
-      } else {
-        other_file_parent_path = NULL;
-      }
-
-      if (g_strcmp0 (file_parent_path, other_file_parent_path) == 0) {
-        notify_parent_change (source, file, GRL_CONTENT_CHANGED);
-      } else {
-        notify_parent_change (source, file, GRL_CONTENT_REMOVED);
-        notify_parent_change (source, other_file, GRL_CONTENT_ADDED);
-      }
-    }
-    g_free (file_parent_path);
-    g_free (other_file_parent_path);
-  }
-}
-
-static void
-cancel_monitors (GrlFilesystemSource *fs_source)
-{
-  g_list_foreach (fs_source->priv->monitors,
-                  (GFunc) g_file_monitor_cancel,
-                  NULL);
-  g_list_foreach (fs_source->priv->monitors,
-                  (GFunc) g_object_unref,
-                  NULL);
-  g_list_free (fs_source->priv->monitors);
-  fs_source->priv->monitors = NULL;
-}
-
-static void
-add_monitor (GrlFilesystemSource *fs_source, GFile *dir)
-{
-  GFileMonitor *monitor;
-
-  monitor = g_file_monitor_directory (dir, G_FILE_MONITOR_SEND_MOVED, NULL, NULL);
-  if (monitor) {
-    fs_source->priv->monitors = g_list_prepend (fs_source->priv->monitors,
-                                                monitor);
-    g_signal_connect (monitor,
-                      "changed",
-                      G_CALLBACK (directory_changed),
-                      fs_source);
-  } else {
-    GRL_DEBUG ("Unable to set up monitor in %s\n", g_file_get_path (dir));
-  }
-}
-
-static gboolean
-directory_cb (GFileInfo *dir_info, RecursiveOperation *operation)
-{
-  RecursiveEntry *entry;
-  GFile *dir;
-  GrlFilesystemSource *fs_source;
-
-  fs_source = GRL_FILESYSTEM_SOURCE (operation->on_dir_data);
-  entry = g_queue_peek_head (operation->directories);
-  dir = g_file_get_child (entry->directory,
-                          g_file_info_get_name (dir_info));
-
-  add_monitor (fs_source, dir);
-  g_object_unref (dir);
-
-  return TRUE;
-}
-
-/* ================== API Implementation ================ */
-
-static const GList *
-grl_filesystem_source_supported_keys (GrlMetadataSource *source)
-{
-  static GList *keys = NULL;
-  if (!keys) {
-    keys = grl_metadata_key_list_new (GRL_METADATA_KEY_ID,
-                                      GRL_METADATA_KEY_TITLE,
-                                      GRL_METADATA_KEY_URL,
-                                      GRL_METADATA_KEY_MIME,
-                                      GRL_METADATA_KEY_DATE,
-                                      GRL_METADATA_KEY_CHILDCOUNT,
-                                      NULL);
-  }
-  return keys;
-}
-
-static void
-grl_filesystem_source_browse (GrlMediaSource *source,
-                              GrlMediaSourceBrowseSpec *bs)
-{
-  const gchar *id;
-  GList *chosen_paths;
-
-  GRL_DEBUG ("grl_filesystem_source_browse");
-
-  id = grl_media_get_id (bs->container);
-  chosen_paths = GRL_FILESYSTEM_SOURCE(source)->priv->chosen_paths;
-  if (!id && chosen_paths) {
-    guint remaining = g_list_length (chosen_paths);
-
-    if (remaining == 1) {
-        produce_from_path (bs, chosen_paths->data);
-    } else {
-      for (; chosen_paths; chosen_paths = g_list_next (chosen_paths)) {
-        GrlMedia *content = create_content (NULL,
-                                            (gchar *) chosen_paths->data,
-                                            GRL_RESOLVE_FAST_ONLY,
-                                            FALSE);
-
-        bs->callback (source,
-                      bs->browse_id,
-                      content,
-                      --remaining,
-                      bs->user_data,
-                      NULL);
-      }
-    }
-  } else {
-    produce_from_path (bs, id ? id : G_DIR_SEPARATOR_S);
-  }
-}
-
-static void grl_filesystem_source_search (GrlMediaSource *source,
-                                          GrlMediaSourceSearchSpec *ss)
-{
-  RecursiveOperation *operation;
-  GrlFilesystemSource *fs_source;
-
-  GRL_DEBUG ("grl_filesystem_source_search");
-
-  fs_source = GRL_FILESYSTEM_SOURCE (source);
-
-  operation = recursive_operation_new ();
-  operation->on_cancel = cancel_cb;
-  operation->on_finish = finish_cb;
-  operation->on_file = file_cb;
-  operation->on_file_data = ss;
-  operation->max_depth = fs_source->priv->max_search_depth;
-  g_hash_table_insert (GRL_FILESYSTEM_SOURCE (source)->priv->cancellables,
-                       GUINT_TO_POINTER (ss->search_id),
-                       operation->cancellable);
-
-  recursive_operation_initialize (operation, fs_source);
-  recursive_operation_next_entry (operation);
-}
-
-static void
-grl_filesystem_source_metadata (GrlMediaSource *source,
-                                GrlMediaSourceMetadataSpec *ms)
-{
-  const gchar *path;
-  const gchar *id;
-
-  GRL_DEBUG ("grl_filesystem_source_metadata");
-
-  id = grl_media_get_id (ms->media);
-  path = id ? id : G_DIR_SEPARATOR_S;
-
-  if (g_file_test (path, G_FILE_TEST_EXISTS)) {
-    create_content (ms->media, path,
-		    ms->flags & GRL_RESOLVE_FAST_ONLY,
-		    !id);
-    ms->callback (ms->source, ms->media, ms->user_data, NULL);
-  } else {
-    GError *error = g_error_new (GRL_CORE_ERROR,
-				 GRL_CORE_ERROR_METADATA_FAILED,
-				 "File '%s' does not exist",
-				 path);
-    ms->callback (ms->source, ms->media, ms->user_data, error);
-    g_error_free (error);
-  }
-}
-
-static gboolean
-grl_filesystem_test_media_from_uri (GrlMediaSource *source,
-                                    const gchar *uri)
-{
-  gchar *path, *scheme;
-  GError *error = NULL;
-  gboolean ret = FALSE;
-
-  GRL_DEBUG ("grl_filesystem_test_media_from_uri");
-
-  scheme = g_uri_parse_scheme (uri);
-  ret = (g_strcmp0(scheme, "file") == 0);
-  g_free (scheme);
-  if (!ret)
-    return ret;
-
-  path = g_filename_from_uri (uri, NULL, &error);
-  if (error != NULL) {
-    g_error_free (error);
-    return FALSE;
-  }
-
-  ret = file_is_valid_content (path, TRUE);
-
-  g_free (path);
-  return ret;
-}
-
-static void grl_filesystem_get_media_from_uri (GrlMediaSource *source,
-                                               GrlMediaSourceMediaFromUriSpec *mfus)
-{
-  gchar *path, *scheme;
-  GError *error = NULL;
-  gboolean ret = FALSE;
-  GrlMedia *media;
-
-  GRL_DEBUG ("grl_filesystem_get_media_from_uri");
-
-  scheme = g_uri_parse_scheme (mfus->uri);
-  ret = (g_strcmp0(scheme, "file") == 0);
-  g_free (scheme);
-  if (!ret) {
-    error = g_error_new (GRL_CORE_ERROR,
-                         GRL_CORE_ERROR_MEDIA_FROM_URI_FAILED,
-                         "Cannot create media from '%s'", mfus->uri);
-    mfus->callback (source, NULL, mfus->user_data, error);
-    g_clear_error (&error);
-    return;
-  }
-
-  path = g_filename_from_uri (mfus->uri, NULL, &error);
-  if (error != NULL) {
-    GError *new_error;
-    new_error = g_error_new (GRL_CORE_ERROR,
-                         GRL_CORE_ERROR_MEDIA_FROM_URI_FAILED,
-                         "Cannot create media from '%s', error message: %s",
-                         mfus->uri, error->message);
-    g_clear_error (&error);
-    mfus->callback (source, NULL, mfus->user_data, new_error);
-    g_clear_error (&new_error);
-    goto beach;
-  }
-
-  /* FIXME: this is a blocking call, not sure we want that in here */
-  /* Note: we assume create_content() never returns NULL, which seems to be true */
-  media = create_content (NULL, path, mfus->flags & GRL_RESOLVE_FAST_ONLY,
-                          FALSE);
-  mfus->callback (source, media, mfus->user_data, NULL);
-
-beach:
-  g_free (path);
-}
-
-static void
-grl_filesystem_source_cancel (GrlMediaSource *source, guint operation_id)
-{
-  GCancellable *cancellable;
-  GrlFilesystemSourcePrivate *priv;
-
-  priv = GRL_FILESYSTEM_SOURCE (source)->priv;
-
-  cancellable =
-      G_CANCELLABLE (g_hash_table_lookup (priv->cancellables,
-                                          GUINT_TO_POINTER (operation_id)));
-  if (cancellable)
-    g_cancellable_cancel (cancellable);
-}
-
-static gboolean
-grl_filesystem_source_notify_change_start (GrlMediaSource *source,
-                                           GError **error)
-{
-  GrlFilesystemSource *fs_source;
-  RecursiveOperation *operation;
-
-  GRL_DEBUG (__func__);
-
-  fs_source = GRL_FILESYSTEM_SOURCE (source);
-  operation = recursive_operation_new ();
-  operation->on_cancel = cancel_cb;
-  operation->on_finish = finish_cb;
-  operation->on_dir = directory_cb;
-  operation->on_dir_data = fs_source;
-  operation->max_depth = fs_source->priv->max_search_depth;
-
-  fs_source->priv->cancellable_monitors = operation->cancellable;
-
-  recursive_operation_initialize (operation, fs_source);
-  recursive_operation_next_entry (operation);
-
-  return TRUE;
-}
-
-static gboolean
-grl_filesystem_source_notify_change_stop (GrlMediaSource *source,
-                                          GError **error)
-{
-  GrlFilesystemSource *fs_source = GRL_FILESYSTEM_SOURCE (source);
-
-  /* Check if notifying is being initialized */
-  if (fs_source->priv->cancellable_monitors) {
-    g_cancellable_cancel (fs_source->priv->cancellable_monitors);
-    fs_source->priv->cancellable_monitors = NULL;
-  } else {
-    /* Cancel and remove all monitors */
-    cancel_monitors (fs_source);
-  }
-
-  return TRUE;
-}
diff --git a/src/filesystem/grl-filesystem.h b/src/filesystem/grl-filesystem.h
deleted file mode 100644
index 428c61b..0000000
--- a/src/filesystem/grl-filesystem.h
+++ /dev/null
@@ -1,81 +0,0 @@
-/*
- * Copyright (C) 2010, 2011 Igalia S.L.
- *
- * Contact: Iago Toral Quiroga <itoral igalia 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_FILESYSTEM_SOURCE_H_
-#define _GRL_FILESYSTEM_SOURCE_H_
-
-#include <grilo.h>
-
-#define GRL_FILESYSTEM_SOURCE_TYPE              \
-  (grl_filesystem_source_get_type ())
-
-#define GRL_FILESYSTEM_SOURCE(obj)                              \
-  (G_TYPE_CHECK_INSTANCE_CAST ((obj),                           \
-                               GRL_FILESYSTEM_SOURCE_TYPE,      \
-                               GrlFilesystemSource))
-
-#define GRL_IS_FILESYSTEM_SOURCE(obj)                           \
-  (G_TYPE_CHECK_INSTANCE_TYPE ((obj),                           \
-                               GRL_FILESYSTEM_SOURCE_TYPE))
-
-#define GRL_FILESYSTEM_SOURCE_CLASS(klass)              \
-  (G_TYPE_CHECK_CLASS_CAST((klass),                     \
-                           GRL_FILESYSTEM_SOURCE_TYPE,  \
-                           GrlFilesystemSourceClass))
-
-#define GRL_IS_FILESYSTEM_SOURCE_CLASS(klass)           \
-  (G_TYPE_CHECK_CLASS_TYPE((klass)                      \
-                           GRL_FILESYSTEM_SOURCE_TYPE))
-
-#define GRL_FILESYSTEM_SOURCE_GET_CLASS(obj)                    \
-  (G_TYPE_INSTANCE_GET_CLASS ((obj),                            \
-                              GRL_FILESYSTEM_SOURCE_TYPE,       \
-                              GrlFilesystemSourceClass))
-
-/* --- Grilo Configuration --- */
-#define GRILO_CONF_CHOSEN_PATH "base-path"
-#define GRILO_CONF_MAX_SEARCH_DEPTH "maximum-search-depth"
-#define GRILO_CONF_MAX_SEARCH_DEPTH_DEFAULT 6
-
-
-typedef struct _GrlFilesystemSource GrlFilesystemSource;
-typedef struct _GrlFilesystemSourcePrivate GrlFilesystemSourcePrivate;
-
-struct _GrlFilesystemSource {
-
-  GrlMediaSource parent;
-
-  /*< private >*/
-  GrlFilesystemSourcePrivate *priv;
-};
-
-typedef struct _GrlFilesystemSourceClass GrlFilesystemSourceClass;
-
-struct _GrlFilesystemSourceClass {
-
-  GrlMediaSourceClass parent_class;
-
-};
-
-GType grl_filesystem_source_get_type (void);
-
-#endif /* _GRL_FILESYSTEM_SOURCE_H_ */
diff --git a/src/filesystem/grl-filesystem.xml b/src/filesystem/grl-filesystem.xml
deleted file mode 100644
index cfb9bc6..0000000
--- a/src/filesystem/grl-filesystem.xml
+++ /dev/null
@@ -1,9 +0,0 @@
-<plugin>
-  <info>
-    <name>Filesystem</name>
-    <description>A plugin for browsing the filesystem</description>
-    <author>Igalia S.L.</author>
-    <license>LGPL</license>
-    <site>http://www.igalia.com</site>
-  </info>
-</plugin>
diff --git a/src/flickr/Makefile.am b/src/flickr/Makefile.am
deleted file mode 100644
index 66da488..0000000
--- a/src/flickr/Makefile.am
+++ /dev/null
@@ -1,40 +0,0 @@
-#
-# Makefile.am
-#
-# Author: Juan A. Suarez Romero <jasuarez igalia com>
-#
-# Copyright (C) 2010, 2011 Igalia S.L. All rights reserved.
-
-libplugin_LTLIBRARIES	= libgrlflickr.la
-
-libgrlflickr_la_CFLAGS =	\
-	$(DEPS_CFLAGS)		\
-	$(XML_CFLAGS)		\
-	$(GRLNET_CFLAGS)
-
-libgrlflickr_la_LIBADD =	\
-	$(DEPS_LIBS)		\
-	$(XML_LIBS)		\
-	$(GRLNET_LIBS)
-
-libgrlflickr_la_LDFLAGS = \
-	-module		  \
-	-avoid-version
-
-libgrlflickr_la_SOURCES =	\
-	grl-flickr.c		\
-	grl-flickr.h		\
-	gflickr.c		\
-	gflickr.h
-
-libplugindir = $(GRL_PLUGINS_DIR)
-flickrlibxmldir = $(GRL_PLUGINS_CONF_DIR)
-flickrlibxml_DATA = $(FLICKR_PLUGIN_ID).xml
-
-EXTRA_DIST = $(flickrlibxml_DATA)
-
-MAINTAINERCLEANFILES =	\
-	*.in		\
-	*~
-
-DISTCLEANFILES = $(MAINTAINERCLEANFILES)
diff --git a/src/flickr/gflickr.c b/src/flickr/gflickr.c
deleted file mode 100644
index 695c7dc..0000000
--- a/src/flickr/gflickr.c
+++ /dev/null
@@ -1,1196 +0,0 @@
-#include "gflickr.h"
-#include "grl-flickr.h"       /* log domain */
-
-#include <libxml/xpath.h>
-#include <gio/gio.h>
-#include <string.h>
-
-#include <grilo.h>
-#include <net/grl-net.h>
-
-
-#define G_FLICKR_GET_PRIVATE(object)            \
-  (G_TYPE_INSTANCE_GET_PRIVATE((object),        \
-                               G_FLICKR_TYPE,   \
-                               GFlickrPrivate))
-
-#define GRL_LOG_DOMAIN_DEFAULT flickr_log_domain
-
-#define FLICKR_PHOTO_ORIG_URL                           \
-  "http://farm%s.static.flickr.com/%s/%s_%s_o.%s";
-
-#define FLICKR_PHOTO_THUMB_URL                          \
-  "http://farm%s.static.flickr.com/%s/%s_%s_t.jpg";
-
-#define FLICKR_PHOTO_LARGEST_URL                        \
-  "http://farm%s.static.flickr.com/%s/%s_%s_b.jpg";
-
-#define FLICKR_ENDPOINT  "http://api.flickr.com/services/rest/?";
-#define FLICKR_AUTHPOINT "http://flickr.com/services/auth/?";
-
-#define FLICKR_PHOTOS_SEARCH_METHOD       "flickr.photos.search"
-#define FLICKR_PHOTOS_GETINFO_METHOD      "flickr.photos.getInfo"
-#define FLICKR_PHOTOS_GETRECENT_METHOD    "flickr.photos.getRecent"
-#define FLICKR_PHOTOSETS_GETLIST_METHOD   "flickr.photosets.getList"
-#define FLICKR_PHOTOSETS_GETPHOTOS_METHOD "flickr.photosets.getPhotos"
-#define FLICKR_TAGS_GETHOTLIST_METHOD     "flickr.tags.getHotList"
-#define FLICKR_AUTH_GETFROB_METHOD        "flickr.auth.getFrob"
-#define FLICKR_AUTH_GETTOKEN_METHOD       "flickr.auth.getToken"
-#define FLICKR_AUTH_CHECKTOKEN_METHOD     "flickr.auth.checkToken"
-
-#define FLICKR_PHOTOS_SEARCH                            \
-  FLICKR_ENDPOINT                                       \
-  "api_key=%s"                                          \
-  "&api_sig=%s"                                         \
-  "&method=" FLICKR_PHOTOS_SEARCH_METHOD                \
-  "&user_id=%s"                                         \
-  "&extras=media,date_taken,owner_name,url_o,url_t"     \
-  "&per_page=%d"                                        \
-  "&page=%d"                                            \
-  "&tags=%s"                                            \
-  "&text=%s"                                            \
-  "%s"
-
-#define FLICKR_PHOTOS_GETRECENT                         \
-  FLICKR_ENDPOINT                                       \
-  "api_key=%s"                                          \
-  "&api_sig=%s"                                         \
-  "&method=" FLICKR_PHOTOS_GETRECENT_METHOD             \
-  "&extras=media,date_taken,owner_name,url_o,url_t"     \
-  "&per_page=%d"                                        \
-  "&page=%d"                                            \
-  "%s"
-
-#define FLICKR_PHOTOSETS_GETLIST                \
-  FLICKR_ENDPOINT                               \
-  "api_key=%s"                                  \
-  "&api_sig=%s"                                 \
-  "&method=" FLICKR_PHOTOSETS_GETLIST_METHOD    \
-  "%s"                                          \
-  "%s"
-
-#define FLICKR_PHOTOSETS_GETPHOTOS                      \
-  FLICKR_ENDPOINT                                       \
-  "api_key=%s"                                          \
-  "&api_sig=%s"                                         \
-  "&method=" FLICKR_PHOTOSETS_GETPHOTOS_METHOD          \
-  "&photoset_id=%s"                                     \
-  "&extras=media,date_taken,owner_name,url_o,url_t"     \
-  "&per_page=%d"                                        \
-  "&page=%d"                                            \
-  "%s"
-
-#define FLICKR_TAGS_GETHOTLIST                          \
-  FLICKR_ENDPOINT                                       \
-  "api_key=%s"                                          \
-  "&api_sig=%s"                                         \
-  "&method=" FLICKR_TAGS_GETHOTLIST_METHOD              \
-  "&count=%d"                                           \
-  "%s"
-
-#define FLICKR_PHOTOS_GETINFO                   \
-  FLICKR_ENDPOINT                               \
-  "api_key=%s"                                  \
-  "&api_sig=%s"                                 \
-  "&method=" FLICKR_PHOTOS_GETINFO_METHOD       \
-  "&photo_id=%ld"                               \
-  "%s"
-
-#define FLICKR_AUTH_GETFROB                     \
-  FLICKR_ENDPOINT                               \
-  "api_key=%s"                                  \
-  "&api_sig=%s"                                 \
-  "&method=" FLICKR_AUTH_GETFROB_METHOD
-
-#define FLICKR_AUTH_GETTOKEN                    \
-  FLICKR_ENDPOINT                               \
-  "api_key=%s"                                  \
-  "&api_sig=%s"                                 \
-  "&method=" FLICKR_AUTH_GETTOKEN_METHOD        \
-  "&frob=%s"
-
-#define FLICKR_AUTH_CHECKTOKEN                  \
-  FLICKR_ENDPOINT                               \
-  "api_key=%s"                                  \
-  "&api_sig=%s"                                 \
-  "&method=" FLICKR_AUTH_CHECKTOKEN_METHOD      \
-  "&auth_token=%s"
-
-#define FLICKR_AUTH_LOGINLINK                   \
-  FLICKR_AUTHPOINT                              \
-  "api_key=%s"                                  \
-  "&api_sig=%s"                                 \
-  "&frob=%s"                                    \
-  "&perms=%s"
-
-typedef void (*ParseXML) (const gchar *xml_result, gpointer user_data);
-
-typedef struct {
-  GFlickr *flickr;
-  ParseXML parse_xml;
-  GFlickrHashTableCb hashtable_cb;
-  GFlickrListCb list_cb;
-  gpointer user_data;
-} GFlickrData;
-
-struct _GFlickrPrivate {
-  gchar *api_key;
-  gchar *auth_secret;
-  gchar *auth_token;
-  gint per_page;
-
-  GrlNetWc *wc;
-};
-
-static void g_flickr_finalize (GObject *object);
-
-/* -------------------- GOBJECT -------------------- */
-
-G_DEFINE_TYPE (GFlickr, g_flickr, G_TYPE_OBJECT);
-
-static void
-g_flickr_class_init (GFlickrClass *klass)
-{
-  GObjectClass *gobject_class = G_OBJECT_CLASS (klass);
-  gobject_class->finalize = g_flickr_finalize;
-
-  g_type_class_add_private (klass, sizeof (GFlickrPrivate));
-}
-
-static void
-g_flickr_init (GFlickr *f)
-{
-  f->priv = G_FLICKR_GET_PRIVATE (f);
-  f->priv->per_page = 100;
-}
-
-static void
-g_flickr_finalize (GObject *object)
-{
-  GFlickr *f = G_FLICKR (object);
-  g_free (f->priv->api_key);
-  g_free (f->priv->auth_token);
-  g_free (f->priv->auth_secret);
-
-  if (f->priv->wc)
-    g_object_unref (f->priv->wc);
-
-  G_OBJECT_CLASS (g_flickr_parent_class)->finalize (object);
-}
-
-GFlickr *
-g_flickr_new (const gchar *api_key, const gchar *auth_secret, const gchar *auth_token)
-{
-  g_return_val_if_fail (api_key && auth_secret, NULL);
-
-  GFlickr *f = g_object_new (G_FLICKR_TYPE, NULL);
-  f->priv->api_key = g_strdup (api_key);
-  f->priv->auth_secret = g_strdup (auth_secret);
-  f->priv->auth_token = g_strdup (auth_token);
-
-  return f;
-}
-
-/* -------------------- PRIVATE API -------------------- */
-
-static gchar *
-get_api_sig (const gchar *secret, ...)
-{
-  GHashTable *hash;
-  GList *key_iter;
-  GList *keys;
-  GString *to_sign;
-  gchar *api_sig;
-  gchar *key;
-  gchar *value;
-  gint text_size = strlen (secret);
-  va_list va_params;
-
-  hash = g_hash_table_new (g_str_hash, g_str_equal);
-
-  va_start (va_params, secret);
-  while ((key = va_arg (va_params, gchar *))) {
-    text_size += strlen (key);
-    value = va_arg (va_params, gchar *);
-    text_size += strlen (value);
-    g_hash_table_insert (hash, key, value);
-  }
-  va_end (va_params);
-
-  to_sign = g_string_sized_new (text_size);
-  g_string_append (to_sign, secret);
-
-  keys = g_hash_table_get_keys (hash);
-  keys = g_list_sort (keys, (GCompareFunc) g_strcmp0);
-  for (key_iter = keys; key_iter; key_iter = g_list_next (key_iter)) {
-    g_string_append (to_sign, key_iter->data);
-    g_string_append (to_sign, g_hash_table_lookup (hash, key_iter->data));
-  }
-
-  api_sig = g_compute_checksum_for_string (G_CHECKSUM_MD5, to_sign->str, -1);
-  g_hash_table_unref (hash);
-  g_list_free (keys);
-  g_string_free (to_sign, TRUE);
-
-  return api_sig;
-}
-
-static gchar *
-get_xpath_element (const gchar *content,
-                   const gchar *xpath_element)
-{
-  gchar *element = NULL;
-  xmlDocPtr xmldoc = NULL;
-  xmlXPathContextPtr xpath_ctx = NULL;
-  xmlXPathObjectPtr xpath_res = NULL;
-
-  xmldoc = xmlReadMemory (content, xmlStrlen ((xmlChar *) content), NULL, NULL,
-                          XML_PARSE_RECOVER | XML_PARSE_NOBLANKS);
-  if (xmldoc) {
-    xpath_ctx = xmlXPathNewContext (xmldoc);
-    if (xpath_ctx) {
-      xpath_res = xmlXPathEvalExpression ((xmlChar *) xpath_element, xpath_ctx);
-      if (xpath_res && xpath_res->nodesetval->nodeTab) {
-        element =
-          (gchar *) xmlNodeListGetString (xmldoc,
-                                          xpath_res->nodesetval->nodeTab[0]->xmlChildrenNode,
-                                          1);
-      }
-    }
-  }
-
-  /* Free data */
-  if (xmldoc) {
-    xmlFreeDoc (xmldoc);
-  }
-
-  if (xpath_ctx) {
-    xmlXPathFreeContext (xpath_ctx);
-  }
-
-  if (xpath_res) {
-    xmlXPathFreeObject (xpath_res);
-  }
-
-  return element;
-}
-
-static gboolean
-result_is_correct (xmlNodePtr node)
-{
-  gboolean correct = FALSE;
-  xmlChar *stat;
-
-  if (xmlStrcmp (node->name, (const xmlChar *) "rsp") == 0) {
-    stat = xmlGetProp (node, (const xmlChar *) "stat");
-    if (stat && xmlStrcmp (stat, (const xmlChar *) "ok") == 0) {
-      correct = TRUE;
-      xmlFree (stat);
-    }
-  }
-
-  return correct;
-}
-
-static void
-add_node (xmlNodePtr node, GHashTable *photo)
-{
-  xmlAttrPtr attr;
-
-  for (attr = node->properties; attr != NULL; attr = attr->next) {
-    g_hash_table_insert (photo,
-                         g_strconcat ((const gchar *) node->name,
-                                      "_",
-                                      (const gchar *) attr->name,
-                                      NULL),
-                         (gchar *) xmlGetProp (node, attr->name));
-  }
-}
-
-static GHashTable *
-get_photo (xmlNodePtr node)
-{
-  GHashTable *photo = g_hash_table_new_full (g_str_hash,
-                                             g_str_equal,
-                                             g_free,
-                                             g_free);
-
-  /* Add photo node */
-  add_node (node, photo);
-
-  /* Add children nodes with their properties */
-
-  node = node->xmlChildrenNode;
-
-  while (node) {
-    if (xmlStrcmp (node->name, (const xmlChar *) "owner") == 0 ||
-        xmlStrcmp (node->name, (const xmlChar *) "dates") == 0) {
-      add_node (node, photo);
-    } else if (xmlStrcmp (node->name, (const xmlChar *) "title") == 0 ||
-               xmlStrcmp (node->name, (const xmlChar *) "description") == 0) {
-      g_hash_table_insert (photo,
-                           g_strdup ((const gchar *) node->name),
-                           (gchar *) xmlNodeGetContent (node));
-    }
-
-    node = node->next;
-  }
-
-  return photo;
-}
-
-static GHashTable *
-get_photoset (xmlNodePtr node)
-{
-  GHashTable *photoset = g_hash_table_new_full (g_str_hash,
-                                             g_str_equal,
-                                             g_free,
-                                             g_free);
-
-  /* Add photoset node */
-  add_node (node, photoset);
-
-  /* Add children nodes with their properties */
-  node = node->xmlChildrenNode;
-
-  while (node) {
-    g_hash_table_insert (photoset,
-                         g_strdup ((const gchar *) node->name),
-                         (gchar *) xmlNodeGetContent (node));
-    node = node->next;
-  }
-
-  return photoset;
-}
-
-static gchar *
-get_tag (xmlNodePtr node)
-{
-  if (xmlStrcmp (node->name, (const xmlChar *) "tag") == 0) {
-    return (gchar *) xmlNodeGetContent (node);
-  } else {
-    return NULL;
-  }
-}
-
-static GHashTable *
-get_token_info (xmlNodePtr node)
-{
-  GHashTable *token = g_hash_table_new_full (g_str_hash,
-                                             g_str_equal,
-                                             g_free,
-                                             g_free);
-  node = node->xmlChildrenNode;
-
-  while (node) {
-    g_hash_table_insert (token,
-                         g_strdup ((const gchar *) node->name),
-                         (gchar *) xmlNodeGetContent (node));
-    add_node (node, token);
-    node = node->next;
-  }
-
-  return token;
-}
-
-static void
-process_photo_result (const gchar *xml_result, gpointer user_data)
-{
-  xmlDocPtr doc;
-  xmlNodePtr node;
-  GFlickrData *data = (GFlickrData *) user_data;
-  GHashTable *photo;
-
-  doc = xmlReadMemory (xml_result, xmlStrlen ((xmlChar*) xml_result), NULL,
-                       NULL, XML_PARSE_RECOVER | XML_PARSE_NOBLANKS);
-  node = xmlDocGetRootElement (doc);
-
-  /* Check result is ok */
-  if (!node || !result_is_correct (node)) {
-    data->hashtable_cb (data->flickr, NULL, data->user_data);
-  } else {
-    node = node->xmlChildrenNode;
-
-    photo = get_photo (node);
-    data->hashtable_cb (data->flickr, photo, data->user_data);
-    g_hash_table_unref (photo);
-  }
-  g_object_unref (data->flickr);
-  g_slice_free (GFlickrData, data);
-  xmlFreeDoc (doc);
-}
-
-static void
-process_photolist_result (const gchar *xml_result, gpointer user_data)
-{
-  GFlickrData *data = (GFlickrData *) user_data;
-  GList *photolist = NULL;
-  xmlDocPtr doc;
-  xmlNodePtr node;
-
-  doc = xmlReadMemory (xml_result, xmlStrlen ((xmlChar*) xml_result), NULL,
-                       NULL, XML_PARSE_RECOVER | XML_PARSE_NOBLANKS);
-  node = xmlDocGetRootElement (doc);
-
-  /* Check result is ok */
-  if (!node || !result_is_correct (node)) {
-    data->list_cb (data->flickr, NULL, data->user_data);
-  } else {
-    node = node->xmlChildrenNode;
-
-    /* Now we're at "photo pages" node */
-    node = node->xmlChildrenNode;
-    while (node) {
-      photolist = g_list_prepend (photolist, get_photo (node));
-      node = node->next;
-    }
-
-    data->list_cb (data->flickr, g_list_reverse (photolist), data->user_data);
-    g_list_foreach (photolist, (GFunc) g_hash_table_unref, NULL);
-    g_list_free (photolist);
-  }
-  g_object_unref (data->flickr);
-  g_slice_free (GFlickrData, data);
-  xmlFreeDoc (doc);
-}
-
-static void
-process_taglist_result (const gchar *xml_result, gpointer user_data)
-{
-  GFlickrData *data = (GFlickrData *) user_data;
-  GList *taglist = NULL;
-  xmlDocPtr doc;
-  xmlNodePtr node;
-
-  doc = xmlReadMemory (xml_result, xmlStrlen ((xmlChar*) xml_result), NULL,
-                       NULL, XML_PARSE_RECOVER | XML_PARSE_NOBLANKS);
-  node = xmlDocGetRootElement (doc);
-
-  /* Check if result is OK */
-  if (!node || !result_is_correct (node)) {
-    data->list_cb (data->flickr, NULL, data->user_data);
-  } else {
-    node = node->xmlChildrenNode;
-
-    /* Now we're at "hot tags" node */
-    node = node->xmlChildrenNode;
-    while (node) {
-      taglist = g_list_prepend (taglist, get_tag (node));
-      node = node->next;
-    }
-
-    data->list_cb (data->flickr, g_list_reverse (taglist), data->user_data);
-    g_list_foreach (taglist, (GFunc) g_free, NULL);
-    g_list_free (taglist);
-  }
-  g_object_unref (data->flickr);
-  g_slice_free (GFlickrData, data);
-  xmlFreeDoc (doc);
-}
-
-static void
-process_photosetslist_result (const gchar *xml_result, gpointer user_data)
-{
-  GFlickrData *data = (GFlickrData *) user_data;
-  GList *photosets = NULL;
-  xmlDocPtr doc;
-  xmlNodePtr node;
-
-  doc = xmlReadMemory (xml_result, xmlStrlen ((xmlChar*) xml_result), NULL,
-                       NULL, XML_PARSE_RECOVER | XML_PARSE_NOBLANKS);
-  node = xmlDocGetRootElement (doc);
-
-  /* Check if result is OK */
-  if (!node || !result_is_correct (node)) {
-    data->list_cb (data->flickr, NULL, data->user_data);
-  } else {
-    node = node->xmlChildrenNode;
-
-    /* Now we're at "photosets" node */
-    node = node->xmlChildrenNode;
-    while (node) {
-      photosets = g_list_prepend (photosets, get_photoset (node));
-      node = node->next;
-    }
-
-    data->list_cb (data->flickr, g_list_reverse (photosets), data->user_data);
-    g_list_foreach (photosets, (GFunc) g_hash_table_unref, NULL);
-    g_list_free (photosets);
-  }
-  g_object_unref (data->flickr);
-  g_slice_free (GFlickrData, data);
-  xmlFreeDoc (doc);
-}
-
-static void
-process_photosetsphotos_result (const gchar *xml_result, gpointer user_data)
-{
-  GFlickrData *data = (GFlickrData *) user_data;
-  GList *list = NULL;
-  xmlDocPtr doc;
-  xmlNodePtr node;
-
-  doc = xmlReadMemory (xml_result, xmlStrlen ((xmlChar*) xml_result), NULL,
-                       NULL, XML_PARSE_RECOVER | XML_PARSE_NOBLANKS);
-  node = xmlDocGetRootElement (doc);
-
-  /* Check result is ok */
-  if (!node || !result_is_correct (node)) {
-    data->list_cb (data->flickr, NULL, data->user_data);
-  } else {
-    node = node->xmlChildrenNode;
-
-    /* Now we're at "photoset page" node */
-    node = node->xmlChildrenNode;
-    while (node) {
-      list = g_list_prepend (list, get_photo (node));
-      node = node->next;
-    }
-
-    data->list_cb (data->flickr, g_list_reverse (list), data->user_data);
-    g_list_foreach (list, (GFunc) g_hash_table_unref, NULL);
-    g_list_free (list);
-  }
-  g_object_unref (data->flickr);
-  g_slice_free (GFlickrData, data);
-  xmlFreeDoc (doc);
-}
-
-static void
-process_token_result (const gchar *xml_result, gpointer user_data)
-{
-  xmlDocPtr doc;
-  xmlNodePtr node;
-  GFlickrData *data = (GFlickrData *) user_data;
-  GHashTable *token;
-
-  doc = xmlReadMemory (xml_result, xmlStrlen ((xmlChar*) xml_result), NULL,
-                       NULL, XML_PARSE_RECOVER | XML_PARSE_NOBLANKS);
-  node = xmlDocGetRootElement (doc);
-
-  /* Check if result is OK */
-  if (!node || !result_is_correct (node)) {
-    data->hashtable_cb (data->flickr, NULL, data->user_data);
-  } else {
-    node = node->xmlChildrenNode;
-    token = get_token_info (node);
-    data->hashtable_cb (data->flickr, token, data->user_data);
-    g_hash_table_unref (token);
-  }
-
-  g_object_unref (data->flickr);
-  g_slice_free (GFlickrData, data);
-  xmlFreeDoc (doc);
-}
-
-inline static GrlNetWc *
-get_wc (GFlickr *f)
-{
-  if (!f->priv->wc)
-    f->priv->wc = grl_net_wc_new ();
-
-  return f->priv->wc;
-}
-
-static void
-read_done_cb (GObject *source_object,
-              GAsyncResult *res,
-              gpointer user_data)
-{
-  gchar *content = NULL;
-  GError *wc_error = NULL;
-  GFlickrData *data = (GFlickrData *) user_data;
-
-  grl_net_wc_request_finish (GRL_NET_WC (source_object),
-                         res,
-                         &content,
-                         NULL,
-                         &wc_error);
-
-  data->parse_xml (content, user_data);
-}
-
-static void
-read_url_async (GFlickr *f,
-                const gchar *url,
-                gpointer user_data)
-{
-  GRL_DEBUG ("Opening '%s'", url);
-  grl_net_wc_request_async (get_wc (f),
-                        url,
-                        NULL,
-                        read_done_cb,
-                        user_data);
-}
-
-/* -------------------- PUBLIC API -------------------- */
-
-void
-g_flickr_set_per_page (GFlickr *f, gint per_page)
-{
-  g_return_if_fail (G_IS_FLICKR (f));
-
-  f->priv->per_page = per_page;
-}
-
-void
-g_flickr_photos_getInfo (GFlickr *f,
-                         glong photo_id,
-                         GFlickrHashTableCb callback,
-                         gpointer user_data)
-{
-  gchar *auth;
-
-  g_return_if_fail (G_IS_FLICKR (f));
-
-  gchar *str_photo_id = g_strdup_printf ("%ld", photo_id);
-  gchar *api_sig = get_api_sig (f->priv->auth_secret,
-                                "api_key", f->priv->api_key,
-                                "method", FLICKR_PHOTOS_GETINFO_METHOD,
-                                "photo_id", str_photo_id,
-                                f->priv->auth_token? "auth_token": "",
-                                f->priv->auth_token? f->priv->auth_token: "",
-                                NULL);
-  g_free (str_photo_id);
-
-  /* Build the request */
-  if (f->priv->auth_token) {
-    auth = g_strdup_printf ("&auth_token=%s", f->priv->auth_token);
-  } else {
-    auth = g_strdup ("");
-  }
-
-  gchar *request = g_strdup_printf (FLICKR_PHOTOS_GETINFO,
-                                    f->priv->api_key,
-                                    api_sig,
-                                    photo_id,
-                                    auth);
-  g_free (api_sig);
-  g_free (auth);
-
-  GFlickrData *gfd = g_slice_new (GFlickrData);
-  gfd->flickr = g_object_ref (f);
-  gfd->parse_xml = process_photo_result;
-  gfd->hashtable_cb = callback;
-  gfd->user_data = user_data;
-
-  read_url_async (f, request, gfd);
-  g_free (request);
-}
-
-void
-g_flickr_photos_search (GFlickr *f,
-                        const gchar *user_id,
-                        const gchar *text,
-                        const gchar *tags,
-                        gint page,
-                        GFlickrListCb callback,
-                        gpointer user_data)
-{
-  gchar *auth;
-  g_return_if_fail (G_IS_FLICKR (f));
-
-  if (!user_id) {
-    user_id = "";
-  }
-
-  if (!text) {
-    text = "";
-  }
-
-  if (!tags) {
-    tags = "";
-  }
-
-  gchar *strpage = g_strdup_printf ("%d", page);
-  gchar *strperpage = g_strdup_printf ("%d", f->priv->per_page);
-
-  gchar *api_sig =
-    get_api_sig (f->priv->auth_secret,
-                 "api_key", f->priv->api_key,
-                 "extras", "media,date_taken,owner_name,url_o,url_t",
-                 "method", FLICKR_PHOTOS_SEARCH_METHOD,
-                 "user_id", user_id,
-                 "page", strpage,
-                 "per_page", strperpage,
-                 "tags", tags,
-                 "text", text,
-                 f->priv->auth_token? "auth_token": "",
-                 f->priv->auth_token? f->priv->auth_token: "",
-                 NULL);
-  g_free (strpage);
-  g_free (strperpage);
-
-  /* Build the request */
-  if (f->priv->auth_token) {
-    auth = g_strdup_printf ("&auth_token=%s", f->priv->auth_token);
-  } else {
-    auth = g_strdup ("");
-  }
-
-  gchar *request = g_strdup_printf (FLICKR_PHOTOS_SEARCH,
-                                    f->priv->api_key,
-                                    api_sig,
-                                    user_id,
-                                    f->priv->per_page,
-                                    page,
-                                    tags,
-                                    text,
-                                    auth);
-  g_free (api_sig);
-  g_free (auth);
-
-  GFlickrData *gfd = g_slice_new (GFlickrData);
-  gfd->flickr = g_object_ref (f);
-  gfd->parse_xml = process_photolist_result;
-  gfd->list_cb = callback;
-  gfd->user_data = user_data;
-
-  read_url_async (f, request, gfd);
-  g_free (request);
-}
-
-void
-g_flickr_photos_getRecent (GFlickr *f,
-                           gint page,
-                           GFlickrListCb callback,
-                           gpointer user_data)
-{
-  gchar *auth;
-  g_return_if_fail (G_IS_FLICKR (f));
-
-  gchar *strpage = g_strdup_printf ("%d", page);
-  gchar *strperpage = g_strdup_printf ("%d", f->priv->per_page);
-
-  gchar *api_sig =
-    get_api_sig (f->priv->auth_secret,
-                 "api_key", f->priv->api_key,
-                 "extras", "media,date_taken,owner_name,url_o,url_t",
-                 "method", FLICKR_PHOTOS_GETRECENT_METHOD,
-                 "page", strpage,
-                 "per_page", strperpage,
-                 f->priv->auth_token? "auth_token": "",
-                 f->priv->auth_token? f->priv->auth_token: "",
-                 NULL);
-  g_free (strpage);
-  g_free (strperpage);
-
-  /* Build the request */
-  if (f->priv->auth_token) {
-    auth = g_strdup_printf ("&auth_token=%s", f->priv->auth_token);
-  } else {
-    auth = g_strdup ("");
-  }
-
-  gchar *request = g_strdup_printf (FLICKR_PHOTOS_GETRECENT,
-                                    f->priv->api_key,
-                                    api_sig,
-                                    f->priv->per_page,
-                                    page,
-                                    auth);
-  g_free (api_sig);
-  g_free (auth);
-
-  GFlickrData *gfd = g_slice_new (GFlickrData);
-  gfd->flickr = g_object_ref (f);
-  gfd->parse_xml = process_photolist_result;
-  gfd->list_cb = callback;
-  gfd->user_data = user_data;
-
-  read_url_async (f, request, gfd);
-  g_free (request);
-}
-
-gchar *
-g_flickr_photo_url_original (GFlickr *f, GHashTable *photo)
-{
-  gchar *extension;
-  gchar *farm_id;
-  gchar *o_secret;
-  gchar *photo_id;
-  gchar *server_id;
-
-  if (!photo) {
-    return NULL;
-  }
-
-  extension = g_hash_table_lookup (photo, "photo_originalformat");
-  farm_id = g_hash_table_lookup (photo, "photo_farm");
-  o_secret = g_hash_table_lookup (photo, "photo_originalsecret");
-  photo_id = g_hash_table_lookup (photo, "photo_id");
-  server_id = g_hash_table_lookup (photo, "photo_server");
-
-  if (!extension || !farm_id || !o_secret || !photo_id || !server_id) {
-    return NULL;
-  } else {
-    return g_strdup_printf (FLICKR_PHOTO_ORIG_URL,
-                            farm_id,
-                            server_id,
-                            photo_id,
-                            o_secret,
-                            extension);
-  }
-}
-
-gchar *
-g_flickr_photo_url_thumbnail (GFlickr *f, GHashTable *photo)
-{
-  gchar *farm_id;
-  gchar *secret;
-  gchar *photo_id;
-  gchar *server_id;
-
-  if (!photo) {
-    return NULL;
-  }
-
-  farm_id = g_hash_table_lookup (photo, "photo_farm");
-  secret = g_hash_table_lookup (photo, "photo_secret");
-  photo_id = g_hash_table_lookup (photo, "photo_id");
-  server_id = g_hash_table_lookup (photo, "photo_server");
-
-  if (!farm_id || !secret || !photo_id || !server_id) {
-    return NULL;
-  } else {
-    return g_strdup_printf (FLICKR_PHOTO_THUMB_URL,
-                            farm_id,
-                            server_id,
-                            photo_id,
-                            secret);
-  }
-}
-
-gchar *
-g_flickr_photo_url_largest (GFlickr *f, GHashTable *photo)
-{
-  gchar *farm_id;
-  gchar *secret;
-  gchar *photo_id;
-  gchar *server_id;
-
-  if (!photo) {
-    return NULL;
-  }
-
-  farm_id = g_hash_table_lookup (photo, "photo_farm");
-  secret = g_hash_table_lookup (photo, "photo_secret");
-  photo_id = g_hash_table_lookup (photo, "photo_id");
-  server_id = g_hash_table_lookup (photo, "photo_server");
-
-  if (!farm_id || !secret || !photo_id || !server_id) {
-    return NULL;
-  } else {
-    return g_strdup_printf (FLICKR_PHOTO_LARGEST_URL,
-                            farm_id,
-                            server_id,
-                            photo_id,
-                            secret);
-  }
-}
-
-void
-g_flickr_tags_getHotList (GFlickr *f,
-                          gint count,
-                          GFlickrListCb callback,
-                          gpointer user_data)
-{
-  gchar *auth;
-
-  g_return_if_fail (G_IS_FLICKR (f));
-
-  gchar *strcount = g_strdup_printf ("%d", count);
-
-  gchar *api_sig = get_api_sig (f->priv->auth_secret,
-                                "api_key", f->priv->api_key,
-                                "count", strcount,
-                                "method", FLICKR_TAGS_GETHOTLIST_METHOD,
-                                f->priv->auth_token? "auth_token": "",
-                                f->priv->auth_token? f->priv->auth_token: "",
-                                NULL);
-  g_free (strcount);
-
-  /* Build the request */
-  if (f->priv->auth_token) {
-    auth = g_strdup_printf ("&auth_token=%s", f->priv->auth_token);
-  } else {
-    auth = g_strdup ("");
-  }
-  gchar *request = g_strdup_printf (FLICKR_TAGS_GETHOTLIST,
-                                    f->priv->api_key,
-                                    api_sig,
-                                    count,
-                                    auth);
-  g_free (api_sig);
-  g_free (auth);
-
-  GFlickrData *gfd = g_slice_new (GFlickrData);
-  gfd->flickr = g_object_ref (f);
-  gfd->parse_xml = process_taglist_result;
-  gfd->list_cb = callback;
-  gfd->user_data = user_data;
-
-  read_url_async (f, request, gfd);
-  g_free (request);
-}
-
-void
-g_flickr_photosets_getList (GFlickr *f,
-                           const gchar *user_id,
-                           GFlickrListCb callback,
-                           gpointer user_data)
-{
-  gchar *user;
-  gchar *auth;
-
-  gchar *api_sig = get_api_sig (f->priv->auth_secret,
-                                "api_key", f->priv->api_key,
-                                "method", FLICKR_PHOTOSETS_GETLIST_METHOD,
-                                user_id? "user_id": "",
-                                user_id? user_id: "",
-                                f->priv->auth_token? "auth_token": "",
-                                f->priv->auth_token? f->priv->auth_token: "",
-                                NULL);
-
-  /* Build the request */
-  if (user_id) {
-    user = g_strdup_printf ("&user_id=%s", user_id);
-  } else {
-    user = g_strdup ("");
-  }
-
-  if (f->priv->auth_token) {
-    auth = g_strdup_printf ("&auth_token=%s", f->priv->auth_token);
-  } else {
-    auth = g_strdup ("");
-  }
-
-  gchar *request = g_strdup_printf (FLICKR_PHOTOSETS_GETLIST,
-                                    f->priv->api_key,
-                                    api_sig,
-                                    user,
-                                    auth);
-
-  g_free (api_sig);
-  g_free (user);
-  g_free (auth);
-
-  GFlickrData *gfd = g_slice_new (GFlickrData);
-  gfd->flickr = g_object_ref (f);
-  gfd->parse_xml = process_photosetslist_result;
-  gfd->list_cb = callback;
-  gfd->user_data = user_data;
-
-  read_url_async (f, request, gfd);
-  g_free (request);
-}
-
-void
-g_flickr_photosets_getPhotos (GFlickr *f,
-                              const gchar *photoset_id,
-                              gint page,
-                              GFlickrListCb callback,
-                              gpointer user_data)
-{
-  gchar *auth;
-
-  g_return_if_fail (G_IS_FLICKR (f));
-  g_return_if_fail (photoset_id);
-
-  gchar *strpage = g_strdup_printf ("%d", page);
-  gchar *strperpage = g_strdup_printf ("%d", f->priv->per_page);
-
-  gchar *api_sig =
-    get_api_sig (f->priv->auth_secret,
-                 "api_key", f->priv->api_key,
-                 "photoset_id", photoset_id,
-                 "extras", "media,date_taken,owner_name,url_o,url_t",
-                 "method", FLICKR_PHOTOSETS_GETPHOTOS_METHOD,
-                 "page", strpage,
-                 "per_page", strperpage,
-                 f->priv->auth_token? "auth_token": "",
-                 f->priv->auth_token? f->priv->auth_token: "",
-                 NULL);
-
-  g_free (strpage);
-  g_free (strperpage);
-
-  /* Build the request */
-  if (f->priv->auth_token) {
-    auth = g_strdup_printf ("&auth_token=%s", f->priv->auth_token);
-  } else {
-    auth = g_strdup ("");
-  }
-
-  gchar *request = g_strdup_printf (FLICKR_PHOTOSETS_GETPHOTOS,
-                                    f->priv->api_key,
-                                    api_sig,
-                                    photoset_id,
-                                    f->priv->per_page,
-                                    page,
-                                    auth);
-  g_free (api_sig);
-  g_free (auth);
-
-  GFlickrData *gfd = g_slice_new (GFlickrData);
-  gfd->flickr = g_object_ref (f);
-  gfd->parse_xml = process_photosetsphotos_result;
-  gfd->list_cb = callback;
-  gfd->user_data = user_data;
-
-  read_url_async (f, request, gfd);
-  g_free (request);
-}
-
-gchar *
-g_flickr_auth_getFrob (GFlickr *f)
-{
-  gchar *api_sig;
-  gchar *url;
-  GVfs *vfs;
-  GFile *uri;
-  gchar *contents;
-  GError *error = NULL;
-  gchar *frob = NULL;
-
-  g_return_val_if_fail (G_IS_FLICKR (f), NULL);
-
-  api_sig = get_api_sig (f->priv->auth_secret,
-                         "api_key", f->priv->api_key,
-                         "method", "flickr.auth.getFrob",
-                         NULL);
-
-  /* Build url */
-  url = g_strdup_printf (FLICKR_AUTH_GETFROB,
-                         f->priv->api_key,
-                         api_sig);
-  g_free (api_sig);
-
-  /* Load content */
-  vfs = g_vfs_get_default ();
-  uri = g_vfs_get_file_for_uri (vfs, url);
-  g_free (url);
-  if (!g_file_load_contents (uri, NULL, &contents, NULL, NULL, &error)) {
-    GRL_WARNING ("Unable to get Flickr's frob: %s", error->message);
-    return NULL;
-  }
-
-  /* Get frob */
-  frob = get_xpath_element (contents, "/rsp/frob");
-  g_free (contents);
-  if (!frob) {
-    GRL_WARNING ("Can not get Flickr's frob");
-  }
-
-  return frob;
-}
-
-gchar *
-g_flickr_auth_loginLink (GFlickr *f,
-                         const gchar *frob,
-                         const gchar *perm)
-{
-  gchar *api_sig;
-  gchar *url;
-
-  g_return_val_if_fail (G_IS_FLICKR (f), NULL);
-  g_return_val_if_fail (frob, NULL);
-  g_return_val_if_fail (perm, NULL);
-
-  api_sig = get_api_sig (f->priv->auth_secret,
-                         "api_key", f->priv->api_key,
-                         "frob", frob,
-                         "perms", perm,
-                         NULL);
-
-  url = g_strdup_printf (FLICKR_AUTH_LOGINLINK,
-                         f->priv->api_key,
-                         api_sig,
-                         frob,
-                         perm);
-  g_free (api_sig);
-
-  return url;
-}
-
-gchar *
-g_flickr_auth_getToken (GFlickr *f,
-                        const gchar *frob)
-{
-  GError *error = NULL;
-  GFile *uri;
-  GVfs *vfs;
-  gchar *api_sig;
-  gchar *contents;
-  gchar *token;
-  gchar *url;
-
-  g_return_val_if_fail (G_IS_FLICKR (f), NULL);
-  g_return_val_if_fail (frob, NULL);
-
-  api_sig = get_api_sig (f->priv->auth_secret,
-                         "method", "flickr.auth.getToken",
-                         "api_key", f->priv->api_key,
-                         "frob", frob,
-                         NULL);
-
-  /* Build url */
-  url = g_strdup_printf (FLICKR_AUTH_GETTOKEN,
-                         f->priv->api_key,
-                         api_sig,
-                         frob);
-  g_free (api_sig);
-
-  /* Load content */
-  vfs = g_vfs_get_default ();
-  uri = g_vfs_get_file_for_uri (vfs, url);
-  g_free (url);
-  if (!g_file_load_contents (uri, NULL, &contents, NULL, NULL, &error)) {
-    GRL_WARNING ("Unable to get Flickr's token: %s", error->message);
-    return NULL;
-  }
-
-  /* Get token */
-  token = get_xpath_element (contents, "/rsp/auth/token");
-  g_free (contents);
-  if (!token) {
-    GRL_WARNING ("Can not get Flickr's token");
-  }
-
-  return token;
-}
-
-void
-g_flickr_auth_checkToken (GFlickr *f,
-                          const gchar *token,
-                          GFlickrHashTableCb callback,
-                          gpointer user_data)
-{
-  gchar *api_sig;
-  gchar *request;
-
-  g_return_if_fail (G_IS_FLICKR (f));
-  g_return_if_fail (token);
-  g_return_if_fail (callback);
-
-  api_sig = get_api_sig (f->priv->auth_secret,
-                         "method", FLICKR_AUTH_CHECKTOKEN_METHOD,
-                         "api_key", f->priv->api_key,
-                         "auth_token", token,
-                         NULL);
-
-  /* Build request */
-  request  = g_strdup_printf (FLICKR_AUTH_CHECKTOKEN,
-                              f->priv->api_key,
-                              api_sig,
-                              token);
-  g_free (api_sig);
-
-  GFlickrData *gfd = g_slice_new (GFlickrData);
-  gfd->flickr = g_object_ref (f);
-  gfd->parse_xml = process_token_result;
-  gfd->hashtable_cb = callback;
-  gfd->user_data = user_data;
-
-  read_url_async (f, request, gfd);
-  g_free (request);
-}
diff --git a/src/flickr/gflickr.h b/src/flickr/gflickr.h
deleted file mode 100644
index dd583d8..0000000
--- a/src/flickr/gflickr.h
+++ /dev/null
@@ -1,155 +0,0 @@
-/*
- * Copyright (C) 2010 Igalia S.L.
- *
- * Contact: Iago Toral Quiroga <itoral igalia com>
- *
- * Authors: Juan A. Suarez Romero <jasuarez igalia 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 _G_FLICKR_H_
-#define _G_FLICKR_H_
-
-#include <glib-object.h>
-
-#define G_FLICKR_TYPE                           \
-  (g_flickr_get_type ())
-
-#define G_FLICKR(obj)                                   \
-  (G_TYPE_CHECK_INSTANCE_CAST ((obj),                   \
-                               G_FLICKR_TYPE,           \
-                               GFlickr))
-
-#define G_IS_FLICKR(obj)                                \
-  (G_TYPE_CHECK_INSTANCE_TYPE ((obj),                   \
-                               G_FLICKR_TYPE))
-
-#define G_FLICKR_CLASS(klass)                           \
-  (G_TYPE_CHECK_CLASS_CAST((klass),                     \
-                           G_FLICKR_TYPE,               \
-                           GFlickrClass))
-
-#define G_IS_FLICKR_CLASS(klass)                        \
-  (G_TYPE_CHECK_CLASS_TYPE((klass),                     \
-                           G_FLICKR_TYPE))
-
-#define G_FLICKR_GET_CLASS(obj)                         \
-  (G_TYPE_INSTANCE_GET_CLASS ((obj),                    \
-                              G_FLICKR_TYPE,            \
-                              GFlickrClass))
-
-typedef struct _GFlickr        GFlickr;
-typedef struct _GFlickrPrivate GFlickrPrivate;
-
-struct _GFlickr {
-
-  GObject parent;
-
-  /*< private >*/
-  GFlickrPrivate *priv;
-};
-
-typedef struct _GFlickrClass GFlickrClass;
-
-struct _GFlickrClass {
-
-  GObjectClass parent_class;
-
-};
-
-
-typedef void (*GFlickrHashTableCb) (GFlickr *f, GHashTable *result, gpointer user_data);
-
-typedef void (*GFlickrListCb) (GFlickr *f, GList *result, gpointer user_data);
-
-typedef void (*GFlickrCheckToken) (GFlickr *f, GHashTable *tokeninfo, gpointer user_data);
-
-GType g_flickr_get_type (void);
-
-GFlickr *g_flickr_new (const gchar *api_key, const gchar *auth_secret, const gchar *auth_token);
-
-void g_flickr_set_per_page (GFlickr *f, gint per_page);
-
-void
-g_flickr_photos_getInfo (GFlickr *f,
-                         glong photo_id,
-                         GFlickrHashTableCb callback,
-                         gpointer user_data);
-
-void
-g_flickr_photos_search (GFlickr *f,
-                        const gchar *user_id,
-                        const gchar *text,
-                        const gchar *tags,
-                        gint page,
-                        GFlickrListCb callback,
-                        gpointer user_data);
-
-void
-g_flickr_photos_getRecent (GFlickr *f,
-                           gint page,
-                           GFlickrListCb callback,
-                           gpointer user_data);
-
-gchar *
-g_flickr_photo_url_original (GFlickr *f, GHashTable *photo);
-
-gchar *
-g_flickr_photo_url_thumbnail (GFlickr *f, GHashTable *photo);
-
-gchar *
-g_flickr_photo_url_largest (GFlickr *f, GHashTable *photo);
-
-void
-g_flickr_tags_getHotList (GFlickr *f,
-                          gint count,
-                          GFlickrListCb callback,
-                          gpointer user_data);
-
-void
-g_flickr_photosets_getList (GFlickr *f,
-                            const gchar *user_id,
-                            GFlickrListCb callback,
-                            gpointer user_data);
-
-void
-g_flickr_photosets_getPhotos (GFlickr *f,
-                              const gchar *photoset_id,
-                              gint page,
-                              GFlickrListCb callback,
-                              gpointer user_data);
-
-gchar *
-g_flickr_auth_getFrob (GFlickr *f);
-
-gchar *
-g_flickr_auth_loginLink (GFlickr *f,
-                         const gchar *frob,
-                         const gchar *perm);
-
-gchar *
-g_flickr_auth_getToken (GFlickr *f,
-                        const gchar *frob);
-
-void
-g_flickr_auth_checkToken (GFlickr *f,
-                          const gchar *token,
-                          GFlickrHashTableCb callback,
-                          gpointer user_data);
-
-#endif /* _G_FLICKR_H_ */
diff --git a/src/flickr/grl-flickr.c b/src/flickr/grl-flickr.c
deleted file mode 100644
index 6065355..0000000
--- a/src/flickr/grl-flickr.c
+++ /dev/null
@@ -1,755 +0,0 @@
-/*
- * Copyright (C) 2010 Igalia S.L.
- * Copyright (C) 2011 Intel Corporation.
- *
- *
- * Contact: Iago Toral Quiroga <itoral igalia com>
- *
- * Authors: Juan A. Suarez Romero <jasuarez igalia 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 <grilo.h>
-#include <string.h>
-#include <stdlib.h>
-
-#include "grl-flickr.h"
-#include "gflickr.h"
-
-#define GRL_FLICKR_SOURCE_GET_PRIVATE(object)                           \
-  (G_TYPE_INSTANCE_GET_PRIVATE((object),                                \
-                               GRL_FLICKR_SOURCE_TYPE,                  \
-                               GrlFlickrSourcePrivate))
-
-/* --------- Logging  -------- */
-
-#define GRL_LOG_DOMAIN_DEFAULT flickr_log_domain
-GRL_LOG_DOMAIN(flickr_log_domain);
-
-#define SEARCH_MAX  500
-#define HOTLIST_MAX 200
-
-/* --- Plugin information --- */
-
-#define PLUGIN_ID   FLICKR_PLUGIN_ID
-
-#define PUBLIC_SOURCE_ID   "grl-flickr"
-#define PUBLIC_SOURCE_NAME "Flickr"
-#define PUBLIC_SOURCE_DESC "A source for browsing and searching Flickr photos"
-
-#define PERSONAL_SOURCE_ID "grl-flickr-%s"
-#define PERSONAL_SOURCE_NAME "%s's Flickr"
-#define PERSONAL_SOURCE_DESC "A source for browsing and searching %s' flickr photos"
-
-#define AUTHOR      "Igalia S.L."
-#define LICENSE     "LGPL"
-#define SITE        "http://www.igalia.com";
-
-typedef struct {
-  GrlMediaSource *source;
-  GrlMediaSourceResultCb callback;
-  gchar *user_id;
-  gchar *tags;
-  gchar *text;
-  guint offset;
-  guint page;
-  gpointer user_data;
-  guint count;
-  guint operation_id;
-} OperationData;
-
-struct _GrlFlickrSourcePrivate {
-  GFlickr *flickr;
-  gchar *user_id;
-};
-
-static void token_info_cb (GFlickr *f,
-                           GHashTable *info,
-                           gpointer user_data);
-
-static GrlFlickrSource *grl_flickr_source_public_new (const gchar *flickr_api_key,
-                                                      const gchar *flickr_secret);
-
-static void grl_flickr_source_personal_new (const GrlPluginInfo *plugin,
-                                            const gchar *flickr_api_key,
-                                            const gchar *flickr_secret,
-                                            const gchar *flickr_token);
-
-static void grl_flickr_source_finalize (GObject *object);
-
-gboolean grl_flickr_plugin_init (GrlPluginRegistry *registry,
-				 const GrlPluginInfo *plugin,
-                                 GList *configs);
-
-
-static const GList *grl_flickr_source_supported_keys (GrlMetadataSource *source);
-
-static void grl_flickr_source_browse (GrlMediaSource *source,
-                                      GrlMediaSourceBrowseSpec *bs);
-
-static void grl_flickr_source_metadata (GrlMediaSource *source,
-                                        GrlMediaSourceMetadataSpec *ss);
-
-static void grl_flickr_source_search (GrlMediaSource *source,
-                                      GrlMediaSourceSearchSpec *ss);
-
-/* =================== Flickr Plugin  =============== */
-
-gboolean
-grl_flickr_plugin_init (GrlPluginRegistry *registry,
-                        const GrlPluginInfo *plugin,
-                        GList *configs)
-{
-  gchar *flickr_key;
-  gchar *flickr_secret;
-  gchar *flickr_token;
-  GrlConfig *config;
-  gboolean public_source_created = FALSE;
-
-  GRL_LOG_DOMAIN_INIT (flickr_log_domain, "flickr");
-
-  GRL_DEBUG ("flickr_plugin_init");
-
-  if (!configs) {
-    GRL_WARNING ("Missing configuration");
-    return FALSE;
-  }
-
-  while (configs) {
-    config = GRL_CONFIG (configs->data);
-
-    flickr_key = grl_config_get_api_key (config);
-    flickr_token = grl_config_get_api_token (config);
-    flickr_secret = grl_config_get_api_secret (config);
-
-    if (!flickr_key || !flickr_secret) {
-      GRL_WARNING ("Required configuration keys not set up");
-    } else if (flickr_token) {
-      grl_flickr_source_personal_new (plugin,
-                                      flickr_key,
-                                      flickr_secret,
-                                      flickr_token);
-    } else if (public_source_created) {
-      GRL_WARNING ("Only one public source can be created");
-    } else {
-      GrlFlickrSource *source = grl_flickr_source_public_new (flickr_key, flickr_secret);
-      public_source_created = TRUE;
-      grl_plugin_registry_register_source (registry,
-                                           plugin,
-                                           GRL_MEDIA_PLUGIN (source),
-                                           NULL);
-    }
-
-    if (flickr_key != NULL)
-      g_free (flickr_key);
-    if (flickr_token != NULL)
-      g_free (flickr_token);
-    if (flickr_secret != NULL)
-      g_free (flickr_secret);
-
-    configs = g_list_next (configs);
-  }
-
-  return TRUE;
-}
-
-GRL_PLUGIN_REGISTER (grl_flickr_plugin_init,
-                     NULL,
-                     PLUGIN_ID);
-
-/* ================== Flickr GObject ================ */
-
-G_DEFINE_TYPE (GrlFlickrSource, grl_flickr_source, GRL_TYPE_MEDIA_SOURCE);
-
-static GrlFlickrSource *
-grl_flickr_source_public_new (const gchar *flickr_api_key,
-                              const gchar *flickr_secret)
-{
-  GrlFlickrSource *source;
-
-  GRL_DEBUG ("grl_flickr_source_new");
-
-  source = g_object_new (GRL_FLICKR_SOURCE_TYPE,
-                         "source-id", PUBLIC_SOURCE_ID,
-                         "source-name", PUBLIC_SOURCE_NAME,
-                         "source-desc", PUBLIC_SOURCE_DESC,
-                         NULL);
-  source->priv->flickr = g_flickr_new (flickr_api_key, flickr_secret, NULL);
-
-  return source;
-}
-
-static void
-grl_flickr_source_personal_new (const GrlPluginInfo *plugin,
-                                const gchar *flickr_api_key,
-                                const gchar *flickr_secret,
-                                const gchar *flickr_token)
-{
-  GFlickr *f;
-
-  f = g_flickr_new (flickr_api_key, flickr_secret, flickr_token);
-  g_flickr_auth_checkToken (f, flickr_token, token_info_cb, (gpointer) plugin);
-}
-
-static void
-grl_flickr_source_class_init (GrlFlickrSourceClass * klass)
-{
-  GObjectClass *gobject_class = G_OBJECT_CLASS (klass);
-  GrlMediaSourceClass *source_class = GRL_MEDIA_SOURCE_CLASS (klass);
-  GrlMetadataSourceClass *metadata_class = GRL_METADATA_SOURCE_CLASS (klass);
-
-  gobject_class->finalize = grl_flickr_source_finalize;
-  source_class->browse = grl_flickr_source_browse;
-  source_class->metadata = grl_flickr_source_metadata;
-  source_class->search = grl_flickr_source_search;
-  metadata_class->supported_keys = grl_flickr_source_supported_keys;
-
-  g_type_class_add_private (klass, sizeof (GrlFlickrSourcePrivate));
-}
-
-static void
-grl_flickr_source_init (GrlFlickrSource *source)
-{
-  source->priv = GRL_FLICKR_SOURCE_GET_PRIVATE (source);
-
-  grl_media_source_set_auto_split_threshold (GRL_MEDIA_SOURCE (source),
-                                             SEARCH_MAX);
-}
-
-static void
-grl_flickr_source_finalize (GObject *object)
-{
-  GrlFlickrSource *source;
-
-  GRL_DEBUG ("grl_flickr_source_finalize");
-
-  source = GRL_FLICKR_SOURCE (object);
-  g_free (source->priv->user_id);
-
-  G_OBJECT_CLASS (grl_flickr_source_parent_class)->finalize (object);
-}
-
-/* ======================= Utilities ==================== */
-
-static void
-token_info_cb (GFlickr *f,
-               GHashTable *info,
-               gpointer user_data)
-{
-  GrlFlickrSource *source;
-  GrlPluginInfo *plugin = (GrlPluginInfo *) user_data;
-  GrlPluginRegistry *registry;
-  gchar *fullname;
-  gchar *source_desc;
-  gchar *source_id;
-  gchar *source_name;
-  gchar *username;
-
-  if (!info) {
-    GRL_WARNING ("Wrong token!");
-    g_object_unref (f);
-    return;
-  }
-
-  registry = grl_plugin_registry_get_default ();
-
-  username = g_hash_table_lookup (info, "user_username");
-  fullname = g_hash_table_lookup (info, "user_fullname");
-
-  source_id = g_strdup_printf (PERSONAL_SOURCE_ID, username);
-  source_name = g_strdup_printf (PERSONAL_SOURCE_NAME, fullname);
-  source_desc = g_strdup_printf (PERSONAL_SOURCE_DESC, fullname);
-
-  /* Check if source is already registered */
-  if (grl_plugin_registry_lookup_source (registry, source_id)) {
-    GRL_DEBUG ("A source with id '%s' is already registered. Skipping...",
-               source_id);
-    g_object_unref (f);
-  } else {
-    source = g_object_new (GRL_FLICKR_SOURCE_TYPE,
-                           "source-id", source_id,
-                           "source-name", source_name,
-                           "source-desc", source_desc,
-                           NULL);
-    source->priv->flickr = f;
-    source->priv->user_id = g_strdup (g_hash_table_lookup (info, "user_nsid"));
-    grl_plugin_registry_register_source (registry,
-                                         plugin,
-                                         GRL_MEDIA_PLUGIN (source),
-                                         NULL);
-  }
-
-  g_free (source_id);
-  g_free (source_name);
-  g_free (source_desc);
-}
-
-static void
-update_media (GrlMedia *media, GHashTable *photo)
-{
-  gchar *author;
-  gchar *date;
-  gchar *description;
-  gchar *id;
-  gchar *thumbnail;
-  gchar *title;
-  gchar *url;
-
-  author = g_hash_table_lookup (photo, "owner_realname");
-  if (!author) {
-    author = g_hash_table_lookup (photo, "photo_ownername");
-  }
-  date = g_hash_table_lookup (photo, "dates_taken");
-  if (!date) {
-    date = g_hash_table_lookup (photo, "photo_datetaken");
-  }
-  description = g_hash_table_lookup (photo, "description");
-  id = g_hash_table_lookup (photo, "photo_id");
-  thumbnail = g_strdup (g_hash_table_lookup (photo, "photo_url_t"));
-  if (!thumbnail) {
-    thumbnail = g_flickr_photo_url_thumbnail (NULL, photo);
-  }
-  title = g_hash_table_lookup (photo, "title");
-  if (!title) {
-    title = g_hash_table_lookup (photo, "photo_title");
-  }
-  url = g_strdup (g_hash_table_lookup (photo, "photo_url_o"));
-  if (!url) {
-    url = g_flickr_photo_url_original (NULL, photo);
-    if (!url) {
-      url = g_flickr_photo_url_largest (NULL, photo);
-    }
-  }
-
-  if (author) {
-    grl_media_set_author (media, author);
-  }
-
-  if (date) {
-    grl_media_set_date (media, date);
-  }
-
-  if (description && description[0] != '\0') {
-    grl_media_set_description (media, description);
-  }
-
-  if (id) {
-    grl_media_set_id (media, id);
-  }
-
-  if (thumbnail) {
-    grl_media_set_thumbnail (media, thumbnail);
-    g_free (thumbnail);
-  }
-
-  if (title && title[0] != '\0') {
-    grl_media_set_title (media, title);
-  }
-
-  if (url) {
-    grl_media_set_url (media, url);
-    g_free (url);
-  }
-}
-
-static void
-getInfo_cb (GFlickr *f, GHashTable *photo, gpointer user_data)
-{
-  GrlMediaSourceMetadataSpec *ms = (GrlMediaSourceMetadataSpec *) user_data;
-
-  if (photo) {
-    update_media (ms->media, photo);
-  }
-
-  ms->callback (ms->source, ms->media, ms->user_data, NULL);
-}
-
-static void
-search_cb (GFlickr *f, GList *photolist, gpointer user_data)
-{
-  GrlMedia *media;
-  OperationData *od = (OperationData *) user_data;
-  gchar *media_type;
-
-  /* Go to offset element */
-  photolist = g_list_nth (photolist, od->offset);
-
-  /* No more elements can be sent */
-  if (!photolist) {
-    od->callback (od->source,
-                  od->operation_id,
-                  NULL,
-                  0,
-                  od->user_data,
-                  NULL);
-    g_slice_free (OperationData, od);
-    return;
-  }
-
-  while (photolist && od->count) {
-    media_type = g_hash_table_lookup (photolist->data, "photo_media");
-    if (strcmp (media_type, "photo") == 0) {
-      media = grl_media_image_new ();
-    } else {
-      media = grl_media_video_new ();
-    }
-    update_media (media, photolist->data);
-    od->callback (od->source,
-                  od->operation_id,
-                  media,
-                  od->count == 1? 0: -1,
-                  od->user_data,
-                  NULL);
-    photolist = g_list_next (photolist);
-    od->count--;
-  }
-
-  /* Get more elements */
-  if (od->count) {
-    od->offset = 0;
-    od->page++;
-    g_flickr_photos_search (f,
-                            od->user_id,
-                            od->text,
-                            od->tags,
-                            od->page,
-                            search_cb,
-                            od);
-  } else {
-    g_slice_free (OperationData, od);
-  }
-}
-
-static void
-photosetslist_cb (GFlickr *f, GList *photosets, gpointer user_data)
-{
-  GrlMedia *media;
-  GrlMediaSourceBrowseSpec *bs = (GrlMediaSourceBrowseSpec *) user_data;
-  gchar *value;
-  gint count;
-
-  /* Go to offset element */
-  photosets = g_list_nth (photosets, bs->skip);
-
-  /* No more elements can be sent */
-  if (!photosets) {
-    bs->callback (bs->source,
-                  bs->browse_id,
-                  NULL,
-                  0,
-                  bs->user_data,
-                  NULL);
-    return;
-  }
-
-  /* Send data */
-  count = g_list_length (photosets);
-  if (count > bs->count) {
-    count = bs->count;
-  }
-
-  while (photosets && count > 0) {
-    count--;
-    media = grl_media_box_new ();
-    grl_media_set_id (media,
-                      g_hash_table_lookup (photosets->data,
-                                           "photoset_id"));
-    value = g_hash_table_lookup (photosets->data, "title");
-    if (value && value[0] != '\0') {
-      grl_media_set_title (media, value);
-    }
-    value = g_hash_table_lookup (photosets->data, "description");
-    if (value && value[0] != '\0') {
-      grl_media_set_description (media, value);
-    }
-
-    bs->callback (bs->source,
-                  bs->browse_id,
-                  media,
-                  count,
-                  bs->user_data,
-                  NULL);
-    photosets = g_list_next (photosets);
-  }
-}
-
-static void
-photosetsphotos_cb (GFlickr *f, GList *photolist, gpointer user_data)
-{
-  GrlMedia *media;
-  OperationData *od = (OperationData *) user_data;
-  gchar *media_type;
-
-  /* Go to offset element */
-  photolist = g_list_nth (photolist, od->offset);
-
-  /* No more elements can be sent */
-  if (!photolist) {
-    od->callback (od->source,
-                  od->operation_id,
-                  NULL,
-                  0,
-                  od->user_data,
-                  NULL);
-    return;
-  }
-
-  while (photolist && od->count) {
-    media_type = g_hash_table_lookup (photolist->data, "photo_media");
-    if (strcmp (media_type, "photo") == 0) {
-      media = grl_media_image_new ();
-    } else {
-      media = grl_media_video_new ();
-    }
-
-    update_media (media, photolist->data);
-    od->callback (od->source,
-                  od->operation_id,
-                  media,
-                  od->count == 1? 0: -1,
-                  od->user_data,
-                  NULL);
-    photolist = g_list_next (photolist);
-    od->count--;
-  }
-
-  /* Get more elements */
-  if (od->count) {
-    od->offset = 0;
-    od->page++;
-    g_flickr_photosets_getPhotos (f, od->text, od->page, photosetsphotos_cb, od);
-  } else {
-    g_slice_free (OperationData, od);
-  }
-}
-
-static void
-gettags_cb (GFlickr *f, GList *taglist, gpointer user_data)
-{
-  GrlMedia *media;
-  GrlMediaSourceBrowseSpec *bs = (GrlMediaSourceBrowseSpec *) user_data;
-  gint count;
-
-  /* Go to offset element */
-  taglist = g_list_nth (taglist, bs->skip);
-
-  /* No more elements can be sent */
-  if (!taglist) {
-    bs->callback (bs->source,
-                  bs->browse_id,
-                  NULL,
-                  0,
-                  bs->user_data,
-                  NULL);
-    return;
-  }
-
-  /* Send data */
-  count = g_list_length (taglist);
-  while (taglist) {
-    count--;
-    media = grl_media_box_new ();
-    grl_media_set_id (media, taglist->data);
-    grl_media_set_title (media, taglist->data);
-    bs->callback (bs->source,
-                  bs->browse_id,
-                  media,
-                  count,
-                  bs->user_data,
-                  NULL);
-    taglist = g_list_next (taglist);
-  }
-}
-
-/* ================== API Implementation ================ */
-
-static const GList *
-grl_flickr_source_supported_keys (GrlMetadataSource *source)
-{
-  static GList *keys = NULL;
-  if (!keys) {
-    keys = grl_metadata_key_list_new (GRL_METADATA_KEY_AUTHOR,
-                                      GRL_METADATA_KEY_DATE,
-                                      GRL_METADATA_KEY_DESCRIPTION,
-                                      GRL_METADATA_KEY_ID,
-                                      GRL_METADATA_KEY_THUMBNAIL,
-                                      GRL_METADATA_KEY_TITLE,
-                                      GRL_METADATA_KEY_URL,
-                                      NULL);
-  }
-  return keys;
-}
-
-static void
-grl_flickr_source_public_browse (GrlMediaSource *source,
-                                 GrlMediaSourceBrowseSpec *bs)
-{
-  GFlickr *f = GRL_FLICKR_SOURCE (source)->priv->flickr;
-  const gchar *container_id;
-  guint per_page;
-  gint request_size;
-
-  container_id = grl_media_get_id (bs->container);
-
-  if (!container_id) {
-    /* Get hot tags list. List is limited up to HOTLIST_MAX tags */
-    if (bs->skip >= HOTLIST_MAX) {
-      bs->callback (bs->source, bs->browse_id, NULL, 0, bs->user_data, NULL);
-    } else {
-      request_size = CLAMP (bs->skip + bs->count, 1, HOTLIST_MAX);
-      g_flickr_tags_getHotList (f, request_size, gettags_cb, bs);
-    }
-  } else {
-    OperationData *od = g_slice_new (OperationData);
-
-    grl_paging_translate (bs->skip,
-                          bs->count,
-                          SEARCH_MAX,
-                          &per_page,
-                          &(od->page),
-                          &(od->offset));
-    g_flickr_set_per_page (f, per_page);
-
-    od->source = bs->source;
-    od->callback = bs->callback;
-    od->user_id = GRL_FLICKR_SOURCE (source)->priv->user_id;
-    od->tags = (gchar *) container_id;
-    od->text = NULL;
-    od->user_data = bs->user_data;
-    od->count = bs->count;
-    od->operation_id = bs->browse_id;
-    g_flickr_photos_search (f,
-                            od->user_id,
-                            NULL,
-                            od->tags,
-                            od->page,
-                            search_cb,
-                            od);
-  }
-}
-
-static void
-grl_flickr_source_personal_browse (GrlMediaSource *source,
-                                   GrlMediaSourceBrowseSpec *bs)
-{
-  GFlickr *f = GRL_FLICKR_SOURCE (source)->priv->flickr;
-  OperationData *od;
-  const gchar *container_id;
-  guint per_page;
-
-  container_id = grl_media_get_id (bs->container);
-
-  if (!container_id) {
-    /* Get photoset */
-    g_flickr_photosets_getList (f, NULL, photosetslist_cb, bs);
-  } else {
-    od = g_slice_new (OperationData);
-
-    /* Compute items per page and page offset */
-    grl_paging_translate (bs->skip,
-                          bs->count,
-                          SEARCH_MAX,
-                          &per_page,
-                          &(od->page),
-                          &(od->offset));
-    g_flickr_set_per_page (f, per_page);
-    od->source = bs->source;
-    od->callback = bs->callback;
-    od->tags = NULL;
-    od->text = (gchar *) container_id;
-    od->user_data = bs->user_data;
-    od->count = bs->count;
-    od->operation_id = bs->browse_id;
-
-    g_flickr_photosets_getPhotos (f, container_id, od->page, photosetsphotos_cb, od);
-  }
-}
-
-static void
-grl_flickr_source_browse (GrlMediaSource *source,
-                          GrlMediaSourceBrowseSpec *bs)
-{
-  if (GRL_FLICKR_SOURCE (source)->priv->user_id) {
-    grl_flickr_source_personal_browse (source, bs);
-  } else {
-    grl_flickr_source_public_browse (source, bs);
-  }
-}
-
-static void
-grl_flickr_source_metadata (GrlMediaSource *source,
-                            GrlMediaSourceMetadataSpec *ms)
-{
-  const gchar *id;
-
-  if (!ms->media || (id = grl_media_get_id (ms->media)) == NULL) {
-    ms->callback (ms->source, ms->media, ms->user_data, NULL);
-    return;
-  }
-
-  g_flickr_photos_getInfo (GRL_FLICKR_SOURCE (source)->priv->flickr,
-                           atol (id),
-                           getInfo_cb,
-                           ms);
-}
-
-static void
-grl_flickr_source_search (GrlMediaSource *source,
-                          GrlMediaSourceSearchSpec *ss)
-{
-  GFlickr *f = GRL_FLICKR_SOURCE (source)->priv->flickr;
-  guint per_page;
-  OperationData *od = g_slice_new (OperationData);
-
-  /* Compute items per page and page offset */
-  grl_paging_translate (ss->skip,
-                        ss->count,
-                        SEARCH_MAX,
-                        &per_page,
-                        &(od->page),
-                        &(od->offset));
-  g_flickr_set_per_page (f, per_page);
-
-  od->source = ss->source;
-  od->callback = ss->callback;
-  od->user_id = GRL_FLICKR_SOURCE (source)->priv->user_id;
-  od->tags = NULL;
-  od->text = ss->text;
-  od->user_data = ss->user_data;
-  od->count = ss->count;
-  od->operation_id = ss->search_id;
-
-  if (od->user_id || od->text) {
-    g_flickr_photos_search (f,
-                            od->user_id,
-                            ss->text,
-                            NULL,
-                            od->page,
-                            search_cb,
-                            od);
-  } else {
-    g_flickr_photos_getRecent (f,
-                               od->page,
-                               search_cb,
-                               od);
-  }
-}
diff --git a/src/flickr/grl-flickr.h b/src/flickr/grl-flickr.h
deleted file mode 100644
index af1a843..0000000
--- a/src/flickr/grl-flickr.h
+++ /dev/null
@@ -1,80 +0,0 @@
-/*
- * Copyright (C) 2010 Igalia S.L.
- *
- * Contact: Iago Toral Quiroga <itoral igalia com>
- *
- * Authors: Juan A. Suarez Romero <jasuarez igalia 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_FLICKR_SOURCE_H_
-#define _GRL_FLICKR_SOURCE_H_
-
-#include <grilo.h>
-
-#define GRL_FLICKR_SOURCE_TYPE                  \
-  (grl_flickr_source_get_type ())
-
-#define GRL_FLICKR_SOURCE(obj)                          \
-  (G_TYPE_CHECK_INSTANCE_CAST ((obj),                   \
-                               GRL_FLICKR_SOURCE_TYPE,  \
-                               GrlFlickrSource))
-
-#define GRL_IS_FLICKR_SOURCE(obj)                       \
-  (G_TYPE_CHECK_INSTANCE_TYPE ((obj),                   \
-                               GRL_FLICKR_SOURCE_TYPE))
-
-#define GRL_FLICKR_SOURCE_CLASS(klass)                  \
-  (G_TYPE_CHECK_CLASS_CAST((klass),                     \
-                           GRL_FLICKR_SOURCE_TYPE,      \
-                           GrlFlickrSourceClass))
-
-#define GRL_IS_FLICKR_SOURCE_CLASS(klass)               \
-  (G_TYPE_CHECK_CLASS_TYPE((klass),                     \
-                           GRL_FLICKR_SOURCE_TYPE))
-
-#define GRL_FLICKR_SOURCE_GET_CLASS(obj)                \
-  (G_TYPE_INSTANCE_GET_CLASS ((obj),                    \
-                              GRL_FLICKR_SOURCE_TYPE,   \
-                              GrlFlickrSourceClass))
-
-/* plugin's log domain */
-GRL_LOG_DOMAIN_EXTERN(flickr_log_domain);
-
-typedef struct _GrlFlickrSource        GrlFlickrSource;
-typedef struct _GrlFlickrSourcePrivate GrlFlickrSourcePrivate;
-
-struct _GrlFlickrSource {
-
-  GrlMediaSource parent;
-
-  /*< private >*/
-  GrlFlickrSourcePrivate *priv;
-};
-
-typedef struct _GrlFlickrSourceClass GrlFlickrSourceClass;
-
-struct _GrlFlickrSourceClass {
-
-  GrlMediaSourceClass parent_class;
-
-};
-
-GType grl_flickr_source_get_type (void);
-
-#endif /* _GRL_FLICKR_SOURCE_H_ */
diff --git a/src/flickr/grl-flickr.xml b/src/flickr/grl-flickr.xml
deleted file mode 100644
index 4b47f05..0000000
--- a/src/flickr/grl-flickr.xml
+++ /dev/null
@@ -1,9 +0,0 @@
-<plugin>
-  <info>
-    <name>Flickr</name>
-    <description>A plugin for browsing and searching Flickr photos</description>
-    <author>Igalia S.L.</author>
-    <license>LGPL</license>
-    <site>http://www.igalia.com</site>
-  </info>
-</plugin>
diff --git a/src/gravatar/Makefile.am b/src/gravatar/Makefile.am
deleted file mode 100644
index 391795e..0000000
--- a/src/gravatar/Makefile.am
+++ /dev/null
@@ -1,32 +0,0 @@
-#
-# Makefile.am
-#
-# Author: Juan A. Suarez Romero <jasuarez igalia com>
-#
-# Copyright (C) 2010, 2011 Igalia S.L. All rights reserved.
-
-lib_LTLIBRARIES = libgrlgravatar.la
-
-libgrlgravatar_la_CFLAGS =	\
-	$(DEPS_CFLAGS)
-
-libgrlgravatar_la_LIBADD =	\
-	$(DEPS_LIBS)
-
-libgrlgravatar_la_LDFLAGS = \
-	-module		    \
-	-avoid-version
-
-libgrlgravatar_la_SOURCES = grl-gravatar.c grl-gravatar.h
-
-libdir=$(GRL_PLUGINS_DIR)
-gravatarxmldir		= $(GRL_PLUGINS_CONF_DIR)
-gravatarxml_DATA	= $(GRAVATAR_PLUGIN_ID).xml
-
-EXTRA_DIST = $(gravatarxml_DATA)
-
-MAINTAINERCLEANFILES =	\
-	*.in	\
-	*~
-
-DISTCLEANFILES = $(MAINTAINERCLEANFILES)
diff --git a/src/gravatar/grl-gravatar.c b/src/gravatar/grl-gravatar.c
deleted file mode 100644
index 41d9fa5..0000000
--- a/src/gravatar/grl-gravatar.c
+++ /dev/null
@@ -1,301 +0,0 @@
-/*
- * Copyright (C) 2010 Igalia S.L.
- *
- * Contact: Iago Toral Quiroga <itoral igalia com>
- *
- * Authors: Juan A. Suarez Romero <jasuarez igalia 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 "grl-gravatar.h"
-
-/* ---------- Logging ---------- */
-
-#define GRL_LOG_DOMAIN_DEFAULT gravatar_log_domain
-GRL_LOG_DOMAIN_STATIC(gravatar_log_domain);
-
-/* -------- Gravatar API -------- */
-
-#define GRAVATAR_URL "http://www.gravatar.com/avatar/%s.jpg";
-
-/* ------- Pluging Info -------- */
-
-#define PLUGIN_ID   GRAVATAR_PLUGIN_ID
-
-#define SOURCE_ID   PLUGIN_ID
-#define SOURCE_NAME "Avatar provider from Gravatar"
-#define SOURCE_DESC "A plugin to get avatars for artist and author fields"
-
-#define AUTHOR      "Igalia S.L."
-#define LICENSE     "LGPL"
-#define SITE        "http://www.igalia.com";
-
-
-static GrlGravatarSource *grl_gravatar_source_new (void);
-
-static void grl_gravatar_source_resolve (GrlMetadataSource *source,
-                                         GrlMetadataSourceResolveSpec *rs);
-
-static const GList *grl_gravatar_source_supported_keys (GrlMetadataSource *source);
-
-static const GList *grl_gravatar_source_key_depends (GrlMetadataSource *source,
-                                                     GrlKeyID key_id);
-
-static GrlKeyID register_gravatar_key (GrlPluginRegistry *registry,
-                                       const gchar *name,
-                                       const gchar *nick,
-                                       const gchar *blurb);
-
-gboolean grl_gravatar_source_plugin_init (GrlPluginRegistry *registry,
-                                          const GrlPluginInfo *plugin,
-                                          GList *configs);
-
-GrlKeyID GRL_METADATA_KEY_ARTIST_AVATAR = NULL;
-GrlKeyID GRL_METADATA_KEY_AUTHOR_AVATAR = NULL;
-
-/* =================== Gravatar Plugin  =============== */
-
-gboolean
-grl_gravatar_source_plugin_init (GrlPluginRegistry *registry,
-                                 const GrlPluginInfo *plugin,
-                                 GList *configs)
-{
-  GRL_LOG_DOMAIN_INIT (gravatar_log_domain, "gravatar");
-
-  GRL_DEBUG ("grl_gravatar_source_plugin_init");
-
-  /* Register keys */
-  GRL_METADATA_KEY_ARTIST_AVATAR =
-    register_gravatar_key (registry,
-                           "artist-avatar",
-                           "ArtistAvatar",
-                           "Avatar for the artist");
-
-  GRL_METADATA_KEY_AUTHOR_AVATAR =
-    register_gravatar_key (registry,
-                           "author-avatar",
-                            "AuthorAvatar",
-                            "Avatar for the author");
-  if (!GRL_METADATA_KEY_ARTIST_AVATAR &&
-      !GRL_METADATA_KEY_AUTHOR_AVATAR) {
-    GRL_WARNING ("Unable to register \"autor-avatar\" nor \"artist-avatar\"");
-    return FALSE;
-  }
-
-  GrlGravatarSource *source = grl_gravatar_source_new ();
-  grl_plugin_registry_register_source (registry,
-                                       plugin,
-                                       GRL_MEDIA_PLUGIN (source),
-                                       NULL);
-  return TRUE;
-}
-
-GRL_PLUGIN_REGISTER (grl_gravatar_source_plugin_init,
-                     NULL,
-                     PLUGIN_ID);
-
-/* ================== Gravatar GObject ================ */
-
-static GrlGravatarSource *
-grl_gravatar_source_new (void)
-{
-  GRL_DEBUG ("grl_gravatar_source_new");
-  return g_object_new (GRL_GRAVATAR_SOURCE_TYPE,
-		       "source-id", SOURCE_ID,
-		       "source-name", SOURCE_NAME,
-		       "source-desc", SOURCE_DESC,
-		       NULL);
-}
-
-static void
-grl_gravatar_source_class_init (GrlGravatarSourceClass * klass)
-{
-  GrlMetadataSourceClass *metadata_class = GRL_METADATA_SOURCE_CLASS (klass);
-  metadata_class->supported_keys = grl_gravatar_source_supported_keys;
-  metadata_class->key_depends = grl_gravatar_source_key_depends;
-  metadata_class->resolve = grl_gravatar_source_resolve;
-}
-
-static void
-grl_gravatar_source_init (GrlGravatarSource *source)
-{
-}
-
-G_DEFINE_TYPE (GrlGravatarSource,
-               grl_gravatar_source,
-               GRL_TYPE_METADATA_SOURCE);
-
-/* ======================= Utilities ==================== */
-
-static GrlKeyID
-register_gravatar_key (GrlPluginRegistry *registry,
-                       const gchar *name,
-                       const gchar *nick,
-                       const gchar *blurb)
-{
-  GParamSpec *spec;
-  GrlKeyID key;
-
-  spec = g_param_spec_string (name,
-                              nick,
-                              blurb,
-                              NULL,
-                              G_PARAM_READWRITE);
-
-  key = grl_plugin_registry_register_metadata_key (registry, spec, NULL);
-
-  /* If key was not registered, could be that it is already registered. If so,
-     check if type is the expected one, and reuse it */
-  if (!key) {
-    g_param_spec_unref (spec);
-    key = grl_plugin_registry_lookup_metadata_key (registry, name);
-    if (!key || GRL_METADATA_KEY_GET_TYPE (key) != G_TYPE_STRING) {
-      key = NULL;
-    }
-  }
-
-  return key;
-}
-
-static gchar *
-get_avatar (const gchar *field) {
-  GMatchInfo *match_info = NULL;
-  gchar *avatar = NULL;
-  gchar *email;
-  gchar *email_hash;
-  gchar *lowercased_field;
-  static GRegex *email_regex = NULL;
-
-  if (!field) {
-    return NULL;
-  }
-
-  lowercased_field = g_utf8_strdown (field, -1);
-
-  if (!email_regex) {
-    email_regex = g_regex_new ("[\\w-]+@([\\w-]+\\.)+[\\w-]+", G_REGEX_OPTIMIZE, 0, NULL);
-  }
-
-  if (g_regex_match (email_regex, lowercased_field, 0, &match_info)) {
-    email = g_match_info_fetch (match_info, 0);
-    g_match_info_free (match_info);
-    email_hash = g_compute_checksum_for_string (G_CHECKSUM_MD5, email, -1);
-    avatar = g_strdup_printf (GRAVATAR_URL, email_hash);
-    g_free (email);
-    g_free (email_hash);
-  }
-
-  return avatar;
-}
-
-/* ================== API Implementation ================ */
-
-static const GList *
-grl_gravatar_source_supported_keys (GrlMetadataSource *source)
-{
-  static GList *keys = NULL;
-
-  if (!keys) {
-    if (GRL_METADATA_KEY_ARTIST_AVATAR) {
-      keys = g_list_prepend (keys, GRL_METADATA_KEY_ARTIST_AVATAR);
-    }
-    if (GRL_METADATA_KEY_AUTHOR_AVATAR) {
-      keys =g_list_prepend (keys, GRL_METADATA_KEY_AUTHOR_AVATAR);
-    }
-  }
-
- return keys;
-}
-
-static const GList *
-grl_gravatar_source_key_depends (GrlMetadataSource *source,
-                                 GrlKeyID key_id)
-{
-  static GList *artist_avatar_deps = NULL;
-  static GList *author_avatar_deps = NULL;
-
-  if (!artist_avatar_deps) {
-    artist_avatar_deps = grl_metadata_key_list_new (GRL_METADATA_KEY_ARTIST,
-                                                    NULL);
-  }
-
-  if (!author_avatar_deps) {
-    author_avatar_deps = grl_metadata_key_list_new (GRL_METADATA_KEY_AUTHOR,
-                                                    NULL);
-  }
-
-  if (key_id == GRL_METADATA_KEY_ARTIST_AVATAR) {
-    return artist_avatar_deps;
-  } else if (key_id == GRL_METADATA_KEY_AUTHOR_AVATAR) {
-    return author_avatar_deps;
-  } else {
-    return  NULL;
-  }
-}
-
-static void
-grl_gravatar_source_resolve (GrlMetadataSource *source,
-                             GrlMetadataSourceResolveSpec *rs)
-{
-  gboolean artist_avatar_required = FALSE;
-  gboolean author_avatar_required = FALSE;
-  gchar *avatar_url;
-
-  GRL_DEBUG ("grl_gravatar_source_resolve");
-
-  GList *iter;
-
-  /* Check that albumart is requested */
-  iter = rs->keys;
-  while (iter && (!artist_avatar_required || !author_avatar_required)) {
-    if (iter->data == GRL_METADATA_KEY_ARTIST_AVATAR) {
-      artist_avatar_required = TRUE;
-    } else if (iter->data == GRL_METADATA_KEY_AUTHOR_AVATAR) {
-      author_avatar_required = TRUE;
-    }
-    iter = g_list_next (iter);
-  }
-
-  if (artist_avatar_required) {
-    avatar_url = get_avatar (grl_data_get_string (GRL_DATA (rs->media),
-                                                  GRL_METADATA_KEY_ARTIST));
-    if (avatar_url) {
-      grl_data_set_string (GRL_DATA (rs->media),
-                           GRL_METADATA_KEY_ARTIST_AVATAR,
-                           avatar_url);
-      g_free (avatar_url);
-    }
-  }
-
-  if (author_avatar_required) {
-    avatar_url = get_avatar (grl_data_get_string (GRL_DATA (rs->media),
-                                                  GRL_METADATA_KEY_AUTHOR));
-    if (avatar_url) {
-      grl_data_set_string (GRL_DATA (rs->media),
-                           GRL_METADATA_KEY_AUTHOR_AVATAR,
-                           avatar_url);
-      g_free (avatar_url);
-    }
-  }
-
-  rs->callback (source, rs->media, rs->user_data, NULL);
-}
diff --git a/src/gravatar/grl-gravatar.h b/src/gravatar/grl-gravatar.h
deleted file mode 100644
index 644efdb..0000000
--- a/src/gravatar/grl-gravatar.h
+++ /dev/null
@@ -1,74 +0,0 @@
-/*
- * Copyright (C) 2010 Igalia S.L.
- *
- * Contact: Iago Toral Quiroga <itoral igalia com>
- *
- * Authors: Juan A. Suarez Romero <jasuarez igalia 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_GRAVATAR_H_
-#define _GRL_GRAVATAR_H_
-
-#include <grilo.h>
-
-#define GRL_GRAVATAR_SOURCE_TYPE                \
-  (grl_gravatar_source_get_type ())
-
-#define GRL_GRAVATAR_SOURCE(obj)                                \
-  (G_TYPE_CHECK_INSTANCE_CAST ((obj),                           \
-                               GRL_GRAVATAR_SOURCE_TYPE,        \
-                               GrlGravatarSource))
-
-#define GRL_IS_GRAVATAR_SOURCE(obj)                             \
-  (G_TYPE_CHECK_INSTANCE_TYPE ((obj),                           \
-                               GRL_GRAVATAR_SOURCE_TYPE))
-
-#define GRL_GRAVATAR_SOURCE_CLASS(klass)                \
-  (G_TYPE_CHECK_CLASS_CAST((klass),                     \
-                           GRL_GRAVATAR_SOURCE_TYPE,    \
-                           GrlGravatarSourceClass))
-
-#define GRL_IS_GRAVATAR_SOURCE_CLASS(klass)             \
-  (G_TYPE_CHECK_CLASS_TYPE((klass),                     \
-                           GRL_GRAVATAR_SOURCE_TYPE))
-
-#define GRL_GRAVATAR_SOURCE_GET_CLASS(obj)              \
-  (G_TYPE_INSTANCE_GET_CLASS ((obj),                    \
-                              GRL_GRAVATAR_SOURCE_TYPE, \
-                              GrlGravatarSourceClass))
-
-typedef struct _GrlGravatarSource GrlGravatarSource;
-
-struct _GrlGravatarSource {
-
-  GrlMetadataSource parent;
-
-};
-
-typedef struct _GrlGravatarSourceClass GrlGravatarSourceClass;
-
-struct _GrlGravatarSourceClass {
-
-  GrlMetadataSourceClass parent_class;
-
-};
-
-GType grl_gravatar_source_get_type (void);
-
-#endif /* _GRL_GRAVATAR_H_ */
diff --git a/src/gravatar/grl-gravatar.xml b/src/gravatar/grl-gravatar.xml
deleted file mode 100644
index 58f60d1..0000000
--- a/src/gravatar/grl-gravatar.xml
+++ /dev/null
@@ -1,9 +0,0 @@
-<plugin>
-  <info>
-    <name>Avatar provider from Gravatar</name>
-    <description>A plugin to get avatars for artist and author fields</description>
-    <author>Igalia S.L.</author>
-    <license>LGPL</license>
-    <site>http://www.igalia.com</site>
-  </info>
-</plugin>
diff --git a/src/jamendo/Makefile.am b/src/jamendo/Makefile.am
deleted file mode 100644
index 3232ac2..0000000
--- a/src/jamendo/Makefile.am
+++ /dev/null
@@ -1,38 +0,0 @@
-#
-# Makefile.am
-#
-# Author: Juan A. Suarez Romero <jasuarez igalia com>
-#
-# Copyright (C) 2010, 2011 Igalia S.L. All rights reserved.
-
-lib_LTLIBRARIES		 = libgrljamendo.la
-
-libgrljamendo_la_CFLAGS =	\
-	$(DEPS_CFLAGS)		\
-	$(GRLNET_CFLAGS)	\
-	$(XML_CFLAGS)
-
-libgrljamendo_la_LIBADD =	\
-	$(DEPS_LIBS)		\
-	$(GRLNET_LIBS)		\
-	$(XML_LIBS)
-
-libgrljamendo_la_LDFLAGS = \
-	-module		   \
-	-avoid-version
-
-libgrljamendo_la_SOURCES = 	\
-	grl-jamendo.c 		\
-	grl-jamendo.h
-
-libdir = $(GRL_PLUGINS_DIR)
-jamendoxmldir = $(GRL_PLUGINS_CONF_DIR)
-jamendoxml_DATA	= $(JAMENDO_PLUGIN_ID).xml
-
-EXTRA_DIST = $(jamendoxml_DATA)
-
-MAINTAINERCLEANFILES =	\
-	*.in		\
-	*~
-
-DISTCLEANFILES = $(MAINTAINERCLEANFILES)
diff --git a/src/jamendo/TODO b/src/jamendo/TODO
deleted file mode 100644
index 2571947..0000000
--- a/src/jamendo/TODO
+++ /dev/null
@@ -1,30 +0,0 @@
-Get global database and work locally
-====================================
-Actually all queries are on-line. But the whole database can be retrieved and
-stored in local, so all operation can be performed off-line.
- 
-Doing it requires to implement the search in local. We should explore if either
-moving to this approach or even creating a new derivative plugin that follows
-this off-line approach worths it.
-
-
-Implement childcount
-====================
-Implementing childcount would require, for each result doing another search and
-retrieve all children, counting them.
-
-
-Limit search-rate
-=================
-Jamendo terms of use states that applications should be limited to 1 query per
-second. Some kind of control should be needed in the plugin.
-
-
-Limit requested elements
-========================
-User can request any arbitrary number of elements, and this request is propagated to Jamendo.
-
-Nevertheless, it would be better to break the request in smaller chunks, so
-though user requests, for instance, 1000 albums, we do the query in steps of 25
-elements.
-
diff --git a/src/jamendo/grl-jamendo.c b/src/jamendo/grl-jamendo.c
deleted file mode 100644
index f6ba5c6..0000000
--- a/src/jamendo/grl-jamendo.c
+++ /dev/null
@@ -1,1365 +0,0 @@
-/*
- * Copyright (C) 2010 Igalia S.L.
- *
- * Contact: Iago Toral Quiroga <itoral igalia com>
- *
- * Authors: Juan A. Suarez Romero <jasuarez igalia 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 <grilo.h>
-#include <net/grl-net.h>
-#include <libxml/parser.h>
-#include <libxml/xmlmemory.h>
-#include <string.h>
-
-#include "grl-jamendo.h"
-
-/* --------- Logging  -------- */
-
-#define GRL_LOG_DOMAIN_DEFAULT jamendo_log_domain
-GRL_LOG_DOMAIN_STATIC(jamendo_log_domain);
-
-#define JAMENDO_ID_SEP    "/"
-#define JAMENDO_ROOT_NAME "Jamendo"
-
-#define MAX_ELEMENTS 100
-
-/* ------- Categories ------- */
-
-#define JAMENDO_ARTIST "artist"
-#define JAMENDO_ALBUM  "album"
-#define JAMENDO_TRACK  "track"
-
-/* ---- Jamendo Web API  ---- */
-
-#define JAMENDO_BASE_ENTRY "http://api.jamendo.com/get2";
-#define JAMENDO_FORMAT     "xml"
-#define JAMENDO_RANGE      "n=%u&pn=%u"
-
-#define JAMENDO_ARTIST_ENTRY JAMENDO_BASE_ENTRY "/%s/" JAMENDO_ARTIST "/" JAMENDO_FORMAT
-
-#define JAMENDO_ALBUM_ENTRY  JAMENDO_BASE_ENTRY "/%s/" JAMENDO_ALBUM  "/" JAMENDO_FORMAT \
-  "/" JAMENDO_ALBUM "_" JAMENDO_ARTIST
-
-#define JAMENDO_TRACK_ENTRY  JAMENDO_BASE_ENTRY "/%s/" JAMENDO_TRACK  "/" JAMENDO_FORMAT \
-  "/" JAMENDO_ALBUM "_" JAMENDO_ARTIST "+" JAMENDO_TRACK "_" JAMENDO_ALBUM
-
-#define JAMENDO_GET_ARTISTS JAMENDO_ARTIST_ENTRY "/?" JAMENDO_RANGE
-#define JAMENDO_GET_ALBUMS  JAMENDO_ALBUM_ENTRY  "/?" JAMENDO_RANGE
-#define JAMENDO_GET_TRACKS  JAMENDO_TRACK_ENTRY  "/?" JAMENDO_RANGE
-
-#define JAMENDO_GET_ALBUMS_FROM_ARTIST JAMENDO_ALBUM_ENTRY "/?" JAMENDO_RANGE "&artist_id=%s"
-#define JAMENDO_GET_TRACKS_FROM_ALBUM JAMENDO_TRACK_ENTRY  "/?" JAMENDO_RANGE "&album_id=%s"
-#define JAMENDO_GET_ARTIST JAMENDO_ARTIST_ENTRY "/?id=%s"
-
-#define JAMENDO_GET_ALBUM  JAMENDO_ALBUM_ENTRY  "/?id=%s"
-#define JAMENDO_GET_TRACK  JAMENDO_TRACK_ENTRY  "/?id=%s"
-
-#define JAMENDO_SEARCH_ARTIST JAMENDO_ARTIST_ENTRY "/?" JAMENDO_RANGE "&searchquery=%s"
-#define JAMENDO_SEARCH_ALBUM  JAMENDO_ALBUM_ENTRY  "/?" JAMENDO_RANGE "&searchquery=%s"
-#define JAMENDO_SEARCH_TRACK  JAMENDO_TRACK_ENTRY  "/?" JAMENDO_RANGE "&searchquery=%s"
-#define JAMENDO_SEARCH_ALL    JAMENDO_TRACK_ENTRY  "/?" JAMENDO_RANGE
-
-/* --- Plugin information --- */
-
-#define PLUGIN_ID   JAMENDO_PLUGIN_ID
-
-#define SOURCE_ID   "grl-jamendo"
-#define SOURCE_NAME "Jamendo"
-#define SOURCE_DESC "A source for browsing and searching Jamendo videos"
-
-#define AUTHOR      "Igalia S.L."
-#define LICENSE     "LGPL"
-#define SITE        "http://www.igalia.com";
-
-enum {
-  METADATA,
-  BROWSE,
-  QUERY,
-  SEARCH
-};
-
-typedef enum {
-  JAMENDO_ARTIST_CAT = 1,
-  JAMENDO_ALBUM_CAT,
-  JAMENDO_FEEDS_CAT,
-  JAMENDO_TRACK_CAT,
-} JamendoCategory;
-
-typedef struct {
-  JamendoCategory category;
-  gchar *id;
-  gchar *artist_name;
-  gchar *artist_genre;
-  gchar *artist_url;
-  gchar *artist_image;
-  gchar *album_name;
-  gchar *album_genre;
-  gchar *album_url;
-  gchar *album_duration;
-  gchar *album_image;
-  gchar *track_name;
-  gchar *track_url;
-  gchar *track_stream;
-  gchar *track_duration;
-  gchar *feed_name;
-} Entry;
-
-typedef struct {
-  gint type;
-  union {
-    GrlMediaSourceBrowseSpec *bs;
-    GrlMediaSourceQuerySpec *qs;
-    GrlMediaSourceMetadataSpec *ms;
-    GrlMediaSourceSearchSpec *ss;
-  } spec;
-  xmlNodePtr node;
-  xmlDocPtr doc;
-  guint total_results;
-  guint index;
-  guint offset;
-  gboolean cancelled;
-} XmlParseEntries;
-
-struct Feeds {
-  gchar *name;
-  JamendoCategory cat;
-  gchar *url;
-} feeds[] = {
-  { "Albums of the week", JAMENDO_ALBUM_CAT,
-    JAMENDO_GET_ALBUMS "&order=ratingweek_desc", },
-  { "Tracks of the week", JAMENDO_TRACK_CAT,
-    JAMENDO_GET_TRACKS "&order=ratingweek_desc", },
-  { "New releases", JAMENDO_TRACK_CAT,
-    JAMENDO_GET_TRACKS "&order=releasedate_desc", },
-  { "Top artists", JAMENDO_ARTIST_CAT,
-    JAMENDO_GET_ARTISTS "&order=rating_desc", },
-  { "Top albums", JAMENDO_ALBUM_CAT,
-    JAMENDO_GET_ALBUMS "&order=rating_desc", },
-  { "Top tracks", JAMENDO_TRACK_CAT,
-    JAMENDO_GET_TRACKS "&order=rating_desc", },
-};
-
-struct _GrlJamendoSourcePriv {
-  GrlNetWc *wc;
-  GCancellable *cancellable;
-};
-
-#define GRL_JAMENDO_SOURCE_GET_PRIVATE(object)		\
-  (G_TYPE_INSTANCE_GET_PRIVATE((object),                \
-                               GRL_JAMENDO_SOURCE_TYPE,	\
-                               GrlJamendoSourcePriv))
-
-static GrlJamendoSource *grl_jamendo_source_new (void);
-
-gboolean grl_jamendo_plugin_init (GrlPluginRegistry *registry,
-                                  const GrlPluginInfo *plugin,
-                                  GList *configs);
-
-static const GList *grl_jamendo_source_supported_keys (GrlMetadataSource *source);
-
-static void grl_jamendo_source_metadata (GrlMediaSource *source,
-                                         GrlMediaSourceMetadataSpec *ms);
-
-static void grl_jamendo_source_browse (GrlMediaSource *source,
-                                       GrlMediaSourceBrowseSpec *bs);
-
-static void grl_jamendo_source_query (GrlMediaSource *source,
-                                      GrlMediaSourceQuerySpec *qs);
-
-static void grl_jamendo_source_search (GrlMediaSource *source,
-                                       GrlMediaSourceSearchSpec *ss);
-
-static void grl_jamendo_source_cancel (GrlMediaSource *source,
-                                       guint operation_id);
-
-/* =================== Jamendo Plugin  =============== */
-
-gboolean
-grl_jamendo_plugin_init (GrlPluginRegistry *registry,
-                         const GrlPluginInfo *plugin,
-                         GList *configs)
-{
-  GRL_LOG_DOMAIN_INIT (jamendo_log_domain, "jamendo");
-
-  GRL_DEBUG ("jamendo_plugin_init");
-
-  GrlJamendoSource *source = grl_jamendo_source_new ();
-  grl_plugin_registry_register_source (registry,
-                                       plugin,
-                                       GRL_MEDIA_PLUGIN (source),
-                                       NULL);
-  return TRUE;
-}
-
-GRL_PLUGIN_REGISTER (grl_jamendo_plugin_init,
-                     NULL,
-                     PLUGIN_ID);
-
-/* ================== Jamendo GObject ================ */
-
-static GrlJamendoSource *
-grl_jamendo_source_new (void)
-{
-  GRL_DEBUG ("grl_jamendo_source_new");
-  return g_object_new (GRL_JAMENDO_SOURCE_TYPE,
-		       "source-id", SOURCE_ID,
-		       "source-name", SOURCE_NAME,
-		       "source-desc", SOURCE_DESC,
-		       NULL);
-}
-
-G_DEFINE_TYPE (GrlJamendoSource, grl_jamendo_source, GRL_TYPE_MEDIA_SOURCE);
-
-static void
-grl_jamendo_source_finalize (GObject *object)
-{
-  GrlJamendoSource *self;
-
-  self = GRL_JAMENDO_SOURCE (object);
-  if (self->priv->wc)
-    g_object_unref (self->priv->wc);
-
-  if (self->priv->cancellable
-      && G_IS_CANCELLABLE (self->priv->cancellable))
-    g_object_unref (self->priv->cancellable);
-
-  G_OBJECT_CLASS (grl_jamendo_source_parent_class)->finalize (object);
-}
-
-static void
-grl_jamendo_source_class_init (GrlJamendoSourceClass * klass)
-{
-  GrlMediaSourceClass *source_class = GRL_MEDIA_SOURCE_CLASS (klass);
-  GrlMetadataSourceClass *metadata_class = GRL_METADATA_SOURCE_CLASS (klass);
-  GObjectClass *g_class = G_OBJECT_CLASS (klass);
-  source_class->metadata = grl_jamendo_source_metadata;
-  source_class->browse = grl_jamendo_source_browse;
-  source_class->query = grl_jamendo_source_query;
-  source_class->search = grl_jamendo_source_search;
-  source_class->cancel = grl_jamendo_source_cancel;
-  metadata_class->supported_keys = grl_jamendo_source_supported_keys;
-  g_class->finalize = grl_jamendo_source_finalize;
-
-  g_type_class_add_private (klass, sizeof (GrlJamendoSourcePriv));
-}
-
-static void
-grl_jamendo_source_init (GrlJamendoSource *source)
-{
-  source->priv = GRL_JAMENDO_SOURCE_GET_PRIVATE (source);
-
-  /* If we try to get too much elements in a single step, Jamendo might return
-     nothing. So limit the maximum amount of elements in each query */
-  grl_media_source_set_auto_split_threshold (GRL_MEDIA_SOURCE (source),
-                                             MAX_ELEMENTS);
-}
-
-/* ======================= Utilities ==================== */
-
-#if 0
-static void
-print_entry (Entry *entry)
-{
-  g_print ("Entry Information:\n");
-  g_print ("            ID: %s\n", entry->id);
-  g_print ("   Artist Name: %s\n", entry->artist_name);
-  g_print ("  Artist Genre: %s\n", entry->artist_genre);
-  g_print ("    Artist URL: %s\n", entry->artist_url);
-  g_print ("  Artist Image: %s\n", entry->artist_image);
-  g_print ("    Album Name: %s\n", entry->album_name);
-  g_print ("   Album Genre: %s\n", entry->album_genre);
-  g_print ("     Album URL: %s\n", entry->album_url);
-  g_print ("Album Duration: %s\n", entry->album_duration);
-  g_print ("   Album Image: %s\n", entry->album_image);
-  g_print ("    Track Name: %s\n", entry->track_name);
-  g_print ("     Track URL: %s\n", entry->track_url);
-  g_print ("  Track Stream: %s\n", entry->track_stream);
-  g_print ("Track Duration: %s\n", entry->track_duration);
-  g_print ("     Feed Name: %s\n", entry->feed_name);
-}
-#endif
-
-static void
-free_entry (Entry *entry)
-{
-  g_free (entry->id);
-  g_free (entry->artist_name);
-  g_free (entry->artist_genre);
-  g_free (entry->artist_url);
-  g_free (entry->artist_image);
-  g_free (entry->album_name);
-  g_free (entry->album_genre);
-  g_free (entry->album_url);
-  g_free (entry->album_duration);
-  g_free (entry->album_image);
-  g_free (entry->track_name);
-  g_free (entry->track_url);
-  g_free (entry->track_stream);
-  g_free (entry->track_duration);
-  g_free (entry->feed_name);
-  g_slice_free (Entry, entry);
-}
-
-static gint
-xml_count_children (xmlNodePtr node)
-{
-#if (LIBXML2_VERSION >= 20700)
-  return xmlChildElementCount (node);
-#else
-  gint nchildren = 0;
-  xmlNodePtr i = node->xmlChildrenNode;
-
-  while (i) {
-    nchildren++;
-    i = i->next;
-  }
-
-  return nchildren;
-#endif
-}
-
-static void
-xml_parse_result (const gchar *str, GError **error, XmlParseEntries *xpe)
-{
-  xmlDocPtr doc;
-  xmlNodePtr node;
-  gint child_nodes = 0;
-
-  doc = xmlReadMemory (str, strlen (str), NULL, NULL,
-                       XML_PARSE_RECOVER | XML_PARSE_NOBLANKS);
-  if (!doc) {
-    *error = g_error_new (GRL_CORE_ERROR,
-			  GRL_CORE_ERROR_BROWSE_FAILED,
-			  "Failed to parse Jamendo's response");
-    goto free_resources;
-  }
-
-  node = xmlDocGetRootElement (doc);
-  if (!node) {
-    *error = g_error_new (GRL_CORE_ERROR,
-			  GRL_CORE_ERROR_BROWSE_FAILED,
-			  "Empty response from Jamendo");
-    goto free_resources;
-  }
-
-  if (xmlStrcmp (node->name, (const xmlChar *) "data")) {
-    *error = g_error_new (GRL_CORE_ERROR,
-			  GRL_CORE_ERROR_BROWSE_FAILED,
-			  "Unexpected response from Jamendo: no data");
-    goto free_resources;
-  }
-
-  child_nodes = xml_count_children (node);
-  node = node->xmlChildrenNode;
-
-  /* Skip offset */
-  while (node && xpe->offset > 0) {
-    node = node->next;
-    child_nodes--;
-    xpe->offset--;
-  }
-
-  xpe->node = node;
-  xpe->doc = doc;
-  xpe->total_results = child_nodes;
-
-  return;
-
- free_resources:
-  xmlFreeDoc (doc);
-}
-
-static Entry *
-xml_parse_entry (xmlDocPtr doc, xmlNodePtr entry)
-{
-  xmlNodePtr node;
-  xmlNs *ns;
-  Entry *data = g_slice_new0 (Entry);
-
-  if (strcmp ((gchar *) entry->name, JAMENDO_ARTIST) == 0) {
-    data->category = JAMENDO_ARTIST_CAT;
-  } else if (strcmp ((gchar *) entry->name, JAMENDO_ALBUM) == 0) {
-    data->category = JAMENDO_ALBUM_CAT;
-  } else if (strcmp ((gchar *) entry->name, JAMENDO_TRACK) == 0) {
-    data->category = JAMENDO_TRACK_CAT;
-  } else {
-    g_return_val_if_reached (NULL);
-  }
-
-  node = entry->xmlChildrenNode;
-
-  while (node) {
-    ns = node->ns;
-
-    if (!xmlStrcmp (node->name, (const xmlChar *) "id")) {
-      data->id =
-        (gchar *) xmlNodeListGetString (doc, node->xmlChildrenNode, 1);
-
-    } else if (!xmlStrcmp (node->name, (const xmlChar *) "artist_name")) {
-      data->artist_name =
-	(gchar *) xmlNodeListGetString (doc, node->xmlChildrenNode, 1);
-
-    } else if (!xmlStrcmp (node->name, (const xmlChar *) "album_name")) {
-      data->album_name =
-	(gchar *) xmlNodeListGetString (doc, node->xmlChildrenNode, 1);
-
-    } else if (!xmlStrcmp (node->name, (const xmlChar *) "artist_genre")) {
-      data->artist_genre =
-        (gchar *) xmlNodeListGetString (doc, node->xmlChildrenNode, 1);
-
-    } else if (!xmlStrcmp (node->name, (const xmlChar *) "artist_url")) {
-      data->artist_url =
-        (gchar *) xmlNodeListGetString (doc, node->xmlChildrenNode, 1);
-
-    } else if (!xmlStrcmp (node->name, (const xmlChar *) "artist_image")) {
-      data->artist_image =
-        (gchar *) xmlNodeListGetString (doc, node->xmlChildrenNode, 1);
-
-    } else if (!xmlStrcmp (node->name, (const xmlChar *) "album_genre")) {
-      data->album_genre =
-        (gchar *) xmlNodeListGetString (doc, node->xmlChildrenNode, 1);
-
-    } else if (!xmlStrcmp (node->name, (const xmlChar *) "album_url")) {
-      data->album_url =
-        (gchar *) xmlNodeListGetString (doc, node->xmlChildrenNode, 1);
-
-    } else if (!xmlStrcmp (node->name, (const xmlChar *) "album_duration")) {
-      data->album_duration =
-        (gchar *) xmlNodeListGetString (doc, node->xmlChildrenNode, 1);
-
-    } else if (!xmlStrcmp (node->name, (const xmlChar *) "album_image")) {
-      data->album_image =
-        (gchar *) xmlNodeListGetString (doc, node->xmlChildrenNode, 1);
-
-    } else if (!xmlStrcmp (node->name, (const xmlChar *) "track_name")) {
-      data->track_name =
-        (gchar *) xmlNodeListGetString (doc, node->xmlChildrenNode, 1);
-
-    } else if (!xmlStrcmp (node->name, (const xmlChar *) "track_url")) {
-      data->track_url =
-        (gchar *) xmlNodeListGetString (doc, node->xmlChildrenNode, 1);
-
-    } else if (!xmlStrcmp (node->name, (const xmlChar *) "track_stream")) {
-      data->track_stream =
-        (gchar *) xmlNodeListGetString (doc, node->xmlChildrenNode, 1);
-
-    } else if (!xmlStrcmp (node->name, (const xmlChar *) "track_duration")) {
-      data->track_duration =
-        (gchar *) xmlNodeListGetString (doc, node->xmlChildrenNode, 1);
-    }
-
-    node = node->next;
-  }
-
-  return data;
-}
-
-static void
-update_media_from_entry (GrlMedia *media, const Entry *entry)
-{
-  gchar *id;
-
-  if (entry->id) {
-    id = g_strdup_printf ("%d/%s", entry->category, entry->id);
-  } else {
-    id = g_strdup_printf ("%d", entry->category);
-  }
-
-  /* Common fields */
-  grl_media_set_id (media, id);
-  g_free (id);
-
-  if (entry->artist_name) {
-    grl_data_set_string (GRL_DATA (media),
-                         GRL_METADATA_KEY_ARTIST,
-                         entry->artist_name);
-  }
-
-  if (entry->album_name) {
-    grl_data_set_string (GRL_DATA (media),
-                         GRL_METADATA_KEY_ALBUM,
-                         entry->album_name);
-  }
-
-  /* Fields for artist */
-  if (entry->category == JAMENDO_ARTIST_CAT) {
-    if (entry->artist_name) {
-      grl_media_set_title (media, entry->artist_name);
-    }
-
-    if (entry->artist_genre) {
-      grl_data_set_string (GRL_DATA (media),
-                           GRL_METADATA_KEY_GENRE,
-                           entry->artist_genre);
-    }
-
-    if (entry->artist_url) {
-      grl_media_set_site (media, entry->artist_url);
-    }
-
-    if (entry->artist_image) {
-      grl_media_set_thumbnail (media, entry->artist_image);
-    }
-
-    /* Fields for album */
-  } else if (entry->category == JAMENDO_ALBUM_CAT) {
-    if (entry->album_name) {
-      grl_media_set_title (media, entry->album_name);
-    }
-
-    if (entry->album_genre) {
-      grl_data_set_string (GRL_DATA (media),
-                           GRL_METADATA_KEY_GENRE,
-                           entry->album_genre);
-    }
-
-    if (entry->album_url) {
-      grl_media_set_site (media, entry->album_url);
-    }
-
-    if (entry->album_image) {
-      grl_media_set_thumbnail (media, entry->album_image);
-    }
-
-    if (entry->album_duration) {
-      grl_media_set_duration (media, atoi (entry->album_duration));
-    }
-
-    /* Fields for track */
-  } else if (entry->category == JAMENDO_TRACK_CAT) {
-    if (entry->track_name) {
-      grl_media_set_title (media, entry->track_name);
-    }
-
-    if (entry->album_genre) {
-      grl_media_audio_set_genre (GRL_MEDIA_AUDIO (media),
-                                 entry->album_genre);
-    }
-
-    if (entry->track_url) {
-      grl_media_set_site (media, entry->track_url);
-    }
-
-    if (entry->album_image) {
-      grl_media_set_thumbnail (media, entry->album_image);
-    }
-
-    if (entry->track_stream) {
-      grl_media_set_url (media, entry->track_stream);
-    }
-
-    if (entry->track_duration) {
-      grl_media_set_duration (media, atoi (entry->track_duration));
-    }
-  } else if (entry->category == JAMENDO_FEEDS_CAT) {
-    if (entry->feed_name) {
-      grl_media_set_title (media, entry->feed_name);
-    }
-  }
-}
-
-static gboolean
-xml_parse_entries_idle (gpointer user_data)
-{
-  XmlParseEntries *xpe = (XmlParseEntries *) user_data;
-  gboolean parse_more;
-  GrlMedia *media = NULL;
-  Entry *entry;
-  gint remaining = 0;
-
-  GRL_DEBUG ("xml_parse_entries_idle");
-
-  parse_more = (xpe->cancelled == FALSE && xpe->node);
-
-  if (parse_more) {
-    entry = xml_parse_entry (xpe->doc, xpe->node);
-    if (entry->category == JAMENDO_TRACK_CAT) {
-      media = grl_media_audio_new ();
-    } else {
-      media = grl_media_box_new ();
-    }
-
-    update_media_from_entry (media, entry);
-    free_entry (entry);
-
-    xpe->index++;
-    xpe->node = xpe->node->next;
-    remaining = xpe->total_results - xpe->index;
-  }
-
-  if (parse_more || xpe->cancelled) {
-    switch (xpe->type) {
-    case BROWSE:
-      xpe->spec.bs->callback (xpe->spec.bs->source,
-                              xpe->spec.bs->browse_id,
-                              media,
-                              remaining,
-                              xpe->spec.bs->user_data,
-                              NULL);
-      break;
-    case QUERY:
-      xpe->spec.qs->callback (xpe->spec.qs->source,
-                              xpe->spec.qs->query_id,
-                              media,
-                              remaining,
-                              xpe->spec.qs->user_data,
-                              NULL);
-      break;
-    case SEARCH:
-      xpe->spec.ss->callback (xpe->spec.ss->source,
-                              xpe->spec.ss->search_id,
-                              media,
-                              remaining,
-                              xpe->spec.ss->user_data,
-                              NULL);
-      break;
-    }
-  }
-
-  if (!parse_more) {
-    xmlFreeDoc (xpe->doc);
-    g_slice_free (XmlParseEntries, xpe);
-  }
-
-  return parse_more;
-}
-
-static void
-read_done_cb (GObject *source_object,
-              GAsyncResult *res,
-              gpointer user_data)
-{
-  XmlParseEntries *xpe = (XmlParseEntries *) user_data;
-  gint error_code = -1;
-  GError *wc_error = NULL;
-  GError *error = NULL;
-  gchar *content = NULL;
-  Entry *entry = NULL;
-
-  /* Check if operation was cancelled */
-  if (xpe->cancelled) {
-    goto invoke_cb;
-  }
-
-  if (!grl_net_wc_request_finish (GRL_NET_WC (source_object),
-                              res,
-                              &content,
-                              NULL,
-                              &wc_error)) {
-    switch (xpe->type) {
-    case METADATA:
-      error_code = GRL_CORE_ERROR_METADATA_FAILED;
-      break;
-    case BROWSE:
-      error_code = GRL_CORE_ERROR_BROWSE_FAILED;
-      break;
-    case QUERY:
-      error_code = GRL_CORE_ERROR_QUERY_FAILED;
-      break;
-    case SEARCH:
-      error_code = GRL_CORE_ERROR_SEARCH_FAILED;
-      break;
-    }
-
-    error = g_error_new (GRL_CORE_ERROR,
-                         error_code,
-                         "Failed to connect Jamendo: '%s'",
-                         wc_error->message);
-    g_error_free (wc_error);
-    goto invoke_cb;
-  }
-
-  if (content) {
-    xml_parse_result (content, &error, xpe);
-  } else {
-    goto invoke_cb;
-  }
-
-  if (error) {
-    goto invoke_cb;
-  }
-
-  if (xpe->node) {
-    if (xpe->type == METADATA) {
-      entry = xml_parse_entry (xpe->doc, xpe->node);
-      xmlFreeDoc (xpe->doc);
-      update_media_from_entry (xpe->spec.ms->media, entry);
-      free_entry (entry);
-      goto invoke_cb;
-    } else {
-      g_idle_add (xml_parse_entries_idle, xpe);
-    }
-  } else {
-    if (xpe->type == METADATA) {
-      error = g_error_new (GRL_CORE_ERROR,
-                           GRL_CORE_ERROR_METADATA_FAILED,
-                           "Unable to get information: '%s'",
-                           grl_media_get_id (xpe->spec.ms->media));
-    }
-    goto invoke_cb;
-  }
-
-  return;
-
- invoke_cb:
-  switch (xpe->type) {
-  case METADATA:
-    xpe->spec.ms->callback (xpe->spec.ms->source,
-                            xpe->spec.ms->media,
-                            xpe->spec.ms->user_data,
-                            error);
-    break;
-  case BROWSE:
-    xpe->spec.bs->callback (xpe->spec.bs->source,
-                            xpe->spec.bs->browse_id,
-                            NULL,
-                            0,
-                            xpe->spec.bs->user_data,
-                            error);
-    break;
-  case QUERY:
-    xpe->spec.qs->callback (xpe->spec.qs->source,
-                            xpe->spec.qs->query_id,
-                            NULL,
-                            0,
-                            xpe->spec.qs->user_data,
-                            error);
-    break;
-  case SEARCH:
-    xpe->spec.ss->callback (xpe->spec.ss->source,
-                            xpe->spec.ss->search_id,
-                            NULL,
-                            0,
-                            xpe->spec.ss->user_data,
-                            error);
-    break;
-  }
-
-  g_slice_free (XmlParseEntries, xpe);
-  if (error) {
-    g_error_free (error);
-  }
-}
-
-static void
-read_url_async (GrlJamendoSource *source,
-                const gchar *url,
-                gpointer user_data)
-{
-  if (!source->priv->wc)
-    source->priv->wc = g_object_new (GRL_TYPE_NET_WC, "throttling", 1, NULL);
-
-  source->priv->cancellable = g_cancellable_new ();
-
-  GRL_DEBUG ("Opening '%s'", url);
-  grl_net_wc_request_async (source->priv->wc,
-                        url,
-                        source->priv->cancellable,
-                        read_done_cb,
-                        user_data);
-}
-
-static void
-update_media_from_root (GrlMedia *media)
-{
-  grl_media_set_title (media, JAMENDO_ROOT_NAME);
-  grl_media_box_set_childcount (GRL_MEDIA_BOX (media), 3);
-}
-
-static void
-update_media_from_artists (GrlMedia *media)
-{
-  Entry entry = {
-    .category = JAMENDO_ARTIST_CAT,
-    .artist_name = JAMENDO_ARTIST "s",
-  };
-
-  update_media_from_entry (media, &entry);
-}
-
-static void
-update_media_from_albums (GrlMedia *media)
-{
-  Entry entry = {
-    .category = JAMENDO_ALBUM_CAT,
-    .album_name = JAMENDO_ALBUM "s",
-  };
-
-  update_media_from_entry (media, &entry);
-}
-
-static void
-update_media_from_feeds (GrlMedia *media)
-{
-  Entry entry = {
-    .category = JAMENDO_FEEDS_CAT,
-    .feed_name = "feeds",
-  };
-
-  update_media_from_entry (media, &entry);
-  grl_media_box_set_childcount (GRL_MEDIA_BOX (media), G_N_ELEMENTS(feeds));
-}
-
-static void
-send_toplevel_categories (GrlMediaSourceBrowseSpec *bs)
-{
-  GrlMedia *media;
-  gint remaining;
-
-  /* Check if all elements must be skipped */
-  if (bs->skip > 1 || bs->count == 0) {
-    bs->callback (bs->source, bs->browse_id, NULL, 0, bs->user_data, NULL);
-    return;
-  }
-
-  remaining = bs->count;
-
-  if (bs->skip == 0) {
-    media = grl_media_box_new ();
-    update_media_from_artists (media);
-    remaining--;
-    bs->callback (bs->source, bs->browse_id, media, remaining, bs->user_data, NULL);
-  }
-
-  if (remaining) {
-    media = grl_media_box_new ();
-    update_media_from_albums (media);
-    bs->callback (bs->source, bs->browse_id, media, remaining, bs->user_data, NULL);
-    remaining--;
-  }
-
-  if (remaining) {
-    media = grl_media_box_new ();
-    update_media_from_feeds (media);
-    bs->callback (bs->source, bs->browse_id, media, 0, bs->user_data, NULL);
-  }
-}
-
-static void
-update_media_from_feed (GrlMedia *media, int i)
-{
-  char *id;
-
-  id = g_strdup_printf("%d/%d", JAMENDO_FEEDS_CAT, i);
-  grl_media_set_id (media, id);
-  g_free (id);
-
-  grl_media_set_title (media, feeds[i].name);
-}
-
-static void
-send_feeds (GrlMediaSourceBrowseSpec *bs)
-{
-  int i;
-  int remaining;
-
-  remaining = MIN (bs->count, G_N_ELEMENTS (feeds));
-  for (i = bs->skip; remaining > 0 && i < G_N_ELEMENTS (feeds); i++) {
-    GrlMedia *media;
-
-    media = grl_media_box_new ();
-    update_media_from_feed (media, i);
-    remaining--;
-    bs->callback (bs->source,
-                  bs->browse_id,
-                  media,
-                  remaining,
-                  bs->user_data,
-                  NULL);
-  }
-}
-
-static gchar *
-get_jamendo_keys (JamendoCategory category)
-{
-  gchar *jamendo_keys = NULL;
-  gchar *keys_for_artist = "artist_name+artist_genre+artist_image+artist_url";
-  gchar *keys_for_album  = "album_name+album_genre+album_image+album_url+album_duration";
-  gchar *keys_for_track  = "track_name+track_stream+track_url+track_duration";
-
-  if (category == JAMENDO_ARTIST_CAT) {
-    jamendo_keys = g_strconcat ("id+", keys_for_artist, NULL);
-  } else if (category == JAMENDO_ALBUM_CAT) {
-    jamendo_keys = g_strconcat ("id+", keys_for_artist,
-                                "+", keys_for_album,
-                                NULL);
-  } else if (category == JAMENDO_TRACK_CAT) {
-    jamendo_keys = g_strconcat ("id+", keys_for_artist,
-                                "+", keys_for_album,
-                                "+", keys_for_track,
-                                NULL);
-  }
-
-  return jamendo_keys;
-}
-
-static gboolean
-parse_query (const gchar *query, JamendoCategory *category, gchar **term)
-{
-  if (!query) {
-    return FALSE;
-  }
-
-  if (g_str_has_prefix (query, JAMENDO_ARTIST "=")) {
-    *category = JAMENDO_ARTIST_CAT;
-    query += 7;
-  } else if (g_str_has_prefix (query, JAMENDO_ALBUM "=")) {
-    *category = JAMENDO_ALBUM_CAT;
-    query += 6;
-  } else if (g_str_has_prefix (query, JAMENDO_TRACK "=")) {
-    *category = JAMENDO_TRACK_CAT;
-    query += 6;
-  } else {
-    return FALSE;
-  }
-
-  *term = g_uri_escape_string (query, NULL, TRUE);
-  return TRUE;
-}
-
-/* ================== API Implementation ================ */
-
-static const GList *
-grl_jamendo_source_supported_keys (GrlMetadataSource *source)
-{
-  static GList *keys = NULL;
-  if (!keys) {
-    keys = grl_metadata_key_list_new (GRL_METADATA_KEY_ID,
-                                      GRL_METADATA_KEY_TITLE,
-                                      GRL_METADATA_KEY_ARTIST,
-                                      GRL_METADATA_KEY_ALBUM,
-                                      GRL_METADATA_KEY_GENRE,
-                                      GRL_METADATA_KEY_URL,
-                                      GRL_METADATA_KEY_DURATION,
-                                      GRL_METADATA_KEY_THUMBNAIL,
-                                      GRL_METADATA_KEY_SITE,
-                                      NULL);
-  }
-  return keys;
-}
-
-static void
-grl_jamendo_source_metadata (GrlMediaSource *source,
-                             GrlMediaSourceMetadataSpec *ms)
-{
-  gchar *url = NULL;
-  gchar *jamendo_keys = NULL;
-  const gchar *id;
-  gchar **id_split = NULL;
-  XmlParseEntries *xpe = NULL;
-  GError *error = NULL;
-  JamendoCategory category;
-
-  GRL_DEBUG ("grl_jamendo_source_metadata");
-
-  if (!ms->media ||
-      !grl_data_key_is_known (GRL_DATA (ms->media),
-                              GRL_METADATA_KEY_ID)) {
-    /* Get info from root */
-    if (!ms->media) {
-      ms->media = grl_media_box_new ();
-    }
-    update_media_from_root (ms->media);
-  } else {
-    id = grl_media_get_id (ms->media);
-    id_split = g_strsplit (id, JAMENDO_ID_SEP, 0);
-
-    if (g_strv_length (id_split) == 0) {
-      error = g_error_new (GRL_CORE_ERROR,
-                           GRL_CORE_ERROR_METADATA_FAILED,
-                           "Invalid id: '%s'",
-                           id);
-      goto send_error;
-    }
-
-    category = atoi (id_split[0]);
-
-    if (category == JAMENDO_ARTIST_CAT) {
-      if (id_split[1]) {
-        /* Requesting information from a specific artist */
-        jamendo_keys = get_jamendo_keys (JAMENDO_ARTIST_CAT);
-        url =
-          g_strdup_printf (JAMENDO_GET_ARTIST,
-                           jamendo_keys,
-                           id_split[1]);
-        g_free (jamendo_keys);
-      } else {
-        /* Requesting information from artist category */
-        update_media_from_artists (ms->media);
-      }
-    } else if (category == JAMENDO_ALBUM_CAT) {
-      if (id_split[1]) {
-        /* Requesting information from a specific album */
-        jamendo_keys = get_jamendo_keys (JAMENDO_ALBUM_CAT);
-        url =
-          g_strdup_printf (JAMENDO_GET_ALBUM,
-                           jamendo_keys,
-                           id_split[1]);
-        g_free (jamendo_keys);
-      } else {
-        /* Requesting information from album category */
-        update_media_from_albums (ms->media);
-      }
-    } else if (category == JAMENDO_TRACK_CAT) {
-      if (id_split[1]) {
-        /* Requesting information from a specific song */
-        jamendo_keys = get_jamendo_keys (JAMENDO_TRACK_CAT);
-        url =
-          g_strdup_printf (JAMENDO_GET_TRACK,
-                           jamendo_keys,
-                           id_split[1]);
-        g_free (jamendo_keys);
-      } else {
-        error = g_error_new (GRL_CORE_ERROR,
-                             GRL_CORE_ERROR_METADATA_FAILED,
-                             "Invalid id: '%s'",
-                             id);
-        g_strfreev (id_split);
-        goto send_error;
-      }
-    } else if (category == JAMENDO_FEEDS_CAT) {
-      if (id_split[1]) {
-        int i;
-
-        i = atoi (id_split[0]);
-        update_media_from_feed (ms->media, i);
-      } else {
-        update_media_from_feeds (ms->media);
-      }
-    } else {
-      error = g_error_new (GRL_CORE_ERROR,
-                           GRL_CORE_ERROR_METADATA_FAILED,
-                           "Invalid id: '%s'",
-                           id);
-      g_strfreev (id_split);
-      goto send_error;
-    }
-  }
-
-  if (id_split) {
-    g_strfreev (id_split);
-  }
-
-  if (url) {
-    xpe = g_slice_new0 (XmlParseEntries);
-    xpe->type = METADATA;
-    xpe->spec.ms = ms;
-    read_url_async (GRL_JAMENDO_SOURCE (source), url, xpe);
-    g_free (url);
-  } else {
-    if (ms->media) {
-      ms->callback (ms->source, ms->media, ms->user_data, NULL);
-    }
-  }
-
-  return;
-
- send_error:
-  ms->callback (ms->source, NULL, ms->user_data, error);
-  g_error_free (error);
-}
-
-static void
-grl_jamendo_source_browse (GrlMediaSource *source,
-                           GrlMediaSourceBrowseSpec *bs)
-{
-  gchar *url = NULL;
-  gchar *jamendo_keys;
-  gchar **container_split = NULL;
-  JamendoCategory category;
-  XmlParseEntries *xpe = NULL;
-  const gchar *container_id;
-  GError *error = NULL;
-  guint page_size;
-  guint page_number;
-  guint page_offset;
-
-  GRL_DEBUG ("grl_jamendo_source_browse");
-
-  container_id = grl_media_get_id (bs->container);
-
-  if (!container_id) {
-    /* Root category: return top-level predefined categories */
-    send_toplevel_categories (bs);
-    return;
-  }
-
-  container_split = g_strsplit (container_id, JAMENDO_ID_SEP, 0);
-
-  if (g_strv_length (container_split) == 0) {
-    error = g_error_new (GRL_CORE_ERROR,
-                         GRL_CORE_ERROR_BROWSE_FAILED,
-                         "Invalid container-id: '%s'",
-                         container_id);
-  } else {
-    category = atoi (container_split[0]);
-    grl_paging_translate (bs->skip,
-                          bs->count,
-                          0,
-                          &page_size,
-                          &page_number,
-                          &page_offset);
-
-    if (category == JAMENDO_ARTIST_CAT) {
-      if (container_split[1]) {
-        jamendo_keys = get_jamendo_keys (JAMENDO_ALBUM_CAT);
-        /* Requesting information from a specific artist */
-        url =
-          g_strdup_printf (JAMENDO_GET_ALBUMS_FROM_ARTIST,
-                           jamendo_keys,
-                           page_size,
-                           page_number,
-                           container_split[1]);
-      } else {
-        /* Browsing through artists */
-        jamendo_keys = get_jamendo_keys (JAMENDO_ARTIST_CAT);
-        url = g_strdup_printf (JAMENDO_GET_ARTISTS,
-                               jamendo_keys,
-                               page_size,
-                               page_number);
-      }
-      g_free (jamendo_keys);
-
-    } else if (category == JAMENDO_ALBUM_CAT) {
-      if (container_split[1]) {
-        /* Requesting information from a specific album */
-        jamendo_keys = get_jamendo_keys (JAMENDO_TRACK_CAT);
-        url =
-          g_strdup_printf (JAMENDO_GET_TRACKS_FROM_ALBUM,
-                           jamendo_keys,
-                           page_size,
-                           page_number,
-                           container_split[1]);
-      } else {
-        /* Browsing through albums */
-        jamendo_keys = get_jamendo_keys (JAMENDO_ALBUM_CAT);
-        url = g_strdup_printf (JAMENDO_GET_ALBUMS,
-                               jamendo_keys,
-                               page_size,
-                               page_number);
-      }
-      g_free (jamendo_keys);
-
-    } else if (category == JAMENDO_FEEDS_CAT) {
-      if (container_split[1]) {
-        int feed_id;
-
-        feed_id = atoi (container_split[1]);
-        jamendo_keys = get_jamendo_keys (feeds[feed_id].cat);
-        url = g_strdup_printf (feeds[feed_id].url,
-                               jamendo_keys,
-                               page_size,
-                               page_number);
-      } else {
-        send_feeds (bs);
-        return;
-      }
-
-    } else if (category == JAMENDO_TRACK_CAT) {
-      error = g_error_new (GRL_CORE_ERROR,
-                           GRL_CORE_ERROR_BROWSE_FAILED,
-                           "Cannot browse through a track: '%s'",
-                           container_id);
-    } else {
-      error = g_error_new (GRL_CORE_ERROR,
-                           GRL_CORE_ERROR_BROWSE_FAILED,
-                           "Invalid container-id: '%s'",
-                           container_id);
-    }
-  }
-
-  if (error) {
-    bs->callback (source, bs->browse_id, NULL, 0, bs->user_data, error);
-    g_error_free (error);
-    return;
-  }
-
-  xpe = g_slice_new0 (XmlParseEntries);
-  xpe->type = BROWSE;
-  xpe->offset = page_offset;
-  xpe->spec.bs = bs;
-
-  grl_media_source_set_operation_data (source, bs->browse_id, xpe);
-
-  read_url_async (GRL_JAMENDO_SOURCE (source), url, xpe);
-  g_free (url);
-  if (container_split) {
-    g_strfreev (container_split);
-  }
-}
-
-/*
- * Query format is "<type>=<text>", where <type> can be either 'artist', 'album'
- * or 'track' and 'text' is the term to search.
- *
- * The result will be also a <type>.
- *
- * Example: search for artists that have the "Shake" in their name or
- * description: "artist=Shake"
- *
- */
-static void
-grl_jamendo_source_query (GrlMediaSource *source,
-                          GrlMediaSourceQuerySpec *qs)
-{
-  GError *error = NULL;
-  JamendoCategory category;
-  gchar *term = NULL;
-  gchar *url;
-  gchar *jamendo_keys = NULL;
-  gchar *query = NULL;
-  XmlParseEntries *xpe = NULL;
-  guint page_size;
-  guint page_number;
-  guint page_offset;
-
-  GRL_DEBUG ("grl_jamendo_source_query");
-
-  if (!parse_query (qs->query, &category, &term)) {
-    error = g_error_new (GRL_CORE_ERROR,
-                         GRL_CORE_ERROR_QUERY_FAILED,
-                         "Query malformed: '%s'",
-                         qs->query);
-    goto send_error;
-  }
-
-  jamendo_keys = get_jamendo_keys (category);
-  switch (category) {
-  case JAMENDO_ARTIST_CAT:
-    query = JAMENDO_SEARCH_ARTIST;
-    break;
-  case JAMENDO_ALBUM_CAT:
-    query = JAMENDO_SEARCH_ALBUM;
-    break;
-  case JAMENDO_TRACK_CAT:
-    query = JAMENDO_SEARCH_TRACK;
-    break;
-  default:
-    g_return_if_reached ();
-  }
-
-  grl_paging_translate (qs->skip,
-                        qs->count,
-                        0,
-                        &page_size,
-                        &page_number,
-                        &page_offset);
-
-  url = g_strdup_printf (query,
-                         jamendo_keys,
-                         page_size,
-                         page_number,
-                         term);
-  g_free (term);
-
-  xpe = g_slice_new0 (XmlParseEntries);
-  xpe->type = QUERY;
-  xpe->offset = page_offset;
-  xpe->spec.qs = qs;
-
-  grl_media_source_set_operation_data (source, qs->query_id, xpe);
-
-  read_url_async (GRL_JAMENDO_SOURCE (source), url, xpe);
-  g_free (url);
-
-  return;
-
- send_error:
-  qs->callback (qs->source, qs->query_id, NULL, 0, qs->user_data, error);
-  g_error_free (error);
-}
-
-
-static void
-grl_jamendo_source_search (GrlMediaSource *source,
-                           GrlMediaSourceSearchSpec *ss)
-{
-  XmlParseEntries *xpe;
-  gchar *jamendo_keys;
-  gchar *url;
-  guint page_size;
-  guint page_number;
-  guint page_offset;
-
-  GRL_DEBUG ("grl_jamendo_source_search");
-
-  jamendo_keys = get_jamendo_keys (JAMENDO_TRACK_CAT);
-
-  grl_paging_translate (ss->skip,
-                        ss->count,
-                        0,
-                        &page_size,
-                        &page_number,
-                        &page_offset);
-
-  if (ss->text) {
-    url = g_strdup_printf (JAMENDO_SEARCH_TRACK,
-                           jamendo_keys,
-                           page_size,
-                           page_number,
-                           ss->text);
-  } else {
-    url = g_strdup_printf (JAMENDO_SEARCH_ALL,
-                           jamendo_keys,
-                           page_size,
-                           page_number);
-  }
-
-  xpe = g_slice_new0 (XmlParseEntries);
-  xpe->type = SEARCH;
-  xpe->offset = page_offset;
-  xpe->spec.ss = ss;
-
-  grl_media_source_set_operation_data (source, ss->search_id, xpe);
-
-  read_url_async (GRL_JAMENDO_SOURCE (source), url, xpe);
-  g_free (url);
-}
-
-static void
-grl_jamendo_source_cancel (GrlMediaSource *source, guint operation_id)
-{
-  XmlParseEntries *xpe;
-  GrlJamendoSourcePriv *priv;
-
-  g_return_if_fail (GRL_IS_JAMENDO_SOURCE (source));
-
-  priv = GRL_JAMENDO_SOURCE_GET_PRIVATE (source);
-
-  if (priv->cancellable && G_IS_CANCELLABLE (priv->cancellable))
-    g_cancellable_cancel (priv->cancellable);
-  priv->cancellable = NULL;
-
-  if (priv->wc)
-    grl_net_wc_flush_delayed_requests (priv->wc);
-
-  GRL_DEBUG ("grl_jamendo_source_cancel");
-
-  xpe = (XmlParseEntries *) grl_media_source_get_operation_data (source,
-                                                                 operation_id);
-
-  if (xpe) {
-    xpe->cancelled = TRUE;
-  }
-}
diff --git a/src/jamendo/grl-jamendo.h b/src/jamendo/grl-jamendo.h
deleted file mode 100644
index 15a4b75..0000000
--- a/src/jamendo/grl-jamendo.h
+++ /dev/null
@@ -1,78 +0,0 @@
-/*
- * Copyright (C) 2010 Igalia S.L.
- *
- * Contact: Iago Toral Quiroga <itoral igalia com>
- *
- * Authors: Juan A. Suarez Romero <jasuarez igalia 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_JAMENDO_SOURCE_H_
-#define _GRL_JAMENDO_SOURCE_H_
-
-#include <grilo.h>
-
-#define GRL_JAMENDO_SOURCE_TYPE                 \
-  (grl_jamendo_source_get_type ())
-
-#define GRL_JAMENDO_SOURCE(obj)                         \
-  (G_TYPE_CHECK_INSTANCE_CAST ((obj),                   \
-                               GRL_JAMENDO_SOURCE_TYPE, \
-                               GrlJamendoSource))
-
-#define GRL_IS_JAMENDO_SOURCE(obj)                              \
-  (G_TYPE_CHECK_INSTANCE_TYPE ((obj),                           \
-                               GRL_JAMENDO_SOURCE_TYPE))
-
-#define GRL_JAMENDO_SOURCE_CLASS(klass)                 \
-  (G_TYPE_CHECK_CLASS_CAST((klass),                     \
-                           GRL_JAMENDO_SOURCE_TYPE,     \
-                           GrlJamendoSourceClass))
-
-#define GRL_IS_JAMENDO_SOURCE_CLASS(klass)              \
-  (G_TYPE_CHECK_CLASS_TYPE((klass),                     \
-                           GRL_JAMENDO_SOURCE_TYPE))
-
-#define GRL_JAMENDO_SOURCE_GET_CLASS(obj)               \
-  (G_TYPE_INSTANCE_GET_CLASS ((obj),                    \
-                              GRL_JAMENDO_SOURCE_TYPE,  \
-                              GrlJamendoSourceClass))
-
-typedef struct _GrlJamendoSource GrlJamendoSource;
-typedef struct _GrlJamendoSourcePriv GrlJamendoSourcePriv;
-
-struct _GrlJamendoSource {
-
-  GrlMediaSource parent;
-
-  /*< private >*/
-  GrlJamendoSourcePriv *priv;
-
-};
-
-typedef struct _GrlJamendoSourceClass GrlJamendoSourceClass;
-
-struct _GrlJamendoSourceClass {
-
-  GrlMediaSourceClass parent_class;
-
-};
-
-GType grl_jamendo_source_get_type (void);
-
-#endif /* _GRL_JAMENDO_SOURCE_H_ */
diff --git a/src/jamendo/grl-jamendo.xml b/src/jamendo/grl-jamendo.xml
deleted file mode 100644
index e864cfe..0000000
--- a/src/jamendo/grl-jamendo.xml
+++ /dev/null
@@ -1,9 +0,0 @@
-<plugin>
-  <info>
-    <name>Jamendo</name>
-    <description>A plugin for browsing and searching Jamendo videos</description>
-    <author>Igalia S.L.</author>
-    <license>LGPL</license>
-    <site>http://www.igalia.com</site>
-  </info>
-</plugin>
diff --git a/src/lastfm-albumart/Makefile.am b/src/lastfm-albumart/Makefile.am
deleted file mode 100644
index ad7bc6e..0000000
--- a/src/lastfm-albumart/Makefile.am
+++ /dev/null
@@ -1,36 +0,0 @@
-#
-# Makefile.am
-#
-# Author: Juan A. Suarez Romero <jasuarez igalia com>
-#
-# Copyright (C) 2010, 2011 Igalia S.L. All rights reserved.
-
-lib_LTLIBRARIES = libgrllastfm-albumart.la
-
-libgrllastfm_albumart_la_CFLAGS =	\
-	$(DEPS_CFLAGS) 			\
-	$(GRLNET_CFLAGS) 		\
-	$(XML_CFLAGS)
-
-libgrllastfm_albumart_la_LIBADD =	\
-	$(DEPS_LIBS)			\
-	$(GRLNET_LIBS)			\
-	$(XML_LIBS)
-
-libgrllastfm_albumart_la_LDFLAGS = \
-	-module			   \
-	-avoid-version
-
-libgrllastfm_albumart_la_SOURCES = grl-lastfm-albumart.c grl-lastfm-albumart.h
-
-libdir = $(GRL_PLUGINS_DIR)
-lastfmalbumartxmldir	= $(GRL_PLUGINS_CONF_DIR)
-lastfmalbumartxml_DATA	= $(LASTFM_ALBUMART_PLUGIN_ID).xml
-
-EXTRA_DIST = $(lastfmalbumartxml_DATA)
-
-MAINTAINERCLEANFILES =	\
-	*.in	\
-	*~
-
-DISTCLEANFILES = $(MAINTAINERCLEANFILES)
diff --git a/src/lastfm-albumart/grl-lastfm-albumart.c b/src/lastfm-albumart/grl-lastfm-albumart.c
deleted file mode 100644
index 99825a2..0000000
--- a/src/lastfm-albumart/grl-lastfm-albumart.c
+++ /dev/null
@@ -1,316 +0,0 @@
-/*
- * Copyright (C) 2010 Igalia S.L.
- *
- * Contact: Iago Toral Quiroga <itoral igalia com>
- *
- * Authors: Juan A. Suarez Romero <jasuarez igalia 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 <net/grl-net.h>
-#include <libxml/parser.h>
-#include <libxml/xmlmemory.h>
-#include <libxml/xpath.h>
-
-#include "grl-lastfm-albumart.h"
-
-/* ---------- Logging ---------- */
-
-#define GRL_LOG_DOMAIN_DEFAULT lastfm_albumart_log_domain
-GRL_LOG_DOMAIN_STATIC(lastfm_albumart_log_domain);
-
-/* -------- Last.FM API -------- */
-
-#define LASTFM_GET_ALBUM "http://ws.audioscrobbler.com/1.0/album/%s/%s/info.xml";
-#define LASTFM_XML_COVER "/album/coverart/medium"
-
-/* ------- Pluging Info -------- */
-
-#define PLUGIN_ID   LASTFM_ALBUMART_PLUGIN_ID
-
-#define SOURCE_ID   "grl-lastfm-albumart"
-#define SOURCE_NAME "Album art Provider from Last.FM"
-#define SOURCE_DESC "A plugin for getting album arts using Last.FM as backend"
-
-#define AUTHOR      "Igalia S.L."
-#define LICENSE     "LGPL"
-#define SITE        "http://www.igalia.com";
-
-static GrlNetWc *wc;
-
-static GrlLastfmAlbumartSource *grl_lastfm_albumart_source_new (void);
-
-static void grl_lastfm_albumart_source_finalize (GObject *object);
-
-static void grl_lastfm_albumart_source_resolve (GrlMetadataSource *source,
-                                                GrlMetadataSourceResolveSpec *rs);
-
-static const GList *grl_lastfm_albumart_source_supported_keys (GrlMetadataSource *source);
-
-static const GList *grl_lastfm_albumart_source_key_depends (GrlMetadataSource *source,
-                                                            GrlKeyID key_id);
-
-gboolean grl_lastfm_albumart_source_plugin_init (GrlPluginRegistry *registry,
-                                                 const GrlPluginInfo *plugin,
-                                                 GList *configs);
-
-
-/* =================== Last.FM-AlbumArt Plugin  =============== */
-
-gboolean
-grl_lastfm_albumart_source_plugin_init (GrlPluginRegistry *registry,
-                                        const GrlPluginInfo *plugin,
-                                        GList *configs)
-{
-  GRL_LOG_DOMAIN_INIT (lastfm_albumart_log_domain, "lastfm-albumart");
-
-  GRL_DEBUG ("grl_lastfm_albumart_source_plugin_init");
-
-  GrlLastfmAlbumartSource *source = grl_lastfm_albumart_source_new ();
-  grl_plugin_registry_register_source (registry,
-                                       plugin,
-                                       GRL_MEDIA_PLUGIN (source),
-                                       NULL);
-  return TRUE;
-}
-
-GRL_PLUGIN_REGISTER (grl_lastfm_albumart_source_plugin_init,
-                     NULL,
-                     PLUGIN_ID);
-
-/* ================== Last.FM-AlbumArt GObject ================ */
-
-static GrlLastfmAlbumartSource *
-grl_lastfm_albumart_source_new (void)
-{
-  GRL_DEBUG ("grl_lastfm_albumart_source_new");
-  return g_object_new (GRL_LASTFM_ALBUMART_SOURCE_TYPE,
-		       "source-id", SOURCE_ID,
-		       "source-name", SOURCE_NAME,
-		       "source-desc", SOURCE_DESC,
-		       NULL);
-}
-
-static void
-grl_lastfm_albumart_source_class_init (GrlLastfmAlbumartSourceClass * klass)
-{
-  GrlMetadataSourceClass *metadata_class = GRL_METADATA_SOURCE_CLASS (klass);
-  metadata_class->supported_keys = grl_lastfm_albumart_source_supported_keys;
-  metadata_class->key_depends = grl_lastfm_albumart_source_key_depends;
-  metadata_class->resolve = grl_lastfm_albumart_source_resolve;
-
-  GObjectClass *gobject_class = G_OBJECT_CLASS (klass);
-  gobject_class->finalize = grl_lastfm_albumart_source_finalize;
-}
-
-static void
-grl_lastfm_albumart_source_init (GrlLastfmAlbumartSource *source)
-{
-}
-
-G_DEFINE_TYPE (GrlLastfmAlbumartSource,
-               grl_lastfm_albumart_source,
-               GRL_TYPE_METADATA_SOURCE);
-
-static void
-grl_lastfm_albumart_source_finalize (GObject *object)
-{
-  if (wc && GRL_IS_NET_WC (wc))
-    g_object_unref (wc);
-
-  G_OBJECT_CLASS (grl_lastfm_albumart_source_parent_class)->finalize (object);
-}
-
-/* ======================= Utilities ==================== */
-
-static gchar *
-xml_get_image (const gchar *xmldata)
-{
-  xmlDocPtr doc;
-  xmlXPathContextPtr xpath_ctx;
-  xmlXPathObjectPtr xpath_res;
-  gchar *image = NULL;
-
-  doc = xmlReadMemory (xmldata, xmlStrlen ((xmlChar*) xmldata), NULL, NULL,
-                       XML_PARSE_RECOVER | XML_PARSE_NOBLANKS);
-  if (!doc) {
-    return NULL;
-  }
-
-  xpath_ctx = xmlXPathNewContext (doc);
-  if (!xpath_ctx) {
-    xmlFreeDoc (doc);
-    return NULL;
-  }
-
-  xpath_res = xmlXPathEvalExpression ((xmlChar *) LASTFM_XML_COVER,
-                                      xpath_ctx);
-  if (!xpath_res) {
-    xmlXPathFreeContext (xpath_ctx);
-    xmlFreeDoc (doc);
-    return NULL;
-  }
-
-  if (xpath_res->nodesetval->nodeTab) {
-    image =
-      (gchar *) xmlNodeListGetString (doc,
-                                      xpath_res->nodesetval->nodeTab[0]->xmlChildrenNode,
-                                      1);
-  }
-  xmlXPathFreeObject (xpath_res);
-  xmlXPathFreeContext (xpath_ctx);
-  xmlFreeDoc (doc);
-
-  return image;
-}
-
-static void
-read_done_cb (GObject *source_object,
-              GAsyncResult *res,
-              gpointer user_data)
-{
-  GrlMetadataSourceResolveSpec *rs =
-    (GrlMetadataSourceResolveSpec *) user_data;
-  GError *error = NULL;
-  GError *wc_error = NULL;
-  gchar *content = NULL;
-  gchar *image = NULL;
-
-  if (!grl_net_wc_request_finish (GRL_NET_WC (source_object),
-                              res,
-                              &content,
-                              NULL,
-                              &wc_error)) {
-    error = g_error_new (GRL_CORE_ERROR,
-                         GRL_CORE_ERROR_RESOLVE_FAILED,
-                         "Failed to connect to Last.FM: '%s'",
-                         wc_error->message);
-    rs->callback (rs->source, rs->media, rs->user_data, error);
-    g_error_free (wc_error);
-    g_error_free (error);
-
-    return;
-  }
-
-  image = xml_get_image (content);
-  if (image) {
-    grl_data_set_string (GRL_DATA (rs->media),
-                         GRL_METADATA_KEY_THUMBNAIL,
-                         image);
-    g_free (image);
-  }
-
-  rs->callback (rs->source, rs->media, rs->user_data, NULL);
-}
-
-static void
-read_url_async (const gchar *url, gpointer user_data)
-{
-  if (!wc)
-    wc = grl_net_wc_new ();
-
-  GRL_DEBUG ("Opening '%s'", url);
-  grl_net_wc_request_async (wc, url, NULL, read_done_cb, user_data);
-}
-
-/* ================== API Implementation ================ */
-
-static const GList *
-grl_lastfm_albumart_source_supported_keys (GrlMetadataSource *source)
-{
-  static GList *keys = NULL;
-
-  if (!keys) {
-    keys = grl_metadata_key_list_new (GRL_METADATA_KEY_THUMBNAIL,
-                                      NULL);
-  }
-
-  return keys;
-}
-
-static const GList *
-grl_lastfm_albumart_source_key_depends (GrlMetadataSource *source,
-                                        GrlKeyID key_id)
-{
-  static GList *deps = NULL;
-
-  if (!deps) {
-    deps = grl_metadata_key_list_new (GRL_METADATA_KEY_ARTIST,
-                                      GRL_METADATA_KEY_ALBUM,
-                                      NULL);
-  }
-
-  if (key_id == GRL_METADATA_KEY_THUMBNAIL) {
-    return deps;
-  }
-
-  return  NULL;
-}
-
-static void
-grl_lastfm_albumart_source_resolve (GrlMetadataSource *source,
-                                    GrlMetadataSourceResolveSpec *rs)
-{
-  const gchar *artist = NULL;
-  const gchar *album = NULL;
-  gchar *esc_artist = NULL;
-  gchar *esc_album = NULL;
-  gchar *url = NULL;
-
-  GRL_DEBUG ("grl_lastfm_albumart_source_resolve");
-
-  GList *iter;
-
-  /* Check that albumart is requested */
-  iter = rs->keys;
-  while (iter) {
-    if (iter->data == GRL_METADATA_KEY_THUMBNAIL) {
-      break;
-    } else {
-      iter = g_list_next (iter);
-    }
-  }
-
-  if (iter == NULL) {
-    GRL_DEBUG ("No supported key was requested");
-    rs->callback (source, rs->media, rs->user_data, NULL);
-  } else {
-    artist = grl_data_get_string (GRL_DATA (rs->media),
-                                  GRL_METADATA_KEY_ARTIST);
-
-    album = grl_data_get_string (GRL_DATA (rs->media),
-                                 GRL_METADATA_KEY_ALBUM);
-
-    if (!artist || !album) {
-      GRL_DEBUG ("Missing dependencies");
-      rs->callback (source, rs->media, rs->user_data, NULL);
-    } else {
-      esc_artist = g_uri_escape_string (artist, NULL, TRUE);
-      esc_album = g_uri_escape_string (album, NULL, TRUE);
-      url = g_strdup_printf (LASTFM_GET_ALBUM, esc_artist, esc_album);
-      read_url_async (url, rs);
-      g_free (esc_artist);
-      g_free (esc_album);
-      g_free (url);
-    }
-  }
-}
diff --git a/src/lastfm-albumart/grl-lastfm-albumart.h b/src/lastfm-albumart/grl-lastfm-albumart.h
deleted file mode 100644
index 43a5ab5..0000000
--- a/src/lastfm-albumart/grl-lastfm-albumart.h
+++ /dev/null
@@ -1,74 +0,0 @@
-/*
- * Copyright (C) 2010 Igalia S.L.
- *
- * Contact: Iago Toral Quiroga <itoral igalia com>
- *
- * Authors: Juan A. Suarez Romero <jasuarez igalia 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_LASTFM_ALBUMART_SOURCE_H_
-#define _GRL_LASTFM_ALBUMART_SOURCE_H_
-
-#include <grilo.h>
-
-#define GRL_LASTFM_ALBUMART_SOURCE_TYPE         \
-  (grl_lastfm_albumart_source_get_type ())
-
-#define GRL_LASTFM_ALBUMART_SOURCE(obj)                         \
-  (G_TYPE_CHECK_INSTANCE_CAST ((obj),                           \
-                               GRL_LASTFM_ALBUMART_SOURCE_TYPE, \
-                               GrlLastfmAlbumartSource))
-
-#define GRL_IS_LASTFM_ALBUMART_SOURCE(obj)                              \
-  (G_TYPE_CHECK_INSTANCE_TYPE ((obj),                                   \
-                               GRL_LASTFM_ALBUMART_SOURCE_TYPE))
-
-#define GRL_LASTFM_ALBUMART_SOURCE_CLASS(klass)                 \
-  (G_TYPE_CHECK_CLASS_CAST((klass),                             \
-                           GRL_LASTFM_ALBUMART_SOURCE_TYPE,     \
-                           GrlLastfmAlbumartSourceClass))
-
-#define GRL_IS_LASTFM_ALBUMART_SOURCE_CLASS(klass)              \
-  (G_TYPE_CHECK_CLASS_TYPE((klass),                             \
-                           GRL_LASTFM_ALBUMART_SOURCE_TYPE))
-
-#define GRL_LASTFM_ALBUMART_SOURCE_GET_CLASS(obj)                       \
-  (G_TYPE_INSTANCE_GET_CLASS ((obj),                                    \
-                              GRL_LASTFM_ALBUMART_SOURCE_TYPE,          \
-                              GrlLastfmAlbumartSourceClass))
-
-typedef struct _GrlLastfmAlbumartSource GrlLastfmAlbumartSource;
-
-struct _GrlLastfmAlbumartSource {
-
-  GrlMetadataSource parent;
-
-};
-
-typedef struct _GrlLastfmAlbumartSourceClass GrlLastfmAlbumartSourceClass;
-
-struct _GrlLastfmAlbumartSourceClass {
-
-  GrlMetadataSourceClass parent_class;
-
-};
-
-GType grl_lastfm_albumart_source_get_type (void);
-
-#endif /* _GRL_LASTFM_ALBUMART_SOURCE_H_ */
diff --git a/src/lastfm-albumart/grl-lastfm-albumart.xml b/src/lastfm-albumart/grl-lastfm-albumart.xml
deleted file mode 100644
index f2b64dd..0000000
--- a/src/lastfm-albumart/grl-lastfm-albumart.xml
+++ /dev/null
@@ -1,9 +0,0 @@
-<plugin>
-  <info>
-    <name>Album art Provider from Last.FM</name>
-    <description>A plugin for getting album arts using Last.FM as backend</description>
-    <author>Igalia S.L.</author>
-    <license>LGPL</license>
-    <site>http://www.igalia.com</site>
-  </info>
-</plugin>
diff --git a/src/local-metadata/Makefile.am b/src/local-metadata/Makefile.am
deleted file mode 100644
index 6840cf3..0000000
--- a/src/local-metadata/Makefile.am
+++ /dev/null
@@ -1,34 +0,0 @@
-#
-# Makefile.am
-#
-# Author: Guillaume Emont <gemont igalia com>
-#
-# Copyright (C) 2010-2011 Igalia S.L. All rights reserved.
-
-lib_LTLIBRARIES		 = libgrllocalmetadata.la
-
-libgrllocalmetadata_la_CFLAGS =	\
-	$(DEPS_CFLAGS) \
-	$(GIO_CFLAGS)
-
-libgrllocalmetadata_la_LIBADD =	\
-	$(DEPS_LIBS) \
-	$(GIO_LIBS)
-
-libgrllocalmetadata_la_LDFLAGS = \
-	-module			\
-	-avoid-version
-
-libgrllocalmetadata_la_SOURCES = grl-local-metadata.c grl-local-metadata.h
-
-libdir=$(GRL_PLUGINS_DIR)
-localmetadataxmldir	= $(GRL_PLUGINS_CONF_DIR)
-localmetadataxml_DATA	= $(LOCALMETADATA_PLUGIN_ID).xml
-
-EXTRA_DIST = $(localmetadataxml_DATA)
-
-MAINTAINERCLEANFILES	 = \
-	*.in \
-	*~
-
-DISTCLEANFILES = $(MAINTAINERCLEANFILES)
diff --git a/src/local-metadata/grl-local-metadata.c b/src/local-metadata/grl-local-metadata.c
deleted file mode 100644
index a535f42..0000000
--- a/src/local-metadata/grl-local-metadata.c
+++ /dev/null
@@ -1,262 +0,0 @@
-/*
- * Copyright (C) 2010-2011 Igalia S.L.
- *
- * Contact: Guillaume Emont <gemont igalia 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 <grilo.h>
-#include <gio/gio.h>
-
-#include "grl-local-metadata.h"
-
-#define GRL_LOG_DOMAIN_DEFAULT local_metadata_log_domain
-GRL_LOG_DOMAIN_STATIC(local_metadata_log_domain);
-
-#define PLUGIN_ID   LOCALMETADATA_PLUGIN_ID
-
-#define SOURCE_ID   "grl-local-metadata"
-#define SOURCE_NAME "Local Metadata Provider"
-#define SOURCE_DESC "A source providing locally available metadata"
-
-#define AUTHOR      "Igalia S.L."
-#define LICENSE     "LGPL"
-#define SITE        "http://www.igalia.com";
-
-
-static GrlLocalMetadataSource *grl_local_metadata_source_new (void);
-
-static void grl_local_metadata_source_resolve (GrlMetadataSource *source,
-                                              GrlMetadataSourceResolveSpec *rs);
-
-static const GList *grl_local_metadata_source_supported_keys (GrlMetadataSource *source);
-
-static const GList * grl_local_metadata_source_key_depends (GrlMetadataSource *source,
-                                                            GrlKeyID key_id);
-
-gboolean grl_local_metadata_source_plugin_init (GrlPluginRegistry *registry,
-                                               const GrlPluginInfo *plugin,
-                                               GList *configs);
-
-
-/* =================== GrlLocalMetadata Plugin  =============== */
-
-gboolean
-grl_local_metadata_source_plugin_init (GrlPluginRegistry *registry,
-                                      const GrlPluginInfo *plugin,
-                                      GList *configs)
-{
-  GRL_LOG_DOMAIN_INIT (local_metadata_log_domain, "local-metadata");
-
-  GRL_DEBUG ("grl_local_metadata_source_plugin_init");
-
-  GrlLocalMetadataSource *source = grl_local_metadata_source_new ();
-  grl_plugin_registry_register_source (registry,
-                                       plugin,
-                                       GRL_MEDIA_PLUGIN (source),
-                                       NULL);
-  return TRUE;
-}
-
-GRL_PLUGIN_REGISTER (grl_local_metadata_source_plugin_init,
-                     NULL,
-                     PLUGIN_ID);
-
-/* ================== GrlLocalMetadata GObject ================ */
-
-static GrlLocalMetadataSource *
-grl_local_metadata_source_new (void)
-{
-  GRL_DEBUG ("grl_local_metadata_source_new");
-  return g_object_new (GRL_LOCAL_METADATA_SOURCE_TYPE,
-		       "source-id", SOURCE_ID,
-		       "source-name", SOURCE_NAME,
-		       "source-desc", SOURCE_DESC,
-		       NULL);
-}
-
-static void
-grl_local_metadata_source_class_init (GrlLocalMetadataSourceClass * klass)
-{
-  GrlMetadataSourceClass *metadata_class = GRL_METADATA_SOURCE_CLASS (klass);
-  metadata_class->supported_keys = grl_local_metadata_source_supported_keys;
-  metadata_class->key_depends = grl_local_metadata_source_key_depends;
-  metadata_class->resolve = grl_local_metadata_source_resolve;
-}
-
-static void
-grl_local_metadata_source_init (GrlLocalMetadataSource *source)
-{
-}
-
-G_DEFINE_TYPE (GrlLocalMetadataSource,
-               grl_local_metadata_source,
-               GRL_TYPE_METADATA_SOURCE);
-
-/* ======================= Utilities ==================== */
-static void
-got_file_info (GFile *file, GAsyncResult *result,
-               GrlMetadataSourceResolveSpec *rs)
-{
-  GFileInfo *info;
-  GError *error = NULL;
-  const gchar *thumbnail_path;
-
-  GRL_DEBUG ("got_file_info");
-
-  info = g_file_query_info_finish (file, result, &error);
-  if (error)
-    goto error;
-
-  thumbnail_path =
-      g_file_info_get_attribute_byte_string (info, G_FILE_ATTRIBUTE_THUMBNAIL_PATH);
-
-
-  if (thumbnail_path) {
-    gchar *thumbnail_uri = g_filename_to_uri (thumbnail_path, NULL, &error);
-    if (error)
-      goto error;
-
-    GRL_INFO ("Got thumbnail %s for media: %s", thumbnail_uri,
-              grl_media_get_url (rs->media));
-    grl_media_set_thumbnail (rs->media, thumbnail_uri);
-    g_free (thumbnail_uri);
-
-    rs->callback (rs->source, rs->media, rs->user_data, NULL);
-  } else {
-    GRL_INFO ("Could not find thumbnail for media: %s",
-              grl_media_get_url (rs->media));
-    rs->callback (rs->source, rs->media, rs->user_data, NULL);
-  }
-
-  goto exit;
-
-error:
-    {
-      GError *new_error = g_error_new (GRL_CORE_ERROR, GRL_CORE_ERROR_RESOLVE_FAILED,
-                                       "Got error: %s", error->message);
-      rs->callback (rs->source, rs->media, rs->user_data, new_error);
-
-      g_error_free (error);
-      g_error_free (new_error);
-    }
-
-exit:
-  if (info)
-    g_object_unref (info);
-}
-
-static void
-resolve_image (GrlMetadataSourceResolveSpec *rs)
-{
-  GFile *file;
-
-  GRL_DEBUG ("resolve_image");
-
-  file = g_file_new_for_uri (grl_media_get_url (rs->media));
-
-  g_file_query_info_async (file, G_FILE_ATTRIBUTE_THUMBNAIL_PATH,
-                           G_FILE_QUERY_INFO_NONE, G_PRIORITY_DEFAULT, NULL,
-                           (GAsyncReadyCallback)got_file_info, rs);
-}
-
-static void
-resolve_album_art (GrlMetadataSourceResolveSpec *rs)
-{
-  /* FIXME: implement this, according to
-   * http://live.gnome.org/MediaArtStorageSpec */
-  GError *error;
-  error = g_error_new (GRL_CORE_ERROR, GRL_CORE_ERROR_RESOLVE_FAILED,
-    "Thumbnail resolution for GrlMediaAudio not implemented in local-metadata");
-  rs->callback (rs->source, rs->media, rs->user_data, error);
-  g_error_free (error);
-}
-
-/* ================== API Implementation ================ */
-
-static const GList *
-grl_local_metadata_source_supported_keys (GrlMetadataSource *source)
-{
-  static GList *keys = NULL;
-  if (!keys) {
-    keys = grl_metadata_key_list_new (GRL_METADATA_KEY_THUMBNAIL,
-                                      NULL);
-  }
-  return keys;
-}
-
-static const GList *
-grl_local_metadata_source_key_depends (GrlMetadataSource *source,
-                                       GrlKeyID key_id)
-{
-  static GList *deps = NULL;
-  if (!deps) {
-    deps = grl_metadata_key_list_new (GRL_METADATA_KEY_URL, NULL);
-  }
-
-  if (key_id == GRL_METADATA_KEY_THUMBNAIL)
-    return deps;
-
-  return NULL;
-}
-
-static void
-grl_local_metadata_source_resolve (GrlMetadataSource *source,
-                                  GrlMetadataSourceResolveSpec *rs)
-{
-  const gchar *url;
-  gchar *scheme;
-  GError *error = NULL;
-
-  GRL_DEBUG ("grl_local_metadata_source_resolve");
-
-  url = grl_media_get_url (rs->media);
-  scheme = g_uri_parse_scheme (url);
-
-  if (0 != g_strcmp0 (scheme, "file"))
-    error = g_error_new (GRL_CORE_ERROR, GRL_CORE_ERROR_RESOLVE_FAILED,
-                         "local-metadata needs a url in the file:// scheme");
-  else if (!g_list_find (rs->keys, GRL_METADATA_KEY_THUMBNAIL))
-    error = g_error_new (GRL_CORE_ERROR, GRL_CORE_ERROR_RESOLVE_FAILED,
-                         "local-metadata can only resolve the thumbnail key");
-
-  if (error) {
-    /* No can do! */
-    rs->callback (source, rs->media, rs->user_data, error);
-    g_error_free (error);
-    goto exit;
-  }
-
-  if (GRL_IS_MEDIA_VIDEO (rs->media)
-      || GRL_IS_MEDIA_IMAGE (rs->media)) {
-    resolve_image (rs);
-  } else if (GRL_IS_MEDIA_AUDIO (rs->media)) {
-    resolve_album_art (rs);
-  } else {
-    /* What's that media type? */
-    rs->callback (source, rs->media, rs->user_data, NULL);
-  }
-
-exit:
-  g_free (scheme);
-}
-
diff --git a/src/local-metadata/grl-local-metadata.h b/src/local-metadata/grl-local-metadata.h
deleted file mode 100644
index 448051e..0000000
--- a/src/local-metadata/grl-local-metadata.h
+++ /dev/null
@@ -1,72 +0,0 @@
-/*
- * Copyright (C) 2010-2011 Igalia S.L.
- *
- * Contact: Guillaume Emont <gemont igalia 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_LOCAL_METADATA_SOURCE_H_
-#define _GRL_LOCAL_METADATA_SOURCE_H_
-
-#include <grilo.h>
-
-#define GRL_LOCAL_METADATA_SOURCE_TYPE           \
-  (grl_local_metadata_source_get_type ())
-
-#define GRL_LOCAL_METADATA_SOURCE(obj)                           \
-  (G_TYPE_CHECK_INSTANCE_CAST ((obj),                           \
-                               GRL_LOCAL_METADATA_SOURCE_TYPE,   \
-                               GrlLocalMetadataSource))
-
-#define GRL_IS_LOCAL_METADATA_SOURCE(obj)                        \
-  (G_TYPE_CHECK_INSTANCE_TYPE ((obj),                           \
-                               GRL_LOCAL_METADATA_SOURCE_TYPE))
-
-#define GRL_LOCAL_METADATA_SOURCE_CLASS(klass)                   \
-  (G_TYPE_CHECK_CLASS_CAST((klass),                             \
-                           GRL_LOCAL_METADATA_SOURCE_TYPE,       \
-                           GrlLocalMetadataSourceClass))
-
-#define GRL_IS_LOCAL_METADATA_SOURCE_CLASS(klass)                \
-  (G_TYPE_CHECK_CLASS_TYPE((klass),                             \
-                           GRL_LOCAL_METADATA_SOURCE_TYPE))
-
-#define GRL_LOCAL_METADATA_SOURCE_GET_CLASS(obj)                 \
-  (G_TYPE_INSTANCE_GET_CLASS ((obj),                            \
-                              GRL_LOCAL_METADATA_SOURCE_TYPE,    \
-                              GrlLocalMetadataSourceClass))
-
-typedef struct _GrlLocalMetadataSource GrlLocalMetadataSource;
-
-struct _GrlLocalMetadataSource {
-
-  GrlMetadataSource parent;
-
-};
-
-typedef struct _GrlLocalMetadataSourceClass GrlLocalMetadataSourceClass;
-
-struct _GrlLocalMetadataSourceClass {
-
-  GrlMetadataSourceClass parent_class;
-
-};
-
-GType grl_local_metadata_source_get_type (void);
-
-#endif /* _GRL_LOCAL_METADATA_SOURCE_H_ */
diff --git a/src/local-metadata/grl-local-metadata.xml b/src/local-metadata/grl-local-metadata.xml
deleted file mode 100644
index 073392e..0000000
--- a/src/local-metadata/grl-local-metadata.xml
+++ /dev/null
@@ -1,9 +0,0 @@
-<plugin>
-  <info>
-    <name>Local Metadata Provider</name>
-    <description>A plugin that gets simple-to-obtain metadata from the local system</description>
-    <author>Igalia S.L.</author>
-    <license>LGPL</license>
-    <site>http://www.igalia.com</site>
-  </info>
-</plugin>
diff --git a/src/media/apple-trailers/Makefile.am b/src/media/apple-trailers/Makefile.am
new file mode 100644
index 0000000..0195de5
--- /dev/null
+++ b/src/media/apple-trailers/Makefile.am
@@ -0,0 +1,38 @@
+#
+# Makefile.am
+#
+# Author: Juan A. Suarez Romero <jasuarez igalia com>
+#
+# Copyright (C) 2010, 2011 Igalia S.L. All rights reserved.
+
+lib_LTLIBRARIES		 = libgrlappletrailers.la
+
+libgrlappletrailers_la_CFLAGS =	\
+	$(DEPS_CFLAGS)		\
+	$(GRLNET_CFLAGS)	\
+	$(XML_CFLAGS)
+
+libgrlappletrailers_la_LIBADD =	\
+	$(DEPS_LIBS)		\
+	$(GRLNET_LIBS)		\
+	$(XML_LIBS)
+
+libgrlappletrailers_la_LDFLAGS = \
+	-module			 \
+	-avoid-version
+
+libgrlappletrailers_la_SOURCES =	\
+	grl-apple-trailers.c		\
+	grl-apple-trailers.h
+
+libdir = $(GRL_PLUGINS_DIR)
+appletrailersxmldir	= $(GRL_PLUGINS_CONF_DIR)
+appletrailersxml_DATA	= $(APPLE_TRAILERS_PLUGIN_ID).xml
+
+EXTRA_DIST = $(appletrailersxml_DATA)
+
+MAINTAINERCLEANFILES =	\
+	*.in		\
+	*~
+
+DISTCLEANFILES = $(MAINTAINERCLEANFILES)
diff --git a/src/media/apple-trailers/grl-apple-trailers.c b/src/media/apple-trailers/grl-apple-trailers.c
new file mode 100644
index 0000000..b81748b
--- /dev/null
+++ b/src/media/apple-trailers/grl-apple-trailers.c
@@ -0,0 +1,611 @@
+/*
+ * Copyright (C) 2010 Igalia S.L.
+ * Copyright (C) 2011 Intel Corporation.
+ *
+ * Contact: Iago Toral Quiroga <itoral igalia com>
+ *
+ * Authors: Juan A. Suarez Romero <jasuarez igalia 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 <grilo.h>
+#include <net/grl-net.h>
+#include <libxml/xpath.h>
+
+#include "grl-apple-trailers.h"
+
+/* --------- Logging  -------- */
+
+#define GRL_LOG_DOMAIN_DEFAULT apple_trailers_log_domain
+GRL_LOG_DOMAIN_STATIC(apple_trailers_log_domain);
+
+/* ---- Apple Trailers Service ---- */
+
+#define APPLE_TRAILERS_CURRENT_SD                               \
+  "http://trailers.apple.com/trailers/home/xml/current_480p.xml";
+
+#define APPLE_TRAILERS_CURRENT_HD                               \
+  "http://trailers.apple.com/trailers/home/xml/current_720p.xml";
+
+/* --- Plugin information --- */
+
+#define PLUGIN_ID   APPLE_TRAILERS_PLUGIN_ID
+
+#define SOURCE_ID   "grl-apple-trailers"
+#define SOURCE_NAME "Apple Movie Trailers"
+#define SOURCE_DESC "A plugin for browsing Apple Movie Trailers"
+
+#define AUTHOR      "Igalia S.L."
+#define LICENSE     "LGPL"
+#define SITE        "http://www.igalia.com";
+
+typedef struct {
+  GrlMediaSourceBrowseSpec *bs;
+  xmlDocPtr xml_doc;
+  xmlNodePtr xml_entries;
+  gboolean cancelled;
+} OperationData;
+
+enum {
+  PROP_0,
+  PROP_HD,
+  PROP_LARGE_POSTER,
+};
+
+struct _GrlAppleTrailersSourcePriv {
+  GrlNetWc *wc;
+  GCancellable *cancellable;
+  gboolean hd;
+  gboolean large_poster;
+};
+
+#define GRL_APPLE_TRAILERS_SOURCE_GET_PRIVATE(object)		\
+  (G_TYPE_INSTANCE_GET_PRIVATE((object),                        \
+                               GRL_APPLE_TRAILERS_SOURCE_TYPE,  \
+                               GrlAppleTrailersSourcePriv))
+
+static GrlAppleTrailersSource *grl_apple_trailers_source_new (gboolean hd,
+                                                              gboolean xlarge);
+
+gboolean grl_apple_trailers_plugin_init (GrlPluginRegistry *registry,
+                                         const GrlPluginInfo *plugin,
+                                         GList *configs);
+
+static const GList *grl_apple_trailers_source_supported_keys (GrlMetadataSource *source);
+
+static void grl_apple_trailers_source_browse (GrlMediaSource *source,
+                                              GrlMediaSourceBrowseSpec *bs);
+
+static void grl_apple_trailers_source_cancel (GrlMediaSource *source,
+                                              guint operation_id);
+
+/* =================== Apple Trailers Plugin  =============== */
+
+gboolean
+grl_apple_trailers_plugin_init (GrlPluginRegistry *registry,
+                                const GrlPluginInfo *plugin,
+                                GList *configs)
+{
+  GrlAppleTrailersSource *source;
+  gboolean hd = FALSE;
+  gboolean xlarge = FALSE;
+
+  GRL_LOG_DOMAIN_INIT (apple_trailers_log_domain, "apple-trailers");
+
+  GRL_DEBUG ("apple_trailers_plugin_init");
+
+  for (; configs; configs = g_list_next (configs)) {
+    GrlConfig *config;
+    gchar *definition, *poster_size;
+
+    config = GRL_CONFIG (configs->data);
+    definition = grl_config_get_string (config, "definition");
+    if (definition) {
+      if (*definition != '\0') {
+        if (g_str_equal (definition, "hd")) {
+          hd = TRUE;
+        }
+      }
+      g_free (definition);
+    }
+
+    poster_size = grl_config_get_string (config, "poster-size");
+    if (poster_size) {
+      if (*poster_size != '\0') {
+        if (g_str_equal (poster_size, "xlarge")) {
+          xlarge = TRUE;
+        }
+      }
+      g_free (poster_size);
+    }
+  }
+
+  source = grl_apple_trailers_source_new (hd, xlarge);
+  grl_plugin_registry_register_source (registry,
+                                       plugin,
+                                       GRL_MEDIA_PLUGIN (source),
+                                       NULL);
+  return TRUE;
+}
+
+GRL_PLUGIN_REGISTER (grl_apple_trailers_plugin_init,
+                     NULL,
+                     PLUGIN_ID);
+
+/* ================== AppleTrailers GObject ================ */
+
+static GrlAppleTrailersSource *
+grl_apple_trailers_source_new (gboolean high_definition,
+                               gboolean xlarge)
+{
+  GrlAppleTrailersSource *source;
+
+  GRL_DEBUG ("grl_apple_trailers_source_new%s%s",
+             high_definition ? " (HD)" : "",
+             xlarge ? " (X-large poster)" : "");
+  source = g_object_new (GRL_APPLE_TRAILERS_SOURCE_TYPE,
+                         "source-id", SOURCE_ID,
+                         "source-name", SOURCE_NAME,
+                         "source-desc", SOURCE_DESC,
+                         "high-definition", high_definition,
+			 "large-poster", xlarge,
+                         NULL);
+
+  return source;
+}
+
+G_DEFINE_TYPE (GrlAppleTrailersSource, grl_apple_trailers_source, GRL_TYPE_MEDIA_SOURCE);
+
+static void
+grl_apple_trailers_source_finalize (GObject *object)
+{
+  GrlAppleTrailersSource *self;
+
+  self = GRL_APPLE_TRAILERS_SOURCE (object);
+
+  if (self->priv->wc)
+    g_object_unref (self->priv->wc);
+
+  if (self->priv->cancellable
+      && G_IS_CANCELLABLE (self->priv->cancellable))
+    g_object_unref (self->priv->cancellable);
+
+  G_OBJECT_CLASS (grl_apple_trailers_source_parent_class)->finalize (object);
+}
+
+static void
+grl_apple_trailers_source_set_property (GObject *object,
+                                        guint propid,
+                                        const GValue *value,
+                                        GParamSpec *pspec)
+{
+  GrlAppleTrailersSource *self;
+  self = GRL_APPLE_TRAILERS_SOURCE (object);
+
+  switch (propid) {
+    case PROP_HD:
+      self->priv->hd = g_value_get_boolean (value);
+      break;
+    case PROP_LARGE_POSTER:
+      self->priv->large_poster = g_value_get_boolean (value);
+      break;
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, propid, pspec);
+    }
+}
+
+static void
+grl_apple_trailers_source_class_init (GrlAppleTrailersSourceClass * klass)
+{
+  GrlMediaSourceClass *source_class = GRL_MEDIA_SOURCE_CLASS (klass);
+  GrlMetadataSourceClass *metadata_class = GRL_METADATA_SOURCE_CLASS (klass);
+  GObjectClass *g_class = G_OBJECT_CLASS (klass);
+  source_class->browse = grl_apple_trailers_source_browse;
+  source_class->cancel = grl_apple_trailers_source_cancel;
+  metadata_class->supported_keys = grl_apple_trailers_source_supported_keys;
+  g_class->finalize = grl_apple_trailers_source_finalize;
+  g_class->set_property = grl_apple_trailers_source_set_property;
+
+  g_object_class_install_property (g_class,
+                                   PROP_HD,
+                                   g_param_spec_boolean ("high-definition",
+                                                         "hd",
+                                                         "Hi/Low definition videos",
+                                                         TRUE,
+                                                         G_PARAM_WRITABLE
+                                                         | G_PARAM_CONSTRUCT_ONLY
+                                                         | G_PARAM_STATIC_NAME));
+
+  g_object_class_install_property (g_class,
+                                   PROP_LARGE_POSTER,
+                                   g_param_spec_boolean ("large-poster",
+                                                         "xlarge",
+                                                         "Pick large poster",
+                                                         TRUE,
+                                                         G_PARAM_WRITABLE
+                                                         | G_PARAM_CONSTRUCT_ONLY
+                                                         | G_PARAM_STATIC_NAME));
+
+  g_type_class_add_private (klass, sizeof (GrlAppleTrailersSourcePriv));
+}
+
+static void
+grl_apple_trailers_source_init (GrlAppleTrailersSource *source)
+{
+  source->priv = GRL_APPLE_TRAILERS_SOURCE_GET_PRIVATE (source);
+  source->priv->hd = TRUE;
+}
+
+/* ==================== Private ==================== */
+
+static gchar *
+get_node_value (xmlNodePtr node, const gchar *node_id)
+{
+  gchar *value;
+  xmlXPathContextPtr xpath_ctx;
+  xmlXPathObjectPtr xpath_res;
+
+  xpath_ctx = xmlXPathNewContext (node->doc);
+  if (!xpath_ctx) {
+    return NULL;
+  }
+
+  xpath_res = xmlXPathEvalExpression ((xmlChar *) node_id, xpath_ctx);
+  if (!xpath_res) {
+    xmlXPathFreeContext (xpath_ctx);
+    return NULL;
+  }
+
+  if (xpath_res->nodesetval->nodeTab) {
+    value =
+      (gchar *) xmlNodeListGetString (node->doc,
+                                      xpath_res->nodesetval->nodeTab[0]->xmlChildrenNode,
+                                      1);
+  } else {
+    value = NULL;
+  }
+
+  xmlXPathFreeObject (xpath_res);
+  xmlXPathFreeContext (xpath_ctx);
+
+  return value;
+}
+
+static gint
+runtime_to_seconds (const gchar *runtime)
+{
+  gchar **items;
+  gint seconds;
+
+  if (!runtime) {
+    return 0;
+  }
+
+  seconds = 0;
+  items = g_strsplit (runtime, ":", -1);
+  if (items && items[0] && items[1])
+    seconds = 3600 * atoi (items[0]) + 60 * atoi (items[1]);
+  g_strfreev (items);
+
+  return seconds;
+}
+static GrlMedia *
+build_media_from_movie (xmlNodePtr node, gboolean xlarge)
+{
+  GrlMedia * media;
+  gchar *movie_author;
+  gchar *movie_date;
+  gchar *movie_description;
+  gchar *movie_duration;
+  gchar *movie_genre;
+  gchar *movie_id;
+  gchar *movie_thumbnail;
+  gchar *movie_title;
+  gchar *movie_url;
+  gchar *movie_rating;
+  gchar *movie_studio;
+  gchar *movie_copyright;
+
+  media = grl_media_video_new ();
+
+  movie_id = (gchar *) xmlGetProp (node, (const xmlChar *) "id");
+
+  /* HACK: as get_node_value applies xpath expression from root node, but we
+     want to do from current node, dup the node and mark it as root node */
+
+  xmlDocPtr xml_doc = xmlNewDoc ((const xmlChar *) "1.0");
+  xmlNodePtr node_dup = xmlCopyNode (node, 1);
+  xmlDocSetRootElement (xml_doc, node_dup);
+  movie_author = get_node_value (node_dup, "/movieinfo/info/director");
+  movie_date = get_node_value (node_dup, "/movieinfo/info/releasedate");
+  movie_description = get_node_value (node_dup, "/movieinfo/info/description");
+  movie_duration = get_node_value (node_dup, "/movieinfo/info/runtime");
+  movie_title = get_node_value (node_dup, "/movieinfo/info/title");
+  movie_genre = get_node_value (node_dup, "/movieinfo/genre/name");
+  if (xlarge)
+    movie_thumbnail = get_node_value (node_dup, "/movieinfo/poster/xlarge");
+  else
+    movie_thumbnail = get_node_value (node_dup, "/movieinfo/poster/location");
+  movie_url = get_node_value (node_dup, "/movieinfo/preview/large");
+  movie_rating = get_node_value (node_dup, "/movieinfo/info/rating");
+  movie_studio = get_node_value (node_dup, "/movieinfo/info/studio");
+  movie_copyright = get_node_value (node_dup, "/movieinfo/info/copyright");
+  xmlFreeDoc (xml_doc);
+
+  grl_media_set_id (media, movie_id);
+  grl_media_set_author (media, movie_author);
+  grl_media_set_date (media, movie_date);
+  grl_media_set_description (media, movie_description);
+  grl_media_set_duration (media, runtime_to_seconds (movie_duration));
+  grl_media_set_title (media, movie_title);
+  grl_data_set_string (GRL_DATA (media),
+                       GRL_METADATA_KEY_GENRE,
+                       movie_genre);
+  grl_media_set_thumbnail (media, movie_thumbnail);
+  grl_media_set_url (media, movie_url);
+  grl_media_set_certificate (media, movie_rating);
+  grl_media_set_studio (media, movie_studio);
+
+  /* FIXME: Translation */
+  grl_media_set_license (media, movie_copyright);
+
+  g_free (movie_id);
+  g_free (movie_author);
+  g_free (movie_date);
+  g_free (movie_description);
+  g_free (movie_duration);
+  g_free (movie_title);
+  g_free (movie_genre);
+  g_free (movie_thumbnail);
+  g_free (movie_url);
+  g_free (movie_rating);
+  g_free (movie_studio);
+  g_free (movie_copyright);
+
+  return media;
+}
+
+static gboolean
+send_movie_info (OperationData *op_data)
+{
+  GrlMedia *media;
+  gboolean last = FALSE;
+
+  if (op_data->cancelled) {
+    op_data->bs->callback (op_data->bs->source,
+                           op_data->bs->browse_id,
+                           NULL,
+                           0,
+                           op_data->bs->user_data,
+                           NULL);
+    last = TRUE;
+  } else {
+    GrlAppleTrailersSource *source =
+      GRL_APPLE_TRAILERS_SOURCE (op_data->bs->source);
+
+    media = build_media_from_movie (op_data->xml_entries,
+                                    source->priv->large_poster);
+    last =
+      !op_data->xml_entries->next  ||
+      op_data->bs->count == 1;
+
+    op_data->bs->callback (op_data->bs->source,
+                           op_data->bs->browse_id,
+                           media,
+                           last? 0: -1,
+                           op_data->bs->user_data,
+                           NULL);
+    op_data->xml_entries = op_data->xml_entries->next;
+    if (!last)
+      op_data->bs->count--;
+  }
+
+  if (last) {
+    xmlFreeDoc (op_data->xml_doc);
+    g_slice_free (OperationData, op_data);
+  }
+
+  return !last;
+}
+
+static void
+xml_parse_result (const gchar *str, OperationData *op_data)
+{
+  GError *error = NULL;
+  xmlNodePtr node;
+
+  if (op_data->cancelled || op_data->bs->count == 0) {
+    goto finalize;
+  }
+
+  op_data->xml_doc = xmlReadMemory (str, xmlStrlen ((xmlChar*) str), NULL, NULL,
+                                    XML_PARSE_RECOVER | XML_PARSE_NOBLANKS);
+  if (!op_data->xml_doc) {
+    error = g_error_new (GRL_CORE_ERROR,
+                         GRL_CORE_ERROR_BROWSE_FAILED,
+                         "Failed to parse response");
+    goto finalize;
+  }
+
+  node = xmlDocGetRootElement (op_data->xml_doc);
+  if (!node || !node->xmlChildrenNode) {
+    error = g_error_new (GRL_CORE_ERROR,
+                         GRL_CORE_ERROR_BROWSE_FAILED,
+                         "Empty response from Apple Trailers");
+    goto finalize;
+  }
+
+  node = node->xmlChildrenNode;
+
+  /* Skip elements */
+  while (node && op_data->bs->skip > 0) {
+    node = node->next;
+    op_data->bs->skip--;
+  }
+
+  if (!node) {
+    goto finalize;
+  } else {
+    op_data->xml_entries = node;
+    g_idle_add ((GSourceFunc) send_movie_info, op_data);
+  }
+
+  return;
+
+ finalize:
+  op_data->bs->callback (op_data->bs->source,
+                         op_data->bs->browse_id,
+                         NULL,
+                         0,
+                         op_data->bs->user_data,
+                         error);
+
+  if (op_data->xml_doc) {
+    xmlFreeDoc (op_data->xml_doc);
+  }
+
+  if (error) {
+    g_error_free (error);
+  }
+
+  g_slice_free (OperationData, op_data);
+}
+
+static void
+read_done_cb (GObject *source_object,
+              GAsyncResult *res,
+              gpointer user_data)
+{
+  GError *error = NULL;
+  GError *wc_error = NULL;
+  OperationData *op_data = (OperationData *) user_data;
+  gchar *content = NULL;
+
+  if (!grl_net_wc_request_finish (GRL_NET_WC (source_object),
+                              res,
+                              &content,
+                              NULL,
+                              &wc_error)) {
+    error = g_error_new (GRL_CORE_ERROR,
+                         GRL_CORE_ERROR_BROWSE_FAILED,
+                         "Failed to connect Apple Trailers: '%s'",
+                         wc_error->message);
+    op_data->bs->callback (op_data->bs->source,
+                           op_data->bs->browse_id,
+                           NULL,
+                           0,
+                           op_data->bs->user_data,
+                           error);
+    g_error_free (wc_error);
+    g_error_free (error);
+    g_slice_free (OperationData, op_data);
+
+    return;
+  }
+
+  xml_parse_result (content, op_data);
+}
+
+static void
+read_url_async (GrlAppleTrailersSource *source,
+                const gchar *url,
+                gpointer user_data)
+{
+  if (!source->priv->wc)
+    source->priv->wc = grl_net_wc_new ();
+
+  source->priv->cancellable = g_cancellable_new ();
+
+  GRL_DEBUG ("Opening '%s'", url);
+  grl_net_wc_request_async (source->priv->wc,
+                        url,
+                        source->priv->cancellable,
+                        read_done_cb,
+                        user_data);
+}
+
+/* ================== API Implementation ================ */
+
+static const GList *
+grl_apple_trailers_source_supported_keys (GrlMetadataSource *source)
+{
+  static GList *keys = NULL;
+  if (!keys) {
+    keys = grl_metadata_key_list_new (GRL_METADATA_KEY_AUTHOR,
+                                      GRL_METADATA_KEY_DATE,
+                                      GRL_METADATA_KEY_DESCRIPTION,
+                                      GRL_METADATA_KEY_DURATION,
+                                      GRL_METADATA_KEY_GENRE,
+                                      GRL_METADATA_KEY_ID,
+                                      GRL_METADATA_KEY_THUMBNAIL,
+                                      GRL_METADATA_KEY_TITLE,
+                                      GRL_METADATA_KEY_URL,
+                                      GRL_METADATA_KEY_CERTIFICATE,
+                                      GRL_METADATA_KEY_STUDIO,
+                                      GRL_METADATA_KEY_LICENSE,
+                                      NULL);
+  }
+  return keys;
+}
+
+static void
+grl_apple_trailers_source_browse (GrlMediaSource *source,
+                                  GrlMediaSourceBrowseSpec *bs)
+{
+  GrlAppleTrailersSource *at_source = GRL_APPLE_TRAILERS_SOURCE (source);
+  OperationData *op_data;
+
+  GRL_DEBUG ("grl_apple_trailers_source_browse");
+
+  op_data = g_slice_new0 (OperationData);
+  op_data->bs = bs;
+  grl_media_source_set_operation_data (source, bs->browse_id, op_data);
+
+  if (at_source->priv->hd) {
+    read_url_async (at_source, APPLE_TRAILERS_CURRENT_HD, op_data);
+  } else {
+    read_url_async (at_source, APPLE_TRAILERS_CURRENT_SD, op_data);
+  }
+}
+
+static void
+grl_apple_trailers_source_cancel (GrlMediaSource *source, guint operation_id)
+{
+  OperationData *op_data;
+  GrlAppleTrailersSourcePriv *priv;
+
+  GRL_DEBUG ("grl_apple_trailers_source_cancel");
+
+  priv = GRL_APPLE_TRAILERS_SOURCE_GET_PRIVATE (source);
+  if (priv->cancellable && G_IS_CANCELLABLE (priv->cancellable))
+    g_cancellable_cancel (priv->cancellable);
+  priv->cancellable = NULL;
+
+  if (priv->wc)
+    grl_net_wc_flush_delayed_requests (priv->wc);
+
+  op_data = (OperationData *) grl_media_source_get_operation_data (source, operation_id);
+
+  if (op_data) {
+    op_data->cancelled = TRUE;
+  }
+}
diff --git a/src/media/apple-trailers/grl-apple-trailers.h b/src/media/apple-trailers/grl-apple-trailers.h
new file mode 100644
index 0000000..74f8f9a
--- /dev/null
+++ b/src/media/apple-trailers/grl-apple-trailers.h
@@ -0,0 +1,78 @@
+/*
+ * Copyright (C) 2010 Igalia S.L.
+ *
+ * Contact: Iago Toral Quiroga <itoral igalia com>
+ *
+ * Authors: Juan A. Suarez Romero <jasuarez igalia 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_APPLE_TRAILERS_SOURCE_H_
+#define _GRL_APPLE_TRAILERS_SOURCE_H_
+
+#include <grilo.h>
+
+#define GRL_APPLE_TRAILERS_SOURCE_TYPE                 \
+  (grl_apple_trailers_source_get_type ())
+
+#define GRL_APPLE_TRAILERS_SOURCE(obj)                         \
+  (G_TYPE_CHECK_INSTANCE_CAST ((obj),                   \
+                               GRL_APPLE_TRAILERS_SOURCE_TYPE, \
+                               GrlAppleTrailersSource))
+
+#define GRL_IS_APPLE_TRAILERS_SOURCE(obj)                              \
+  (G_TYPE_CHECK_INSTANCE_TYPE ((obj),                           \
+                               GRL_APPLE_TRAILERS_SOURCE_TYPE))
+
+#define GRL_APPLE_TRAILERS_SOURCE_CLASS(klass)                 \
+  (G_TYPE_CHECK_CLASS_CAST((klass),                     \
+                           GRL_APPLE_TRAILERS_SOURCE_TYPE,     \
+                           GrlAppleTrailersSourceClass))
+
+#define GRL_IS_APPLE_TRAILERS_SOURCE_CLASS(klass)              \
+  (G_TYPE_CHECK_CLASS_TYPE((klass),                     \
+                           GRL_APPLE_TRAILERS_SOURCE_TYPE))
+
+#define GRL_APPLE_TRAILERS_SOURCE_GET_CLASS(obj)               \
+  (G_TYPE_INSTANCE_GET_CLASS ((obj),                    \
+                              GRL_APPLE_TRAILERS_SOURCE_TYPE,  \
+                              GrlAppleTrailersSourceClass))
+
+typedef struct _GrlAppleTrailersSource GrlAppleTrailersSource;
+typedef struct _GrlAppleTrailersSourcePriv GrlAppleTrailersSourcePriv;
+
+struct _GrlAppleTrailersSource {
+
+  GrlMediaSource parent;
+
+  /*< private >*/
+  GrlAppleTrailersSourcePriv *priv;
+
+};
+
+typedef struct _GrlAppleTrailersSourceClass GrlAppleTrailersSourceClass;
+
+struct _GrlAppleTrailersSourceClass {
+
+  GrlMediaSourceClass parent_class;
+
+};
+
+GType grl_apple_trailers_source_get_type (void);
+
+#endif /* _GRL_APPLE_TRAILERS_SOURCE_H_ */
diff --git a/src/media/apple-trailers/grl-apple-trailers.xml b/src/media/apple-trailers/grl-apple-trailers.xml
new file mode 100644
index 0000000..9fd929a
--- /dev/null
+++ b/src/media/apple-trailers/grl-apple-trailers.xml
@@ -0,0 +1,9 @@
+<plugin>
+  <info>
+    <name>Apple Movie Trailers</name>
+    <description>A plugin for browsing Apple Movie Trailers</description>
+    <author>Igalia S.L.</author>
+    <license>LGPL</license>
+    <site>http://www.igalia.com</site>
+  </info>
+</plugin>
diff --git a/src/media/bookmarks/Makefile.am b/src/media/bookmarks/Makefile.am
new file mode 100644
index 0000000..0a031f5
--- /dev/null
+++ b/src/media/bookmarks/Makefile.am
@@ -0,0 +1,34 @@
+#
+# Makefile.am
+#
+# Author: Iago Toral Quiroga <itoral igalia com>
+#
+# Copyright (C) 2010, 2011 Igalia S.L. All rights reserved.
+
+lib_LTLIBRARIES = libgrlbookmarks.la
+
+libgrlbookmarks_la_CFLAGS =	\
+	$(DEPS_CFLAGS)		\
+	$(SQLITE_CFLAGS)
+
+libgrlbookmarks_la_LIBADD =	\
+	$(DEPS_LIBS)		\
+	$(SQLITE_LIBS)
+
+libgrlbookmarks_la_LDFLAGS = \
+	-module		     \
+	-avoid-version
+
+libgrlbookmarks_la_SOURCES = grl-bookmarks.c grl-bookmarks.h
+
+libdir=$(GRL_PLUGINS_DIR)
+bookmarksxmldir		= $(GRL_PLUGINS_CONF_DIR)
+bookmarksxml_DATA	= $(BOOKMARKS_PLUGIN_ID).xml
+
+EXTRA_DIST = $(bookmarksxml_DATA)
+
+MAINTAINERCLEANFILES =	\
+	*.in		\
+	*~
+
+DISTCLEANFILES = $(MAINTAINERCLEANFILES)
diff --git a/src/media/bookmarks/grl-bookmarks.c b/src/media/bookmarks/grl-bookmarks.c
new file mode 100644
index 0000000..1c7de10
--- /dev/null
+++ b/src/media/bookmarks/grl-bookmarks.c
@@ -0,0 +1,888 @@
+/*
+ * Copyright (C) 2010 Igalia S.L.
+ *
+ * Contact: Iago Toral Quiroga <itoral igalia 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 <grilo.h>
+#include <sqlite3.h>
+#include <string.h>
+#include <stdlib.h>
+
+#include "grl-bookmarks.h"
+
+#define GRL_BOOKMARKS_GET_PRIVATE(object)			 \
+  (G_TYPE_INSTANCE_GET_PRIVATE((object),			 \
+                               GRL_BOOKMARKS_SOURCE_TYPE,        \
+                               GrlBookmarksPrivate))
+
+#define GRL_ROOT_TITLE "Bookmarks"
+
+/* --------- Logging  -------- */
+
+#define GRL_LOG_DOMAIN_DEFAULT bookmarks_log_domain
+GRL_LOG_DOMAIN_STATIC(bookmarks_log_domain);
+
+/* --- Database --- */
+
+#define GRL_SQL_DB        ".grl-bookmarks"
+
+#define GRL_SQL_CREATE_TABLE_BOOKMARKS			 \
+  "CREATE TABLE IF NOT EXISTS bookmarks ("		 \
+  "id     INTEGER PRIMARY KEY AUTOINCREMENT,"		 \
+  "parent INTEGER REFERENCES bookmarks (id),"		 \
+  "type   INTEGER,"					 \
+  "url    TEXT,"					 \
+  "title  TEXT,"					 \
+  "date   TEXT,"					 \
+  "mime   TEXT,"					 \
+  "desc   TEXT)"
+
+#define GRL_SQL_GET_BOOKMARKS_BY_PARENT			\
+  "SELECT b1.*, count(b2.parent <> '') "		\
+  "FROM bookmarks b1 LEFT OUTER JOIN bookmarks b2 "	\
+  "  ON b1.id = b2.parent "				\
+  "WHERE b1.parent='%s' "				\
+  "GROUP BY b1.id "					\
+  "LIMIT %u OFFSET %u"
+
+#define GRL_SQL_GET_BOOKMARK_BY_ID			\
+  "SELECT b1.*, count(b2.parent <> '') "		\
+  "FROM bookmarks b1 LEFT OUTER JOIN bookmarks b2 "	\
+  "  ON b1.id = b2.parent "				\
+  "WHERE b1.id='%s' "					\
+  "GROUP BY b1.id "					\
+  "LIMIT 1"
+
+#define GRL_SQL_STORE_BOOKMARK				  \
+  "INSERT INTO bookmarks "				  \
+  "(parent, type, url, title, date, mime, desc) "	  \
+  "VALUES (?, ?, ?, ?, ?, ?, ?)"
+
+#define GRL_SQL_REMOVE_BOOKMARK			\
+  "DELETE FROM bookmarks "			\
+  "WHERE id='%s' or parent='%s'"
+
+#define GRL_SQL_REMOVE_ORPHAN			\
+  "DELETE FROM bookmarks "			\
+  "WHERE id in ( "				\
+  "  SELECT DISTINCT id FROM bookmarks "	\
+  "  WHERE parent NOT IN ( "			\
+  "    SELECT DISTINCT id FROM bookmarks) "	\
+  "  and parent <> 0)"
+
+#define GRL_SQL_GET_BOOKMARKS_BY_TEXT				\
+  "SELECT b1.*, count(b2.parent <> '') "			\
+  "FROM bookmarks b1 LEFT OUTER JOIN bookmarks b2 "		\
+  "  ON b1.id = b2.parent "					\
+  "WHERE (b1.title LIKE '%%%s%%' OR b1.desc LIKE '%%%s%%') "	\
+  "  AND b1.type = 1 "                                          \
+  "GROUP BY b1.id "						\
+  "LIMIT %u OFFSET %u"
+
+#define GRL_SQL_GET_BOOKMARKS_BY_QUERY				\
+  "SELECT b1.*, count(b2.parent <> '') "			\
+  "FROM bookmarks b1 LEFT OUTER JOIN bookmarks b2 "		\
+  "  ON b1.id = b2.parent "					\
+  "WHERE %s "							\
+  "GROUP BY b1.id "						\
+  "LIMIT %u OFFSET %u"
+
+/* --- Plugin information --- */
+
+#define PLUGIN_ID   BOOKMARKS_PLUGIN_ID
+
+#define SOURCE_ID   "grl-bookmarks"
+#define SOURCE_NAME "Bookmarks"
+#define SOURCE_DESC "A source for organizing media bookmarks"
+
+#define AUTHOR      "Igalia S.L."
+#define LICENSE     "LGPL"
+#define SITE        "http://www.igalia.com";
+
+
+enum {
+  BOOKMARK_TYPE_CATEGORY = 0,
+  BOOKMARK_TYPE_STREAM,
+};
+
+enum {
+  BOOKMARK_ID = 0,
+  BOOKMARK_PARENT,
+  BOOKMARK_TYPE,
+  BOOKMARK_URL,
+  BOOKMARK_TITLE,
+  BOOKMARK_DATE,
+  BOOKMARK_MIME,
+  BOOKMARK_DESC,
+  BOOKMARK_CHILDCOUNT
+};
+
+struct _GrlBookmarksPrivate {
+  sqlite3 *db;
+  gboolean notify_changes;
+};
+
+typedef struct {
+  GrlMediaSource *source;
+  guint operation_id;
+  const gchar *media_id;
+  guint skip;
+  guint count;
+  GrlMediaSourceResultCb callback;
+  guint error_code;
+  gboolean is_query;
+  gpointer user_data;
+} OperationSpec;
+
+static GrlBookmarksSource *grl_bookmarks_source_new (void);
+
+static void grl_bookmarks_source_finalize (GObject *plugin);
+
+static const GList *grl_bookmarks_source_supported_keys (GrlMetadataSource *source);
+static GrlSupportedOps grl_bookmarks_source_supported_operations (GrlMetadataSource *metadata_source);
+
+static void grl_bookmarks_source_search (GrlMediaSource *source,
+					 GrlMediaSourceSearchSpec *ss);
+static void grl_bookmarks_source_query (GrlMediaSource *source,
+					GrlMediaSourceQuerySpec *qs);
+static void grl_bookmarks_source_browse (GrlMediaSource *source,
+                                        GrlMediaSourceBrowseSpec *bs);
+static void grl_bookmarks_source_metadata (GrlMediaSource *source,
+					   GrlMediaSourceMetadataSpec *ms);
+static void grl_bookmarks_source_store (GrlMediaSource *source,
+                                       GrlMediaSourceStoreSpec *ss);
+static void grl_bookmarks_source_remove (GrlMediaSource *source,
+					 GrlMediaSourceRemoveSpec *rs);
+
+static gboolean grl_bookmarks_source_notify_change_start (GrlMediaSource *source,
+                                                          GError **error);
+
+static gboolean grl_bookmarks_source_notify_change_stop (GrlMediaSource *source,
+                                                         GError **error);
+
+ /* =================== Bookmarks Plugin  =============== */
+
+ static gboolean
+ grl_bookmarks_plugin_init (GrlPluginRegistry *registry,
+                            const GrlPluginInfo *plugin,
+                            GList *configs)
+ {
+   GRL_LOG_DOMAIN_INIT (bookmarks_log_domain, "bookmarks");
+
+   GRL_DEBUG ("grl_bookmarks_plugin_init");
+
+   GrlBookmarksSource *source = grl_bookmarks_source_new ();
+   grl_plugin_registry_register_source (registry,
+                                        plugin,
+                                        GRL_MEDIA_PLUGIN (source),
+                                        NULL);
+   return TRUE;
+ }
+
+ GRL_PLUGIN_REGISTER (grl_bookmarks_plugin_init,
+                      NULL,
+                      PLUGIN_ID);
+
+ /* ================== Bookmarks GObject ================ */
+
+ static GrlBookmarksSource *
+ grl_bookmarks_source_new (void)
+ {
+   GRL_DEBUG ("grl_bookmarks_source_new");
+   return g_object_new (GRL_BOOKMARKS_SOURCE_TYPE,
+                        "source-id", SOURCE_ID,
+                        "source-name", SOURCE_NAME,
+                        "source-desc", SOURCE_DESC,
+                        NULL);
+ }
+
+ static void
+ grl_bookmarks_source_class_init (GrlBookmarksSourceClass * klass)
+ {
+   GObjectClass *gobject_class = G_OBJECT_CLASS (klass);
+   GrlMediaSourceClass *source_class = GRL_MEDIA_SOURCE_CLASS (klass);
+   GrlMetadataSourceClass *metadata_class = GRL_METADATA_SOURCE_CLASS (klass);
+
+   gobject_class->finalize = grl_bookmarks_source_finalize;
+
+   metadata_class->supported_operations =
+     grl_bookmarks_source_supported_operations;
+
+   source_class->browse = grl_bookmarks_source_browse;
+   source_class->search = grl_bookmarks_source_search;
+   source_class->query = grl_bookmarks_source_query;
+   source_class->store = grl_bookmarks_source_store;
+   source_class->remove = grl_bookmarks_source_remove;
+   source_class->metadata = grl_bookmarks_source_metadata;
+   source_class->notify_change_start = grl_bookmarks_source_notify_change_start;
+   source_class->notify_change_stop = grl_bookmarks_source_notify_change_stop;
+
+  metadata_class->supported_keys = grl_bookmarks_source_supported_keys;
+
+  g_type_class_add_private (klass, sizeof (GrlBookmarksPrivate));
+}
+
+static void
+grl_bookmarks_source_init (GrlBookmarksSource *source)
+{
+  gint r;
+  const gchar *home;
+  gchar *db_path;
+  gchar *sql_error = NULL;
+
+  source->priv = GRL_BOOKMARKS_GET_PRIVATE (source);
+
+  home = g_getenv ("HOME");
+  if (!home) {
+    GRL_WARNING ("$HOME not set, cannot open database");
+    return;
+  }
+
+  GRL_DEBUG ("Opening database connection...");
+  db_path = g_strconcat (home, G_DIR_SEPARATOR_S, GRL_SQL_DB, NULL);
+  r = sqlite3_open (db_path, &source->priv->db);
+  if (r) {
+    g_critical ("Failed to open database '%s': %s",
+		db_path, sqlite3_errmsg (source->priv->db));
+    sqlite3_close (source->priv->db);
+    return;
+  }
+  GRL_DEBUG ("  OK");
+
+  GRL_DEBUG ("Checking database tables...");
+  r = sqlite3_exec (source->priv->db, GRL_SQL_CREATE_TABLE_BOOKMARKS,
+		    NULL, NULL, &sql_error);
+
+  if (r) {
+    if (sql_error) {
+      GRL_WARNING ("Failed to create database tables: %s", sql_error);
+      sqlite3_free (sql_error);
+      sql_error = NULL;
+    } else {
+      GRL_WARNING ("Failed to create database tables.");
+    }
+    sqlite3_close (source->priv->db);
+    return;
+  }
+  GRL_DEBUG ("  OK");
+
+  g_free (db_path);
+}
+
+G_DEFINE_TYPE (GrlBookmarksSource, grl_bookmarks_source, GRL_TYPE_MEDIA_SOURCE);
+
+static void
+grl_bookmarks_source_finalize (GObject *object)
+{
+  GrlBookmarksSource *source;
+
+  GRL_DEBUG ("grl_bookmarks_source_finalize");
+
+  source = GRL_BOOKMARKS_SOURCE (object);
+
+  sqlite3_close (source->priv->db);
+
+  G_OBJECT_CLASS (grl_bookmarks_source_parent_class)->finalize (object);
+}
+
+/* ======================= Utilities ==================== */
+
+static gboolean
+mime_is_video (const gchar *mime)
+{
+  return mime && strstr (mime, "video") != NULL;
+}
+
+static gboolean
+mime_is_audio (const gchar *mime)
+{
+  return mime && strstr (mime, "audio") != NULL;
+}
+
+static GrlMedia *
+build_media_from_stmt (GrlMedia *content, sqlite3_stmt *sql_stmt)
+{
+  GrlMedia *media = NULL;
+  gchar *id;
+  gchar *title;
+  gchar *url;
+  gchar *desc;
+  gchar *date;
+  gchar *mime;
+  guint type;
+  guint childcount;
+
+  if (content) {
+    media = content;
+  }
+
+  id = (gchar *) sqlite3_column_text (sql_stmt, BOOKMARK_ID);
+  title = (gchar *) sqlite3_column_text (sql_stmt, BOOKMARK_TITLE);
+  url = (gchar *) sqlite3_column_text (sql_stmt, BOOKMARK_URL);
+  desc = (gchar *) sqlite3_column_text (sql_stmt, BOOKMARK_DESC);
+  date = (gchar *) sqlite3_column_text (sql_stmt, BOOKMARK_DATE);
+  mime = (gchar *) sqlite3_column_text (sql_stmt, BOOKMARK_MIME);
+  type = (guint) sqlite3_column_int (sql_stmt, BOOKMARK_TYPE);
+  childcount = (guint) sqlite3_column_int (sql_stmt, BOOKMARK_CHILDCOUNT);
+
+  if (!media) {
+    if (type == BOOKMARK_TYPE_CATEGORY) {
+      media = GRL_MEDIA (grl_media_box_new ());
+    } else if (mime_is_audio (mime)) {
+      media = GRL_MEDIA (grl_media_new ());
+    } else if (mime_is_video (mime)) {
+      media = GRL_MEDIA (grl_media_new ());
+    } else {
+      media = GRL_MEDIA (grl_media_new ());
+    }
+  }
+
+  grl_media_set_id (media, id);
+  grl_media_set_title (media, title);
+  if (url) {
+    grl_media_set_url (media, url);
+  }
+  if (desc) {
+    grl_media_set_description (media, desc);
+  }
+  if (date) {
+    grl_media_set_date (media, date);
+  }
+
+  if (type == BOOKMARK_TYPE_CATEGORY) {
+    grl_media_box_set_childcount (GRL_MEDIA_BOX (media), childcount);
+  }
+
+  return media;
+}
+
+static void
+bookmark_metadata (GrlMediaSourceMetadataSpec *ms)
+{
+  gint r;
+  sqlite3_stmt *sql_stmt = NULL;
+  sqlite3 *db;
+  GError *error = NULL;
+  gchar *sql;
+  const gchar *id;
+  
+  GRL_DEBUG ("bookmark_metadata");
+
+  db = GRL_BOOKMARKS_SOURCE (ms->source)->priv->db;
+
+  id = grl_media_get_id (ms->media);
+  if (!id) {
+    /* Root category: special case */
+    grl_media_set_title (ms->media, GRL_ROOT_TITLE);
+    ms->callback (ms->source, ms->media, ms->user_data, NULL);
+    return;
+  }
+
+  sql = g_strdup_printf (GRL_SQL_GET_BOOKMARK_BY_ID, id);
+  GRL_DEBUG ("%s", sql);
+  r = sqlite3_prepare_v2 (db, sql, strlen (sql), &sql_stmt, NULL);
+  g_free (sql);
+
+  if (r != SQLITE_OK) {
+    GRL_WARNING ("Failed to get bookmark: %s", sqlite3_errmsg (db));
+    error = g_error_new (GRL_CORE_ERROR,
+			 GRL_CORE_ERROR_METADATA_FAILED,
+			 "Failed to get bookmark metadata");
+    ms->callback (ms->source, ms->media, ms->user_data, error);
+    g_error_free (error);
+    return;
+  }
+
+  while ((r = sqlite3_step (sql_stmt)) == SQLITE_BUSY);
+
+  if (r == SQLITE_ROW) {
+    build_media_from_stmt (ms->media, sql_stmt);
+    ms->callback (ms->source, ms->media, ms->user_data, NULL);
+  } else {
+    GRL_WARNING ("Failed to get bookmark: %s", sqlite3_errmsg (db));
+    error = g_error_new (GRL_CORE_ERROR,
+			 GRL_CORE_ERROR_METADATA_FAILED,
+			 "Failed to get bookmark metadata");
+    ms->callback (ms->source, ms->media, ms->user_data, error);
+    g_error_free (error);
+  }
+
+  sqlite3_finalize (sql_stmt);
+}
+
+static void
+produce_bookmarks_from_sql (OperationSpec *os, const gchar *sql)
+{
+  gint r;
+  sqlite3_stmt *sql_stmt = NULL;
+  sqlite3 *db;
+  GrlMedia *media;
+  GError *error = NULL;
+  GList *medias = NULL;
+  guint count = 0;
+  GList *iter;
+
+  GRL_DEBUG ("produce_bookmarks_from_sql");
+
+  GRL_DEBUG ("%s", sql);
+  db = GRL_BOOKMARKS_SOURCE (os->source)->priv->db;
+  r = sqlite3_prepare_v2 (db, sql, strlen (sql), &sql_stmt, NULL);
+
+  if (r != SQLITE_OK) {
+    GRL_WARNING ("Failed to retrieve bookmarks: %s", sqlite3_errmsg (db));
+    error = g_error_new (GRL_CORE_ERROR,
+			 os->error_code,
+			 "Failed to retrieve bookmarks list");
+    os->callback (os->source, os->operation_id, NULL, 0, os->user_data, error);
+    g_error_free (error);
+    goto free_resources;
+  }
+
+  while ((r = sqlite3_step (sql_stmt)) == SQLITE_BUSY);
+
+  while (r == SQLITE_ROW) {
+    media = build_media_from_stmt (NULL, sql_stmt);
+    medias = g_list_prepend (medias, media);
+    count++;
+    r = sqlite3_step (sql_stmt);
+  }
+
+  if (r != SQLITE_DONE) {
+    GRL_WARNING ("Failed to retrieve bookmarks: %s", sqlite3_errmsg (db));
+    error = g_error_new (GRL_CORE_ERROR,
+			 os->error_code,
+			 "Failed to retrieve bookmarks list");
+    os->callback (os->source, os->operation_id, NULL, 0, os->user_data, error);
+    g_error_free (error);
+    goto free_resources;
+  }
+
+  if (count > 0) {
+    medias = g_list_reverse (medias);
+    iter = medias;
+    while (iter) {
+      media = GRL_MEDIA (iter->data);
+      os->callback (os->source,
+		    os->operation_id,
+		    media,
+		    --count,
+		    os->user_data,
+		    NULL);
+      iter = g_list_next (iter);
+    }
+    g_list_free (medias);
+  } else {
+    os->callback (os->source, os->operation_id, NULL, 0, os->user_data, NULL);
+  }
+
+ free_resources:
+  if (sql_stmt)
+    sqlite3_finalize (sql_stmt);
+}
+
+static void
+produce_bookmarks_by_query (OperationSpec *os, const gchar *query)
+{
+  gchar *sql;
+  GRL_DEBUG ("produce_bookmarks_by_query");
+  sql = g_strdup_printf (GRL_SQL_GET_BOOKMARKS_BY_QUERY,
+			 query, os->count, os->skip);    
+  produce_bookmarks_from_sql (os, sql);
+  g_free (sql);
+}
+
+static void
+produce_bookmarks_by_text (OperationSpec *os, const gchar *text)
+{
+  gchar *sql;
+  GRL_DEBUG ("produce_bookmarks_by_text");
+  sql = g_strdup_printf (GRL_SQL_GET_BOOKMARKS_BY_TEXT,
+			 text? text: "",
+                         text? text: "",
+                         os->count,
+                         os->skip);
+  produce_bookmarks_from_sql (os, sql);
+  g_free (sql);
+}
+
+static void
+produce_bookmarks_from_category (OperationSpec *os, const gchar *category_id)
+{
+  gchar *sql;
+  GRL_DEBUG ("produce_bookmarks_from_category");
+  sql = g_strdup_printf (GRL_SQL_GET_BOOKMARKS_BY_PARENT,
+			 category_id, os->count, os->skip);    
+  produce_bookmarks_from_sql (os, sql);
+  g_free (sql);
+}
+
+static void
+remove_bookmark (GrlBookmarksSource *bookmarks_source,
+                 const gchar *bookmark_id,
+                 GError **error)
+{
+  gint r;
+  gchar *sql_error;
+  gchar *sql;
+
+  GRL_DEBUG ("remove_bookmark");
+
+  sql = g_strdup_printf (GRL_SQL_REMOVE_BOOKMARK, bookmark_id, bookmark_id);
+  GRL_DEBUG ("%s", sql);
+  r = sqlite3_exec (bookmarks_source->priv->db, sql, NULL, NULL, &sql_error);
+  g_free (sql);
+
+  if (r != SQLITE_OK) {
+    GRL_WARNING ("Failed to remove bookmark '%s': %s", bookmark_id, sql_error);
+    *error = g_error_new (GRL_CORE_ERROR,
+			  GRL_CORE_ERROR_REMOVE_FAILED,
+			  "Failed to remove bookmark");
+    sqlite3_free (sql_error);
+  }
+
+  /* Remove orphan nodes from database */
+  GRL_DEBUG ("%s", GRL_SQL_REMOVE_ORPHAN);
+  r = sqlite3_exec (bookmarks_source->priv->db,
+                    GRL_SQL_REMOVE_ORPHAN,
+                    NULL, NULL, NULL);
+
+  if (bookmarks_source->priv->notify_changes) {
+    /* We can improve accuracy computing the parent container of removed
+       element */
+    grl_media_source_notify_change (GRL_MEDIA_SOURCE (bookmarks_source),
+                                    NULL,
+                                    GRL_CONTENT_REMOVED,
+                                    TRUE);
+  }
+}
+
+static void
+store_bookmark (GrlBookmarksSource *bookmarks_source,
+		GrlMediaBox *parent,
+		GrlMedia *bookmark,
+		GError **error)
+{
+  gint r;
+  sqlite3_stmt *sql_stmt = NULL;
+  const gchar *title;
+  const gchar *url;
+  const gchar *desc;
+  GTimeVal now;
+  const gchar *parent_id;
+  const gchar *mime;
+  gchar *date;
+  guint type;
+  gchar *id;
+
+  GRL_DEBUG ("store_bookmark");
+
+  title = grl_media_get_title (bookmark);
+  url = grl_media_get_url (bookmark);
+  desc = grl_media_get_description (bookmark);
+  mime = grl_media_get_mime (bookmark);
+  g_get_current_time (&now);
+  date = g_time_val_to_iso8601 (&now);
+
+  if (!parent) {
+    parent_id = "0";
+  } else {
+    parent_id = grl_media_get_id (GRL_MEDIA (parent));
+  }
+  if (!parent_id) {
+    parent_id = "0";
+  }
+
+  GRL_DEBUG ("%s", GRL_SQL_STORE_BOOKMARK);
+  r = sqlite3_prepare_v2 (bookmarks_source->priv->db,
+			  GRL_SQL_STORE_BOOKMARK,
+			  strlen (GRL_SQL_STORE_BOOKMARK),
+			  &sql_stmt, NULL);
+  if (r != SQLITE_OK) {
+    GRL_WARNING ("Failed to store bookmark '%s': %s", title,
+                 sqlite3_errmsg (bookmarks_source->priv->db));
+    *error = g_error_new (GRL_CORE_ERROR,
+			  GRL_CORE_ERROR_STORE_FAILED,
+			  "Failed to store bookmark '%s'", title);
+    return;
+  }
+
+  GRL_DEBUG ("URL: '%s'", url);
+
+  if (GRL_IS_MEDIA_BOX (bookmark)) {
+    type = BOOKMARK_TYPE_CATEGORY;
+  } else {
+    type = BOOKMARK_TYPE_STREAM;
+  }
+
+  sqlite3_bind_text (sql_stmt, BOOKMARK_PARENT, parent_id, -1, SQLITE_STATIC);
+  sqlite3_bind_int (sql_stmt, BOOKMARK_TYPE, type);
+  if (type == BOOKMARK_TYPE_STREAM) {
+    sqlite3_bind_text (sql_stmt, BOOKMARK_URL, url, -1, SQLITE_STATIC);
+  } else {
+    sqlite3_bind_null (sql_stmt, BOOKMARK_URL);
+  }
+  sqlite3_bind_text (sql_stmt, BOOKMARK_TITLE, title, -1, SQLITE_STATIC);
+  if (date) {
+    sqlite3_bind_text (sql_stmt, BOOKMARK_DATE, date, -1, SQLITE_STATIC);
+  } else {
+    sqlite3_bind_null (sql_stmt, BOOKMARK_DATE);
+  }
+  if (mime) {
+    sqlite3_bind_text (sql_stmt, BOOKMARK_MIME, mime, -1, SQLITE_STATIC);
+  } else {
+    sqlite3_bind_null (sql_stmt, BOOKMARK_MIME);
+  }
+  if (desc) {
+    sqlite3_bind_text (sql_stmt, BOOKMARK_DESC, desc, -1, SQLITE_STATIC);
+  } else {
+    sqlite3_bind_null (sql_stmt, BOOKMARK_DESC);
+  }
+
+  while ((r = sqlite3_step (sql_stmt)) == SQLITE_BUSY);
+
+  if (r != SQLITE_DONE) {
+    GRL_WARNING ("Failed to store bookmark '%s': %s", title,
+                 sqlite3_errmsg (bookmarks_source->priv->db));
+    *error = g_error_new (GRL_CORE_ERROR,
+			  GRL_CORE_ERROR_STORE_FAILED,
+			  "Failed to store bookmark '%s'", title);
+    sqlite3_finalize (sql_stmt);
+    return;
+  }
+
+  sqlite3_finalize (sql_stmt);
+
+  id = g_strdup_printf ("%llu",
+                        sqlite3_last_insert_rowid (bookmarks_source->priv->db));
+  grl_media_set_id (bookmark, id);
+  g_free (id);
+
+  if (bookmarks_source->priv->notify_changes) {
+    grl_media_source_notify_change (GRL_MEDIA_SOURCE (bookmarks_source),
+                                    GRL_MEDIA (parent),
+                                    GRL_CONTENT_ADDED,
+                                    FALSE);
+  }
+}
+
+/* ================== API Implementation ================ */
+
+static const GList *
+grl_bookmarks_source_supported_keys (GrlMetadataSource *source)
+{
+  static GList *keys = NULL;
+  if (!keys) {
+    keys = grl_metadata_key_list_new (GRL_METADATA_KEY_ID,
+                                      GRL_METADATA_KEY_TITLE,
+                                      GRL_METADATA_KEY_URL,
+                                      GRL_METADATA_KEY_CHILDCOUNT,
+                                      GRL_METADATA_KEY_DESCRIPTION,
+                                      GRL_METADATA_KEY_DATE,
+                                      NULL);
+  }
+  return keys;
+}
+
+static void
+grl_bookmarks_source_browse (GrlMediaSource *source,
+                            GrlMediaSourceBrowseSpec *bs)
+{
+  GRL_DEBUG ("grl_bookmarks_source_browse");
+
+  OperationSpec *os;
+  GrlBookmarksSource *bookmarks_source;
+  GError *error = NULL;
+
+  bookmarks_source = GRL_BOOKMARKS_SOURCE (source);
+  if (!bookmarks_source->priv->db) {
+    GRL_WARNING ("Can't execute operation: no database connection.");
+    error = g_error_new (GRL_CORE_ERROR,
+			 GRL_CORE_ERROR_BROWSE_FAILED,
+			 "No database connection");
+    bs->callback (bs->source, bs->browse_id, NULL, 0, bs->user_data, error);
+    g_error_free (error);
+  }
+
+  /* Configure browse operation */
+  os = g_slice_new0 (OperationSpec);
+  os->source = bs->source;
+  os->operation_id = bs->browse_id;
+  os->media_id = grl_media_get_id (bs->container);
+  os->count = bs->count;
+  os->skip = bs->skip;
+  os->callback = bs->callback;
+  os->user_data = bs->user_data;
+  os->error_code = GRL_CORE_ERROR_BROWSE_FAILED;
+
+  produce_bookmarks_from_category (os, os->media_id ? os->media_id : "0");
+  g_slice_free (OperationSpec, os);
+}
+
+static void
+grl_bookmarks_source_search (GrlMediaSource *source,
+			     GrlMediaSourceSearchSpec *ss)
+{
+  GRL_DEBUG ("grl_bookmarks_source_search");
+
+  GrlBookmarksSource *bookmarks_source;
+  OperationSpec *os;
+  GError *error = NULL;
+
+  bookmarks_source = GRL_BOOKMARKS_SOURCE (source);
+  if (!bookmarks_source->priv->db) {
+    GRL_WARNING ("Can't execute operation: no database connection.");
+    error = g_error_new (GRL_CORE_ERROR,
+			 GRL_CORE_ERROR_QUERY_FAILED,
+			 "No database connection");
+    ss->callback (ss->source, ss->search_id, NULL, 0, ss->user_data, error);
+    g_error_free (error);
+  }
+
+  os = g_slice_new0 (OperationSpec);
+  os->source = ss->source;
+  os->operation_id = ss->search_id;
+  os->count = ss->count;
+  os->skip = ss->skip;
+  os->callback = ss->callback;
+  os->user_data = ss->user_data;
+  os->error_code = GRL_CORE_ERROR_SEARCH_FAILED;
+  produce_bookmarks_by_text (os, ss->text);
+  g_slice_free (OperationSpec, os);
+}
+
+static void
+grl_bookmarks_source_query (GrlMediaSource *source,
+			    GrlMediaSourceQuerySpec *qs)
+{
+  GRL_DEBUG ("grl_bookmarks_source_query");
+
+  GrlBookmarksSource *bookmarks_source;
+  OperationSpec *os;
+  GError *error = NULL;
+
+  bookmarks_source = GRL_BOOKMARKS_SOURCE (source);
+  if (!bookmarks_source->priv->db) {
+    GRL_WARNING ("Can't execute operation: no database connection.");
+    error = g_error_new (GRL_CORE_ERROR,
+			 GRL_CORE_ERROR_QUERY_FAILED,
+			 "No database connection");
+    qs->callback (qs->source, qs->query_id, NULL, 0, qs->user_data, error);
+    g_error_free (error);
+  }
+
+  os = g_slice_new0 (OperationSpec);
+  os->source = qs->source;
+  os->operation_id = qs->query_id;
+  os->count = qs->count;
+  os->skip = qs->skip;
+  os->callback = qs->callback;
+  os->user_data = qs->user_data;
+  os->error_code = GRL_CORE_ERROR_SEARCH_FAILED;
+  produce_bookmarks_by_query (os, qs->query);
+  g_slice_free (OperationSpec, os);
+}
+
+static void
+grl_bookmarks_source_store (GrlMediaSource *source, GrlMediaSourceStoreSpec *ss)
+{
+  GRL_DEBUG ("grl_bookmarks_source_store");
+  /* FIXME: Try to guess bookmark mime somehow */
+  GError *error = NULL;
+  store_bookmark (GRL_BOOKMARKS_SOURCE (ss->source),
+		  ss->parent, ss->media, &error);
+  ss->callback (ss->source, ss->parent, ss->media, ss->user_data, error);
+  if (error) {
+    g_error_free (error);
+  }
+}
+
+static void grl_bookmarks_source_remove (GrlMediaSource *source,
+					 GrlMediaSourceRemoveSpec *rs)
+{
+  GRL_DEBUG ("grl_bookmarks_source_remove");
+  GError *error = NULL;
+  remove_bookmark (GRL_BOOKMARKS_SOURCE (rs->source),
+		   rs->media_id, &error);
+  rs->callback (rs->source, rs->media, rs->user_data, error);
+  if (error) {
+    g_error_free (error);
+  }
+}
+
+static void
+grl_bookmarks_source_metadata (GrlMediaSource *source,
+			       GrlMediaSourceMetadataSpec *ms)
+{
+  GRL_DEBUG ("grl_bookmarks_source_metadata");
+
+  GrlBookmarksSource *bookmarks_source;
+  GError *error = NULL;
+
+  bookmarks_source = GRL_BOOKMARKS_SOURCE (source);
+  if (!bookmarks_source->priv->db) {
+    GRL_WARNING ("Can't execute operation: no database connection.");
+    error = g_error_new (GRL_CORE_ERROR,
+			 GRL_CORE_ERROR_METADATA_FAILED,
+			 "No database connection");
+    ms->callback (ms->source, ms->media, ms->user_data, error);
+    g_error_free (error);
+  }
+
+  bookmark_metadata (ms);
+}
+
+static GrlSupportedOps
+grl_bookmarks_source_supported_operations (GrlMetadataSource *metadata_source)
+{
+  GrlSupportedOps caps;
+  GrlBookmarksSource *source;
+
+  source = GRL_BOOKMARKS_SOURCE (metadata_source);
+  caps = GRL_OP_BROWSE | GRL_OP_METADATA | GRL_OP_SEARCH | GRL_OP_QUERY |
+    GRL_OP_STORE | GRL_OP_STORE_PARENT | GRL_OP_REMOVE | GRL_OP_NOTIFY_CHANGE;
+
+  return caps;
+}
+
+static gboolean
+grl_bookmarks_source_notify_change_start (GrlMediaSource *source,
+                                          GError **error)
+{
+  GrlBookmarksSource *bookmarks_source = GRL_BOOKMARKS_SOURCE (source);
+
+  bookmarks_source->priv->notify_changes = TRUE;
+
+  return TRUE;
+}
+
+static gboolean
+grl_bookmarks_source_notify_change_stop (GrlMediaSource *source,
+                                         GError **error)
+{
+  GrlBookmarksSource *bookmarks_source = GRL_BOOKMARKS_SOURCE (source);
+
+  bookmarks_source->priv->notify_changes = FALSE;
+
+  return TRUE;
+}
diff --git a/src/media/bookmarks/grl-bookmarks.h b/src/media/bookmarks/grl-bookmarks.h
new file mode 100644
index 0000000..b5529b1
--- /dev/null
+++ b/src/media/bookmarks/grl-bookmarks.h
@@ -0,0 +1,75 @@
+/*
+ * Copyright (C) 2010 Igalia S.L.
+ *
+ * Contact: Iago Toral Quiroga <itoral igalia 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_BOOKMARKS_SOURCE_H_
+#define _GRL_BOOKMARKS_SOURCE_H_
+
+#include <grilo.h>
+
+#define GRL_BOOKMARKS_SOURCE_TYPE                \
+  (grl_bookmarks_source_get_type ())
+
+#define GRL_BOOKMARKS_SOURCE(obj)                                \
+  (G_TYPE_CHECK_INSTANCE_CAST ((obj),                           \
+                               GRL_BOOKMARKS_SOURCE_TYPE,        \
+                               GrlBookmarksSource))
+
+#define GRL_IS_BOOKMARKS_SOURCE(obj)                             \
+  (G_TYPE_CHECK_INSTANCE_TYPE ((obj),                           \
+                               GRL_BOOKMARKS_SOURCE_TYPE))
+
+#define GRL_BOOKMARKS_SOURCE_CLASS(klass)                \
+  (G_TYPE_CHECK_CLASS_CAST((klass),                     \
+                           GRL_BOOKMARKS_SOURCE_TYPE,    \
+                           GrlBookmarksSourceClass))
+
+#define GRL_IS_BOOKMARKS_SOURCE_CLASS(klass)             \
+  (G_TYPE_CHECK_CLASS_TYPE((klass),                     \
+                           GRL_BOOKMARKS_SOURCE_TYPE))
+
+#define GRL_BOOKMARKS_SOURCE_GET_CLASS(obj)              \
+  (G_TYPE_INSTANCE_GET_CLASS ((obj),                    \
+                              GRL_BOOKMARKS_SOURCE_TYPE, \
+                              GrlBookmarksSourceClass))
+
+typedef struct _GrlBookmarksPrivate GrlBookmarksPrivate;
+typedef struct _GrlBookmarksSource  GrlBookmarksSource;
+
+struct _GrlBookmarksSource {
+
+  GrlMediaSource parent;
+
+  /*< private >*/
+  GrlBookmarksPrivate *priv;
+};
+
+typedef struct _GrlBookmarksSourceClass GrlBookmarksSourceClass;
+
+struct _GrlBookmarksSourceClass {
+
+  GrlMediaSourceClass parent_class;
+
+};
+
+GType grl_bookmarks_source_get_type (void);
+
+#endif /* _GRL_BOOKMARKS_SOURCE_H_ */
diff --git a/src/media/bookmarks/grl-bookmarks.xml b/src/media/bookmarks/grl-bookmarks.xml
new file mode 100644
index 0000000..74ef3ee
--- /dev/null
+++ b/src/media/bookmarks/grl-bookmarks.xml
@@ -0,0 +1,9 @@
+<plugin>
+  <info>
+    <name>Bookmarks</name>
+    <description>A plugin for organizing media bookmarks</description>
+    <author>Igalia S.L.</author>
+    <license>LGPL</license>
+    <site>http://www.igalia.com</site>
+  </info>
+</plugin>
diff --git a/src/media/filesystem/Makefile.am b/src/media/filesystem/Makefile.am
new file mode 100644
index 0000000..c685b85
--- /dev/null
+++ b/src/media/filesystem/Makefile.am
@@ -0,0 +1,34 @@
+#
+# Makefile.am
+#
+# Author: Iago Toral Quiroga <itoral igalia com>
+#
+# Copyright (C) 2010, 2011 Igalia S.L. All rights reserved.
+
+lib_LTLIBRARIES		 = libgrlfilesystem.la
+
+libgrlfilesystem_la_CFLAGS =	\
+	$(DEPS_CFLAGS)		\
+	$(GIO_CFLAGS)
+
+libgrlfilesystem_la_LIBADD =	\
+	$(DEPS_LIBS)		\
+	$(GIO_LIBS)
+
+libgrlfilesystem_la_LDFLAGS = \
+	-module		      \
+	-avoid-version
+
+libgrlfilesystem_la_SOURCES = grl-filesystem.c grl-filesystem.h
+
+libdir=$(GRL_PLUGINS_DIR)
+filesystemxmldir	= $(GRL_PLUGINS_CONF_DIR)
+filesystemxml_DATA	= $(FILESYSTEM_PLUGIN_ID).xml
+
+EXTRA_DIST = $(filesystemxml_DATA)
+
+MAINTAINERCLEANFILES =	\
+	*.in		\
+	*~
+
+DISTCLEANFILES = $(MAINTAINERCLEANFILES)
diff --git a/src/media/filesystem/TODO b/src/media/filesystem/TODO
new file mode 100644
index 0000000..fb6c8ed
--- /dev/null
+++ b/src/media/filesystem/TODO
@@ -0,0 +1,2 @@
+- childcount for categories does not filter media-only files
+  - maybe this is acceptable (since this way it has a performance benefot)
diff --git a/src/media/filesystem/grl-filesystem.c b/src/media/filesystem/grl-filesystem.c
new file mode 100644
index 0000000..d4fe60c
--- /dev/null
+++ b/src/media/filesystem/grl-filesystem.c
@@ -0,0 +1,1343 @@
+/*
+ * Copyright (C) 2010, 2011 Igalia S.L.
+ * Copyright (C) 2011 Intel Corporation.
+ *
+ * Contact: Iago Toral Quiroga <itoral igalia 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 <grilo.h>
+#include <gio/gio.h>
+#include <string.h>
+#include <stdlib.h>
+
+#include "grl-filesystem.h"
+
+/* --------- Logging  -------- */
+
+#define GRL_LOG_DOMAIN_DEFAULT filesystem_log_domain
+GRL_LOG_DOMAIN_STATIC(filesystem_log_domain);
+
+/* -------- File info ------- */
+
+#define FILE_ATTRIBUTES                         \
+  G_FILE_ATTRIBUTE_STANDARD_DISPLAY_NAME ","    \
+  G_FILE_ATTRIBUTE_STANDARD_CONTENT_TYPE ","    \
+  G_FILE_ATTRIBUTE_STANDARD_TYPE ","            \
+  G_FILE_ATTRIBUTE_STANDARD_IS_HIDDEN ","       \
+  G_FILE_ATTRIBUTE_TIME_MODIFIED ","            \
+  G_FILE_ATTRIBUTE_THUMBNAIL_PATH ","           \
+  G_FILE_ATTRIBUTE_THUMBNAILING_FAILED
+
+#define FILE_ATTRIBUTES_FAST                    \
+  G_FILE_ATTRIBUTE_STANDARD_IS_HIDDEN
+
+/* ---- Emission chunks ----- */
+
+#define BROWSE_IDLE_CHUNK_SIZE 5
+
+/* --- Plugin information --- */
+
+#define PLUGIN_ID   FILESYSTEM_PLUGIN_ID
+
+#define SOURCE_ID   "grl-filesystem"
+#define SOURCE_NAME "Filesystem"
+#define SOURCE_DESC "A source for browsing the filesystem"
+
+#define AUTHOR      "Igalia S.L."
+#define LICENSE     "LGPL"
+#define SITE        "http://www.igalia.com";
+
+/* --- Grilo Filesystem Private --- */
+
+#define GRL_FILESYSTEM_SOURCE_GET_PRIVATE(object)         \
+  (G_TYPE_INSTANCE_GET_PRIVATE((object),                  \
+			     GRL_FILESYSTEM_SOURCE_TYPE,  \
+			     GrlFilesystemSourcePrivate))
+
+struct _GrlFilesystemSourcePrivate {
+  GList *chosen_paths;
+  guint max_search_depth;
+  /* a mapping operation_id -> GCancellable to cancel this operation */
+  GHashTable *cancellables;
+  /* Monitors for changes in directories */
+  GList *monitors;
+  GCancellable *cancellable_monitors;
+};
+
+/* --- Data types --- */
+
+typedef struct _RecursiveOperation RecursiveOperation;
+
+typedef gboolean (*RecursiveOperationCb) (GFileInfo *file_info,
+                                          RecursiveOperation *operation);
+
+typedef struct {
+  GrlMediaSourceBrowseSpec *spec;
+  GList *entries;
+  GList *current;
+  const gchar *path;
+  guint remaining;
+  GCancellable *cancellable;
+  guint id;
+}  BrowseIdleData;
+
+struct _RecursiveOperation {
+  RecursiveOperationCb on_cancel;
+  RecursiveOperationCb on_finish;
+  RecursiveOperationCb on_dir;
+  RecursiveOperationCb on_file;
+  gpointer on_dir_data;
+  gpointer on_file_data;
+  GCancellable *cancellable;
+  GQueue *directories;
+  guint max_depth;
+};
+
+typedef struct {
+  guint depth;
+  GFile *directory;
+} RecursiveEntry;
+
+
+static GrlFilesystemSource *grl_filesystem_source_new (void);
+
+static void grl_filesystem_source_finalize (GObject *object);
+
+gboolean grl_filesystem_plugin_init (GrlPluginRegistry *registry,
+                                     const GrlPluginInfo *plugin,
+                                     GList *configs);
+
+static const GList *grl_filesystem_source_supported_keys (GrlMetadataSource *source);
+
+static void grl_filesystem_source_metadata (GrlMediaSource *source,
+                                            GrlMediaSourceMetadataSpec *ms);
+
+static void grl_filesystem_source_browse (GrlMediaSource *source,
+                                          GrlMediaSourceBrowseSpec *bs);
+
+static void grl_filesystem_source_search (GrlMediaSource *source,
+                                          GrlMediaSourceSearchSpec *ss);
+
+
+static gboolean grl_filesystem_test_media_from_uri (GrlMediaSource *source,
+                                                    const gchar *uri);
+
+static void grl_filesystem_get_media_from_uri (GrlMediaSource *source,
+                                               GrlMediaSourceMediaFromUriSpec *mfus);
+
+static void grl_filesystem_source_cancel (GrlMediaSource *source,
+                                          guint operation_id);
+
+static gboolean grl_filesystem_source_notify_change_start (GrlMediaSource *source,
+                                                           GError **error);
+
+static gboolean grl_filesystem_source_notify_change_stop (GrlMediaSource *source,
+                                                          GError **error);
+
+/* =================== Filesystem Plugin  =============== */
+
+gboolean
+grl_filesystem_plugin_init (GrlPluginRegistry *registry,
+                            const GrlPluginInfo *plugin,
+                            GList *configs)
+{
+  GrlConfig *config;
+  GList *chosen_paths = NULL;
+  guint max_search_depth = GRILO_CONF_MAX_SEARCH_DEPTH_DEFAULT;
+
+  GRL_LOG_DOMAIN_INIT (filesystem_log_domain, "filesystem");
+
+  GRL_DEBUG ("filesystem_plugin_init");
+
+  GrlFilesystemSource *source = grl_filesystem_source_new ();
+
+  for (; configs; configs = g_list_next (configs)) {
+    gchar *path;
+    config = GRL_CONFIG (configs->data);
+    path = grl_config_get_string (config, GRILO_CONF_CHOSEN_PATH);
+    if (path) {
+      chosen_paths = g_list_append (chosen_paths, path);
+    }
+    if (grl_config_has_param (config, GRILO_CONF_MAX_SEARCH_DEPTH)) {
+      max_search_depth = (guint)grl_config_get_int (config, GRILO_CONF_MAX_SEARCH_DEPTH);
+    }
+  }
+  source->priv->chosen_paths = chosen_paths;
+  source->priv->max_search_depth = max_search_depth;
+
+  grl_plugin_registry_register_source (registry,
+                                       plugin,
+                                       GRL_MEDIA_PLUGIN (source),
+                                       NULL);
+
+  return TRUE;
+}
+
+GRL_PLUGIN_REGISTER (grl_filesystem_plugin_init,
+                     NULL,
+                     PLUGIN_ID);
+
+/* ================== Filesystem GObject ================ */
+
+
+G_DEFINE_TYPE (GrlFilesystemSource,
+               grl_filesystem_source,
+               GRL_TYPE_MEDIA_SOURCE);
+
+static GrlFilesystemSource *
+grl_filesystem_source_new (void)
+{
+  GRL_DEBUG ("grl_filesystem_source_new");
+  return g_object_new (GRL_FILESYSTEM_SOURCE_TYPE,
+		       "source-id", SOURCE_ID,
+		       "source-name", SOURCE_NAME,
+		       "source-desc", SOURCE_DESC,
+		       NULL);
+}
+
+static void
+grl_filesystem_source_class_init (GrlFilesystemSourceClass * klass)
+{
+  GrlMediaSourceClass *source_class = GRL_MEDIA_SOURCE_CLASS (klass);
+  GrlMetadataSourceClass *metadata_class = GRL_METADATA_SOURCE_CLASS (klass);
+  source_class->browse = grl_filesystem_source_browse;
+  source_class->search = grl_filesystem_source_search;
+  source_class->cancel = grl_filesystem_source_cancel;
+  source_class->notify_change_start = grl_filesystem_source_notify_change_start;
+  source_class->notify_change_stop = grl_filesystem_source_notify_change_stop;
+  source_class->metadata = grl_filesystem_source_metadata;
+  source_class->test_media_from_uri = grl_filesystem_test_media_from_uri;
+  source_class->media_from_uri = grl_filesystem_get_media_from_uri;
+  G_OBJECT_CLASS (source_class)->finalize = grl_filesystem_source_finalize;
+  metadata_class->supported_keys = grl_filesystem_source_supported_keys;
+  g_type_class_add_private (klass, sizeof (GrlFilesystemSourcePrivate));
+}
+
+static void
+grl_filesystem_source_init (GrlFilesystemSource *source)
+{
+  source->priv = GRL_FILESYSTEM_SOURCE_GET_PRIVATE (source);
+  source->priv->cancellables = g_hash_table_new (NULL, NULL);
+}
+
+static void
+grl_filesystem_source_finalize (GObject *object)
+{
+  GrlFilesystemSource *filesystem_source = GRL_FILESYSTEM_SOURCE (object);
+  g_list_foreach (filesystem_source->priv->chosen_paths, (GFunc) g_free, NULL);
+  g_list_free (filesystem_source->priv->chosen_paths);
+  g_hash_table_unref (filesystem_source->priv->cancellables);
+  G_OBJECT_CLASS (grl_filesystem_source_parent_class)->finalize (object);
+}
+
+/* ======================= Utilities ==================== */
+
+static void recursive_operation_next_entry (RecursiveOperation *operation);
+static void add_monitor (GrlFilesystemSource *fs_source, GFile *dir);
+static void cancel_monitors (GrlFilesystemSource *fs_source);
+
+static gboolean
+mime_is_video (const gchar *mime)
+{
+  return strstr (mime, "video") != NULL;
+}
+
+static gboolean
+mime_is_audio (const gchar *mime)
+{
+  return strstr (mime, "audio") != NULL;
+}
+
+static gboolean
+mime_is_image (const gchar *mime)
+{
+  return strstr (mime, "image") != NULL;
+}
+
+static gboolean
+mime_is_media (const gchar *mime)
+{
+  if (!mime)
+    return FALSE;
+  if (!strcmp (mime, "inode/directory"))
+    return TRUE;
+  if (mime_is_audio (mime))
+    return TRUE;
+  if (mime_is_video (mime))
+    return TRUE;
+  if (mime_is_image (mime))
+    return TRUE;
+  return FALSE;
+}
+
+static gboolean
+file_is_valid_content (const gchar *path, gboolean fast)
+{
+  const gchar *mime;
+  GError *error = NULL;
+  gboolean is_media;
+  GFile *file;
+  GFileInfo *info;
+  GFileType type;
+  const gchar *spec;
+
+  if (fast) {
+    spec = FILE_ATTRIBUTES_FAST;
+  } else {
+    spec = FILE_ATTRIBUTES;
+  }
+
+  file = g_file_new_for_path (path);
+  info = g_file_query_info (file, spec, 0, NULL, &error);
+  if (error) {
+    GRL_WARNING ("Failed to get attributes for file '%s': %s",
+                 path, error->message);
+    g_error_free (error);
+    g_object_unref (file);
+    return FALSE;
+  } else {
+    if (g_file_info_get_is_hidden (info)) {
+      is_media = FALSE;
+    } else {
+      if (fast) {
+	/* In fast mode we do not check mime-types,
+	   any non-hidden file is accepted */
+	is_media = TRUE;
+      } else {
+	type = g_file_info_get_file_type (info);
+	mime = g_file_info_get_content_type (info);
+        if (type == G_FILE_TYPE_DIRECTORY || mime_is_media (mime)) {
+          is_media = TRUE;
+        } else {
+          is_media = FALSE;
+        }
+      }
+    }
+    g_object_unref (info);
+    g_object_unref (file);
+    return is_media;
+  }
+}
+
+static void
+set_container_childcount (const gchar *path,
+			  GrlMedia *media,
+			  gboolean fast)
+{
+  GDir *dir;
+  GError *error = NULL;
+  gint count;
+  const gchar *entry_name;
+
+  /* Open directory */
+  GRL_DEBUG ("Opening directory '%s' for childcount", path);
+  dir = g_dir_open (path, 0, &error);
+  if (error) {
+    GRL_WARNING ("Failed to open directory '%s': %s", path, error->message);
+    g_error_free (error);
+    return;
+  }
+
+  /* Count valid entries */
+  count = 0;
+  while ((entry_name = g_dir_read_name (dir)) != NULL) {
+    gchar *entry_path;
+    if (strcmp (path, G_DIR_SEPARATOR_S)) {
+      entry_path = g_strconcat (path, G_DIR_SEPARATOR_S, entry_name, NULL);
+    } else {
+      entry_path = g_strconcat (path, entry_name, NULL);
+    }
+    if (file_is_valid_content (entry_path, fast)) {
+      if (fast) {
+        /* in fast mode we don't compute  mime-types because it is slow,
+           so we can only check if the directory is totally empty (no subdirs,
+           and no files), otherwise we just say we do not know the actual
+           childcount */
+        count = GRL_METADATA_KEY_CHILDCOUNT_UNKNOWN;
+        break;
+      }
+      count++;
+    }
+    g_free (entry_path);
+  }
+
+  g_dir_close (dir);
+
+  grl_media_box_set_childcount (GRL_MEDIA_BOX (media), count);
+}
+
+static GrlMedia *
+create_content (GrlMedia *content,
+                const gchar *path,
+                gboolean only_fast,
+		gboolean root_dir)
+{
+  GrlMedia *media = NULL;
+  gchar *str;
+  const gchar *mime;
+  GError *error = NULL;
+
+  GFile *file = g_file_new_for_path (path);
+  GFileInfo *info = g_file_query_info (file,
+				       FILE_ATTRIBUTES,
+				       0,
+				       NULL,
+				       &error);
+
+  /* Update mode */
+  if (content) {
+    media = content;
+  }
+
+  if (error) {
+    GRL_WARNING ("Failed to get info for file '%s': %s", path,
+                 error->message);
+    if (!media) {
+      media = grl_media_new ();
+    }
+
+    /* Title */
+    str = g_strrstr (path, G_DIR_SEPARATOR_S);
+    if (!str) {
+      str = (gchar *) path;
+    }
+    grl_media_set_title (media, str);
+    g_error_free (error);
+  } else {
+    mime = g_file_info_get_content_type (info);
+
+    if (!media) {
+      if (g_file_info_get_file_type (info) == G_FILE_TYPE_DIRECTORY) {
+	media = GRL_MEDIA (grl_media_box_new ());
+      } else {
+	if (mime_is_video (mime)) {
+	  media = grl_media_video_new ();
+	} else if (mime_is_audio (mime)) {
+	  media = grl_media_audio_new ();
+	} else if (mime_is_image (mime)) {
+	  media = grl_media_image_new ();
+	} else {
+	  media = grl_media_new ();
+	}
+      }
+    }
+
+    if (!GRL_IS_MEDIA_BOX (media)) {
+      grl_media_set_mime (media, mime);
+    }
+
+    /* Title */
+    str = (gchar *) g_file_info_get_display_name (info);
+    grl_media_set_title (media, str);
+
+    /* Date */
+    GTimeVal time;
+    gchar *time_str;
+    g_file_info_get_modification_time (info, &time);
+    time_str = g_time_val_to_iso8601 (&time);
+    grl_media_set_date (media, time_str);
+    g_free (time_str);
+
+    /* Thumbnail */
+    gboolean thumb_failed =
+      g_file_info_get_attribute_boolean (info,
+                                         G_FILE_ATTRIBUTE_THUMBNAILING_FAILED);
+    if (!thumb_failed) {
+      const gchar *thumb =
+        g_file_info_get_attribute_byte_string (info,
+                                               G_FILE_ATTRIBUTE_THUMBNAIL_PATH);
+      if (thumb) {
+	gchar *thumb_uri = g_filename_to_uri (thumb, NULL, NULL);
+	if (thumb_uri) {
+	  grl_media_set_thumbnail (media, thumb_uri);
+	  g_free (thumb_uri);
+	}
+      }
+    }
+
+    g_object_unref (info);
+  }
+
+  grl_media_set_id (media,  root_dir ? NULL : path);
+
+  /* URL */
+  str = g_strconcat ("file://", path, NULL);
+  grl_media_set_url (media, str);
+  g_free (str);
+
+  /* Childcount */
+  if (GRL_IS_MEDIA_BOX (media)) {
+    set_container_childcount (path, media, only_fast);
+  }
+
+  g_object_unref (file);
+
+  return media;
+}
+
+static gboolean
+browse_emit_idle (gpointer user_data)
+{
+  BrowseIdleData *idle_data;
+  guint count;
+  GrlFilesystemSource *fs_source;
+
+  GRL_DEBUG ("browse_emit_idle");
+
+  idle_data = (BrowseIdleData *) user_data;
+  fs_source = GRL_FILESYSTEM_SOURCE (idle_data->spec->source);
+
+  if (g_cancellable_is_cancelled (idle_data->cancellable)) {
+    GRL_DEBUG ("Browse operation %d (\"%s\") has been cancelled",
+               idle_data->id, idle_data->path);
+    idle_data->spec->callback(idle_data->spec->source,
+                              idle_data->id, NULL, 0,
+                              idle_data->spec->user_data, NULL);
+    goto finish;
+  }
+
+  count = 0;
+  do {
+    gchar *entry_path;
+    GrlMedia *content;
+
+    entry_path = (gchar *) idle_data->current->data;
+    content = create_content (NULL,
+			      entry_path,
+			      idle_data->spec->flags & GRL_RESOLVE_FAST_ONLY,
+			      FALSE);
+    g_free (idle_data->current->data);
+
+    idle_data->spec->callback (idle_data->spec->source,
+			       idle_data->spec->browse_id,
+			       content,
+			       idle_data->remaining--,
+			       idle_data->spec->user_data,
+			       NULL);
+
+    idle_data->current = g_list_next (idle_data->current);
+    count++;
+  } while (count < BROWSE_IDLE_CHUNK_SIZE && idle_data->current);
+
+  if (!idle_data->current)
+    goto finish;
+
+  return TRUE;
+
+finish:
+    g_list_free (idle_data->entries);
+    g_hash_table_remove (fs_source->priv->cancellables,
+                         GUINT_TO_POINTER (idle_data->id));
+    g_object_unref (idle_data->cancellable);
+    g_slice_free (BrowseIdleData, idle_data);
+    return FALSE;
+}
+
+static void
+produce_from_path (GrlMediaSourceBrowseSpec *bs, const gchar *path)
+{
+  GDir *dir;
+  GError *error = NULL;
+  const gchar *entry;
+  guint skip, count;
+  GList *entries = NULL;
+  GList *iter;
+
+  /* Open directory */
+  GRL_DEBUG ("Opening directory '%s'", path);
+  dir = g_dir_open (path, 0, &error);
+  if (error) {
+    GRL_WARNING ("Failed to open directory '%s': %s", path, error->message);
+    bs->callback (bs->source, bs->browse_id, NULL, 0, bs->user_data, error);
+    g_error_free (error);
+    return;
+  }
+
+  /* Filter out media and directories */
+  while ((entry = g_dir_read_name (dir)) != NULL) {
+    gchar *file;
+    if (strcmp (path, G_DIR_SEPARATOR_S)) {
+      file = g_strconcat (path, G_DIR_SEPARATOR_S, entry, NULL);
+    } else {
+      file = g_strconcat (path, entry, NULL);
+    }
+    if (file_is_valid_content (file, FALSE)) {
+      entries = g_list_prepend (entries, file);
+    }
+  }
+
+  /* Apply skip and count */
+  skip = bs->skip;
+  count = bs->count;
+  iter = entries;
+  while (iter) {
+    gboolean remove;
+    GList *tmp;
+    if (skip > 0)  {
+      skip--;
+      remove = TRUE;
+    } else if (count > 0) {
+      count--;
+      remove = FALSE;
+    } else {
+      remove = TRUE;
+    }
+    if (remove) {
+      tmp = iter;
+      iter = g_list_next (iter);
+      g_free (tmp->data);
+      entries = g_list_delete_link (entries, tmp);
+    } else {
+      iter = g_list_next (iter);
+    }
+  }
+
+  /* Emit results */
+  if (entries) {
+    /* Use the idle loop to avoid blocking for too long */
+    BrowseIdleData *idle_data = g_slice_new (BrowseIdleData);
+    idle_data->spec = bs;
+    idle_data->remaining = bs->count - count - 1;
+    idle_data->path = path;
+    idle_data->entries = entries;
+    idle_data->current = entries;
+    idle_data->cancellable = g_cancellable_new ();
+    idle_data->id = bs->browse_id;
+    g_hash_table_insert (GRL_FILESYSTEM_SOURCE (bs->source)->priv->cancellables,
+                         GUINT_TO_POINTER (bs->browse_id),
+                         idle_data->cancellable);
+
+    g_idle_add (browse_emit_idle, idle_data);
+  } else {
+    /* No results */
+    bs->callback (bs->source,
+		  bs->browse_id,
+		  NULL,
+		  0,
+		  bs->user_data,
+		  NULL);
+  }
+
+  g_dir_close (dir);
+}
+
+static RecursiveEntry *
+recursive_entry_new (guint depth, GFile *directory)
+{
+  RecursiveEntry *entry;
+
+  entry = g_slice_new(RecursiveEntry);
+  entry->depth = depth;
+  entry->directory = g_object_ref (directory);
+
+  return entry;
+}
+
+static void
+recursive_entry_free (RecursiveEntry *entry)
+{
+  g_object_unref (entry->directory);
+  g_slice_free (RecursiveEntry, entry);
+}
+
+static RecursiveOperation *
+recursive_operation_new ()
+{
+  RecursiveOperation *operation;
+
+  operation = g_slice_new0 (RecursiveOperation);
+  operation->directories = g_queue_new ();
+  operation->cancellable = g_cancellable_new ();
+
+  return operation;
+}
+
+static void
+recursive_operation_free (RecursiveOperation *operation)
+{
+  g_queue_foreach (operation->directories, (GFunc) recursive_entry_free, NULL);
+  g_queue_free (operation->directories);
+  g_object_unref (operation->cancellable);
+  g_slice_free (RecursiveOperation, operation);
+}
+
+static void
+recursive_operation_got_file (GFileEnumerator *enumerator, GAsyncResult *res, RecursiveOperation *operation)
+{
+  GList *files;
+  GError *error = NULL;
+  gboolean continue_operation = TRUE;
+
+  GRL_DEBUG (__func__);
+
+  files = g_file_enumerator_next_files_finish (enumerator, res, &error);
+  if (error) {
+    if (!g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED))
+      GRL_WARNING ("Got error: %s", error->message);
+    g_error_free (error);
+    goto finished;
+  }
+
+  if (files) {
+    GFileInfo *file_info;
+    RecursiveEntry *entry;
+
+    /* we assume there is only one GFileInfo in the list since that's what we ask
+     * for when calling g_file_enumerator_next_files_async() */
+    file_info = (GFileInfo *)files->data;
+    g_list_free (files);
+    /* Get the entry we are running now */
+    entry = g_queue_peek_head (operation->directories);
+    switch (g_file_info_get_file_type (file_info)) {
+    case G_FILE_TYPE_SYMBOLIC_LINK:
+      /* we're too afraid of infinite recursion to touch this for now */
+      break;
+    case G_FILE_TYPE_DIRECTORY:
+        {
+          if (entry->depth < operation->max_depth) {
+            GFile *subdir;
+            RecursiveEntry *subentry;
+
+            if (operation->on_dir) {
+              continue_operation = operation->on_dir(file_info, operation);
+            }
+
+            if (continue_operation) {
+              subdir = g_file_get_child (entry->directory,
+                                         g_file_info_get_name (file_info));
+              subentry = recursive_entry_new (entry->depth + 1, subdir);
+              g_queue_push_tail (operation->directories, subentry);
+              g_object_unref (subdir);
+            } else {
+              g_object_unref (file_info);
+              goto finished;
+            }
+          }
+        }
+      break;
+    case G_FILE_TYPE_REGULAR:
+      if (operation->on_file) {
+        continue_operation = operation->on_file(file_info, operation);
+        if (!continue_operation) {
+          g_object_unref (file_info);
+          goto finished;
+        }
+      }
+      break;
+    default:
+      /* this file is a weirdo, we ignore it */
+      break;
+    }
+    g_object_unref (file_info);
+  } else {    /* end of enumerator */
+    goto finished;
+  }
+
+  g_file_enumerator_next_files_async (enumerator, 1, G_PRIORITY_DEFAULT,
+                                      operation->cancellable,
+                                      (GAsyncReadyCallback)recursive_operation_got_file,
+                                      operation);
+
+  return;
+
+finished:
+  /* we're done with this dir/enumerator, let's treat the next one */
+  g_object_unref (enumerator);
+  recursive_entry_free (g_queue_pop_head (operation->directories));
+  if (continue_operation) {
+    recursive_operation_next_entry (operation);
+  } else {
+    recursive_operation_free (operation);
+  }
+}
+
+static void
+recursive_operation_got_entry (GFile *directory, GAsyncResult *res, RecursiveOperation *operation)
+{
+  GError *error = NULL;
+  GFileEnumerator *enumerator;
+
+  GRL_DEBUG (__func__);
+
+  enumerator = g_file_enumerate_children_finish (directory, res, &error);
+  if (error) {
+    GRL_WARNING ("Got error: %s", error->message);
+    g_error_free (error);
+    g_object_unref (enumerator);
+    /* we couldn't get the children of this directory, but we probably have
+     * other directories to try */
+    recursive_entry_free (g_queue_pop_head (operation->directories));
+    recursive_operation_next_entry (operation);
+    return;
+  }
+
+  g_file_enumerator_next_files_async (enumerator, 1, G_PRIORITY_DEFAULT,
+                                      operation->cancellable,
+                                      (GAsyncReadyCallback)recursive_operation_got_file,
+                                      operation);
+}
+
+static void
+recursive_operation_next_entry (RecursiveOperation *operation)
+{
+  RecursiveEntry *entry;
+
+  GRL_DEBUG (__func__);
+
+  if (g_cancellable_is_cancelled (operation->cancellable)) {
+    /* We've been cancelled! */
+    GRL_DEBUG ("Operation has been cancelled");
+    if (operation->on_cancel) {
+      operation->on_cancel(NULL, operation);
+    }
+    goto finished;
+  }
+
+  entry = g_queue_peek_head (operation->directories);
+  if (!entry) { /* We've crawled everything, before reaching count */
+    if (operation->on_finish) {
+      operation->on_finish (NULL, operation);
+    }
+    goto finished;
+  }
+
+  g_file_enumerate_children_async (entry->directory, G_FILE_ATTRIBUTE_STANDARD_TYPE ","
+                                   G_FILE_ATTRIBUTE_STANDARD_NAME ","
+                                   G_FILE_ATTRIBUTE_STANDARD_DISPLAY_NAME,
+                                   G_FILE_QUERY_INFO_NOFOLLOW_SYMLINKS,
+                                   G_PRIORITY_DEFAULT,
+                                   operation->cancellable,
+                                   (GAsyncReadyCallback)recursive_operation_got_entry,
+                                   operation);
+
+  return;
+
+finished:
+  recursive_operation_free (operation);
+}
+
+static void
+recursive_operation_initialize (RecursiveOperation *operation, GrlFilesystemSource *source)
+{
+  GList *chosen_paths, *path;
+
+  chosen_paths = source->priv->chosen_paths;
+  if (chosen_paths) {
+    for (path = chosen_paths; path; path = g_list_next (path)) {
+      GFile *directory = g_file_new_for_path (path->data);
+      g_queue_push_tail (operation->directories,
+                         recursive_entry_new (0, directory));
+      g_object_unref (directory);
+    }
+  } else {
+    const gchar *home;
+    GFile *directory;
+
+    home = g_getenv ("HOME");
+    directory = g_file_new_for_path (home);
+    g_queue_push_tail (operation->directories,
+                       recursive_entry_new (0, directory));
+    g_object_unref (directory);
+  }
+}
+
+static gboolean
+cancel_cb (GFileInfo *file_info, RecursiveOperation *operation)
+{
+  GrlFilesystemSource *fs_source;
+
+  if (operation->on_file_data) {
+    GrlMediaSourceSearchSpec *ss =
+      (GrlMediaSourceSearchSpec *) operation->on_file_data;
+    fs_source = GRL_FILESYSTEM_SOURCE (ss->source);
+    g_hash_table_remove (fs_source->priv->cancellables,
+                         GUINT_TO_POINTER (ss->search_id));
+    ss->callback (ss->source, ss->search_id, NULL, 0, ss->user_data, NULL);
+  }
+
+  if (operation->on_dir_data) {
+    /* Remove all monitors */
+    fs_source = GRL_FILESYSTEM_SOURCE (operation->on_dir_data);
+    cancel_monitors (fs_source);
+  }
+  return FALSE;
+}
+
+static gboolean
+finish_cb (GFileInfo *file_info, RecursiveOperation *operation)
+{
+  if (operation->on_file_data) {
+    GrlMediaSourceSearchSpec *ss =
+      (GrlMediaSourceSearchSpec *) operation->on_file_data;
+    g_hash_table_remove (GRL_FILESYSTEM_SOURCE (ss->source)->priv->cancellables,
+                         GUINT_TO_POINTER (ss->search_id));
+    ss->callback (ss->source, ss->search_id, NULL, 0, ss->user_data, NULL);
+  }
+
+  if (operation->on_dir_data) {
+    GRL_FILESYSTEM_SOURCE (operation->on_dir_data)->priv->cancellable_monitors = NULL;
+  }
+
+  return FALSE;
+}
+
+/* return TRUE if more files need to be returned, FALSE if we sent the count */
+static gboolean
+file_cb (GFileInfo *file_info, RecursiveOperation *operation)
+{
+  gchar *needle = NULL;
+  gchar *haystack = NULL;
+  gchar *normalized_needle = NULL;
+  gchar *normalized_haystack = NULL;
+  GrlMediaSourceSearchSpec *ss = operation->on_file_data;
+  gint remaining = -1;
+
+  GRL_DEBUG (__func__);
+
+  if (ss == NULL)
+    return FALSE;
+
+  if (ss->text) {
+    haystack = g_utf8_casefold (g_file_info_get_display_name (file_info), -1);
+    normalized_haystack = g_utf8_normalize (haystack, -1, G_NORMALIZE_ALL);
+
+    needle = g_utf8_casefold (ss->text, -1);
+    normalized_needle = g_utf8_normalize (needle, -1, G_NORMALIZE_ALL);
+  }
+
+  if (!ss->text ||
+      strstr (normalized_haystack, normalized_needle)) {
+    GrlMedia *media = NULL;
+    RecursiveEntry *entry;
+    GFile *file;
+    gchar *path;
+
+    entry = g_queue_peek_head (operation->directories);
+    file = g_file_get_child (entry->directory,
+                             g_file_info_get_name (file_info));
+    path = g_file_get_path (file);
+
+    /* FIXME: both file_is_valid_content() and create_content() are likely to block */
+    if (file_is_valid_content (path, FALSE)) {
+      if (ss->skip) {
+        ss->skip--;
+      } else {
+        media = create_content (NULL, path, ss->flags & GRL_RESOLVE_FAST_ONLY, FALSE);
+      }
+    }
+
+    g_free (path);
+    g_object_unref (file);
+
+    if (media) {
+      ss->count--;
+      if (ss->count == 0) {
+        remaining = 0;
+      }
+      ss->callback (ss->source, ss->search_id, media, remaining, ss->user_data, NULL);
+    }
+  }
+
+  g_free (haystack);
+  g_free (normalized_haystack);
+  g_free (needle);
+  g_free (normalized_needle);
+  return remaining == -1;
+}
+
+static void
+notify_parent_change (GrlMediaSource *source, GFile *child, GrlMediaSourceChangeType change)
+{
+  GFile *parent;
+  GrlMedia *media;
+  gchar *parent_path;
+
+  parent = g_file_get_parent (child);
+  if (parent) {
+    parent_path = g_file_get_path (parent);
+  } else {
+    parent_path = g_strdup ("/");
+  }
+
+  media = create_content (NULL, parent_path, GRL_RESOLVE_FAST_ONLY, parent == NULL);
+  grl_media_source_notify_change (source, media, change, FALSE);
+  g_object_unref (media);
+
+  if (parent) {
+    g_object_unref (parent);
+  }
+  g_free (parent_path);
+}
+
+static void
+directory_changed (GFileMonitor *monitor,
+                   GFile *file,
+                   GFile *other_file,
+                   GFileMonitorEvent event,
+                   gpointer data)
+{
+  GrlMediaSource *source = GRL_MEDIA_SOURCE (data);
+  gchar *file_path, *other_file_path;
+  gchar *file_parent_path = NULL;
+  gchar *other_file_parent_path = NULL;
+  GFile *file_parent, *other_file_parent;
+  GFileInfo *file_info;
+
+  if (event == G_FILE_MONITOR_EVENT_CHANGES_DONE_HINT ||
+      event == G_FILE_MONITOR_EVENT_CREATED) {
+    file_path = g_file_get_path (file);
+    if (file_is_valid_content (file_path, TRUE)) {
+      notify_parent_change (source,
+                            file,
+                            (event == G_FILE_MONITOR_EVENT_CREATED)? GRL_CONTENT_ADDED: GRL_CONTENT_CHANGED);
+      if (event == G_FILE_MONITOR_EVENT_CREATED) {
+        file_info = g_file_query_info (file,
+                                       G_FILE_ATTRIBUTE_STANDARD_TYPE,
+                                       G_FILE_QUERY_INFO_NOFOLLOW_SYMLINKS,
+                                       NULL,
+                                       NULL);
+        if (file_info) {
+          if (g_file_info_get_file_type (file_info) == G_FILE_TYPE_DIRECTORY) {
+            add_monitor (GRL_FILESYSTEM_SOURCE (source), file);
+          }
+          g_object_unref (file_info);
+        }
+      }
+    }
+    g_free (file_path);
+  } else if (event == G_FILE_MONITOR_EVENT_DELETED) {
+    notify_parent_change (source, file, GRL_CONTENT_REMOVED);
+  } else if (event == G_FILE_MONITOR_EVENT_MOVED) {
+    other_file_path = g_file_get_path (other_file);
+    if (file_is_valid_content (other_file_path, TRUE)) {
+      file_parent = g_file_get_parent (file);
+      if (file_parent) {
+        file_parent_path = g_file_get_path (file_parent);
+        g_object_unref (file_parent);
+      } else {
+        file_parent_path = NULL;
+      }
+      other_file_parent = g_file_get_parent (other_file);
+      if (other_file_parent) {
+        other_file_parent_path = g_file_get_path (other_file_parent);
+        g_object_unref (other_file_parent);
+      } else {
+        other_file_parent_path = NULL;
+      }
+
+      if (g_strcmp0 (file_parent_path, other_file_parent_path) == 0) {
+        notify_parent_change (source, file, GRL_CONTENT_CHANGED);
+      } else {
+        notify_parent_change (source, file, GRL_CONTENT_REMOVED);
+        notify_parent_change (source, other_file, GRL_CONTENT_ADDED);
+      }
+    }
+    g_free (file_parent_path);
+    g_free (other_file_parent_path);
+  }
+}
+
+static void
+cancel_monitors (GrlFilesystemSource *fs_source)
+{
+  g_list_foreach (fs_source->priv->monitors,
+                  (GFunc) g_file_monitor_cancel,
+                  NULL);
+  g_list_foreach (fs_source->priv->monitors,
+                  (GFunc) g_object_unref,
+                  NULL);
+  g_list_free (fs_source->priv->monitors);
+  fs_source->priv->monitors = NULL;
+}
+
+static void
+add_monitor (GrlFilesystemSource *fs_source, GFile *dir)
+{
+  GFileMonitor *monitor;
+
+  monitor = g_file_monitor_directory (dir, G_FILE_MONITOR_SEND_MOVED, NULL, NULL);
+  if (monitor) {
+    fs_source->priv->monitors = g_list_prepend (fs_source->priv->monitors,
+                                                monitor);
+    g_signal_connect (monitor,
+                      "changed",
+                      G_CALLBACK (directory_changed),
+                      fs_source);
+  } else {
+    GRL_DEBUG ("Unable to set up monitor in %s\n", g_file_get_path (dir));
+  }
+}
+
+static gboolean
+directory_cb (GFileInfo *dir_info, RecursiveOperation *operation)
+{
+  RecursiveEntry *entry;
+  GFile *dir;
+  GrlFilesystemSource *fs_source;
+
+  fs_source = GRL_FILESYSTEM_SOURCE (operation->on_dir_data);
+  entry = g_queue_peek_head (operation->directories);
+  dir = g_file_get_child (entry->directory,
+                          g_file_info_get_name (dir_info));
+
+  add_monitor (fs_source, dir);
+  g_object_unref (dir);
+
+  return TRUE;
+}
+
+/* ================== API Implementation ================ */
+
+static const GList *
+grl_filesystem_source_supported_keys (GrlMetadataSource *source)
+{
+  static GList *keys = NULL;
+  if (!keys) {
+    keys = grl_metadata_key_list_new (GRL_METADATA_KEY_ID,
+                                      GRL_METADATA_KEY_TITLE,
+                                      GRL_METADATA_KEY_URL,
+                                      GRL_METADATA_KEY_MIME,
+                                      GRL_METADATA_KEY_DATE,
+                                      GRL_METADATA_KEY_CHILDCOUNT,
+                                      NULL);
+  }
+  return keys;
+}
+
+static void
+grl_filesystem_source_browse (GrlMediaSource *source,
+                              GrlMediaSourceBrowseSpec *bs)
+{
+  const gchar *id;
+  GList *chosen_paths;
+
+  GRL_DEBUG ("grl_filesystem_source_browse");
+
+  id = grl_media_get_id (bs->container);
+  chosen_paths = GRL_FILESYSTEM_SOURCE(source)->priv->chosen_paths;
+  if (!id && chosen_paths) {
+    guint remaining = g_list_length (chosen_paths);
+
+    if (remaining == 1) {
+        produce_from_path (bs, chosen_paths->data);
+    } else {
+      for (; chosen_paths; chosen_paths = g_list_next (chosen_paths)) {
+        GrlMedia *content = create_content (NULL,
+                                            (gchar *) chosen_paths->data,
+                                            GRL_RESOLVE_FAST_ONLY,
+                                            FALSE);
+
+        bs->callback (source,
+                      bs->browse_id,
+                      content,
+                      --remaining,
+                      bs->user_data,
+                      NULL);
+      }
+    }
+  } else {
+    produce_from_path (bs, id ? id : G_DIR_SEPARATOR_S);
+  }
+}
+
+static void grl_filesystem_source_search (GrlMediaSource *source,
+                                          GrlMediaSourceSearchSpec *ss)
+{
+  RecursiveOperation *operation;
+  GrlFilesystemSource *fs_source;
+
+  GRL_DEBUG ("grl_filesystem_source_search");
+
+  fs_source = GRL_FILESYSTEM_SOURCE (source);
+
+  operation = recursive_operation_new ();
+  operation->on_cancel = cancel_cb;
+  operation->on_finish = finish_cb;
+  operation->on_file = file_cb;
+  operation->on_file_data = ss;
+  operation->max_depth = fs_source->priv->max_search_depth;
+  g_hash_table_insert (GRL_FILESYSTEM_SOURCE (source)->priv->cancellables,
+                       GUINT_TO_POINTER (ss->search_id),
+                       operation->cancellable);
+
+  recursive_operation_initialize (operation, fs_source);
+  recursive_operation_next_entry (operation);
+}
+
+static void
+grl_filesystem_source_metadata (GrlMediaSource *source,
+                                GrlMediaSourceMetadataSpec *ms)
+{
+  const gchar *path;
+  const gchar *id;
+
+  GRL_DEBUG ("grl_filesystem_source_metadata");
+
+  id = grl_media_get_id (ms->media);
+  path = id ? id : G_DIR_SEPARATOR_S;
+
+  if (g_file_test (path, G_FILE_TEST_EXISTS)) {
+    create_content (ms->media, path,
+		    ms->flags & GRL_RESOLVE_FAST_ONLY,
+		    !id);
+    ms->callback (ms->source, ms->media, ms->user_data, NULL);
+  } else {
+    GError *error = g_error_new (GRL_CORE_ERROR,
+				 GRL_CORE_ERROR_METADATA_FAILED,
+				 "File '%s' does not exist",
+				 path);
+    ms->callback (ms->source, ms->media, ms->user_data, error);
+    g_error_free (error);
+  }
+}
+
+static gboolean
+grl_filesystem_test_media_from_uri (GrlMediaSource *source,
+                                    const gchar *uri)
+{
+  gchar *path, *scheme;
+  GError *error = NULL;
+  gboolean ret = FALSE;
+
+  GRL_DEBUG ("grl_filesystem_test_media_from_uri");
+
+  scheme = g_uri_parse_scheme (uri);
+  ret = (g_strcmp0(scheme, "file") == 0);
+  g_free (scheme);
+  if (!ret)
+    return ret;
+
+  path = g_filename_from_uri (uri, NULL, &error);
+  if (error != NULL) {
+    g_error_free (error);
+    return FALSE;
+  }
+
+  ret = file_is_valid_content (path, TRUE);
+
+  g_free (path);
+  return ret;
+}
+
+static void grl_filesystem_get_media_from_uri (GrlMediaSource *source,
+                                               GrlMediaSourceMediaFromUriSpec *mfus)
+{
+  gchar *path, *scheme;
+  GError *error = NULL;
+  gboolean ret = FALSE;
+  GrlMedia *media;
+
+  GRL_DEBUG ("grl_filesystem_get_media_from_uri");
+
+  scheme = g_uri_parse_scheme (mfus->uri);
+  ret = (g_strcmp0(scheme, "file") == 0);
+  g_free (scheme);
+  if (!ret) {
+    error = g_error_new (GRL_CORE_ERROR,
+                         GRL_CORE_ERROR_MEDIA_FROM_URI_FAILED,
+                         "Cannot create media from '%s'", mfus->uri);
+    mfus->callback (source, NULL, mfus->user_data, error);
+    g_clear_error (&error);
+    return;
+  }
+
+  path = g_filename_from_uri (mfus->uri, NULL, &error);
+  if (error != NULL) {
+    GError *new_error;
+    new_error = g_error_new (GRL_CORE_ERROR,
+                         GRL_CORE_ERROR_MEDIA_FROM_URI_FAILED,
+                         "Cannot create media from '%s', error message: %s",
+                         mfus->uri, error->message);
+    g_clear_error (&error);
+    mfus->callback (source, NULL, mfus->user_data, new_error);
+    g_clear_error (&new_error);
+    goto beach;
+  }
+
+  /* FIXME: this is a blocking call, not sure we want that in here */
+  /* Note: we assume create_content() never returns NULL, which seems to be true */
+  media = create_content (NULL, path, mfus->flags & GRL_RESOLVE_FAST_ONLY,
+                          FALSE);
+  mfus->callback (source, media, mfus->user_data, NULL);
+
+beach:
+  g_free (path);
+}
+
+static void
+grl_filesystem_source_cancel (GrlMediaSource *source, guint operation_id)
+{
+  GCancellable *cancellable;
+  GrlFilesystemSourcePrivate *priv;
+
+  priv = GRL_FILESYSTEM_SOURCE (source)->priv;
+
+  cancellable =
+      G_CANCELLABLE (g_hash_table_lookup (priv->cancellables,
+                                          GUINT_TO_POINTER (operation_id)));
+  if (cancellable)
+    g_cancellable_cancel (cancellable);
+}
+
+static gboolean
+grl_filesystem_source_notify_change_start (GrlMediaSource *source,
+                                           GError **error)
+{
+  GrlFilesystemSource *fs_source;
+  RecursiveOperation *operation;
+
+  GRL_DEBUG (__func__);
+
+  fs_source = GRL_FILESYSTEM_SOURCE (source);
+  operation = recursive_operation_new ();
+  operation->on_cancel = cancel_cb;
+  operation->on_finish = finish_cb;
+  operation->on_dir = directory_cb;
+  operation->on_dir_data = fs_source;
+  operation->max_depth = fs_source->priv->max_search_depth;
+
+  fs_source->priv->cancellable_monitors = operation->cancellable;
+
+  recursive_operation_initialize (operation, fs_source);
+  recursive_operation_next_entry (operation);
+
+  return TRUE;
+}
+
+static gboolean
+grl_filesystem_source_notify_change_stop (GrlMediaSource *source,
+                                          GError **error)
+{
+  GrlFilesystemSource *fs_source = GRL_FILESYSTEM_SOURCE (source);
+
+  /* Check if notifying is being initialized */
+  if (fs_source->priv->cancellable_monitors) {
+    g_cancellable_cancel (fs_source->priv->cancellable_monitors);
+    fs_source->priv->cancellable_monitors = NULL;
+  } else {
+    /* Cancel and remove all monitors */
+    cancel_monitors (fs_source);
+  }
+
+  return TRUE;
+}
diff --git a/src/media/filesystem/grl-filesystem.h b/src/media/filesystem/grl-filesystem.h
new file mode 100644
index 0000000..428c61b
--- /dev/null
+++ b/src/media/filesystem/grl-filesystem.h
@@ -0,0 +1,81 @@
+/*
+ * Copyright (C) 2010, 2011 Igalia S.L.
+ *
+ * Contact: Iago Toral Quiroga <itoral igalia 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_FILESYSTEM_SOURCE_H_
+#define _GRL_FILESYSTEM_SOURCE_H_
+
+#include <grilo.h>
+
+#define GRL_FILESYSTEM_SOURCE_TYPE              \
+  (grl_filesystem_source_get_type ())
+
+#define GRL_FILESYSTEM_SOURCE(obj)                              \
+  (G_TYPE_CHECK_INSTANCE_CAST ((obj),                           \
+                               GRL_FILESYSTEM_SOURCE_TYPE,      \
+                               GrlFilesystemSource))
+
+#define GRL_IS_FILESYSTEM_SOURCE(obj)                           \
+  (G_TYPE_CHECK_INSTANCE_TYPE ((obj),                           \
+                               GRL_FILESYSTEM_SOURCE_TYPE))
+
+#define GRL_FILESYSTEM_SOURCE_CLASS(klass)              \
+  (G_TYPE_CHECK_CLASS_CAST((klass),                     \
+                           GRL_FILESYSTEM_SOURCE_TYPE,  \
+                           GrlFilesystemSourceClass))
+
+#define GRL_IS_FILESYSTEM_SOURCE_CLASS(klass)           \
+  (G_TYPE_CHECK_CLASS_TYPE((klass)                      \
+                           GRL_FILESYSTEM_SOURCE_TYPE))
+
+#define GRL_FILESYSTEM_SOURCE_GET_CLASS(obj)                    \
+  (G_TYPE_INSTANCE_GET_CLASS ((obj),                            \
+                              GRL_FILESYSTEM_SOURCE_TYPE,       \
+                              GrlFilesystemSourceClass))
+
+/* --- Grilo Configuration --- */
+#define GRILO_CONF_CHOSEN_PATH "base-path"
+#define GRILO_CONF_MAX_SEARCH_DEPTH "maximum-search-depth"
+#define GRILO_CONF_MAX_SEARCH_DEPTH_DEFAULT 6
+
+
+typedef struct _GrlFilesystemSource GrlFilesystemSource;
+typedef struct _GrlFilesystemSourcePrivate GrlFilesystemSourcePrivate;
+
+struct _GrlFilesystemSource {
+
+  GrlMediaSource parent;
+
+  /*< private >*/
+  GrlFilesystemSourcePrivate *priv;
+};
+
+typedef struct _GrlFilesystemSourceClass GrlFilesystemSourceClass;
+
+struct _GrlFilesystemSourceClass {
+
+  GrlMediaSourceClass parent_class;
+
+};
+
+GType grl_filesystem_source_get_type (void);
+
+#endif /* _GRL_FILESYSTEM_SOURCE_H_ */
diff --git a/src/media/filesystem/grl-filesystem.xml b/src/media/filesystem/grl-filesystem.xml
new file mode 100644
index 0000000..cfb9bc6
--- /dev/null
+++ b/src/media/filesystem/grl-filesystem.xml
@@ -0,0 +1,9 @@
+<plugin>
+  <info>
+    <name>Filesystem</name>
+    <description>A plugin for browsing the filesystem</description>
+    <author>Igalia S.L.</author>
+    <license>LGPL</license>
+    <site>http://www.igalia.com</site>
+  </info>
+</plugin>
diff --git a/src/media/flickr/Makefile.am b/src/media/flickr/Makefile.am
new file mode 100644
index 0000000..66da488
--- /dev/null
+++ b/src/media/flickr/Makefile.am
@@ -0,0 +1,40 @@
+#
+# Makefile.am
+#
+# Author: Juan A. Suarez Romero <jasuarez igalia com>
+#
+# Copyright (C) 2010, 2011 Igalia S.L. All rights reserved.
+
+libplugin_LTLIBRARIES	= libgrlflickr.la
+
+libgrlflickr_la_CFLAGS =	\
+	$(DEPS_CFLAGS)		\
+	$(XML_CFLAGS)		\
+	$(GRLNET_CFLAGS)
+
+libgrlflickr_la_LIBADD =	\
+	$(DEPS_LIBS)		\
+	$(XML_LIBS)		\
+	$(GRLNET_LIBS)
+
+libgrlflickr_la_LDFLAGS = \
+	-module		  \
+	-avoid-version
+
+libgrlflickr_la_SOURCES =	\
+	grl-flickr.c		\
+	grl-flickr.h		\
+	gflickr.c		\
+	gflickr.h
+
+libplugindir = $(GRL_PLUGINS_DIR)
+flickrlibxmldir = $(GRL_PLUGINS_CONF_DIR)
+flickrlibxml_DATA = $(FLICKR_PLUGIN_ID).xml
+
+EXTRA_DIST = $(flickrlibxml_DATA)
+
+MAINTAINERCLEANFILES =	\
+	*.in		\
+	*~
+
+DISTCLEANFILES = $(MAINTAINERCLEANFILES)
diff --git a/src/media/flickr/gflickr.c b/src/media/flickr/gflickr.c
new file mode 100644
index 0000000..695c7dc
--- /dev/null
+++ b/src/media/flickr/gflickr.c
@@ -0,0 +1,1196 @@
+#include "gflickr.h"
+#include "grl-flickr.h"       /* log domain */
+
+#include <libxml/xpath.h>
+#include <gio/gio.h>
+#include <string.h>
+
+#include <grilo.h>
+#include <net/grl-net.h>
+
+
+#define G_FLICKR_GET_PRIVATE(object)            \
+  (G_TYPE_INSTANCE_GET_PRIVATE((object),        \
+                               G_FLICKR_TYPE,   \
+                               GFlickrPrivate))
+
+#define GRL_LOG_DOMAIN_DEFAULT flickr_log_domain
+
+#define FLICKR_PHOTO_ORIG_URL                           \
+  "http://farm%s.static.flickr.com/%s/%s_%s_o.%s";
+
+#define FLICKR_PHOTO_THUMB_URL                          \
+  "http://farm%s.static.flickr.com/%s/%s_%s_t.jpg";
+
+#define FLICKR_PHOTO_LARGEST_URL                        \
+  "http://farm%s.static.flickr.com/%s/%s_%s_b.jpg";
+
+#define FLICKR_ENDPOINT  "http://api.flickr.com/services/rest/?";
+#define FLICKR_AUTHPOINT "http://flickr.com/services/auth/?";
+
+#define FLICKR_PHOTOS_SEARCH_METHOD       "flickr.photos.search"
+#define FLICKR_PHOTOS_GETINFO_METHOD      "flickr.photos.getInfo"
+#define FLICKR_PHOTOS_GETRECENT_METHOD    "flickr.photos.getRecent"
+#define FLICKR_PHOTOSETS_GETLIST_METHOD   "flickr.photosets.getList"
+#define FLICKR_PHOTOSETS_GETPHOTOS_METHOD "flickr.photosets.getPhotos"
+#define FLICKR_TAGS_GETHOTLIST_METHOD     "flickr.tags.getHotList"
+#define FLICKR_AUTH_GETFROB_METHOD        "flickr.auth.getFrob"
+#define FLICKR_AUTH_GETTOKEN_METHOD       "flickr.auth.getToken"
+#define FLICKR_AUTH_CHECKTOKEN_METHOD     "flickr.auth.checkToken"
+
+#define FLICKR_PHOTOS_SEARCH                            \
+  FLICKR_ENDPOINT                                       \
+  "api_key=%s"                                          \
+  "&api_sig=%s"                                         \
+  "&method=" FLICKR_PHOTOS_SEARCH_METHOD                \
+  "&user_id=%s"                                         \
+  "&extras=media,date_taken,owner_name,url_o,url_t"     \
+  "&per_page=%d"                                        \
+  "&page=%d"                                            \
+  "&tags=%s"                                            \
+  "&text=%s"                                            \
+  "%s"
+
+#define FLICKR_PHOTOS_GETRECENT                         \
+  FLICKR_ENDPOINT                                       \
+  "api_key=%s"                                          \
+  "&api_sig=%s"                                         \
+  "&method=" FLICKR_PHOTOS_GETRECENT_METHOD             \
+  "&extras=media,date_taken,owner_name,url_o,url_t"     \
+  "&per_page=%d"                                        \
+  "&page=%d"                                            \
+  "%s"
+
+#define FLICKR_PHOTOSETS_GETLIST                \
+  FLICKR_ENDPOINT                               \
+  "api_key=%s"                                  \
+  "&api_sig=%s"                                 \
+  "&method=" FLICKR_PHOTOSETS_GETLIST_METHOD    \
+  "%s"                                          \
+  "%s"
+
+#define FLICKR_PHOTOSETS_GETPHOTOS                      \
+  FLICKR_ENDPOINT                                       \
+  "api_key=%s"                                          \
+  "&api_sig=%s"                                         \
+  "&method=" FLICKR_PHOTOSETS_GETPHOTOS_METHOD          \
+  "&photoset_id=%s"                                     \
+  "&extras=media,date_taken,owner_name,url_o,url_t"     \
+  "&per_page=%d"                                        \
+  "&page=%d"                                            \
+  "%s"
+
+#define FLICKR_TAGS_GETHOTLIST                          \
+  FLICKR_ENDPOINT                                       \
+  "api_key=%s"                                          \
+  "&api_sig=%s"                                         \
+  "&method=" FLICKR_TAGS_GETHOTLIST_METHOD              \
+  "&count=%d"                                           \
+  "%s"
+
+#define FLICKR_PHOTOS_GETINFO                   \
+  FLICKR_ENDPOINT                               \
+  "api_key=%s"                                  \
+  "&api_sig=%s"                                 \
+  "&method=" FLICKR_PHOTOS_GETINFO_METHOD       \
+  "&photo_id=%ld"                               \
+  "%s"
+
+#define FLICKR_AUTH_GETFROB                     \
+  FLICKR_ENDPOINT                               \
+  "api_key=%s"                                  \
+  "&api_sig=%s"                                 \
+  "&method=" FLICKR_AUTH_GETFROB_METHOD
+
+#define FLICKR_AUTH_GETTOKEN                    \
+  FLICKR_ENDPOINT                               \
+  "api_key=%s"                                  \
+  "&api_sig=%s"                                 \
+  "&method=" FLICKR_AUTH_GETTOKEN_METHOD        \
+  "&frob=%s"
+
+#define FLICKR_AUTH_CHECKTOKEN                  \
+  FLICKR_ENDPOINT                               \
+  "api_key=%s"                                  \
+  "&api_sig=%s"                                 \
+  "&method=" FLICKR_AUTH_CHECKTOKEN_METHOD      \
+  "&auth_token=%s"
+
+#define FLICKR_AUTH_LOGINLINK                   \
+  FLICKR_AUTHPOINT                              \
+  "api_key=%s"                                  \
+  "&api_sig=%s"                                 \
+  "&frob=%s"                                    \
+  "&perms=%s"
+
+typedef void (*ParseXML) (const gchar *xml_result, gpointer user_data);
+
+typedef struct {
+  GFlickr *flickr;
+  ParseXML parse_xml;
+  GFlickrHashTableCb hashtable_cb;
+  GFlickrListCb list_cb;
+  gpointer user_data;
+} GFlickrData;
+
+struct _GFlickrPrivate {
+  gchar *api_key;
+  gchar *auth_secret;
+  gchar *auth_token;
+  gint per_page;
+
+  GrlNetWc *wc;
+};
+
+static void g_flickr_finalize (GObject *object);
+
+/* -------------------- GOBJECT -------------------- */
+
+G_DEFINE_TYPE (GFlickr, g_flickr, G_TYPE_OBJECT);
+
+static void
+g_flickr_class_init (GFlickrClass *klass)
+{
+  GObjectClass *gobject_class = G_OBJECT_CLASS (klass);
+  gobject_class->finalize = g_flickr_finalize;
+
+  g_type_class_add_private (klass, sizeof (GFlickrPrivate));
+}
+
+static void
+g_flickr_init (GFlickr *f)
+{
+  f->priv = G_FLICKR_GET_PRIVATE (f);
+  f->priv->per_page = 100;
+}
+
+static void
+g_flickr_finalize (GObject *object)
+{
+  GFlickr *f = G_FLICKR (object);
+  g_free (f->priv->api_key);
+  g_free (f->priv->auth_token);
+  g_free (f->priv->auth_secret);
+
+  if (f->priv->wc)
+    g_object_unref (f->priv->wc);
+
+  G_OBJECT_CLASS (g_flickr_parent_class)->finalize (object);
+}
+
+GFlickr *
+g_flickr_new (const gchar *api_key, const gchar *auth_secret, const gchar *auth_token)
+{
+  g_return_val_if_fail (api_key && auth_secret, NULL);
+
+  GFlickr *f = g_object_new (G_FLICKR_TYPE, NULL);
+  f->priv->api_key = g_strdup (api_key);
+  f->priv->auth_secret = g_strdup (auth_secret);
+  f->priv->auth_token = g_strdup (auth_token);
+
+  return f;
+}
+
+/* -------------------- PRIVATE API -------------------- */
+
+static gchar *
+get_api_sig (const gchar *secret, ...)
+{
+  GHashTable *hash;
+  GList *key_iter;
+  GList *keys;
+  GString *to_sign;
+  gchar *api_sig;
+  gchar *key;
+  gchar *value;
+  gint text_size = strlen (secret);
+  va_list va_params;
+
+  hash = g_hash_table_new (g_str_hash, g_str_equal);
+
+  va_start (va_params, secret);
+  while ((key = va_arg (va_params, gchar *))) {
+    text_size += strlen (key);
+    value = va_arg (va_params, gchar *);
+    text_size += strlen (value);
+    g_hash_table_insert (hash, key, value);
+  }
+  va_end (va_params);
+
+  to_sign = g_string_sized_new (text_size);
+  g_string_append (to_sign, secret);
+
+  keys = g_hash_table_get_keys (hash);
+  keys = g_list_sort (keys, (GCompareFunc) g_strcmp0);
+  for (key_iter = keys; key_iter; key_iter = g_list_next (key_iter)) {
+    g_string_append (to_sign, key_iter->data);
+    g_string_append (to_sign, g_hash_table_lookup (hash, key_iter->data));
+  }
+
+  api_sig = g_compute_checksum_for_string (G_CHECKSUM_MD5, to_sign->str, -1);
+  g_hash_table_unref (hash);
+  g_list_free (keys);
+  g_string_free (to_sign, TRUE);
+
+  return api_sig;
+}
+
+static gchar *
+get_xpath_element (const gchar *content,
+                   const gchar *xpath_element)
+{
+  gchar *element = NULL;
+  xmlDocPtr xmldoc = NULL;
+  xmlXPathContextPtr xpath_ctx = NULL;
+  xmlXPathObjectPtr xpath_res = NULL;
+
+  xmldoc = xmlReadMemory (content, xmlStrlen ((xmlChar *) content), NULL, NULL,
+                          XML_PARSE_RECOVER | XML_PARSE_NOBLANKS);
+  if (xmldoc) {
+    xpath_ctx = xmlXPathNewContext (xmldoc);
+    if (xpath_ctx) {
+      xpath_res = xmlXPathEvalExpression ((xmlChar *) xpath_element, xpath_ctx);
+      if (xpath_res && xpath_res->nodesetval->nodeTab) {
+        element =
+          (gchar *) xmlNodeListGetString (xmldoc,
+                                          xpath_res->nodesetval->nodeTab[0]->xmlChildrenNode,
+                                          1);
+      }
+    }
+  }
+
+  /* Free data */
+  if (xmldoc) {
+    xmlFreeDoc (xmldoc);
+  }
+
+  if (xpath_ctx) {
+    xmlXPathFreeContext (xpath_ctx);
+  }
+
+  if (xpath_res) {
+    xmlXPathFreeObject (xpath_res);
+  }
+
+  return element;
+}
+
+static gboolean
+result_is_correct (xmlNodePtr node)
+{
+  gboolean correct = FALSE;
+  xmlChar *stat;
+
+  if (xmlStrcmp (node->name, (const xmlChar *) "rsp") == 0) {
+    stat = xmlGetProp (node, (const xmlChar *) "stat");
+    if (stat && xmlStrcmp (stat, (const xmlChar *) "ok") == 0) {
+      correct = TRUE;
+      xmlFree (stat);
+    }
+  }
+
+  return correct;
+}
+
+static void
+add_node (xmlNodePtr node, GHashTable *photo)
+{
+  xmlAttrPtr attr;
+
+  for (attr = node->properties; attr != NULL; attr = attr->next) {
+    g_hash_table_insert (photo,
+                         g_strconcat ((const gchar *) node->name,
+                                      "_",
+                                      (const gchar *) attr->name,
+                                      NULL),
+                         (gchar *) xmlGetProp (node, attr->name));
+  }
+}
+
+static GHashTable *
+get_photo (xmlNodePtr node)
+{
+  GHashTable *photo = g_hash_table_new_full (g_str_hash,
+                                             g_str_equal,
+                                             g_free,
+                                             g_free);
+
+  /* Add photo node */
+  add_node (node, photo);
+
+  /* Add children nodes with their properties */
+
+  node = node->xmlChildrenNode;
+
+  while (node) {
+    if (xmlStrcmp (node->name, (const xmlChar *) "owner") == 0 ||
+        xmlStrcmp (node->name, (const xmlChar *) "dates") == 0) {
+      add_node (node, photo);
+    } else if (xmlStrcmp (node->name, (const xmlChar *) "title") == 0 ||
+               xmlStrcmp (node->name, (const xmlChar *) "description") == 0) {
+      g_hash_table_insert (photo,
+                           g_strdup ((const gchar *) node->name),
+                           (gchar *) xmlNodeGetContent (node));
+    }
+
+    node = node->next;
+  }
+
+  return photo;
+}
+
+static GHashTable *
+get_photoset (xmlNodePtr node)
+{
+  GHashTable *photoset = g_hash_table_new_full (g_str_hash,
+                                             g_str_equal,
+                                             g_free,
+                                             g_free);
+
+  /* Add photoset node */
+  add_node (node, photoset);
+
+  /* Add children nodes with their properties */
+  node = node->xmlChildrenNode;
+
+  while (node) {
+    g_hash_table_insert (photoset,
+                         g_strdup ((const gchar *) node->name),
+                         (gchar *) xmlNodeGetContent (node));
+    node = node->next;
+  }
+
+  return photoset;
+}
+
+static gchar *
+get_tag (xmlNodePtr node)
+{
+  if (xmlStrcmp (node->name, (const xmlChar *) "tag") == 0) {
+    return (gchar *) xmlNodeGetContent (node);
+  } else {
+    return NULL;
+  }
+}
+
+static GHashTable *
+get_token_info (xmlNodePtr node)
+{
+  GHashTable *token = g_hash_table_new_full (g_str_hash,
+                                             g_str_equal,
+                                             g_free,
+                                             g_free);
+  node = node->xmlChildrenNode;
+
+  while (node) {
+    g_hash_table_insert (token,
+                         g_strdup ((const gchar *) node->name),
+                         (gchar *) xmlNodeGetContent (node));
+    add_node (node, token);
+    node = node->next;
+  }
+
+  return token;
+}
+
+static void
+process_photo_result (const gchar *xml_result, gpointer user_data)
+{
+  xmlDocPtr doc;
+  xmlNodePtr node;
+  GFlickrData *data = (GFlickrData *) user_data;
+  GHashTable *photo;
+
+  doc = xmlReadMemory (xml_result, xmlStrlen ((xmlChar*) xml_result), NULL,
+                       NULL, XML_PARSE_RECOVER | XML_PARSE_NOBLANKS);
+  node = xmlDocGetRootElement (doc);
+
+  /* Check result is ok */
+  if (!node || !result_is_correct (node)) {
+    data->hashtable_cb (data->flickr, NULL, data->user_data);
+  } else {
+    node = node->xmlChildrenNode;
+
+    photo = get_photo (node);
+    data->hashtable_cb (data->flickr, photo, data->user_data);
+    g_hash_table_unref (photo);
+  }
+  g_object_unref (data->flickr);
+  g_slice_free (GFlickrData, data);
+  xmlFreeDoc (doc);
+}
+
+static void
+process_photolist_result (const gchar *xml_result, gpointer user_data)
+{
+  GFlickrData *data = (GFlickrData *) user_data;
+  GList *photolist = NULL;
+  xmlDocPtr doc;
+  xmlNodePtr node;
+
+  doc = xmlReadMemory (xml_result, xmlStrlen ((xmlChar*) xml_result), NULL,
+                       NULL, XML_PARSE_RECOVER | XML_PARSE_NOBLANKS);
+  node = xmlDocGetRootElement (doc);
+
+  /* Check result is ok */
+  if (!node || !result_is_correct (node)) {
+    data->list_cb (data->flickr, NULL, data->user_data);
+  } else {
+    node = node->xmlChildrenNode;
+
+    /* Now we're at "photo pages" node */
+    node = node->xmlChildrenNode;
+    while (node) {
+      photolist = g_list_prepend (photolist, get_photo (node));
+      node = node->next;
+    }
+
+    data->list_cb (data->flickr, g_list_reverse (photolist), data->user_data);
+    g_list_foreach (photolist, (GFunc) g_hash_table_unref, NULL);
+    g_list_free (photolist);
+  }
+  g_object_unref (data->flickr);
+  g_slice_free (GFlickrData, data);
+  xmlFreeDoc (doc);
+}
+
+static void
+process_taglist_result (const gchar *xml_result, gpointer user_data)
+{
+  GFlickrData *data = (GFlickrData *) user_data;
+  GList *taglist = NULL;
+  xmlDocPtr doc;
+  xmlNodePtr node;
+
+  doc = xmlReadMemory (xml_result, xmlStrlen ((xmlChar*) xml_result), NULL,
+                       NULL, XML_PARSE_RECOVER | XML_PARSE_NOBLANKS);
+  node = xmlDocGetRootElement (doc);
+
+  /* Check if result is OK */
+  if (!node || !result_is_correct (node)) {
+    data->list_cb (data->flickr, NULL, data->user_data);
+  } else {
+    node = node->xmlChildrenNode;
+
+    /* Now we're at "hot tags" node */
+    node = node->xmlChildrenNode;
+    while (node) {
+      taglist = g_list_prepend (taglist, get_tag (node));
+      node = node->next;
+    }
+
+    data->list_cb (data->flickr, g_list_reverse (taglist), data->user_data);
+    g_list_foreach (taglist, (GFunc) g_free, NULL);
+    g_list_free (taglist);
+  }
+  g_object_unref (data->flickr);
+  g_slice_free (GFlickrData, data);
+  xmlFreeDoc (doc);
+}
+
+static void
+process_photosetslist_result (const gchar *xml_result, gpointer user_data)
+{
+  GFlickrData *data = (GFlickrData *) user_data;
+  GList *photosets = NULL;
+  xmlDocPtr doc;
+  xmlNodePtr node;
+
+  doc = xmlReadMemory (xml_result, xmlStrlen ((xmlChar*) xml_result), NULL,
+                       NULL, XML_PARSE_RECOVER | XML_PARSE_NOBLANKS);
+  node = xmlDocGetRootElement (doc);
+
+  /* Check if result is OK */
+  if (!node || !result_is_correct (node)) {
+    data->list_cb (data->flickr, NULL, data->user_data);
+  } else {
+    node = node->xmlChildrenNode;
+
+    /* Now we're at "photosets" node */
+    node = node->xmlChildrenNode;
+    while (node) {
+      photosets = g_list_prepend (photosets, get_photoset (node));
+      node = node->next;
+    }
+
+    data->list_cb (data->flickr, g_list_reverse (photosets), data->user_data);
+    g_list_foreach (photosets, (GFunc) g_hash_table_unref, NULL);
+    g_list_free (photosets);
+  }
+  g_object_unref (data->flickr);
+  g_slice_free (GFlickrData, data);
+  xmlFreeDoc (doc);
+}
+
+static void
+process_photosetsphotos_result (const gchar *xml_result, gpointer user_data)
+{
+  GFlickrData *data = (GFlickrData *) user_data;
+  GList *list = NULL;
+  xmlDocPtr doc;
+  xmlNodePtr node;
+
+  doc = xmlReadMemory (xml_result, xmlStrlen ((xmlChar*) xml_result), NULL,
+                       NULL, XML_PARSE_RECOVER | XML_PARSE_NOBLANKS);
+  node = xmlDocGetRootElement (doc);
+
+  /* Check result is ok */
+  if (!node || !result_is_correct (node)) {
+    data->list_cb (data->flickr, NULL, data->user_data);
+  } else {
+    node = node->xmlChildrenNode;
+
+    /* Now we're at "photoset page" node */
+    node = node->xmlChildrenNode;
+    while (node) {
+      list = g_list_prepend (list, get_photo (node));
+      node = node->next;
+    }
+
+    data->list_cb (data->flickr, g_list_reverse (list), data->user_data);
+    g_list_foreach (list, (GFunc) g_hash_table_unref, NULL);
+    g_list_free (list);
+  }
+  g_object_unref (data->flickr);
+  g_slice_free (GFlickrData, data);
+  xmlFreeDoc (doc);
+}
+
+static void
+process_token_result (const gchar *xml_result, gpointer user_data)
+{
+  xmlDocPtr doc;
+  xmlNodePtr node;
+  GFlickrData *data = (GFlickrData *) user_data;
+  GHashTable *token;
+
+  doc = xmlReadMemory (xml_result, xmlStrlen ((xmlChar*) xml_result), NULL,
+                       NULL, XML_PARSE_RECOVER | XML_PARSE_NOBLANKS);
+  node = xmlDocGetRootElement (doc);
+
+  /* Check if result is OK */
+  if (!node || !result_is_correct (node)) {
+    data->hashtable_cb (data->flickr, NULL, data->user_data);
+  } else {
+    node = node->xmlChildrenNode;
+    token = get_token_info (node);
+    data->hashtable_cb (data->flickr, token, data->user_data);
+    g_hash_table_unref (token);
+  }
+
+  g_object_unref (data->flickr);
+  g_slice_free (GFlickrData, data);
+  xmlFreeDoc (doc);
+}
+
+inline static GrlNetWc *
+get_wc (GFlickr *f)
+{
+  if (!f->priv->wc)
+    f->priv->wc = grl_net_wc_new ();
+
+  return f->priv->wc;
+}
+
+static void
+read_done_cb (GObject *source_object,
+              GAsyncResult *res,
+              gpointer user_data)
+{
+  gchar *content = NULL;
+  GError *wc_error = NULL;
+  GFlickrData *data = (GFlickrData *) user_data;
+
+  grl_net_wc_request_finish (GRL_NET_WC (source_object),
+                         res,
+                         &content,
+                         NULL,
+                         &wc_error);
+
+  data->parse_xml (content, user_data);
+}
+
+static void
+read_url_async (GFlickr *f,
+                const gchar *url,
+                gpointer user_data)
+{
+  GRL_DEBUG ("Opening '%s'", url);
+  grl_net_wc_request_async (get_wc (f),
+                        url,
+                        NULL,
+                        read_done_cb,
+                        user_data);
+}
+
+/* -------------------- PUBLIC API -------------------- */
+
+void
+g_flickr_set_per_page (GFlickr *f, gint per_page)
+{
+  g_return_if_fail (G_IS_FLICKR (f));
+
+  f->priv->per_page = per_page;
+}
+
+void
+g_flickr_photos_getInfo (GFlickr *f,
+                         glong photo_id,
+                         GFlickrHashTableCb callback,
+                         gpointer user_data)
+{
+  gchar *auth;
+
+  g_return_if_fail (G_IS_FLICKR (f));
+
+  gchar *str_photo_id = g_strdup_printf ("%ld", photo_id);
+  gchar *api_sig = get_api_sig (f->priv->auth_secret,
+                                "api_key", f->priv->api_key,
+                                "method", FLICKR_PHOTOS_GETINFO_METHOD,
+                                "photo_id", str_photo_id,
+                                f->priv->auth_token? "auth_token": "",
+                                f->priv->auth_token? f->priv->auth_token: "",
+                                NULL);
+  g_free (str_photo_id);
+
+  /* Build the request */
+  if (f->priv->auth_token) {
+    auth = g_strdup_printf ("&auth_token=%s", f->priv->auth_token);
+  } else {
+    auth = g_strdup ("");
+  }
+
+  gchar *request = g_strdup_printf (FLICKR_PHOTOS_GETINFO,
+                                    f->priv->api_key,
+                                    api_sig,
+                                    photo_id,
+                                    auth);
+  g_free (api_sig);
+  g_free (auth);
+
+  GFlickrData *gfd = g_slice_new (GFlickrData);
+  gfd->flickr = g_object_ref (f);
+  gfd->parse_xml = process_photo_result;
+  gfd->hashtable_cb = callback;
+  gfd->user_data = user_data;
+
+  read_url_async (f, request, gfd);
+  g_free (request);
+}
+
+void
+g_flickr_photos_search (GFlickr *f,
+                        const gchar *user_id,
+                        const gchar *text,
+                        const gchar *tags,
+                        gint page,
+                        GFlickrListCb callback,
+                        gpointer user_data)
+{
+  gchar *auth;
+  g_return_if_fail (G_IS_FLICKR (f));
+
+  if (!user_id) {
+    user_id = "";
+  }
+
+  if (!text) {
+    text = "";
+  }
+
+  if (!tags) {
+    tags = "";
+  }
+
+  gchar *strpage = g_strdup_printf ("%d", page);
+  gchar *strperpage = g_strdup_printf ("%d", f->priv->per_page);
+
+  gchar *api_sig =
+    get_api_sig (f->priv->auth_secret,
+                 "api_key", f->priv->api_key,
+                 "extras", "media,date_taken,owner_name,url_o,url_t",
+                 "method", FLICKR_PHOTOS_SEARCH_METHOD,
+                 "user_id", user_id,
+                 "page", strpage,
+                 "per_page", strperpage,
+                 "tags", tags,
+                 "text", text,
+                 f->priv->auth_token? "auth_token": "",
+                 f->priv->auth_token? f->priv->auth_token: "",
+                 NULL);
+  g_free (strpage);
+  g_free (strperpage);
+
+  /* Build the request */
+  if (f->priv->auth_token) {
+    auth = g_strdup_printf ("&auth_token=%s", f->priv->auth_token);
+  } else {
+    auth = g_strdup ("");
+  }
+
+  gchar *request = g_strdup_printf (FLICKR_PHOTOS_SEARCH,
+                                    f->priv->api_key,
+                                    api_sig,
+                                    user_id,
+                                    f->priv->per_page,
+                                    page,
+                                    tags,
+                                    text,
+                                    auth);
+  g_free (api_sig);
+  g_free (auth);
+
+  GFlickrData *gfd = g_slice_new (GFlickrData);
+  gfd->flickr = g_object_ref (f);
+  gfd->parse_xml = process_photolist_result;
+  gfd->list_cb = callback;
+  gfd->user_data = user_data;
+
+  read_url_async (f, request, gfd);
+  g_free (request);
+}
+
+void
+g_flickr_photos_getRecent (GFlickr *f,
+                           gint page,
+                           GFlickrListCb callback,
+                           gpointer user_data)
+{
+  gchar *auth;
+  g_return_if_fail (G_IS_FLICKR (f));
+
+  gchar *strpage = g_strdup_printf ("%d", page);
+  gchar *strperpage = g_strdup_printf ("%d", f->priv->per_page);
+
+  gchar *api_sig =
+    get_api_sig (f->priv->auth_secret,
+                 "api_key", f->priv->api_key,
+                 "extras", "media,date_taken,owner_name,url_o,url_t",
+                 "method", FLICKR_PHOTOS_GETRECENT_METHOD,
+                 "page", strpage,
+                 "per_page", strperpage,
+                 f->priv->auth_token? "auth_token": "",
+                 f->priv->auth_token? f->priv->auth_token: "",
+                 NULL);
+  g_free (strpage);
+  g_free (strperpage);
+
+  /* Build the request */
+  if (f->priv->auth_token) {
+    auth = g_strdup_printf ("&auth_token=%s", f->priv->auth_token);
+  } else {
+    auth = g_strdup ("");
+  }
+
+  gchar *request = g_strdup_printf (FLICKR_PHOTOS_GETRECENT,
+                                    f->priv->api_key,
+                                    api_sig,
+                                    f->priv->per_page,
+                                    page,
+                                    auth);
+  g_free (api_sig);
+  g_free (auth);
+
+  GFlickrData *gfd = g_slice_new (GFlickrData);
+  gfd->flickr = g_object_ref (f);
+  gfd->parse_xml = process_photolist_result;
+  gfd->list_cb = callback;
+  gfd->user_data = user_data;
+
+  read_url_async (f, request, gfd);
+  g_free (request);
+}
+
+gchar *
+g_flickr_photo_url_original (GFlickr *f, GHashTable *photo)
+{
+  gchar *extension;
+  gchar *farm_id;
+  gchar *o_secret;
+  gchar *photo_id;
+  gchar *server_id;
+
+  if (!photo) {
+    return NULL;
+  }
+
+  extension = g_hash_table_lookup (photo, "photo_originalformat");
+  farm_id = g_hash_table_lookup (photo, "photo_farm");
+  o_secret = g_hash_table_lookup (photo, "photo_originalsecret");
+  photo_id = g_hash_table_lookup (photo, "photo_id");
+  server_id = g_hash_table_lookup (photo, "photo_server");
+
+  if (!extension || !farm_id || !o_secret || !photo_id || !server_id) {
+    return NULL;
+  } else {
+    return g_strdup_printf (FLICKR_PHOTO_ORIG_URL,
+                            farm_id,
+                            server_id,
+                            photo_id,
+                            o_secret,
+                            extension);
+  }
+}
+
+gchar *
+g_flickr_photo_url_thumbnail (GFlickr *f, GHashTable *photo)
+{
+  gchar *farm_id;
+  gchar *secret;
+  gchar *photo_id;
+  gchar *server_id;
+
+  if (!photo) {
+    return NULL;
+  }
+
+  farm_id = g_hash_table_lookup (photo, "photo_farm");
+  secret = g_hash_table_lookup (photo, "photo_secret");
+  photo_id = g_hash_table_lookup (photo, "photo_id");
+  server_id = g_hash_table_lookup (photo, "photo_server");
+
+  if (!farm_id || !secret || !photo_id || !server_id) {
+    return NULL;
+  } else {
+    return g_strdup_printf (FLICKR_PHOTO_THUMB_URL,
+                            farm_id,
+                            server_id,
+                            photo_id,
+                            secret);
+  }
+}
+
+gchar *
+g_flickr_photo_url_largest (GFlickr *f, GHashTable *photo)
+{
+  gchar *farm_id;
+  gchar *secret;
+  gchar *photo_id;
+  gchar *server_id;
+
+  if (!photo) {
+    return NULL;
+  }
+
+  farm_id = g_hash_table_lookup (photo, "photo_farm");
+  secret = g_hash_table_lookup (photo, "photo_secret");
+  photo_id = g_hash_table_lookup (photo, "photo_id");
+  server_id = g_hash_table_lookup (photo, "photo_server");
+
+  if (!farm_id || !secret || !photo_id || !server_id) {
+    return NULL;
+  } else {
+    return g_strdup_printf (FLICKR_PHOTO_LARGEST_URL,
+                            farm_id,
+                            server_id,
+                            photo_id,
+                            secret);
+  }
+}
+
+void
+g_flickr_tags_getHotList (GFlickr *f,
+                          gint count,
+                          GFlickrListCb callback,
+                          gpointer user_data)
+{
+  gchar *auth;
+
+  g_return_if_fail (G_IS_FLICKR (f));
+
+  gchar *strcount = g_strdup_printf ("%d", count);
+
+  gchar *api_sig = get_api_sig (f->priv->auth_secret,
+                                "api_key", f->priv->api_key,
+                                "count", strcount,
+                                "method", FLICKR_TAGS_GETHOTLIST_METHOD,
+                                f->priv->auth_token? "auth_token": "",
+                                f->priv->auth_token? f->priv->auth_token: "",
+                                NULL);
+  g_free (strcount);
+
+  /* Build the request */
+  if (f->priv->auth_token) {
+    auth = g_strdup_printf ("&auth_token=%s", f->priv->auth_token);
+  } else {
+    auth = g_strdup ("");
+  }
+  gchar *request = g_strdup_printf (FLICKR_TAGS_GETHOTLIST,
+                                    f->priv->api_key,
+                                    api_sig,
+                                    count,
+                                    auth);
+  g_free (api_sig);
+  g_free (auth);
+
+  GFlickrData *gfd = g_slice_new (GFlickrData);
+  gfd->flickr = g_object_ref (f);
+  gfd->parse_xml = process_taglist_result;
+  gfd->list_cb = callback;
+  gfd->user_data = user_data;
+
+  read_url_async (f, request, gfd);
+  g_free (request);
+}
+
+void
+g_flickr_photosets_getList (GFlickr *f,
+                           const gchar *user_id,
+                           GFlickrListCb callback,
+                           gpointer user_data)
+{
+  gchar *user;
+  gchar *auth;
+
+  gchar *api_sig = get_api_sig (f->priv->auth_secret,
+                                "api_key", f->priv->api_key,
+                                "method", FLICKR_PHOTOSETS_GETLIST_METHOD,
+                                user_id? "user_id": "",
+                                user_id? user_id: "",
+                                f->priv->auth_token? "auth_token": "",
+                                f->priv->auth_token? f->priv->auth_token: "",
+                                NULL);
+
+  /* Build the request */
+  if (user_id) {
+    user = g_strdup_printf ("&user_id=%s", user_id);
+  } else {
+    user = g_strdup ("");
+  }
+
+  if (f->priv->auth_token) {
+    auth = g_strdup_printf ("&auth_token=%s", f->priv->auth_token);
+  } else {
+    auth = g_strdup ("");
+  }
+
+  gchar *request = g_strdup_printf (FLICKR_PHOTOSETS_GETLIST,
+                                    f->priv->api_key,
+                                    api_sig,
+                                    user,
+                                    auth);
+
+  g_free (api_sig);
+  g_free (user);
+  g_free (auth);
+
+  GFlickrData *gfd = g_slice_new (GFlickrData);
+  gfd->flickr = g_object_ref (f);
+  gfd->parse_xml = process_photosetslist_result;
+  gfd->list_cb = callback;
+  gfd->user_data = user_data;
+
+  read_url_async (f, request, gfd);
+  g_free (request);
+}
+
+void
+g_flickr_photosets_getPhotos (GFlickr *f,
+                              const gchar *photoset_id,
+                              gint page,
+                              GFlickrListCb callback,
+                              gpointer user_data)
+{
+  gchar *auth;
+
+  g_return_if_fail (G_IS_FLICKR (f));
+  g_return_if_fail (photoset_id);
+
+  gchar *strpage = g_strdup_printf ("%d", page);
+  gchar *strperpage = g_strdup_printf ("%d", f->priv->per_page);
+
+  gchar *api_sig =
+    get_api_sig (f->priv->auth_secret,
+                 "api_key", f->priv->api_key,
+                 "photoset_id", photoset_id,
+                 "extras", "media,date_taken,owner_name,url_o,url_t",
+                 "method", FLICKR_PHOTOSETS_GETPHOTOS_METHOD,
+                 "page", strpage,
+                 "per_page", strperpage,
+                 f->priv->auth_token? "auth_token": "",
+                 f->priv->auth_token? f->priv->auth_token: "",
+                 NULL);
+
+  g_free (strpage);
+  g_free (strperpage);
+
+  /* Build the request */
+  if (f->priv->auth_token) {
+    auth = g_strdup_printf ("&auth_token=%s", f->priv->auth_token);
+  } else {
+    auth = g_strdup ("");
+  }
+
+  gchar *request = g_strdup_printf (FLICKR_PHOTOSETS_GETPHOTOS,
+                                    f->priv->api_key,
+                                    api_sig,
+                                    photoset_id,
+                                    f->priv->per_page,
+                                    page,
+                                    auth);
+  g_free (api_sig);
+  g_free (auth);
+
+  GFlickrData *gfd = g_slice_new (GFlickrData);
+  gfd->flickr = g_object_ref (f);
+  gfd->parse_xml = process_photosetsphotos_result;
+  gfd->list_cb = callback;
+  gfd->user_data = user_data;
+
+  read_url_async (f, request, gfd);
+  g_free (request);
+}
+
+gchar *
+g_flickr_auth_getFrob (GFlickr *f)
+{
+  gchar *api_sig;
+  gchar *url;
+  GVfs *vfs;
+  GFile *uri;
+  gchar *contents;
+  GError *error = NULL;
+  gchar *frob = NULL;
+
+  g_return_val_if_fail (G_IS_FLICKR (f), NULL);
+
+  api_sig = get_api_sig (f->priv->auth_secret,
+                         "api_key", f->priv->api_key,
+                         "method", "flickr.auth.getFrob",
+                         NULL);
+
+  /* Build url */
+  url = g_strdup_printf (FLICKR_AUTH_GETFROB,
+                         f->priv->api_key,
+                         api_sig);
+  g_free (api_sig);
+
+  /* Load content */
+  vfs = g_vfs_get_default ();
+  uri = g_vfs_get_file_for_uri (vfs, url);
+  g_free (url);
+  if (!g_file_load_contents (uri, NULL, &contents, NULL, NULL, &error)) {
+    GRL_WARNING ("Unable to get Flickr's frob: %s", error->message);
+    return NULL;
+  }
+
+  /* Get frob */
+  frob = get_xpath_element (contents, "/rsp/frob");
+  g_free (contents);
+  if (!frob) {
+    GRL_WARNING ("Can not get Flickr's frob");
+  }
+
+  return frob;
+}
+
+gchar *
+g_flickr_auth_loginLink (GFlickr *f,
+                         const gchar *frob,
+                         const gchar *perm)
+{
+  gchar *api_sig;
+  gchar *url;
+
+  g_return_val_if_fail (G_IS_FLICKR (f), NULL);
+  g_return_val_if_fail (frob, NULL);
+  g_return_val_if_fail (perm, NULL);
+
+  api_sig = get_api_sig (f->priv->auth_secret,
+                         "api_key", f->priv->api_key,
+                         "frob", frob,
+                         "perms", perm,
+                         NULL);
+
+  url = g_strdup_printf (FLICKR_AUTH_LOGINLINK,
+                         f->priv->api_key,
+                         api_sig,
+                         frob,
+                         perm);
+  g_free (api_sig);
+
+  return url;
+}
+
+gchar *
+g_flickr_auth_getToken (GFlickr *f,
+                        const gchar *frob)
+{
+  GError *error = NULL;
+  GFile *uri;
+  GVfs *vfs;
+  gchar *api_sig;
+  gchar *contents;
+  gchar *token;
+  gchar *url;
+
+  g_return_val_if_fail (G_IS_FLICKR (f), NULL);
+  g_return_val_if_fail (frob, NULL);
+
+  api_sig = get_api_sig (f->priv->auth_secret,
+                         "method", "flickr.auth.getToken",
+                         "api_key", f->priv->api_key,
+                         "frob", frob,
+                         NULL);
+
+  /* Build url */
+  url = g_strdup_printf (FLICKR_AUTH_GETTOKEN,
+                         f->priv->api_key,
+                         api_sig,
+                         frob);
+  g_free (api_sig);
+
+  /* Load content */
+  vfs = g_vfs_get_default ();
+  uri = g_vfs_get_file_for_uri (vfs, url);
+  g_free (url);
+  if (!g_file_load_contents (uri, NULL, &contents, NULL, NULL, &error)) {
+    GRL_WARNING ("Unable to get Flickr's token: %s", error->message);
+    return NULL;
+  }
+
+  /* Get token */
+  token = get_xpath_element (contents, "/rsp/auth/token");
+  g_free (contents);
+  if (!token) {
+    GRL_WARNING ("Can not get Flickr's token");
+  }
+
+  return token;
+}
+
+void
+g_flickr_auth_checkToken (GFlickr *f,
+                          const gchar *token,
+                          GFlickrHashTableCb callback,
+                          gpointer user_data)
+{
+  gchar *api_sig;
+  gchar *request;
+
+  g_return_if_fail (G_IS_FLICKR (f));
+  g_return_if_fail (token);
+  g_return_if_fail (callback);
+
+  api_sig = get_api_sig (f->priv->auth_secret,
+                         "method", FLICKR_AUTH_CHECKTOKEN_METHOD,
+                         "api_key", f->priv->api_key,
+                         "auth_token", token,
+                         NULL);
+
+  /* Build request */
+  request  = g_strdup_printf (FLICKR_AUTH_CHECKTOKEN,
+                              f->priv->api_key,
+                              api_sig,
+                              token);
+  g_free (api_sig);
+
+  GFlickrData *gfd = g_slice_new (GFlickrData);
+  gfd->flickr = g_object_ref (f);
+  gfd->parse_xml = process_token_result;
+  gfd->hashtable_cb = callback;
+  gfd->user_data = user_data;
+
+  read_url_async (f, request, gfd);
+  g_free (request);
+}
diff --git a/src/media/flickr/gflickr.h b/src/media/flickr/gflickr.h
new file mode 100644
index 0000000..dd583d8
--- /dev/null
+++ b/src/media/flickr/gflickr.h
@@ -0,0 +1,155 @@
+/*
+ * Copyright (C) 2010 Igalia S.L.
+ *
+ * Contact: Iago Toral Quiroga <itoral igalia com>
+ *
+ * Authors: Juan A. Suarez Romero <jasuarez igalia 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 _G_FLICKR_H_
+#define _G_FLICKR_H_
+
+#include <glib-object.h>
+
+#define G_FLICKR_TYPE                           \
+  (g_flickr_get_type ())
+
+#define G_FLICKR(obj)                                   \
+  (G_TYPE_CHECK_INSTANCE_CAST ((obj),                   \
+                               G_FLICKR_TYPE,           \
+                               GFlickr))
+
+#define G_IS_FLICKR(obj)                                \
+  (G_TYPE_CHECK_INSTANCE_TYPE ((obj),                   \
+                               G_FLICKR_TYPE))
+
+#define G_FLICKR_CLASS(klass)                           \
+  (G_TYPE_CHECK_CLASS_CAST((klass),                     \
+                           G_FLICKR_TYPE,               \
+                           GFlickrClass))
+
+#define G_IS_FLICKR_CLASS(klass)                        \
+  (G_TYPE_CHECK_CLASS_TYPE((klass),                     \
+                           G_FLICKR_TYPE))
+
+#define G_FLICKR_GET_CLASS(obj)                         \
+  (G_TYPE_INSTANCE_GET_CLASS ((obj),                    \
+                              G_FLICKR_TYPE,            \
+                              GFlickrClass))
+
+typedef struct _GFlickr        GFlickr;
+typedef struct _GFlickrPrivate GFlickrPrivate;
+
+struct _GFlickr {
+
+  GObject parent;
+
+  /*< private >*/
+  GFlickrPrivate *priv;
+};
+
+typedef struct _GFlickrClass GFlickrClass;
+
+struct _GFlickrClass {
+
+  GObjectClass parent_class;
+
+};
+
+
+typedef void (*GFlickrHashTableCb) (GFlickr *f, GHashTable *result, gpointer user_data);
+
+typedef void (*GFlickrListCb) (GFlickr *f, GList *result, gpointer user_data);
+
+typedef void (*GFlickrCheckToken) (GFlickr *f, GHashTable *tokeninfo, gpointer user_data);
+
+GType g_flickr_get_type (void);
+
+GFlickr *g_flickr_new (const gchar *api_key, const gchar *auth_secret, const gchar *auth_token);
+
+void g_flickr_set_per_page (GFlickr *f, gint per_page);
+
+void
+g_flickr_photos_getInfo (GFlickr *f,
+                         glong photo_id,
+                         GFlickrHashTableCb callback,
+                         gpointer user_data);
+
+void
+g_flickr_photos_search (GFlickr *f,
+                        const gchar *user_id,
+                        const gchar *text,
+                        const gchar *tags,
+                        gint page,
+                        GFlickrListCb callback,
+                        gpointer user_data);
+
+void
+g_flickr_photos_getRecent (GFlickr *f,
+                           gint page,
+                           GFlickrListCb callback,
+                           gpointer user_data);
+
+gchar *
+g_flickr_photo_url_original (GFlickr *f, GHashTable *photo);
+
+gchar *
+g_flickr_photo_url_thumbnail (GFlickr *f, GHashTable *photo);
+
+gchar *
+g_flickr_photo_url_largest (GFlickr *f, GHashTable *photo);
+
+void
+g_flickr_tags_getHotList (GFlickr *f,
+                          gint count,
+                          GFlickrListCb callback,
+                          gpointer user_data);
+
+void
+g_flickr_photosets_getList (GFlickr *f,
+                            const gchar *user_id,
+                            GFlickrListCb callback,
+                            gpointer user_data);
+
+void
+g_flickr_photosets_getPhotos (GFlickr *f,
+                              const gchar *photoset_id,
+                              gint page,
+                              GFlickrListCb callback,
+                              gpointer user_data);
+
+gchar *
+g_flickr_auth_getFrob (GFlickr *f);
+
+gchar *
+g_flickr_auth_loginLink (GFlickr *f,
+                         const gchar *frob,
+                         const gchar *perm);
+
+gchar *
+g_flickr_auth_getToken (GFlickr *f,
+                        const gchar *frob);
+
+void
+g_flickr_auth_checkToken (GFlickr *f,
+                          const gchar *token,
+                          GFlickrHashTableCb callback,
+                          gpointer user_data);
+
+#endif /* _G_FLICKR_H_ */
diff --git a/src/media/flickr/grl-flickr.c b/src/media/flickr/grl-flickr.c
new file mode 100644
index 0000000..6065355
--- /dev/null
+++ b/src/media/flickr/grl-flickr.c
@@ -0,0 +1,755 @@
+/*
+ * Copyright (C) 2010 Igalia S.L.
+ * Copyright (C) 2011 Intel Corporation.
+ *
+ *
+ * Contact: Iago Toral Quiroga <itoral igalia com>
+ *
+ * Authors: Juan A. Suarez Romero <jasuarez igalia 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 <grilo.h>
+#include <string.h>
+#include <stdlib.h>
+
+#include "grl-flickr.h"
+#include "gflickr.h"
+
+#define GRL_FLICKR_SOURCE_GET_PRIVATE(object)                           \
+  (G_TYPE_INSTANCE_GET_PRIVATE((object),                                \
+                               GRL_FLICKR_SOURCE_TYPE,                  \
+                               GrlFlickrSourcePrivate))
+
+/* --------- Logging  -------- */
+
+#define GRL_LOG_DOMAIN_DEFAULT flickr_log_domain
+GRL_LOG_DOMAIN(flickr_log_domain);
+
+#define SEARCH_MAX  500
+#define HOTLIST_MAX 200
+
+/* --- Plugin information --- */
+
+#define PLUGIN_ID   FLICKR_PLUGIN_ID
+
+#define PUBLIC_SOURCE_ID   "grl-flickr"
+#define PUBLIC_SOURCE_NAME "Flickr"
+#define PUBLIC_SOURCE_DESC "A source for browsing and searching Flickr photos"
+
+#define PERSONAL_SOURCE_ID "grl-flickr-%s"
+#define PERSONAL_SOURCE_NAME "%s's Flickr"
+#define PERSONAL_SOURCE_DESC "A source for browsing and searching %s' flickr photos"
+
+#define AUTHOR      "Igalia S.L."
+#define LICENSE     "LGPL"
+#define SITE        "http://www.igalia.com";
+
+typedef struct {
+  GrlMediaSource *source;
+  GrlMediaSourceResultCb callback;
+  gchar *user_id;
+  gchar *tags;
+  gchar *text;
+  guint offset;
+  guint page;
+  gpointer user_data;
+  guint count;
+  guint operation_id;
+} OperationData;
+
+struct _GrlFlickrSourcePrivate {
+  GFlickr *flickr;
+  gchar *user_id;
+};
+
+static void token_info_cb (GFlickr *f,
+                           GHashTable *info,
+                           gpointer user_data);
+
+static GrlFlickrSource *grl_flickr_source_public_new (const gchar *flickr_api_key,
+                                                      const gchar *flickr_secret);
+
+static void grl_flickr_source_personal_new (const GrlPluginInfo *plugin,
+                                            const gchar *flickr_api_key,
+                                            const gchar *flickr_secret,
+                                            const gchar *flickr_token);
+
+static void grl_flickr_source_finalize (GObject *object);
+
+gboolean grl_flickr_plugin_init (GrlPluginRegistry *registry,
+				 const GrlPluginInfo *plugin,
+                                 GList *configs);
+
+
+static const GList *grl_flickr_source_supported_keys (GrlMetadataSource *source);
+
+static void grl_flickr_source_browse (GrlMediaSource *source,
+                                      GrlMediaSourceBrowseSpec *bs);
+
+static void grl_flickr_source_metadata (GrlMediaSource *source,
+                                        GrlMediaSourceMetadataSpec *ss);
+
+static void grl_flickr_source_search (GrlMediaSource *source,
+                                      GrlMediaSourceSearchSpec *ss);
+
+/* =================== Flickr Plugin  =============== */
+
+gboolean
+grl_flickr_plugin_init (GrlPluginRegistry *registry,
+                        const GrlPluginInfo *plugin,
+                        GList *configs)
+{
+  gchar *flickr_key;
+  gchar *flickr_secret;
+  gchar *flickr_token;
+  GrlConfig *config;
+  gboolean public_source_created = FALSE;
+
+  GRL_LOG_DOMAIN_INIT (flickr_log_domain, "flickr");
+
+  GRL_DEBUG ("flickr_plugin_init");
+
+  if (!configs) {
+    GRL_WARNING ("Missing configuration");
+    return FALSE;
+  }
+
+  while (configs) {
+    config = GRL_CONFIG (configs->data);
+
+    flickr_key = grl_config_get_api_key (config);
+    flickr_token = grl_config_get_api_token (config);
+    flickr_secret = grl_config_get_api_secret (config);
+
+    if (!flickr_key || !flickr_secret) {
+      GRL_WARNING ("Required configuration keys not set up");
+    } else if (flickr_token) {
+      grl_flickr_source_personal_new (plugin,
+                                      flickr_key,
+                                      flickr_secret,
+                                      flickr_token);
+    } else if (public_source_created) {
+      GRL_WARNING ("Only one public source can be created");
+    } else {
+      GrlFlickrSource *source = grl_flickr_source_public_new (flickr_key, flickr_secret);
+      public_source_created = TRUE;
+      grl_plugin_registry_register_source (registry,
+                                           plugin,
+                                           GRL_MEDIA_PLUGIN (source),
+                                           NULL);
+    }
+
+    if (flickr_key != NULL)
+      g_free (flickr_key);
+    if (flickr_token != NULL)
+      g_free (flickr_token);
+    if (flickr_secret != NULL)
+      g_free (flickr_secret);
+
+    configs = g_list_next (configs);
+  }
+
+  return TRUE;
+}
+
+GRL_PLUGIN_REGISTER (grl_flickr_plugin_init,
+                     NULL,
+                     PLUGIN_ID);
+
+/* ================== Flickr GObject ================ */
+
+G_DEFINE_TYPE (GrlFlickrSource, grl_flickr_source, GRL_TYPE_MEDIA_SOURCE);
+
+static GrlFlickrSource *
+grl_flickr_source_public_new (const gchar *flickr_api_key,
+                              const gchar *flickr_secret)
+{
+  GrlFlickrSource *source;
+
+  GRL_DEBUG ("grl_flickr_source_new");
+
+  source = g_object_new (GRL_FLICKR_SOURCE_TYPE,
+                         "source-id", PUBLIC_SOURCE_ID,
+                         "source-name", PUBLIC_SOURCE_NAME,
+                         "source-desc", PUBLIC_SOURCE_DESC,
+                         NULL);
+  source->priv->flickr = g_flickr_new (flickr_api_key, flickr_secret, NULL);
+
+  return source;
+}
+
+static void
+grl_flickr_source_personal_new (const GrlPluginInfo *plugin,
+                                const gchar *flickr_api_key,
+                                const gchar *flickr_secret,
+                                const gchar *flickr_token)
+{
+  GFlickr *f;
+
+  f = g_flickr_new (flickr_api_key, flickr_secret, flickr_token);
+  g_flickr_auth_checkToken (f, flickr_token, token_info_cb, (gpointer) plugin);
+}
+
+static void
+grl_flickr_source_class_init (GrlFlickrSourceClass * klass)
+{
+  GObjectClass *gobject_class = G_OBJECT_CLASS (klass);
+  GrlMediaSourceClass *source_class = GRL_MEDIA_SOURCE_CLASS (klass);
+  GrlMetadataSourceClass *metadata_class = GRL_METADATA_SOURCE_CLASS (klass);
+
+  gobject_class->finalize = grl_flickr_source_finalize;
+  source_class->browse = grl_flickr_source_browse;
+  source_class->metadata = grl_flickr_source_metadata;
+  source_class->search = grl_flickr_source_search;
+  metadata_class->supported_keys = grl_flickr_source_supported_keys;
+
+  g_type_class_add_private (klass, sizeof (GrlFlickrSourcePrivate));
+}
+
+static void
+grl_flickr_source_init (GrlFlickrSource *source)
+{
+  source->priv = GRL_FLICKR_SOURCE_GET_PRIVATE (source);
+
+  grl_media_source_set_auto_split_threshold (GRL_MEDIA_SOURCE (source),
+                                             SEARCH_MAX);
+}
+
+static void
+grl_flickr_source_finalize (GObject *object)
+{
+  GrlFlickrSource *source;
+
+  GRL_DEBUG ("grl_flickr_source_finalize");
+
+  source = GRL_FLICKR_SOURCE (object);
+  g_free (source->priv->user_id);
+
+  G_OBJECT_CLASS (grl_flickr_source_parent_class)->finalize (object);
+}
+
+/* ======================= Utilities ==================== */
+
+static void
+token_info_cb (GFlickr *f,
+               GHashTable *info,
+               gpointer user_data)
+{
+  GrlFlickrSource *source;
+  GrlPluginInfo *plugin = (GrlPluginInfo *) user_data;
+  GrlPluginRegistry *registry;
+  gchar *fullname;
+  gchar *source_desc;
+  gchar *source_id;
+  gchar *source_name;
+  gchar *username;
+
+  if (!info) {
+    GRL_WARNING ("Wrong token!");
+    g_object_unref (f);
+    return;
+  }
+
+  registry = grl_plugin_registry_get_default ();
+
+  username = g_hash_table_lookup (info, "user_username");
+  fullname = g_hash_table_lookup (info, "user_fullname");
+
+  source_id = g_strdup_printf (PERSONAL_SOURCE_ID, username);
+  source_name = g_strdup_printf (PERSONAL_SOURCE_NAME, fullname);
+  source_desc = g_strdup_printf (PERSONAL_SOURCE_DESC, fullname);
+
+  /* Check if source is already registered */
+  if (grl_plugin_registry_lookup_source (registry, source_id)) {
+    GRL_DEBUG ("A source with id '%s' is already registered. Skipping...",
+               source_id);
+    g_object_unref (f);
+  } else {
+    source = g_object_new (GRL_FLICKR_SOURCE_TYPE,
+                           "source-id", source_id,
+                           "source-name", source_name,
+                           "source-desc", source_desc,
+                           NULL);
+    source->priv->flickr = f;
+    source->priv->user_id = g_strdup (g_hash_table_lookup (info, "user_nsid"));
+    grl_plugin_registry_register_source (registry,
+                                         plugin,
+                                         GRL_MEDIA_PLUGIN (source),
+                                         NULL);
+  }
+
+  g_free (source_id);
+  g_free (source_name);
+  g_free (source_desc);
+}
+
+static void
+update_media (GrlMedia *media, GHashTable *photo)
+{
+  gchar *author;
+  gchar *date;
+  gchar *description;
+  gchar *id;
+  gchar *thumbnail;
+  gchar *title;
+  gchar *url;
+
+  author = g_hash_table_lookup (photo, "owner_realname");
+  if (!author) {
+    author = g_hash_table_lookup (photo, "photo_ownername");
+  }
+  date = g_hash_table_lookup (photo, "dates_taken");
+  if (!date) {
+    date = g_hash_table_lookup (photo, "photo_datetaken");
+  }
+  description = g_hash_table_lookup (photo, "description");
+  id = g_hash_table_lookup (photo, "photo_id");
+  thumbnail = g_strdup (g_hash_table_lookup (photo, "photo_url_t"));
+  if (!thumbnail) {
+    thumbnail = g_flickr_photo_url_thumbnail (NULL, photo);
+  }
+  title = g_hash_table_lookup (photo, "title");
+  if (!title) {
+    title = g_hash_table_lookup (photo, "photo_title");
+  }
+  url = g_strdup (g_hash_table_lookup (photo, "photo_url_o"));
+  if (!url) {
+    url = g_flickr_photo_url_original (NULL, photo);
+    if (!url) {
+      url = g_flickr_photo_url_largest (NULL, photo);
+    }
+  }
+
+  if (author) {
+    grl_media_set_author (media, author);
+  }
+
+  if (date) {
+    grl_media_set_date (media, date);
+  }
+
+  if (description && description[0] != '\0') {
+    grl_media_set_description (media, description);
+  }
+
+  if (id) {
+    grl_media_set_id (media, id);
+  }
+
+  if (thumbnail) {
+    grl_media_set_thumbnail (media, thumbnail);
+    g_free (thumbnail);
+  }
+
+  if (title && title[0] != '\0') {
+    grl_media_set_title (media, title);
+  }
+
+  if (url) {
+    grl_media_set_url (media, url);
+    g_free (url);
+  }
+}
+
+static void
+getInfo_cb (GFlickr *f, GHashTable *photo, gpointer user_data)
+{
+  GrlMediaSourceMetadataSpec *ms = (GrlMediaSourceMetadataSpec *) user_data;
+
+  if (photo) {
+    update_media (ms->media, photo);
+  }
+
+  ms->callback (ms->source, ms->media, ms->user_data, NULL);
+}
+
+static void
+search_cb (GFlickr *f, GList *photolist, gpointer user_data)
+{
+  GrlMedia *media;
+  OperationData *od = (OperationData *) user_data;
+  gchar *media_type;
+
+  /* Go to offset element */
+  photolist = g_list_nth (photolist, od->offset);
+
+  /* No more elements can be sent */
+  if (!photolist) {
+    od->callback (od->source,
+                  od->operation_id,
+                  NULL,
+                  0,
+                  od->user_data,
+                  NULL);
+    g_slice_free (OperationData, od);
+    return;
+  }
+
+  while (photolist && od->count) {
+    media_type = g_hash_table_lookup (photolist->data, "photo_media");
+    if (strcmp (media_type, "photo") == 0) {
+      media = grl_media_image_new ();
+    } else {
+      media = grl_media_video_new ();
+    }
+    update_media (media, photolist->data);
+    od->callback (od->source,
+                  od->operation_id,
+                  media,
+                  od->count == 1? 0: -1,
+                  od->user_data,
+                  NULL);
+    photolist = g_list_next (photolist);
+    od->count--;
+  }
+
+  /* Get more elements */
+  if (od->count) {
+    od->offset = 0;
+    od->page++;
+    g_flickr_photos_search (f,
+                            od->user_id,
+                            od->text,
+                            od->tags,
+                            od->page,
+                            search_cb,
+                            od);
+  } else {
+    g_slice_free (OperationData, od);
+  }
+}
+
+static void
+photosetslist_cb (GFlickr *f, GList *photosets, gpointer user_data)
+{
+  GrlMedia *media;
+  GrlMediaSourceBrowseSpec *bs = (GrlMediaSourceBrowseSpec *) user_data;
+  gchar *value;
+  gint count;
+
+  /* Go to offset element */
+  photosets = g_list_nth (photosets, bs->skip);
+
+  /* No more elements can be sent */
+  if (!photosets) {
+    bs->callback (bs->source,
+                  bs->browse_id,
+                  NULL,
+                  0,
+                  bs->user_data,
+                  NULL);
+    return;
+  }
+
+  /* Send data */
+  count = g_list_length (photosets);
+  if (count > bs->count) {
+    count = bs->count;
+  }
+
+  while (photosets && count > 0) {
+    count--;
+    media = grl_media_box_new ();
+    grl_media_set_id (media,
+                      g_hash_table_lookup (photosets->data,
+                                           "photoset_id"));
+    value = g_hash_table_lookup (photosets->data, "title");
+    if (value && value[0] != '\0') {
+      grl_media_set_title (media, value);
+    }
+    value = g_hash_table_lookup (photosets->data, "description");
+    if (value && value[0] != '\0') {
+      grl_media_set_description (media, value);
+    }
+
+    bs->callback (bs->source,
+                  bs->browse_id,
+                  media,
+                  count,
+                  bs->user_data,
+                  NULL);
+    photosets = g_list_next (photosets);
+  }
+}
+
+static void
+photosetsphotos_cb (GFlickr *f, GList *photolist, gpointer user_data)
+{
+  GrlMedia *media;
+  OperationData *od = (OperationData *) user_data;
+  gchar *media_type;
+
+  /* Go to offset element */
+  photolist = g_list_nth (photolist, od->offset);
+
+  /* No more elements can be sent */
+  if (!photolist) {
+    od->callback (od->source,
+                  od->operation_id,
+                  NULL,
+                  0,
+                  od->user_data,
+                  NULL);
+    return;
+  }
+
+  while (photolist && od->count) {
+    media_type = g_hash_table_lookup (photolist->data, "photo_media");
+    if (strcmp (media_type, "photo") == 0) {
+      media = grl_media_image_new ();
+    } else {
+      media = grl_media_video_new ();
+    }
+
+    update_media (media, photolist->data);
+    od->callback (od->source,
+                  od->operation_id,
+                  media,
+                  od->count == 1? 0: -1,
+                  od->user_data,
+                  NULL);
+    photolist = g_list_next (photolist);
+    od->count--;
+  }
+
+  /* Get more elements */
+  if (od->count) {
+    od->offset = 0;
+    od->page++;
+    g_flickr_photosets_getPhotos (f, od->text, od->page, photosetsphotos_cb, od);
+  } else {
+    g_slice_free (OperationData, od);
+  }
+}
+
+static void
+gettags_cb (GFlickr *f, GList *taglist, gpointer user_data)
+{
+  GrlMedia *media;
+  GrlMediaSourceBrowseSpec *bs = (GrlMediaSourceBrowseSpec *) user_data;
+  gint count;
+
+  /* Go to offset element */
+  taglist = g_list_nth (taglist, bs->skip);
+
+  /* No more elements can be sent */
+  if (!taglist) {
+    bs->callback (bs->source,
+                  bs->browse_id,
+                  NULL,
+                  0,
+                  bs->user_data,
+                  NULL);
+    return;
+  }
+
+  /* Send data */
+  count = g_list_length (taglist);
+  while (taglist) {
+    count--;
+    media = grl_media_box_new ();
+    grl_media_set_id (media, taglist->data);
+    grl_media_set_title (media, taglist->data);
+    bs->callback (bs->source,
+                  bs->browse_id,
+                  media,
+                  count,
+                  bs->user_data,
+                  NULL);
+    taglist = g_list_next (taglist);
+  }
+}
+
+/* ================== API Implementation ================ */
+
+static const GList *
+grl_flickr_source_supported_keys (GrlMetadataSource *source)
+{
+  static GList *keys = NULL;
+  if (!keys) {
+    keys = grl_metadata_key_list_new (GRL_METADATA_KEY_AUTHOR,
+                                      GRL_METADATA_KEY_DATE,
+                                      GRL_METADATA_KEY_DESCRIPTION,
+                                      GRL_METADATA_KEY_ID,
+                                      GRL_METADATA_KEY_THUMBNAIL,
+                                      GRL_METADATA_KEY_TITLE,
+                                      GRL_METADATA_KEY_URL,
+                                      NULL);
+  }
+  return keys;
+}
+
+static void
+grl_flickr_source_public_browse (GrlMediaSource *source,
+                                 GrlMediaSourceBrowseSpec *bs)
+{
+  GFlickr *f = GRL_FLICKR_SOURCE (source)->priv->flickr;
+  const gchar *container_id;
+  guint per_page;
+  gint request_size;
+
+  container_id = grl_media_get_id (bs->container);
+
+  if (!container_id) {
+    /* Get hot tags list. List is limited up to HOTLIST_MAX tags */
+    if (bs->skip >= HOTLIST_MAX) {
+      bs->callback (bs->source, bs->browse_id, NULL, 0, bs->user_data, NULL);
+    } else {
+      request_size = CLAMP (bs->skip + bs->count, 1, HOTLIST_MAX);
+      g_flickr_tags_getHotList (f, request_size, gettags_cb, bs);
+    }
+  } else {
+    OperationData *od = g_slice_new (OperationData);
+
+    grl_paging_translate (bs->skip,
+                          bs->count,
+                          SEARCH_MAX,
+                          &per_page,
+                          &(od->page),
+                          &(od->offset));
+    g_flickr_set_per_page (f, per_page);
+
+    od->source = bs->source;
+    od->callback = bs->callback;
+    od->user_id = GRL_FLICKR_SOURCE (source)->priv->user_id;
+    od->tags = (gchar *) container_id;
+    od->text = NULL;
+    od->user_data = bs->user_data;
+    od->count = bs->count;
+    od->operation_id = bs->browse_id;
+    g_flickr_photos_search (f,
+                            od->user_id,
+                            NULL,
+                            od->tags,
+                            od->page,
+                            search_cb,
+                            od);
+  }
+}
+
+static void
+grl_flickr_source_personal_browse (GrlMediaSource *source,
+                                   GrlMediaSourceBrowseSpec *bs)
+{
+  GFlickr *f = GRL_FLICKR_SOURCE (source)->priv->flickr;
+  OperationData *od;
+  const gchar *container_id;
+  guint per_page;
+
+  container_id = grl_media_get_id (bs->container);
+
+  if (!container_id) {
+    /* Get photoset */
+    g_flickr_photosets_getList (f, NULL, photosetslist_cb, bs);
+  } else {
+    od = g_slice_new (OperationData);
+
+    /* Compute items per page and page offset */
+    grl_paging_translate (bs->skip,
+                          bs->count,
+                          SEARCH_MAX,
+                          &per_page,
+                          &(od->page),
+                          &(od->offset));
+    g_flickr_set_per_page (f, per_page);
+    od->source = bs->source;
+    od->callback = bs->callback;
+    od->tags = NULL;
+    od->text = (gchar *) container_id;
+    od->user_data = bs->user_data;
+    od->count = bs->count;
+    od->operation_id = bs->browse_id;
+
+    g_flickr_photosets_getPhotos (f, container_id, od->page, photosetsphotos_cb, od);
+  }
+}
+
+static void
+grl_flickr_source_browse (GrlMediaSource *source,
+                          GrlMediaSourceBrowseSpec *bs)
+{
+  if (GRL_FLICKR_SOURCE (source)->priv->user_id) {
+    grl_flickr_source_personal_browse (source, bs);
+  } else {
+    grl_flickr_source_public_browse (source, bs);
+  }
+}
+
+static void
+grl_flickr_source_metadata (GrlMediaSource *source,
+                            GrlMediaSourceMetadataSpec *ms)
+{
+  const gchar *id;
+
+  if (!ms->media || (id = grl_media_get_id (ms->media)) == NULL) {
+    ms->callback (ms->source, ms->media, ms->user_data, NULL);
+    return;
+  }
+
+  g_flickr_photos_getInfo (GRL_FLICKR_SOURCE (source)->priv->flickr,
+                           atol (id),
+                           getInfo_cb,
+                           ms);
+}
+
+static void
+grl_flickr_source_search (GrlMediaSource *source,
+                          GrlMediaSourceSearchSpec *ss)
+{
+  GFlickr *f = GRL_FLICKR_SOURCE (source)->priv->flickr;
+  guint per_page;
+  OperationData *od = g_slice_new (OperationData);
+
+  /* Compute items per page and page offset */
+  grl_paging_translate (ss->skip,
+                        ss->count,
+                        SEARCH_MAX,
+                        &per_page,
+                        &(od->page),
+                        &(od->offset));
+  g_flickr_set_per_page (f, per_page);
+
+  od->source = ss->source;
+  od->callback = ss->callback;
+  od->user_id = GRL_FLICKR_SOURCE (source)->priv->user_id;
+  od->tags = NULL;
+  od->text = ss->text;
+  od->user_data = ss->user_data;
+  od->count = ss->count;
+  od->operation_id = ss->search_id;
+
+  if (od->user_id || od->text) {
+    g_flickr_photos_search (f,
+                            od->user_id,
+                            ss->text,
+                            NULL,
+                            od->page,
+                            search_cb,
+                            od);
+  } else {
+    g_flickr_photos_getRecent (f,
+                               od->page,
+                               search_cb,
+                               od);
+  }
+}
diff --git a/src/media/flickr/grl-flickr.h b/src/media/flickr/grl-flickr.h
new file mode 100644
index 0000000..af1a843
--- /dev/null
+++ b/src/media/flickr/grl-flickr.h
@@ -0,0 +1,80 @@
+/*
+ * Copyright (C) 2010 Igalia S.L.
+ *
+ * Contact: Iago Toral Quiroga <itoral igalia com>
+ *
+ * Authors: Juan A. Suarez Romero <jasuarez igalia 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_FLICKR_SOURCE_H_
+#define _GRL_FLICKR_SOURCE_H_
+
+#include <grilo.h>
+
+#define GRL_FLICKR_SOURCE_TYPE                  \
+  (grl_flickr_source_get_type ())
+
+#define GRL_FLICKR_SOURCE(obj)                          \
+  (G_TYPE_CHECK_INSTANCE_CAST ((obj),                   \
+                               GRL_FLICKR_SOURCE_TYPE,  \
+                               GrlFlickrSource))
+
+#define GRL_IS_FLICKR_SOURCE(obj)                       \
+  (G_TYPE_CHECK_INSTANCE_TYPE ((obj),                   \
+                               GRL_FLICKR_SOURCE_TYPE))
+
+#define GRL_FLICKR_SOURCE_CLASS(klass)                  \
+  (G_TYPE_CHECK_CLASS_CAST((klass),                     \
+                           GRL_FLICKR_SOURCE_TYPE,      \
+                           GrlFlickrSourceClass))
+
+#define GRL_IS_FLICKR_SOURCE_CLASS(klass)               \
+  (G_TYPE_CHECK_CLASS_TYPE((klass),                     \
+                           GRL_FLICKR_SOURCE_TYPE))
+
+#define GRL_FLICKR_SOURCE_GET_CLASS(obj)                \
+  (G_TYPE_INSTANCE_GET_CLASS ((obj),                    \
+                              GRL_FLICKR_SOURCE_TYPE,   \
+                              GrlFlickrSourceClass))
+
+/* plugin's log domain */
+GRL_LOG_DOMAIN_EXTERN(flickr_log_domain);
+
+typedef struct _GrlFlickrSource        GrlFlickrSource;
+typedef struct _GrlFlickrSourcePrivate GrlFlickrSourcePrivate;
+
+struct _GrlFlickrSource {
+
+  GrlMediaSource parent;
+
+  /*< private >*/
+  GrlFlickrSourcePrivate *priv;
+};
+
+typedef struct _GrlFlickrSourceClass GrlFlickrSourceClass;
+
+struct _GrlFlickrSourceClass {
+
+  GrlMediaSourceClass parent_class;
+
+};
+
+GType grl_flickr_source_get_type (void);
+
+#endif /* _GRL_FLICKR_SOURCE_H_ */
diff --git a/src/media/flickr/grl-flickr.xml b/src/media/flickr/grl-flickr.xml
new file mode 100644
index 0000000..4b47f05
--- /dev/null
+++ b/src/media/flickr/grl-flickr.xml
@@ -0,0 +1,9 @@
+<plugin>
+  <info>
+    <name>Flickr</name>
+    <description>A plugin for browsing and searching Flickr photos</description>
+    <author>Igalia S.L.</author>
+    <license>LGPL</license>
+    <site>http://www.igalia.com</site>
+  </info>
+</plugin>
diff --git a/src/media/jamendo/Makefile.am b/src/media/jamendo/Makefile.am
new file mode 100644
index 0000000..3232ac2
--- /dev/null
+++ b/src/media/jamendo/Makefile.am
@@ -0,0 +1,38 @@
+#
+# Makefile.am
+#
+# Author: Juan A. Suarez Romero <jasuarez igalia com>
+#
+# Copyright (C) 2010, 2011 Igalia S.L. All rights reserved.
+
+lib_LTLIBRARIES		 = libgrljamendo.la
+
+libgrljamendo_la_CFLAGS =	\
+	$(DEPS_CFLAGS)		\
+	$(GRLNET_CFLAGS)	\
+	$(XML_CFLAGS)
+
+libgrljamendo_la_LIBADD =	\
+	$(DEPS_LIBS)		\
+	$(GRLNET_LIBS)		\
+	$(XML_LIBS)
+
+libgrljamendo_la_LDFLAGS = \
+	-module		   \
+	-avoid-version
+
+libgrljamendo_la_SOURCES = 	\
+	grl-jamendo.c 		\
+	grl-jamendo.h
+
+libdir = $(GRL_PLUGINS_DIR)
+jamendoxmldir = $(GRL_PLUGINS_CONF_DIR)
+jamendoxml_DATA	= $(JAMENDO_PLUGIN_ID).xml
+
+EXTRA_DIST = $(jamendoxml_DATA)
+
+MAINTAINERCLEANFILES =	\
+	*.in		\
+	*~
+
+DISTCLEANFILES = $(MAINTAINERCLEANFILES)
diff --git a/src/media/jamendo/TODO b/src/media/jamendo/TODO
new file mode 100644
index 0000000..2571947
--- /dev/null
+++ b/src/media/jamendo/TODO
@@ -0,0 +1,30 @@
+Get global database and work locally
+====================================
+Actually all queries are on-line. But the whole database can be retrieved and
+stored in local, so all operation can be performed off-line.
+ 
+Doing it requires to implement the search in local. We should explore if either
+moving to this approach or even creating a new derivative plugin that follows
+this off-line approach worths it.
+
+
+Implement childcount
+====================
+Implementing childcount would require, for each result doing another search and
+retrieve all children, counting them.
+
+
+Limit search-rate
+=================
+Jamendo terms of use states that applications should be limited to 1 query per
+second. Some kind of control should be needed in the plugin.
+
+
+Limit requested elements
+========================
+User can request any arbitrary number of elements, and this request is propagated to Jamendo.
+
+Nevertheless, it would be better to break the request in smaller chunks, so
+though user requests, for instance, 1000 albums, we do the query in steps of 25
+elements.
+
diff --git a/src/media/jamendo/grl-jamendo.c b/src/media/jamendo/grl-jamendo.c
new file mode 100644
index 0000000..f6ba5c6
--- /dev/null
+++ b/src/media/jamendo/grl-jamendo.c
@@ -0,0 +1,1365 @@
+/*
+ * Copyright (C) 2010 Igalia S.L.
+ *
+ * Contact: Iago Toral Quiroga <itoral igalia com>
+ *
+ * Authors: Juan A. Suarez Romero <jasuarez igalia 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 <grilo.h>
+#include <net/grl-net.h>
+#include <libxml/parser.h>
+#include <libxml/xmlmemory.h>
+#include <string.h>
+
+#include "grl-jamendo.h"
+
+/* --------- Logging  -------- */
+
+#define GRL_LOG_DOMAIN_DEFAULT jamendo_log_domain
+GRL_LOG_DOMAIN_STATIC(jamendo_log_domain);
+
+#define JAMENDO_ID_SEP    "/"
+#define JAMENDO_ROOT_NAME "Jamendo"
+
+#define MAX_ELEMENTS 100
+
+/* ------- Categories ------- */
+
+#define JAMENDO_ARTIST "artist"
+#define JAMENDO_ALBUM  "album"
+#define JAMENDO_TRACK  "track"
+
+/* ---- Jamendo Web API  ---- */
+
+#define JAMENDO_BASE_ENTRY "http://api.jamendo.com/get2";
+#define JAMENDO_FORMAT     "xml"
+#define JAMENDO_RANGE      "n=%u&pn=%u"
+
+#define JAMENDO_ARTIST_ENTRY JAMENDO_BASE_ENTRY "/%s/" JAMENDO_ARTIST "/" JAMENDO_FORMAT
+
+#define JAMENDO_ALBUM_ENTRY  JAMENDO_BASE_ENTRY "/%s/" JAMENDO_ALBUM  "/" JAMENDO_FORMAT \
+  "/" JAMENDO_ALBUM "_" JAMENDO_ARTIST
+
+#define JAMENDO_TRACK_ENTRY  JAMENDO_BASE_ENTRY "/%s/" JAMENDO_TRACK  "/" JAMENDO_FORMAT \
+  "/" JAMENDO_ALBUM "_" JAMENDO_ARTIST "+" JAMENDO_TRACK "_" JAMENDO_ALBUM
+
+#define JAMENDO_GET_ARTISTS JAMENDO_ARTIST_ENTRY "/?" JAMENDO_RANGE
+#define JAMENDO_GET_ALBUMS  JAMENDO_ALBUM_ENTRY  "/?" JAMENDO_RANGE
+#define JAMENDO_GET_TRACKS  JAMENDO_TRACK_ENTRY  "/?" JAMENDO_RANGE
+
+#define JAMENDO_GET_ALBUMS_FROM_ARTIST JAMENDO_ALBUM_ENTRY "/?" JAMENDO_RANGE "&artist_id=%s"
+#define JAMENDO_GET_TRACKS_FROM_ALBUM JAMENDO_TRACK_ENTRY  "/?" JAMENDO_RANGE "&album_id=%s"
+#define JAMENDO_GET_ARTIST JAMENDO_ARTIST_ENTRY "/?id=%s"
+
+#define JAMENDO_GET_ALBUM  JAMENDO_ALBUM_ENTRY  "/?id=%s"
+#define JAMENDO_GET_TRACK  JAMENDO_TRACK_ENTRY  "/?id=%s"
+
+#define JAMENDO_SEARCH_ARTIST JAMENDO_ARTIST_ENTRY "/?" JAMENDO_RANGE "&searchquery=%s"
+#define JAMENDO_SEARCH_ALBUM  JAMENDO_ALBUM_ENTRY  "/?" JAMENDO_RANGE "&searchquery=%s"
+#define JAMENDO_SEARCH_TRACK  JAMENDO_TRACK_ENTRY  "/?" JAMENDO_RANGE "&searchquery=%s"
+#define JAMENDO_SEARCH_ALL    JAMENDO_TRACK_ENTRY  "/?" JAMENDO_RANGE
+
+/* --- Plugin information --- */
+
+#define PLUGIN_ID   JAMENDO_PLUGIN_ID
+
+#define SOURCE_ID   "grl-jamendo"
+#define SOURCE_NAME "Jamendo"
+#define SOURCE_DESC "A source for browsing and searching Jamendo videos"
+
+#define AUTHOR      "Igalia S.L."
+#define LICENSE     "LGPL"
+#define SITE        "http://www.igalia.com";
+
+enum {
+  METADATA,
+  BROWSE,
+  QUERY,
+  SEARCH
+};
+
+typedef enum {
+  JAMENDO_ARTIST_CAT = 1,
+  JAMENDO_ALBUM_CAT,
+  JAMENDO_FEEDS_CAT,
+  JAMENDO_TRACK_CAT,
+} JamendoCategory;
+
+typedef struct {
+  JamendoCategory category;
+  gchar *id;
+  gchar *artist_name;
+  gchar *artist_genre;
+  gchar *artist_url;
+  gchar *artist_image;
+  gchar *album_name;
+  gchar *album_genre;
+  gchar *album_url;
+  gchar *album_duration;
+  gchar *album_image;
+  gchar *track_name;
+  gchar *track_url;
+  gchar *track_stream;
+  gchar *track_duration;
+  gchar *feed_name;
+} Entry;
+
+typedef struct {
+  gint type;
+  union {
+    GrlMediaSourceBrowseSpec *bs;
+    GrlMediaSourceQuerySpec *qs;
+    GrlMediaSourceMetadataSpec *ms;
+    GrlMediaSourceSearchSpec *ss;
+  } spec;
+  xmlNodePtr node;
+  xmlDocPtr doc;
+  guint total_results;
+  guint index;
+  guint offset;
+  gboolean cancelled;
+} XmlParseEntries;
+
+struct Feeds {
+  gchar *name;
+  JamendoCategory cat;
+  gchar *url;
+} feeds[] = {
+  { "Albums of the week", JAMENDO_ALBUM_CAT,
+    JAMENDO_GET_ALBUMS "&order=ratingweek_desc", },
+  { "Tracks of the week", JAMENDO_TRACK_CAT,
+    JAMENDO_GET_TRACKS "&order=ratingweek_desc", },
+  { "New releases", JAMENDO_TRACK_CAT,
+    JAMENDO_GET_TRACKS "&order=releasedate_desc", },
+  { "Top artists", JAMENDO_ARTIST_CAT,
+    JAMENDO_GET_ARTISTS "&order=rating_desc", },
+  { "Top albums", JAMENDO_ALBUM_CAT,
+    JAMENDO_GET_ALBUMS "&order=rating_desc", },
+  { "Top tracks", JAMENDO_TRACK_CAT,
+    JAMENDO_GET_TRACKS "&order=rating_desc", },
+};
+
+struct _GrlJamendoSourcePriv {
+  GrlNetWc *wc;
+  GCancellable *cancellable;
+};
+
+#define GRL_JAMENDO_SOURCE_GET_PRIVATE(object)		\
+  (G_TYPE_INSTANCE_GET_PRIVATE((object),                \
+                               GRL_JAMENDO_SOURCE_TYPE,	\
+                               GrlJamendoSourcePriv))
+
+static GrlJamendoSource *grl_jamendo_source_new (void);
+
+gboolean grl_jamendo_plugin_init (GrlPluginRegistry *registry,
+                                  const GrlPluginInfo *plugin,
+                                  GList *configs);
+
+static const GList *grl_jamendo_source_supported_keys (GrlMetadataSource *source);
+
+static void grl_jamendo_source_metadata (GrlMediaSource *source,
+                                         GrlMediaSourceMetadataSpec *ms);
+
+static void grl_jamendo_source_browse (GrlMediaSource *source,
+                                       GrlMediaSourceBrowseSpec *bs);
+
+static void grl_jamendo_source_query (GrlMediaSource *source,
+                                      GrlMediaSourceQuerySpec *qs);
+
+static void grl_jamendo_source_search (GrlMediaSource *source,
+                                       GrlMediaSourceSearchSpec *ss);
+
+static void grl_jamendo_source_cancel (GrlMediaSource *source,
+                                       guint operation_id);
+
+/* =================== Jamendo Plugin  =============== */
+
+gboolean
+grl_jamendo_plugin_init (GrlPluginRegistry *registry,
+                         const GrlPluginInfo *plugin,
+                         GList *configs)
+{
+  GRL_LOG_DOMAIN_INIT (jamendo_log_domain, "jamendo");
+
+  GRL_DEBUG ("jamendo_plugin_init");
+
+  GrlJamendoSource *source = grl_jamendo_source_new ();
+  grl_plugin_registry_register_source (registry,
+                                       plugin,
+                                       GRL_MEDIA_PLUGIN (source),
+                                       NULL);
+  return TRUE;
+}
+
+GRL_PLUGIN_REGISTER (grl_jamendo_plugin_init,
+                     NULL,
+                     PLUGIN_ID);
+
+/* ================== Jamendo GObject ================ */
+
+static GrlJamendoSource *
+grl_jamendo_source_new (void)
+{
+  GRL_DEBUG ("grl_jamendo_source_new");
+  return g_object_new (GRL_JAMENDO_SOURCE_TYPE,
+		       "source-id", SOURCE_ID,
+		       "source-name", SOURCE_NAME,
+		       "source-desc", SOURCE_DESC,
+		       NULL);
+}
+
+G_DEFINE_TYPE (GrlJamendoSource, grl_jamendo_source, GRL_TYPE_MEDIA_SOURCE);
+
+static void
+grl_jamendo_source_finalize (GObject *object)
+{
+  GrlJamendoSource *self;
+
+  self = GRL_JAMENDO_SOURCE (object);
+  if (self->priv->wc)
+    g_object_unref (self->priv->wc);
+
+  if (self->priv->cancellable
+      && G_IS_CANCELLABLE (self->priv->cancellable))
+    g_object_unref (self->priv->cancellable);
+
+  G_OBJECT_CLASS (grl_jamendo_source_parent_class)->finalize (object);
+}
+
+static void
+grl_jamendo_source_class_init (GrlJamendoSourceClass * klass)
+{
+  GrlMediaSourceClass *source_class = GRL_MEDIA_SOURCE_CLASS (klass);
+  GrlMetadataSourceClass *metadata_class = GRL_METADATA_SOURCE_CLASS (klass);
+  GObjectClass *g_class = G_OBJECT_CLASS (klass);
+  source_class->metadata = grl_jamendo_source_metadata;
+  source_class->browse = grl_jamendo_source_browse;
+  source_class->query = grl_jamendo_source_query;
+  source_class->search = grl_jamendo_source_search;
+  source_class->cancel = grl_jamendo_source_cancel;
+  metadata_class->supported_keys = grl_jamendo_source_supported_keys;
+  g_class->finalize = grl_jamendo_source_finalize;
+
+  g_type_class_add_private (klass, sizeof (GrlJamendoSourcePriv));
+}
+
+static void
+grl_jamendo_source_init (GrlJamendoSource *source)
+{
+  source->priv = GRL_JAMENDO_SOURCE_GET_PRIVATE (source);
+
+  /* If we try to get too much elements in a single step, Jamendo might return
+     nothing. So limit the maximum amount of elements in each query */
+  grl_media_source_set_auto_split_threshold (GRL_MEDIA_SOURCE (source),
+                                             MAX_ELEMENTS);
+}
+
+/* ======================= Utilities ==================== */
+
+#if 0
+static void
+print_entry (Entry *entry)
+{
+  g_print ("Entry Information:\n");
+  g_print ("            ID: %s\n", entry->id);
+  g_print ("   Artist Name: %s\n", entry->artist_name);
+  g_print ("  Artist Genre: %s\n", entry->artist_genre);
+  g_print ("    Artist URL: %s\n", entry->artist_url);
+  g_print ("  Artist Image: %s\n", entry->artist_image);
+  g_print ("    Album Name: %s\n", entry->album_name);
+  g_print ("   Album Genre: %s\n", entry->album_genre);
+  g_print ("     Album URL: %s\n", entry->album_url);
+  g_print ("Album Duration: %s\n", entry->album_duration);
+  g_print ("   Album Image: %s\n", entry->album_image);
+  g_print ("    Track Name: %s\n", entry->track_name);
+  g_print ("     Track URL: %s\n", entry->track_url);
+  g_print ("  Track Stream: %s\n", entry->track_stream);
+  g_print ("Track Duration: %s\n", entry->track_duration);
+  g_print ("     Feed Name: %s\n", entry->feed_name);
+}
+#endif
+
+static void
+free_entry (Entry *entry)
+{
+  g_free (entry->id);
+  g_free (entry->artist_name);
+  g_free (entry->artist_genre);
+  g_free (entry->artist_url);
+  g_free (entry->artist_image);
+  g_free (entry->album_name);
+  g_free (entry->album_genre);
+  g_free (entry->album_url);
+  g_free (entry->album_duration);
+  g_free (entry->album_image);
+  g_free (entry->track_name);
+  g_free (entry->track_url);
+  g_free (entry->track_stream);
+  g_free (entry->track_duration);
+  g_free (entry->feed_name);
+  g_slice_free (Entry, entry);
+}
+
+static gint
+xml_count_children (xmlNodePtr node)
+{
+#if (LIBXML2_VERSION >= 20700)
+  return xmlChildElementCount (node);
+#else
+  gint nchildren = 0;
+  xmlNodePtr i = node->xmlChildrenNode;
+
+  while (i) {
+    nchildren++;
+    i = i->next;
+  }
+
+  return nchildren;
+#endif
+}
+
+static void
+xml_parse_result (const gchar *str, GError **error, XmlParseEntries *xpe)
+{
+  xmlDocPtr doc;
+  xmlNodePtr node;
+  gint child_nodes = 0;
+
+  doc = xmlReadMemory (str, strlen (str), NULL, NULL,
+                       XML_PARSE_RECOVER | XML_PARSE_NOBLANKS);
+  if (!doc) {
+    *error = g_error_new (GRL_CORE_ERROR,
+			  GRL_CORE_ERROR_BROWSE_FAILED,
+			  "Failed to parse Jamendo's response");
+    goto free_resources;
+  }
+
+  node = xmlDocGetRootElement (doc);
+  if (!node) {
+    *error = g_error_new (GRL_CORE_ERROR,
+			  GRL_CORE_ERROR_BROWSE_FAILED,
+			  "Empty response from Jamendo");
+    goto free_resources;
+  }
+
+  if (xmlStrcmp (node->name, (const xmlChar *) "data")) {
+    *error = g_error_new (GRL_CORE_ERROR,
+			  GRL_CORE_ERROR_BROWSE_FAILED,
+			  "Unexpected response from Jamendo: no data");
+    goto free_resources;
+  }
+
+  child_nodes = xml_count_children (node);
+  node = node->xmlChildrenNode;
+
+  /* Skip offset */
+  while (node && xpe->offset > 0) {
+    node = node->next;
+    child_nodes--;
+    xpe->offset--;
+  }
+
+  xpe->node = node;
+  xpe->doc = doc;
+  xpe->total_results = child_nodes;
+
+  return;
+
+ free_resources:
+  xmlFreeDoc (doc);
+}
+
+static Entry *
+xml_parse_entry (xmlDocPtr doc, xmlNodePtr entry)
+{
+  xmlNodePtr node;
+  xmlNs *ns;
+  Entry *data = g_slice_new0 (Entry);
+
+  if (strcmp ((gchar *) entry->name, JAMENDO_ARTIST) == 0) {
+    data->category = JAMENDO_ARTIST_CAT;
+  } else if (strcmp ((gchar *) entry->name, JAMENDO_ALBUM) == 0) {
+    data->category = JAMENDO_ALBUM_CAT;
+  } else if (strcmp ((gchar *) entry->name, JAMENDO_TRACK) == 0) {
+    data->category = JAMENDO_TRACK_CAT;
+  } else {
+    g_return_val_if_reached (NULL);
+  }
+
+  node = entry->xmlChildrenNode;
+
+  while (node) {
+    ns = node->ns;
+
+    if (!xmlStrcmp (node->name, (const xmlChar *) "id")) {
+      data->id =
+        (gchar *) xmlNodeListGetString (doc, node->xmlChildrenNode, 1);
+
+    } else if (!xmlStrcmp (node->name, (const xmlChar *) "artist_name")) {
+      data->artist_name =
+	(gchar *) xmlNodeListGetString (doc, node->xmlChildrenNode, 1);
+
+    } else if (!xmlStrcmp (node->name, (const xmlChar *) "album_name")) {
+      data->album_name =
+	(gchar *) xmlNodeListGetString (doc, node->xmlChildrenNode, 1);
+
+    } else if (!xmlStrcmp (node->name, (const xmlChar *) "artist_genre")) {
+      data->artist_genre =
+        (gchar *) xmlNodeListGetString (doc, node->xmlChildrenNode, 1);
+
+    } else if (!xmlStrcmp (node->name, (const xmlChar *) "artist_url")) {
+      data->artist_url =
+        (gchar *) xmlNodeListGetString (doc, node->xmlChildrenNode, 1);
+
+    } else if (!xmlStrcmp (node->name, (const xmlChar *) "artist_image")) {
+      data->artist_image =
+        (gchar *) xmlNodeListGetString (doc, node->xmlChildrenNode, 1);
+
+    } else if (!xmlStrcmp (node->name, (const xmlChar *) "album_genre")) {
+      data->album_genre =
+        (gchar *) xmlNodeListGetString (doc, node->xmlChildrenNode, 1);
+
+    } else if (!xmlStrcmp (node->name, (const xmlChar *) "album_url")) {
+      data->album_url =
+        (gchar *) xmlNodeListGetString (doc, node->xmlChildrenNode, 1);
+
+    } else if (!xmlStrcmp (node->name, (const xmlChar *) "album_duration")) {
+      data->album_duration =
+        (gchar *) xmlNodeListGetString (doc, node->xmlChildrenNode, 1);
+
+    } else if (!xmlStrcmp (node->name, (const xmlChar *) "album_image")) {
+      data->album_image =
+        (gchar *) xmlNodeListGetString (doc, node->xmlChildrenNode, 1);
+
+    } else if (!xmlStrcmp (node->name, (const xmlChar *) "track_name")) {
+      data->track_name =
+        (gchar *) xmlNodeListGetString (doc, node->xmlChildrenNode, 1);
+
+    } else if (!xmlStrcmp (node->name, (const xmlChar *) "track_url")) {
+      data->track_url =
+        (gchar *) xmlNodeListGetString (doc, node->xmlChildrenNode, 1);
+
+    } else if (!xmlStrcmp (node->name, (const xmlChar *) "track_stream")) {
+      data->track_stream =
+        (gchar *) xmlNodeListGetString (doc, node->xmlChildrenNode, 1);
+
+    } else if (!xmlStrcmp (node->name, (const xmlChar *) "track_duration")) {
+      data->track_duration =
+        (gchar *) xmlNodeListGetString (doc, node->xmlChildrenNode, 1);
+    }
+
+    node = node->next;
+  }
+
+  return data;
+}
+
+static void
+update_media_from_entry (GrlMedia *media, const Entry *entry)
+{
+  gchar *id;
+
+  if (entry->id) {
+    id = g_strdup_printf ("%d/%s", entry->category, entry->id);
+  } else {
+    id = g_strdup_printf ("%d", entry->category);
+  }
+
+  /* Common fields */
+  grl_media_set_id (media, id);
+  g_free (id);
+
+  if (entry->artist_name) {
+    grl_data_set_string (GRL_DATA (media),
+                         GRL_METADATA_KEY_ARTIST,
+                         entry->artist_name);
+  }
+
+  if (entry->album_name) {
+    grl_data_set_string (GRL_DATA (media),
+                         GRL_METADATA_KEY_ALBUM,
+                         entry->album_name);
+  }
+
+  /* Fields for artist */
+  if (entry->category == JAMENDO_ARTIST_CAT) {
+    if (entry->artist_name) {
+      grl_media_set_title (media, entry->artist_name);
+    }
+
+    if (entry->artist_genre) {
+      grl_data_set_string (GRL_DATA (media),
+                           GRL_METADATA_KEY_GENRE,
+                           entry->artist_genre);
+    }
+
+    if (entry->artist_url) {
+      grl_media_set_site (media, entry->artist_url);
+    }
+
+    if (entry->artist_image) {
+      grl_media_set_thumbnail (media, entry->artist_image);
+    }
+
+    /* Fields for album */
+  } else if (entry->category == JAMENDO_ALBUM_CAT) {
+    if (entry->album_name) {
+      grl_media_set_title (media, entry->album_name);
+    }
+
+    if (entry->album_genre) {
+      grl_data_set_string (GRL_DATA (media),
+                           GRL_METADATA_KEY_GENRE,
+                           entry->album_genre);
+    }
+
+    if (entry->album_url) {
+      grl_media_set_site (media, entry->album_url);
+    }
+
+    if (entry->album_image) {
+      grl_media_set_thumbnail (media, entry->album_image);
+    }
+
+    if (entry->album_duration) {
+      grl_media_set_duration (media, atoi (entry->album_duration));
+    }
+
+    /* Fields for track */
+  } else if (entry->category == JAMENDO_TRACK_CAT) {
+    if (entry->track_name) {
+      grl_media_set_title (media, entry->track_name);
+    }
+
+    if (entry->album_genre) {
+      grl_media_audio_set_genre (GRL_MEDIA_AUDIO (media),
+                                 entry->album_genre);
+    }
+
+    if (entry->track_url) {
+      grl_media_set_site (media, entry->track_url);
+    }
+
+    if (entry->album_image) {
+      grl_media_set_thumbnail (media, entry->album_image);
+    }
+
+    if (entry->track_stream) {
+      grl_media_set_url (media, entry->track_stream);
+    }
+
+    if (entry->track_duration) {
+      grl_media_set_duration (media, atoi (entry->track_duration));
+    }
+  } else if (entry->category == JAMENDO_FEEDS_CAT) {
+    if (entry->feed_name) {
+      grl_media_set_title (media, entry->feed_name);
+    }
+  }
+}
+
+static gboolean
+xml_parse_entries_idle (gpointer user_data)
+{
+  XmlParseEntries *xpe = (XmlParseEntries *) user_data;
+  gboolean parse_more;
+  GrlMedia *media = NULL;
+  Entry *entry;
+  gint remaining = 0;
+
+  GRL_DEBUG ("xml_parse_entries_idle");
+
+  parse_more = (xpe->cancelled == FALSE && xpe->node);
+
+  if (parse_more) {
+    entry = xml_parse_entry (xpe->doc, xpe->node);
+    if (entry->category == JAMENDO_TRACK_CAT) {
+      media = grl_media_audio_new ();
+    } else {
+      media = grl_media_box_new ();
+    }
+
+    update_media_from_entry (media, entry);
+    free_entry (entry);
+
+    xpe->index++;
+    xpe->node = xpe->node->next;
+    remaining = xpe->total_results - xpe->index;
+  }
+
+  if (parse_more || xpe->cancelled) {
+    switch (xpe->type) {
+    case BROWSE:
+      xpe->spec.bs->callback (xpe->spec.bs->source,
+                              xpe->spec.bs->browse_id,
+                              media,
+                              remaining,
+                              xpe->spec.bs->user_data,
+                              NULL);
+      break;
+    case QUERY:
+      xpe->spec.qs->callback (xpe->spec.qs->source,
+                              xpe->spec.qs->query_id,
+                              media,
+                              remaining,
+                              xpe->spec.qs->user_data,
+                              NULL);
+      break;
+    case SEARCH:
+      xpe->spec.ss->callback (xpe->spec.ss->source,
+                              xpe->spec.ss->search_id,
+                              media,
+                              remaining,
+                              xpe->spec.ss->user_data,
+                              NULL);
+      break;
+    }
+  }
+
+  if (!parse_more) {
+    xmlFreeDoc (xpe->doc);
+    g_slice_free (XmlParseEntries, xpe);
+  }
+
+  return parse_more;
+}
+
+static void
+read_done_cb (GObject *source_object,
+              GAsyncResult *res,
+              gpointer user_data)
+{
+  XmlParseEntries *xpe = (XmlParseEntries *) user_data;
+  gint error_code = -1;
+  GError *wc_error = NULL;
+  GError *error = NULL;
+  gchar *content = NULL;
+  Entry *entry = NULL;
+
+  /* Check if operation was cancelled */
+  if (xpe->cancelled) {
+    goto invoke_cb;
+  }
+
+  if (!grl_net_wc_request_finish (GRL_NET_WC (source_object),
+                              res,
+                              &content,
+                              NULL,
+                              &wc_error)) {
+    switch (xpe->type) {
+    case METADATA:
+      error_code = GRL_CORE_ERROR_METADATA_FAILED;
+      break;
+    case BROWSE:
+      error_code = GRL_CORE_ERROR_BROWSE_FAILED;
+      break;
+    case QUERY:
+      error_code = GRL_CORE_ERROR_QUERY_FAILED;
+      break;
+    case SEARCH:
+      error_code = GRL_CORE_ERROR_SEARCH_FAILED;
+      break;
+    }
+
+    error = g_error_new (GRL_CORE_ERROR,
+                         error_code,
+                         "Failed to connect Jamendo: '%s'",
+                         wc_error->message);
+    g_error_free (wc_error);
+    goto invoke_cb;
+  }
+
+  if (content) {
+    xml_parse_result (content, &error, xpe);
+  } else {
+    goto invoke_cb;
+  }
+
+  if (error) {
+    goto invoke_cb;
+  }
+
+  if (xpe->node) {
+    if (xpe->type == METADATA) {
+      entry = xml_parse_entry (xpe->doc, xpe->node);
+      xmlFreeDoc (xpe->doc);
+      update_media_from_entry (xpe->spec.ms->media, entry);
+      free_entry (entry);
+      goto invoke_cb;
+    } else {
+      g_idle_add (xml_parse_entries_idle, xpe);
+    }
+  } else {
+    if (xpe->type == METADATA) {
+      error = g_error_new (GRL_CORE_ERROR,
+                           GRL_CORE_ERROR_METADATA_FAILED,
+                           "Unable to get information: '%s'",
+                           grl_media_get_id (xpe->spec.ms->media));
+    }
+    goto invoke_cb;
+  }
+
+  return;
+
+ invoke_cb:
+  switch (xpe->type) {
+  case METADATA:
+    xpe->spec.ms->callback (xpe->spec.ms->source,
+                            xpe->spec.ms->media,
+                            xpe->spec.ms->user_data,
+                            error);
+    break;
+  case BROWSE:
+    xpe->spec.bs->callback (xpe->spec.bs->source,
+                            xpe->spec.bs->browse_id,
+                            NULL,
+                            0,
+                            xpe->spec.bs->user_data,
+                            error);
+    break;
+  case QUERY:
+    xpe->spec.qs->callback (xpe->spec.qs->source,
+                            xpe->spec.qs->query_id,
+                            NULL,
+                            0,
+                            xpe->spec.qs->user_data,
+                            error);
+    break;
+  case SEARCH:
+    xpe->spec.ss->callback (xpe->spec.ss->source,
+                            xpe->spec.ss->search_id,
+                            NULL,
+                            0,
+                            xpe->spec.ss->user_data,
+                            error);
+    break;
+  }
+
+  g_slice_free (XmlParseEntries, xpe);
+  if (error) {
+    g_error_free (error);
+  }
+}
+
+static void
+read_url_async (GrlJamendoSource *source,
+                const gchar *url,
+                gpointer user_data)
+{
+  if (!source->priv->wc)
+    source->priv->wc = g_object_new (GRL_TYPE_NET_WC, "throttling", 1, NULL);
+
+  source->priv->cancellable = g_cancellable_new ();
+
+  GRL_DEBUG ("Opening '%s'", url);
+  grl_net_wc_request_async (source->priv->wc,
+                        url,
+                        source->priv->cancellable,
+                        read_done_cb,
+                        user_data);
+}
+
+static void
+update_media_from_root (GrlMedia *media)
+{
+  grl_media_set_title (media, JAMENDO_ROOT_NAME);
+  grl_media_box_set_childcount (GRL_MEDIA_BOX (media), 3);
+}
+
+static void
+update_media_from_artists (GrlMedia *media)
+{
+  Entry entry = {
+    .category = JAMENDO_ARTIST_CAT,
+    .artist_name = JAMENDO_ARTIST "s",
+  };
+
+  update_media_from_entry (media, &entry);
+}
+
+static void
+update_media_from_albums (GrlMedia *media)
+{
+  Entry entry = {
+    .category = JAMENDO_ALBUM_CAT,
+    .album_name = JAMENDO_ALBUM "s",
+  };
+
+  update_media_from_entry (media, &entry);
+}
+
+static void
+update_media_from_feeds (GrlMedia *media)
+{
+  Entry entry = {
+    .category = JAMENDO_FEEDS_CAT,
+    .feed_name = "feeds",
+  };
+
+  update_media_from_entry (media, &entry);
+  grl_media_box_set_childcount (GRL_MEDIA_BOX (media), G_N_ELEMENTS(feeds));
+}
+
+static void
+send_toplevel_categories (GrlMediaSourceBrowseSpec *bs)
+{
+  GrlMedia *media;
+  gint remaining;
+
+  /* Check if all elements must be skipped */
+  if (bs->skip > 1 || bs->count == 0) {
+    bs->callback (bs->source, bs->browse_id, NULL, 0, bs->user_data, NULL);
+    return;
+  }
+
+  remaining = bs->count;
+
+  if (bs->skip == 0) {
+    media = grl_media_box_new ();
+    update_media_from_artists (media);
+    remaining--;
+    bs->callback (bs->source, bs->browse_id, media, remaining, bs->user_data, NULL);
+  }
+
+  if (remaining) {
+    media = grl_media_box_new ();
+    update_media_from_albums (media);
+    bs->callback (bs->source, bs->browse_id, media, remaining, bs->user_data, NULL);
+    remaining--;
+  }
+
+  if (remaining) {
+    media = grl_media_box_new ();
+    update_media_from_feeds (media);
+    bs->callback (bs->source, bs->browse_id, media, 0, bs->user_data, NULL);
+  }
+}
+
+static void
+update_media_from_feed (GrlMedia *media, int i)
+{
+  char *id;
+
+  id = g_strdup_printf("%d/%d", JAMENDO_FEEDS_CAT, i);
+  grl_media_set_id (media, id);
+  g_free (id);
+
+  grl_media_set_title (media, feeds[i].name);
+}
+
+static void
+send_feeds (GrlMediaSourceBrowseSpec *bs)
+{
+  int i;
+  int remaining;
+
+  remaining = MIN (bs->count, G_N_ELEMENTS (feeds));
+  for (i = bs->skip; remaining > 0 && i < G_N_ELEMENTS (feeds); i++) {
+    GrlMedia *media;
+
+    media = grl_media_box_new ();
+    update_media_from_feed (media, i);
+    remaining--;
+    bs->callback (bs->source,
+                  bs->browse_id,
+                  media,
+                  remaining,
+                  bs->user_data,
+                  NULL);
+  }
+}
+
+static gchar *
+get_jamendo_keys (JamendoCategory category)
+{
+  gchar *jamendo_keys = NULL;
+  gchar *keys_for_artist = "artist_name+artist_genre+artist_image+artist_url";
+  gchar *keys_for_album  = "album_name+album_genre+album_image+album_url+album_duration";
+  gchar *keys_for_track  = "track_name+track_stream+track_url+track_duration";
+
+  if (category == JAMENDO_ARTIST_CAT) {
+    jamendo_keys = g_strconcat ("id+", keys_for_artist, NULL);
+  } else if (category == JAMENDO_ALBUM_CAT) {
+    jamendo_keys = g_strconcat ("id+", keys_for_artist,
+                                "+", keys_for_album,
+                                NULL);
+  } else if (category == JAMENDO_TRACK_CAT) {
+    jamendo_keys = g_strconcat ("id+", keys_for_artist,
+                                "+", keys_for_album,
+                                "+", keys_for_track,
+                                NULL);
+  }
+
+  return jamendo_keys;
+}
+
+static gboolean
+parse_query (const gchar *query, JamendoCategory *category, gchar **term)
+{
+  if (!query) {
+    return FALSE;
+  }
+
+  if (g_str_has_prefix (query, JAMENDO_ARTIST "=")) {
+    *category = JAMENDO_ARTIST_CAT;
+    query += 7;
+  } else if (g_str_has_prefix (query, JAMENDO_ALBUM "=")) {
+    *category = JAMENDO_ALBUM_CAT;
+    query += 6;
+  } else if (g_str_has_prefix (query, JAMENDO_TRACK "=")) {
+    *category = JAMENDO_TRACK_CAT;
+    query += 6;
+  } else {
+    return FALSE;
+  }
+
+  *term = g_uri_escape_string (query, NULL, TRUE);
+  return TRUE;
+}
+
+/* ================== API Implementation ================ */
+
+static const GList *
+grl_jamendo_source_supported_keys (GrlMetadataSource *source)
+{
+  static GList *keys = NULL;
+  if (!keys) {
+    keys = grl_metadata_key_list_new (GRL_METADATA_KEY_ID,
+                                      GRL_METADATA_KEY_TITLE,
+                                      GRL_METADATA_KEY_ARTIST,
+                                      GRL_METADATA_KEY_ALBUM,
+                                      GRL_METADATA_KEY_GENRE,
+                                      GRL_METADATA_KEY_URL,
+                                      GRL_METADATA_KEY_DURATION,
+                                      GRL_METADATA_KEY_THUMBNAIL,
+                                      GRL_METADATA_KEY_SITE,
+                                      NULL);
+  }
+  return keys;
+}
+
+static void
+grl_jamendo_source_metadata (GrlMediaSource *source,
+                             GrlMediaSourceMetadataSpec *ms)
+{
+  gchar *url = NULL;
+  gchar *jamendo_keys = NULL;
+  const gchar *id;
+  gchar **id_split = NULL;
+  XmlParseEntries *xpe = NULL;
+  GError *error = NULL;
+  JamendoCategory category;
+
+  GRL_DEBUG ("grl_jamendo_source_metadata");
+
+  if (!ms->media ||
+      !grl_data_key_is_known (GRL_DATA (ms->media),
+                              GRL_METADATA_KEY_ID)) {
+    /* Get info from root */
+    if (!ms->media) {
+      ms->media = grl_media_box_new ();
+    }
+    update_media_from_root (ms->media);
+  } else {
+    id = grl_media_get_id (ms->media);
+    id_split = g_strsplit (id, JAMENDO_ID_SEP, 0);
+
+    if (g_strv_length (id_split) == 0) {
+      error = g_error_new (GRL_CORE_ERROR,
+                           GRL_CORE_ERROR_METADATA_FAILED,
+                           "Invalid id: '%s'",
+                           id);
+      goto send_error;
+    }
+
+    category = atoi (id_split[0]);
+
+    if (category == JAMENDO_ARTIST_CAT) {
+      if (id_split[1]) {
+        /* Requesting information from a specific artist */
+        jamendo_keys = get_jamendo_keys (JAMENDO_ARTIST_CAT);
+        url =
+          g_strdup_printf (JAMENDO_GET_ARTIST,
+                           jamendo_keys,
+                           id_split[1]);
+        g_free (jamendo_keys);
+      } else {
+        /* Requesting information from artist category */
+        update_media_from_artists (ms->media);
+      }
+    } else if (category == JAMENDO_ALBUM_CAT) {
+      if (id_split[1]) {
+        /* Requesting information from a specific album */
+        jamendo_keys = get_jamendo_keys (JAMENDO_ALBUM_CAT);
+        url =
+          g_strdup_printf (JAMENDO_GET_ALBUM,
+                           jamendo_keys,
+                           id_split[1]);
+        g_free (jamendo_keys);
+      } else {
+        /* Requesting information from album category */
+        update_media_from_albums (ms->media);
+      }
+    } else if (category == JAMENDO_TRACK_CAT) {
+      if (id_split[1]) {
+        /* Requesting information from a specific song */
+        jamendo_keys = get_jamendo_keys (JAMENDO_TRACK_CAT);
+        url =
+          g_strdup_printf (JAMENDO_GET_TRACK,
+                           jamendo_keys,
+                           id_split[1]);
+        g_free (jamendo_keys);
+      } else {
+        error = g_error_new (GRL_CORE_ERROR,
+                             GRL_CORE_ERROR_METADATA_FAILED,
+                             "Invalid id: '%s'",
+                             id);
+        g_strfreev (id_split);
+        goto send_error;
+      }
+    } else if (category == JAMENDO_FEEDS_CAT) {
+      if (id_split[1]) {
+        int i;
+
+        i = atoi (id_split[0]);
+        update_media_from_feed (ms->media, i);
+      } else {
+        update_media_from_feeds (ms->media);
+      }
+    } else {
+      error = g_error_new (GRL_CORE_ERROR,
+                           GRL_CORE_ERROR_METADATA_FAILED,
+                           "Invalid id: '%s'",
+                           id);
+      g_strfreev (id_split);
+      goto send_error;
+    }
+  }
+
+  if (id_split) {
+    g_strfreev (id_split);
+  }
+
+  if (url) {
+    xpe = g_slice_new0 (XmlParseEntries);
+    xpe->type = METADATA;
+    xpe->spec.ms = ms;
+    read_url_async (GRL_JAMENDO_SOURCE (source), url, xpe);
+    g_free (url);
+  } else {
+    if (ms->media) {
+      ms->callback (ms->source, ms->media, ms->user_data, NULL);
+    }
+  }
+
+  return;
+
+ send_error:
+  ms->callback (ms->source, NULL, ms->user_data, error);
+  g_error_free (error);
+}
+
+static void
+grl_jamendo_source_browse (GrlMediaSource *source,
+                           GrlMediaSourceBrowseSpec *bs)
+{
+  gchar *url = NULL;
+  gchar *jamendo_keys;
+  gchar **container_split = NULL;
+  JamendoCategory category;
+  XmlParseEntries *xpe = NULL;
+  const gchar *container_id;
+  GError *error = NULL;
+  guint page_size;
+  guint page_number;
+  guint page_offset;
+
+  GRL_DEBUG ("grl_jamendo_source_browse");
+
+  container_id = grl_media_get_id (bs->container);
+
+  if (!container_id) {
+    /* Root category: return top-level predefined categories */
+    send_toplevel_categories (bs);
+    return;
+  }
+
+  container_split = g_strsplit (container_id, JAMENDO_ID_SEP, 0);
+
+  if (g_strv_length (container_split) == 0) {
+    error = g_error_new (GRL_CORE_ERROR,
+                         GRL_CORE_ERROR_BROWSE_FAILED,
+                         "Invalid container-id: '%s'",
+                         container_id);
+  } else {
+    category = atoi (container_split[0]);
+    grl_paging_translate (bs->skip,
+                          bs->count,
+                          0,
+                          &page_size,
+                          &page_number,
+                          &page_offset);
+
+    if (category == JAMENDO_ARTIST_CAT) {
+      if (container_split[1]) {
+        jamendo_keys = get_jamendo_keys (JAMENDO_ALBUM_CAT);
+        /* Requesting information from a specific artist */
+        url =
+          g_strdup_printf (JAMENDO_GET_ALBUMS_FROM_ARTIST,
+                           jamendo_keys,
+                           page_size,
+                           page_number,
+                           container_split[1]);
+      } else {
+        /* Browsing through artists */
+        jamendo_keys = get_jamendo_keys (JAMENDO_ARTIST_CAT);
+        url = g_strdup_printf (JAMENDO_GET_ARTISTS,
+                               jamendo_keys,
+                               page_size,
+                               page_number);
+      }
+      g_free (jamendo_keys);
+
+    } else if (category == JAMENDO_ALBUM_CAT) {
+      if (container_split[1]) {
+        /* Requesting information from a specific album */
+        jamendo_keys = get_jamendo_keys (JAMENDO_TRACK_CAT);
+        url =
+          g_strdup_printf (JAMENDO_GET_TRACKS_FROM_ALBUM,
+                           jamendo_keys,
+                           page_size,
+                           page_number,
+                           container_split[1]);
+      } else {
+        /* Browsing through albums */
+        jamendo_keys = get_jamendo_keys (JAMENDO_ALBUM_CAT);
+        url = g_strdup_printf (JAMENDO_GET_ALBUMS,
+                               jamendo_keys,
+                               page_size,
+                               page_number);
+      }
+      g_free (jamendo_keys);
+
+    } else if (category == JAMENDO_FEEDS_CAT) {
+      if (container_split[1]) {
+        int feed_id;
+
+        feed_id = atoi (container_split[1]);
+        jamendo_keys = get_jamendo_keys (feeds[feed_id].cat);
+        url = g_strdup_printf (feeds[feed_id].url,
+                               jamendo_keys,
+                               page_size,
+                               page_number);
+      } else {
+        send_feeds (bs);
+        return;
+      }
+
+    } else if (category == JAMENDO_TRACK_CAT) {
+      error = g_error_new (GRL_CORE_ERROR,
+                           GRL_CORE_ERROR_BROWSE_FAILED,
+                           "Cannot browse through a track: '%s'",
+                           container_id);
+    } else {
+      error = g_error_new (GRL_CORE_ERROR,
+                           GRL_CORE_ERROR_BROWSE_FAILED,
+                           "Invalid container-id: '%s'",
+                           container_id);
+    }
+  }
+
+  if (error) {
+    bs->callback (source, bs->browse_id, NULL, 0, bs->user_data, error);
+    g_error_free (error);
+    return;
+  }
+
+  xpe = g_slice_new0 (XmlParseEntries);
+  xpe->type = BROWSE;
+  xpe->offset = page_offset;
+  xpe->spec.bs = bs;
+
+  grl_media_source_set_operation_data (source, bs->browse_id, xpe);
+
+  read_url_async (GRL_JAMENDO_SOURCE (source), url, xpe);
+  g_free (url);
+  if (container_split) {
+    g_strfreev (container_split);
+  }
+}
+
+/*
+ * Query format is "<type>=<text>", where <type> can be either 'artist', 'album'
+ * or 'track' and 'text' is the term to search.
+ *
+ * The result will be also a <type>.
+ *
+ * Example: search for artists that have the "Shake" in their name or
+ * description: "artist=Shake"
+ *
+ */
+static void
+grl_jamendo_source_query (GrlMediaSource *source,
+                          GrlMediaSourceQuerySpec *qs)
+{
+  GError *error = NULL;
+  JamendoCategory category;
+  gchar *term = NULL;
+  gchar *url;
+  gchar *jamendo_keys = NULL;
+  gchar *query = NULL;
+  XmlParseEntries *xpe = NULL;
+  guint page_size;
+  guint page_number;
+  guint page_offset;
+
+  GRL_DEBUG ("grl_jamendo_source_query");
+
+  if (!parse_query (qs->query, &category, &term)) {
+    error = g_error_new (GRL_CORE_ERROR,
+                         GRL_CORE_ERROR_QUERY_FAILED,
+                         "Query malformed: '%s'",
+                         qs->query);
+    goto send_error;
+  }
+
+  jamendo_keys = get_jamendo_keys (category);
+  switch (category) {
+  case JAMENDO_ARTIST_CAT:
+    query = JAMENDO_SEARCH_ARTIST;
+    break;
+  case JAMENDO_ALBUM_CAT:
+    query = JAMENDO_SEARCH_ALBUM;
+    break;
+  case JAMENDO_TRACK_CAT:
+    query = JAMENDO_SEARCH_TRACK;
+    break;
+  default:
+    g_return_if_reached ();
+  }
+
+  grl_paging_translate (qs->skip,
+                        qs->count,
+                        0,
+                        &page_size,
+                        &page_number,
+                        &page_offset);
+
+  url = g_strdup_printf (query,
+                         jamendo_keys,
+                         page_size,
+                         page_number,
+                         term);
+  g_free (term);
+
+  xpe = g_slice_new0 (XmlParseEntries);
+  xpe->type = QUERY;
+  xpe->offset = page_offset;
+  xpe->spec.qs = qs;
+
+  grl_media_source_set_operation_data (source, qs->query_id, xpe);
+
+  read_url_async (GRL_JAMENDO_SOURCE (source), url, xpe);
+  g_free (url);
+
+  return;
+
+ send_error:
+  qs->callback (qs->source, qs->query_id, NULL, 0, qs->user_data, error);
+  g_error_free (error);
+}
+
+
+static void
+grl_jamendo_source_search (GrlMediaSource *source,
+                           GrlMediaSourceSearchSpec *ss)
+{
+  XmlParseEntries *xpe;
+  gchar *jamendo_keys;
+  gchar *url;
+  guint page_size;
+  guint page_number;
+  guint page_offset;
+
+  GRL_DEBUG ("grl_jamendo_source_search");
+
+  jamendo_keys = get_jamendo_keys (JAMENDO_TRACK_CAT);
+
+  grl_paging_translate (ss->skip,
+                        ss->count,
+                        0,
+                        &page_size,
+                        &page_number,
+                        &page_offset);
+
+  if (ss->text) {
+    url = g_strdup_printf (JAMENDO_SEARCH_TRACK,
+                           jamendo_keys,
+                           page_size,
+                           page_number,
+                           ss->text);
+  } else {
+    url = g_strdup_printf (JAMENDO_SEARCH_ALL,
+                           jamendo_keys,
+                           page_size,
+                           page_number);
+  }
+
+  xpe = g_slice_new0 (XmlParseEntries);
+  xpe->type = SEARCH;
+  xpe->offset = page_offset;
+  xpe->spec.ss = ss;
+
+  grl_media_source_set_operation_data (source, ss->search_id, xpe);
+
+  read_url_async (GRL_JAMENDO_SOURCE (source), url, xpe);
+  g_free (url);
+}
+
+static void
+grl_jamendo_source_cancel (GrlMediaSource *source, guint operation_id)
+{
+  XmlParseEntries *xpe;
+  GrlJamendoSourcePriv *priv;
+
+  g_return_if_fail (GRL_IS_JAMENDO_SOURCE (source));
+
+  priv = GRL_JAMENDO_SOURCE_GET_PRIVATE (source);
+
+  if (priv->cancellable && G_IS_CANCELLABLE (priv->cancellable))
+    g_cancellable_cancel (priv->cancellable);
+  priv->cancellable = NULL;
+
+  if (priv->wc)
+    grl_net_wc_flush_delayed_requests (priv->wc);
+
+  GRL_DEBUG ("grl_jamendo_source_cancel");
+
+  xpe = (XmlParseEntries *) grl_media_source_get_operation_data (source,
+                                                                 operation_id);
+
+  if (xpe) {
+    xpe->cancelled = TRUE;
+  }
+}
diff --git a/src/media/jamendo/grl-jamendo.h b/src/media/jamendo/grl-jamendo.h
new file mode 100644
index 0000000..15a4b75
--- /dev/null
+++ b/src/media/jamendo/grl-jamendo.h
@@ -0,0 +1,78 @@
+/*
+ * Copyright (C) 2010 Igalia S.L.
+ *
+ * Contact: Iago Toral Quiroga <itoral igalia com>
+ *
+ * Authors: Juan A. Suarez Romero <jasuarez igalia 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_JAMENDO_SOURCE_H_
+#define _GRL_JAMENDO_SOURCE_H_
+
+#include <grilo.h>
+
+#define GRL_JAMENDO_SOURCE_TYPE                 \
+  (grl_jamendo_source_get_type ())
+
+#define GRL_JAMENDO_SOURCE(obj)                         \
+  (G_TYPE_CHECK_INSTANCE_CAST ((obj),                   \
+                               GRL_JAMENDO_SOURCE_TYPE, \
+                               GrlJamendoSource))
+
+#define GRL_IS_JAMENDO_SOURCE(obj)                              \
+  (G_TYPE_CHECK_INSTANCE_TYPE ((obj),                           \
+                               GRL_JAMENDO_SOURCE_TYPE))
+
+#define GRL_JAMENDO_SOURCE_CLASS(klass)                 \
+  (G_TYPE_CHECK_CLASS_CAST((klass),                     \
+                           GRL_JAMENDO_SOURCE_TYPE,     \
+                           GrlJamendoSourceClass))
+
+#define GRL_IS_JAMENDO_SOURCE_CLASS(klass)              \
+  (G_TYPE_CHECK_CLASS_TYPE((klass),                     \
+                           GRL_JAMENDO_SOURCE_TYPE))
+
+#define GRL_JAMENDO_SOURCE_GET_CLASS(obj)               \
+  (G_TYPE_INSTANCE_GET_CLASS ((obj),                    \
+                              GRL_JAMENDO_SOURCE_TYPE,  \
+                              GrlJamendoSourceClass))
+
+typedef struct _GrlJamendoSource GrlJamendoSource;
+typedef struct _GrlJamendoSourcePriv GrlJamendoSourcePriv;
+
+struct _GrlJamendoSource {
+
+  GrlMediaSource parent;
+
+  /*< private >*/
+  GrlJamendoSourcePriv *priv;
+
+};
+
+typedef struct _GrlJamendoSourceClass GrlJamendoSourceClass;
+
+struct _GrlJamendoSourceClass {
+
+  GrlMediaSourceClass parent_class;
+
+};
+
+GType grl_jamendo_source_get_type (void);
+
+#endif /* _GRL_JAMENDO_SOURCE_H_ */
diff --git a/src/media/jamendo/grl-jamendo.xml b/src/media/jamendo/grl-jamendo.xml
new file mode 100644
index 0000000..e864cfe
--- /dev/null
+++ b/src/media/jamendo/grl-jamendo.xml
@@ -0,0 +1,9 @@
+<plugin>
+  <info>
+    <name>Jamendo</name>
+    <description>A plugin for browsing and searching Jamendo videos</description>
+    <author>Igalia S.L.</author>
+    <license>LGPL</license>
+    <site>http://www.igalia.com</site>
+  </info>
+</plugin>
diff --git a/src/media/podcasts/Makefile.am b/src/media/podcasts/Makefile.am
new file mode 100644
index 0000000..2383042
--- /dev/null
+++ b/src/media/podcasts/Makefile.am
@@ -0,0 +1,38 @@
+#
+# Makefile.am
+#
+# Author: Iago Toral Quiroga <itoral igalia com>
+#
+# Copyright (C) 2010, 2011 Igalia S.L. All rights reserved.
+
+lib_LTLIBRARIES = libgrlpodcasts.la
+
+libgrlpodcasts_la_CFLAGS =	\
+	$(DEPS_CFLAGS)		\
+	$(GRLNET_CFLAGS)	\
+	$(XML_CFLAGS)		\
+	$(SQLITE_CFLAGS)
+
+libgrlpodcasts_la_LIBADD =	\
+	$(DEPS_LIBS)		\
+	$(GRLNET_LIBS)		\
+	$(XML_LIBS)		\
+	$(SQLITE_LIBS)
+
+libgrlpodcasts_la_LDFLAGS = \
+	-module		    \
+	-avoid-version
+
+libgrlpodcasts_la_SOURCES = grl-podcasts.c grl-podcasts.h
+
+libdir = $(GRL_PLUGINS_DIR)
+podcastsxmldir		= $(GRL_PLUGINS_CONF_DIR)
+podcastsxml_DATA	= $(PODCASTS_PLUGIN_ID).xml
+
+EXTRA_DIST = $(podcastsxml_DATA)
+
+MAINTAINERCLEANFILES =	\
+	*.in		\
+	*~
+
+DISTCLEANFILES = $(MAINTAINERCLEANFILES)
diff --git a/src/media/podcasts/TODO b/src/media/podcasts/TODO
new file mode 100644
index 0000000..12f3311
--- /dev/null
+++ b/src/media/podcasts/TODO
@@ -0,0 +1,5 @@
+- IGN podcasts contains items with no enclosure. These should be skipped.
+- Support more tags (like author, site, etc)
+- Parsing and inserting in the DB seems to block long enough, split the parsing to handle only
+  one element in each go.
+- Implement support for remove() -- Needs support in core first
diff --git a/src/media/podcasts/grl-podcasts.c b/src/media/podcasts/grl-podcasts.c
new file mode 100644
index 0000000..a4d5ad3
--- /dev/null
+++ b/src/media/podcasts/grl-podcasts.c
@@ -0,0 +1,1636 @@
+/*
+ * Copyright (C) 2010, 2011 Igalia S.L.
+ *
+ * Contact: Iago Toral Quiroga <itoral igalia 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 <grilo.h>
+#include <net/grl-net.h>
+#include <libxml/xpath.h>
+#include <sqlite3.h>
+#include <string.h>
+
+#include "grl-podcasts.h"
+
+#define GRL_PODCASTS_GET_PRIVATE(object)                        \
+  (G_TYPE_INSTANCE_GET_PRIVATE((object),                        \
+                               GRL_PODCASTS_SOURCE_TYPE,        \
+                               GrlPodcastsPrivate))
+
+#define GRL_ROOT_TITLE "Podcasts"
+
+/* --------- Logging  -------- */
+
+#define GRL_LOG_DOMAIN_DEFAULT podcasts_log_domain
+GRL_LOG_DOMAIN_STATIC(podcasts_log_domain);
+
+/* --- Database --- */
+
+#define GRL_SQL_DB        ".grl-podcasts"
+
+#define GRL_SQL_CREATE_TABLE_PODCASTS           \
+  "CREATE TABLE IF NOT EXISTS podcasts ("       \
+  "id    INTEGER  PRIMARY KEY AUTOINCREMENT,"   \
+  "title TEXT,"                                 \
+  "url   TEXT,"                                 \
+  "desc  TEXT,"                                 \
+  "last_refreshed DATE)"
+
+#define GRL_SQL_CREATE_TABLE_STREAMS		 \
+  "CREATE TABLE IF NOT EXISTS streams ( "        \
+  "podcast INTEGER REFERENCES podcasts (id), "   \
+  "url     TEXT, "				 \
+  "title   TEXT, "                               \
+  "length  INTEGER, "                            \
+  "mime    TEXT, "                               \
+  "date    TEXT, "                               \
+  "desc    TEXT)"
+
+#define GRL_SQL_GET_PODCASTS				\
+  "SELECT p.*, count(s.podcast <> '') "			\
+  "FROM podcasts p LEFT OUTER JOIN streams s "		\
+  "  ON p.id = s.podcast "				\
+  "GROUP BY p.id "					\
+  "LIMIT %u OFFSET %u"
+
+#define GRL_SQL_GET_PODCASTS_BY_QUERY				\
+  "SELECT p.*, count(s.podcast <> '') "				\
+  "FROM podcasts p LEFT OUTER JOIN streams s "			\
+  "  ON p.id = s.podcast "					\
+  "WHERE %s "							\
+  "GROUP BY p.id "						\
+  "LIMIT %u OFFSET %u"
+
+#define GRL_SQL_GET_PODCAST_BY_ID               \
+  "SELECT * FROM podcasts "                     \
+  "WHERE id='%s' "                              \
+  "LIMIT 1"
+
+#define GRL_SQL_STORE_PODCAST                   \
+  "INSERT INTO podcasts "                       \
+  "(url, title, desc) "                         \
+  "VALUES (?, ?, ?)"
+
+#define GRL_SQL_REMOVE_PODCAST                  \
+  "DELETE FROM podcasts "                       \
+  "WHERE id='%s'"
+
+#define GRL_SQL_REMOVE_STREAM                   \
+  "DELETE FROM streams "                        \
+  "WHERE url='%s'"
+
+#define GRL_SQL_STORE_STREAM                            \
+  "INSERT INTO streams "                                \
+  "(podcast, url, title, length, mime, date, desc) "    \
+  "VALUES (?, ?, ?, ?, ?, ?, ?)"
+
+#define GRL_SQL_DELETE_PODCAST_STREAMS          \
+  "DELETE FROM streams WHERE podcast='%s'"
+
+#define GRL_SQL_GET_PODCAST_STREAMS             \
+  "SELECT * FROM streams "                      \
+  "WHERE podcast='%s' "                         \
+  "LIMIT %u  OFFSET %u"
+
+#define GRL_SQL_GET_PODCAST_STREAMS_BY_TEXT                     \
+  "SELECT s.* "                                                 \
+  "FROM streams s LEFT OUTER JOIN podcasts p "			\
+  "  ON s.podcast = p.id "					\
+  "WHERE s.title LIKE '%%%s%%' OR s.desc LIKE '%%%s%%' "	\
+  "  OR p.title LIKE '%%%s%%' OR p.desc LIKE '%%%s%%' "         \
+  "LIMIT %u OFFSET %u"
+
+#define GRL_SQL_GET_PODCAST_STREAMS_ALL         \
+  "SELECT * FROM streams "                      \
+  "LIMIT %u OFFSET %u"
+
+#define GRL_SQL_GET_PODCAST_STREAM              \
+  "SELECT * FROM streams "                      \
+  "WHERE url='%s' "                             \
+  "LIMIT 1"
+
+#define GRL_SQL_TOUCH_PODCAST			\
+  "UPDATE podcasts "				\
+  "SET last_refreshed='%s' "			\
+  "WHERE id='%s'"
+
+/* --- Other --- */
+
+#define CACHE_DURATION (24 * 60 * 60)
+
+/* --- Plugin information --- */
+
+#define PLUGIN_ID   PODCASTS_PLUGIN_ID
+
+#define SOURCE_ID   "grl-podcasts"
+#define SOURCE_NAME "Podcasts"
+#define SOURCE_DESC "A source for browsing podcasts"
+
+#define AUTHOR      "Igalia S.L."
+#define LICENSE     "LGPL"
+#define SITE        "http://www.igalia.com";
+
+enum {
+  PODCAST_ID = 0,
+  PODCAST_TITLE,
+  PODCAST_URL,
+  PODCAST_DESC,
+  PODCAST_LAST_REFRESHED,
+  PODCAST_LAST,
+};
+
+enum {
+  STREAM_PODCAST = 0,
+  STREAM_URL,
+  STREAM_TITLE,
+  STREAM_LENGTH,
+  STREAM_MIME,
+  STREAM_DATE,
+  STREAM_DESC,
+};
+
+typedef void (*AsyncReadCbFunc) (gchar *data, gpointer user_data);
+
+typedef struct {
+  AsyncReadCbFunc callback;
+  gchar *url;
+  gpointer user_data;
+} AsyncReadCb;
+
+typedef struct {
+  gchar *id;
+  gchar *url;
+  gchar *title;
+  gchar *published;
+  gchar *duration;
+  gchar *summary;
+  gchar *mime;
+} Entry;
+
+struct _GrlPodcastsPrivate {
+  sqlite3 *db;
+  GrlNetWc *wc;
+  gboolean notify_changes;
+};
+
+typedef struct {
+  GrlMediaSource *source;
+  guint operation_id;
+  const gchar *media_id;
+  guint skip;
+  guint count;
+  const gchar *text;
+  GrlMediaSourceResultCb callback;
+  guint error_code;
+  gboolean is_query;
+  gpointer user_data;
+} OperationSpec;
+
+typedef struct {
+  OperationSpec *os;
+  xmlDocPtr doc;
+  xmlXPathContextPtr xpathCtx;
+  xmlXPathObjectPtr xpathObj;
+  guint parse_count;
+  guint parse_index;
+  guint parse_valid_index;
+  GrlMedia *last_media;
+} OperationSpecParse;
+
+static GrlPodcastsSource *grl_podcasts_source_new (void);
+
+static void grl_podcasts_source_finalize (GObject *plugin);
+
+static const GList *grl_podcasts_source_supported_keys (GrlMetadataSource *source);
+
+static void grl_podcasts_source_browse (GrlMediaSource *source,
+                                        GrlMediaSourceBrowseSpec *bs);
+static void grl_podcasts_source_search (GrlMediaSource *source,
+                                        GrlMediaSourceSearchSpec *ss);
+static void grl_podcasts_source_query (GrlMediaSource *source,
+                                       GrlMediaSourceQuerySpec *qs);
+static void grl_podcasts_source_metadata (GrlMediaSource *source,
+                                          GrlMediaSourceMetadataSpec *ms);
+static void grl_podcasts_source_store (GrlMediaSource *source,
+                                       GrlMediaSourceStoreSpec *ss);
+static void grl_podcasts_source_remove (GrlMediaSource *source,
+                                        GrlMediaSourceRemoveSpec *rs);
+static gboolean grl_podcasts_source_notify_change_start (GrlMediaSource *source,
+                                                         GError **error);
+static gboolean grl_podcasts_source_notify_change_stop (GrlMediaSource *source,
+                                                        GError **error);
+
+/* =================== Podcasts Plugin  =============== */
+
+static gboolean
+grl_podcasts_plugin_init (GrlPluginRegistry *registry,
+                          const GrlPluginInfo *plugin,
+                          GList *configs)
+{
+  GRL_LOG_DOMAIN_INIT (podcasts_log_domain, "podcasts");
+
+  GRL_DEBUG ("podcasts_plugin_init");
+
+  GrlPodcastsSource *source = grl_podcasts_source_new ();
+  grl_plugin_registry_register_source (registry,
+                                       plugin,
+                                       GRL_MEDIA_PLUGIN (source),
+                                       NULL);
+  return TRUE;
+}
+
+GRL_PLUGIN_REGISTER (grl_podcasts_plugin_init,
+                     NULL,
+                     PLUGIN_ID);
+
+/* ================== Podcasts GObject ================ */
+
+static GrlPodcastsSource *
+grl_podcasts_source_new (void)
+{
+  GRL_DEBUG ("grl_podcasts_source_new");
+  return g_object_new (GRL_PODCASTS_SOURCE_TYPE,
+		       "source-id", SOURCE_ID,
+		       "source-name", SOURCE_NAME,
+		       "source-desc", SOURCE_DESC,
+		       NULL);
+}
+
+static void
+grl_podcasts_source_class_init (GrlPodcastsSourceClass * klass)
+{
+  GObjectClass *gobject_class = G_OBJECT_CLASS (klass);
+  GrlMediaSourceClass *source_class = GRL_MEDIA_SOURCE_CLASS (klass);
+  GrlMetadataSourceClass *metadata_class = GRL_METADATA_SOURCE_CLASS (klass);
+
+  gobject_class->finalize = grl_podcasts_source_finalize;
+
+  source_class->browse = grl_podcasts_source_browse;
+  source_class->search = grl_podcasts_source_search;
+  source_class->query = grl_podcasts_source_query;
+  source_class->metadata = grl_podcasts_source_metadata;
+  source_class->store = grl_podcasts_source_store;
+  source_class->remove = grl_podcasts_source_remove;
+  source_class->notify_change_start = grl_podcasts_source_notify_change_start;
+  source_class->notify_change_stop = grl_podcasts_source_notify_change_stop;
+
+  metadata_class->supported_keys = grl_podcasts_source_supported_keys;
+
+  g_type_class_add_private (klass, sizeof (GrlPodcastsPrivate));
+}
+
+static void
+grl_podcasts_source_init (GrlPodcastsSource *source)
+{
+  gint r;
+  const gchar *home;
+  gchar *db_path;
+  gchar *sql_error = NULL;
+
+  source->priv = GRL_PODCASTS_GET_PRIVATE (source);
+
+  home = g_getenv ("HOME");
+  if (!home) {
+    GRL_WARNING ("$HOME not set, cannot open database");
+    return;
+  }
+
+  GRL_DEBUG ("Opening database connection...");
+  db_path = g_strconcat (home, G_DIR_SEPARATOR_S, GRL_SQL_DB, NULL);
+  r = sqlite3_open (db_path, &source->priv->db);
+  if (r) {
+    g_critical ("Failed to open database '%s': %s",
+		db_path, sqlite3_errmsg (source->priv->db));
+    sqlite3_close (source->priv->db);
+    return;
+  }
+  GRL_DEBUG ("  OK");
+
+  GRL_DEBUG ("Checking database tables...");
+  r = sqlite3_exec (source->priv->db, GRL_SQL_CREATE_TABLE_PODCASTS,
+		    NULL, NULL, &sql_error);
+
+  if (!r) {
+    /* TODO: if this fails, sqlite stays in an unreliable state fix that */
+    r = sqlite3_exec (source->priv->db, GRL_SQL_CREATE_TABLE_STREAMS,
+		      NULL, NULL, &sql_error);
+  }
+  if (r) {
+    if (sql_error) {
+      GRL_WARNING ("Failed to create database tables: %s", sql_error);
+      sqlite3_free (sql_error);
+      sql_error = NULL;
+    } else {
+      GRL_WARNING ("Failed to create database tables.");
+    }
+    sqlite3_close (source->priv->db);
+    return;
+  }
+  GRL_DEBUG ("  OK");
+
+  g_free (db_path);
+}
+
+G_DEFINE_TYPE (GrlPodcastsSource, grl_podcasts_source, GRL_TYPE_MEDIA_SOURCE);
+
+static void
+grl_podcasts_source_finalize (GObject *object)
+{
+  GrlPodcastsSource *source;
+
+  GRL_DEBUG ("grl_podcasts_source_finalize");
+
+  source = GRL_PODCASTS_SOURCE (object);
+
+  if (source->priv->wc)
+    g_object_unref (source->priv->wc);
+
+  sqlite3_close (source->priv->db);
+
+  G_OBJECT_CLASS (grl_podcasts_source_parent_class)->finalize (object);
+}
+
+/* ======================= Utilities ==================== */
+
+static void
+print_entry (Entry *entry)
+{
+  g_print ("Entry Information:\n");
+  g_print ("            ID: %s\n", entry->id);
+  g_print ("         Title: %s\n", entry->title);
+  g_print ("          Date: %s\n", entry->published);
+  g_print ("      Duration: %s\n", entry->duration);
+  g_print ("       Summary: %s\n", entry->summary);
+  g_print ("           URL: %s\n", entry->url);
+  g_print ("          Mime: %s\n", entry->mime);
+}
+
+static void
+free_entry (Entry *entry)
+{
+  g_free (entry->id);
+  g_free (entry->title);
+  g_free (entry->published);
+  g_free (entry->summary);
+  g_free (entry->url);
+  g_free (entry->mime);
+}
+
+static void
+read_done_cb (GObject *source_object,
+              GAsyncResult *res,
+              gpointer user_data)
+{
+  AsyncReadCb *arc = (AsyncReadCb *) user_data;
+  GError *wc_error = NULL;
+  gchar *content = NULL;
+
+  GRL_DEBUG ("  Done");
+
+  grl_net_wc_request_finish (GRL_NET_WC (source_object),
+                         res,
+                         &content,
+                         NULL,
+                         &wc_error);
+  if (wc_error) {
+    GRL_WARNING ("Failed to open '%s': %s", arc->url, wc_error->message);
+    g_error_free (wc_error);
+  } else {
+    arc->callback (content, arc->user_data);
+  }
+  g_free (arc->url);
+  g_slice_free (AsyncReadCb, arc);
+}
+
+static void
+read_url_async (GrlPodcastsSource *source,
+                const gchar *url,
+                AsyncReadCbFunc callback,
+                gpointer user_data)
+{
+  AsyncReadCb *arc;
+
+  GRL_DEBUG ("Opening async '%s'", url);
+
+  arc = g_slice_new0 (AsyncReadCb);
+  arc->url = g_strdup (url);
+  arc->callback = callback;
+  arc->user_data = user_data;
+
+  /* We would need a different Wc if we change of URL.
+   * In this case, as we don't know the previous URL,
+   * we ditch the Wc and create another. It's cheap.
+   */
+  if (!source->priv->wc)
+    g_object_unref (source->priv->wc);
+
+  source->priv->wc = grl_net_wc_new ();
+  grl_net_wc_request_async (source->priv->wc, url, NULL, read_done_cb, arc);
+}
+
+static gint
+duration_to_seconds (const gchar *str)
+{
+  gint seconds = 0;
+  gchar **parts;
+  gint i;
+  guint multiplier = 1;
+
+  if (!str || str[0] == '\0') {
+    return 0;
+  }
+
+  parts = g_strsplit (str, ":", 3);
+
+  /* Get last portion (seconds) */
+  i = 0;
+  while (parts[i]) i++;
+  if (i == 0) {
+    g_strfreev (parts);
+    return 0;
+  } else {
+    i--;
+  }
+
+  do {
+    seconds += atoi (parts[i]) * multiplier;
+    multiplier *= 60;
+    i--;
+  } while (i >= 0);
+
+  g_strfreev (parts);
+
+  return seconds;
+}
+
+static gboolean
+mime_is_video (const gchar *mime)
+{
+  return mime && strstr (mime, "video") != NULL;
+}
+
+static gboolean
+mime_is_audio (const gchar *mime)
+{
+  return mime && strstr (mime, "audio") != NULL;
+}
+
+static gchar *
+get_site_from_url (const gchar *url)
+{
+  gchar *p;
+
+  if (g_str_has_prefix (url, "file://")) {
+    return NULL;
+  }
+
+  p = strstr (url, "://");
+  if (!p) {
+    return NULL;
+  } else {
+    p += 3;
+  }
+
+  while (*p != '/') p++;
+
+  return g_strndup (url, p - url);
+}
+
+static GrlMedia *
+build_media (GrlMedia *content,
+	     gboolean is_podcast,
+	     const gchar *id,
+	     const gchar *title,
+	     const gchar *url,
+	     const gchar *desc,
+	     const gchar *mime,
+	     const gchar *date,
+	     guint duration,
+	     guint childcount)
+{
+  GrlMedia *media = NULL;
+  gchar *site;
+
+  if (content) {
+    media = content;
+  }
+
+  if (is_podcast) {
+    if (!media) {
+      media = GRL_MEDIA (grl_media_box_new ());
+    }
+
+    grl_media_set_id (media, id);
+    if (desc)
+      grl_media_set_description (media, desc);
+    grl_media_box_set_childcount (GRL_MEDIA_BOX (media), childcount);
+  } else {
+    if (!media) {
+      if (mime_is_audio (mime)) {
+	media = grl_media_audio_new ();
+      } else if (mime_is_video (mime)) {
+	media = grl_media_video_new ();
+      } else {
+	media = grl_media_new ();
+      }
+    }
+
+    grl_media_set_id (media, url);
+    if (date)
+      grl_media_set_date (media, date);
+    if (desc)
+      grl_media_set_description (media, desc);
+    if (mime)
+      grl_media_set_mime (media, mime);
+    if (duration > 0) {
+      grl_media_set_duration (media, duration);
+    }
+  }
+
+  grl_media_set_title (media, title);
+  grl_media_set_url (media, url);
+
+  site = get_site_from_url (url);
+  if (site) {
+    grl_media_set_site (media, site);
+    g_free (site);
+  }
+
+  return media;
+}
+
+static GrlMedia *
+build_media_from_entry (Entry *entry)
+{
+  GrlMedia *media;
+  gint duration;
+
+  duration = duration_to_seconds (entry->duration);
+  media = build_media (NULL, FALSE,
+		       entry->url, entry->title, entry->url,
+		       entry->summary, entry->mime, entry->published,
+		       duration, 0);
+  return media;
+}
+
+static GrlMedia *
+build_media_from_stmt (GrlMedia *content,
+		       sqlite3_stmt *sql_stmt,
+		       gboolean is_podcast)
+{
+  GrlMedia *media;
+  gchar *id;
+  gchar *title;
+  gchar *url;
+  gchar *desc;
+  gchar *mime;
+  gchar *date;
+  guint duration;
+  guint childcount;
+
+  if (is_podcast) {
+    id = (gchar *) sqlite3_column_text (sql_stmt, PODCAST_ID);
+    title = (gchar *) sqlite3_column_text (sql_stmt, PODCAST_TITLE);
+    url = (gchar *) sqlite3_column_text (sql_stmt, PODCAST_URL);
+    desc = (gchar *) sqlite3_column_text (sql_stmt, PODCAST_DESC);
+    childcount = (guint) sqlite3_column_int (sql_stmt, PODCAST_LAST);
+    media = build_media (content, is_podcast,
+			 id, title, url, desc, NULL, NULL, 0, childcount);
+  } else {
+    mime = (gchar *) sqlite3_column_text (sql_stmt, STREAM_MIME);
+    url = (gchar *) sqlite3_column_text (sql_stmt, STREAM_URL);
+    title = (gchar *) sqlite3_column_text (sql_stmt, STREAM_TITLE);
+    date = (gchar *) sqlite3_column_text (sql_stmt, STREAM_DATE);
+    desc = (gchar *) sqlite3_column_text (sql_stmt, STREAM_DESC);
+    duration = sqlite3_column_int (sql_stmt, STREAM_LENGTH);
+    media = build_media (content, is_podcast,
+			 url, title, url, desc, mime, date, duration, 0);
+  }
+
+  return media;
+}
+
+static void
+produce_podcast_contents_from_db (OperationSpec *os)
+{
+  sqlite3 *db;
+  gchar *sql;
+  sqlite3_stmt *sql_stmt = NULL;
+  GList *iter, *medias = NULL;
+  guint count = 0;
+  GrlMedia *media;
+  gint r;
+  GError *error = NULL;
+
+  GRL_DEBUG ("produce_podcast_contents_from_db");
+
+  db = GRL_PODCASTS_SOURCE (os->source)->priv->db;
+  /* Check if searching or browsing */
+  if (os->is_query) {
+    if (os->text) {
+      /* Search text */
+      sql = g_strdup_printf (GRL_SQL_GET_PODCAST_STREAMS_BY_TEXT,
+                             os->text, os->text, os->text, os->text,
+                             os->count, os->skip);
+    } else {
+      /* Return all */
+      sql = g_strdup_printf (GRL_SQL_GET_PODCAST_STREAMS_ALL,
+                             os->count, os->skip);
+    }
+  } else {
+    sql = g_strdup_printf (GRL_SQL_GET_PODCAST_STREAMS,
+                           os->media_id, os->count, os->skip);
+  }
+  GRL_DEBUG ("%s", sql);
+  r = sqlite3_prepare_v2 (db, sql, strlen (sql), &sql_stmt, NULL);
+  g_free (sql);
+
+  if (r != SQLITE_OK) {
+    GRL_WARNING ("Failed to retrieve podcast streams: %s", sqlite3_errmsg (db));
+    error = g_error_new (GRL_CORE_ERROR,
+			 os->error_code,
+			 "Failed to retrieve podcast streams");
+    os->callback (os->source, os->operation_id, NULL, 0, os->user_data, error);
+    g_error_free (error);
+    return;
+  }
+
+  while ((r = sqlite3_step (sql_stmt)) == SQLITE_BUSY);
+
+  while (r == SQLITE_ROW) {
+    media = build_media_from_stmt (NULL, sql_stmt, FALSE);
+    medias = g_list_prepend (medias, media);
+    count++;
+    r = sqlite3_step (sql_stmt);
+  }
+
+  if (r != SQLITE_DONE) {
+    GRL_WARNING ("Failed to retrive podcast streams: %s", sqlite3_errmsg (db));
+    error = g_error_new (GRL_CORE_ERROR,
+			 os->error_code,
+			 "Failed to retrieve podcast streams");
+    os->callback (os->source, os->operation_id, NULL, 0, os->user_data, error);
+    g_error_free (error);
+    sqlite3_finalize (sql_stmt);
+    return;
+  }
+
+  sqlite3_finalize (sql_stmt);
+
+  if (count > 0) {
+    medias = g_list_reverse (medias);
+    iter = medias;
+    while (iter) {
+      media = GRL_MEDIA (iter->data);
+      os->callback (os->source,
+		    os->operation_id,
+		    media,
+		    --count,
+		    os->user_data,
+		    NULL);
+      iter = g_list_next (iter);
+    }
+    g_list_free (medias);
+  } else {
+    os->callback (os->source, os->operation_id, NULL, 0, os->user_data, NULL);
+  }
+}
+
+static void
+remove_podcast_streams (sqlite3 *db, const gchar *podcast_id, GError **error)
+{
+  gchar *sql;
+  gchar *sql_error;
+  gint r;
+
+  sql = g_strdup_printf (GRL_SQL_DELETE_PODCAST_STREAMS, podcast_id);
+  GRL_DEBUG ("%s", sql);
+  r = sqlite3_exec (db, sql, NULL, NULL, &sql_error);
+  g_free (sql);
+  if (r) {
+    GRL_WARNING ("Failed to remove podcast streams cache: %s", sql_error);
+    *error = g_error_new (GRL_CORE_ERROR,
+			  GRL_CORE_ERROR_REMOVE_FAILED,
+			  "Failed to remove podcast streams");
+    sqlite3_free (error);
+  }
+}
+
+static void
+remove_podcast (GrlPodcastsSource *podcasts_source,
+                const gchar *podcast_id,
+                GError **error)
+{
+  gint r;
+  gchar *sql_error;
+  gchar *sql;
+
+  GRL_DEBUG ("remove_podcast");
+
+  remove_podcast_streams (podcasts_source->priv->db, podcast_id, error);
+  if (*error) {
+    return;
+  }
+
+  sql = g_strdup_printf (GRL_SQL_REMOVE_PODCAST, podcast_id);
+  GRL_DEBUG ("%s", sql);
+  r = sqlite3_exec (podcasts_source->priv->db, sql, NULL, NULL, &sql_error);
+  g_free (sql);
+
+  if (r != SQLITE_OK) {
+    GRL_WARNING ("Failed to remove podcast '%s': %s", podcast_id, sql_error);
+    g_set_error_literal (error,
+                         GRL_CORE_ERROR,
+                         GRL_CORE_ERROR_REMOVE_FAILED,
+                         "Failed to remove podcast");
+    sqlite3_free (sql_error);
+  } else if (podcasts_source->priv->notify_changes) {
+    grl_media_source_notify_change (GRL_MEDIA_SOURCE (podcasts_source),
+                                    NULL,
+                                    GRL_CONTENT_REMOVED,
+                                    TRUE);
+  }
+}
+
+static void
+remove_stream (GrlPodcastsSource *podcasts_source,
+               const gchar *url,
+               GError **error)
+{
+  gint r;
+  gchar *sql_error;
+  gchar *sql;
+
+  GRL_DEBUG ("remove_stream");
+
+  sql = g_strdup_printf (GRL_SQL_REMOVE_STREAM, url);
+  GRL_DEBUG ("%s", sql);
+  r = sqlite3_exec (podcasts_source->priv->db, sql, NULL, NULL, &sql_error);
+  g_free (sql);
+
+  if (r != SQLITE_OK) {
+    GRL_WARNING ("Failed to remove podcast stream '%s': %s", url, sql_error);
+    g_set_error_literal (error,
+                         GRL_CORE_ERROR,
+                         GRL_CORE_ERROR_REMOVE_FAILED,
+                         "Failed to remove podcast stream");
+    sqlite3_free (sql_error);
+  } else if (podcasts_source->priv->notify_changes) {
+    grl_media_source_notify_change (GRL_MEDIA_SOURCE (podcasts_source),
+                                    NULL,
+                                    GRL_CONTENT_REMOVED,
+                                    TRUE);
+  }
+}
+
+static void
+store_podcast (GrlPodcastsSource *podcasts_source,
+               GrlMedia *podcast,
+               GError **error)
+{
+  gint r;
+  sqlite3_stmt *sql_stmt = NULL;
+  const gchar *title;
+  const gchar *url;
+  const gchar *desc;
+  gchar *id;
+
+  GRL_DEBUG ("store_podcast");
+
+  title = grl_media_get_title (podcast);
+  url = grl_media_get_url (podcast);
+  desc = grl_media_get_description (podcast);
+
+  GRL_DEBUG ("%s", GRL_SQL_STORE_PODCAST);
+  r = sqlite3_prepare_v2 (podcasts_source->priv->db,
+			  GRL_SQL_STORE_PODCAST,
+			  strlen (GRL_SQL_STORE_PODCAST),
+			  &sql_stmt, NULL);
+  if (r != SQLITE_OK) {
+    GRL_WARNING ("Failed to store podcast '%s': %s", title,
+                 sqlite3_errmsg (podcasts_source->priv->db));
+    g_set_error (error,
+                 GRL_CORE_ERROR,
+                 GRL_CORE_ERROR_STORE_FAILED,
+                 "Failed to store podcast '%s'", title);
+    return;
+  }
+
+  sqlite3_bind_text (sql_stmt, 1, url, -1, SQLITE_STATIC);
+  sqlite3_bind_text (sql_stmt, 2, title, -1, SQLITE_STATIC);
+  if (desc) {
+    sqlite3_bind_text (sql_stmt, 3, desc, -1, SQLITE_STATIC);
+  } else {
+    sqlite3_bind_text (sql_stmt, 3, "", -1, SQLITE_STATIC);
+  }
+
+  while ((r = sqlite3_step (sql_stmt)) == SQLITE_BUSY);
+
+  if (r != SQLITE_DONE) {
+    GRL_WARNING ("Failed to store podcast '%s': %s", title,
+                 sqlite3_errmsg (podcasts_source->priv->db));
+    g_set_error (error,
+                 GRL_CORE_ERROR,
+                 GRL_CORE_ERROR_STORE_FAILED,
+                 "Failed to store podcast '%s'", title);
+    sqlite3_finalize (sql_stmt);
+    return;
+  }
+
+  sqlite3_finalize (sql_stmt);
+
+  id = g_strdup_printf ("%llu",
+                        sqlite3_last_insert_rowid (podcasts_source->priv->db));
+  grl_media_set_id (podcast, id);
+  g_free (id);
+
+  if (podcasts_source->priv->notify_changes) {
+    grl_media_source_notify_change (GRL_MEDIA_SOURCE (podcasts_source),
+                                    NULL,
+                                    GRL_CONTENT_ADDED,
+                                    FALSE);
+  }
+}
+
+static void
+store_stream (sqlite3 *db, const gchar *podcast_id, Entry *entry)
+{
+  gint r;
+  guint seconds;
+  sqlite3_stmt *sql_stmt = NULL;
+
+  if (!entry->url || entry->url[0] == '\0') {
+    GRL_DEBUG ("Podcast stream has no URL, skipping");
+    return;
+  }
+
+  seconds = duration_to_seconds (entry->duration);
+  GRL_DEBUG ("%s", GRL_SQL_STORE_STREAM);
+  r = sqlite3_prepare_v2 (db,
+			  GRL_SQL_STORE_STREAM,
+			  strlen (GRL_SQL_STORE_STREAM),
+			  &sql_stmt, NULL);
+  if (r != SQLITE_OK) {
+    GRL_WARNING ("Failed to store podcast stream '%s': %s",
+                 entry->url, sqlite3_errmsg (db));
+    return;
+  }
+
+  sqlite3_bind_text (sql_stmt, 1, podcast_id, -1, SQLITE_STATIC);
+  sqlite3_bind_text (sql_stmt, 2, entry->url, -1, SQLITE_STATIC);
+  sqlite3_bind_text (sql_stmt, 3, entry->title, -1, SQLITE_STATIC);
+  sqlite3_bind_int  (sql_stmt, 4, seconds);
+  sqlite3_bind_text (sql_stmt, 5, entry->mime, -1, SQLITE_STATIC);
+  sqlite3_bind_text (sql_stmt, 6, entry->published, -1, SQLITE_STATIC);
+  sqlite3_bind_text (sql_stmt, 7, entry->summary, -1, SQLITE_STATIC);
+
+  while ((r = sqlite3_step (sql_stmt)) == SQLITE_BUSY);
+
+  if (r != SQLITE_DONE) {
+    GRL_WARNING ("Failed to store podcast stream '%s': %s",
+                 entry->url, sqlite3_errmsg (db));
+  }
+
+  sqlite3_finalize (sql_stmt);
+}
+
+static void
+parse_entry (xmlDocPtr doc, xmlNodePtr entry, Entry *data)
+{
+  xmlNodePtr node;
+  node = entry->xmlChildrenNode;
+  while (node) {
+    if (!xmlStrcmp (node->name, (const xmlChar *) "title")) {
+      data->title =
+	(gchar *) xmlNodeListGetString (doc, node->xmlChildrenNode, 1);
+    } else if (!xmlStrcmp (node->name, (const xmlChar *) "enclosure")) {
+      data->id = (gchar *) xmlGetProp (node, (xmlChar *) "url");
+      data->url = g_strdup (data->id);
+      data->mime = (gchar *) xmlGetProp (node, (xmlChar *) "type");
+    } else if (!xmlStrcmp (node->name, (const xmlChar *) "summary")) {
+      data->summary =
+	(gchar *) xmlNodeListGetString (doc, node->xmlChildrenNode, 1);
+    } else if (!xmlStrcmp (node->name, (const xmlChar *) "pubDate")) {
+      data->published =
+	(gchar *) xmlNodeListGetString (doc, node->xmlChildrenNode, 1);
+    } else if (!xmlStrcmp (node->name, (const xmlChar *) "duration")) {
+      data->duration =
+	(gchar *) xmlNodeListGetString (doc, node->xmlChildrenNode, 1);
+    }
+    node = node->next;
+  }
+}
+
+static void
+touch_podcast (sqlite3 *db, const gchar *podcast_id)
+{
+  gint r;
+  gchar *sql, *sql_error;
+  GTimeVal now;
+  gchar *now_str;
+
+  GRL_DEBUG ("touch_podcast");
+
+  g_get_current_time (&now);
+  now_str = g_time_val_to_iso8601 (&now);
+
+  sql = g_strdup_printf (GRL_SQL_TOUCH_PODCAST, now_str, podcast_id);
+  GRL_DEBUG ("%s", sql);
+  r = sqlite3_exec (db, sql, NULL, NULL, &sql_error);
+  g_free (sql);
+
+  if (r != SQLITE_OK) {
+    GRL_WARNING ("Failed to touch podcast, '%s': %s", podcast_id, sql_error);
+    sqlite3_free (sql_error);
+    return;
+  }
+}
+
+static gboolean
+parse_entry_idle (gpointer user_data)
+{
+  OperationSpecParse *osp = (OperationSpecParse *) user_data;
+  xmlNodeSetPtr nodes;
+  guint remaining;
+  GrlMedia *media;
+
+  nodes = osp->xpathObj->nodesetval;
+
+  /* Parse entry */
+  Entry *entry = g_slice_new0 (Entry);
+  if (nodes->nodeTab) {
+    parse_entry (osp->doc, nodes->nodeTab[osp->parse_index], entry);
+  }
+  if (0) print_entry (entry);
+
+  /* Check if entry is valid */
+  if (!entry->url || entry->url[0] == '\0') {
+    GRL_DEBUG ("Podcast stream has no URL, skipping");
+  } else {
+    /* Provide results to user as fast as possible */
+    if (osp->parse_valid_index >= osp->os->skip &&
+	osp->parse_valid_index < osp->os->skip + osp->os->count) {
+      media = build_media_from_entry (entry);
+      remaining = osp->os->skip + osp->os->count - osp->parse_valid_index - 1;
+
+      /* Hack: if we emit the last result now the UI may request more results
+	 right away while we are still parsing the XML, so we keep the last
+	 result until we are done processing the whole feed, this way when
+	 the next query arrives all the stuff is stored in the database
+	 and the query can be resolved normally */
+      if (remaining > 0) {
+	osp->os->callback (osp->os->source,
+			   osp->os->operation_id,
+			   media,
+			   remaining,
+			   osp->os->user_data,
+			   NULL);
+      } else {
+	osp->last_media = media;
+      }
+    }
+
+    osp->parse_valid_index++;
+
+    /* And store stream in database cache */
+    store_stream (GRL_PODCASTS_SOURCE (osp->os->source)->priv->db,
+		  osp->os->media_id, entry);
+  }
+
+  osp->parse_index++;
+  free_entry (entry);
+
+  if (osp->parse_index >= osp->parse_count) {
+    /* Send last result */
+    osp->os->callback (osp->os->source,
+		       osp->os->operation_id,
+		       osp->last_media,
+		       0,
+		       osp->os->user_data,
+		       NULL);
+    /* Notify about changes */
+    if (GRL_PODCASTS_SOURCE (osp->os->source)->priv->notify_changes) {
+      media = grl_media_box_new ();
+      grl_media_set_id (media, osp->os->media_id);
+      grl_media_source_notify_change (osp->os->source,
+                                      media,
+                                      GRL_CONTENT_CHANGED,
+                                      FALSE);
+      g_object_unref (media);
+    }
+    g_slice_free (OperationSpec, osp->os);
+    xmlXPathFreeObject (osp->xpathObj);
+    xmlXPathFreeContext (osp->xpathCtx);
+    xmlFreeDoc (osp->doc);
+    g_slice_free (OperationSpecParse, osp);
+  }
+
+  return osp->parse_index < osp->parse_count;
+}
+
+static void
+parse_feed (OperationSpec *os, const gchar *str, GError **error)
+{
+  GrlMedia *podcast = NULL;
+  xmlDocPtr doc = NULL;
+  xmlXPathContextPtr xpathCtx = NULL;
+  xmlXPathObjectPtr xpathObj = NULL;
+  guint stream_count;
+
+  GRL_DEBUG ("parse_feed");
+
+  doc = xmlParseDoc ((xmlChar *) str);
+  if (!doc) {
+    *error = g_error_new (GRL_CORE_ERROR,
+			  os->error_code,
+			  "Failed to parse podcast contents");
+    goto free_resources;
+  }
+
+  /* Get feed stream list */
+  xpathCtx = xmlXPathNewContext (doc);
+  if (!xpathCtx) {
+    *error = g_error_new (GRL_CORE_ERROR,
+			  os->error_code,
+			  "Failed to parse podcast contents");
+    goto free_resources;
+  }
+  
+  xpathObj = xmlXPathEvalExpression ((xmlChar *) "/rss/channel/item",
+				     xpathCtx);
+  if(xpathObj == NULL) {
+    *error = g_error_new (GRL_CORE_ERROR,
+			  os->error_code,
+			  "Failed to parse podcast contents");
+    goto free_resources;
+  }
+
+  /* Feed is ok, let's process it */
+
+  /* First, remove old entries for this podcast */
+  remove_podcast_streams (GRL_PODCASTS_SOURCE (os->source)->priv->db,
+			  os->media_id, error);
+  if (*error) {
+    (*error)->code = os->error_code;
+    goto free_resources;
+  }
+
+  /* Then update the last_refreshed date of the podcast */
+  touch_podcast (GRL_PODCASTS_SOURCE (os->source)->priv->db, os->media_id);
+
+  /* If the feed contains no streams, notify and bail out */
+  stream_count = xpathObj->nodesetval ? xpathObj->nodesetval->nodeNr : 0;
+  GRL_DEBUG ("Got %d streams", stream_count);
+  
+  if (stream_count <= 0) {
+    if (GRL_PODCASTS_SOURCE (os->source)->priv->notify_changes) {
+      podcast = grl_media_box_new ();
+      grl_media_set_id (podcast, os->media_id);
+      grl_media_source_notify_change (os->source,
+                                      podcast,
+                                      GRL_CONTENT_CHANGED,
+                                      FALSE);
+      g_object_unref (podcast);
+    }
+    os->callback (os->source,
+		  os->operation_id,
+		  NULL,
+		  0,
+		  os->user_data,
+		  NULL);
+    goto free_resources;
+  }
+
+  /* Otherwise parse the streams in idle loop to prevent blocking */
+  OperationSpecParse *osp = g_slice_new0 (OperationSpecParse);
+  osp->os = os;
+  osp->doc = doc;
+  osp->xpathCtx = xpathCtx;
+  osp->xpathObj = xpathObj;
+  osp->parse_count = stream_count;
+  g_idle_add (parse_entry_idle, osp);
+  return;
+
+ free_resources:
+  if (xpathObj)
+    xmlXPathFreeObject (xpathObj);
+  if (xpathCtx)
+    xmlXPathFreeContext (xpathCtx);
+  if (doc)
+    xmlFreeDoc (doc);
+}
+
+static void
+read_feed_cb (gchar *xmldata, gpointer user_data)
+{
+  GError *error = NULL;
+  OperationSpec *os = (OperationSpec *) user_data;
+
+  if (!xmldata) {
+    error = g_error_new (GRL_CORE_ERROR,
+			 GRL_CORE_ERROR_BROWSE_FAILED,
+			 "Failed to read data from podcast");
+  } else {
+    parse_feed (os, xmldata, &error);
+  }
+
+  if (error) {
+    os->callback (os->source,
+		  os->operation_id,
+		  NULL,
+		  0,
+		  os->user_data,
+		  error);
+    g_error_free (error);
+    g_slice_free (OperationSpec, os);
+  }
+}
+
+static sqlite3_stmt *
+get_podcast_info (sqlite3 *db, const gchar *podcast_id)
+{
+  gint r;
+  sqlite3_stmt *sql_stmt = NULL;
+  gchar *sql;
+
+  GRL_DEBUG ("get_podcast_info");
+
+  sql = g_strdup_printf (GRL_SQL_GET_PODCAST_BY_ID, podcast_id);
+  GRL_DEBUG ("%s", sql);
+  r = sqlite3_prepare_v2 (db, sql, strlen (sql), &sql_stmt, NULL);
+  g_free (sql);
+
+  if (r != SQLITE_OK) {
+    GRL_WARNING ("Failed to retrieve podcast '%s': %s",
+                 podcast_id, sqlite3_errmsg (db));
+    return NULL;
+  }
+
+  while ((r = sqlite3_step (sql_stmt)) == SQLITE_BUSY);
+
+  if (r == SQLITE_ROW) {
+    return sql_stmt;
+  } else {
+    GRL_WARNING ("Failed to retrieve podcast information: %s",
+                 sqlite3_errmsg (db));
+    sqlite3_finalize (sql_stmt);
+    return NULL;
+  }
+}
+
+static void
+produce_podcast_contents (OperationSpec *os)
+{
+  sqlite3_stmt *sql_stmt = NULL;
+  sqlite3 *db;
+  GError *error;
+  gchar *url;
+
+  GRL_DEBUG ("produce_podcast_contents");
+
+  /* First we get some information about the podcast */
+  db = GRL_PODCASTS_SOURCE (os->source)->priv->db;
+  sql_stmt = get_podcast_info (db, os->media_id);
+  if (sql_stmt) {
+    gchar *lr_str;
+    GTimeVal lr;
+    GTimeVal now;
+
+    /* Check if we have to refresh the podcast */
+    lr_str = (gchar *) sqlite3_column_text (sql_stmt, PODCAST_LAST_REFRESHED);
+    GRL_DEBUG ("Podcast last-refreshed: '%s'", lr_str);
+    g_time_val_from_iso8601 (lr_str ? lr_str : "", &lr);
+    g_get_current_time (&now);
+    now.tv_sec -= CACHE_DURATION;
+    if (now.tv_sec >= lr.tv_sec) {
+      /* We have to read the podcast feed again */
+      GRL_DEBUG ("Refreshing podcast '%s'...", os->media_id);
+      url = g_strdup ((gchar *) sqlite3_column_text (sql_stmt, PODCAST_URL));
+      read_url_async (GRL_PODCASTS_SOURCE (os->source), url, read_feed_cb, os);
+      g_free (url);
+    } else {
+      /* We can read the podcast entries from the database cache */
+      produce_podcast_contents_from_db (os);
+      g_slice_free (OperationSpec, os);
+    }
+    sqlite3_finalize (sql_stmt);
+  } else {
+    error = g_error_new (GRL_CORE_ERROR,
+			 os->error_code,
+			 "Failed to retrieve podcast information");
+    os->callback (os->source, os->operation_id, NULL, 0, os->user_data, error);
+    g_error_free (error);
+    g_slice_free (OperationSpec, os);
+  }
+
+}
+
+static void
+produce_podcasts (OperationSpec *os)
+{
+  gint r;
+  sqlite3_stmt *sql_stmt = NULL;
+  sqlite3 *db;
+  GrlMedia *media;
+  GError *error = NULL;
+  GList *medias = NULL;
+  guint count = 0;
+  GList *iter;
+  gchar *sql;
+
+  GRL_DEBUG ("produce_podcasts");
+
+  db = GRL_PODCASTS_SOURCE (os->source)->priv->db;
+
+  if (os->is_query) {
+    /* Query */
+    sql = g_strdup_printf (GRL_SQL_GET_PODCASTS_BY_QUERY,
+			   os->text, os->count, os->skip);
+  } else {
+    /* Browse */
+    sql = g_strdup_printf (GRL_SQL_GET_PODCASTS, os->count, os->skip);
+  }
+  GRL_DEBUG ("%s", sql);
+  r = sqlite3_prepare_v2 (db, sql, strlen (sql), &sql_stmt, NULL);
+  g_free (sql);
+
+  if (r != SQLITE_OK) {
+    GRL_WARNING ("Failed to retrieve podcasts: %s", sqlite3_errmsg (db));
+    error = g_error_new (GRL_CORE_ERROR,
+			 os->error_code,
+			 "Failed to retrieve podcasts list");
+    os->callback (os->source, os->operation_id, NULL, 0, os->user_data, error);
+    g_error_free (error);
+    goto free_resources;
+  }
+
+  while ((r = sqlite3_step (sql_stmt)) == SQLITE_BUSY);
+
+  while (r == SQLITE_ROW) {
+    media = build_media_from_stmt (NULL, sql_stmt, TRUE);
+    medias = g_list_prepend (medias, media);
+    count++;
+    r = sqlite3_step (sql_stmt);
+  }
+
+  if (r != SQLITE_DONE) {
+    GRL_WARNING ("Failed to retrieve podcasts: %s", sqlite3_errmsg (db));
+    error = g_error_new (GRL_CORE_ERROR,
+			 os->error_code,
+			 "Failed to retrieve podcasts list");
+    os->callback (os->source, os->operation_id, NULL, 0, os->user_data, error);
+    g_error_free (error);
+    goto free_resources;
+  }
+
+  if (count > 0) {
+    medias = g_list_reverse (medias);
+    iter = medias;
+    while (iter) {
+      media = GRL_MEDIA (iter->data);
+      os->callback (os->source,
+		    os->operation_id,
+		    media,
+		    --count,
+		    os->user_data,
+		    NULL);
+      iter = g_list_next (iter);
+    }
+    g_list_free (medias);
+  } else {
+    os->callback (os->source, os->operation_id, NULL, 0, os->user_data, NULL);
+  }
+
+ free_resources:
+  if (sql_stmt)
+    sqlite3_finalize (sql_stmt);
+}
+
+static void
+stream_metadata (GrlMediaSourceMetadataSpec *ms)
+{
+  gint r;
+  sqlite3_stmt *sql_stmt = NULL;
+  sqlite3 *db;
+  GError *error = NULL;
+  gchar *sql;
+  const gchar *id;
+
+  GRL_DEBUG ("stream_metadata");
+
+  db = GRL_PODCASTS_SOURCE (ms->source)->priv->db;
+
+  id = grl_media_get_id (ms->media);
+  sql = g_strdup_printf (GRL_SQL_GET_PODCAST_STREAM, id);
+  GRL_DEBUG ("%s", sql);
+  r = sqlite3_prepare_v2 (db, sql, strlen (sql), &sql_stmt, NULL);
+  g_free (sql);
+
+  if (r != SQLITE_OK) {
+    GRL_WARNING ("Failed to get podcast stream: %s", sqlite3_errmsg (db));
+    error = g_error_new (GRL_CORE_ERROR,
+			 GRL_CORE_ERROR_METADATA_FAILED,
+			 "Failed to get podcast stream metadata");
+    ms->callback (ms->source, ms->media, ms->user_data, error);
+    g_error_free (error);
+    return;
+  }
+
+  while ((r = sqlite3_step (sql_stmt)) == SQLITE_BUSY);
+
+  if (r == SQLITE_ROW) {
+    build_media_from_stmt (ms->media, sql_stmt, FALSE);
+    ms->callback (ms->source, ms->media, ms->user_data, NULL);
+  } else {
+    GRL_WARNING ("Failed to get podcast stream: %s", sqlite3_errmsg (db));
+    error = g_error_new (GRL_CORE_ERROR,
+			 GRL_CORE_ERROR_METADATA_FAILED,
+			 "Failed to get podcast stream metadata");
+    ms->callback (ms->source, ms->media, ms->user_data, error);
+    g_error_free (error);
+  }
+
+  sqlite3_finalize (sql_stmt);
+}
+
+static void
+podcast_metadata (GrlMediaSourceMetadataSpec *ms)
+{
+  sqlite3_stmt *sql_stmt = NULL;
+  sqlite3 *db;
+  GError *error = NULL;
+  const gchar *id;
+
+  GRL_DEBUG ("podcast_metadata");
+
+  db = GRL_PODCASTS_SOURCE (ms->source)->priv->db;
+
+  id = grl_media_get_id (ms->media);
+  if (!id) {
+    /* Root category: special case */
+    grl_media_set_title (ms->media, GRL_ROOT_TITLE);
+    ms->callback (ms->source, ms->media, ms->user_data, NULL);
+    return;
+  }
+
+  sql_stmt = get_podcast_info (db, id);
+
+  if (sql_stmt) {
+    build_media_from_stmt (ms->media, sql_stmt, TRUE);
+    ms->callback (ms->source, ms->media, ms->user_data, NULL);
+    sqlite3_finalize (sql_stmt);
+  } else {
+    GRL_WARNING ("Failed to get podcast: %s", sqlite3_errmsg (db));
+    error = g_error_new (GRL_CORE_ERROR,
+			 GRL_CORE_ERROR_METADATA_FAILED,
+			 "Failed to get podcast metadata");
+    ms->callback (ms->source, ms->media, ms->user_data, error);
+    g_error_free (error);
+  }
+}
+
+static gboolean
+media_id_is_podcast (const gchar *id)
+{
+  return g_ascii_strtoll (id, NULL, 10) != 0;
+}
+
+/* ================== API Implementation ================ */
+
+static const GList *
+grl_podcasts_source_supported_keys (GrlMetadataSource *source)
+{
+  static GList *keys = NULL;
+  if (!keys) {
+    keys = grl_metadata_key_list_new (GRL_METADATA_KEY_ID,
+                                      GRL_METADATA_KEY_TITLE,
+                                      GRL_METADATA_KEY_URL,
+                                      GRL_METADATA_KEY_CHILDCOUNT,
+                                      GRL_METADATA_KEY_SITE,
+                                      NULL);
+  }
+  return keys;
+}
+
+static void
+grl_podcasts_source_browse (GrlMediaSource *source,
+                            GrlMediaSourceBrowseSpec *bs)
+{
+  GRL_DEBUG ("grl_podcasts_source_browse");
+
+  OperationSpec *os;
+  GrlPodcastsSource *podcasts_source;
+  GError *error = NULL;
+
+  podcasts_source = GRL_PODCASTS_SOURCE (source);
+  if (!podcasts_source->priv->db) {
+    GRL_WARNING ("Can't execute operation: no database connection.");
+    error = g_error_new (GRL_CORE_ERROR,
+			 GRL_CORE_ERROR_BROWSE_FAILED,
+			 "No database connection");
+    bs->callback (bs->source, bs->browse_id, NULL, 0, bs->user_data, error);
+    g_error_free (error);
+    return;
+  }
+
+  /* Configure browse operation */
+  os = g_slice_new0 (OperationSpec);
+  os->source = bs->source;
+  os->operation_id = bs->browse_id;
+  os->media_id = grl_media_get_id (bs->container);
+  os->count = bs->count;
+  os->skip = bs->skip;
+  os->callback = bs->callback;
+  os->user_data = bs->user_data;
+  os->error_code = GRL_CORE_ERROR_BROWSE_FAILED;
+
+  if (!os->media_id) {
+    /* Browsing podcasts list */
+    produce_podcasts (os);
+    g_slice_free (OperationSpec, os);
+  } else {
+    /* Browsing a particular podcast. We may need to parse
+       the feed (async) and in that case we will need to keep
+       os, so we do not free os here */
+    produce_podcast_contents (os);
+  }
+}
+
+static void
+grl_podcasts_source_search (GrlMediaSource *source,
+                            GrlMediaSourceSearchSpec *ss)
+{
+  GRL_DEBUG ("grl_podcasts_source_search");
+
+  GrlPodcastsSource *podcasts_source;
+  OperationSpec *os;
+  GError *error = NULL;
+
+  podcasts_source = GRL_PODCASTS_SOURCE (source);
+  if (!podcasts_source->priv->db) {
+    GRL_WARNING ("Can't execute operation: no database connection.");
+    error = g_error_new (GRL_CORE_ERROR,
+			 GRL_CORE_ERROR_QUERY_FAILED,
+			 "No database connection");
+    ss->callback (ss->source, ss->search_id, NULL, 0, ss->user_data, error);
+    g_error_free (error);
+    return;
+  }
+
+  os = g_slice_new0 (OperationSpec);
+  os->source = ss->source;
+  os->operation_id = ss->search_id;
+  os->text = ss->text;
+  os->count = ss->count;
+  os->skip = ss->skip;
+  os->callback = ss->callback;
+  os->user_data = ss->user_data;
+  os->is_query = TRUE;
+  os->error_code = GRL_CORE_ERROR_SEARCH_FAILED;
+  produce_podcast_contents_from_db (os);
+  g_slice_free (OperationSpec, os);
+}
+
+static void
+grl_podcasts_source_query (GrlMediaSource *source, GrlMediaSourceQuerySpec *qs)
+{
+  GRL_DEBUG ("grl_podcasts_source_query");
+
+  GrlPodcastsSource *podcasts_source;
+  OperationSpec *os;
+  GError *error = NULL;
+
+  podcasts_source = GRL_PODCASTS_SOURCE (source);
+  if (!podcasts_source->priv->db) {
+    GRL_WARNING ("Can't execute operation: no database connection.");
+    error = g_error_new (GRL_CORE_ERROR,
+			 GRL_CORE_ERROR_QUERY_FAILED,
+			 "No database connection");
+    qs->callback (qs->source, qs->query_id, NULL, 0, qs->user_data, error);
+    g_error_free (error);
+    return;
+  }
+
+  os = g_slice_new0 (OperationSpec);
+  os->source = qs->source;
+  os->operation_id = qs->query_id;
+  os->text = qs->query;
+  os->count = qs->count;
+  os->skip = qs->skip;
+  os->callback = qs->callback;
+  os->user_data = qs->user_data;
+  os->is_query = TRUE;
+  os->error_code = GRL_CORE_ERROR_QUERY_FAILED;
+  produce_podcasts (os);
+  g_slice_free (OperationSpec, os);
+}
+
+static void
+grl_podcasts_source_metadata (GrlMediaSource *source,
+                              GrlMediaSourceMetadataSpec *ms)
+{
+  GRL_DEBUG ("grl_podcasts_source_metadata");
+
+  GrlPodcastsSource *podcasts_source;
+  GError *error = NULL;
+  const gchar *media_id;
+
+  podcasts_source = GRL_PODCASTS_SOURCE (source);
+  if (!podcasts_source->priv->db) {
+    GRL_WARNING ("Can't execute operation: no database connection.");
+    error = g_error_new (GRL_CORE_ERROR,
+			 GRL_CORE_ERROR_METADATA_FAILED,
+			 "No database connection");
+    ms->callback (ms->source, ms->media, ms->user_data, error);
+    g_error_free (error);
+    return;
+  }
+
+  media_id = grl_media_get_id (ms->media);
+  if (!media_id || media_id_is_podcast (media_id)) {
+    podcast_metadata (ms);
+  } else {
+    stream_metadata (ms);
+  }
+}
+
+static void
+grl_podcasts_source_store (GrlMediaSource *source, GrlMediaSourceStoreSpec *ss)
+{
+  GRL_DEBUG ("grl_podcasts_source_store");
+  GError *error = NULL;
+  if (GRL_IS_MEDIA_BOX (ss->media)) {
+    error = g_error_new (GRL_CORE_ERROR,
+			 GRL_CORE_ERROR_STORE_FAILED,
+			 "Cannot create containers. Only feeds are accepted.");
+  } else {
+    store_podcast (GRL_PODCASTS_SOURCE (ss->source),
+		   ss->media,
+		   &error);
+  }
+  ss->callback (ss->source, ss->parent, ss->media, ss->user_data, error);
+  if (error) {
+    g_error_free (error);
+  }
+}
+
+static void
+grl_podcasts_source_remove (GrlMediaSource *source,
+                            GrlMediaSourceRemoveSpec *rs)
+{
+  GRL_DEBUG ("grl_podcasts_source_remove");
+  GError *error = NULL;
+  if (media_id_is_podcast (rs->media_id)) {
+    remove_podcast (GRL_PODCASTS_SOURCE (rs->source),
+		    rs->media_id, &error);
+  } else {
+    remove_stream (GRL_PODCASTS_SOURCE (rs->source),
+		   rs->media_id, &error);
+  }
+  rs->callback (rs->source, rs->media, rs->user_data, error);
+  if (error) {
+    g_error_free (error);
+  }
+}
+
+static gboolean
+grl_podcasts_source_notify_change_start (GrlMediaSource *source,
+                                         GError **error)
+{
+  GrlPodcastsSource *podcasts_source = GRL_PODCASTS_SOURCE (source);
+
+  podcasts_source->priv->notify_changes = TRUE;
+
+  return TRUE;
+}
+
+static gboolean
+grl_podcasts_source_notify_change_stop (GrlMediaSource *source,
+                                        GError **error)
+{
+  GrlPodcastsSource *podcasts_source = GRL_PODCASTS_SOURCE (source);
+
+  podcasts_source->priv->notify_changes = FALSE;
+
+  return TRUE;
+}
diff --git a/src/media/podcasts/grl-podcasts.h b/src/media/podcasts/grl-podcasts.h
new file mode 100644
index 0000000..039ba64
--- /dev/null
+++ b/src/media/podcasts/grl-podcasts.h
@@ -0,0 +1,75 @@
+/*
+ * Copyright (C) 2010 Igalia S.L.
+ *
+ * Contact: Iago Toral Quiroga <itoral igalia 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_PODCASTS_SOURCE_H_
+#define _GRL_PODCASTS_SOURCE_H_
+
+#include <grilo.h>
+
+#define GRL_PODCASTS_SOURCE_TYPE                \
+  (grl_podcasts_source_get_type ())
+
+#define GRL_PODCASTS_SOURCE(obj)                                \
+  (G_TYPE_CHECK_INSTANCE_CAST ((obj),                           \
+                               GRL_PODCASTS_SOURCE_TYPE,        \
+                               GrlPodcastsSource))
+
+#define GRL_IS_PODCASTS_SOURCE(obj)                             \
+  (G_TYPE_CHECK_INSTANCE_TYPE ((obj),                           \
+                               GRL_PODCASTS_SOURCE_TYPE))
+
+#define GRL_PODCASTS_SOURCE_CLASS(klass)                \
+  (G_TYPE_CHECK_CLASS_CAST((klass),                     \
+                           GRL_PODCASTS_SOURCE_TYPE,    \
+                           GrlPodcastsSourceClass))
+
+#define GRL_IS_PODCASTS_SOURCE_CLASS(klass)             \
+  (G_TYPE_CHECK_CLASS_TYPE((klass),                     \
+                           GRL_PODCASTS_SOURCE_TYPE))
+
+#define GRL_PODCASTS_SOURCE_GET_CLASS(obj)              \
+  (G_TYPE_INSTANCE_GET_CLASS ((obj),                    \
+                              GRL_PODCASTS_SOURCE_TYPE, \
+                              GrlPodcastsSourceClass))
+
+typedef struct _GrlPodcastsPrivate GrlPodcastsPrivate;
+typedef struct _GrlPodcastsSource  GrlPodcastsSource;
+
+struct _GrlPodcastsSource {
+
+  GrlMediaSource parent;
+
+  /*< private >*/
+  GrlPodcastsPrivate *priv;
+};
+
+typedef struct _GrlPodcastsSourceClass GrlPodcastsSourceClass;
+
+struct _GrlPodcastsSourceClass {
+
+  GrlMediaSourceClass parent_class;
+
+};
+
+GType grl_podcasts_source_get_type (void);
+
+#endif /* _GRL_PODCASTS_SOURCE_H_ */
diff --git a/src/media/podcasts/grl-podcasts.xml b/src/media/podcasts/grl-podcasts.xml
new file mode 100644
index 0000000..1222f5c
--- /dev/null
+++ b/src/media/podcasts/grl-podcasts.xml
@@ -0,0 +1,9 @@
+<plugin>
+  <info>
+    <name>Podcasts</name>
+    <description>A plugin for browsing podcasts</description>
+    <author>Igalia S.L.</author>
+    <license>LGPL</license>
+    <site>http://www.igalia.com</site>
+  </info>
+</plugin>
diff --git a/src/media/shoutcast/Makefile.am b/src/media/shoutcast/Makefile.am
new file mode 100644
index 0000000..3940692
--- /dev/null
+++ b/src/media/shoutcast/Makefile.am
@@ -0,0 +1,37 @@
+#
+# Makefile.am
+#
+# Author: Juan A. Suarez Romero <jasuarez igalia com>
+#
+# Copyright (C) 2010, 2011 Igalia S.L. All rights reserved.
+
+lib_LTLIBRARIES		 = libgrlshoutcast.la
+
+libgrlshoutcast_la_CFLAGS =	\
+	$(DEPS_CFLAGS)		\
+	$(GRLNET_CFLAGS)	\
+	$(XML_CFLAGS)
+
+libgrlshoutcast_la_LIBADD =	\
+	$(DEPS_LIBS)		\
+	$(GRLNET_LIBS)		\
+	$(XML_LIBS)
+
+libgrlshoutcast_la_LDFLAGS = \
+	-module		     \
+	-avoid-version
+
+libgrlshoutcast_la_SOURCES	 = grl-shoutcast.c grl-shoutcast.h
+
+libdir=$(GRL_PLUGINS_DIR)
+
+shoutcastxmldir		= $(GRL_PLUGINS_CONF_DIR)
+shoutcastxml_DATA	= $(SHOUTCAST_PLUGIN_ID).xml
+
+EXTRA_DIST = $(shoutcastxml_DATA)
+
+MAINTAINERCLEANFILES =	\
+	*.in		\
+	*~
+
+DISTCLEANFILES = $(MAINTAINERCLEANFILES)
diff --git a/src/media/shoutcast/grl-shoutcast.c b/src/media/shoutcast/grl-shoutcast.c
new file mode 100644
index 0000000..6b08928
--- /dev/null
+++ b/src/media/shoutcast/grl-shoutcast.c
@@ -0,0 +1,727 @@
+/*
+ * Copyright (C) 2010 Igalia S.L.
+ *
+ * Contact: Iago Toral Quiroga <itoral igalia com>
+ *
+ * Authors: Juan A. Suarez Romero <jasuarez igalia 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 <grilo.h>
+#include <net/grl-net.h>
+#include <libxml/parser.h>
+#include <libxml/xpath.h>
+
+#include "grl-shoutcast.h"
+
+#define EXPIRE_CACHE_TIMEOUT 300
+
+/* --------- Logging  -------- */
+
+#define GRL_LOG_DOMAIN_DEFAULT shoutcast_log_domain
+GRL_LOG_DOMAIN_STATIC(shoutcast_log_domain);
+
+/* ------ SHOUTcast API ------ */
+
+#define SHOUTCAST_BASE_ENTRY "http://yp.shoutcast.com";
+
+#define SHOUTCAST_GET_GENRES    SHOUTCAST_BASE_ENTRY "/sbin/newxml.phtml"
+#define SHOUTCAST_GET_RADIOS    SHOUTCAST_GET_GENRES "?genre=%s&limit=%u"
+#define SHOUTCAST_SEARCH_RADIOS SHOUTCAST_GET_GENRES "?search=%s&limit=%u"
+#define SHOUTCAST_TUNE          SHOUTCAST_BASE_ENTRY "/sbin/tunein-station.pls?id=%s"
+
+/* --- Plugin information --- */
+
+#define PLUGIN_ID   SHOUTCAST_PLUGIN_ID
+
+#define SOURCE_ID   "grl-shoutcast"
+#define SOURCE_NAME "SHOUTcast"
+#define SOURCE_DESC "A source for browsing SHOUTcast radios"
+
+#define AUTHOR      "Igalia S.L."
+#define LICENSE     "LGPL"
+#define SITE        "http://www.igalia.com";
+
+typedef struct {
+  GrlMedia *media;
+  GrlMediaSource *source;
+  GrlMediaSourceMetadataCb metadata_cb;
+  GrlMediaSourceResultCb result_cb;
+  gboolean cancelled;
+  gboolean cache;
+  gchar *filter_entry;
+  gchar *genre;
+  gint error_code;
+  gint operation_id;
+  gint to_send;
+  gpointer user_data;
+  guint count;
+  guint skip;
+  xmlDocPtr xml_doc;
+  xmlNodePtr xml_entries;
+} OperationData;
+
+static GrlNetWc *wc = NULL;
+static GCancellable *cancellable;
+static gchar *cached_page = NULL;
+static gboolean cached_page_expired = TRUE;
+
+static GrlShoutcastSource *grl_shoutcast_source_new (void);
+
+gboolean grl_shoutcast_plugin_init (GrlPluginRegistry *registry,
+                                    const GrlPluginInfo *plugin,
+                                    GList *configs);
+
+static const GList *grl_shoutcast_source_supported_keys (GrlMetadataSource *source);
+
+static void grl_shoutcast_source_metadata (GrlMediaSource *source,
+                                           GrlMediaSourceMetadataSpec *ms);
+
+static void grl_shoutcast_source_browse (GrlMediaSource *source,
+                                         GrlMediaSourceBrowseSpec *bs);
+
+static void grl_shoutcast_source_search (GrlMediaSource *source,
+                                         GrlMediaSourceSearchSpec *ss);
+
+static void grl_shoutcast_source_cancel (GrlMediaSource *source,
+                                         guint operation_id);
+
+static void read_url_async (const gchar *url, OperationData *op_data);
+
+static void grl_shoutcast_source_finalize (GObject *object);
+
+/* =================== SHOUTcast Plugin  =============== */
+
+gboolean
+grl_shoutcast_plugin_init (GrlPluginRegistry *registry,
+                           const GrlPluginInfo *plugin,
+                           GList *configs)
+{
+  GRL_LOG_DOMAIN_INIT (shoutcast_log_domain, "shoutcast");
+
+  GRL_DEBUG ("shoutcast_plugin_init");
+
+  GrlShoutcastSource *source = grl_shoutcast_source_new ();
+  grl_plugin_registry_register_source (registry,
+                                       plugin,
+                                       GRL_MEDIA_PLUGIN (source),
+                                       NULL);
+  return TRUE;
+}
+
+GRL_PLUGIN_REGISTER (grl_shoutcast_plugin_init,
+                     NULL,
+                     PLUGIN_ID);
+
+/* ================== SHOUTcast GObject ================ */
+
+static GrlShoutcastSource *
+grl_shoutcast_source_new (void)
+{
+  GRL_DEBUG ("grl_shoutcast_source_new");
+  return g_object_new (GRL_SHOUTCAST_SOURCE_TYPE,
+		       "source-id", SOURCE_ID,
+		       "source-name", SOURCE_NAME,
+		       "source-desc", SOURCE_DESC,
+		       NULL);
+}
+
+static void
+grl_shoutcast_source_class_init (GrlShoutcastSourceClass * klass)
+{
+  GrlMediaSourceClass *source_class = GRL_MEDIA_SOURCE_CLASS (klass);
+  GrlMetadataSourceClass *metadata_class = GRL_METADATA_SOURCE_CLASS (klass);
+  GObjectClass *gobject_class = G_OBJECT_CLASS (klass);
+  source_class->metadata = grl_shoutcast_source_metadata;
+  source_class->browse = grl_shoutcast_source_browse;
+  source_class->search = grl_shoutcast_source_search;
+  source_class->cancel = grl_shoutcast_source_cancel;
+  metadata_class->supported_keys = grl_shoutcast_source_supported_keys;
+  gobject_class->finalize = grl_shoutcast_source_finalize;
+}
+
+static void
+grl_shoutcast_source_init (GrlShoutcastSource *source)
+{
+}
+
+G_DEFINE_TYPE (GrlShoutcastSource, grl_shoutcast_source, GRL_TYPE_MEDIA_SOURCE);
+
+static void
+grl_shoutcast_source_finalize (GObject *object)
+{
+  if (wc && GRL_IS_NET_WC (wc))
+    g_object_unref (wc);
+
+  if (cancellable && G_IS_CANCELLABLE (cancellable))
+    g_cancellable_cancel (cancellable);
+
+  G_OBJECT_CLASS (grl_shoutcast_source_parent_class)->finalize (object);
+}
+
+/* ======================= Private ==================== */
+
+static gint
+xml_count_nodes (xmlNodePtr node)
+{
+  gint count = 0;
+
+  while (node) {
+    count++;
+    node = node->next;
+  }
+
+  return count;
+}
+
+static GrlMedia *
+build_media_from_genre (OperationData *op_data)
+{
+  GrlMedia *media;
+  gchar *genre_name;
+
+  if (op_data->media) {
+    media = op_data->media;
+  } else {
+    media = grl_media_box_new ();
+  }
+
+  genre_name = (gchar *) xmlGetProp (op_data->xml_entries,
+                                     (const xmlChar *) "name");
+
+  grl_media_set_id (media, genre_name);
+  grl_media_set_title (media, genre_name);
+  grl_data_set_string (GRL_DATA (media),
+                       GRL_METADATA_KEY_GENRE,
+                       genre_name);
+  g_free (genre_name);
+
+  return media;
+}
+
+static GrlMedia *
+build_media_from_station (OperationData *op_data)
+{
+  GrlMedia *media;
+  gchar **station_genres = NULL;
+  gchar *media_id;
+  gchar *media_url;
+  gchar *station_bitrate;
+  gchar *station_genre;
+  gchar *station_genre_field;
+  gchar *station_id;
+  gchar *station_mime;
+  gchar *station_name;
+
+  station_name = (gchar *) xmlGetProp (op_data->xml_entries,
+                                       (const xmlChar *) "name");
+  station_mime = (gchar *) xmlGetProp (op_data->xml_entries,
+                                       (const xmlChar *) "mt");
+  station_id = (gchar *) xmlGetProp (op_data->xml_entries,
+                                     (const xmlChar *) "id");
+  station_bitrate = (gchar *) xmlGetProp (op_data->xml_entries,
+                                          (const xmlChar *) "br");
+  if (op_data->media) {
+    media = op_data->media;
+  } else {
+    media = grl_media_audio_new ();
+  }
+
+  if (op_data->genre) {
+    station_genre = op_data->genre;
+  } else {
+    station_genre_field = (gchar *) xmlGetProp (op_data->xml_entries,
+                                                (const xmlChar *) "genre");
+    station_genres = g_strsplit (station_genre_field, " ", -1);
+    g_free (station_genre_field);
+    station_genre = station_genres[0];
+  }
+
+  media_id = g_strconcat (station_genre, "/", station_id, NULL);
+  media_url = g_strdup_printf (SHOUTCAST_TUNE, station_id);
+
+  grl_media_set_id (media, media_id);
+  grl_media_set_title (media, station_name);
+  grl_media_set_mime (media, station_mime);
+  grl_media_audio_set_genre (GRL_MEDIA_AUDIO (media), station_genre);
+  grl_media_set_url (media, media_url);
+  grl_media_audio_set_bitrate (GRL_MEDIA_AUDIO (media),
+                               atoi (station_bitrate));
+
+  g_free (station_name);
+  g_free (station_mime);
+  g_free (station_id);
+  g_free (station_bitrate);
+  g_free (media_id);
+  g_free (media_url);
+  if (station_genres) {
+    g_strfreev (station_genres);
+  }
+
+  return media;
+}
+
+static gboolean
+send_media (OperationData *op_data, GrlMedia *media)
+{
+  if (!op_data->cancelled) {
+    op_data->result_cb (op_data->source,
+                        op_data->operation_id,
+                        media,
+                        --op_data->to_send,
+                        op_data->user_data,
+                        NULL);
+
+    op_data->xml_entries = op_data->xml_entries->next;
+  } else {
+    op_data->result_cb (op_data->source,
+                        op_data->operation_id,
+                        NULL,
+                        0,
+                        op_data->user_data,
+                        NULL);
+  }
+
+  if (op_data->to_send == 0 || op_data->cancelled) {
+    xmlFreeDoc (op_data->xml_doc);
+    g_slice_free (OperationData, op_data);
+    return FALSE;
+  } else {
+    return TRUE;
+  }
+}
+
+static gboolean
+send_genrelist_entries (OperationData *op_data)
+{
+  return send_media (op_data,
+                     build_media_from_genre (op_data));
+}
+
+static gboolean
+send_stationlist_entries (OperationData *op_data)
+{
+  return send_media (op_data,
+                     build_media_from_station (op_data));
+}
+
+static void
+xml_parse_result (const gchar *str, OperationData *op_data)
+{
+  GError *error = NULL;
+  gboolean stationlist_result;
+  gchar *xpath_expression;
+  xmlNodePtr node;
+  xmlXPathContextPtr xpath_ctx;
+  xmlXPathObjectPtr xpath_res;
+
+  if (op_data->cancelled) {
+    op_data->result_cb (op_data->source,
+                        op_data->operation_id,
+                        NULL,
+                        0,
+                        op_data->user_data,
+                        NULL);
+    g_slice_free (OperationData, op_data);
+    return;
+  }
+
+  op_data->xml_doc = xmlReadMemory (str, xmlStrlen ((xmlChar*) str), NULL, NULL,
+                                    XML_PARSE_RECOVER | XML_PARSE_NOBLANKS);
+  if (!op_data->xml_doc) {
+    error = g_error_new (GRL_CORE_ERROR,
+                         op_data->error_code,
+                         "Failed to parse SHOUTcast's response");
+    goto finalize;
+  }
+
+  node = xmlDocGetRootElement (op_data->xml_doc);
+  if  (!node) {
+    error = g_error_new (GRL_CORE_ERROR,
+                         op_data->error_code,
+                         "Empty response from SHOUTcast");
+    goto finalize;
+  }
+
+  stationlist_result = (xmlStrcmp (node->name,
+                                   (const xmlChar *) "stationlist") == 0);
+
+  op_data->xml_entries = node->xmlChildrenNode;
+
+  /* Check if we are interesting only in updating a media (that is, a metadata()
+     operation) or just browsing/searching */
+  if (op_data->media) {
+
+    /* Search for node */
+    xpath_ctx = xmlXPathNewContext (op_data->xml_doc);
+    if (xpath_ctx) {
+      if (stationlist_result) {
+        xpath_expression = g_strdup_printf ("//station[@id = \"%s\"]",
+                                            op_data->filter_entry);
+      } else {
+        xpath_expression = g_strdup_printf ("//genre[@name = \"%s\"]",
+                                            op_data->filter_entry);
+      }
+      xpath_res = xmlXPathEvalExpression ((xmlChar *) xpath_expression,
+                                          xpath_ctx);
+      g_free (xpath_expression);
+
+      if (xpath_res && xpath_res->nodesetval->nodeTab &&
+          xpath_res->nodesetval->nodeTab[0]) {
+        op_data->xml_entries = xpath_res->nodesetval->nodeTab[0];
+        if (stationlist_result) {
+          build_media_from_station (op_data);
+        } else {
+          build_media_from_genre (op_data);
+        }
+      } else {
+        error = g_error_new (GRL_CORE_ERROR,
+                             op_data->error_code,
+                             "Can not find media '%s'",
+                             grl_media_get_id (op_data->media));
+      }
+      if (xpath_res) {
+        xmlXPathFreeObject (xpath_res);
+      }
+      xmlXPathFreeContext (xpath_ctx);
+    } else {
+      error = g_error_new (GRL_CORE_ERROR,
+                           op_data->error_code,
+                           "Can not build xpath context");
+    }
+
+    op_data->metadata_cb (
+                          op_data->source,
+                          op_data->media,
+                          op_data->user_data,
+                          error);
+    goto free_resources;
+  }
+
+  if (stationlist_result) {
+    /* First node is "tunein"; skip it */
+    op_data->xml_entries = op_data->xml_entries->next;
+  }
+
+  /* Skip elements */
+  while (op_data->xml_entries && op_data->skip > 0) {
+    op_data->xml_entries = op_data->xml_entries->next;
+    op_data->skip--;
+  }
+
+  /* Check if there are elements to send*/
+  if (!op_data->xml_entries || op_data->count == 0) {
+    goto finalize;
+  }
+
+  /* Compute how many items are to be sent */
+  op_data->to_send = xml_count_nodes (op_data->xml_entries);
+  if (op_data->to_send > op_data->count) {
+    op_data->to_send = op_data->count;
+  }
+
+  if (stationlist_result) {
+    g_idle_add ((GSourceFunc) send_stationlist_entries, op_data);
+  } else {
+    g_idle_add ((GSourceFunc) send_genrelist_entries, op_data);
+  }
+
+  return;
+
+ finalize:
+  op_data->result_cb (op_data->source,
+                      op_data->operation_id,
+                      NULL,
+                      0,
+                      op_data->user_data,
+                      error);
+
+ free_resources:
+  if (op_data->xml_doc) {
+    xmlFreeDoc (op_data->xml_doc);
+  }
+
+  if (op_data->filter_entry) {
+    g_free (op_data->filter_entry);
+  }
+
+  if (error) {
+    g_error_free (error);
+  }
+  g_slice_free (OperationData, op_data);
+}
+
+static gboolean
+expire_cache (gpointer user_data)
+{
+  GRL_DEBUG ("Cached page expired");
+  cached_page_expired = TRUE;
+  return FALSE;
+}
+
+static void
+read_done_cb (GObject *source_object,
+              GAsyncResult *res,
+              gpointer user_data)
+{
+  GError *error = NULL;
+  GError *wc_error = NULL;
+  OperationData *op_data = (OperationData *) user_data;
+  gboolean cache;
+  gchar *content = NULL;
+
+  if (!grl_net_wc_request_finish (GRL_NET_WC (source_object),
+                            res,
+                            &content,
+                            NULL,
+                            &wc_error)) {
+    error = g_error_new (GRL_CORE_ERROR,
+                         op_data->error_code,
+                         "Failed to connect SHOUTcast: '%s'",
+                         wc_error->message);
+    op_data->result_cb (op_data->source,
+                        op_data->operation_id,
+                        NULL,
+                        0,
+                        op_data->user_data,
+                        error);
+    g_error_free (wc_error);
+    g_error_free (error);
+    g_slice_free (OperationData, op_data);
+
+    return;
+  }
+
+  cache = op_data->cache;
+  xml_parse_result (content, op_data);
+  if (cache && cached_page_expired) {
+    GRL_DEBUG ("Caching page");
+    g_free (cached_page);
+    cached_page = g_strdup (content);
+    cached_page_expired = FALSE;
+    g_timeout_add_seconds (EXPIRE_CACHE_TIMEOUT, expire_cache, NULL);
+  }
+}
+
+static gboolean
+read_cached_page (OperationData *op_data)
+{
+  xml_parse_result (cached_page, op_data);
+  return FALSE;
+}
+
+static void
+read_url_async (const gchar *url, OperationData *op_data)
+{
+  if (op_data->cache && !cached_page_expired) {
+    GRL_DEBUG ("Using cached page");
+    g_idle_add ((GSourceFunc) read_cached_page, op_data);
+  } else {
+    if (!wc)
+      wc = grl_net_wc_new ();
+
+    cancellable = g_cancellable_new ();
+    grl_net_wc_request_async (wc, url, cancellable, read_done_cb, op_data);
+  }
+}
+
+/* ================== API Implementation ================ */
+
+static const GList *
+grl_shoutcast_source_supported_keys (GrlMetadataSource *source)
+{
+  static GList *keys = NULL;
+  if (!keys) {
+    keys = grl_metadata_key_list_new (GRL_METADATA_KEY_BITRATE,
+                                      GRL_METADATA_KEY_GENRE,
+                                      GRL_METADATA_KEY_ID,
+                                      GRL_METADATA_KEY_MIME,
+                                      GRL_METADATA_KEY_TITLE,
+                                      GRL_METADATA_KEY_URL,
+                                      NULL);
+  }
+  return keys;
+}
+
+static void
+grl_shoutcast_source_metadata (GrlMediaSource *source,
+                               GrlMediaSourceMetadataSpec *ms)
+{
+  const gchar *media_id;
+  gchar **id_tokens;
+  gchar *url = NULL;
+  OperationData *data = NULL;
+
+  /* Unfortunately, shoutcast does not have an API to get information about a
+     station.  Thus, steps done to obtain the Content must be repeated. For
+     instance, if we have a Media with id "Pop/1321", it means that it is
+     station #1321 that was obtained after browsing "Pop" category. Thus we have
+     repeat the Pop browsing and get the result with station id 1321. If it
+     doesn't exist (think in results obtained from a search), we do nothing */
+
+  media_id = grl_media_get_id (ms->media);
+
+  /* Check if we need to report about root category */
+  if (!media_id) {
+    grl_media_set_title (ms->media, "SHOUTcast");
+  } else {
+    data = g_slice_new0 (OperationData);
+    data->source = source;
+    data->count = 1;
+    data->metadata_cb = ms->callback;
+    data->user_data = ms->user_data;
+    data->error_code = GRL_CORE_ERROR_METADATA_FAILED;
+    data->media = ms->media;
+
+    id_tokens = g_strsplit (media_id, "/", -1);
+
+    /* Check if Content is a media */
+    if (id_tokens[1]) {
+      data->filter_entry = g_strdup (id_tokens[1]);
+
+      /* Check if result is from a previous search */
+      if (id_tokens[0][0] == '?') {
+        url = g_strdup_printf (SHOUTCAST_SEARCH_RADIOS,
+                               id_tokens[0]+1,
+                               G_MAXINT);
+      } else {
+        url = g_strdup_printf (SHOUTCAST_GET_RADIOS,
+                               id_tokens[0],
+                               G_MAXINT);
+      }
+    } else {
+      data->filter_entry = g_strdup (id_tokens[0]);
+      data->cache = TRUE;
+      url = g_strdup (SHOUTCAST_GET_GENRES);
+    }
+
+    g_strfreev (id_tokens);
+  }
+
+  if (url) {
+    read_url_async (url, data);
+    g_free (url);
+  } else {
+    ms->callback (ms->source, ms->media, ms->user_data, NULL);
+  }
+}
+
+static void
+grl_shoutcast_source_browse (GrlMediaSource *source,
+                             GrlMediaSourceBrowseSpec *bs)
+{
+  OperationData *data;
+  const gchar *container_id;
+  gchar *url;
+
+  GRL_DEBUG ("grl_shoutcast_source_browse");
+
+  data = g_slice_new0 (OperationData);
+  data->source = source;
+  data->operation_id = bs->browse_id;
+  data->result_cb = bs->callback;
+  data->skip = bs->skip;
+  data->count = bs->count;
+  data->user_data = bs->user_data;
+  data->error_code = GRL_CORE_ERROR_BROWSE_FAILED;
+
+  container_id = grl_media_get_id (bs->container);
+
+  /* If it's root category send list of genres; else send list of radios */
+  if (!container_id) {
+    data->cache = TRUE;
+    url = g_strdup (SHOUTCAST_GET_GENRES);
+  } else {
+    url = g_strdup_printf (SHOUTCAST_GET_RADIOS,
+                           container_id,
+                           bs->skip + bs->count);
+    data->genre = g_strdup (container_id);
+  }
+
+  grl_media_source_set_operation_data (source, bs->browse_id, data);
+
+  read_url_async (url, data);
+
+  g_free (url);
+}
+
+static void
+grl_shoutcast_source_search (GrlMediaSource *source,
+                             GrlMediaSourceSearchSpec *ss)
+{
+  GError *error;
+  OperationData *data;
+  gchar *url;
+
+  /* Check if there is text to search */
+  if (!ss->text || ss->text[0] == '\0') {
+    error = g_error_new (GRL_CORE_ERROR,
+                         GRL_CORE_ERROR_SEARCH_FAILED,
+                         "Search text not specified");
+    ss->callback (ss->source,
+                  ss->search_id,
+                  NULL,
+                  0,
+                  ss->user_data,
+                  error);
+
+    g_error_free (error);
+    return;
+  }
+
+  data = g_slice_new0 (OperationData);
+  data->source = source;
+  data->operation_id = ss->search_id;
+  data->result_cb = ss->callback;
+  data->skip = ss->skip;
+  data->count = ss->count;
+  data->user_data = ss->user_data;
+  data->error_code = GRL_CORE_ERROR_SEARCH_FAILED;
+
+  grl_media_source_set_operation_data (source, ss->search_id, data);
+
+  url = g_strdup_printf (SHOUTCAST_SEARCH_RADIOS,
+                         ss->text,
+                         ss->skip + ss->count);
+
+  read_url_async (url, data);
+
+  g_free (url);
+}
+
+static void
+grl_shoutcast_source_cancel (GrlMediaSource *source, guint operation_id)
+{
+  OperationData *op_data;
+
+  GRL_DEBUG ("grl_shoutcast_source_cancel");
+
+  if (cancellable && G_IS_CANCELLABLE (cancellable))
+    g_cancellable_cancel (cancellable);
+  cancellable = NULL;
+
+  op_data = (OperationData *) grl_media_source_get_operation_data (source, operation_id);
+
+  if (op_data) {
+    op_data->cancelled = TRUE;
+  }
+}
diff --git a/src/media/shoutcast/grl-shoutcast.h b/src/media/shoutcast/grl-shoutcast.h
new file mode 100644
index 0000000..741fac0
--- /dev/null
+++ b/src/media/shoutcast/grl-shoutcast.h
@@ -0,0 +1,74 @@
+/*
+ * Copyright (C) 2010 Igalia S.L.
+ *
+ * Contact: Iago Toral Quiroga <itoral igalia com>
+ *
+ * Authors: Juan A. Suarez Romero <jasuarez igalia 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_SHOUTCAST_SOURCE_H_
+#define _GRL_SHOUTCAST_SOURCE_H_
+
+#include <grilo.h>
+
+#define GRL_SHOUTCAST_SOURCE_TYPE                 \
+  (grl_shoutcast_source_get_type ())
+
+#define GRL_SHOUTCAST_SOURCE(obj)                         \
+  (G_TYPE_CHECK_INSTANCE_CAST ((obj),                   \
+                               GRL_SHOUTCAST_SOURCE_TYPE, \
+                               GrlShoutcastSource))
+
+#define GRL_IS_SHOUTCAST_SOURCE(obj)                              \
+  (G_TYPE_CHECK_INSTANCE_TYPE ((obj),                           \
+                               GRL_SHOUTCAST_SOURCE_TYPE))
+
+#define GRL_SHOUTCAST_SOURCE_CLASS(klass)                 \
+  (G_TYPE_CHECK_CLASS_CAST((klass),                     \
+                           GRL_SHOUTCAST_SOURCE_TYPE,     \
+                           GrlShoutcastSourceClass))
+
+#define GRL_IS_SHOUTCAST_SOURCE_CLASS(klass)              \
+  (G_TYPE_CHECK_CLASS_TYPE((klass),                     \
+                           GRL_SHOUTCAST_SOURCE_TYPE))
+
+#define GRL_SHOUTCAST_SOURCE_GET_CLASS(obj)               \
+  (G_TYPE_INSTANCE_GET_CLASS ((obj),                    \
+                              GRL_SHOUTCAST_SOURCE_TYPE,  \
+                              GrlShoutcastSourceClass))
+
+typedef struct _GrlShoutcastSource GrlShoutcastSource;
+
+struct _GrlShoutcastSource {
+
+  GrlMediaSource parent;
+
+};
+
+typedef struct _GrlShoutcastSourceClass GrlShoutcastSourceClass;
+
+struct _GrlShoutcastSourceClass {
+
+  GrlMediaSourceClass parent_class;
+
+};
+
+GType grl_shoutcast_source_get_type (void);
+
+#endif /* _GRL_SHOUTCAST_SOURCE_H_ */
diff --git a/src/media/shoutcast/grl-shoutcast.xml b/src/media/shoutcast/grl-shoutcast.xml
new file mode 100644
index 0000000..2f9e916
--- /dev/null
+++ b/src/media/shoutcast/grl-shoutcast.xml
@@ -0,0 +1,9 @@
+<plugin>
+  <info>
+    <name>SHOUTcast</name>
+    <description>A plugin for browsing SHOUTcast radios</description>
+    <author>Igalia S.L.</author>
+    <license>LGPL</license>
+    <site>http://www.igalia.com</site>
+  </info>
+</plugin>
diff --git a/src/media/tracker/Makefile.am b/src/media/tracker/Makefile.am
new file mode 100644
index 0000000..5510f5a
--- /dev/null
+++ b/src/media/tracker/Makefile.am
@@ -0,0 +1,36 @@
+#
+# Makefile.am
+#
+# Author: Juan A. Suarez Romero <jasuarez igalia com>
+#
+# Copyright (C) 2011 Igalia S.L. All rights reserved.
+
+lib_LTLIBRARIES		 = libgrltracker.la
+
+libgrltracker_la_CFLAGS =	\
+	$(DEPS_CFLAGS)		\
+	$(TRACKER_SPARQL_CFLAGS)
+
+libgrltracker_la_LIBADD =	\
+	$(DEPS_LIBS)		\
+	$(TRACKER_SPARQL_LIBS)
+
+libgrltracker_la_LDFLAGS = \
+	-module		   \
+	-avoid-version
+
+libgrltracker_la_SOURCES = 	\
+	grl-tracker.c 		\
+	grl-tracker.h
+
+libdir = $(GRL_PLUGINS_DIR)
+trackerxmldir = $(GRL_PLUGINS_CONF_DIR)
+trackerxml_DATA	= $(TRACKER_PLUGIN_ID).xml
+
+EXTRA_DIST = $(trackerxml_DATA)
+
+MAINTAINERCLEANFILES =	\
+	*.in		\
+	*~
+
+DISTCLEANFILES = $(MAINTAINERCLEANFILES)
diff --git a/src/media/tracker/grl-tracker.c b/src/media/tracker/grl-tracker.c
new file mode 100644
index 0000000..b07627f
--- /dev/null
+++ b/src/media/tracker/grl-tracker.c
@@ -0,0 +1,1511 @@
+/*
+ * Copyright (C) 2011 Igalia S.L.
+ * Copyright (C) 2011 Intel Corporation.
+ *
+ * Contact: Iago Toral Quiroga <itoral igalia com>
+ *
+ * Authors: Juan A. Suarez Romero <jasuarez igalia 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 <grilo.h>
+#include <string.h>
+#include <gio/gio.h>
+#include <tracker-sparql.h>
+
+#include "grl-tracker.h"
+
+/* --------- Logging  -------- */
+
+#define GRL_LOG_DOMAIN_DEFAULT tracker_log_domain
+GRL_LOG_DOMAIN_STATIC(tracker_log_domain);
+
+/* ------- Definitions ------- */
+
+#define MEDIA_TYPE "grilo-media-type"
+
+#define RDF_TYPE_ALBUM  "nmm#MusicAlbum"
+#define RDF_TYPE_ARTIST "nmm#Artist"
+#define RDF_TYPE_AUDIO  "nfo#Audio"
+#define RDF_TYPE_MUSIC  "nmm#MusicPiece"
+#define RDF_TYPE_IMAGE  "nmm#Photo"
+#define RDF_TYPE_VIDEO  "nmm#Video"
+#define RDF_TYPE_BOX    "grilo#Box"
+
+/* ---- Plugin information --- */
+
+#define PLUGIN_ID   TRACKER_PLUGIN_ID
+
+#define SOURCE_ID   "grl-tracker"
+#define SOURCE_NAME "Tracker"
+#define SOURCE_DESC "A plugin for searching multimedia content using Tracker"
+
+#define AUTHOR      "Igalia S.L."
+#define LICENSE     "LGPL"
+#define SITE        "http://www.igalia.com";
+
+enum {
+  METADATA,
+  BROWSE,
+  QUERY,
+  SEARCH
+};
+
+/* --- Other --- */
+
+#define TRACKER_MOUNTED_DATASOURCES_START                    \
+  "SELECT nie:dataSource(?urn) AS ?datasource "              \
+  "(SELECT nie:url(tracker:mountPoint(?ds)) "                \
+  "WHERE { ?urn nie:dataSource ?ds  }) "                     \
+  "(SELECT GROUP_CONCAT(tracker:isMounted(?ds), \",\") "     \
+  "WHERE { ?urn nie:dataSource ?ds  }) "                     \
+  "WHERE { ?urn a nfo:FileDataObject . FILTER (?urn IN ("
+
+#define TRACKER_MOUNTED_DATASOURCES_END " ))} GROUP BY (?datasource)"
+
+#define TRACKER_DATASOURCES_REQUEST                                     \
+  "SELECT ?urn nie:dataSource(?urn) AS ?source "                        \
+  "(SELECT GROUP_CONCAT(nie:url(tracker:mountPoint(?ds)), \",\") "      \
+  "WHERE { ?urn nie:dataSource ?ds  }) "                                \
+  "WHERE { "                                                            \
+  "?urn tracker:available ?tr . "                                       \
+  "?source a tracker:Volume . "                                         \
+  "FILTER (bound(nie:dataSource(?urn))) "                               \
+  "} "                                                                  \
+  "GROUP BY (?source)"
+
+#define TRACKER_QUERY_REQUEST                                         \
+  "SELECT rdf:type(?urn) %s "                                         \
+  "WHERE { %s . %s } "                                                \
+  "ORDER BY DESC(nfo:fileLastModified(?urn)) "                        \
+  "OFFSET %i "                                                        \
+  "LIMIT %i"
+
+#define TRACKER_SEARCH_REQUEST                   \
+  "SELECT rdf:type(?urn) %s "                    \
+  "WHERE "                                       \
+  "{ "                                           \
+  "?urn a nfo:Media . "                          \
+  "?urn tracker:available ?tr . "                \
+  "?urn fts:match '%s' . "                       \
+  "%s "                                          \
+  "} "                                           \
+  "ORDER BY DESC(nfo:fileLastModified(?urn)) "   \
+  "OFFSET %i "                                   \
+  "LIMIT %i"
+
+#define TRACKER_SEARCH_ALL_REQUEST               \
+  "SELECT rdf:type(?urn) %s "                    \
+  "WHERE "                                       \
+  "{ "                                           \
+  "?urn a nfo:Media . "                          \
+  "?urn tracker:available ?tr . "                \
+  "%s "                                          \
+  "} "                                           \
+  "ORDER BY DESC(nfo:fileLastModified(?urn)) "   \
+  "OFFSET %i "                                   \
+  "LIMIT %i"
+
+#define TRACKER_BROWSE_CATEGORY_REQUEST                                 \
+  "SELECT rdf:type(?urn) %s "                                           \
+  "WHERE "                                                              \
+  "{ "                                                                  \
+  "?urn a %s . "                                                        \
+  "?urn tracker:available ?tr . "                                       \
+  "%s "                                                                 \
+  "} "                                                                  \
+  "ORDER BY DESC(nfo:fileLastModified(?urn)) "                          \
+  "OFFSET %i "                                                          \
+  "LIMIT %i"
+
+#define TRACKER_METADATA_REQUEST                                    \
+  "SELECT %s "							    \
+  "WHERE { ?urn a nie:DataObject . FILTER (?urn = <%s>) }"
+
+typedef struct {
+  GrlKeyID     grl_key;
+  const gchar *sparql_key_name;
+  const gchar *sparql_key_attr;
+  const gchar *sparql_key_flavor;
+} tracker_grl_sparql_t;
+
+typedef struct {
+  gboolean in_use;
+
+  GHashTable *updated_items;
+  GList *updated_items_list;
+  GList *updated_items_iter;
+
+  /* GList *updated_sources; */
+
+  TrackerSparqlCursor *cursor;
+} tracker_evt_update_t;
+
+struct OperationSpec {
+  GrlMediaSource         *source;
+  GrlTrackerSourcePriv   *priv;
+  guint                   operation_id;
+  GCancellable           *cancel_op;
+  const GList            *keys;
+  guint                   skip;
+  guint                   count;
+  guint                   current;
+  GrlMediaSourceResultCb  callback;
+  gpointer                user_data;
+  TrackerSparqlCursor    *cursor;
+};
+
+enum {
+  PROP_0,
+  PROP_TRACKER_CONNECTION,
+};
+
+struct _GrlTrackerSourcePriv {
+  TrackerSparqlConnection *tracker_connection;
+
+  GHashTable *operations;
+
+  gchar *tracker_datasource;
+};
+
+#define GRL_TRACKER_SOURCE_GET_PRIVATE(object)		\
+  (G_TYPE_INSTANCE_GET_PRIVATE((object),                \
+                               GRL_TRACKER_SOURCE_TYPE,	\
+                               GrlTrackerSourcePriv))
+
+static GrlTrackerSource *grl_tracker_source_new (TrackerSparqlConnection *connection);
+
+static void grl_tracker_source_set_property (GObject      *object,
+                                             guint         propid,
+                                             const GValue *value,
+                                             GParamSpec   *pspec);
+
+static void grl_tracker_source_constructed (GObject *object);
+
+static void grl_tracker_source_finalize (GObject *object);
+
+gboolean grl_tracker_plugin_init (GrlPluginRegistry *registry,
+                                  const GrlPluginInfo *plugin,
+                                  GList *configs);
+
+static const GList *grl_tracker_source_supported_keys (GrlMetadataSource *source);
+
+static void grl_tracker_source_query (GrlMediaSource *source,
+                                      GrlMediaSourceQuerySpec *qs);
+
+static void grl_tracker_source_metadata (GrlMediaSource *source,
+                                         GrlMediaSourceMetadataSpec *ms);
+
+static void grl_tracker_source_search (GrlMediaSource *source,
+                                       GrlMediaSourceSearchSpec *ss);
+
+static void grl_tracker_source_browse (GrlMediaSource *source,
+                                       GrlMediaSourceBrowseSpec *bs);
+
+static void grl_tracker_source_cancel (GrlMediaSource *source,
+                                       guint operation_id);
+
+static gchar *get_tracker_source_name (const gchar *uri,
+                                       const gchar *datasource);
+
+static void setup_key_mappings (void);
+
+/* ===================== Globals  ================= */
+
+static GHashTable *grl_to_sparql_mapping = NULL;
+static GHashTable *sparql_to_grl_mapping = NULL;
+
+static GVolumeMonitor *volume_monitor = NULL;
+static TrackerSparqlConnection *tracker_connection = NULL;
+static gboolean tracker_per_device_source = FALSE;
+static const GrlPluginInfo *tracker_grl_plugin;
+static guint tracker_dbus_signal_id = 0;
+
+/* =================== Tracker Plugin  =============== */
+
+static tracker_evt_update_t *
+tracker_evt_update_new (void)
+{
+  tracker_evt_update_t *evt = g_slice_new0 (tracker_evt_update_t);
+
+  evt->updated_items = g_hash_table_new (g_direct_hash, g_direct_equal);
+
+  return evt;
+}
+
+static void
+tracker_evt_update_free (tracker_evt_update_t *evt)
+{
+  if (!evt)
+    return;
+
+  g_hash_table_destroy (evt->updated_items);
+  g_list_free (evt->updated_items_list);
+
+  if (evt->cursor != NULL)
+    g_object_unref (evt->cursor);
+
+  g_slice_free (tracker_evt_update_t, evt);
+}
+
+static void
+grl_tracker_add_source (GrlTrackerSource *source)
+{
+  grl_plugin_registry_register_source (grl_plugin_registry_get_default (),
+                                       tracker_grl_plugin,
+                                       GRL_MEDIA_PLUGIN (source),
+                                       NULL);
+}
+
+static void
+grl_tracker_del_source (GrlTrackerSource *source)
+{
+  grl_plugin_registry_unregister_source (grl_plugin_registry_get_default (),
+                                         GRL_MEDIA_PLUGIN (source),
+                                         NULL);
+}
+
+static void
+tracker_evt_update_process_item_cb (GObject              *object,
+                                    GAsyncResult         *result,
+                                    tracker_evt_update_t *evt)
+{
+  const gchar *datasource, *uri;
+  gboolean source_mounted;
+  gchar *source_name = NULL;
+  GrlMediaPlugin *plugin;
+  GError *tracker_error = NULL;
+
+  GRL_DEBUG ("%s", __FUNCTION__);
+
+  if (!tracker_sparql_cursor_next_finish (evt->cursor,
+                                          result,
+                                          &tracker_error)) {
+    if (tracker_error != NULL) {
+      GRL_DEBUG ("\terror in parsing : %s", tracker_error->message);
+      g_error_free (tracker_error);
+    } else {
+      GRL_DEBUG ("\tend of parsing :)");
+    }
+
+    tracker_evt_update_free (evt);
+    return;
+  }
+
+  datasource = tracker_sparql_cursor_get_string (evt->cursor, 0, NULL);
+  uri = tracker_sparql_cursor_get_string (evt->cursor, 1, NULL);
+  source_mounted = tracker_sparql_cursor_get_boolean (evt->cursor, 2);
+
+  plugin = grl_plugin_registry_lookup_source (grl_plugin_registry_get_default (),
+                                              datasource);
+
+  GRL_DEBUG ("\tdatasource=%s uri=%s mounted=%i plugin=%p",
+             datasource, uri, source_mounted, plugin);
+
+  if (source_mounted) {
+    if (plugin == NULL) {
+      source_name = get_tracker_source_name (uri, datasource);
+
+      plugin = g_object_new (GRL_TRACKER_SOURCE_TYPE,
+                             "source-id", datasource,
+                             "source-name", source_name,
+                             "source-desc", SOURCE_DESC,
+                             "tracker-connection", tracker_connection,
+                             NULL);
+      grl_tracker_add_source (GRL_TRACKER_SOURCE (plugin));
+      g_free (source_name);
+    } else {
+      GRL_DEBUG ("\tChanges on source %p / %s", plugin, datasource);
+    }
+  } else if (!source_mounted && plugin != NULL) {
+    grl_tracker_del_source (GRL_TRACKER_SOURCE (plugin));
+  }
+
+  tracker_sparql_cursor_next_async (evt->cursor, NULL,
+                                    (GAsyncReadyCallback) tracker_evt_update_process_item_cb,
+                                    (gpointer) evt);
+}
+
+static void
+tracker_evt_update_process_cb (GObject              *object,
+                               GAsyncResult         *result,
+                               tracker_evt_update_t *evt)
+{
+  GError *tracker_error = NULL;
+
+  GRL_DEBUG ("%s", __FUNCTION__);
+
+  evt->cursor = tracker_sparql_connection_query_finish (tracker_connection,
+                                                        result, NULL);
+
+  if (tracker_error != NULL) {
+    GRL_WARNING ("Could not execute sparql query: %s", tracker_error->message);
+
+    g_error_free (tracker_error);
+    tracker_evt_update_free (evt);
+    return;
+  }
+
+  tracker_sparql_cursor_next_async (evt->cursor, NULL,
+                                    (GAsyncReadyCallback) tracker_evt_update_process_item_cb,
+                                    (gpointer) evt);
+}
+
+static void
+tracker_evt_update_process (tracker_evt_update_t *evt)
+{
+  GString *request_str = g_string_new (TRACKER_MOUNTED_DATASOURCES_START);
+
+  GRL_DEBUG ("%s", __FUNCTION__);
+
+  evt->updated_items_iter = evt->updated_items_list;
+  g_string_append_printf (request_str, "%i",
+                          GPOINTER_TO_INT (evt->updated_items_iter->data));
+  evt->updated_items_iter = evt->updated_items_iter->next;
+
+  while (evt->updated_items_iter != NULL) {
+    g_string_append_printf (request_str, ", %i",
+                            GPOINTER_TO_INT (evt->updated_items_iter->data));
+    evt->updated_items_iter = evt->updated_items_iter->next;
+  }
+
+  g_string_append (request_str, TRACKER_MOUNTED_DATASOURCES_END);
+
+  GRL_DEBUG ("\trequest : %s", request_str->str);
+
+  tracker_sparql_connection_query_async (tracker_connection,
+                                         request_str->str,
+                                         NULL,
+                                         (GAsyncReadyCallback) tracker_evt_update_process_cb,
+                                         evt);
+  g_string_free (request_str, TRUE);
+}
+
+static void
+tracker_dbus_signal_cb (GDBusConnection *connection,
+                        const gchar     *sender_name,
+                        const gchar     *object_path,
+                        const gchar     *interface_name,
+                        const gchar     *signal_name,
+                        GVariant        *parameters,
+                        gpointer         user_data)
+
+{
+  gchar *class_name;
+  gint graph = 0, subject = 0, predicate = 0, object = 0, subject_state;
+  GVariantIter *iter1, *iter2;
+  tracker_evt_update_t *evt = tracker_evt_update_new ();
+
+  GRL_DEBUG ("%s", __FUNCTION__);
+
+  g_variant_get (parameters, "(&sa(iiii)a(iiii))", &class_name, &iter1, &iter2);
+
+  GRL_DEBUG ("\tTracker update event for class=%s ins=%lu del=%lu",
+             class_name,
+             (unsigned long) g_variant_iter_n_children (iter1),
+             (unsigned long) g_variant_iter_n_children (iter2));
+
+  while (g_variant_iter_loop (iter1, "(iiii)", &graph,
+                              &subject, &predicate, &object)) {
+    subject_state = GPOINTER_TO_INT (g_hash_table_lookup (evt->updated_items,
+                                                          GSIZE_TO_POINTER (subject)));
+
+    if (subject_state == 0) {
+      g_hash_table_insert (evt->updated_items,
+                           GSIZE_TO_POINTER (subject),
+                           GSIZE_TO_POINTER (1));
+      evt->updated_items_list = g_list_append (evt->updated_items_list,
+                                               GSIZE_TO_POINTER (subject));
+    } else if (subject_state == 2)
+      evt->updated_items_list = g_list_append (evt->updated_items_list,
+                                               GSIZE_TO_POINTER (subject));
+  }
+  g_variant_iter_free (iter1);
+
+
+  while (g_variant_iter_loop (iter2, "(iiii)", &graph,
+                              &subject, &predicate, &object)) {
+    subject_state = GPOINTER_TO_INT (g_hash_table_lookup (evt->updated_items,
+                                                          GSIZE_TO_POINTER (subject)));
+
+    if (subject_state == 0) {
+      g_hash_table_insert (evt->updated_items,
+                           GSIZE_TO_POINTER (subject),
+                           GSIZE_TO_POINTER (1));
+      evt->updated_items_list = g_list_append (evt->updated_items_list,
+                                              GSIZE_TO_POINTER (subject));
+    } else if (subject_state == 2)
+      evt->updated_items_list = g_list_append (evt->updated_items_list,
+                                              GSIZE_TO_POINTER (subject));
+  }
+  g_variant_iter_free (iter2);
+
+  evt->updated_items_iter = evt->updated_items_list;
+
+  GRL_DEBUG ("\t%u elements updated", g_hash_table_size (evt->updated_items));
+  GRL_DEBUG ("\t%u elements updated (list)",
+             g_list_length (evt->updated_items_list));
+
+  tracker_evt_update_process (evt);
+}
+
+static void
+tracker_dbus_start_watch (void)
+{
+  GDBusConnection *connection = g_bus_get_sync (G_BUS_TYPE_SESSION, NULL, NULL);
+
+  tracker_dbus_signal_id = g_dbus_connection_signal_subscribe (connection,
+                                                               TRACKER_DBUS_SERVICE,
+                                                               TRACKER_DBUS_INTERFACE_RESOURCES,
+                                                               "GraphUpdated",
+                                                               TRACKER_DBUS_OBJECT_RESOURCES,
+                                                               NULL,
+                                                               G_DBUS_SIGNAL_FLAGS_NONE,
+                                                               tracker_dbus_signal_cb,
+                                                               NULL,
+                                                               NULL);
+}
+
+static void
+tracker_get_datasource_cb (GObject             *object,
+                           GAsyncResult        *result,
+                           TrackerSparqlCursor *cursor)
+{
+  const gchar *datasource, *uri;
+  gchar *source_name;
+  GError *tracker_error = NULL;
+  GrlTrackerSource *source;
+
+  GRL_DEBUG ("%s", __FUNCTION__);
+
+  if (!tracker_sparql_cursor_next_finish (cursor, result, &tracker_error)) {
+    if (tracker_error == NULL) {
+      GRL_DEBUG ("\tEnd of parsing of devices");
+    } else {
+      GRL_DEBUG ("\tError while parsing devices: %s", tracker_error->message);
+      g_error_free (tracker_error);
+    }
+    g_object_unref (G_OBJECT (cursor));
+    return;
+  }
+
+  datasource = tracker_sparql_cursor_get_string (cursor, 1, NULL);
+  uri = tracker_sparql_cursor_get_string (cursor, 2, NULL);
+  source = GRL_TRACKER_SOURCE (grl_plugin_registry_lookup_source (grl_plugin_registry_get_default (),
+                                                                  datasource));
+
+  if (source == NULL) {
+    source_name = get_tracker_source_name (uri, datasource); /* TODO: get a better name */
+
+    GRL_DEBUG ("\tnew datasource: urn=%s uri=%s\n", datasource, uri);
+
+    source = g_object_new (GRL_TRACKER_SOURCE_TYPE,
+                           "source-id", datasource,
+                           "source-name", source_name,
+                           "source-desc", SOURCE_DESC,
+                           "tracker-connection", tracker_connection,
+                           NULL);
+    grl_plugin_registry_register_source (grl_plugin_registry_get_default (),
+                                         tracker_grl_plugin,
+                                         GRL_MEDIA_PLUGIN (source),
+                                         NULL);
+    g_free (source_name);
+  }
+
+  tracker_sparql_cursor_next_async (cursor, NULL,
+                                    (GAsyncReadyCallback) tracker_get_datasource_cb,
+                                    cursor);
+}
+
+static void
+tracker_get_datasources_cb (GObject      *object,
+                            GAsyncResult *result,
+                            gpointer      data)
+{
+  TrackerSparqlCursor *cursor;
+
+  GRL_DEBUG ("%s", __FUNCTION__);
+
+  cursor = tracker_sparql_connection_query_finish (tracker_connection,
+                                                   result, NULL);
+
+  tracker_sparql_cursor_next_async (cursor, NULL,
+                                    (GAsyncReadyCallback) tracker_get_datasource_cb,
+                                    cursor);
+}
+
+static void
+tracker_get_connection_cb (GObject             *object,
+                           GAsyncResult        *res,
+                           const GrlPluginInfo *plugin)
+{
+  /* GrlTrackerSource *source; */
+
+  GRL_DEBUG ("%s", __FUNCTION__);
+
+  tracker_connection = tracker_sparql_connection_get_finish (res, NULL);
+
+  if (tracker_connection != NULL) {
+    if (tracker_per_device_source == TRUE) {
+      /* Let's discover available data sources. */
+      GRL_DEBUG ("per device source mode");
+
+      tracker_dbus_start_watch ();
+
+      volume_monitor = g_volume_monitor_get ();
+
+      tracker_sparql_connection_query_async (tracker_connection,
+                                             TRACKER_DATASOURCES_REQUEST,
+                                             NULL,
+                                             (GAsyncReadyCallback) tracker_get_datasources_cb,
+                                             NULL);
+    } else {
+      /* One source to rule them all. */
+      grl_tracker_add_source (grl_tracker_source_new (tracker_connection));
+    }
+  }
+}
+
+gboolean
+grl_tracker_plugin_init (GrlPluginRegistry *registry,
+                         const GrlPluginInfo *plugin,
+                         GList *configs)
+{
+  GrlConfig *config;
+  gint config_count;
+
+  GRL_LOG_DOMAIN_INIT (tracker_log_domain, "tracker");
+
+  GRL_DEBUG ("%s", __FUNCTION__);
+
+  tracker_grl_plugin = plugin;
+
+  if (!configs) {
+    GRL_WARNING ("Configuration not provided! Using default configuration.");
+  } else {
+    config_count = g_list_length (configs);
+    if (config_count > 1) {
+      GRL_WARNING ("Provided %i configs, but will only use one", config_count);
+    }
+
+    config = GRL_CONFIG (configs->data);
+
+    tracker_per_device_source = grl_config_get_boolean (config,
+                                                        "per-device-source");
+  }
+
+  tracker_sparql_connection_get_async (NULL,
+                                       (GAsyncReadyCallback) tracker_get_connection_cb,
+                                       (gpointer) plugin);
+  return TRUE;
+}
+
+GRL_PLUGIN_REGISTER (grl_tracker_plugin_init,
+                     NULL,
+                     PLUGIN_ID);
+
+/* ================== Tracker GObject ================ */
+
+static GrlTrackerSource *
+grl_tracker_source_new (TrackerSparqlConnection *connection)
+{
+  GRL_DEBUG ("%s", __FUNCTION__);
+
+  return g_object_new (GRL_TRACKER_SOURCE_TYPE,
+                       "source-id", SOURCE_ID,
+                       "source-name", SOURCE_NAME,
+                       "source-desc", SOURCE_DESC,
+                       "tracker-connection", connection,
+                       NULL);
+}
+
+G_DEFINE_TYPE (GrlTrackerSource, grl_tracker_source, GRL_TYPE_MEDIA_SOURCE);
+
+static void
+grl_tracker_source_class_init (GrlTrackerSourceClass * klass)
+{
+  GrlMediaSourceClass    *source_class   = GRL_MEDIA_SOURCE_CLASS (klass);
+  GrlMetadataSourceClass *metadata_class = GRL_METADATA_SOURCE_CLASS (klass);
+  GObjectClass           *g_class        = G_OBJECT_CLASS (klass);
+
+  source_class->query    = grl_tracker_source_query;
+  source_class->metadata = grl_tracker_source_metadata;
+  source_class->search   = grl_tracker_source_search;
+  source_class->browse   = grl_tracker_source_browse;
+  source_class->cancel   = grl_tracker_source_cancel;
+
+  metadata_class->supported_keys = grl_tracker_source_supported_keys;
+
+  g_class->finalize     = grl_tracker_source_finalize;
+  g_class->set_property = grl_tracker_source_set_property;
+  g_class->constructed  = grl_tracker_source_constructed;
+
+  g_object_class_install_property (g_class,
+                                   PROP_TRACKER_CONNECTION,
+                                   g_param_spec_object ("tracker-connection",
+                                                        "tracker connection",
+                                                        "A Tracker connection",
+                                                        TRACKER_SPARQL_TYPE_CONNECTION,
+                                                        G_PARAM_WRITABLE
+                                                        | G_PARAM_CONSTRUCT_ONLY
+                                                        | G_PARAM_STATIC_NAME));
+
+  g_type_class_add_private (klass, sizeof (GrlTrackerSourcePriv));
+
+  setup_key_mappings ();
+}
+
+static void
+grl_tracker_source_init (GrlTrackerSource *source)
+{
+  GrlTrackerSourcePriv *priv = GRL_TRACKER_SOURCE_GET_PRIVATE (source);
+
+  source->priv = priv;
+
+  priv->operations = g_hash_table_new (g_direct_hash, g_direct_equal);
+}
+
+static void
+grl_tracker_source_constructed (GObject *object)
+{
+  GrlTrackerSourcePriv *priv = GRL_TRACKER_SOURCE_GET_PRIVATE (object);
+
+  if (tracker_per_device_source)
+    g_object_get (object, "source-id", &priv->tracker_datasource, NULL);
+}
+
+static void
+grl_tracker_source_finalize (GObject *object)
+{
+  GrlTrackerSource *self;
+
+  self = GRL_TRACKER_SOURCE (object);
+  if (self->priv->tracker_connection)
+    g_object_unref (self->priv->tracker_connection);
+
+  G_OBJECT_CLASS (grl_tracker_source_parent_class)->finalize (object);
+}
+
+static void
+grl_tracker_source_set_property (GObject      *object,
+                                 guint         propid,
+                                 const GValue *value,
+                                 GParamSpec   *pspec)
+
+{
+  GrlTrackerSourcePriv *priv = GRL_TRACKER_SOURCE_GET_PRIVATE (object);
+
+  switch (propid) {
+  case PROP_TRACKER_CONNECTION:
+    if (priv->tracker_connection != NULL)
+      g_object_unref (G_OBJECT (priv->tracker_connection));
+    priv->tracker_connection = g_object_ref (g_value_get_object (value));
+    break;
+
+  default:
+    G_OBJECT_WARN_INVALID_PROPERTY_ID (object, propid, pspec);
+  }
+}
+
+/* ======================= Utilities ==================== */
+
+static gchar *
+get_tracker_source_name (const gchar *uri, const gchar *datasource)
+{
+  gchar *source_name = NULL;
+  GList *mounts, *mount;
+  GFile *file;
+
+  if (uri != NULL) {
+    mounts = g_volume_monitor_get_mounts (volume_monitor);
+    file = g_file_new_for_uri (uri);
+
+    mount = mounts;
+    while (mount != NULL) {
+      GFile *m_file = g_mount_get_root (G_MOUNT (mount->data));
+
+      if (g_file_equal (m_file, file)) {
+        gchar *m_name = g_mount_get_name (G_MOUNT (mount->data));
+        g_object_unref (G_OBJECT (m_file));
+        source_name = g_strdup_printf ("%s %s", SOURCE_NAME, m_name);
+        g_free (m_name);
+        break;
+      }
+      g_object_unref (G_OBJECT (m_file));
+
+      mount = mount->next;
+    }
+    g_list_foreach (mounts, (GFunc) g_object_unref, NULL);
+    g_list_free (mounts);
+    g_object_unref (G_OBJECT (file));
+
+    if (source_name == NULL)
+      source_name = g_strdup_printf  ("%s %s", SOURCE_NAME, datasource);
+  } else {
+    source_name = g_strdup (SOURCE_NAME " Local");
+  }
+
+  return source_name;
+}
+
+static gchar *
+build_flavored_key (gchar *key, const gchar *flavor)
+{
+  gint i = 0;
+
+  while (key[i] != '\0') {
+    if (!g_ascii_isalnum (key[i])) {
+      key[i] = '_';
+     }
+    i++;
+  }
+
+  return g_strdup_printf ("%s_%s", key, flavor);
+}
+
+static void
+insert_key_mapping (GrlKeyID     grl_key,
+                    const gchar *sparql_key_attr,
+                    const gchar *sparql_key_flavor)
+{
+  tracker_grl_sparql_t *assoc = g_slice_new0 (tracker_grl_sparql_t);
+  GList *assoc_list = g_hash_table_lookup (grl_to_sparql_mapping, grl_key);
+  gchar *canon_name = g_strdup (g_param_spec_get_name (grl_key));
+
+  assoc->grl_key           = grl_key;
+  assoc->sparql_key_name   = build_flavored_key (canon_name, sparql_key_flavor);
+  assoc->sparql_key_attr   = sparql_key_attr;
+  assoc->sparql_key_flavor = sparql_key_flavor;
+
+  assoc_list = g_list_append (assoc_list, assoc);
+
+  g_hash_table_insert (grl_to_sparql_mapping, grl_key, assoc_list);
+  g_hash_table_insert (sparql_to_grl_mapping,
+                       (gpointer) assoc->sparql_key_name,
+                       assoc);
+  g_hash_table_insert (sparql_to_grl_mapping,
+                       (gpointer) g_param_spec_get_name (G_PARAM_SPEC (grl_key)),
+                       assoc);
+
+  g_free (canon_name);
+}
+
+static void
+setup_key_mappings (void)
+{
+  grl_to_sparql_mapping = g_hash_table_new (g_direct_hash, g_direct_equal);
+  sparql_to_grl_mapping = g_hash_table_new (g_str_hash, g_str_equal);
+
+  insert_key_mapping (GRL_METADATA_KEY_ALBUM,
+                      "nmm:albumTitle(nmm:musicAlbum(?urn))",
+                      "audio");
+
+  insert_key_mapping (GRL_METADATA_KEY_ARTIST,
+                      "nmm:artistName(nmm:performer(?urn))",
+                      "audio");
+
+  insert_key_mapping (GRL_METADATA_KEY_AUTHOR,
+                      "nmm:artistName(nmm:performer(?urn))",
+                      "audio");
+
+  insert_key_mapping (GRL_METADATA_KEY_BITRATE,
+                      "nfo:averageBitrate(?urn)",
+                      "audio");
+
+  insert_key_mapping (GRL_METADATA_KEY_CHILDCOUNT,
+                      "nfo:entryCounter(?urn)",
+                      "directory");
+
+  insert_key_mapping (GRL_METADATA_KEY_DATE,
+                      "nfo:fileLastModified(?urn)",
+                      "file");
+
+  insert_key_mapping (GRL_METADATA_KEY_DURATION,
+                      "nfo:duration(?urn)",
+                      "audio");
+
+  insert_key_mapping (GRL_METADATA_KEY_FRAMERATE,
+                      "nfo:frameRate(?urn)",
+                      "video");
+
+  insert_key_mapping (GRL_METADATA_KEY_HEIGHT,
+                      "nfo:height(?urn)",
+                      "video");
+
+  insert_key_mapping (GRL_METADATA_KEY_ID,
+                      "?urn",
+                      "file");
+
+  insert_key_mapping (GRL_METADATA_KEY_LAST_PLAYED,
+                      "nfo:fileLastAccessed(?urn)",
+                      "file");
+
+  insert_key_mapping (GRL_METADATA_KEY_MIME,
+                      "nie:mimeType(?urn)",
+                      "file");
+
+  insert_key_mapping (GRL_METADATA_KEY_SITE,
+                      "nie:url(?urn)",
+                      "file");
+
+  insert_key_mapping (GRL_METADATA_KEY_TITLE,
+                      "nie:title(?urn)",
+                      "audio");
+
+  insert_key_mapping (GRL_METADATA_KEY_TITLE,
+                      "nfo:fileName(?urn)",
+                      "file");
+
+  insert_key_mapping (GRL_METADATA_KEY_URL,
+                      "nie:url(?urn)",
+                      "file");
+
+  insert_key_mapping (GRL_METADATA_KEY_WIDTH,
+                      "nfo:width(?urn)",
+                      "video");
+}
+
+static tracker_grl_sparql_t *
+get_mapping_from_sparql (const gchar *key)
+{
+  return (tracker_grl_sparql_t *) g_hash_table_lookup (sparql_to_grl_mapping,
+                                                       key);
+}
+
+static GList *
+get_mapping_from_grl (const GrlKeyID key)
+{
+  return (GList *) g_hash_table_lookup (grl_to_sparql_mapping, key);
+}
+
+static struct OperationSpec *
+tracker_operation_initiate (GrlMediaSource *source,
+                            GrlTrackerSourcePriv *priv,
+                            guint operation_id)
+{
+  struct OperationSpec *os = g_slice_new0 (struct OperationSpec);
+
+  os->source       = source;
+  os->priv         = priv;
+  os->operation_id = operation_id;
+  os->cancel_op    = g_cancellable_new ();
+
+  g_hash_table_insert (priv->operations, GSIZE_TO_POINTER (operation_id), os);
+
+  return os;
+}
+
+static void
+tracker_operation_terminate (struct OperationSpec *os)
+{
+  if (os == NULL)
+    return;
+
+  g_hash_table_remove (os->priv->operations,
+                       GSIZE_TO_POINTER (os->operation_id));
+
+  g_object_unref (G_OBJECT (os->cursor));
+  g_object_unref (G_OBJECT (os->cancel_op));
+  g_slice_free (struct OperationSpec, os);
+}
+
+static gchar *
+get_select_string (GrlMediaSource *source, const GList *keys)
+{
+  const GList *key = keys;
+  GString *gstr = g_string_new ("");
+  GList *assoc_list;
+  tracker_grl_sparql_t *assoc;
+
+  while (key != NULL) {
+    assoc_list = get_mapping_from_grl ((GrlKeyID) key->data);
+    while (assoc_list != NULL) {
+      assoc = (tracker_grl_sparql_t *) assoc_list->data;
+      if (assoc != NULL) {
+        g_string_append_printf (gstr, "%s AS %s",
+                                assoc->sparql_key_attr,
+                                assoc->sparql_key_name);
+        g_string_append (gstr, " ");
+      }
+      assoc_list = assoc_list->next;
+    }
+    key = key->next;
+  }
+
+  return g_string_free (gstr, FALSE);
+}
+
+/* Builds an appropriate GrlMedia based on ontology type returned by tracker, or
+   NULL if unknown */
+static GrlMedia *
+build_grilo_media (const gchar *rdf_type)
+{
+  GrlMedia *media = NULL;
+  gchar **rdf_single_type;
+  int i;
+
+  if (!rdf_type) {
+    return NULL;
+  }
+
+  /* As rdf_type can be formed by several types, split them */
+  rdf_single_type = g_strsplit (rdf_type, ",", -1);
+  i = g_strv_length (rdf_single_type) - 1;
+
+  while (!media && i >= 0) {
+    if (g_str_has_suffix (rdf_single_type[i], RDF_TYPE_MUSIC)) {
+      media = grl_media_audio_new ();
+    } else if (g_str_has_suffix (rdf_single_type[i], RDF_TYPE_VIDEO)) {
+      media = grl_media_video_new ();
+    } else if (g_str_has_suffix (rdf_single_type[i], RDF_TYPE_IMAGE)) {
+      media = grl_media_image_new ();
+    } else if (g_str_has_suffix (rdf_single_type[i], RDF_TYPE_ARTIST)) {
+      media = grl_media_box_new ();
+    } else if (g_str_has_suffix (rdf_single_type[i], RDF_TYPE_ALBUM)) {
+      media = grl_media_box_new ();
+    } else if (g_str_has_suffix (rdf_single_type[i], RDF_TYPE_BOX)) {
+      media = grl_media_box_new ();
+    }
+    i--;
+  }
+
+  g_strfreev (rdf_single_type);
+
+  return media;
+}
+
+static void
+fill_grilo_media_from_sparql (GrlMedia            *media,
+                              TrackerSparqlCursor *cursor,
+                              gint                 column)
+{
+  const gchar *sparql_key = tracker_sparql_cursor_get_variable_name (cursor, column);
+  tracker_grl_sparql_t *assoc = get_mapping_from_sparql (sparql_key);;
+  union {
+    gint int_val;
+    gdouble double_val;
+    const gchar *str_val;
+  } val;
+
+  if (assoc == NULL)
+    return;
+
+  GRL_DEBUG ("\tSetting media prop (col=%i/var=%s/prop=%s) %s",
+             column,
+             sparql_key,
+             g_param_spec_get_name (G_PARAM_SPEC (assoc->grl_key)),
+             tracker_sparql_cursor_get_string (cursor, column, NULL));
+
+  if (tracker_sparql_cursor_is_bound (cursor, column) == FALSE) {
+    GRL_DEBUG ("\t\tDropping, no data");
+    return;
+  }
+
+  if (grl_data_has_key (GRL_DATA (media), assoc->grl_key)) {
+    GRL_DEBUG ("\t\tDropping, already here");
+    return;
+  }
+
+  switch (G_PARAM_SPEC (assoc->grl_key)->value_type) {
+  case G_TYPE_STRING:
+    val.str_val = tracker_sparql_cursor_get_string (cursor, column, NULL);
+    if (val.str_val != NULL)
+      grl_data_set_string (GRL_DATA (media), assoc->grl_key, val.str_val);
+    break;
+
+  case G_TYPE_INT:
+    val.int_val = tracker_sparql_cursor_get_integer (cursor, column);
+    grl_data_set_int (GRL_DATA (media), assoc->grl_key, val.int_val);
+    break;
+
+  case G_TYPE_FLOAT:
+    val.double_val = tracker_sparql_cursor_get_double (cursor, column);
+    grl_data_set_float (GRL_DATA (media), assoc->grl_key, (gfloat) val.double_val);
+    break;
+
+  default:
+    GRL_DEBUG ("\t\tUnexpected data type");
+    break;
+  }
+}
+
+static void
+tracker_query_result_cb (GObject              *source_object,
+                         GAsyncResult         *result,
+                         struct OperationSpec *operation)
+{
+  gint         col;
+  const gchar *sparql_type;
+  GError      *tracker_error = NULL, *error = NULL;
+  GrlMedia    *media;
+
+  GRL_DEBUG ("%s", __FUNCTION__);
+
+  if (g_cancellable_is_cancelled (operation->cancel_op)) {
+    GRL_DEBUG ("\tOperation %u cancelled", operation->operation_id);
+    operation->callback (operation->source,
+                         operation->operation_id,
+                         NULL, 0,
+                         operation->user_data, NULL);
+    tracker_operation_terminate (operation);
+
+    return;
+  }
+
+  if (!tracker_sparql_cursor_next_finish (operation->cursor,
+                                          result,
+                                          &tracker_error)) {
+    if (tracker_error != NULL) {
+      GRL_DEBUG ("\terror in parsing : %s", tracker_error->message);
+
+      error = g_error_new (GRL_CORE_ERROR,
+                           GRL_CORE_ERROR_BROWSE_FAILED,
+                           "Failed to start browse action : %s",
+                           tracker_error->message);
+
+      operation->callback (operation->source,
+                           operation->operation_id,
+                           NULL, 0,
+                           operation->user_data, error);
+
+      g_error_free (error);
+      g_error_free (tracker_error);
+    } else {
+      GRL_DEBUG ("\tend of parsing :)");
+
+      /* Only emit this last one if more result than expected */
+      if (operation->count > 1)
+        operation->callback (operation->source,
+                             operation->operation_id,
+                             NULL, 0,
+                             operation->user_data, NULL);
+    }
+
+    tracker_operation_terminate (operation);
+    return;
+  }
+
+  sparql_type = tracker_sparql_cursor_get_string (operation->cursor, 0, NULL);
+
+  GRL_DEBUG ("Parsing line %i of type %s", operation->current, sparql_type);
+
+  media = build_grilo_media (sparql_type);
+
+  if (media != NULL) {
+    for (col = 1 ;
+         col < tracker_sparql_cursor_get_n_columns (operation->cursor) ;
+         col++) {
+      fill_grilo_media_from_sparql (media, operation->cursor, col);
+    }
+
+    operation->callback (operation->source,
+                         operation->operation_id,
+                         media,
+                         --operation->count,
+                         operation->user_data,
+                         NULL);
+  }
+
+  /* Schedule the next line to parse */
+  operation->current++;
+  if (operation->count < 1)
+        tracker_operation_terminate (operation);
+  else
+    tracker_sparql_cursor_next_async (operation->cursor, operation->cancel_op,
+                                      (GAsyncReadyCallback) tracker_query_result_cb,
+                                      (gpointer) operation);
+}
+
+static void
+tracker_query_cb (GObject              *source_object,
+                  GAsyncResult         *result,
+                  struct OperationSpec *operation)
+{
+  GError *tracker_error = NULL, *error = NULL;
+
+  GRL_DEBUG ("%s", __FUNCTION__);
+
+  operation->cursor =
+    tracker_sparql_connection_query_finish (operation->priv->tracker_connection,
+                                            result, &tracker_error);
+
+  if (tracker_error) {
+    GRL_WARNING ("Could not execute sparql query: %s", tracker_error->message);
+
+    error = g_error_new (GRL_CORE_ERROR,
+			 GRL_CORE_ERROR_BROWSE_FAILED,
+			 "Failed to start browse action : %s",
+                         tracker_error->message);
+
+    operation->callback (operation->source, operation->operation_id, NULL, 0,
+                         operation->user_data, error);
+
+    g_error_free (tracker_error);
+    g_error_free (error);
+    g_slice_free (struct OperationSpec, operation);
+
+    return;
+  }
+
+  /* Start parsing results */
+  operation->current = 0;
+  tracker_sparql_cursor_next_async (operation->cursor, NULL,
+                                    (GAsyncReadyCallback) tracker_query_result_cb,
+                                    (gpointer) operation);
+}
+
+static void
+tracker_metadata_cb (GObject                    *source_object,
+                     GAsyncResult               *result,
+                     GrlMediaSourceMetadataSpec *ms)
+{
+  GrlTrackerSourcePriv *priv = GRL_TRACKER_SOURCE_GET_PRIVATE (ms->source);
+  gint                  col;
+  GError               *tracker_error = NULL, *error = NULL;
+  TrackerSparqlCursor  *cursor;
+
+  GRL_DEBUG ("%s", __FUNCTION__);
+
+  cursor = tracker_sparql_connection_query_finish (priv->tracker_connection,
+                                                   result, &tracker_error);
+
+  if (tracker_error) {
+    GRL_WARNING ("Could not execute sparql query: %s", tracker_error->message);
+
+    error = g_error_new (GRL_CORE_ERROR,
+			 GRL_CORE_ERROR_BROWSE_FAILED,
+			 "Failed to start browse action : %s",
+                         tracker_error->message);
+
+    ms->callback (ms->source, NULL, ms->user_data, error);
+
+    g_error_free (tracker_error);
+    g_error_free (error);
+
+    goto end_operation;
+  }
+
+
+  tracker_sparql_cursor_next (cursor, NULL, NULL);
+
+  /* Translate Sparql result into Grilo result */
+  for (col = 0 ; col < tracker_sparql_cursor_get_n_columns (cursor) ; col++) {
+    fill_grilo_media_from_sparql (ms->media, cursor, col);
+  }
+
+  ms->callback (ms->source, ms->media, ms->user_data, NULL);
+
+ end_operation:
+  if (cursor)
+    g_object_unref (G_OBJECT (cursor));
+}
+
+static gchar *
+tracker_source_get_device_constraint (GrlTrackerSourcePriv *priv)
+{
+  if (priv->tracker_datasource == NULL)
+    return g_strdup ("");
+
+  return g_strdup_printf ("?urn nie:dataSource <%s> .",
+                          priv->tracker_datasource);
+}
+
+/* ================== API Implementation ================ */
+
+static const GList *
+grl_tracker_source_supported_keys (GrlMetadataSource *source)
+{
+  return grl_plugin_registry_get_metadata_keys (grl_plugin_registry_get_default ());
+}
+
+/**
+ * Query is a SPARQL query.
+ *
+ * Columns must be named with the Grilo key name that the column
+ * represent. Unnamed or unknown columns will be ignored.
+ *
+ * First column must be the media type, and it does not need to be named.  It
+ * must match with any value supported in rdf:type() property, or
+ * grilo#Box. Types understood are:
+ *
+ * <itemizedlist>
+ *   <listitem>
+ *     <para>
+ *       <literal>nmm#MusicPiece</literal>
+ *     </para>
+ *   </listitem>
+ *   <listitem>
+ *     <para>
+ *       <literal>nmm#Video</literal>
+ *     </para>
+ *   </listitem>
+ *   <listitem>
+ *     <para>
+ *       <literal>nmm#Photo</literal>
+ *     </para>
+ *   </listitem>
+ *   <listitem>
+ *     <para>
+ *       <literal>nmm#Artist</literal>
+ *     </para>
+ *   </listitem>
+ *   <listitem>
+ *     <para>
+ *       <literal>nmm#MusicAlbum</literal>
+ *     </para>
+ *   </listitem>
+ *   <listitem>
+ *     <para>
+ *       <literal>grilo#Box</literal>
+ *     </para>
+ *   </listitem>
+ * </itemizedlist>
+ *
+ * An example for searching all songs:
+ *
+ * <informalexample>
+ *   <programlisting>
+ *     SELECT rdf:type(?song)
+ *            ?song            AS id
+ *            nie:title(?song) AS title
+ *            nie:url(?song)   AS url
+ *     WHERE { ?song a nmm:MusicPiece }
+ *   </programlisting>
+ * </informalexample>
+ *
+ * Alternatively, we can use a partial SPARQL query: just specify the sentence
+ * in the WHERE part. In this case, "?urn" is the ontology concept to be used in
+ * the clause.
+ *
+ * An example of such partial query:
+ *
+ * <informalexample>
+ *   <programlisting>
+ *     ?urn a nfo:Media
+ *   </programlisting>
+ * </informalexample>
+ *
+ * In this case, all data required to build a full SPARQL query will be get from
+ * the query spec.
+ */
+static void
+grl_tracker_source_query (GrlMediaSource *source,
+                          GrlMediaSourceQuerySpec *qs)
+{
+  GError               *error = NULL;
+  GrlTrackerSourcePriv *priv  = GRL_TRACKER_SOURCE_GET_PRIVATE (source);
+  gchar                *constraint;
+  gchar                *sparql_final;
+  gchar                *sparql_select;
+  struct OperationSpec *os;
+
+  GRL_DEBUG ("%s: id=%u", __FUNCTION__, qs->query_id);
+
+  if (!qs->query || qs->query[0] == '\0') {
+    error = g_error_new_literal (GRL_CORE_ERROR,
+                                 GRL_CORE_ERROR_QUERY_FAILED,
+                                 "Empty query");
+    goto send_error;
+  }
+
+  /* Check if it is a full sparql query */
+  if (g_ascii_strncasecmp (qs->query, "select ", 7) != 0) {
+    constraint = tracker_source_get_device_constraint (priv);
+    sparql_select = get_select_string (source, qs->keys);
+    sparql_final = g_strdup_printf (TRACKER_QUERY_REQUEST,
+                                    sparql_select,
+                                    qs->query,
+                                    constraint,
+                                    qs->skip,
+                                    qs->count);
+    g_free (constraint);
+    g_free (qs->query);
+    g_free (sparql_select);
+    qs->query = sparql_final;
+    grl_tracker_source_query (source, qs);
+    return;
+  }
+
+  GRL_DEBUG ("select : %s", qs->query);
+
+  os = tracker_operation_initiate (source, priv, qs->query_id);
+  os->keys         = qs->keys;
+  os->skip         = qs->skip;
+  os->count        = qs->count;
+  os->callback     = qs->callback;
+  os->user_data    = qs->user_data;
+
+  tracker_sparql_connection_query_async (priv->tracker_connection,
+                                         qs->query,
+                                         os->cancel_op,
+                                         (GAsyncReadyCallback) tracker_query_cb,
+                                         os);
+
+  return;
+
+ send_error:
+  qs->callback (qs->source, qs->query_id, NULL, 0, qs->user_data, error);
+  g_error_free (error);
+}
+
+static void
+grl_tracker_source_metadata (GrlMediaSource *source,
+                             GrlMediaSourceMetadataSpec *ms)
+{
+  GrlTrackerSourcePriv *priv = GRL_TRACKER_SOURCE_GET_PRIVATE (source);
+  gchar                *sparql_select, *sparql_final;
+
+  GRL_DEBUG ("%s: id=%i", __FUNCTION__, ms->metadata_id);
+
+  sparql_select = get_select_string (source, ms->keys);
+  sparql_final = g_strdup_printf (TRACKER_METADATA_REQUEST, sparql_select,
+                                  grl_media_get_id (ms->media));
+
+  GRL_DEBUG ("select: '%s'", sparql_final);
+
+  tracker_sparql_connection_query_async (priv->tracker_connection,
+                                         sparql_final,
+                                         NULL,
+                                         (GAsyncReadyCallback) tracker_metadata_cb,
+                                         ms);
+
+  if (sparql_select != NULL)
+    g_free (sparql_select);
+  if (sparql_final != NULL)
+    g_free (sparql_final);
+}
+
+static void
+grl_tracker_source_search (GrlMediaSource *source, GrlMediaSourceSearchSpec *ss)
+{
+  GrlTrackerSourcePriv *priv  = GRL_TRACKER_SOURCE_GET_PRIVATE (source);
+  gchar                *constraint;
+  gchar                *sparql_select;
+  gchar                *sparql_final;
+  struct OperationSpec *os;
+
+  GRL_DEBUG ("%s: id=%u", __FUNCTION__, ss->search_id);
+
+  constraint = tracker_source_get_device_constraint (priv);
+  sparql_select = get_select_string (source, ss->keys);
+  if (!ss->text || ss->text[0] == '\0') {
+    /* Search all */
+    sparql_final = g_strdup_printf (TRACKER_SEARCH_ALL_REQUEST, sparql_select,
+                                    constraint, ss->skip, ss->count);
+  } else {
+    sparql_final = g_strdup_printf (TRACKER_SEARCH_REQUEST, sparql_select,
+                                    ss->text, constraint, ss->skip, ss->count);
+  }
+
+  GRL_DEBUG ("select: '%s'", sparql_final);
+
+  os = tracker_operation_initiate (source, priv, ss->search_id);
+  os->keys         = ss->keys;
+  os->skip         = ss->skip;
+  os->count        = ss->count;
+  os->callback     = ss->callback;
+  os->user_data    = ss->user_data;
+
+  tracker_sparql_connection_query_async (priv->tracker_connection,
+                                         sparql_final,
+                                         os->cancel_op,
+                                         (GAsyncReadyCallback) tracker_query_cb,
+                                         os);
+
+  g_free (constraint);
+  g_free (sparql_select);
+  g_free (sparql_final);
+}
+
+static void
+grl_tracker_source_browse (GrlMediaSource *source,
+                           GrlMediaSourceBrowseSpec *bs)
+{
+  GrlTrackerSourcePriv *priv  = GRL_TRACKER_SOURCE_GET_PRIVATE (source);
+  gchar                *constraint;
+  gchar                *sparql_select;
+  gchar                *sparql_final;
+  struct OperationSpec *os;
+  GrlMedia             *media;
+
+  GRL_DEBUG ("%s: id=%u", __FUNCTION__, bs->browse_id);
+
+  if ((bs->container == NULL || grl_media_get_id (bs->container) == NULL)) {
+    /* Hardcoded categories */
+    media = grl_media_box_new ();
+    grl_media_set_title (media, "Music");
+    grl_media_set_id (media, "nmm:MusicPiece");
+    bs->callback (bs->source, bs->browse_id, media, 2, bs->user_data, NULL);
+
+    media = grl_media_box_new ();
+    grl_media_set_title (media, "Photo");
+    grl_media_set_id (media, "nmm:Photo");
+    bs->callback (bs->source, bs->browse_id, media, 1, bs->user_data, NULL);
+
+    media = grl_media_box_new ();
+    grl_media_set_title (media, "Video");
+    grl_media_set_id (media, "nmm:Video");
+    bs->callback (bs->source, bs->browse_id, media, 0, bs->user_data, NULL);
+    return;
+  }
+
+  constraint = tracker_source_get_device_constraint (priv);
+  sparql_select = get_select_string (bs->source, bs->keys);
+  sparql_final = g_strdup_printf (TRACKER_BROWSE_CATEGORY_REQUEST,
+                                  sparql_select,
+                                  grl_media_get_id (bs->container),
+                                  constraint,
+                                  bs->skip, bs->count);
+
+  GRL_DEBUG ("select: '%s'", sparql_final);
+
+  os = tracker_operation_initiate (source, priv, bs->browse_id);
+  os->keys         = bs->keys;
+  os->skip         = bs->skip;
+  os->count        = bs->count;
+  os->callback     = bs->callback;
+  os->user_data    = bs->user_data;
+
+  tracker_sparql_connection_query_async (priv->tracker_connection,
+                                         sparql_final,
+                                         os->cancel_op,
+                                         (GAsyncReadyCallback) tracker_query_cb,
+                                         os);
+
+  g_free (constraint);
+  g_free (sparql_select);
+  g_free (sparql_final);
+}
+
+static void
+grl_tracker_source_cancel (GrlMediaSource *source, guint operation_id)
+{
+  GrlTrackerSourcePriv *priv = GRL_TRACKER_SOURCE_GET_PRIVATE (source);
+  struct OperationSpec *os;
+
+  GRL_DEBUG ("%s: id=%u", __FUNCTION__, operation_id);
+
+  os = g_hash_table_lookup (priv->operations, GSIZE_TO_POINTER (operation_id));
+
+  if (os != NULL)
+    g_cancellable_cancel (os->cancel_op);
+}
diff --git a/src/media/tracker/grl-tracker.h b/src/media/tracker/grl-tracker.h
new file mode 100644
index 0000000..ddf46ce
--- /dev/null
+++ b/src/media/tracker/grl-tracker.h
@@ -0,0 +1,78 @@
+/*
+ * Copyright (C) 2011 Igalia S.L.
+ *
+ * Contact: Iago Toral Quiroga <itoral igalia com>
+ *
+ * Authors: Juan A. Suarez Romero <jasuarez igalia 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_TRACKER_H_
+#define _GRL_TRACKER_H_
+
+#include <grilo.h>
+
+#define GRL_TRACKER_SOURCE_TYPE                 \
+  (grl_tracker_source_get_type ())
+
+#define GRL_TRACKER_SOURCE(obj)                         \
+  (G_TYPE_CHECK_INSTANCE_CAST ((obj),                   \
+                               GRL_TRACKER_SOURCE_TYPE, \
+                               GrlTrackerSource))
+
+#define GRL_IS_TRACKER_SOURCE(obj)                              \
+  (G_TYPE_CHECK_INSTANCE_TYPE ((obj),                           \
+                               GRL_TRACKER_SOURCE_TYPE))
+
+#define GRL_TRACKER_SOURCE_CLASS(klass)                 \
+  (G_TYPE_CHECK_CLASS_CAST((klass),                     \
+                           GRL_TRACKER_SOURCE_TYPE,     \
+                           GrlTrackerSourceClass))
+
+#define GRL_IS_TRACKER_SOURCE_CLASS(klass)              \
+  (G_TYPE_CHECK_CLASS_TYPE((klass),                     \
+                           GRL_TRACKER_SOURCE_TYPE))
+
+#define GRL_TRACKER_SOURCE_GET_CLASS(obj)               \
+  (G_TYPE_INSTANCE_GET_CLASS ((obj),                    \
+                              GRL_TRACKER_SOURCE_TYPE,  \
+                              GrlTrackerSourceClass))
+
+typedef struct _GrlTrackerSource GrlTrackerSource;
+typedef struct _GrlTrackerSourcePriv GrlTrackerSourcePriv;
+
+struct _GrlTrackerSource {
+
+  GrlMediaSource parent;
+
+  /*< private >*/
+  GrlTrackerSourcePriv *priv;
+
+};
+
+typedef struct _GrlTrackerSourceClass GrlTrackerSourceClass;
+
+struct _GrlTrackerSourceClass {
+
+  GrlMediaSourceClass parent_class;
+
+};
+
+GType grl_tracker_source_get_type (void);
+
+#endif /* _GRL_TRACKER_H_ */
diff --git a/src/media/tracker/grl-tracker.xml b/src/media/tracker/grl-tracker.xml
new file mode 100644
index 0000000..20f7bb6
--- /dev/null
+++ b/src/media/tracker/grl-tracker.xml
@@ -0,0 +1,9 @@
+<plugin>
+  <info>
+    <name>Tracker</name>
+    <description>A plugin for searching multimedia content using Tracker</description>
+    <author>Igalia S.L.</author>
+    <license>LGPL</license>
+    <site>http://www.igalia.com</site>
+  </info>
+</plugin>
diff --git a/src/media/upnp/Makefile.am b/src/media/upnp/Makefile.am
new file mode 100644
index 0000000..b6b09bd
--- /dev/null
+++ b/src/media/upnp/Makefile.am
@@ -0,0 +1,46 @@
+#
+# Makefile.am
+#
+# Author: Iago Toral Quiroga <itoral igalia com>
+#
+# Copyright (C) 2010, 2011 Igalia S.L. All rights reserved.
+
+lib_LTLIBRARIES	= libgrlupnp.la
+
+libgrlupnp_la_CFLAGS =		\
+	$(DEPS_CFLAGS)		\
+	$(GUPNP_CFLAGS)		\
+	$(GUPNPAV_CFLAGS)	\
+	$(GTHREAD_CFLAGS)	\
+	$(XML_CFLAGS)
+
+libgrlupnp_la_LIBADD =	\
+	$(DEPS_LIBS)	\
+	$(GUPNP_LIBS)	\
+	$(GUPNPAV_LIBS)	\
+	$(GTHREAD_LIBS)	\
+	$(XML_LIBS)
+
+libgrlupnp_la_LDFLAGS = \
+	-module		\
+	-avoid-version
+
+libgrlupnp_la_CFLAGS +=	\
+	$(XML_CFLAGS)
+
+libgrlupnp_la_LIBADD +=	\
+	$(XML_LIBS)
+
+libgrlupnp_la_SOURCES = grl-upnp.c grl-upnp.h
+
+libdir = $(GRL_PLUGINS_DIR)
+upnpxmldir	= $(GRL_PLUGINS_CONF_DIR)
+upnpxml_DATA	= $(UPNP_PLUGIN_ID).xml
+
+EXTRA_DIST = $(upnpxml_DATA)
+
+MAINTAINERCLEANFILES =	\
+	*.in		\
+	*~
+
+DISTCLEANFILES = $(MAINTAINERCLEANFILES)
diff --git a/src/media/upnp/grl-upnp.c b/src/media/upnp/grl-upnp.c
new file mode 100644
index 0000000..f024b53
--- /dev/null
+++ b/src/media/upnp/grl-upnp.c
@@ -0,0 +1,1356 @@
+/*
+ * Copyright (C) 2010, 2011 Igalia S.L.
+ * Copyright (C) 2011 Intel Corporation.
+ *
+ * This component is based on Maemo's mafw-upnp-source source code.
+ *
+ * Contact: Iago Toral Quiroga <itoral igalia 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 <grilo.h>
+#include <libgupnp/gupnp.h>
+#include <libgupnp-av/gupnp-av.h>
+#include <string.h>
+#include <stdlib.h>
+#include <libxml/tree.h>
+
+#include "grl-upnp.h"
+
+#define GRL_UPNP_GET_PRIVATE(object)                                    \
+  (G_TYPE_INSTANCE_GET_PRIVATE((object), GRL_UPNP_SOURCE_TYPE, GrlUpnpPrivate))
+
+/* --------- Logging  -------- */
+
+#define GRL_LOG_DOMAIN_DEFAULT upnp_log_domain
+GRL_LOG_DOMAIN_STATIC(upnp_log_domain);
+
+/* --- Plugin information --- */
+
+#define PLUGIN_ID   UPNP_PLUGIN_ID
+
+#define SOURCE_ID_TEMPLATE    "grl-upnp-%s"
+#define SOURCE_NAME_TEMPLATE  "UPnP - %s"
+#define SOURCE_DESC_TEMPLATE  "A source for browsing the UPnP server '%s'"
+
+#define AUTHOR      "Igalia S.L."
+#define LICENSE     "LGPL"
+#define SITE        "http://www.igalia.com";
+
+/* --- Other --- */
+
+#ifndef CONTENT_DIR_SERVICE
+#define CONTENT_DIR_SERVICE "urn:schemas-upnp-org:service:ContentDirectory"
+#endif
+
+#define UPNP_SEARCH_SPEC				\
+  "dc:title contains \"%s\" or "			\
+  "upnp:album contains \"%s\" or "			\
+  "upnp:artist contains \"%s\""
+
+#define UPNP_SEARCH_ALL                         \
+  "upnp:class derivedfrom \"object.item\""
+
+struct _GrlUpnpPrivate {
+  GUPnPDeviceProxy* device;
+  GUPnPServiceProxy* service;
+  GUPnPControlPoint *cp;
+  gboolean search_enabled;
+  gchar *upnp_name;
+};
+
+struct OperationSpec {
+  GrlMediaSource *source;
+  guint operation_id;
+  GList *keys;
+  guint skip;
+  guint count;
+  GrlMediaSourceResultCb callback;
+  gpointer user_data;
+};
+
+struct SourceInfo {
+  gchar *source_id;
+  gchar *source_name;
+  GUPnPDeviceProxy* device;
+  GUPnPServiceProxy* service;
+  GrlPluginInfo *plugin;
+};
+
+static void setup_key_mappings (void);
+
+static gchar *build_source_id (const gchar *udn);
+
+static GrlUpnpSource *grl_upnp_source_new (const gchar *id, const gchar *name);
+
+gboolean grl_upnp_plugin_init (GrlPluginRegistry *registry,
+                               const GrlPluginInfo *plugin,
+                               GList *configs);
+
+static void grl_upnp_source_finalize (GObject *plugin);
+
+static const GList *grl_upnp_source_supported_keys (GrlMetadataSource *source);
+
+static void grl_upnp_source_browse (GrlMediaSource *source,
+                                    GrlMediaSourceBrowseSpec *bs);
+
+static void grl_upnp_source_search (GrlMediaSource *source,
+                                    GrlMediaSourceSearchSpec *ss);
+
+static void grl_upnp_source_query (GrlMediaSource *source,
+                                   GrlMediaSourceQuerySpec *qs);
+
+static void grl_upnp_source_metadata (GrlMediaSource *source,
+                                      GrlMediaSourceMetadataSpec *ms);
+
+static GrlSupportedOps grl_upnp_source_supported_operations (GrlMetadataSource *source);
+
+static gboolean grl_upnp_source_notify_change_start (GrlMediaSource *source,
+                                                     GError **error);
+
+static gboolean grl_upnp_source_notify_change_stop (GrlMediaSource *source,
+                                                    GError **error);
+
+static void context_available_cb (GUPnPContextManager *context_manager,
+				  GUPnPContext *context,
+				  gpointer user_data);
+static void device_available_cb (GUPnPControlPoint *cp,
+				 GUPnPDeviceProxy *device,
+				 gpointer user_data);
+static void device_unavailable_cb (GUPnPControlPoint *cp,
+				   GUPnPDeviceProxy *device,
+				   gpointer user_data);
+
+/* ===================== Globals  ================= */
+
+static GHashTable *key_mapping = NULL;
+static GHashTable *filter_key_mapping = NULL;
+static GUPnPContextManager *context_manager = NULL;
+
+/* =================== UPnP Plugin  =============== */
+
+gboolean
+grl_upnp_plugin_init (GrlPluginRegistry *registry,
+                      const GrlPluginInfo *plugin,
+                      GList *configs)
+{
+  GRL_LOG_DOMAIN_INIT (upnp_log_domain, "upnp");
+
+  GRL_DEBUG ("grl_upnp_plugin_init");
+
+  /* libsoup needs this */
+  if (!g_thread_supported()) {
+    g_thread_init (NULL);
+  }
+
+  context_manager = gupnp_context_manager_new (NULL, 0);
+  g_signal_connect (context_manager,
+                    "context-available",
+                    G_CALLBACK (context_available_cb),
+                    (gpointer)plugin);
+
+  return TRUE;
+}
+
+static void
+grl_upnp_plugin_deinit (void)
+{
+  GRL_DEBUG ("grl_upnp_plugin_deinit");
+
+  if (context_manager != NULL) {
+    g_object_unref (context_manager);
+    context_manager = NULL;
+  }
+}
+
+GRL_PLUGIN_REGISTER (grl_upnp_plugin_init,
+                     grl_upnp_plugin_deinit,
+                     PLUGIN_ID);
+
+/* ================== UPnP GObject ================ */
+
+G_DEFINE_TYPE (GrlUpnpSource, grl_upnp_source, GRL_TYPE_MEDIA_SOURCE);
+
+static GrlUpnpSource *
+grl_upnp_source_new (const gchar *source_id, const gchar *name)
+{
+  gchar *source_name, *source_desc;
+  GrlUpnpSource *source;
+
+  GRL_DEBUG ("grl_upnp_source_new");
+  source_name = g_strdup_printf (SOURCE_NAME_TEMPLATE, name);
+  source_desc = g_strdup_printf (SOURCE_DESC_TEMPLATE, name);
+
+  source = g_object_new (GRL_UPNP_SOURCE_TYPE,
+			 "source-id", source_id,
+			 "source-name", source_name,
+			 "source-desc", source_desc,
+			 NULL);
+
+  source->priv->upnp_name = g_strdup (name);
+
+  g_free (source_name);
+  g_free (source_desc);
+
+  return source;
+}
+
+static void
+grl_upnp_source_class_init (GrlUpnpSourceClass * klass)
+{
+  GObjectClass *gobject_class = G_OBJECT_CLASS (klass);
+  GrlMediaSourceClass *source_class = GRL_MEDIA_SOURCE_CLASS (klass);
+  GrlMetadataSourceClass *metadata_class = GRL_METADATA_SOURCE_CLASS (klass);
+
+  gobject_class->finalize = grl_upnp_source_finalize;
+
+  metadata_class->supported_keys = grl_upnp_source_supported_keys;
+  metadata_class->supported_operations = grl_upnp_source_supported_operations;
+
+  source_class->browse = grl_upnp_source_browse;
+  source_class->search = grl_upnp_source_search;
+  source_class->query = grl_upnp_source_query;
+  source_class->metadata = grl_upnp_source_metadata;
+  source_class->notify_change_start = grl_upnp_source_notify_change_start;
+  source_class->notify_change_stop = grl_upnp_source_notify_change_stop;
+
+  g_type_class_add_private (klass, sizeof (GrlUpnpPrivate));
+
+  setup_key_mappings ();
+}
+
+static void
+grl_upnp_source_init (GrlUpnpSource *source)
+{
+  source->priv = GRL_UPNP_GET_PRIVATE (source);
+}
+
+static void
+grl_upnp_source_finalize (GObject *object)
+{
+  GrlUpnpSource *source;
+
+  GRL_DEBUG ("grl_upnp_source_finalize");
+
+  source = GRL_UPNP_SOURCE (object);
+
+  g_object_unref (source->priv->device);
+  g_object_unref (source->priv->service);
+  g_free (source->priv->upnp_name);
+
+  G_OBJECT_CLASS (grl_upnp_source_parent_class)->finalize (object);
+}
+
+/* ======================= Utilities ==================== */
+
+static gchar *
+build_source_id (const gchar *udn)
+{
+  return g_strdup_printf (SOURCE_ID_TEMPLATE, udn);
+}
+
+static void
+free_source_info (struct SourceInfo *info)
+{
+  g_free (info->source_id);
+  g_free (info->source_name);
+  g_object_unref (info->device);
+  g_object_unref (info->service);
+  g_slice_free (struct SourceInfo, info);
+}
+
+static void
+container_changed_cb (GUPnPServiceProxy *proxy,
+                      const char *variable,
+                      GValue *value,
+                      gpointer user_data)
+{
+  GrlMedia *container;
+  GrlMediaSource *source = GRL_MEDIA_SOURCE (user_data);
+  gchar **tokens;
+  gint i = 0;
+
+  GRL_DEBUG (__func__);
+
+  /* Value is a list of pairs (id, number), where "id" is the container id */
+  tokens = g_strsplit (g_value_get_string (value), ",", -1);
+  while (tokens[i]) {
+    container = grl_media_box_new ();
+    grl_media_set_id (container, tokens[i]);
+    grl_media_source_notify_change (source,
+                                    container,
+                                    GRL_CONTENT_CHANGED,
+                                    FALSE);
+    g_object_unref (container);
+    i += 2;
+  }
+  g_strfreev (tokens);
+}
+
+static void
+gupnp_search_caps_cb (GUPnPServiceProxy *service,
+		      GUPnPServiceProxyAction *action,
+		      gpointer user_data)
+{
+  GError *error = NULL;
+  gchar *caps = NULL;
+  gchar *name;
+  GrlUpnpSource *source;
+  gchar *source_id;
+  GrlPluginRegistry *registry;
+  struct SourceInfo *source_info;
+  gboolean result;
+
+  result =
+    gupnp_service_proxy_end_action (service, action, &error,
+				    "SearchCaps", G_TYPE_STRING, &caps,
+				    NULL);
+  if (!result) {
+    GRL_WARNING ("Failed to execute GetSearchCaps operation");
+    if (error) {
+      GRL_WARNING ("Reason: %s", error->message);
+      g_error_free (error);
+    }
+  }
+
+  source_info = (struct SourceInfo *) user_data;
+  name = source_info->source_name;
+  source_id = source_info->source_id;
+
+  registry = grl_plugin_registry_get_default ();
+  if (grl_plugin_registry_lookup_source (registry, source_id)) {
+    GRL_DEBUG ("A source with id '%s' is already registered. Skipping...",
+               source_id);
+    goto free_resources;
+  }
+
+  source = grl_upnp_source_new (source_id, name);
+  source->priv->device = g_object_ref (source_info->device);
+  source->priv->service = g_object_ref (source_info->service);
+
+  GRL_DEBUG ("Search caps for source '%s': '%s'", name, caps);
+
+  if (caps && caps[0] != '\0') {
+    GRL_DEBUG ("Setting search enabled for source '%s'", name );
+    source->priv->search_enabled = TRUE;
+  } else {
+    GRL_DEBUG ("Setting search disabled for source '%s'", name );
+  }
+
+  grl_plugin_registry_register_source (registry,
+                                       source_info->plugin,
+                                       GRL_MEDIA_PLUGIN (source),
+                                       NULL);
+
+ free_resources:
+  free_source_info (source_info);
+}
+
+static void
+context_available_cb (GUPnPContextManager *context_manager,
+		      GUPnPContext *context,
+		      gpointer user_data)
+{
+  GUPnPControlPoint *cp;
+
+  GRL_DEBUG ("%s", __func__);
+
+  cp = gupnp_control_point_new (context, "urn:schemas-upnp-org:device:MediaServer:1");
+  g_signal_connect (cp,
+		    "device-proxy-available",
+		    G_CALLBACK (device_available_cb),
+		    user_data);
+  g_signal_connect (cp,
+		    "device-proxy-unavailable",
+		    G_CALLBACK (device_unavailable_cb),
+		    NULL);
+
+  gssdp_resource_browser_set_active (GSSDP_RESOURCE_BROWSER (cp), TRUE);
+
+  /* Let context manager take care of the control point life cycle */
+  gupnp_context_manager_manage_control_point (context_manager, cp);
+  g_object_unref (cp);
+}
+
+static void
+device_available_cb (GUPnPControlPoint *cp,
+		     GUPnPDeviceProxy *device,
+		     gpointer user_data)
+{
+  gchar* name;
+  const gchar* udn;
+  const char *type;
+  GUPnPServiceInfo *service;
+  GrlPluginRegistry *registry;
+  gchar *source_id;
+
+  GRL_DEBUG ("device_available_cb");
+
+  type = gupnp_device_info_get_device_type (GUPNP_DEVICE_INFO (device));
+  GRL_DEBUG ("  type: %s", type);
+
+  service = gupnp_device_info_get_service (GUPNP_DEVICE_INFO (device),
+					   CONTENT_DIR_SERVICE);
+  if (!service) {
+    GRL_DEBUG ("Device does not provide required service, ignoring...");
+    return;
+  }
+
+  udn = gupnp_device_info_get_udn (GUPNP_DEVICE_INFO (device));
+  GRL_DEBUG ("   udn: %s ", udn);
+
+  name = gupnp_device_info_get_friendly_name (GUPNP_DEVICE_INFO (device));
+  GRL_DEBUG ("  name: %s", name);
+
+  registry = grl_plugin_registry_get_default ();
+  source_id = build_source_id (udn);
+  if (grl_plugin_registry_lookup_source (registry, source_id)) {
+    GRL_DEBUG ("A source with id '%s' is already registered. Skipping...",
+               source_id);
+    goto free_resources;
+  }
+
+  /* We got a valid UPnP source */
+  /* Now let's check if it supports search operations before registering */
+  struct SourceInfo *source_info = g_slice_new0 (struct SourceInfo);
+  source_info->source_id = g_strdup (source_id);
+  source_info->source_name = g_strdup (name);
+  source_info->device = g_object_ref (device);
+  source_info->service = g_object_ref (service);
+  source_info->plugin = (GrlPluginInfo *) user_data;
+
+  if (!gupnp_service_proxy_begin_action (GUPNP_SERVICE_PROXY (service),
+					 "GetSearchCapabilities",
+					 gupnp_search_caps_cb,
+					 source_info,
+					 NULL)) {
+    GrlUpnpSource *source = grl_upnp_source_new (source_id, name);
+    GRL_WARNING ("Failed to start GetCapabilitiesSearch action");
+    GRL_DEBUG ("Setting search disabled for source '%s'", name );
+    registry = grl_plugin_registry_get_default ();
+    grl_plugin_registry_register_source (registry,
+                                         source_info->plugin,
+                                         GRL_MEDIA_PLUGIN (source),
+                                         NULL);
+    free_source_info (source_info);
+  }
+
+ free_resources:
+  g_object_unref (service);
+  g_free (source_id);
+}
+
+static void
+device_unavailable_cb (GUPnPControlPoint *cp,
+		       GUPnPDeviceProxy *device,
+		       gpointer user_data)
+{
+  const gchar* udn;
+  GrlMediaPlugin *source;
+  GrlPluginRegistry *registry;
+  gchar *source_id;
+
+  GRL_DEBUG ("device_unavailable_cb");
+
+  udn = gupnp_device_info_get_udn (GUPNP_DEVICE_INFO (device));
+  GRL_DEBUG ("   udn: %s ", udn);
+
+  registry = grl_plugin_registry_get_default ();
+  source_id = build_source_id (udn);
+  source = grl_plugin_registry_lookup_source (registry, source_id);
+  if (!source) {
+    GRL_DEBUG ("No source registered with id '%s', ignoring", source_id);
+  } else {
+    grl_plugin_registry_unregister_source (registry, source, NULL);
+  }
+
+  g_free (source_id);
+}
+
+const static gchar *
+get_upnp_key (const GrlKeyID key_id)
+{
+  return g_hash_table_lookup (key_mapping, key_id);
+}
+
+const static gchar *
+get_upnp_key_for_filter (const GrlKeyID key_id)
+{
+  return g_hash_table_lookup (filter_key_mapping, key_id);
+}
+
+static gchar *
+get_upnp_filter (const GList *keys)
+{
+  GString *filter;
+  GList *iter;
+  gchar *upnp_key;
+  guint first = TRUE;
+
+  filter = g_string_new ("");
+  iter = (GList *) keys;
+  while (iter) {
+    upnp_key =
+      (gchar *) get_upnp_key_for_filter (iter->data);
+    if (upnp_key) {
+      if (!first) {
+	g_string_append (filter, ",");
+      }
+      g_string_append (filter, upnp_key);
+      first = FALSE;
+    }
+    iter = g_list_next (iter);
+  }
+
+  return g_string_free (filter, FALSE);
+}
+
+static gchar *
+get_upnp_search (const gchar *text)
+{
+  if (text) {
+    return g_strdup_printf (UPNP_SEARCH_SPEC, text, text, text);
+  } else {
+    return g_strdup (UPNP_SEARCH_ALL);
+  }
+}
+
+static void
+setup_key_mappings (void)
+{
+  /* For key_mapping we only have to set mapping for keys that
+     are not handled directly with the corresponding fw key
+     (see ket_valur_for_key) */
+  key_mapping = g_hash_table_new (g_direct_hash, g_direct_equal);
+  filter_key_mapping = g_hash_table_new (g_direct_hash, g_direct_equal);
+
+  g_hash_table_insert (key_mapping, GRL_METADATA_KEY_TITLE, "title");
+  g_hash_table_insert (key_mapping, GRL_METADATA_KEY_ARTIST, "artist");
+  g_hash_table_insert (key_mapping, GRL_METADATA_KEY_ALBUM, "album");
+  g_hash_table_insert (key_mapping, GRL_METADATA_KEY_GENRE, "genre");
+  g_hash_table_insert (key_mapping, GRL_METADATA_KEY_URL, "res");
+  g_hash_table_insert (key_mapping, GRL_METADATA_KEY_DATE, "modified");
+  g_hash_table_insert (filter_key_mapping, GRL_METADATA_KEY_TITLE, "title");
+  g_hash_table_insert (filter_key_mapping, GRL_METADATA_KEY_URL, "res");
+  g_hash_table_insert (filter_key_mapping, GRL_METADATA_KEY_DATE, "modified");
+  g_hash_table_insert (filter_key_mapping, GRL_METADATA_KEY_ARTIST, "upnp:artist");
+  g_hash_table_insert (filter_key_mapping, GRL_METADATA_KEY_ALBUM, "upnp:album");
+  g_hash_table_insert (filter_key_mapping, GRL_METADATA_KEY_GENRE, "upnp:genre");
+  g_hash_table_insert (filter_key_mapping, GRL_METADATA_KEY_DURATION, "res@duration");
+  g_hash_table_insert (filter_key_mapping, GRL_METADATA_KEY_DATE, "modified");
+}
+
+static gchar *
+didl_res_get_protocol_info (xmlNode* res_node, gint field)
+{
+  gchar* pinfo;
+  gchar* value;
+  gchar** array;
+
+  pinfo = (gchar *) xmlGetProp (res_node, (const xmlChar *) "protocolInfo");
+  if (pinfo == NULL) {
+    return NULL;
+  }
+
+  /* 0:protocol, 1:network, 2:mime-type and 3:additional info. */
+  array = g_strsplit (pinfo, ":", 4);
+  g_free(pinfo);
+  if (g_strv_length (array) < 4) {
+    value = NULL;
+  } else {
+    value = g_strdup (array[field]);
+  }
+
+  g_strfreev (array);
+
+  return value;
+}
+
+static GList *
+didl_get_supported_resources (GUPnPDIDLLiteObject *didl)
+{
+  GList *properties, *node;
+  xmlNode *xml_node;
+  gchar *protocol;
+
+  properties = gupnp_didl_lite_object_get_properties (didl, "res");
+
+  node = properties;
+  while (node) {
+    xml_node = (xmlNode *) node->data;
+    if (!xml_node) {
+      node = properties = g_list_delete_link (properties, node);
+      continue;
+    }
+
+    protocol = didl_res_get_protocol_info (xml_node, 0);
+    if (protocol && strcmp (protocol, "http-get") != 0) {
+      node = properties = g_list_delete_link (properties, node);
+      g_free (protocol);
+      continue;
+    }
+    g_free (protocol);
+    node = g_list_next (node);
+  }
+
+  return properties;
+}
+
+static gint
+didl_h_mm_ss_to_int (const gchar *time)
+{
+  guint len = 0;
+  guint i = 0;
+  guint head = 0;
+  guint tail = 0;
+  int result = 0;
+  gchar* tmp = NULL;
+  gboolean has_hours = FALSE;
+
+  if (!time) {
+    return -1;
+  }
+
+  len = strlen (time);
+  tmp = g_new0 (gchar, sizeof (gchar) * len);
+
+  /* Find the first colon (it can be anywhere) and also count the
+   * amount of colons to know if there are hours or not */
+  for (i = 0; i < len; i++) {
+    if (time[i] == ':') {
+      if (tail != 0) {
+	has_hours = TRUE;
+      } else {
+	tail = i;
+      }
+    }
+  }
+
+  if (tail > len || head > tail) {
+    g_free (tmp);
+    return -1;
+  }
+
+  /* Hours */
+  if (has_hours == TRUE) {
+    memcpy (tmp, time + head, tail - head);
+    tmp[tail - head + 1] = '\0';
+    result += 3600 * atoi (tmp);
+    /* The next colon should be exactly 2 chars right */
+    head = tail + 1;
+    tail = head + 2;
+  } else {
+    /* The format is now exactly MM:SS */
+    head = 0;
+    tail = 2;
+  }
+
+  /* Bail out if tail goes too far or head is bigger than tail */
+  if (tail > len || head > tail) {
+    g_free (tmp);
+    return -1;
+  }
+
+  /* Minutes */
+  memcpy (tmp, time + head, tail - head);
+  tmp[2] = '\0';
+  result += 60 * atoi (tmp);
+
+  /* The next colon should again be exactly 2 chars right */
+  head = tail + 1;
+  tail = head + 2;
+
+  /* Bail out if tail goes too far or head is bigger than tail */
+  if (tail > len || head > tail) {
+    g_free (tmp);
+    return -1;
+  }
+
+  /* Extract seconds */
+  memcpy (tmp, time + head, tail - head);
+  tmp[2] = '\0';
+  result += atoi(tmp);
+  g_free (tmp);
+
+  return result;
+
+}
+
+static gboolean
+is_image (xmlNode *node)
+{
+  gchar *mime_type;
+  gboolean ret;
+
+  mime_type = didl_res_get_protocol_info (node, 2);
+  ret = g_str_has_prefix (mime_type, "image/");
+
+  g_free (mime_type);
+  return ret;
+}
+
+static gboolean
+is_http_get (xmlNode *node)
+{
+  gboolean ret;
+  gchar *protocol;
+
+  protocol = didl_res_get_protocol_info (node, 0);
+  ret = g_str_has_prefix (protocol, "http-get");
+
+  g_free (protocol);
+  return ret;
+}
+
+static gboolean
+has_thumbnail_marker (xmlNode *node)
+{
+  gchar *dlna_stuff;
+  gboolean ret;
+
+  dlna_stuff = didl_res_get_protocol_info (node, 3);
+  ret = strstr("JPEG_TN", dlna_stuff) != NULL;
+
+  g_free (dlna_stuff);
+  return ret;
+}
+
+static gchar *
+get_thumbnail (GList *nodes)
+{
+  GList *element;
+  gchar *val = NULL;
+  guint counter = 0;
+
+  /* chose, depending on availability, the first with DLNA.ORG_PN=JPEG_TN, or
+   * the last http-get with mimetype image/something if there is more than one
+   * http-get.
+   * This covers at least mediatomb and rygel.
+   * This could be improved by handling resolution and/or size */
+
+  for (element=nodes; element; element=g_list_next (element)) {
+    xmlNode *node = (xmlNode *)element->data;
+
+    if (is_http_get (node)) {
+      counter++;
+      if (is_image (node)) {
+        if (val)
+          g_free (val);
+        val = (gchar *) xmlNodeGetContent (node);
+
+        if (has_thumbnail_marker (node))  /* that's definitely it! */
+          return val;
+      }
+    }
+  }
+
+  if (val && counter == 1) {
+    /* There was only one element with http-get protocol: that's the uri of the
+     * media itself, not a thumbnail */
+    g_free (val);
+    val = NULL;
+  }
+
+  return val;
+}
+
+static gchar *
+get_value_for_key (GrlKeyID key_id,
+                   GUPnPDIDLLiteObject *didl,
+                   GList *props)
+{
+  GList* list;
+  gchar* val = NULL;
+  const gchar* upnp_key;
+
+  xmlNode *didl_node = gupnp_didl_lite_object_get_xml_node (didl);
+  upnp_key = get_upnp_key (key_id);
+
+  if (key_id == GRL_METADATA_KEY_CHILDCOUNT) {
+    val = (gchar *) xmlGetProp (didl_node,
+                                (const xmlChar *) "childCount");
+  } else if (key_id == GRL_METADATA_KEY_MIME && props) {
+    val = didl_res_get_protocol_info ((xmlNode *) props->data, 2);
+  } else if (key_id == GRL_METADATA_KEY_DURATION && props) {
+    val = (gchar *) xmlGetProp ((xmlNodePtr) props->data,
+                                (const xmlChar *) "duration");
+  } else if (key_id == GRL_METADATA_KEY_URL && props) {
+    val = (gchar *) xmlNodeGetContent ((xmlNode *) props->data);
+  } else if (key_id == GRL_METADATA_KEY_THUMBNAIL && props) {
+    val = g_strdup (gupnp_didl_lite_object_get_album_art (didl));
+    if (!val)
+      val = get_thumbnail (props);
+  } else if (upnp_key) {
+    list = gupnp_didl_lite_object_get_properties (didl, upnp_key);
+    if (list) {
+      val = (gchar *) xmlNodeGetContent ((xmlNode*) list->data);
+      g_list_free (list);
+    } else if (props && props->data) {
+      val = (gchar *) xmlGetProp ((xmlNodePtr) props->data,
+                                  (const xmlChar *) upnp_key);
+    }
+  }
+
+  return val;
+}
+
+static void
+set_metadata_value (GrlData *data,
+                    GrlKeyID key_id,
+                    const gchar *value)
+{
+  if (key_id == GRL_METADATA_KEY_DURATION) {
+    gint duration = didl_h_mm_ss_to_int (value);
+    if (duration >= 0) {
+      grl_data_set_int (data, GRL_METADATA_KEY_DURATION, duration);
+    }
+  } else if (key_id == GRL_METADATA_KEY_CHILDCOUNT && value) {
+    grl_data_set_int (data, GRL_METADATA_KEY_CHILDCOUNT, atoi (value));
+  } else {
+    grl_data_set_string (data, key_id, value);
+  }
+}
+
+static GrlMedia *
+build_media_from_didl (GrlMedia *content,
+                       GUPnPDIDLLiteObject *didl_node,
+                       GList *keys)
+{
+  const gchar *id;
+  const gchar *class;
+
+  GrlMedia *media = NULL;
+  GList *didl_props;
+  GList *iter;
+
+  GRL_DEBUG ("build_media_from_didl");
+
+  if (content) {
+    media = content;
+  } else {
+
+    if (GUPNP_IS_DIDL_LITE_CONTAINER (didl_node)) {
+      media = grl_media_box_new ();
+    } else {
+      if (!media) {
+        class = gupnp_didl_lite_object_get_upnp_class (didl_node);
+        if (class) {
+          if (g_str_has_prefix (class, "object.item.audioItem")) {
+            media = grl_media_audio_new ();
+          } else if (g_str_has_prefix (class, "object.item.videoItem")) {
+            media = grl_media_video_new ();
+          } else if (g_str_has_prefix (class, "object.item.imageItem")) {
+            media = grl_media_image_new ();
+          } else {
+            media = grl_media_new ();
+          }
+        } else {
+          media = grl_media_new ();
+        }
+      }
+    }
+  }
+
+  id = gupnp_didl_lite_object_get_id (didl_node);
+  /* Root category's id is always NULL */
+  if (g_strcmp0 (id, "0") == 0) {
+    grl_media_set_id (media, NULL);
+  } else {
+    grl_media_set_id (media, id);
+  }
+
+  didl_props = didl_get_supported_resources (didl_node);
+
+  iter = keys;
+  while (iter) {
+    gchar *value = get_value_for_key (iter->data, didl_node, didl_props);
+    if (value) {
+      set_metadata_value (GRL_DATA (media), iter->data, value);
+    }
+    iter = g_list_next (iter);
+  }
+
+  g_list_free (didl_props);
+
+  return media;
+}
+
+static void
+gupnp_browse_result_cb (GUPnPDIDLLiteParser *parser,
+			GUPnPDIDLLiteObject *didl,
+			gpointer user_data)
+{
+  GrlMedia *media;
+  struct OperationSpec *os = (struct OperationSpec *) user_data;
+  if (gupnp_didl_lite_object_get_id (didl)) {
+    media = build_media_from_didl (NULL, didl, os->keys);
+    os->callback (os->source,
+		  os->operation_id,
+		  media,
+		  --os->count,
+		  os->user_data,
+		  NULL);
+  }
+}
+
+static void
+gupnp_browse_cb (GUPnPServiceProxy *service,
+		 GUPnPServiceProxyAction *action,
+		 gpointer user_data)
+{
+  GError *error = NULL;
+  gchar *didl = NULL;
+  guint returned = 0;
+  guint matches = 0;
+  gboolean result;
+  struct OperationSpec *os;
+  GUPnPDIDLLiteParser *didl_parser;
+
+  GRL_DEBUG ("gupnp_browse_cb");
+
+  os = (struct OperationSpec *) user_data;
+  didl_parser = gupnp_didl_lite_parser_new ();
+
+  result =
+    gupnp_service_proxy_end_action (service, action, &error,
+				    "Result", G_TYPE_STRING, &didl,
+				    "NumberReturned", G_TYPE_UINT, &returned,
+				    "TotalMatches", G_TYPE_UINT, &matches,
+				    NULL);
+
+  if (!result) {
+    GRL_WARNING ("Operation (browse, search or query) failed");
+    os->callback (os->source, os->operation_id, NULL, 0, os->user_data, error);
+    if (error) {
+      GRL_WARNING ("  Reason: %s", error->message);
+      g_error_free (error);
+    }
+
+    goto free_resources;
+  }
+
+  if (!didl || !returned) {
+    GRL_DEBUG ("Got no results");
+    os->callback (os->source, os->operation_id, NULL, 0, os->user_data, NULL);
+
+    goto free_resources;
+  }
+
+  /* Use os->count to emit "remaining" information */
+  if (os->count > returned) {
+    os->count = returned;
+  }
+
+  g_signal_connect (G_OBJECT (didl_parser),
+                    "object-available",
+                    G_CALLBACK (gupnp_browse_result_cb),
+                    os);
+  gupnp_didl_lite_parser_parse_didl (didl_parser,
+                                     didl,
+                                     &error);
+  if (error) {
+    GRL_WARNING ("Failed to parse DIDL result: %s", error->message);
+    os->callback (os->source, os->operation_id, NULL, 0, os->user_data, error);
+    g_error_free (error);
+
+    goto free_resources;
+  }
+
+ free_resources:
+  g_slice_free (struct OperationSpec, os);
+  g_free (didl);
+  g_object_unref (didl_parser);
+}
+
+static void
+gupnp_metadata_result_cb (GUPnPDIDLLiteParser *parser,
+			  GUPnPDIDLLiteObject *didl,
+			  gpointer user_data)
+{
+  GrlMediaSourceMetadataSpec *ms = (GrlMediaSourceMetadataSpec *) user_data;
+  if (gupnp_didl_lite_object_get_id (didl)) {
+    build_media_from_didl (ms->media, didl, ms->keys);
+    ms->callback (ms->source, ms->media, ms->user_data, NULL);
+  }
+}
+
+static void
+gupnp_metadata_cb (GUPnPServiceProxy *service,
+		   GUPnPServiceProxyAction *action,
+		   gpointer user_data)
+{
+  GError *error = NULL;
+  gchar *didl = NULL;
+  gboolean result;
+  GrlMediaSourceMetadataSpec *ms;
+  GUPnPDIDLLiteParser *didl_parser;
+
+  GRL_DEBUG ("gupnp_metadata_cb");
+
+  ms = (GrlMediaSourceMetadataSpec *) user_data;
+  didl_parser = gupnp_didl_lite_parser_new ();
+
+  result =
+    gupnp_service_proxy_end_action (service, action, &error,
+				    "Result", G_TYPE_STRING, &didl,
+				    NULL);
+
+  if (!result) {
+    GRL_WARNING ("Metadata operation failed");
+    ms->callback (ms->source, ms->media, ms->user_data, error);
+    if (error) {
+      GRL_WARNING ("  Reason: %s", error->message);
+      g_error_free (error);
+    }
+
+    goto free_resources;
+  }
+
+  if (!didl) {
+    GRL_DEBUG ("Got no metadata");
+    ms->callback (ms->source, ms->media,  ms->user_data, NULL);
+
+    goto free_resources;
+  }
+
+  g_signal_connect (G_OBJECT (didl_parser),
+                    "object-available",
+                    G_CALLBACK (gupnp_metadata_result_cb),
+                    ms);
+  gupnp_didl_lite_parser_parse_didl (didl_parser,
+                                     didl,
+                                     &error);
+  if (error) {
+    GRL_WARNING ("Failed to parse DIDL result: %s", error->message);
+    ms->callback (ms->source, ms->media, ms->user_data, error);
+    g_error_free (error);
+    goto free_resources;
+  }
+
+ free_resources:
+  g_free (didl);
+  g_object_unref (didl_parser);
+}
+
+/* ================== API Implementation ================ */
+
+static const GList *
+grl_upnp_source_supported_keys (GrlMetadataSource *source)
+{
+  static GList *keys = NULL;
+  if (!keys) {
+    keys = grl_metadata_key_list_new (GRL_METADATA_KEY_ID,
+                                      GRL_METADATA_KEY_TITLE,
+                                      GRL_METADATA_KEY_URL,
+                                      GRL_METADATA_KEY_MIME,
+                                      GRL_METADATA_KEY_DATE,
+                                      GRL_METADATA_KEY_DURATION,
+                                      GRL_METADATA_KEY_ARTIST,
+                                      GRL_METADATA_KEY_ALBUM,
+                                      GRL_METADATA_KEY_GENRE,
+                                      GRL_METADATA_KEY_CHILDCOUNT,
+                                      GRL_METADATA_KEY_THUMBNAIL,
+                                      NULL);
+  }
+  return keys;
+}
+
+static void
+grl_upnp_source_browse (GrlMediaSource *source, GrlMediaSourceBrowseSpec *bs)
+{
+  GUPnPServiceProxyAction* action;
+  gchar *upnp_filter;
+  gchar *container_id;
+  GError *error = NULL;
+  struct OperationSpec *os;
+
+  GRL_DEBUG ("grl_upnp_source_browse");
+
+  upnp_filter = get_upnp_filter (bs->keys);
+  GRL_DEBUG ("filter: '%s'", upnp_filter);
+
+  os = g_slice_new0 (struct OperationSpec);
+  os->source = bs->source;
+  os->operation_id = bs->browse_id;
+  os->keys = bs->keys;
+  os->skip = bs->skip;
+  os->count = bs->count;
+  os->callback = bs->callback;
+  os->user_data = bs->user_data;
+
+  container_id = (gchar *) grl_media_get_id (bs->container);
+  if (!container_id) {
+    container_id = "0";
+  }
+
+  action =
+    gupnp_service_proxy_begin_action (GRL_UPNP_SOURCE (source)->priv->service,
+				      "Browse", gupnp_browse_cb,
+                                      os,
+				      "ObjectID", G_TYPE_STRING,
+                                      container_id,
+				      "BrowseFlag", G_TYPE_STRING,
+                                      "BrowseDirectChildren",
+				      "Filter", G_TYPE_STRING,
+                                      upnp_filter,
+				      "StartingIndex", G_TYPE_UINT,
+                                      bs->skip,
+				      "RequestedCount", G_TYPE_UINT,
+                                      bs->count,
+				      "SortCriteria", G_TYPE_STRING,
+                                      "",
+				      NULL);
+  if (!action) {
+    error = g_error_new (GRL_CORE_ERROR,
+			 GRL_CORE_ERROR_BROWSE_FAILED,
+			 "Failed to start browse action");
+    bs->callback (bs->source, bs->browse_id, NULL, 0, bs->user_data, error);
+    g_error_free (error);
+    g_slice_free (struct OperationSpec, os);
+  }
+
+  g_free (upnp_filter);
+}
+
+static void
+grl_upnp_source_search (GrlMediaSource *source, GrlMediaSourceSearchSpec *ss)
+{
+  GUPnPServiceProxyAction* action;
+  gchar *upnp_filter;
+  GError *error = NULL;
+  gchar *upnp_search;
+  struct OperationSpec *os;
+
+  GRL_DEBUG ("grl_upnp_source_search");
+
+  upnp_filter = get_upnp_filter (ss->keys);
+  GRL_DEBUG ("filter: '%s'", upnp_filter);
+
+  upnp_search = get_upnp_search (ss->text);
+  GRL_DEBUG ("search: '%s'", upnp_search);
+
+  os = g_slice_new0 (struct OperationSpec);
+  os->source = ss->source;
+  os->operation_id = ss->search_id;
+  os->keys = ss->keys;
+  os->skip = ss->skip;
+  os->count = ss->count;
+  os->callback = ss->callback;
+  os->user_data = ss->user_data;
+
+  action =
+    gupnp_service_proxy_begin_action (GRL_UPNP_SOURCE (source)->priv->service,
+				      "Search", gupnp_browse_cb,
+                                      os,
+				      "ContainerID", G_TYPE_STRING,
+                                      "0",
+				      "SearchCriteria", G_TYPE_STRING,
+                                      upnp_search,
+				      "Filter", G_TYPE_STRING,
+                                      upnp_filter,
+				      "StartingIndex", G_TYPE_UINT,
+                                      ss->skip,
+				      "RequestedCount", G_TYPE_UINT,
+                                      ss->count,
+				      "SortCriteria", G_TYPE_STRING,
+                                      "",
+				      NULL);
+  if (!action) {
+    error = g_error_new (GRL_CORE_ERROR,
+			 GRL_CORE_ERROR_SEARCH_FAILED,
+			 "Failed to start browse action");
+    ss->callback (ss->source, ss->search_id, NULL, 0, ss->user_data, error);
+    g_error_free (error);
+    g_slice_free (struct OperationSpec, os);
+  }
+
+  g_free (upnp_filter);
+  g_free (upnp_search);
+}
+
+/*
+ * Query format is the UPnP ContentDirectory SearchCriteria format, e.g.
+ * 'upnp:artist contains "Rick Astley" and
+ *  (upnp:class derivedfrom "object.item.audioItem")'
+ *
+ * Note that we don't guarantee or check that the server actually
+ * supports the given criteria. Offering the searchcaps as
+ * additional metadata to clients that _really_ are interested might
+ * be useful.
+ */
+static void
+grl_upnp_source_query (GrlMediaSource *source, GrlMediaSourceQuerySpec *qs)
+{
+  GUPnPServiceProxyAction* action;
+  gchar *upnp_filter;
+  GError *error = NULL;
+  struct OperationSpec *os;
+
+  GRL_DEBUG (__func__);
+
+  upnp_filter = get_upnp_filter (qs->keys);
+  GRL_DEBUG ("filter: '%s'", upnp_filter);
+
+  GRL_DEBUG ("query: '%s'", qs->query);
+
+  os = g_slice_new0 (struct OperationSpec);
+  os->source = qs->source;
+  os->operation_id = qs->query_id;
+  os->keys = qs->keys;
+  os->skip = qs->skip;
+  os->count = qs->count;
+  os->callback = qs->callback;
+  os->user_data = qs->user_data;
+
+  action =
+    gupnp_service_proxy_begin_action (GRL_UPNP_SOURCE (source)->priv->service,
+				      "Search", gupnp_browse_cb, os,
+				      "ContainerID", G_TYPE_STRING,
+				      "0",
+				      "SearchCriteria", G_TYPE_STRING,
+				      qs->query,
+				      "Filter", G_TYPE_STRING,
+				      upnp_filter,
+				      "StartingIndex", G_TYPE_UINT,
+				      qs->skip,
+				      "RequestedCount", G_TYPE_UINT,
+				      qs->count,
+				      "SortCriteria", G_TYPE_STRING,
+				      "",
+				      NULL);
+  if (!action) {
+    error = g_error_new (GRL_CORE_ERROR,
+			 GRL_CORE_ERROR_QUERY_FAILED,
+			 "Failed to start query action");
+    qs->callback (qs->source, qs->query_id, NULL, 0, qs->user_data, error);
+    g_error_free (error);
+    g_slice_free (struct OperationSpec, os);
+  }
+
+  g_free (upnp_filter);
+}
+
+static void
+grl_upnp_source_metadata (GrlMediaSource *source,
+                          GrlMediaSourceMetadataSpec *ms)
+{
+  GUPnPServiceProxyAction* action;
+  gchar *upnp_filter;
+  gchar *id;
+  GError *error = NULL;
+
+  GRL_DEBUG ("grl_upnp_source_metadata");
+
+  upnp_filter = get_upnp_filter (ms->keys);
+
+  GRL_DEBUG ("filter: '%s'", upnp_filter);
+
+  id = (gchar *) grl_media_get_id (ms->media);
+  if (!id) {
+    grl_media_set_title (ms->media, GRL_UPNP_SOURCE (source)->priv->upnp_name);
+    id = "0";
+  }
+
+  action =
+    gupnp_service_proxy_begin_action (GRL_UPNP_SOURCE (source)->priv->service,
+				      "Browse", gupnp_metadata_cb,
+                                      ms,
+				      "ObjectID", G_TYPE_STRING,
+                                      id,
+				      "BrowseFlag", G_TYPE_STRING,
+                                      "BrowseMetadata",
+				      "Filter", G_TYPE_STRING,
+                                      upnp_filter,
+				      "StartingIndex", G_TYPE_UINT,
+                                      0,
+				      "RequestedCount", G_TYPE_UINT,
+                                      0,
+				      "SortCriteria", G_TYPE_STRING,
+                                      "",
+				      NULL);
+  if (!action) {
+    error = g_error_new (GRL_CORE_ERROR,
+			 GRL_CORE_ERROR_METADATA_FAILED,
+			 "Failed to start metadata action");
+    ms->callback (ms->source, ms->media, ms->user_data, error);
+    g_error_free (error);
+  }
+
+  g_free (upnp_filter);
+}
+
+static GrlSupportedOps
+grl_upnp_source_supported_operations (GrlMetadataSource *metadata_source)
+{
+  GrlSupportedOps caps;
+  GrlUpnpSource *source;
+
+  /* Some sources may support search() while other not, so we rewrite
+     supported_operations() to take that into account.
+     See also note in grl_upnp_source_query() */
+
+  source = GRL_UPNP_SOURCE (metadata_source);
+  caps = GRL_OP_BROWSE | GRL_OP_METADATA | GRL_OP_NOTIFY_CHANGE;
+  if (source->priv->search_enabled)
+    caps = caps | GRL_OP_SEARCH | GRL_OP_QUERY;
+
+  return caps;
+}
+
+static gboolean
+grl_upnp_source_notify_change_start (GrlMediaSource *source,
+                                     GError **error)
+{
+  GrlUpnpSource *upnp_source = GRL_UPNP_SOURCE (source);
+
+  if (!gupnp_service_proxy_add_notify (upnp_source->priv->service,
+                                       "ContainerUpdateIDs",
+                                       G_TYPE_STRING,
+                                       container_changed_cb,
+                                       source)) {
+    g_set_error (error,
+                 GRL_CORE_ERROR,
+                 GRL_CORE_ERROR_NOTIFY_CHANGED_FAILED,
+                 "Unable to listen for changes in %s",
+                 grl_metadata_source_get_id (GRL_METADATA_SOURCE (source)));
+    return FALSE;
+  }
+  gupnp_service_proxy_set_subscribed (upnp_source->priv->service, TRUE);
+
+  return TRUE;
+}
+
+
+static gboolean
+grl_upnp_source_notify_change_stop (GrlMediaSource *source,
+                                    GError **error)
+{
+  GrlUpnpSource *upnp_source = GRL_UPNP_SOURCE (source);
+
+  gupnp_service_proxy_set_subscribed (upnp_source->priv->service, FALSE);
+  gupnp_service_proxy_remove_notify (upnp_source->priv->service,
+                                     "ContainerUpdateIDs",
+                                     container_changed_cb,
+                                     source);
+
+  return TRUE;
+}
diff --git a/src/media/upnp/grl-upnp.h b/src/media/upnp/grl-upnp.h
new file mode 100644
index 0000000..e0bc748
--- /dev/null
+++ b/src/media/upnp/grl-upnp.h
@@ -0,0 +1,75 @@
+/*
+ * Copyright (C) 2010 Igalia S.L.
+ *
+ * Contact: Iago Toral Quiroga <itoral igalia 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_UPNP_SOURCE_H_
+#define _GRL_UPNP_SOURCE_H_
+
+#include <grilo.h>
+
+#define GRL_UPNP_SOURCE_TYPE                    \
+  (grl_upnp_source_get_type ())
+
+#define GRL_UPNP_SOURCE(obj)                            \
+  (G_TYPE_CHECK_INSTANCE_CAST ((obj),                   \
+                               GRL_UPNP_SOURCE_TYPE,    \
+                               GrlUpnpSource))
+
+#define GRL_IS_UPNP_SOURCE(obj)                         \
+  (G_TYPE_CHECK_INSTANCE_TYPE ((obj),                   \
+                               GRL_UPNP_SOURCE_TYPE))
+
+#define GRL_UPNP_SOURCE_CLASS(klass)                    \
+  (G_TYPE_CHECK_CLASS_CAST((klass),                     \
+                           GRL_UPNP_SOURCE_TYPE,        \
+                           GrlUpnpSourceClass))
+
+#define GRL_IS_UPNP_SOURCE_CLASS(klass)                 \
+  (G_TYPE_CHECK_CLASS_TYPE((klass),                     \
+                           GRL_UPNP_SOURCE_TYPE))
+
+#define GRL_UPNP_SOURCE_GET_CLASS(obj)                  \
+  (G_TYPE_INSTANCE_GET_CLASS ((obj),                    \
+                              GRL_UPNP_SOURCE_TYPE,     \
+                              GrlUpnpSourceClass))
+
+typedef struct _GrlUpnpPrivate GrlUpnpPrivate;
+typedef struct _GrlUpnpSource  GrlUpnpSource;
+
+struct _GrlUpnpSource {
+
+  GrlMediaSource parent;
+
+  /*< private >*/
+  GrlUpnpPrivate *priv;
+};
+
+typedef struct _GrlUpnpSourceClass GrlUpnpSourceClass;
+
+struct _GrlUpnpSourceClass {
+
+  GrlMediaSourceClass parent_class;
+
+};
+
+GType grl_upnp_source_get_type (void);
+
+#endif /* _GRL_UPNP_SOURCE_H_ */
diff --git a/src/media/upnp/grl-upnp.xml b/src/media/upnp/grl-upnp.xml
new file mode 100644
index 0000000..3617b98
--- /dev/null
+++ b/src/media/upnp/grl-upnp.xml
@@ -0,0 +1,9 @@
+<plugin>
+  <info>
+    <name>UPnP</name>
+    <description>A plugin for browsing UPnP servers</description>
+    <author>Igalia S.L.</author>
+    <license>LGPL</license>
+    <site>http://www.igalia.com</site>
+  </info>
+</plugin>
diff --git a/src/media/vimeo/Makefile.am b/src/media/vimeo/Makefile.am
new file mode 100644
index 0000000..b810218
--- /dev/null
+++ b/src/media/vimeo/Makefile.am
@@ -0,0 +1,42 @@
+#
+# Makefile.am
+#
+# Author: Joaquim Rocha <jrocha igalia com>
+#
+# Copyright (C) 2010, 2011 Igalia S.L. All rights reserved.
+
+lib_LTLIBRARIES		 = libgrlvimeo.la
+
+libgrlvimeo_la_CFLAGS =	\
+	$(DEPS_CFLAGS)		\
+	$(XML_CFLAGS)		\
+	$(GTHREAD_CFLAGS)	\
+	$(LIBSOUP_CFLAGS)
+
+libgrlvimeo_la_LIBADD =	\
+	$(DEPS_LIBS)		\
+	$(XML_LIBS)		\
+	$(GTHREAD_LIBS)		\
+	$(LIBSOUP_LIBS)
+
+libgrlvimeo_la_LDFLAGS = \
+	-module		 \
+	-avoid-version
+
+libgrlvimeo_la_SOURCES =	\
+	grl-vimeo.c		\
+	grl-vimeo.h		\
+	gvimeo.c		\
+	gvimeo.h
+
+libdir=$(GRL_PLUGINS_DIR)
+vimeoxmldir	= $(GRL_PLUGINS_CONF_DIR)
+vimeoxml_DATA	= $(VIMEO_PLUGIN_ID).xml
+
+EXTRA_DIST = $(vimeoxml_DATA)
+
+MAINTAINERCLEANFILES =	\
+	*.in		\
+	*~
+
+DISTCLEANFILES = $(MAINTAINERCLEANFILES)
diff --git a/src/media/vimeo/grl-vimeo.c b/src/media/vimeo/grl-vimeo.c
new file mode 100644
index 0000000..a235217
--- /dev/null
+++ b/src/media/vimeo/grl-vimeo.c
@@ -0,0 +1,413 @@
+/*
+ * Copyright (C) 2010 Igalia S.L.
+ * Copyright (C) 2011 Intel Corporation.
+ *
+ * Contact: Iago Toral Quiroga <itoral igalia com>
+ *
+ * Authors: Joaquim Rocha <jrocha igalia 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 <grilo.h>
+#include <string.h>
+#include <stdlib.h>
+#include <errno.h>
+
+#include "grl-vimeo.h"
+#include "gvimeo.h"
+
+#define GRL_VIMEO_SOURCE_GET_PRIVATE(object)                           \
+  (G_TYPE_INSTANCE_GET_PRIVATE((object),                                \
+                               GRL_VIMEO_SOURCE_TYPE,                  \
+                               GrlVimeoSourcePrivate))
+
+/* --------- Logging  -------- */
+
+#define GRL_LOG_DOMAIN_DEFAULT vimeo_log_domain
+GRL_LOG_DOMAIN_STATIC(vimeo_log_domain);
+
+/* --- Plugin information --- */
+
+#define PLUGIN_ID   VIMEO_PLUGIN_ID
+
+#define SOURCE_ID   "grl-vimeo"
+#define SOURCE_NAME "Vimeo"
+#define SOURCE_DESC "A source for browsing and searching Vimeo videos"
+
+#define AUTHOR      "Igalia S.L."
+#define LICENSE     "LGPL"
+#define SITE        "http://www.igalia.com";
+
+typedef struct {
+  GrlMediaSourceSearchSpec *ss;
+  gint offset;
+  gint page;
+} SearchData;
+
+struct _GrlVimeoSourcePrivate {
+  GVimeo *vimeo;
+};
+
+static GrlVimeoSource *grl_vimeo_source_new (void);
+
+gboolean grl_vimeo_plugin_init (GrlPluginRegistry *registry,
+				const GrlPluginInfo *plugin,
+				GList *configs);
+
+static const GList *grl_vimeo_source_supported_keys (GrlMetadataSource *source);
+
+static void grl_vimeo_source_metadata (GrlMediaSource *source,
+				       GrlMediaSourceMetadataSpec *ss);
+
+static void grl_vimeo_source_search (GrlMediaSource *source,
+				     GrlMediaSourceSearchSpec *ss);
+
+/* =================== Vimeo Plugin  =============== */
+
+gboolean
+grl_vimeo_plugin_init (GrlPluginRegistry *registry,
+                        const GrlPluginInfo *plugin,
+                        GList *configs)
+{
+  gchar *vimeo_key;
+  gchar *vimeo_secret;
+  GrlConfig *config;
+  gint config_count;
+  gboolean init_result = FALSE;
+  GrlVimeoSource *source;
+
+  GRL_LOG_DOMAIN_INIT (vimeo_log_domain, "vimeo");
+
+  GRL_DEBUG ("vimeo_plugin_init");
+
+  if (!g_thread_supported ()) {
+    g_thread_init (NULL);
+  }
+
+  if (!configs) {
+    GRL_WARNING ("Missing configuration");
+    return FALSE;
+  }
+
+  config_count = g_list_length (configs);
+  if (config_count > 1) {
+    GRL_WARNING ("Provided %d configs, but will only use one", config_count);
+  }
+
+  config = GRL_CONFIG (configs->data);
+
+  vimeo_key = grl_config_get_api_key (config);
+  vimeo_secret = grl_config_get_api_secret (config);
+
+  if (!vimeo_key || !vimeo_secret) {
+    GRL_WARNING ("Required configuration keys not set up");
+    goto go_out;
+  }
+
+  source = grl_vimeo_source_new ();
+  source->priv->vimeo = g_vimeo_new (vimeo_key, vimeo_secret);
+
+  grl_plugin_registry_register_source (registry,
+                                       plugin,
+                                       GRL_MEDIA_PLUGIN (source),
+                                       NULL);
+  init_result = TRUE;
+
+ go_out:
+
+  if (vimeo_key != NULL)
+    g_free (vimeo_key);
+  if (vimeo_secret != NULL)
+    g_free (vimeo_secret);
+
+  return init_result;
+}
+
+GRL_PLUGIN_REGISTER (grl_vimeo_plugin_init,
+                     NULL,
+                     PLUGIN_ID);
+
+/* ================== Vimeo GObject ================ */
+
+static GrlVimeoSource *
+grl_vimeo_source_new (void)
+{
+  GRL_DEBUG ("grl_vimeo_source_new");
+
+  return g_object_new (GRL_VIMEO_SOURCE_TYPE,
+                       "source-id", SOURCE_ID,
+                       "source-name", SOURCE_NAME,
+                       "source-desc", SOURCE_DESC,
+                       NULL);
+}
+
+static void
+grl_vimeo_source_class_init (GrlVimeoSourceClass * klass)
+{
+  GrlMediaSourceClass *source_class = GRL_MEDIA_SOURCE_CLASS (klass);
+  GrlMetadataSourceClass *metadata_class = GRL_METADATA_SOURCE_CLASS (klass);
+
+  source_class->metadata = grl_vimeo_source_metadata;
+  source_class->search = grl_vimeo_source_search;
+  metadata_class->supported_keys = grl_vimeo_source_supported_keys;
+
+  g_type_class_add_private (klass, sizeof (GrlVimeoSourcePrivate));
+}
+
+static void
+grl_vimeo_source_init (GrlVimeoSource *source)
+{
+  source->priv = GRL_VIMEO_SOURCE_GET_PRIVATE (source);
+}
+
+G_DEFINE_TYPE (GrlVimeoSource, grl_vimeo_source, GRL_TYPE_MEDIA_SOURCE);
+
+/* ======================= Utilities ==================== */
+
+static gint
+str_to_gint (gchar *str)
+{
+  gint number;
+
+  errno = 0;
+  number = (gint) g_ascii_strtod (str, NULL);
+  if (errno == 0)
+  {
+    return number;
+  }
+  return 0;
+}
+
+static void
+update_media (GrlMedia *media, GHashTable *video)
+{
+  gchar *str;
+
+  str = g_hash_table_lookup (video, VIMEO_VIDEO_ID);
+  if (str)
+  {
+    grl_media_set_id (media, str);
+  }
+
+  str = g_hash_table_lookup (video, VIMEO_VIDEO_TITLE);
+  if (str)
+  {
+    grl_media_set_title (media, str);
+  }
+
+  str = g_hash_table_lookup (video, VIMEO_VIDEO_DESCRIPTION);
+  if (str)
+  {
+    grl_media_set_description (media, str);
+  }
+
+  str = g_hash_table_lookup (video, VIMEO_VIDEO_DURATION);
+  if (str)
+  {
+    grl_media_set_duration (media, str_to_gint (str));
+  }
+
+  str = g_hash_table_lookup (video, VIMEO_VIDEO_OWNER_NAME);
+  if (str)
+  {
+    grl_media_set_author (media, str);
+  }
+
+  str = g_hash_table_lookup (video, VIMEO_VIDEO_UPLOAD_DATE);
+  if (str)
+  {
+    grl_media_set_date (media, str);
+  }
+
+  str = g_hash_table_lookup (video, VIMEO_VIDEO_THUMBNAIL);
+  if (str)
+  {
+    grl_media_set_thumbnail (media, str);
+  }
+
+  str = g_hash_table_lookup (video, VIMEO_VIDEO_WIDTH);
+  if (str)
+  {
+    grl_media_video_set_width (GRL_MEDIA_VIDEO (media), str_to_gint (str));
+  }
+
+  str = g_hash_table_lookup (video, VIMEO_VIDEO_HEIGHT);
+  if (str)
+  {
+    grl_media_video_set_height (GRL_MEDIA_VIDEO (media), str_to_gint (str));
+  }
+}
+
+
+static void
+search_cb (GVimeo *vimeo, GList *video_list, gpointer user_data)
+{
+  GrlMedia *media = NULL;
+  SearchData *sd = (SearchData *) user_data;
+  gint count = sd->ss->count;
+  gchar *media_type;
+
+  /* Go to offset element */
+  video_list = g_list_nth (video_list, sd->offset);
+
+  /* No more elements can be sent */
+  if (!video_list) {
+    sd->ss->callback (sd->ss->source,
+                      sd->ss->search_id,
+                      NULL,
+                      0,
+                      sd->ss->user_data,
+                      NULL);
+    g_slice_free (SearchData, sd);
+    return;
+  }
+
+  while (video_list && count)
+  {
+    media_type = g_hash_table_lookup (video_list->data, "title");
+    if (media_type) {
+      media = grl_media_video_new ();
+    }
+
+    if (media)
+    {
+      update_media (media, video_list->data);
+      sd->ss->callback (sd->ss->source,
+			sd->ss->search_id,
+			media,
+			sd->ss->count == 1? 0: -1,
+			sd->ss->user_data,
+			NULL);
+    }
+    video_list = g_list_next (video_list);
+
+    if (--count)
+      sd->ss->count = count;
+
+    media = NULL;
+  }
+
+  /* Get more elements */
+  if (count)
+  {
+    sd->offset = 0;
+    sd->page++;
+    g_vimeo_videos_search (vimeo, sd->ss->text, sd->page, search_cb, sd);
+  }
+  else
+  {
+    g_slice_free (SearchData, sd);
+  }
+}
+
+static void
+video_get_play_url_cb (gchar *url, gpointer user_data)
+{
+  GrlMediaSourceMetadataSpec *ms = (GrlMediaSourceMetadataSpec *) user_data;
+
+  if (url)
+  {
+    grl_media_set_url (ms->media, url);
+  }
+
+  ms->callback (ms->source, ms->media, ms->user_data, NULL);
+}
+
+/* ================== API Implementation ================ */
+
+static const GList *
+grl_vimeo_source_supported_keys (GrlMetadataSource *source)
+{
+  static GList *keys = NULL;
+  if (!keys) {
+    keys = grl_metadata_key_list_new (GRL_METADATA_KEY_ID,
+				      GRL_METADATA_KEY_TITLE,
+				      GRL_METADATA_KEY_DESCRIPTION,
+				      GRL_METADATA_KEY_URL,
+				      GRL_METADATA_KEY_AUTHOR,
+                                      GRL_METADATA_KEY_DATE,
+				      GRL_METADATA_KEY_THUMBNAIL,
+				      GRL_METADATA_KEY_DURATION,
+				      GRL_METADATA_KEY_WIDTH,
+				      GRL_METADATA_KEY_HEIGHT,
+				      NULL);
+  }
+  return keys;
+}
+
+static void
+grl_vimeo_source_metadata (GrlMediaSource *source,
+			   GrlMediaSourceMetadataSpec *ms)
+{
+  gint id;
+  const gchar *id_str;
+
+  if (!ms->media || (id_str = grl_media_get_id (ms->media)) == NULL)
+  {
+    ms->callback (ms->source, ms->media, ms->user_data, NULL);
+    return;
+  }
+
+  errno = 0;
+  id = (gint) g_ascii_strtod (id_str, NULL);
+  if (errno != 0)
+  {
+    return;
+  }
+
+  g_vimeo_video_get_play_url (GRL_VIMEO_SOURCE (source)->priv->vimeo,
+			      id,
+			      video_get_play_url_cb,
+			      ms);
+}
+
+static void
+grl_vimeo_source_search (GrlMediaSource *source,
+			 GrlMediaSourceSearchSpec *ss)
+{
+  SearchData *sd;
+  GError *error;
+  gint per_page;
+  GVimeo *vimeo = GRL_VIMEO_SOURCE (source)->priv->vimeo;
+
+  if (!ss->text) {
+    /* Vimeo does not support searching all */
+    error =
+      g_error_new_literal (GRL_CORE_ERROR,
+                           GRL_CORE_ERROR_SEARCH_NULL_UNSUPPORTED,
+                           "Unable to execute search: non NULL search text is required");
+    ss->callback (ss->source, ss->search_id, NULL, 0, ss->user_data, error);
+    g_error_free (error);
+    return;
+  }
+
+  /* Compute items per page and page offset */
+  per_page = CLAMP (1 + ss->skip + ss->count, 0, 100);
+  g_vimeo_set_per_page (vimeo, per_page);
+
+  sd = g_slice_new (SearchData);
+  sd->page = 1 + (ss->skip / per_page);
+  sd->offset = ss->skip % per_page;
+  sd->ss = ss;
+
+  g_vimeo_videos_search (vimeo, ss->text, sd->page, search_cb, sd);
+}
diff --git a/src/media/vimeo/grl-vimeo.h b/src/media/vimeo/grl-vimeo.h
new file mode 100644
index 0000000..fde4f36
--- /dev/null
+++ b/src/media/vimeo/grl-vimeo.h
@@ -0,0 +1,77 @@
+/*
+ * Copyright (C) 2010 Igalia S.L.
+ *
+ * Contact: Iago Toral Quiroga <itoral igalia com>
+ *
+ * Authors: Joaquim Rocha <jrocha igalia 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_VIMEO_SOURCE_H_
+#define _GRL_VIMEO_SOURCE_H_
+
+#include <grilo.h>
+
+#define GRL_VIMEO_SOURCE_TYPE                  \
+  (grl_vimeo_source_get_type ())
+
+#define GRL_VIMEO_SOURCE(obj)                          \
+  (G_TYPE_CHECK_INSTANCE_CAST ((obj),                   \
+                               GRL_VIMEO_SOURCE_TYPE,  \
+                               GrlVimeoSource))
+
+#define GRL_IS_VIMEO_SOURCE(obj)                       \
+  (G_TYPE_CHECK_INSTANCE_TYPE ((obj),                   \
+                               GRL_VIMEO_SOURCE_TYPE))
+
+#define GRL_VIMEO_SOURCE_CLASS(klass)                  \
+  (G_TYPE_CHECK_CLASS_CAST((klass),                     \
+                           GRL_VIMEO_SOURCE_TYPE,      \
+                           GrlVimeoSourceClass))
+
+#define GRL_IS_VIMEO_SOURCE_CLASS(klass)               \
+  (G_TYPE_CHECK_CLASS_TYPE((klass),                     \
+                           GRL_VIMEO_SOURCE_TYPE))
+
+#define GRL_VIMEO_SOURCE_GET_CLASS(obj)                \
+  (G_TYPE_INSTANCE_GET_CLASS ((obj),                    \
+                              GRL_VIMEO_SOURCE_TYPE,   \
+                              GrlVimeoSourceClass))
+
+typedef struct _GrlVimeoSource        GrlVimeoSource;
+typedef struct _GrlVimeoSourcePrivate GrlVimeoSourcePrivate;
+
+struct _GrlVimeoSource {
+
+  GrlMediaSource parent;
+
+  /*< private >*/
+  GrlVimeoSourcePrivate *priv;
+};
+
+typedef struct _GrlVimeoSourceClass GrlVimeoSourceClass;
+
+struct _GrlVimeoSourceClass {
+
+  GrlMediaSourceClass parent_class;
+
+};
+
+GType grl_vimeo_source_get_type (void);
+
+#endif /* _GRL_VIMEO_SOURCE_H_ */
diff --git a/src/media/vimeo/grl-vimeo.xml b/src/media/vimeo/grl-vimeo.xml
new file mode 100644
index 0000000..b80c9d9
--- /dev/null
+++ b/src/media/vimeo/grl-vimeo.xml
@@ -0,0 +1,9 @@
+<plugin>
+  <info>
+    <name>Vimeo</name>
+    <description>A plugin for searching Vimeo videos</description>
+    <author>Igalia S.L.</author>
+    <license>LGPL</license>
+    <site>http://www.igalia.com</site>
+  </info>
+</plugin>
diff --git a/src/media/vimeo/gvimeo.c b/src/media/vimeo/gvimeo.c
new file mode 100644
index 0000000..3b7f2fc
--- /dev/null
+++ b/src/media/vimeo/gvimeo.c
@@ -0,0 +1,517 @@
+/*
+ * Copyright (C) 2010 Igalia S.L.
+ *
+ * Contact: Iago Toral Quiroga <itoral igalia com>
+ *
+ * Authors: Joaquim Rocha <jrocha igalia 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 <gcrypt.h>
+#include <libsoup/soup.h>
+#include "gvimeo.h"
+#include <libxml/parser.h>
+#include <libxml/xpath.h>
+
+#define G_VIMEO_GET_PRIVATE(object)             \
+  (G_TYPE_INSTANCE_GET_PRIVATE((object),        \
+                               G_VIMEO_TYPE,    \
+                               GVimeoPrivate))
+
+#define PLUGIN_USER_AGENT             "Grilo Vimeo Plugin"
+
+#define VIMEO_ENDPOINT                "http://vimeo.com/api/rest/v2";
+#define VIMEO_VIDEO_LOAD_URL          "http://vimeo.com/moogaloop/load/clip:";
+#define VIMEO_VIDEO_PLAY_URL          "http://vimeo.com/moogaloop/play/clip:";
+
+#define VIMEO_VIDEO_SEARCH_METHOD     "vimeo.videos.search"
+#define VIMEO_API_OAUTH_SIGN_METHOD   "HMAC-SHA1"
+#define VIMEO_API_OAUTH_SIGNATURE_PARAM "&oauth_signature=%s"
+
+#define VIMEO_VIDEO_SEARCH					\
+  "full_response=yes"						\
+  "&method=%s"							\
+  "&oauth_consumer_key=%s"					\
+  "&oauth_nonce=%s"						\
+  "&oauth_signature_method=" VIMEO_API_OAUTH_SIGN_METHOD	\
+  "&oauth_timestamp=%s"						\
+  "&oauth_token="						\
+  "&page=%d"							\
+  "&per_page=%d"						\
+  "&query=%s"
+
+typedef struct {
+  GVimeo *vimeo;
+  GVimeoVideoSearchCb search_cb;
+  gpointer user_data;
+} GVimeoVideoSearchData;
+
+typedef struct {
+  GVimeo *vimeo;
+  gint video_id;
+  GVimeoURLCb callback;
+  gpointer user_data;
+} GVimeoVideoURLData;
+
+struct _GVimeoPrivate {
+  gchar *api_key;
+  gchar *auth_token;
+  gchar *auth_secret;
+  gint per_page;
+  SoupSession *async_session;
+};
+
+enum InfoType {SIMPLE, EXTENDED};
+
+typedef struct {
+  enum InfoType type;
+  gchar *name;
+} VideoInfo;
+
+static VideoInfo video_info[] = {{SIMPLE, VIMEO_VIDEO_TITLE},
+				 {SIMPLE, VIMEO_VIDEO_DESCRIPTION},
+				 {SIMPLE, VIMEO_VIDEO_UPLOAD_DATE},
+				 {SIMPLE, VIMEO_VIDEO_WIDTH},
+				 {SIMPLE, VIMEO_VIDEO_HEIGHT},
+				 {SIMPLE, VIMEO_VIDEO_OWNER},
+				 {SIMPLE, VIMEO_VIDEO_URL},
+				 {SIMPLE, VIMEO_VIDEO_THUMBNAIL},
+				 {SIMPLE, VIMEO_VIDEO_DURATION},
+				 {EXTENDED, VIMEO_VIDEO_OWNER}};
+
+static void g_vimeo_finalize (GObject *object);
+static gchar * encode_uri (const gchar *uri);
+
+/* -------------------- GOBJECT -------------------- */
+
+G_DEFINE_TYPE (GVimeo, g_vimeo, G_TYPE_OBJECT);
+
+static void
+g_vimeo_class_init (GVimeoClass *klass)
+{
+  GObjectClass *gobject_class = G_OBJECT_CLASS (klass);
+  gobject_class->finalize = g_vimeo_finalize;
+
+  g_type_class_add_private (klass, sizeof (GVimeoPrivate));
+}
+
+static void
+g_vimeo_init (GVimeo *vimeo)
+{
+  vimeo->priv = G_VIMEO_GET_PRIVATE (vimeo);
+  vimeo->priv->per_page = 50;
+  vimeo->priv->async_session = soup_session_async_new ();
+}
+
+static void
+g_vimeo_finalize (GObject *object)
+{
+  GVimeo *vimeo = G_VIMEO (object);
+  g_free (vimeo->priv->api_key);
+  g_free (vimeo->priv->auth_secret);
+  g_object_unref (vimeo->priv->async_session);
+
+  G_OBJECT_CLASS (g_vimeo_parent_class)->finalize (object);
+}
+
+GVimeo *
+g_vimeo_new (const gchar *api_key, const gchar *auth_secret)
+{
+  GVimeo *vimeo = g_object_new (G_VIMEO_TYPE, NULL);
+  vimeo->priv->api_key = g_strdup (api_key);
+  vimeo->priv->auth_secret = g_strdup (auth_secret);
+
+  return vimeo;
+}
+
+/* -------------------- PRIVATE API -------------------- */
+
+static gchar *
+get_timestamp (void)
+{
+  time_t t = time (NULL);
+  return g_strdup_printf ("%d", (gint) t);
+}
+
+static gchar *
+get_nonce (void)
+{
+  gchar *timestamp = get_timestamp();
+  guint rnd_number = g_random_int ();
+  gchar *rnd_str = g_strdup_printf ("%d_%s", rnd_number, timestamp);
+  gchar *nonce = g_compute_checksum_for_string (G_CHECKSUM_MD5, rnd_str, -1);
+  g_free (timestamp);
+  g_free (rnd_str);
+
+  return nonce;
+}
+
+static gchar *
+get_videos_search_params (GVimeo *vimeo, const gchar *text, gint page) {
+  gchar *encoded_text = encode_uri (text);
+  gchar *timestamp = get_timestamp ();
+  gchar *nonce = get_nonce ();
+  gchar *params = g_strdup_printf (VIMEO_VIDEO_SEARCH,
+				   VIMEO_VIDEO_SEARCH_METHOD,
+				   vimeo->priv->api_key,
+				   nonce,
+				   timestamp,
+				   page,
+				   vimeo->priv->per_page,
+				   encoded_text);
+  g_free (timestamp);
+  g_free (nonce);
+  g_free (encoded_text);
+
+  return params;
+}
+
+static gchar *
+sign_string (gchar *message, gchar *key)
+{
+  gchar *signed_message = NULL;
+  gcry_md_hd_t digest_obj;
+  unsigned char *hmac_digest;
+  guint digest_len;
+
+  gcry_md_open(&digest_obj,
+	       GCRY_MD_SHA1,
+	       GCRY_MD_FLAG_SECURE | GCRY_MD_FLAG_HMAC);
+  gcry_md_setkey(digest_obj, key, strlen (key));
+  gcry_md_write (digest_obj, message, strlen (message));
+  gcry_md_final (digest_obj);
+  hmac_digest = gcry_md_read (digest_obj, 0);
+
+  digest_len = gcry_md_get_algo_dlen (GCRY_MD_SHA1);
+  signed_message = g_base64_encode (hmac_digest, digest_len);
+
+  gcry_md_close (digest_obj);
+
+  return signed_message;
+}
+
+static gboolean
+result_is_correct (xmlNodePtr node)
+{
+  gboolean correct = FALSE;
+  xmlChar *stat;
+
+  if (xmlStrcmp (node->name, (const xmlChar *) "rsp") == 0)
+  {
+    stat = xmlGetProp (node, (const xmlChar *) "stat");
+    if (stat && xmlStrcmp (stat, (const xmlChar *) "ok") == 0)
+    {
+      correct = TRUE;
+      xmlFree (stat);
+    }
+  }
+
+  return correct;
+}
+
+static void
+add_node (xmlNodePtr node, GHashTable *video)
+{
+  xmlAttrPtr attr;
+
+  for (attr = node->properties; attr != NULL; attr = attr->next)
+  {
+    g_hash_table_insert (video,
+                         g_strconcat ((const gchar *) node->name,
+                                      "_",
+                                      (const gchar *) attr->name,
+                                      NULL),
+                         (gchar *) xmlGetProp (node, attr->name));
+  }
+}
+
+static xmlNodePtr
+xpath_get_node (xmlXPathContextPtr context, gchar *xpath_expr)
+{
+  xmlNodePtr node = NULL;
+  xmlXPathObjectPtr xpath_obj;
+  xpath_obj = xmlXPathEvalExpression ((xmlChar *) xpath_expr, context);
+
+  if (xpath_obj && xpath_obj->nodesetval->nodeTab)
+  {
+    node = xpath_obj->nodesetval->nodeTab[0];
+  }
+  xmlXPathFreeObject (xpath_obj);
+
+  return node;
+}
+
+static gchar *
+get_node_text (xmlXPathContextPtr context, gchar *xpath_expr)
+{
+  xmlNodePtr node;
+  gchar *node_text = NULL;
+
+  node = xpath_get_node (context, xpath_expr);
+  if (node)
+  {
+    node_text = (gchar *) xmlNodeGetContent (node);
+  }
+
+  return node_text;
+}
+
+static GHashTable *
+get_video (xmlNodePtr node)
+{
+  gint i;
+  gint array_length;
+  xmlXPathContextPtr context;
+  gchar *video_id;
+  GHashTable *video = g_hash_table_new_full (g_str_hash,
+                                             g_str_equal,
+                                             g_free,
+                                             g_free);
+
+  /* Adds the video node's properties */
+  add_node (node, video);
+
+  context = xmlXPathNewContext (node->doc);
+  video_id = (gchar *) xmlGetProp (node, (xmlChar *) "id");
+
+  array_length = G_N_ELEMENTS (video_info);
+  for (i = 0; i < array_length; i++)
+  {
+    /* Look for the wanted information only under the current video */
+    gchar *xpath_name = g_strdup_printf ("//video[@id=%s]//%s",
+					 video_id,
+					 video_info[i].name);
+    xmlNodePtr info_node = xpath_get_node (context, xpath_name);
+    if (info_node)
+    {
+      if (video_info[i].type == EXTENDED) {
+	add_node (info_node, video);
+      }
+      else
+      {
+	g_hash_table_insert (video,
+			     g_strdup ((const gchar *) info_node->name),
+			     (gchar *) xmlNodeGetContent (info_node));
+      }
+    }
+    g_free (xpath_name);
+  }
+  g_free (video_id);
+
+  xmlXPathFreeContext (context);
+
+  return video;
+}
+
+
+static void
+process_video_search_result (const gchar *xml_result, gpointer user_data)
+{
+  xmlDocPtr doc;
+  xmlNodePtr node;
+  GList *video_list = NULL;
+  GVimeoVideoSearchData *data = (GVimeoVideoSearchData *) user_data;
+
+  doc = xmlReadMemory (xml_result,
+		       xmlStrlen ((xmlChar *) xml_result),
+		       NULL,
+		       NULL,
+		       XML_PARSE_RECOVER | XML_PARSE_NOBLANKS);
+  node = xmlDocGetRootElement (doc);
+
+  /* Check result is ok */
+  if (!node || !result_is_correct (node))
+  {
+    data->search_cb (data->vimeo, NULL, data->user_data);
+  }
+  else
+  {
+    node = node->xmlChildrenNode;
+
+    /* Now we're at "video pages" node */
+    node = node->xmlChildrenNode;
+    while (node)
+    {
+      video_list = g_list_prepend (video_list, get_video (node));
+      node = node->next;
+    }
+
+    data->search_cb (data->vimeo, g_list_reverse (video_list), data->user_data);
+    g_list_foreach (video_list, (GFunc) g_hash_table_unref, NULL);
+    g_list_free (video_list);
+  }
+  g_slice_free (GVimeoVideoSearchData, data);
+  xmlFreeDoc (doc);
+}
+
+static void
+search_videos_complete_cb (SoupSession *session,
+			   SoupMessage *message,
+			   gpointer *data)
+{
+  GVimeoVideoSearchCb *search_data = (GVimeoVideoSearchCb *) data;
+  process_video_search_result (message->response_body->data, search_data);
+}
+
+static gchar *
+get_play_url_from_vimeo_xml (const gchar *xml, gint video_id)
+{
+  xmlDocPtr doc = xmlRecoverDoc ((xmlChar *) xml);
+  xmlXPathContextPtr context = xmlXPathNewContext (doc);
+  gchar *request_signature = get_node_text (context,
+					    "/xml/request_signature[1]");
+  gchar *signature_expires = get_node_text (context,
+					    "/xml/request_signature_expires[1]");
+
+  gchar *url = g_strdup_printf ("%s%d/%s/%s/?q=sd",
+				VIMEO_VIDEO_PLAY_URL,
+			        video_id,
+				request_signature,
+				signature_expires);
+
+  g_free (request_signature);
+  g_free (signature_expires);
+  xmlXPathFreeContext (context);
+  xmlFreeDoc (doc);
+
+  return url;
+}
+
+static void
+get_video_play_url_complete_cb (SoupSession *session,
+				SoupMessage *message,
+				gpointer *data)
+{
+  GVimeoVideoURLData *url_data;
+  gchar *url;
+
+  if (message->response_body == NULL)
+  {
+    return;
+  }
+
+  url_data = (GVimeoVideoURLData *) data;
+  url =  get_play_url_from_vimeo_xml (message->response_body->data,
+				      url_data->video_id);
+  url_data->callback (url, url_data->user_data);
+  g_slice_free (GVimeoVideoURLData, url_data);
+}
+
+static gchar *
+encode_uri (const gchar *uri)
+{
+  return soup_uri_encode (uri, "%!*'();:@&=+$,/?#[] ");
+}
+
+static gchar *
+build_request (GVimeo *vimeo, const gchar *query, gint page)
+{
+  gchar *params;
+  gchar *endpoint_encoded;
+  gchar *key;
+  gchar *escaped_str;
+  gchar *tmp_str;
+  gchar *signature;
+
+  g_return_val_if_fail (G_IS_VIMEO (vimeo), NULL);
+
+  params = get_videos_search_params (vimeo, query, page);
+  endpoint_encoded = encode_uri (VIMEO_ENDPOINT);
+  key = g_strdup_printf ("%s&", vimeo->priv->auth_secret);
+  escaped_str = encode_uri (params);
+  tmp_str = g_strdup_printf ("GET&%s&%s", endpoint_encoded, escaped_str);
+  signature = sign_string (tmp_str, key);
+  g_free (escaped_str);
+  g_free (tmp_str);
+  escaped_str = encode_uri (signature);
+  tmp_str = g_strdup_printf ("%s?%s" VIMEO_API_OAUTH_SIGNATURE_PARAM,
+			     VIMEO_ENDPOINT,
+			     params,
+			     escaped_str);
+
+  g_free (endpoint_encoded);
+  g_free (params);
+  g_free (key);
+  g_free (escaped_str);
+  g_free (signature);
+
+  return tmp_str;
+}
+
+/* -------------------- PUBLIC API -------------------- */
+
+void
+g_vimeo_set_per_page (GVimeo *vimeo, gint per_page)
+{
+  g_return_if_fail (G_IS_VIMEO (vimeo));
+  vimeo->priv->per_page = per_page;
+}
+
+void
+g_vimeo_videos_search (GVimeo *vimeo,
+		       const gchar *text,
+		       gint page,
+		       GVimeoVideoSearchCb callback,
+		       gpointer user_data)
+{
+  SoupMessage *message;
+  GVimeoVideoSearchData *search_data;
+  gchar *request;
+
+  g_return_if_fail (G_IS_VIMEO (vimeo));
+
+  request = build_request (vimeo, text, page);
+  search_data = g_slice_new (GVimeoVideoSearchData);
+  search_data->vimeo = vimeo;
+  search_data->search_cb = callback;
+  search_data->user_data = user_data;
+
+  message = soup_message_new ("GET", request);
+  soup_session_queue_message (vimeo->priv->async_session,
+			      message,
+			      (SoupSessionCallback) search_videos_complete_cb,
+			      search_data);
+  g_free (request);
+}
+
+void
+g_vimeo_video_get_play_url (GVimeo *vimeo,
+			    gint id,
+			    GVimeoURLCb callback,
+			    gpointer user_data)
+{
+  GVimeoVideoURLData *data;
+  gchar *url = g_strdup_printf ("%s%d",
+				VIMEO_VIDEO_LOAD_URL,
+				id);
+  SoupMessage *message = soup_message_new ("GET", url);
+  SoupMessageHeaders *headers = message->request_headers;
+  soup_message_headers_append (headers, "User-Agent", PLUGIN_USER_AGENT);
+
+  data = g_slice_new (GVimeoVideoURLData);
+  data->video_id = id;
+  data->vimeo = vimeo;
+  data->callback = callback;
+  data->user_data = user_data;
+
+  soup_session_queue_message (vimeo->priv->async_session,
+			      message,
+			      (SoupSessionCallback) get_video_play_url_complete_cb,
+			      data);
+  g_free (url);
+}
diff --git a/src/media/vimeo/gvimeo.h b/src/media/vimeo/gvimeo.h
new file mode 100644
index 0000000..34e17a3
--- /dev/null
+++ b/src/media/vimeo/gvimeo.h
@@ -0,0 +1,112 @@
+/*
+ * Copyright (C) 2010 Igalia S.L.
+ *
+ * Contact: Iago Toral Quiroga <itoral igalia com>
+ *
+ * Authors: Joaquim Rocha <jrocha igalia 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 _G_VIMEO_H_
+#define _G_VIMEO_H_
+
+#include <glib-object.h>
+
+#define G_VIMEO_TYPE                           \
+  (g_vimeo_get_type ())
+
+#define G_VIMEO(obj)                                   \
+  (G_TYPE_CHECK_INSTANCE_CAST ((obj),                   \
+                               G_VIMEO_TYPE,           \
+                               GVimeo))
+
+#define G_IS_VIMEO(obj)                                \
+  (G_TYPE_CHECK_INSTANCE_TYPE ((obj),                   \
+                               G_VIMEO_TYPE))
+
+#define G_VIMEO_CLASS(klass)                           \
+  (G_TYPE_CHECK_CLASS_CAST((klass),                     \
+                           G_VIMEO_TYPE,               \
+                           GVimeoClass))
+
+#define G_IS_VIMEO_CLASS(klass)                        \
+  (G_TYPE_CHECK_CLASS_TYPE((klass),                     \
+                           G_VIMEO_TYPE))
+
+#define G_VIMEO_GET_CLASS(obj)                         \
+  (G_TYPE_INSTANCE_GET_CLASS ((obj),                    \
+                              G_VIMEO_TYPE,            \
+                              GVimeoClass))
+
+#define VIMEO_VIDEO "video"
+#define VIMEO_VIDEO_ID VIMEO_VIDEO "_id"
+#define VIMEO_VIDEO_TITLE "title"
+#define VIMEO_VIDEO_DESCRIPTION "description"
+#define VIMEO_VIDEO_URL "url"
+#define VIMEO_VIDEO_UPLOAD_DATE "upload_date"
+#define VIMEO_VIDEO_WIDTH "width"
+#define VIMEO_VIDEO_HEIGHT "height"
+#define VIMEO_VIDEO_DURATION "duration"
+#define VIMEO_VIDEO_OWNER "owner"
+#define VIMEO_VIDEO_THUMBNAIL "thumbnail"
+
+#define VIMEO_VIDEO_OWNER_NAME VIMEO_VIDEO_OWNER "_realname"
+
+typedef struct _GVimeo        GVimeo;
+typedef struct _GVimeoPrivate GVimeoPrivate;
+
+struct _GVimeo {
+
+  GObject parent;
+
+  /*< private >*/
+  GVimeoPrivate *priv;
+};
+
+typedef struct _GVimeoClass GVimeoClass;
+
+struct _GVimeoClass {
+
+  GObjectClass parent_class;
+
+};
+
+typedef void (*GVimeoVideoSearchCb) (GVimeo *vimeo,
+				     GList *videolist,
+				     gpointer user_data);
+
+typedef void (*GVimeoURLCb) (gchar *url, gpointer user_data);
+
+GType g_vimeo_get_type (void);
+
+GVimeo *g_vimeo_new (const gchar *api_key, const gchar *auth_secret);
+
+void g_vimeo_video_get_play_url (GVimeo *vimeo,
+				 gint id,
+				 GVimeoURLCb callback,
+				 gpointer user_data);
+
+void g_vimeo_set_per_page (GVimeo *vimeo, gint per_page);
+
+void g_vimeo_videos_search (GVimeo *vimeo,
+			    const gchar *text,
+			    gint page,
+			    GVimeoVideoSearchCb callback,
+			    gpointer user_data);
+
+#endif /* _G_VIMEO_H_ */
diff --git a/src/media/youtube/Makefile.am b/src/media/youtube/Makefile.am
new file mode 100644
index 0000000..2384bb8
--- /dev/null
+++ b/src/media/youtube/Makefile.am
@@ -0,0 +1,40 @@
+#
+# Makefile.am
+#
+# Author: Iago Toral Quiroga <itoral igalia com>
+#
+# Copyright (C) 2010, 2011 Igalia S.L. All rights reserved.
+
+lib_LTLIBRARIES = libgrlyoutube.la
+
+libgrlyoutube_la_CFLAGS =	\
+	$(DEPS_CFLAGS)		\
+	$(GRLNET_CFLAGS)	\
+	$(XML_CFLAGS) 		\
+	$(GTHREAD_CFLAGS)	\
+	$(GDATA_CFLAGS)
+
+libgrlyoutube_la_LIBADD =	\
+	$(DEPS_LIBS)		\
+	$(GRLNET_LIBS)		\
+	$(XML_LIBS) 		\
+	$(GTHREAD_LIBS)		\
+	$(GDATA_LIBS)
+
+libgrlyoutube_la_LDFLAGS = \
+	-module		   \
+	-avoid-version
+
+libgrlyoutube_la_SOURCES = grl-youtube.c grl-youtube.h
+
+libdir = $(GRL_PLUGINS_DIR)
+youtubexmldir	= $(GRL_PLUGINS_CONF_DIR)
+youtubexml_DATA	= $(YOUTUBE_PLUGIN_ID).xml
+
+EXTRA_DIST = $(youtubexml_DATA)
+
+MAINTAINERCLEANFILES	 = \
+	*.in \
+	*~
+
+DISTCLEANFILES = $(MAINTAINERCLEANFILES)
diff --git a/src/media/youtube/TODO b/src/media/youtube/TODO
new file mode 100644
index 0000000..409835c
--- /dev/null
+++ b/src/media/youtube/TODO
@@ -0,0 +1,6 @@
+- Consider using libgdata.
+- Fix get_metadata() for standard feeds.
+- Consider implementing store() -> video upload.
+- Consider implementing configuration (personal account)
+  -> Could add new category for browsing (my videos)
+  -> would enable store() and remove() operations.
diff --git a/src/media/youtube/grl-youtube.c b/src/media/youtube/grl-youtube.c
new file mode 100644
index 0000000..c6dfd2e
--- /dev/null
+++ b/src/media/youtube/grl-youtube.c
@@ -0,0 +1,1630 @@
+/*
+ * Copyright (C) 2010 Igalia S.L.
+ * Copyright (C) 2011 Intel Corporation.
+ *
+ * Contact: Iago Toral Quiroga <itoral igalia 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 <grilo.h>
+#include <net/grl-net.h>
+#include <gdata/gdata.h>
+#include <string.h>
+
+#include "grl-youtube.h"
+
+enum {
+  PROP_0,
+  PROP_SERVICE,
+};
+
+#define GRL_YOUTUBE_SOURCE_GET_PRIVATE(object)            \
+  (G_TYPE_INSTANCE_GET_PRIVATE((object),                  \
+                               GRL_YOUTUBE_SOURCE_TYPE,   \
+                               GrlYoutubeSourcePriv))
+
+/* --------- Logging  -------- */
+
+#define GRL_LOG_DOMAIN_DEFAULT youtube_log_domain
+GRL_LOG_DOMAIN_STATIC(youtube_log_domain);
+
+/* ----- Root categories ---- */
+
+#define YOUTUBE_ROOT_NAME       "Youtube"
+
+#define ROOT_DIR_FEEDS_INDEX      0
+#define ROOT_DIR_CATEGORIES_INDEX 1
+
+#define YOUTUBE_FEEDS_ID        "standard-feeds"
+#define YOUTUBE_FEEDS_NAME      "Standard feeds"
+
+#define YOUTUBE_CATEGORIES_ID   "categories"
+#define YOUTUBE_CATEGORIES_NAME "Categories"
+#define YOUTUBE_CATEGORIES_URL  "http://gdata.youtube.com/schemas/2007/categories.cat";
+
+/* ----- Feeds categories ---- */
+
+#define YOUTUBE_TOP_RATED_ID         (YOUTUBE_FEEDS_ID "/0")
+#define YOUTUBE_TOP_RATED_NAME       "Top Rated"
+
+#define YOUTUBE_TOP_FAVS_ID          (YOUTUBE_FEEDS_ID "/1")
+#define YOUTUBE_TOP_FAVS_NAME        "Top Favorites"
+
+#define YOUTUBE_MOST_VIEWED_ID       (YOUTUBE_FEEDS_ID "/2")
+#define YOUTUBE_MOST_VIEWED_NAME     "Most Viewed"
+
+#define YOUTUBE_MOST_POPULAR_ID      (YOUTUBE_FEEDS_ID "/3")
+#define YOUTUBE_MOST_POPULAR_NAME    "Most Popular"
+
+#define YOUTUBE_MOST_RECENT_ID       (YOUTUBE_FEEDS_ID "/4")
+#define YOUTUBE_MOST_RECENT_NAME     "Most Recent"
+
+#define YOUTUBE_MOST_DISCUSSED_ID    (YOUTUBE_FEEDS_ID "/5")
+#define YOUTUBE_MOST_DISCUSSED_NAME  "Most Discussed"
+
+#define YOUTUBE_MOST_LINKED_ID       (YOUTUBE_FEEDS_ID "/6")
+#define YOUTUBE_MOST_LINKED_NAME     "Most Linked"
+
+#define YOUTUBE_MOST_RESPONDED_ID    (YOUTUBE_FEEDS_ID "/7")
+#define YOUTUBE_MOST_RESPONDED_NAME  "Most Responded"
+
+#define YOUTUBE_FEATURED_ID          (YOUTUBE_FEEDS_ID "/8")
+#define YOUTUBE_FEATURED_NAME        "Recently Featured"
+
+#define YOUTUBE_MOBILE_ID            (YOUTUBE_FEEDS_ID "/9")
+#define YOUTUBE_MOBILE_NAME          "Watch On Mobile"
+
+/* --- Other --- */
+
+#define YOUTUBE_MAX_CHUNK       50
+
+#define YOUTUBE_VIDEO_INFO_URL  "http://www.youtube.com/get_video_info?video_id=%s";
+#define YOUTUBE_VIDEO_URL       "http://www.youtube.com/get_video?video_id=%s&t=%s&asv=";
+#define YOUTUBE_CATEGORY_URL    "http://gdata.youtube.com/feeds/api/videos/-/%s?&start-index=%s&max-results=%s";
+#define YOUTUBE_WATCH_URL       "http://www.youtube.com/watch?v=";
+
+#define YOUTUBE_VIDEO_MIME      "application/x-shockwave-flash"
+#define YOUTUBE_SITE_URL        "www.youtube.com"
+
+
+/* --- Plugin information --- */
+
+#define PLUGIN_ID   YOUTUBE_PLUGIN_ID
+
+#define SOURCE_ID   "grl-youtube"
+#define SOURCE_NAME "Youtube"
+#define SOURCE_DESC "A source for browsing and searching Youtube videos"
+
+#define AUTHOR      "Igalia S.L."
+#define LICENSE     "LGPL"
+#define SITE        "http://www.igalia.com";
+
+/* --- Data types --- */
+
+typedef void (*AsyncReadCbFunc) (gchar *data, gpointer user_data);
+
+typedef void (*BuildMediaFromEntryCbFunc) (GrlMedia *media, gpointer user_data);
+
+typedef struct {
+  gchar *id;
+  gchar *name;
+  guint count;
+} CategoryInfo;
+
+typedef struct {
+  GrlMediaSource *source;
+  guint operation_id;
+  const gchar *container_id;
+  GList *keys;
+  GrlMetadataResolutionFlags flags;
+  guint skip;
+  guint count;
+  GrlMediaSourceResultCb callback;
+  gpointer user_data;
+  guint error_code;
+  CategoryInfo *category_info;
+  guint emitted;
+  guint matches;
+  guint ref_count;
+} OperationSpec;
+
+typedef struct {
+  GDataService *service;
+  CategoryInfo *category_info;
+} CategoryCountCb;
+
+typedef struct {
+  AsyncReadCbFunc callback;
+  gchar *url;
+  gpointer user_data;
+} AsyncReadCb;
+
+typedef struct {
+  GrlMedia *media;
+  BuildMediaFromEntryCbFunc callback;
+  gpointer user_data;
+} SetMediaUrlAsyncReadCb;
+
+typedef enum {
+  YOUTUBE_MEDIA_TYPE_ROOT,
+  YOUTUBE_MEDIA_TYPE_FEEDS,
+  YOUTUBE_MEDIA_TYPE_CATEGORIES,
+  YOUTUBE_MEDIA_TYPE_FEED,
+  YOUTUBE_MEDIA_TYPE_CATEGORY,
+  YOUTUBE_MEDIA_TYPE_VIDEO,
+} YoutubeMediaType;
+
+struct _GrlYoutubeSourcePriv {
+  GDataService *service;
+
+  GrlNetWc *wc;
+};
+
+#define YOUTUBE_CLIENT_ID "grilo"
+
+static GrlYoutubeSource *grl_youtube_source_new (const gchar *api_key,
+						 const gchar *client_id);
+
+static void grl_youtube_source_set_property (GObject *object,
+                                             guint propid,
+                                             const GValue *value,
+                                             GParamSpec *pspec);
+static void grl_youtube_source_finalize (GObject *object);
+
+gboolean grl_youtube_plugin_init (GrlPluginRegistry *registry,
+                                  const GrlPluginInfo *plugin,
+                                  GList *configs);
+
+static const GList *grl_youtube_source_supported_keys (GrlMetadataSource *source);
+
+static const GList *grl_youtube_source_slow_keys (GrlMetadataSource *source);
+
+static void grl_youtube_source_search (GrlMediaSource *source,
+                                       GrlMediaSourceSearchSpec *ss);
+
+static void grl_youtube_source_browse (GrlMediaSource *source,
+                                       GrlMediaSourceBrowseSpec *bs);
+
+static void grl_youtube_source_metadata (GrlMediaSource *source,
+                                         GrlMediaSourceMetadataSpec *ms);
+
+static gboolean grl_youtube_test_media_from_uri (GrlMediaSource *source,
+						 const gchar *uri);
+
+static void grl_youtube_get_media_from_uri (GrlMediaSource *source,
+					    GrlMediaSourceMediaFromUriSpec *mfus);
+
+static void build_directories (GDataService *service);
+static void compute_feed_counts (GDataService *service);
+static void compute_category_counts (GDataService *service);
+
+/* ==================== Global Data  ================= */
+
+guint root_dir_size = 2;
+CategoryInfo root_dir[] = {
+  {YOUTUBE_FEEDS_ID,      YOUTUBE_FEEDS_NAME,      10},
+  {YOUTUBE_CATEGORIES_ID, YOUTUBE_CATEGORIES_NAME,  0},
+  {NULL, NULL, 0}
+};
+
+CategoryInfo feeds_dir[] = {
+  {YOUTUBE_TOP_RATED_ID,      YOUTUBE_TOP_RATED_NAME,       0},
+  {YOUTUBE_TOP_FAVS_ID,       YOUTUBE_TOP_FAVS_NAME,        0},
+  {YOUTUBE_MOST_VIEWED_ID,    YOUTUBE_MOST_VIEWED_NAME,     0},
+  {YOUTUBE_MOST_POPULAR_ID,   YOUTUBE_MOST_POPULAR_NAME,    0},
+  {YOUTUBE_MOST_RECENT_ID,    YOUTUBE_MOST_RECENT_NAME,     0},
+  {YOUTUBE_MOST_DISCUSSED_ID, YOUTUBE_MOST_DISCUSSED_NAME,  0},
+  {YOUTUBE_MOST_LINKED_ID,    YOUTUBE_MOST_LINKED_NAME,     0},
+  {YOUTUBE_MOST_RESPONDED_ID, YOUTUBE_MOST_RESPONDED_NAME,  0},
+  {YOUTUBE_FEATURED_ID,       YOUTUBE_FEATURED_NAME,        0},
+  {YOUTUBE_MOBILE_ID,         YOUTUBE_MOBILE_NAME,          0},
+  {NULL, NULL, 0}
+};
+
+CategoryInfo *categories_dir = NULL;
+
+static GrlYoutubeSource *ytsrc = NULL;
+
+/* =================== Youtube Plugin  =============== */
+
+gboolean
+grl_youtube_plugin_init (GrlPluginRegistry *registry,
+                         const GrlPluginInfo *plugin,
+                         GList *configs)
+{
+  gchar *api_key;
+  GrlConfig *config;
+  gint config_count;
+
+  GRL_LOG_DOMAIN_INIT (youtube_log_domain, "youtube");
+
+  GRL_DEBUG ("youtube_plugin_init");
+
+  if (!configs) {
+    GRL_WARNING ("Configuration not provided! Cannot configure plugin.");
+    return FALSE;
+  }
+
+  config_count = g_list_length (configs);
+  if (config_count > 1) {
+    GRL_WARNING ("Provided %d configs, but will only use one", config_count);
+  }
+
+  config = GRL_CONFIG (configs->data);
+  api_key = grl_config_get_api_key (config);
+  if (!api_key) {
+    GRL_WARNING ("Missing API Key, cannot configure Youtube plugin");
+    return FALSE;
+  }
+
+  /* libgdata needs this */
+  if (!g_thread_supported()) {
+    g_thread_init (NULL);
+  }
+
+  GrlYoutubeSource *source =
+    grl_youtube_source_new (api_key, YOUTUBE_CLIENT_ID);
+
+  grl_plugin_registry_register_source (registry,
+                                       plugin,
+                                       GRL_MEDIA_PLUGIN (source),
+                                       NULL);
+
+  g_free (api_key);
+
+  return TRUE;
+}
+
+GRL_PLUGIN_REGISTER (grl_youtube_plugin_init,
+                     NULL,
+                     PLUGIN_ID);
+
+/* ================== Youtube GObject ================ */
+
+G_DEFINE_TYPE (GrlYoutubeSource, grl_youtube_source, GRL_TYPE_MEDIA_SOURCE);
+
+static GrlYoutubeSource *
+grl_youtube_source_new (const gchar *api_key, const gchar *client_id)
+{
+  GRL_DEBUG ("grl_youtube_source_new");
+
+  GrlYoutubeSource *source;
+  GDataYouTubeService *service;
+
+  service = gdata_youtube_service_new (api_key, client_id);
+  if (!service) {
+    GRL_WARNING ("Failed to initialize gdata service");
+    return NULL;
+  }
+
+  /* Use auto-split mode because Youtube fails for queries
+     that request more than YOUTUBE_MAX_CHUNK results */
+  source = GRL_YOUTUBE_SOURCE (g_object_new (GRL_YOUTUBE_SOURCE_TYPE,
+					     "source-id", SOURCE_ID,
+					     "source-name", SOURCE_NAME,
+					     "source-desc", SOURCE_DESC,
+					     "auto-split-threshold",
+					     YOUTUBE_MAX_CHUNK,
+                                             "yt-service", service,
+					     NULL));
+
+  ytsrc = source;
+
+  /* Build browse content hierarchy:
+      - Query Youtube for available categories
+      - Compute category childcounts
+      We only need to do this once */
+  if (!categories_dir) {
+    build_directories (GDATA_SERVICE (service));
+  }
+
+  return source;
+}
+
+static void
+grl_youtube_source_class_init (GrlYoutubeSourceClass * klass)
+{
+  GrlMediaSourceClass *source_class = GRL_MEDIA_SOURCE_CLASS (klass);
+  GrlMetadataSourceClass *metadata_class = GRL_METADATA_SOURCE_CLASS (klass);
+  GObjectClass *gobject_class = G_OBJECT_CLASS (klass);
+  source_class->search = grl_youtube_source_search;
+  source_class->browse = grl_youtube_source_browse;
+  source_class->metadata = grl_youtube_source_metadata;
+  source_class->test_media_from_uri = grl_youtube_test_media_from_uri;
+  source_class->media_from_uri = grl_youtube_get_media_from_uri;
+  metadata_class->supported_keys = grl_youtube_source_supported_keys;
+  metadata_class->slow_keys = grl_youtube_source_slow_keys;
+  gobject_class->set_property = grl_youtube_source_set_property;
+  gobject_class->finalize = grl_youtube_source_finalize;
+
+  g_object_class_install_property (gobject_class,
+                                   PROP_SERVICE,
+                                   g_param_spec_object ("yt-service",
+                                                        "youtube-service",
+                                                        "gdata youtube service object",
+                                                        GDATA_TYPE_YOUTUBE_SERVICE,
+                                                        G_PARAM_WRITABLE
+                                                        | G_PARAM_CONSTRUCT_ONLY
+                                                        | G_PARAM_STATIC_NAME));
+
+  g_type_class_add_private (klass, sizeof (GrlYoutubeSourcePriv));
+}
+
+static void
+grl_youtube_source_init (GrlYoutubeSource *source)
+{
+  source->priv = GRL_YOUTUBE_SOURCE_GET_PRIVATE (source);
+}
+
+static void
+grl_youtube_source_set_property (GObject *object,
+                                 guint propid,
+                                 const GValue *value,
+                                 GParamSpec *pspec)
+
+{
+  switch (propid) {
+  case PROP_SERVICE: {
+    GrlYoutubeSource *self;
+    self = GRL_YOUTUBE_SOURCE (object);
+    self->priv->service = g_value_get_object (value);
+    break;
+  }
+  default:
+    G_OBJECT_WARN_INVALID_PROPERTY_ID (object, propid, pspec);
+  }
+}
+
+static void
+grl_youtube_source_finalize (GObject *object)
+{
+  GrlYoutubeSource *self;
+
+  self = GRL_YOUTUBE_SOURCE (object);
+
+  if (self->priv->wc)
+    g_object_unref (self->priv->wc);
+
+  if (self->priv->service)
+    g_object_unref (self->priv->service);
+
+  G_OBJECT_CLASS (grl_youtube_source_parent_class)->finalize (object);
+}
+
+/* ======================= Utilities ==================== */
+
+static OperationSpec *
+operation_spec_new ()
+{
+  GRL_DEBUG ("Allocating new spec");
+  OperationSpec *os =  g_slice_new0 (OperationSpec);
+  os->ref_count = 1;
+  return os;
+}
+
+static void
+operation_spec_unref (OperationSpec *os)
+{
+  os->ref_count--;
+  if (os->ref_count == 0) {
+    g_slice_free (OperationSpec, os);
+    GRL_DEBUG ("freeing spec");
+  }
+}
+
+static void
+operation_spec_ref (OperationSpec *os)
+{
+  GRL_DEBUG ("Reffing spec");
+  os->ref_count++;
+}
+
+inline static GrlNetWc *
+get_wc ()
+{
+  if (ytsrc && !ytsrc->priv->wc)
+    ytsrc->priv->wc = grl_net_wc_new ();
+
+  return ytsrc->priv->wc;
+}
+
+static void
+read_done_cb (GObject *source_object,
+              GAsyncResult *res,
+              gpointer user_data)
+{
+  AsyncReadCb *arc = (AsyncReadCb *) user_data;
+  GError *wc_error = NULL;
+  gchar *content = NULL;
+
+  grl_net_wc_request_finish (GRL_NET_WC (source_object),
+                         res,
+                         &content,
+                         NULL,
+                         &wc_error);
+  if (wc_error) {
+    GRL_WARNING ("Failed to open '%s': %s", arc->url, wc_error->message);
+    arc->callback (NULL, arc->user_data);
+    g_error_free (wc_error);
+  } else {
+    arc->callback (content, arc->user_data);
+  }
+  g_free (arc->url);
+  g_slice_free (AsyncReadCb, arc);
+}
+
+static void
+read_url_async (const gchar *url,
+                AsyncReadCbFunc callback,
+                gpointer user_data)
+{
+  AsyncReadCb *arc;
+
+  arc = g_slice_new0 (AsyncReadCb);
+  arc->url = g_strdup (url);
+  arc->callback = callback;
+  arc->user_data = user_data;
+
+  GRL_DEBUG ("Opening async '%s'", url);
+  grl_net_wc_request_async (get_wc (),
+                        url,
+                        NULL,
+                        read_done_cb,
+                        arc);
+}
+
+static void
+set_media_url_async_read_cb (gchar *data, gpointer user_data)
+{
+  SetMediaUrlAsyncReadCb *cb_data = (SetMediaUrlAsyncReadCb *) user_data;
+  gchar *url = NULL;
+  GMatchInfo *match_info = NULL;
+  static GRegex *regex = NULL;
+
+  if (!data) {
+    goto done;
+  }
+
+  if (regex == NULL) {
+    regex = g_regex_new (".*&fmt_url_map=([^&]+)&", G_REGEX_OPTIMIZE, 0, NULL);
+  }
+
+  /* Check if we find the url mapping */
+  g_regex_match (regex, data, 0, &match_info);
+  if (g_match_info_matches (match_info) == TRUE) {
+    gchar *url_map_escaped, *url_map;
+    gchar **mappings;
+
+    url_map_escaped = g_match_info_fetch (match_info, 1);
+    url_map = g_uri_unescape_string (url_map_escaped, NULL);
+    g_free (url_map_escaped);
+
+    mappings = g_strsplit (url_map, ",", 0);
+    g_free (url_map);
+
+    if (mappings != NULL) {
+      /* TODO: We get the URL from the first format available.
+       * We should provide the list of available urls or let the user
+       * configure preferred formats
+       */
+      gchar **mapping = g_strsplit (mappings[0], "|", 2);
+      url = g_strdup (mapping[1]);
+      g_strfreev (mapping);
+    }
+  } else {
+    GRL_DEBUG ("Format array not found, using token workaround");
+    gchar *token_start;
+    gchar *token_end;
+    gchar *token;
+    const gchar *video_id;
+
+    token_start = g_strrstr (data, "&token=");
+    if (!token_start) {
+      goto done;
+    }
+    token_start += 7;
+    token_end = strstr (token_start, "&");
+    token = g_strndup (token_start, token_end - token_start);
+
+    video_id = grl_media_get_id (cb_data->media);
+    url = g_strdup_printf (YOUTUBE_VIDEO_URL, video_id, token);
+    g_free (token);
+  }
+
+ done:
+  if (url) {
+    grl_media_set_url (cb_data->media, url);
+    g_free (url);
+  }
+
+  cb_data->callback (cb_data->media, cb_data->user_data);
+
+  g_free (cb_data);
+}
+
+static void
+set_media_url (GrlMedia *media,
+	       BuildMediaFromEntryCbFunc callback,
+	       gpointer user_data)
+{
+  const gchar *video_id;
+  gchar *video_info_url;
+  SetMediaUrlAsyncReadCb *set_media_url_async_read_data;
+
+  /* The procedure to get the video url is:
+   * 1) Read the video info URL using the video id (async operation)
+   * 2) In the video info page, there should be an array of supported formats
+   *    and their corresponding URLs, right now we just use the first one we get.
+   *    (see set_media_url_async_read_cb).
+   *    TODO: we should be able to provide various urls or at least
+   *          select preferred formats via configuration
+   *    TODO: we should set mime-type accordingly to the format selected
+   * 3) As a workaround in case no format array is found we get the video token
+   *    and figure out the url of the video using the video id and the token.
+   */
+
+  video_id = grl_media_get_id (media);
+  video_info_url = g_strdup_printf (YOUTUBE_VIDEO_INFO_URL, video_id);
+
+  set_media_url_async_read_data = g_new0 (SetMediaUrlAsyncReadCb, 1);
+  set_media_url_async_read_data->media = media;
+  set_media_url_async_read_data->callback = callback;
+  set_media_url_async_read_data->user_data = user_data;
+
+  read_url_async (video_info_url,
+		  set_media_url_async_read_cb,
+		  set_media_url_async_read_data);
+
+  g_free (video_info_url);
+}
+
+static void
+build_media_from_entry (GrlMedia *content,
+			GDataEntry *entry,
+			const GList *keys,
+			BuildMediaFromEntryCbFunc callback,
+			gpointer user_data)
+{
+  GDataYouTubeVideo *video;
+  GrlMedia *media;
+  GList *iter;
+  gboolean need_url = FALSE;
+
+  if (!content) {
+    media = grl_media_video_new ();
+  } else {
+    media = content;
+  }
+
+  video = GDATA_YOUTUBE_VIDEO (entry);
+
+  /* Make sure we set the media id in any case */
+  if (!grl_media_get_id (media)) {
+    grl_media_set_id (media, gdata_youtube_video_get_video_id (video));
+  }
+
+  iter = (GList *) keys;
+  while (iter) {
+    if (iter->data == GRL_METADATA_KEY_TITLE) {
+      grl_media_set_title (media, gdata_entry_get_title (entry));
+    } else if (iter->data == GRL_METADATA_KEY_DESCRIPTION) {
+      grl_media_set_description (media,
+				 gdata_youtube_video_get_description (video));
+    } else if (iter->data == GRL_METADATA_KEY_THUMBNAIL) {
+      GList *thumb_list;
+      thumb_list = gdata_youtube_video_get_thumbnails (video);
+      if (thumb_list) {
+        GDataMediaThumbnail *thumbnail;
+        thumbnail = GDATA_MEDIA_THUMBNAIL (thumb_list->data);
+        grl_media_set_thumbnail (media,
+                                 gdata_media_thumbnail_get_uri (thumbnail));
+      }
+    } else if (iter->data == GRL_METADATA_KEY_DATE) {
+      GTimeVal date;
+      gchar *date_str;
+#ifdef GDATA_API_SUBJECT_TO_CHANGE
+      gint64 published = gdata_entry_get_published (entry);
+      date.tv_sec = (glong) published;
+#else
+      gdata_entry_get_published (entry, &date);
+#endif
+      if (date.tv_sec != 0 || date.tv_usec != 0) {
+        date_str = g_time_val_to_iso8601 (&date);
+        grl_media_set_date (media, date_str);
+        g_free (date_str);
+      }
+    } else if (iter->data == GRL_METADATA_KEY_DURATION) {
+      grl_media_set_duration (media, gdata_youtube_video_get_duration (video));
+    } else if (iter->data == GRL_METADATA_KEY_MIME) {
+      grl_media_set_mime (media, YOUTUBE_VIDEO_MIME);
+    } else if (iter->data == GRL_METADATA_KEY_SITE) {
+      grl_media_set_site (media, gdata_youtube_video_get_player_uri (video));
+    } else if (iter->data == GRL_METADATA_KEY_EXTERNAL_URL) {
+      grl_media_set_external_url (media,
+				  gdata_youtube_video_get_player_uri (video));
+    } else if (iter->data == GRL_METADATA_KEY_RATING) {
+      gdouble average;
+      gdata_youtube_video_get_rating (video, NULL, NULL, NULL, &average);
+      grl_media_set_rating (media, average, 5.00);
+    } else if (iter->data == GRL_METADATA_KEY_URL) {
+      /* This needs another query and will be resolved asynchronously p Q*/
+      need_url = TRUE;
+    } else if (iter->data == GRL_METADATA_KEY_EXTERNAL_PLAYER) {
+      GDataYouTubeContent *youtube_content;
+      youtube_content =
+	gdata_youtube_video_look_up_content (video,
+					     "application/x-shockwave-flash");
+      if (youtube_content != NULL) {
+	GDataMediaContent *content = GDATA_MEDIA_CONTENT (youtube_content);
+	grl_media_set_external_player (media,
+				       gdata_media_content_get_uri (content));
+      }
+    }
+    iter = g_list_next (iter);
+  }
+
+  if (need_url) {
+    /* URL resolution is async */
+    set_media_url (media, callback, user_data);
+  } else {
+    callback (media, user_data);
+  }
+}
+
+static void
+parse_categories (xmlDocPtr doc, xmlNodePtr node, GDataService *service)
+{
+  GRL_DEBUG ("parse_categories");
+
+  guint total = 0;
+  GList *all = NULL, *iter;
+  CategoryInfo *cat_info;
+  gchar *id;
+  guint index = 0;
+
+  while (node) {
+    cat_info = g_slice_new (CategoryInfo);
+    id = (gchar *) xmlGetProp (node, (xmlChar *) "term");
+    cat_info->id = g_strconcat (YOUTUBE_CATEGORIES_ID, "/", id, NULL);
+    cat_info->name = (gchar *) xmlGetProp (node, (xmlChar *) "label");
+    all = g_list_prepend (all, cat_info);
+    g_free (id);
+    node = node->next;
+    total++;
+    GRL_DEBUG ("  Found category: '%d - %s'", index++, cat_info->name);
+  }
+
+  if (all) {
+    root_dir[ROOT_DIR_CATEGORIES_INDEX].count = total;
+    categories_dir = g_new0 (CategoryInfo, total + 1);
+    iter = all;
+    do {
+      cat_info = (CategoryInfo *) iter->data;
+      categories_dir[total - 1].id = cat_info->id ;
+      categories_dir[total - 1].name = cat_info->name;
+      categories_dir[total - 1].count = 0;
+      total--;
+      g_slice_free (CategoryInfo, cat_info);
+      iter = g_list_next (iter);
+    } while (iter);
+    g_list_free (all);
+
+    compute_category_counts (service);
+  }
+}
+
+static void
+build_categories_directory_read_cb (gchar *xmldata, gpointer user_data)
+{
+  xmlDocPtr doc;
+  xmlNodePtr node;
+
+  if (!xmldata) {
+    g_critical ("Failed to build category directory (1)");
+    return;
+  }
+
+  doc = xmlReadMemory (xmldata, strlen (xmldata), NULL, NULL,
+                       XML_PARSE_RECOVER | XML_PARSE_NOBLANKS);
+  if (!doc) {
+    g_critical ("Failed to build category directory (2)");
+    goto free_resources;
+  }
+
+  node = xmlDocGetRootElement (doc);
+  if (!node) {
+    g_critical ("Failed to build category directory (3)");
+    goto free_resources;
+  }
+
+  if (xmlStrcmp (node->name, (const xmlChar *) "categories")) {
+    g_critical ("Failed to build category directory (4)");
+    goto free_resources;
+  }
+
+  node = node->xmlChildrenNode;
+  if (!node) {
+    g_critical ("Failed to build category directory (5)");
+    goto free_resources;
+  }
+
+  parse_categories (doc, node, GDATA_SERVICE (user_data));
+
+ free_resources:
+  xmlFreeDoc (doc);
+}
+
+static gint
+get_feed_type_from_id (const gchar *feed_id)
+{
+  gchar *tmp;
+  gchar *test;
+  gint feed_type;
+
+  tmp = g_strrstr (feed_id, "/");
+  if (!tmp) {
+    return -1;
+  }
+  tmp++;
+
+  feed_type = strtol (tmp, &test, 10);
+  if (*test != '\0') {
+    return -1;
+  }
+
+  return feed_type;
+}
+
+static const gchar *
+get_category_term_from_id (const gchar *category_id)
+{
+  gchar *term;
+  term = g_strrstr (category_id, "/");
+  if (!term) {
+    return NULL;
+  }
+  return ++term;
+}
+
+static gint
+get_category_index_from_id (const gchar *category_id)
+{
+  gint i;
+
+  for (i=0; i<root_dir[ROOT_DIR_CATEGORIES_INDEX].count; i++) {
+    if (!strcmp (categories_dir[i].id, category_id)) {
+      return i;
+    }
+  }
+  return -1;
+}
+
+static void
+item_count_cb (GObject *object, GAsyncResult *result, CategoryCountCb *cc)
+{
+  GRL_DEBUG ("item_count_cb");
+
+  GDataFeed *feed;
+  GError *error = NULL;
+
+  feed = gdata_service_query_finish (GDATA_SERVICE (cc->service),
+				     result, &error);
+  if (error) {
+    GRL_WARNING ("Failed to compute count for category '%s': %s",
+                 cc->category_info->id, error->message);
+    g_error_free (error);
+  } else if (feed) {
+    cc->category_info->count = gdata_feed_get_total_results (feed);
+    GRL_DEBUG ("Category '%s' - childcount: '%u'",
+               cc->category_info->id, cc->category_info->count);
+  }
+
+  if (feed) {
+    g_object_unref (feed);
+  }
+  g_slice_free (CategoryCountCb, cc);
+}
+
+static void
+compute_category_counts (GDataService *service)
+{
+  gint i;
+
+  GRL_DEBUG ("compute_category_counts");
+
+  for (i=0; i<root_dir[ROOT_DIR_CATEGORIES_INDEX].count; i++) {
+    GRL_DEBUG ("Computing chilcount for category '%s'", categories_dir[i].id);
+    GDataQuery *query = gdata_query_new_with_limits (NULL, 0, 1);
+    const gchar *category_term =
+      get_category_term_from_id (categories_dir[i].id);
+    gdata_query_set_categories (query, category_term);
+    CategoryCountCb *cc = g_slice_new (CategoryCountCb);
+    cc->service = service;
+    cc->category_info = &categories_dir[i];
+    gdata_youtube_service_query_videos_async (GDATA_YOUTUBE_SERVICE (service),
+					      query,
+					      NULL, NULL, NULL,
+					      (GAsyncReadyCallback) item_count_cb,
+					      cc);
+    g_object_unref (query);
+  }
+}
+
+static void
+compute_feed_counts (GDataService *service)
+{
+  gint i;
+  GRL_DEBUG ("compute_feed_counts");
+
+  for (i=0; i<root_dir[ROOT_DIR_FEEDS_INDEX].count; i++) {
+    GRL_DEBUG ("Computing chilcount for feed '%s'", feeds_dir[i].id);
+    gint feed_type = get_feed_type_from_id (feeds_dir[i].id);
+    GDataQuery *query = gdata_query_new_with_limits (NULL, 0, 1);
+    CategoryCountCb *cc = g_slice_new (CategoryCountCb);
+    cc->service = service;
+    cc->category_info = &feeds_dir[i];
+    gdata_youtube_service_query_standard_feed_async (GDATA_YOUTUBE_SERVICE (service),
+						     feed_type,
+						     query,
+						     NULL, NULL, NULL,
+						     (GAsyncReadyCallback) item_count_cb,
+						     cc);
+    g_object_unref (query);
+  }
+}
+
+static void
+build_media_from_entry_metadata_cb (GrlMedia *media, gpointer user_data)
+{
+  GrlMediaSourceMetadataSpec *ms = (GrlMediaSourceMetadataSpec *) user_data;
+  ms->callback (ms->source, media, ms->user_data, NULL);
+}
+
+static void
+build_media_from_entry_search_cb (GrlMedia *media, gpointer user_data)
+{
+  /*
+   * TODO: Async resolution of URL messes (or could mess) with the sorting,
+   * If we want to ensure a particular sorting or implement sorting
+   * mechanisms we should add code to handle that here so we emit items in
+   * the right order and not just when we got the URL resolved (would
+   * damage response time though).
+   */
+  OperationSpec *os = (OperationSpec *) user_data;
+  guint remaining;
+
+  if (os->emitted < os->count) {
+    remaining = os->count - os->emitted - 1;
+    os->callback (os->source,
+		  os->operation_id,
+		  media,
+		  remaining,
+		  os->user_data,
+		  NULL);
+    if (remaining == 0) {
+      GRL_DEBUG ("Unreffing spec in build_media_from_entry_search_cb");
+      operation_spec_unref (os);
+    } else {
+      os->emitted++;
+    }
+  }
+}
+
+static void
+build_directories (GDataService *service)
+{
+  GRL_DEBUG ("build_drectories");
+
+  /* Parse category list from Youtube and compute category counts */
+  read_url_async (YOUTUBE_CATEGORIES_URL,
+                  build_categories_directory_read_cb,
+                  service);
+
+  /* Compute feed counts */
+  compute_feed_counts (service);
+}
+
+static void
+metadata_cb (GObject *object,
+	     GAsyncResult *result,
+	     gpointer user_data)
+{
+  GRL_DEBUG ("metadata_cb");
+
+  GError *error = NULL;
+  GrlYoutubeSource *source;
+  GDataEntry *video;
+  GDataService *service;
+  GrlMediaSourceMetadataSpec *ms = (GrlMediaSourceMetadataSpec *) user_data;
+
+  source = GRL_YOUTUBE_SOURCE (ms->source);
+  service = GDATA_SERVICE (source->priv->service);
+
+#ifdef GDATA_API_SUBJECT_TO_CHANGE
+  video = gdata_service_query_single_entry_finish (service, result, &error);
+#else
+  video =
+    GDATA_ENTRY (gdata_youtube_service_query_single_video_finish
+                   (GDATA_YOUTUBE_SERVICE (service), result, &error));
+#endif
+  if (error) {
+    error->code = GRL_CORE_ERROR_METADATA_FAILED;
+    ms->callback (ms->source, ms->media, ms->user_data, error);
+    g_error_free (error);
+  } else {
+    build_media_from_entry (ms->media, video, ms->keys,
+			    build_media_from_entry_metadata_cb, ms);
+  }
+
+  if (video) {
+    g_object_unref (video);
+  }
+}
+
+static void
+search_progress_cb (GDataEntry *entry,
+		    guint index,
+		    guint count,
+		    gpointer user_data)
+{
+  OperationSpec *os = (OperationSpec *) user_data;
+  if (index < count) {
+    /* Keep track of the items we got here. Due to the asynchronous
+     * nature of build_media_from_entry(), when search_cb is invoked
+     * we have to check if we got as many results as we requested or
+     * not, and handle that situation properly */
+    os->matches++;
+    build_media_from_entry (NULL, entry, os->keys,
+			    build_media_from_entry_search_cb, os);
+  } else {
+    GRL_WARNING ("Invalid index/count received grom libgdata, ignoring result");
+  }
+
+  /* The entry will be freed when freeing the feed in search_cb */
+}
+
+static void
+search_cb (GObject *object, GAsyncResult *result, OperationSpec *os)
+{
+  GRL_DEBUG ("search_cb");
+
+  GDataFeed *feed;
+  GError *error = NULL;
+  gboolean need_extra_unref = FALSE;
+  GrlYoutubeSource *source = GRL_YOUTUBE_SOURCE (os->source);
+
+  feed = gdata_service_query_finish (source->priv->service, result, &error);
+  if (!error && feed) {
+    /* If we are browsing a category, update the count for it */
+    if (os->category_info) {
+      os->category_info->count = gdata_feed_get_total_results (feed);
+    }
+
+    /* Check if we got as many results as we requested */
+    if (os->matches < os->count) {
+      os->count = os->matches;
+      /* In case we are resolving URLs asynchronously, from now on
+       * results will be sent with appropriate remaining, but it can
+       * also be the case that we have sent all the results already
+       * and the last one was sent with remaining>0, in that case
+       * we should send a finishing message now. */
+      if (os->emitted == os->count) {
+	GRL_DEBUG ("sending finishing message");
+	os->callback (os->source, os->operation_id,
+		      NULL, 0, os->user_data, NULL);
+	need_extra_unref = TRUE;
+      }
+    }
+  } else {
+    if (!error) {
+      error = g_error_new (GRL_CORE_ERROR,
+			   os->error_code,
+			   "Failed to obtain feed from Youtube");
+    } else {
+      error->code = os->error_code;
+    }
+    os->callback (os->source, os->operation_id, NULL, 0, os->user_data, error);
+    g_error_free (error);
+    need_extra_unref = TRUE;
+  }
+
+  if (feed)
+    g_object_unref (feed);
+
+  GRL_DEBUG ("Unreffing spec in search_cb");
+  operation_spec_unref (os);
+  if (need_extra_unref) {
+    /* We did not free the spec in the emission callback, do it here */
+    GRL_DEBUG ("need extra spec unref in search_cb");
+    operation_spec_unref (os);
+  }
+}
+
+static gboolean
+is_category_container (const gchar *container_id)
+{
+  return g_str_has_prefix (container_id, YOUTUBE_CATEGORIES_ID "/");
+}
+
+static gboolean
+is_feeds_container (const gchar *container_id)
+{
+  return g_str_has_prefix (container_id, YOUTUBE_FEEDS_ID "/");
+}
+
+static YoutubeMediaType
+classify_media_id (const gchar *media_id)
+{
+  if (!media_id) {
+    return YOUTUBE_MEDIA_TYPE_ROOT;
+  } else if (!strcmp (media_id, YOUTUBE_FEEDS_ID)) {
+    return YOUTUBE_MEDIA_TYPE_FEEDS;
+  } else if (!strcmp (media_id, YOUTUBE_CATEGORIES_ID)) {
+    return YOUTUBE_MEDIA_TYPE_CATEGORIES;
+  } else if (is_category_container (media_id)) {
+    return YOUTUBE_MEDIA_TYPE_CATEGORY;
+  } else if (is_feeds_container (media_id)) {
+    return YOUTUBE_MEDIA_TYPE_FEED;
+  } else {
+    return YOUTUBE_MEDIA_TYPE_VIDEO;
+  }
+}
+
+static void
+set_category_childcount (GDataService *service,
+			 GrlMediaBox *content,
+                         CategoryInfo *dir,
+                         guint index)
+{
+  gint childcount;
+  gboolean set_childcount = TRUE;
+  const gchar *container_id;
+
+  container_id = grl_media_get_id (GRL_MEDIA (content));
+
+  if (dir == NULL) {
+    /* Special case: we want childcount of root category */
+    childcount = root_dir_size;
+  } else if (!strcmp (dir[index].id, YOUTUBE_FEEDS_ID)) {
+    childcount = root_dir[ROOT_DIR_FEEDS_INDEX].count;
+  } else if (!strcmp (dir[index].id, YOUTUBE_CATEGORIES_ID)) {
+    childcount = root_dir[ROOT_DIR_CATEGORIES_INDEX].count;
+  } else if (is_feeds_container (container_id)) {
+    gint feed_index = get_feed_type_from_id (container_id);
+    if (feed_index >= 0) {
+      childcount = feeds_dir[feed_index].count;
+    } else {
+      set_childcount = FALSE;
+    }
+  } else if (is_category_container (container_id)) {
+    gint cat_index = get_category_index_from_id (container_id);
+    if (cat_index >= 0) {
+      childcount = categories_dir[cat_index].count;
+    } else {
+      set_childcount = FALSE;
+    }
+  } else {
+    set_childcount = FALSE;
+  }
+
+  if (set_childcount) {
+    grl_media_box_set_childcount (content, childcount);
+  }
+}
+
+static GrlMedia *
+produce_container_from_directory (GDataService *service,
+				  GrlMedia *media,
+				  CategoryInfo *dir,
+				  guint index)
+{
+  GrlMedia *content;
+
+  if (!media) {
+    /* Create mode */
+    content = grl_media_box_new ();
+  } else {
+    /* Update mode */
+    content = media;
+  }
+
+  if (!dir) {
+    grl_media_set_id (content, NULL);
+    grl_media_set_title (content, YOUTUBE_ROOT_NAME);
+  } else {
+    grl_media_set_id (content, dir[index].id);
+    grl_media_set_title (content, dir[index].name);
+  }
+  grl_media_set_site (content, YOUTUBE_SITE_URL);
+  set_category_childcount (service, GRL_MEDIA_BOX (content), dir, index);
+
+  return content;
+}
+
+static void
+produce_from_directory (CategoryInfo *dir, guint dir_size, OperationSpec *os)
+{
+  GRL_DEBUG ("produce_from_directory");
+
+  guint index, remaining;
+
+  /* Youtube's first index is 1, but the directories start at 0 */
+  os->skip--;
+
+  if (os->skip >= dir_size) {
+    /* No results */
+    os->callback (os->source,
+		  os->operation_id,
+		  NULL,
+		  0,
+		  os->user_data,
+		  NULL);
+    operation_spec_unref (os);
+  } else {
+    index = os->skip;
+    remaining = MIN (dir_size - os->skip, os->count);
+
+    do {
+      GDataService *service = GRL_YOUTUBE_SOURCE (os->source)->priv->service;
+
+      GrlMedia *content =
+	produce_container_from_directory (service, NULL, dir, index);
+
+      remaining--;
+      index++;
+
+      os->callback (os->source,
+		    os->operation_id,
+		    content,
+		    remaining,
+		    os->user_data,
+		    NULL);
+
+      if (remaining == 0) {
+	operation_spec_unref (os);
+      }
+    } while (remaining > 0);
+  }
+}
+
+static void
+produce_from_feed (OperationSpec *os)
+{
+  GError *error = NULL;
+  gint feed_type;
+  GDataQuery *query;
+  GDataService *service;
+
+  feed_type = get_feed_type_from_id (os->container_id);
+
+  if (feed_type < 0) {
+    error = g_error_new (GRL_CORE_ERROR,
+			 GRL_CORE_ERROR_BROWSE_FAILED,
+			 "Invalid feed id: %s", os->container_id);
+    os->callback (os->source,
+		  os->operation_id,
+		  NULL,
+		  0,
+		  os->user_data,
+		  error);
+    g_error_free (error);
+    operation_spec_unref (os);
+    return;
+  }
+  /* OPERATION_SPEC_REF_RATIONALE
+   * Depending on wether the URL has been requested, metadata resolution
+   * for each item in the result set may or may not be asynchronous.
+   * We cannot free the spec in search_cb because that may be called
+   * before the asynchronous URL resolution is finished, and we cannot
+   * do it in build_media_from_entry_search_cb either, because in the
+   * synchronous case (when we do not request URL) search_cb will
+   * be invoked after it.
+   * Thus, the solution is to increase the reference count here and
+   * have both places unreffing the spec, that way, no matter which
+   * is invoked last, the spec will be freed only once. */
+  operation_spec_ref (os);
+
+  service = GRL_YOUTUBE_SOURCE (os->source)->priv->service;
+  query = gdata_query_new_with_limits (NULL , os->skip, os->count);
+  os->category_info = &feeds_dir[feed_type];
+  gdata_youtube_service_query_standard_feed_async (GDATA_YOUTUBE_SERVICE (service),
+                                                   feed_type,
+                                                   query,
+                                                   NULL,
+                                                   search_progress_cb,
+                                                   os,
+                                                   (GAsyncReadyCallback) search_cb,
+                                                   os);
+  g_object_unref (query);
+}
+
+static void
+produce_from_category (OperationSpec *os)
+{
+  GError *error = NULL;
+  GDataQuery *query;
+  GDataService *service;
+  const gchar *category_term;
+  gint category_index;
+
+  category_term = get_category_term_from_id (os->container_id);
+  category_index = get_category_index_from_id (os->container_id);
+
+  if (!category_term) {
+    error = g_error_new (GRL_CORE_ERROR,
+			 GRL_CORE_ERROR_BROWSE_FAILED,
+			 "Invalid category id: %s", os->container_id);
+    os->callback (os->source,
+		  os->operation_id,
+		  NULL,
+		  0,
+		  os->user_data,
+		  error);
+    g_error_free (error);
+    operation_spec_unref (os);
+    return;
+  }
+
+  /* Look for OPERATION_SPEC_REF_RATIONALE for details */
+  operation_spec_ref (os);
+
+  service = GRL_YOUTUBE_SOURCE (os->source)->priv->service;
+  query = gdata_query_new_with_limits (NULL , os->skip, os->count);
+  os->category_info = &categories_dir[category_index];
+  gdata_query_set_categories (query, category_term);
+  gdata_youtube_service_query_videos_async (GDATA_YOUTUBE_SERVICE (service),
+					    query,
+					    NULL,
+					    search_progress_cb,
+					    os,
+					    (GAsyncReadyCallback) search_cb,
+					    os);
+  g_object_unref (query);
+}
+
+static gchar *
+get_video_id_from_url (const gchar *url)
+{
+  gchar *marker, *end, *video_id;
+
+  if (url == NULL) {
+    return NULL;
+  }
+
+  marker = strstr (url, YOUTUBE_WATCH_URL);
+  if (!marker) {
+    return NULL;
+  }
+
+  marker += strlen (YOUTUBE_WATCH_URL);
+
+  end = marker;
+  while (*end != '\0' && *end != '&') {
+    end++;
+  }
+
+  video_id = g_strndup (marker, end - marker);
+
+  return video_id;
+}
+
+static void
+build_media_from_entry_media_from_uri_cb (GrlMedia *media, gpointer user_data)
+{
+  GrlMediaSourceMediaFromUriSpec *mfus =
+    (GrlMediaSourceMediaFromUriSpec *) user_data;
+  mfus->callback (mfus->source, media, mfus->user_data, NULL);
+}
+
+static void
+media_from_uri_cb (GObject *object, GAsyncResult *result, gpointer user_data)
+{
+  GError *error = NULL;
+  GrlYoutubeSource *source;
+  GDataEntry *video;
+  GDataService *service;
+  GrlMediaSourceMediaFromUriSpec *mfus =
+    (GrlMediaSourceMediaFromUriSpec *) user_data;
+
+  source = GRL_YOUTUBE_SOURCE (mfus->source);
+  service = GDATA_SERVICE (source->priv->service);
+
+#ifdef GDATA_API_SUBJECT_TO_CHANGE
+  video = gdata_service_query_single_entry_finish (service, result, &error);
+#else
+  video =
+    GDATA_ENTRY (gdata_youtube_service_query_single_video_finish
+		 (GDATA_YOUTUBE_SERVICE (service), result, &error));
+#endif
+
+  if (error) {
+    error->code = GRL_CORE_ERROR_MEDIA_FROM_URI_FAILED;
+    mfus->callback (mfus->source, NULL, mfus->user_data, error);
+    g_error_free (error);
+  } else {
+    build_media_from_entry (NULL, video, mfus->keys,
+			    build_media_from_entry_media_from_uri_cb,
+			    mfus);
+  }
+
+  if (video) {
+    g_object_unref (video);
+  }
+}
+
+/* ================== API Implementation ================ */
+
+static const GList *
+grl_youtube_source_supported_keys (GrlMetadataSource *source)
+{
+  static GList *keys = NULL;
+  if (!keys) {
+    keys = grl_metadata_key_list_new (GRL_METADATA_KEY_ID,
+                                      GRL_METADATA_KEY_TITLE,
+                                      GRL_METADATA_KEY_URL,
+				      GRL_METADATA_KEY_EXTERNAL_URL,
+                                      GRL_METADATA_KEY_DESCRIPTION,
+                                      GRL_METADATA_KEY_DURATION,
+                                      GRL_METADATA_KEY_DATE,
+                                      GRL_METADATA_KEY_THUMBNAIL,
+                                      GRL_METADATA_KEY_MIME,
+                                      GRL_METADATA_KEY_CHILDCOUNT,
+                                      GRL_METADATA_KEY_SITE,
+                                      GRL_METADATA_KEY_RATING,
+				      GRL_METADATA_KEY_EXTERNAL_PLAYER,
+                                      NULL);
+  }
+  return keys;
+}
+
+static const GList *
+grl_youtube_source_slow_keys (GrlMetadataSource *source)
+{
+  static GList *keys = NULL;
+  if (!keys) {
+    keys = grl_metadata_key_list_new (GRL_METADATA_KEY_URL,
+                                      NULL);
+  }
+  return keys;
+}
+
+static void
+grl_youtube_source_search (GrlMediaSource *source,
+                           GrlMediaSourceSearchSpec *ss)
+{
+  OperationSpec *os;
+  GDataQuery *query;
+
+  GRL_DEBUG ("grl_youtube_source_search (%u, %u)", ss->skip, ss->count);
+
+  os = operation_spec_new ();
+  os->source = source;
+  os->operation_id = ss->search_id;
+  os->keys = ss->keys;
+  os->skip = ss->skip + 1;
+  os->count = ss->count;
+  os->callback = ss->callback;
+  os->user_data = ss->user_data;
+  os->error_code = GRL_CORE_ERROR_SEARCH_FAILED;
+
+  /* Look for OPERATION_SPEC_REF_RATIONALE for details */
+  operation_spec_ref (os);
+
+  query = gdata_query_new_with_limits (ss->text, os->skip, os->count);
+  gdata_youtube_service_query_videos_async (GDATA_YOUTUBE_SERVICE (GRL_YOUTUBE_SOURCE (source)->priv->service),
+					    query,
+					    NULL,
+					    search_progress_cb,
+					    os,
+					    (GAsyncReadyCallback) search_cb,
+					    os);
+  g_object_unref (query);
+}
+
+static void
+grl_youtube_source_browse (GrlMediaSource *source,
+                           GrlMediaSourceBrowseSpec *bs)
+{
+  OperationSpec *os;
+  const gchar *container_id;
+
+  GRL_DEBUG ("grl_youtube_source_browse: %s", grl_media_get_id (bs->container));
+
+  container_id = grl_media_get_id (bs->container);
+
+  os = operation_spec_new ();
+  os->source = bs->source;
+  os->operation_id = bs->browse_id;
+  os->container_id = container_id;
+  os->keys = bs->keys;
+  os->flags = bs->flags;
+  os->skip = bs->skip + 1;
+  os->count = bs->count;
+  os->callback = bs->callback;
+  os->user_data = bs->user_data;
+  os->error_code = GRL_CORE_ERROR_BROWSE_FAILED;
+
+  switch (classify_media_id (container_id))
+    {
+    case YOUTUBE_MEDIA_TYPE_ROOT:
+      produce_from_directory (root_dir, root_dir_size, os);
+      break;
+    case YOUTUBE_MEDIA_TYPE_FEEDS:
+      produce_from_directory (feeds_dir,
+			      root_dir[ROOT_DIR_FEEDS_INDEX].count, os);
+      break;
+    case YOUTUBE_MEDIA_TYPE_CATEGORIES:
+      produce_from_directory (categories_dir,
+			      root_dir[ROOT_DIR_CATEGORIES_INDEX].count, os);
+      break;
+    case YOUTUBE_MEDIA_TYPE_FEED:
+      produce_from_feed (os);
+      break;
+    case YOUTUBE_MEDIA_TYPE_CATEGORY:
+      produce_from_category (os);
+      break;
+    default:
+      g_assert_not_reached ();
+      break;
+    }
+}
+
+static void
+grl_youtube_source_metadata (GrlMediaSource *source,
+                             GrlMediaSourceMetadataSpec *ms)
+{
+  YoutubeMediaType media_type;
+  const gchar *id;
+  GDataService *service;
+  GError *error = NULL;
+  GrlMedia *media = NULL;
+
+  GRL_DEBUG ("grl_youtube_source_metadata");
+
+  id = grl_media_get_id (ms->media);
+  media_type = classify_media_id (id);
+  service = GRL_YOUTUBE_SOURCE (source)->priv->service;
+
+  switch (media_type) {
+  case YOUTUBE_MEDIA_TYPE_ROOT:
+    media = produce_container_from_directory (service, ms->media, NULL, 0);
+    break;
+  case YOUTUBE_MEDIA_TYPE_FEEDS:
+    media = produce_container_from_directory (service, ms->media, root_dir, 0);
+    break;
+  case YOUTUBE_MEDIA_TYPE_CATEGORIES:
+    media = produce_container_from_directory (service, ms->media, root_dir, 1);
+    break;
+  case YOUTUBE_MEDIA_TYPE_FEED:
+    {
+      gint index = get_feed_type_from_id (id);
+      if (index >= 0) {
+	media = produce_container_from_directory (service, ms->media, feeds_dir,
+						  index);
+      } else {
+	error = g_error_new (GRL_CORE_ERROR,
+			     GRL_CORE_ERROR_METADATA_FAILED,
+			     "Invalid feed id");
+      }
+    }
+    break;
+  case YOUTUBE_MEDIA_TYPE_CATEGORY:
+    {
+      gint index = get_category_index_from_id (id);
+      if (index >= 0) {
+	media = produce_container_from_directory (service, ms->media,
+						  categories_dir, index);
+      } else {
+	error = g_error_new (GRL_CORE_ERROR,
+			     GRL_CORE_ERROR_METADATA_FAILED,
+			     "Invalid category id");
+      }
+    }
+    break;
+  case YOUTUBE_MEDIA_TYPE_VIDEO:
+  default:
+#ifdef GDATA_API_SUBJECT_TO_CHANGE
+    {
+      gchar *entryid = g_strconcat ("tag:youtube.com,2008:video:", id, NULL);
+      gdata_service_query_single_entry_async (service,
+                                              entryid,
+                                              NULL,
+                                              GDATA_TYPE_YOUTUBE_VIDEO,
+                                              NULL,
+                                              metadata_cb,
+                                              ms);
+      g_free (entryid);
+    }
+#else
+    gdata_youtube_service_query_single_video_async (GDATA_YOUTUBE_SERVICE (service),
+                                                    NULL,
+                                                    id,
+                                                    NULL,
+                                                    metadata_cb,
+                                                    ms);
+#endif
+    break;
+  }
+
+  if (error) {
+    ms->callback (ms->source, ms->media, ms->user_data, error);
+    g_error_free (error);
+  } else if (media) {
+    ms->callback (ms->source, ms->media, ms->user_data, NULL);
+  }
+}
+
+static gboolean
+grl_youtube_test_media_from_uri (GrlMediaSource *source, const gchar *uri)
+{
+  GRL_DEBUG ("grl_youtube_test_media_from_uri");
+
+  gchar *video_id;
+  gboolean ok;
+
+  video_id = get_video_id_from_url (uri);
+  ok = (video_id != NULL);
+  g_free (video_id);
+  return ok;
+}
+
+static void
+grl_youtube_get_media_from_uri (GrlMediaSource *source,
+				 GrlMediaSourceMediaFromUriSpec *mfus)
+{
+  GRL_DEBUG ("grl_youtube_get_media_from_uri");
+
+  gchar *video_id;
+  GError *error;
+  GDataService *service;
+
+  video_id = get_video_id_from_url (mfus->uri);
+  if (video_id == NULL) {
+    error = g_error_new (GRL_CORE_ERROR,
+			 GRL_CORE_ERROR_MEDIA_FROM_URI_FAILED,
+			 "Cannot create media from '%s'", mfus->uri);
+    mfus->callback (source, NULL, mfus->user_data, error);
+    g_error_free (error);
+    return;
+  }
+
+  service = GRL_YOUTUBE_SOURCE (source)->priv->service;
+
+#ifdef GDATA_API_SUBJECT_TO_CHANGE
+  gchar *entry_id = g_strconcat ("tag:youtube.com,2008:video:", video_id, NULL);
+  gdata_service_query_single_entry_async (service,
+					  entry_id,
+					  NULL,
+					  GDATA_TYPE_YOUTUBE_VIDEO,
+					  NULL,
+					  media_from_uri_cb,
+					  mfus);
+  g_free (entry_id);
+#else
+  gdata_youtube_service_query_single_video_async (GDATA_YOUTUBE_SERVICE (service),
+						  NULL,
+						  video_id,
+						  NULL,
+						  media_from_uri_cb,
+						  mfus);
+#endif
+}
diff --git a/src/media/youtube/grl-youtube.h b/src/media/youtube/grl-youtube.h
new file mode 100644
index 0000000..dc88589
--- /dev/null
+++ b/src/media/youtube/grl-youtube.h
@@ -0,0 +1,74 @@
+/*
+ * Copyright (C) 2010 Igalia S.L.
+ *
+ * Contact: Iago Toral Quiroga <itoral igalia 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_YOUTUBE_SOURCE_H_
+#define _GRL_YOUTUBE_SOURCE_H_
+
+#include <grilo.h>
+
+#define GRL_YOUTUBE_SOURCE_TYPE                 \
+  (grl_youtube_source_get_type ())
+
+#define GRL_YOUTUBE_SOURCE(obj)                         \
+  (G_TYPE_CHECK_INSTANCE_CAST ((obj),                   \
+                               GRL_YOUTUBE_SOURCE_TYPE, \
+                               GrlYoutubeSource))
+
+#define GRL_IS_YOUTUBE_SOURCE(obj)                              \
+  (G_TYPE_CHECK_INSTANCE_TYPE ((obj),                           \
+                               GRL_YOUTUBE_SOURCE_TYPE))
+
+#define GRL_YOUTUBE_SOURCE_CLASS(klass)                 \
+  (G_TYPE_CHECK_CLASS_CAST((klass),                     \
+                           GRL_YOUTUBE_SOURCE_TYPE,     \
+                           GrlYoutubeSourceClass))
+
+#define GRL_IS_YOUTUBE_SOURCE_CLASS(klass)              \
+  (G_TYPE_CHECK_CLASS_TYPE((klass),                     \
+                           GRL_YOUTUBE_SOURCE_TYPE))
+
+#define GRL_YOUTUBE_SOURCE_GET_CLASS(obj)               \
+  (G_TYPE_INSTANCE_GET_CLASS ((obj),                    \
+                              GRL_YOUTUBE_SOURCE_TYPE,  \
+                              GrlYoutubeSourceClass))
+
+typedef struct _GrlYoutubeSource GrlYoutubeSource;
+typedef struct _GrlYoutubeSourcePriv GrlYoutubeSourcePriv;
+
+struct _GrlYoutubeSource {
+
+  GrlMediaSource parent;
+  GrlYoutubeSourcePriv *priv;
+
+};
+
+typedef struct _GrlYoutubeSourceClass GrlYoutubeSourceClass;
+
+struct _GrlYoutubeSourceClass {
+
+  GrlMediaSourceClass parent_class;
+
+};
+
+GType grl_youtube_source_get_type (void);
+
+#endif /* _GRL_YOUTUBE_SOURCE_H_ */
diff --git a/src/media/youtube/grl-youtube.xml b/src/media/youtube/grl-youtube.xml
new file mode 100644
index 0000000..b874678
--- /dev/null
+++ b/src/media/youtube/grl-youtube.xml
@@ -0,0 +1,9 @@
+<plugin>
+  <info>
+    <name>YouTube</name>
+    <description>A plugin for browsing and searching Youtube videos</description>
+    <author>Igalia S.L.</author>
+    <license>LGPL</license>
+    <site>http://www.igalia.com</site>
+  </info>
+</plugin>
diff --git a/src/metadata-store/Makefile.am b/src/metadata-store/Makefile.am
deleted file mode 100644
index 372f4a8..0000000
--- a/src/metadata-store/Makefile.am
+++ /dev/null
@@ -1,34 +0,0 @@
-#
-# Makefile.am
-#
-# Author: Iago Toral Quiroga <itoral igalia com>
-#
-# Copyright (C) 2010, 2011 Igalia S.L. All rights reserved.
-
-lib_LTLIBRARIES		 = libgrlmetadatastore.la
-
-libgrlmetadatastore_la_CFLAGS =	\
-	$(DEPS_CFLAGS) \
-	$(SQLITE_CFLAGS)
-
-libgrlmetadatastore_la_LIBADD =	\
-	$(DEPS_LIBS) \
-	$(SQLITE_LIBS)
-
-libgrlmetadatastore_la_LDFLAGS = \
-	-module			 \
-	-avoid-version
-
-libgrlmetadatastore_la_SOURCES = grl-metadata-store.c grl-metadata-store.h
-
-libdir=$(GRL_PLUGINS_DIR)
-metadatastorexmldir	= $(GRL_PLUGINS_CONF_DIR)
-metadatastorexml_DATA	= $(METADATA_STORE_PLUGIN_ID).xml
-
-EXTRA_DIST = $(metadatastorexml_DATA)
-
-MAINTAINERCLEANFILES	 = \
-	*.in \
-	*~
-
-DISTCLEANFILES = $(MAINTAINERCLEANFILES)
diff --git a/src/metadata-store/grl-metadata-store.c b/src/metadata-store/grl-metadata-store.c
deleted file mode 100644
index f596585..0000000
--- a/src/metadata-store/grl-metadata-store.c
+++ /dev/null
@@ -1,650 +0,0 @@
-/*
- * Copyright (C) 2010 Igalia S.L.
- *
- * Contact: Iago Toral Quiroga <itoral igalia 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 <grilo.h>
-#include <sqlite3.h>
-#include <string.h>
-
-#include "grl-metadata-store.h"
-
-#define GRL_METADATA_STORE_GET_PRIVATE(object)			 \
-  (G_TYPE_INSTANCE_GET_PRIVATE((object),			 \
-                               GRL_METADATA_STORE_SOURCE_TYPE,	 \
-                               GrlMetadataStorePrivate))
-
-#define GRL_LOG_DOMAIN_DEFAULT metadata_store_log_domain
-GRL_LOG_DOMAIN_STATIC(metadata_store_log_domain);
-
-#define PLUGIN_ID   METADATA_STORE_PLUGIN_ID
-
-#define SOURCE_ID   "grl-metadata-store"
-#define SOURCE_NAME "Metadata Store"
-#define SOURCE_DESC "A plugin for storing extra metadata information"
-
-#define AUTHOR      "Igalia S.L."
-#define LICENSE     "LGPL"
-#define SITE        "http://www.igalia.com";
-
-#define GRL_SQL_DB        ".grl-metadata-store"
-
-#define GRL_SQL_CREATE_TABLE_STORE			 \
-  "CREATE TABLE IF NOT EXISTS store ("			 \
-  "source_id TEXT,"					 \
-  "media_id TEXT,"					 \
-  "play_count INTEGER,"					 \
-  "rating REAL,"					 \
-  "last_position INTEGER,"				 \
-  "last_played DATE)"
-
-#define GRL_SQL_GET_METADATA				\
-  "SELECT * FROM store "				\
-  "WHERE source_id='%s' AND media_id='%s' "		\
-  "LIMIT 1"
-
-#define GRL_SQL_UPDATE_METADATA			\
-  "UPDATE store SET %s "			\
-  "WHERE source_id=? AND media_id=?"
-
-#define GRL_SQL_INSERT_METADATA			\
-  "INSERT INTO store "				\
-  "(%s source_id, media_id) VALUES "		\
-  "(%s ?, ?)"
-
-struct _GrlMetadataStorePrivate {
-  sqlite3 *db;
-};
-
-enum {
-  STORE_SOURCE_ID = 0,
-  STORE_MEDIA_ID,
-  STORE_PLAY_COUNT,
-  STORE_RATING,
-  STORE_LAST_POSITION,
-  STORE_LAST_PLAYED,
-};
-
-static GrlMetadataStoreSource *grl_metadata_store_source_new (void);
-
-static void grl_metadata_store_source_resolve (GrlMetadataSource *source,
-					       GrlMetadataSourceResolveSpec *rs);
-
-static void grl_metadata_store_source_set_metadata (GrlMetadataSource *source,
-						    GrlMetadataSourceSetMetadataSpec *sms);
-
-static const GList *grl_metadata_store_source_supported_keys (GrlMetadataSource *source);
-
-static const GList *grl_metadata_store_source_key_depends (GrlMetadataSource *source,
-							   GrlKeyID key_id);
-static const GList *grl_metadata_store_source_writable_keys (GrlMetadataSource *source);
-
-gboolean grl_metadata_store_source_plugin_init (GrlPluginRegistry *registry,
-						const GrlPluginInfo *plugin,
-						GList *configs);
-
-
-/* =================== GrlMetadataStore Plugin  =============== */
-
-gboolean
-grl_metadata_store_source_plugin_init (GrlPluginRegistry *registry,
-                                      const GrlPluginInfo *plugin,
-                                      GList *configs)
-{
-  GRL_LOG_DOMAIN_INIT (metadata_store_log_domain, "metadata-store");
-
-  GRL_DEBUG ("grl_metadata_store_source_plugin_init");
-
-  GrlMetadataStoreSource *source = grl_metadata_store_source_new ();
-  grl_plugin_registry_register_source (registry,
-                                       plugin,
-                                       GRL_MEDIA_PLUGIN (source),
-                                       NULL);
-  return TRUE;
-}
-
-GRL_PLUGIN_REGISTER (grl_metadata_store_source_plugin_init,
-                     NULL,
-                     PLUGIN_ID);
-
-/* ================== GrlMetadataStore GObject ================ */
-
-static GrlMetadataStoreSource *
-grl_metadata_store_source_new (void)
-{
-  GRL_DEBUG ("grl_metadata_store_source_new");
-  return g_object_new (GRL_METADATA_STORE_SOURCE_TYPE,
-		       "source-id", SOURCE_ID,
-		       "source-name", SOURCE_NAME,
-		       "source-desc", SOURCE_DESC,
-		       NULL);
-}
-
-static void
-grl_metadata_store_source_class_init (GrlMetadataStoreSourceClass * klass)
-{
-  GrlMetadataSourceClass *metadata_class = GRL_METADATA_SOURCE_CLASS (klass);
-  metadata_class->supported_keys = grl_metadata_store_source_supported_keys;
-  metadata_class->key_depends = grl_metadata_store_source_key_depends;
-  metadata_class->writable_keys = grl_metadata_store_source_writable_keys;
-  metadata_class->resolve = grl_metadata_store_source_resolve;
-  metadata_class->set_metadata = grl_metadata_store_source_set_metadata;
-
-  g_type_class_add_private (klass, sizeof (GrlMetadataStorePrivate));
-}
-
-static void
-grl_metadata_store_source_init (GrlMetadataStoreSource *source)
-{
-  gint r;
-  const gchar *home;
-  gchar *db_path;
-  gchar *sql_error = NULL;
-
-  source->priv = GRL_METADATA_STORE_GET_PRIVATE (source);
-
-  home = g_getenv ("HOME");
-  if (!home) {
-    GRL_WARNING ("$HOME not set, cannot open database");
-    return;
-  }
-
-  GRL_DEBUG ("Opening database connection...");
-  db_path = g_strconcat (home, G_DIR_SEPARATOR_S, GRL_SQL_DB, NULL);
-  r = sqlite3_open (db_path, &source->priv->db);
-  if (r) {
-    g_critical ("Failed to open database '%s': %s",
-		db_path, sqlite3_errmsg (source->priv->db));
-    sqlite3_close (source->priv->db);
-    return;
-  }
-  GRL_DEBUG ("  OK");
-
-  GRL_DEBUG ("Checking database tables...");
-  r = sqlite3_exec (source->priv->db, GRL_SQL_CREATE_TABLE_STORE,
-		    NULL, NULL, &sql_error);
-
-  if (r) {
-    if (sql_error) {
-      GRL_WARNING ("Failed to create database tables: %s", sql_error);
-      sqlite3_free (sql_error);
-      sql_error = NULL;
-    } else {
-      GRL_WARNING ("Failed to create database tables.");
-    }
-    sqlite3_close (source->priv->db);
-    return;
-  }
-  GRL_DEBUG ("  OK");
-
-  g_free (db_path);
-}
-
-G_DEFINE_TYPE (GrlMetadataStoreSource, grl_metadata_store_source,
-               GRL_TYPE_METADATA_SOURCE);
-
-/* ======================= Utilities ==================== */
-
-static sqlite3_stmt *
-query_metadata_store (sqlite3 *db,
-		      const gchar *source_id,
-		      const gchar *media_id)
-{
-  gint r;
-  sqlite3_stmt *sql_stmt = NULL;
-  gchar *sql;
-
-  GRL_DEBUG ("get_metadata");
-
-  sql = g_strdup_printf (GRL_SQL_GET_METADATA, source_id, media_id);
-  GRL_DEBUG ("%s", sql);
-  r = sqlite3_prepare_v2 (db, sql, strlen (sql), &sql_stmt, NULL);
-  g_free (sql);
-
-  if (r != SQLITE_OK) {
-    GRL_WARNING ("Failed to get metadata: %s", sqlite3_errmsg (db));
-    return NULL;
-  }
-
-  return sql_stmt;
-}
-
-static void
-fill_metadata (GrlMedia *media, GList *keys, sqlite3_stmt *stmt)
-{
-  GList *iter;
-  gint play_count, last_position;
-  gdouble rating;
-  gchar *last_played;
-  gint r;
-
-  while ((r = sqlite3_step (stmt)) == SQLITE_BUSY);
-
-  if (r != SQLITE_ROW) {
-    /* No info in DB for this item, bail out silently */
-    sqlite3_finalize (stmt);
-    return;
-  }
-
-  iter = keys;
-  while (iter) {
-    if (iter->data == GRL_METADATA_KEY_PLAY_COUNT) {
-      play_count = sqlite3_column_int (stmt, STORE_PLAY_COUNT);
-      grl_media_set_play_count (media, play_count);
-    } else if (iter->data == GRL_METADATA_KEY_RATING) {
-      rating = sqlite3_column_double (stmt, STORE_RATING);
-      grl_media_set_rating (media, rating, 5.00);
-    } else if (iter->data == GRL_METADATA_KEY_LAST_PLAYED) {
-      last_played = (gchar *) sqlite3_column_text (stmt, STORE_LAST_PLAYED);
-      grl_media_set_last_played (media, last_played);
-    } else if (iter->data == GRL_METADATA_KEY_LAST_POSITION) {
-      last_position = sqlite3_column_int (stmt, STORE_LAST_POSITION);
-      grl_media_set_last_position (media, last_position);
-    }
-    iter = g_list_next (iter);
-  }
-
-  sqlite3_finalize (stmt);
-}
-
-static const gchar *
-get_column_name_from_key_id (GrlKeyID key_id)
-{
-  static const gchar *col_names[] = {"rating", "last_played", "last_position",
-				     "play_count"};
-  if (key_id == GRL_METADATA_KEY_RATING) {
-    return col_names[0];
-  } else if (key_id == GRL_METADATA_KEY_LAST_PLAYED) {
-    return col_names[1];
-  } else if (key_id == GRL_METADATA_KEY_LAST_POSITION) {
-    return col_names[2];
-  } else if (key_id == GRL_METADATA_KEY_PLAY_COUNT) {
-    return col_names[3];
-  } else {
-    return NULL;
-  }
-}
-
-static gboolean
-bind_and_exec (sqlite3 *db,
-	       const gchar *sql,
-	       const gchar *source_id,
-	       const gchar *media_id,
-	       GList *col_names,
-	       GList *keys,
-	       GrlMedia *media)
-{
-  gint r;
-  const gchar *char_value;
-  gint int_value;
-  double double_value;
-  GList *iter_names, *iter_keys;
-  guint count;
-  sqlite3_stmt *stmt;
-
-  /* Create statement from sql */
-  GRL_DEBUG ("%s", sql);
-  r = sqlite3_prepare_v2 (db, sql, strlen (sql), &stmt, NULL);
-
-  if (r != SQLITE_OK) {
-    GRL_WARNING ("Failed to update metadata for '%s - %s': %s",
-                     source_id, media_id, sqlite3_errmsg (db));
-    sqlite3_finalize (stmt);
-    return FALSE;
-  }
-
-  /* Bind column values */
-  count = 1;
-  iter_names = col_names;
-  iter_keys = keys;
-  while (iter_names) {
-    if (iter_names->data) {
-      if (iter_keys->data == GRL_METADATA_KEY_RATING) {
-	double_value = grl_media_get_rating (media);
-	sqlite3_bind_double (stmt, count, double_value);
-      } else if (iter_keys->data == GRL_METADATA_KEY_PLAY_COUNT) {
-	int_value = grl_media_get_play_count (media);
-	sqlite3_bind_int (stmt, count, int_value);
-      } else if (iter_keys->data == GRL_METADATA_KEY_LAST_POSITION) {
-	int_value = grl_media_get_last_position (media);
-	sqlite3_bind_int (stmt, count, int_value);
-      } else if (iter_keys->data == GRL_METADATA_KEY_LAST_PLAYED) {
-	char_value = grl_media_get_last_played (media);
-	sqlite3_bind_text (stmt, count, char_value, -1, SQLITE_STATIC);
-      }
-      count++;
-    }
-    iter_keys = g_list_next (iter_keys);
-    iter_names = g_list_next (iter_names);
-  }
-
-  sqlite3_bind_text (stmt, count++, source_id, -1, SQLITE_STATIC);
-  sqlite3_bind_text (stmt, count++, media_id, -1, SQLITE_STATIC);
-
-  /* execute query */
-  while ((r = sqlite3_step (stmt)) == SQLITE_BUSY);
-
-  sqlite3_finalize (stmt);
-
-  return (r == SQLITE_DONE);
-}
-
-static gboolean
-prepare_and_exec_update (sqlite3 *db,
-			 const gchar *source_id,
-			 const gchar *media_id,
-			 GList *col_names,
-			 GList *keys,
-			 GrlMedia *media)
-{
-  gchar *sql;
-  gint r;
-  GList *iter_names;
-  GString *sql_buf;
-  gchar *sql_set;
-  guint count;
-
-  GRL_DEBUG ("prepare_and_exec_update");
-
-  /* Prepare sql "set" for update query */
-  count = 0;
-  sql_buf = g_string_new ("");
-  iter_names = col_names;
-  while (iter_names) {
-    gchar *col_name = (gchar *) iter_names->data;
-    if (col_name) {
-      if (count > 0) {
-	g_string_append (sql_buf, " AND ");
-      }
-      g_string_append_printf (sql_buf, "%s=?", col_name);
-      count++;
-    }
-    iter_names = g_list_next (iter_names);
-  }
-  sql_set = g_string_free (sql_buf, FALSE);
-
-  /* Execute query */
-  sql = g_strdup_printf (GRL_SQL_UPDATE_METADATA, sql_set);
-  r = bind_and_exec (db, sql, source_id, media_id, col_names, keys, media);
-  g_free (sql);
-  g_free (sql_set);
-
-  return r;
-}
-
-static gboolean
-prepare_and_exec_insert (sqlite3 *db,
-			 const gchar *source_id,
-			 const gchar *media_id,
-			 GList *col_names,
-			 GList *keys,
-			 GrlMedia *media)
-{
-  gchar *sql;
-  gint r;
-  GList *iter_names;
-  GString *sql_buf_cols, *sql_buf_values;
-  gchar *sql_cols, *sql_values;
-
-  GRL_DEBUG ("prepare_and_exec_insert");
-
-  /* Prepare sql for insert query */
-  sql_buf_cols = g_string_new ("");
-  sql_buf_values = g_string_new ("");
-  iter_names = col_names;
-  while (iter_names) {
-    gchar *col_name = (gchar *) iter_names->data;
-    if (col_name) {
-      g_string_append_printf (sql_buf_cols, "%s, ", col_name);
-      g_string_append (sql_buf_values, "?, ");
-    }
-    iter_names = g_list_next (iter_names);
-  }
-  sql_cols = g_string_free (sql_buf_cols, FALSE);
-  sql_values = g_string_free (sql_buf_values, FALSE);
-
-  /* Execute query */
-  sql = g_strdup_printf (GRL_SQL_INSERT_METADATA, sql_cols, sql_values);
-  r = bind_and_exec (db, sql, source_id, media_id, col_names, keys, media);
-  g_free (sql);
-  g_free (sql_cols);
-  g_free (sql_values);
-
-  return r;
-}
-
-static GList *
-write_keys (sqlite3 *db,
-	    const gchar *source_id,
-	    const gchar *media_id,
-	    GrlMetadataSourceSetMetadataSpec *sms,
-	    GError **error)
-{
-  GList *col_names = NULL;
-  GList *iter;
-  GList *failed_keys = NULL;
-  guint supported_keys = 0;
-  gint r;
-
-  /* Get DB column names for each key to be updated */
-  iter = sms->keys;
-  while (iter) {
-    const gchar *col_name = get_column_name_from_key_id (iter->data);
-    if (!col_name) {
-      GRL_WARNING ("Key %" GRL_KEYID_FORMAT " is not supported for "
-                       "writing, ignoring...",
-                 iter->data);
-      failed_keys = g_list_prepend (failed_keys, iter->data);
-    } else {
-      supported_keys++;
-    }
-    col_names = g_list_prepend (col_names, (gchar *) col_name);
-    iter = g_list_next (iter);
-  }
-  col_names = g_list_reverse (col_names);
-
-  if (supported_keys == 0) {
-    GRL_WARNING ("Failed to update metadata, none of the specified "
-                     "keys is writable");
-    *error = g_error_new (GRL_CORE_ERROR,
-			  GRL_CORE_ERROR_SET_METADATA_FAILED,
-			  "Failed to update metadata, "
-			  "specified keys are not writable");
-    goto done;
-  }
-
-  r = prepare_and_exec_update (db,
-			       source_id,
-			       media_id,
-			       col_names,
-			       sms->keys,
-			       sms->media);
-    
-  if (!r) {
-    GRL_WARNING ("Failed to update metadata for '%s - %s': %s",
-                     source_id, media_id, sqlite3_errmsg (db));
-    g_list_free (failed_keys);
-    failed_keys = g_list_copy (sms->keys);
-    *error = g_error_new (GRL_CORE_ERROR,
-			  GRL_CORE_ERROR_SET_METADATA_FAILED,
-			  "Failed to update metadata");
-    goto done;
-  } 
-  
-  if (sqlite3_changes (db) == 0) {
-    /* We have to create the row */
-    r = prepare_and_exec_insert (db,
-				 source_id,
-				 media_id,
-				 col_names,
-				 sms->keys,
-				 sms->media);
-  }
-
-  if (!r) {
-    GRL_WARNING ("Failed to update metadata for '%s - %s': %s",
-                     source_id, media_id, sqlite3_errmsg (db));
-    g_list_free (failed_keys);
-    failed_keys = g_list_copy (sms->keys);
-    *error = g_error_new (GRL_CORE_ERROR,
-			  GRL_CORE_ERROR_SET_METADATA_FAILED,
-			  "Failed to update metadata");
-    goto done;
-  } 
-
- done:
-  g_list_free (col_names);
-  return failed_keys;
-}
-
-/* ================== API Implementation ================ */
-
-static const GList *
-grl_metadata_store_source_supported_keys (GrlMetadataSource *source)
-{
-  static GList *keys = NULL;
-  if (!keys) {
-    keys = grl_metadata_key_list_new (GRL_METADATA_KEY_RATING,
-                                      GRL_METADATA_KEY_PLAY_COUNT,
-                                      GRL_METADATA_KEY_LAST_PLAYED,
-                                      GRL_METADATA_KEY_LAST_POSITION,
-                                      NULL);
-  }
-  return keys;
-}
-
-static const GList *
-grl_metadata_store_source_writable_keys (GrlMetadataSource *source)
-{
-  static GList *keys = NULL;
-  if (!keys) {
-    keys = grl_metadata_key_list_new (GRL_METADATA_KEY_RATING,
-                                      GRL_METADATA_KEY_PLAY_COUNT,
-                                      GRL_METADATA_KEY_LAST_PLAYED,
-                                      GRL_METADATA_KEY_LAST_POSITION,
-                                      NULL);
-  }
-  return keys;
-}
-
-static const GList *
-grl_metadata_store_source_key_depends (GrlMetadataSource *source,
-				       GrlKeyID key_id)
-{
-  static GList *deps = NULL;
-  if (!deps) {
-    deps = grl_metadata_key_list_new (GRL_METADATA_KEY_ID, NULL);
-  }
-
-  if (key_id == GRL_METADATA_KEY_RATING ||
-      key_id == GRL_METADATA_KEY_PLAY_COUNT ||
-      key_id == GRL_METADATA_KEY_LAST_PLAYED ||
-      key_id == GRL_METADATA_KEY_LAST_POSITION) {
-    return deps;
-  }
-
-  return NULL;
-}
-
-static void
-grl_metadata_store_source_resolve (GrlMetadataSource *source,
-				   GrlMetadataSourceResolveSpec *rs)
-{
-  GRL_DEBUG ("grl_metadata_store_source_resolve");
-
-  const gchar *source_id, *media_id;
-  sqlite3_stmt *stmt;
-  GError *error = NULL;
-
-  source_id = grl_media_get_source (rs->media);
-  media_id = grl_media_get_id (rs->media);
-
-  /* We need the source id */
-  if (!source_id) {
-    GRL_WARNING ("Failed to resolve metadata: source-id not available");
-    error = g_error_new (GRL_CORE_ERROR,
-			 GRL_CORE_ERROR_RESOLVE_FAILED,
-			 "source-id not available, cannot resolve metadata.");
-    rs->callback (rs->source, rs->media, rs->user_data, error);
-    g_error_free (error);
-    return;
-  }
-
-  /* Special case for root categories */
-  if (!media_id) {
-    media_id = "";
-  }
-
-  stmt = query_metadata_store (GRL_METADATA_STORE_SOURCE (source)->priv->db,
-			       source_id, media_id);
-  if (stmt) {
-    fill_metadata (rs->media, rs->keys, stmt);
-    rs->callback (rs->source, rs->media, rs->user_data, NULL);
-  } else {
-    GRL_WARNING ("Failed to resolve metadata");
-    error = g_error_new (GRL_CORE_ERROR,
-			 GRL_CORE_ERROR_RESOLVE_FAILED,
-			 "Failed to resolve metadata.");
-    rs->callback (rs->source, rs->media, rs->user_data, error);
-    g_error_free (error);
-  }
-}
-
-static void
-grl_metadata_store_source_set_metadata (GrlMetadataSource *source,
-					GrlMetadataSourceSetMetadataSpec *sms)
-{
-  GRL_DEBUG ("grl_metadata_store_source_set_metadata");
-
-  const gchar *media_id, *source_id;
-  GError *error = NULL;
-  GList *failed_keys = NULL;
-
-  source_id = grl_media_get_source (sms->media);
-  media_id = grl_media_get_id (sms->media);
-
-  /* We need the source id */
-  if (!source_id) {
-    GRL_WARNING ("Failed to update metadata: source-id not available");
-    error = g_error_new (GRL_CORE_ERROR,
-			 GRL_CORE_ERROR_SET_METADATA_FAILED,
-			 "source-id not available, cannot update metadata.");
-    failed_keys = g_list_copy (sms->keys);
-  } else {
-    /* Special case for root categories */
-    if (!media_id) {
-      media_id = "";
-    }
-    
-    failed_keys = write_keys (GRL_METADATA_STORE_SOURCE (source)->priv->db,
-			      source_id, media_id, sms, &error);
-  }
-
-  sms->callback (sms->source, sms->media, failed_keys, sms->user_data, error);
-
-  if (error) {
-    g_error_free (error);
-  }
-  g_list_free (failed_keys);
-}
diff --git a/src/metadata-store/grl-metadata-store.h b/src/metadata-store/grl-metadata-store.h
deleted file mode 100644
index 47df494..0000000
--- a/src/metadata-store/grl-metadata-store.h
+++ /dev/null
@@ -1,75 +0,0 @@
-/*
- * Copyright (C) 2010 Igalia S.L.
- *
- * Contact: Iago Toral Quiroga <itoral igalia 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_METADATA_STORE_SOURCE_H_
-#define _GRL_METADATA_STORE_SOURCE_H_
-
-#include <grilo.h>
-
-#define GRL_METADATA_STORE_SOURCE_TYPE           \
-  (grl_metadata_store_source_get_type ())
-
-#define GRL_METADATA_STORE_SOURCE(obj)                           \
-  (G_TYPE_CHECK_INSTANCE_CAST ((obj),                           \
-                               GRL_METADATA_STORE_SOURCE_TYPE,   \
-                               GrlMetadataStoreSource))
-
-#define GRL_IS_METADATA_STORE_SOURCE(obj)                        \
-  (G_TYPE_CHECK_INSTANCE_TYPE ((obj),                           \
-                               GRL_METADATA_STORE_SOURCE_TYPE))
-
-#define GRL_METADATA_STORE_SOURCE_CLASS(klass)                   \
-  (G_TYPE_CHECK_CLASS_CAST((klass),				 \
-                           GRL_METADATA_STORE_SOURCE_TYPE,       \
-                           GrlMetadataStoreSourceClass))
-
-#define GRL_IS_METADATA_STORE_SOURCE_CLASS(klass)		\
-  (G_TYPE_CHECK_CLASS_TYPE((klass),                             \
-                           GRL_METADATA_STORE_SOURCE_TYPE))
-
-#define GRL_METADATA_STORE_SOURCE_GET_CLASS(obj)                 \
-  (G_TYPE_INSTANCE_GET_CLASS ((obj),				 \
-                              GRL_METADATA_STORE_SOURCE_TYPE,    \
-                              GrlMetadataStoreSourceClass))
-
-typedef struct _GrlMetadataStorePrivate GrlMetadataStorePrivate;
-typedef struct _GrlMetadataStoreSource GrlMetadataStoreSource;
-
-struct _GrlMetadataStoreSource {
-
-  GrlMetadataSource parent;
-
-  /*< private >*/
-  GrlMetadataStorePrivate *priv;
-};
-
-typedef struct _GrlMetadataStoreSourceClass GrlMetadataStoreSourceClass;
-
-struct _GrlMetadataStoreSourceClass {
-
-  GrlMetadataSourceClass parent_class;
-
-};
-
-GType grl_metadata_store_source_get_type (void);
-
-#endif /* _GRL_METADATA_STORE_SOURCE_H_ */
diff --git a/src/metadata-store/grl-metadata-store.xml b/src/metadata-store/grl-metadata-store.xml
deleted file mode 100644
index b8bbbb8..0000000
--- a/src/metadata-store/grl-metadata-store.xml
+++ /dev/null
@@ -1,9 +0,0 @@
-<plugin>
-  <info>
-    <name>Metadata Store</name>
-    <description>A plugin for storing extra metadata information</description>
-    <author>Igalia S.L.</author>
-    <license>LGPL</license>
-    <site>http://www.igalia.com</site>
-  </info>
-</plugin>
diff --git a/src/metadata/fake-metadata/Makefile.am b/src/metadata/fake-metadata/Makefile.am
new file mode 100644
index 0000000..e0a1b81
--- /dev/null
+++ b/src/metadata/fake-metadata/Makefile.am
@@ -0,0 +1,32 @@
+#
+# Makefile.am
+#
+# Author: Iago Toral Quiroga <itoral igalia com>
+#
+# Copyright (C) 2010, 2011 Igalia S.L. All rights reserved.
+
+lib_LTLIBRARIES		 = libgrlfakemetadata.la
+
+libgrlfakemetadata_la_CFLAGS =	\
+	$(DEPS_CFLAGS)
+
+libgrlfakemetadata_la_LIBADD =	\
+	$(DEPS_LIBS)
+
+libgrlfakemetadata_la_LDFLAGS = \
+	-module			\
+	-avoid-version
+
+libgrlfakemetadata_la_SOURCES = grl-fake-metadata.c grl-fake-metadata.h
+
+libdir=$(GRL_PLUGINS_DIR)
+fakemetadataxmldir	= $(GRL_PLUGINS_CONF_DIR)
+fakemetadataxml_DATA	= $(FAKEMETADATA_PLUGIN_ID).xml
+
+EXTRA_DIST = $(fakemetadataxml_DATA)
+
+MAINTAINERCLEANFILES	 = \
+	*.in \
+	*~
+
+DISTCLEANFILES = $(MAINTAINERCLEANFILES)
diff --git a/src/metadata/fake-metadata/grl-fake-metadata.c b/src/metadata/fake-metadata/grl-fake-metadata.c
new file mode 100644
index 0000000..463b284
--- /dev/null
+++ b/src/metadata/fake-metadata/grl-fake-metadata.c
@@ -0,0 +1,231 @@
+/*
+ * Copyright (C) 2010 Igalia S.L.
+ *
+ * Contact: Iago Toral Quiroga <itoral igalia 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 <grilo.h>
+
+#include "grl-fake-metadata.h"
+
+#define GRL_LOG_DOMAIN_DEFAULT fake_metadata_log_domain
+GRL_LOG_DOMAIN_STATIC(fake_metadata_log_domain);
+
+#define PLUGIN_ID   FAKEMETADATA_PLUGIN_ID
+
+#define SOURCE_ID   "grl-fake-metadata"
+#define SOURCE_NAME "Fake Metadata Provider"
+#define SOURCE_DESC "A source for faking metadata resolution"
+
+#define AUTHOR      "Igalia S.L."
+#define LICENSE     "LGPL"
+#define SITE        "http://www.igalia.com";
+
+
+static GrlFakeMetadataSource *grl_fake_metadata_source_new (void);
+
+static void grl_fake_metadata_source_resolve (GrlMetadataSource *source,
+                                              GrlMetadataSourceResolveSpec *rs);
+
+static void grl_fake_metadata_source_set_metadata (GrlMetadataSource *source,
+						   GrlMetadataSourceSetMetadataSpec *sms);
+
+static const GList *grl_fake_metadata_source_supported_keys (GrlMetadataSource *source);
+
+static const GList *grl_fake_metadata_source_key_depends (GrlMetadataSource *source,
+                                                          GrlKeyID key_id);
+
+static const GList *grl_fake_metadata_source_writable_keys (GrlMetadataSource *source);
+
+gboolean grl_fake_metadata_source_plugin_init (GrlPluginRegistry *registry,
+                                               const GrlPluginInfo *plugin,
+                                               GList *configs);
+
+
+/* =================== GrlFakeMetadata Plugin  =============== */
+
+gboolean
+grl_fake_metadata_source_plugin_init (GrlPluginRegistry *registry,
+                                      const GrlPluginInfo *plugin,
+                                      GList *configs)
+{
+  GRL_LOG_DOMAIN_INIT (fake_metadata_log_domain, "fake-metadata");
+
+  GRL_DEBUG ("grl_fake_metadata_source_plugin_init");
+
+  GrlFakeMetadataSource *source = grl_fake_metadata_source_new ();
+  grl_plugin_registry_register_source (registry,
+                                       plugin,
+                                       GRL_MEDIA_PLUGIN (source),
+                                       NULL);
+  return TRUE;
+}
+
+GRL_PLUGIN_REGISTER (grl_fake_metadata_source_plugin_init,
+                     NULL,
+                     PLUGIN_ID);
+
+/* ================== GrlFakeMetadata GObject ================ */
+
+static GrlFakeMetadataSource *
+grl_fake_metadata_source_new (void)
+{
+  GRL_DEBUG ("grl_fake_metadata_source_new");
+  return g_object_new (GRL_FAKE_METADATA_SOURCE_TYPE,
+		       "source-id", SOURCE_ID,
+		       "source-name", SOURCE_NAME,
+		       "source-desc", SOURCE_DESC,
+		       NULL);
+}
+
+static void
+grl_fake_metadata_source_class_init (GrlFakeMetadataSourceClass * klass)
+{
+  GrlMetadataSourceClass *metadata_class = GRL_METADATA_SOURCE_CLASS (klass);
+  metadata_class->supported_keys = grl_fake_metadata_source_supported_keys;
+  metadata_class->key_depends = grl_fake_metadata_source_key_depends;
+  metadata_class->resolve = grl_fake_metadata_source_resolve;
+  metadata_class->set_metadata = grl_fake_metadata_source_set_metadata;
+  metadata_class->writable_keys = grl_fake_metadata_source_writable_keys;
+}
+
+static void
+grl_fake_metadata_source_init (GrlFakeMetadataSource *source)
+{
+}
+
+G_DEFINE_TYPE (GrlFakeMetadataSource,
+               grl_fake_metadata_source,
+               GRL_TYPE_METADATA_SOURCE);
+
+/* ======================= Utilities ==================== */
+
+static void
+fill_metadata (GrlMedia *media, GrlKeyID key_id)
+{
+  if (key_id == GRL_METADATA_KEY_AUTHOR) {
+    grl_media_set_author (media, "fake author");
+  } else if (key_id == GRL_METADATA_KEY_ARTIST) {
+    grl_data_set_string (GRL_DATA (media),
+                         GRL_METADATA_KEY_ARTIST, "fake artist");
+  } else if (key_id == GRL_METADATA_KEY_ALBUM) {
+    grl_data_set_string (GRL_DATA (media),
+                         GRL_METADATA_KEY_ALBUM, "fake album");
+  } else if (key_id == GRL_METADATA_KEY_GENRE) {
+    grl_data_set_string (GRL_DATA (media),
+                         GRL_METADATA_KEY_GENRE, "fake genre");
+  } else if (key_id == GRL_METADATA_KEY_DESCRIPTION) {
+    grl_media_set_description (media, "fake description");
+  } else if (key_id == GRL_METADATA_KEY_DURATION) {
+    grl_media_set_duration (media, 99);
+  } else if (key_id == GRL_METADATA_KEY_DATE) {
+    grl_data_set_string (GRL_DATA (media),
+                         GRL_METADATA_KEY_DATE, "01/01/1970");
+  } else if (key_id == GRL_METADATA_KEY_THUMBNAIL) {
+    grl_media_set_thumbnail (media,
+                                  "http://fake.thumbnail.com/fake-image.jpg";);
+  }
+}
+
+
+/* ================== API Implementation ================ */
+
+static const GList *
+grl_fake_metadata_source_supported_keys (GrlMetadataSource *source)
+{
+  static GList *keys = NULL;
+  if (!keys) {
+    keys = grl_metadata_key_list_new (GRL_METADATA_KEY_AUTHOR,
+                                      GRL_METADATA_KEY_ARTIST,
+                                      GRL_METADATA_KEY_ALBUM,
+                                      GRL_METADATA_KEY_GENRE,
+                                      GRL_METADATA_KEY_DESCRIPTION,
+                                      GRL_METADATA_KEY_DURATION,
+                                      GRL_METADATA_KEY_DATE,
+                                      GRL_METADATA_KEY_THUMBNAIL,
+                                      NULL);
+  }
+  return keys;
+}
+
+static const GList *
+grl_fake_metadata_source_key_depends (GrlMetadataSource *source,
+                                      GrlKeyID key_id)
+{
+  static GList *deps = NULL;
+  if (!deps) {
+    deps = grl_metadata_key_list_new (GRL_METADATA_KEY_TITLE, NULL);
+  }
+
+  if (key_id == GRL_METADATA_KEY_AUTHOR ||
+      key_id == GRL_METADATA_KEY_ARTIST ||
+      key_id == GRL_METADATA_KEY_ALBUM ||
+      key_id == GRL_METADATA_KEY_GENRE ||
+      key_id == GRL_METADATA_KEY_DESCRIPTION ||
+      key_id == GRL_METADATA_KEY_DURATION ||
+      key_id == GRL_METADATA_KEY_DATE ||
+      key_id == GRL_METADATA_KEY_THUMBNAIL) {
+    return deps;
+  }
+
+  return  NULL;
+}
+
+static const GList *
+grl_fake_metadata_source_writable_keys (GrlMetadataSource *source)
+{
+  static GList *keys = NULL;
+  if (!keys) {
+    keys = grl_metadata_key_list_new (GRL_METADATA_KEY_ALBUM,
+                                      GRL_METADATA_KEY_ARTIST,
+                                      GRL_METADATA_KEY_GENRE,
+                                      NULL);
+  }
+  return keys;
+}
+
+static void
+grl_fake_metadata_source_resolve (GrlMetadataSource *source,
+                                  GrlMetadataSourceResolveSpec *rs)
+{
+  GRL_DEBUG ("grl_fake_metadata_source_resolve");
+
+  GList *iter;
+
+  iter = rs->keys;
+  while (iter) {
+    fill_metadata (GRL_MEDIA (rs->media), iter->data);
+    iter = g_list_next (iter);
+  }
+
+  rs->callback (source, rs->media, rs->user_data, NULL);
+}
+
+static void
+grl_fake_metadata_source_set_metadata (GrlMetadataSource *source,
+				       GrlMetadataSourceSetMetadataSpec *sms)
+{
+  GRL_DEBUG ("grl_fake_metadata_source_set_metadata");
+  GRL_DEBUG ("  Faking set metadata for %d keys!", g_list_length (sms->keys));
+  sms->callback (sms->source, sms->media, NULL, sms->user_data, NULL);
+}
diff --git a/src/metadata/fake-metadata/grl-fake-metadata.h b/src/metadata/fake-metadata/grl-fake-metadata.h
new file mode 100644
index 0000000..5d80453
--- /dev/null
+++ b/src/metadata/fake-metadata/grl-fake-metadata.h
@@ -0,0 +1,72 @@
+/*
+ * Copyright (C) 2010 Igalia S.L.
+ *
+ * Contact: Iago Toral Quiroga <itoral igalia 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_FAKE_METADATA_SOURCE_H_
+#define _GRL_FAKE_METADATA_SOURCE_H_
+
+#include <grilo.h>
+
+#define GRL_FAKE_METADATA_SOURCE_TYPE           \
+  (grl_fake_metadata_source_get_type ())
+
+#define GRL_FAKE_METADATA_SOURCE(obj)                           \
+  (G_TYPE_CHECK_INSTANCE_CAST ((obj),                           \
+                               GRL_FAKE_METADATA_SOURCE_TYPE,   \
+                               GrlFakeMetadataSource))
+
+#define GRL_IS_FAKE_METADATA_SOURCE(obj)                        \
+  (G_TYPE_CHECK_INSTANCE_TYPE ((obj),                           \
+                               GRL_FAKE_METADATA_SOURCE_TYPE))
+
+#define GRL_FAKE_METADATA_SOURCE_CLASS(klass)                   \
+  (G_TYPE_CHECK_CLASS_CAST((klass),                             \
+                           GRL_FAKE_METADATA_SOURCE_TYPE,       \
+                           GrlFakeMetadataSourceClass))
+
+#define GRL_IS_FAKE_METADATA_SOURCE_CLASS(klass)                \
+  (G_TYPE_CHECK_CLASS_TYPE((klass),                             \
+                           GRL_FAKE_METADATA_SOURCE_TYPE))
+
+#define GRL_FAKE_METADATA_SOURCE_GET_CLASS(obj)                 \
+  (G_TYPE_INSTANCE_GET_CLASS ((obj),                            \
+                              GRL_FAKE_METADATA_SOURCE_TYPE,    \
+                              GrlFakeMetadataSourceClass))
+
+typedef struct _GrlFakeMetadataSource GrlFakeMetadataSource;
+
+struct _GrlFakeMetadataSource {
+
+  GrlMetadataSource parent;
+
+};
+
+typedef struct _GrlFakeMetadataSourceClass GrlFakeMetadataSourceClass;
+
+struct _GrlFakeMetadataSourceClass {
+
+  GrlMetadataSourceClass parent_class;
+
+};
+
+GType grl_fake_metadata_source_get_type (void);
+
+#endif /* _GRL_FAKE_METADATA_SOURCE_H_ */
diff --git a/src/metadata/fake-metadata/grl-fake-metadata.xml b/src/metadata/fake-metadata/grl-fake-metadata.xml
new file mode 100644
index 0000000..11e4d32
--- /dev/null
+++ b/src/metadata/fake-metadata/grl-fake-metadata.xml
@@ -0,0 +1,9 @@
+<plugin>
+  <info>
+    <name>Fake Metadata Provider</name>
+    <description>A plugin for faking metadata resolution</description>
+    <author>Igalia S.L.</author>
+    <license>LGPL</license>
+    <site>http://www.igalia.com</site>
+  </info>
+</plugin>
diff --git a/src/metadata/gravatar/Makefile.am b/src/metadata/gravatar/Makefile.am
new file mode 100644
index 0000000..391795e
--- /dev/null
+++ b/src/metadata/gravatar/Makefile.am
@@ -0,0 +1,32 @@
+#
+# Makefile.am
+#
+# Author: Juan A. Suarez Romero <jasuarez igalia com>
+#
+# Copyright (C) 2010, 2011 Igalia S.L. All rights reserved.
+
+lib_LTLIBRARIES = libgrlgravatar.la
+
+libgrlgravatar_la_CFLAGS =	\
+	$(DEPS_CFLAGS)
+
+libgrlgravatar_la_LIBADD =	\
+	$(DEPS_LIBS)
+
+libgrlgravatar_la_LDFLAGS = \
+	-module		    \
+	-avoid-version
+
+libgrlgravatar_la_SOURCES = grl-gravatar.c grl-gravatar.h
+
+libdir=$(GRL_PLUGINS_DIR)
+gravatarxmldir		= $(GRL_PLUGINS_CONF_DIR)
+gravatarxml_DATA	= $(GRAVATAR_PLUGIN_ID).xml
+
+EXTRA_DIST = $(gravatarxml_DATA)
+
+MAINTAINERCLEANFILES =	\
+	*.in	\
+	*~
+
+DISTCLEANFILES = $(MAINTAINERCLEANFILES)
diff --git a/src/metadata/gravatar/grl-gravatar.c b/src/metadata/gravatar/grl-gravatar.c
new file mode 100644
index 0000000..41d9fa5
--- /dev/null
+++ b/src/metadata/gravatar/grl-gravatar.c
@@ -0,0 +1,301 @@
+/*
+ * Copyright (C) 2010 Igalia S.L.
+ *
+ * Contact: Iago Toral Quiroga <itoral igalia com>
+ *
+ * Authors: Juan A. Suarez Romero <jasuarez igalia 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 "grl-gravatar.h"
+
+/* ---------- Logging ---------- */
+
+#define GRL_LOG_DOMAIN_DEFAULT gravatar_log_domain
+GRL_LOG_DOMAIN_STATIC(gravatar_log_domain);
+
+/* -------- Gravatar API -------- */
+
+#define GRAVATAR_URL "http://www.gravatar.com/avatar/%s.jpg";
+
+/* ------- Pluging Info -------- */
+
+#define PLUGIN_ID   GRAVATAR_PLUGIN_ID
+
+#define SOURCE_ID   PLUGIN_ID
+#define SOURCE_NAME "Avatar provider from Gravatar"
+#define SOURCE_DESC "A plugin to get avatars for artist and author fields"
+
+#define AUTHOR      "Igalia S.L."
+#define LICENSE     "LGPL"
+#define SITE        "http://www.igalia.com";
+
+
+static GrlGravatarSource *grl_gravatar_source_new (void);
+
+static void grl_gravatar_source_resolve (GrlMetadataSource *source,
+                                         GrlMetadataSourceResolveSpec *rs);
+
+static const GList *grl_gravatar_source_supported_keys (GrlMetadataSource *source);
+
+static const GList *grl_gravatar_source_key_depends (GrlMetadataSource *source,
+                                                     GrlKeyID key_id);
+
+static GrlKeyID register_gravatar_key (GrlPluginRegistry *registry,
+                                       const gchar *name,
+                                       const gchar *nick,
+                                       const gchar *blurb);
+
+gboolean grl_gravatar_source_plugin_init (GrlPluginRegistry *registry,
+                                          const GrlPluginInfo *plugin,
+                                          GList *configs);
+
+GrlKeyID GRL_METADATA_KEY_ARTIST_AVATAR = NULL;
+GrlKeyID GRL_METADATA_KEY_AUTHOR_AVATAR = NULL;
+
+/* =================== Gravatar Plugin  =============== */
+
+gboolean
+grl_gravatar_source_plugin_init (GrlPluginRegistry *registry,
+                                 const GrlPluginInfo *plugin,
+                                 GList *configs)
+{
+  GRL_LOG_DOMAIN_INIT (gravatar_log_domain, "gravatar");
+
+  GRL_DEBUG ("grl_gravatar_source_plugin_init");
+
+  /* Register keys */
+  GRL_METADATA_KEY_ARTIST_AVATAR =
+    register_gravatar_key (registry,
+                           "artist-avatar",
+                           "ArtistAvatar",
+                           "Avatar for the artist");
+
+  GRL_METADATA_KEY_AUTHOR_AVATAR =
+    register_gravatar_key (registry,
+                           "author-avatar",
+                            "AuthorAvatar",
+                            "Avatar for the author");
+  if (!GRL_METADATA_KEY_ARTIST_AVATAR &&
+      !GRL_METADATA_KEY_AUTHOR_AVATAR) {
+    GRL_WARNING ("Unable to register \"autor-avatar\" nor \"artist-avatar\"");
+    return FALSE;
+  }
+
+  GrlGravatarSource *source = grl_gravatar_source_new ();
+  grl_plugin_registry_register_source (registry,
+                                       plugin,
+                                       GRL_MEDIA_PLUGIN (source),
+                                       NULL);
+  return TRUE;
+}
+
+GRL_PLUGIN_REGISTER (grl_gravatar_source_plugin_init,
+                     NULL,
+                     PLUGIN_ID);
+
+/* ================== Gravatar GObject ================ */
+
+static GrlGravatarSource *
+grl_gravatar_source_new (void)
+{
+  GRL_DEBUG ("grl_gravatar_source_new");
+  return g_object_new (GRL_GRAVATAR_SOURCE_TYPE,
+		       "source-id", SOURCE_ID,
+		       "source-name", SOURCE_NAME,
+		       "source-desc", SOURCE_DESC,
+		       NULL);
+}
+
+static void
+grl_gravatar_source_class_init (GrlGravatarSourceClass * klass)
+{
+  GrlMetadataSourceClass *metadata_class = GRL_METADATA_SOURCE_CLASS (klass);
+  metadata_class->supported_keys = grl_gravatar_source_supported_keys;
+  metadata_class->key_depends = grl_gravatar_source_key_depends;
+  metadata_class->resolve = grl_gravatar_source_resolve;
+}
+
+static void
+grl_gravatar_source_init (GrlGravatarSource *source)
+{
+}
+
+G_DEFINE_TYPE (GrlGravatarSource,
+               grl_gravatar_source,
+               GRL_TYPE_METADATA_SOURCE);
+
+/* ======================= Utilities ==================== */
+
+static GrlKeyID
+register_gravatar_key (GrlPluginRegistry *registry,
+                       const gchar *name,
+                       const gchar *nick,
+                       const gchar *blurb)
+{
+  GParamSpec *spec;
+  GrlKeyID key;
+
+  spec = g_param_spec_string (name,
+                              nick,
+                              blurb,
+                              NULL,
+                              G_PARAM_READWRITE);
+
+  key = grl_plugin_registry_register_metadata_key (registry, spec, NULL);
+
+  /* If key was not registered, could be that it is already registered. If so,
+     check if type is the expected one, and reuse it */
+  if (!key) {
+    g_param_spec_unref (spec);
+    key = grl_plugin_registry_lookup_metadata_key (registry, name);
+    if (!key || GRL_METADATA_KEY_GET_TYPE (key) != G_TYPE_STRING) {
+      key = NULL;
+    }
+  }
+
+  return key;
+}
+
+static gchar *
+get_avatar (const gchar *field) {
+  GMatchInfo *match_info = NULL;
+  gchar *avatar = NULL;
+  gchar *email;
+  gchar *email_hash;
+  gchar *lowercased_field;
+  static GRegex *email_regex = NULL;
+
+  if (!field) {
+    return NULL;
+  }
+
+  lowercased_field = g_utf8_strdown (field, -1);
+
+  if (!email_regex) {
+    email_regex = g_regex_new ("[\\w-]+@([\\w-]+\\.)+[\\w-]+", G_REGEX_OPTIMIZE, 0, NULL);
+  }
+
+  if (g_regex_match (email_regex, lowercased_field, 0, &match_info)) {
+    email = g_match_info_fetch (match_info, 0);
+    g_match_info_free (match_info);
+    email_hash = g_compute_checksum_for_string (G_CHECKSUM_MD5, email, -1);
+    avatar = g_strdup_printf (GRAVATAR_URL, email_hash);
+    g_free (email);
+    g_free (email_hash);
+  }
+
+  return avatar;
+}
+
+/* ================== API Implementation ================ */
+
+static const GList *
+grl_gravatar_source_supported_keys (GrlMetadataSource *source)
+{
+  static GList *keys = NULL;
+
+  if (!keys) {
+    if (GRL_METADATA_KEY_ARTIST_AVATAR) {
+      keys = g_list_prepend (keys, GRL_METADATA_KEY_ARTIST_AVATAR);
+    }
+    if (GRL_METADATA_KEY_AUTHOR_AVATAR) {
+      keys =g_list_prepend (keys, GRL_METADATA_KEY_AUTHOR_AVATAR);
+    }
+  }
+
+ return keys;
+}
+
+static const GList *
+grl_gravatar_source_key_depends (GrlMetadataSource *source,
+                                 GrlKeyID key_id)
+{
+  static GList *artist_avatar_deps = NULL;
+  static GList *author_avatar_deps = NULL;
+
+  if (!artist_avatar_deps) {
+    artist_avatar_deps = grl_metadata_key_list_new (GRL_METADATA_KEY_ARTIST,
+                                                    NULL);
+  }
+
+  if (!author_avatar_deps) {
+    author_avatar_deps = grl_metadata_key_list_new (GRL_METADATA_KEY_AUTHOR,
+                                                    NULL);
+  }
+
+  if (key_id == GRL_METADATA_KEY_ARTIST_AVATAR) {
+    return artist_avatar_deps;
+  } else if (key_id == GRL_METADATA_KEY_AUTHOR_AVATAR) {
+    return author_avatar_deps;
+  } else {
+    return  NULL;
+  }
+}
+
+static void
+grl_gravatar_source_resolve (GrlMetadataSource *source,
+                             GrlMetadataSourceResolveSpec *rs)
+{
+  gboolean artist_avatar_required = FALSE;
+  gboolean author_avatar_required = FALSE;
+  gchar *avatar_url;
+
+  GRL_DEBUG ("grl_gravatar_source_resolve");
+
+  GList *iter;
+
+  /* Check that albumart is requested */
+  iter = rs->keys;
+  while (iter && (!artist_avatar_required || !author_avatar_required)) {
+    if (iter->data == GRL_METADATA_KEY_ARTIST_AVATAR) {
+      artist_avatar_required = TRUE;
+    } else if (iter->data == GRL_METADATA_KEY_AUTHOR_AVATAR) {
+      author_avatar_required = TRUE;
+    }
+    iter = g_list_next (iter);
+  }
+
+  if (artist_avatar_required) {
+    avatar_url = get_avatar (grl_data_get_string (GRL_DATA (rs->media),
+                                                  GRL_METADATA_KEY_ARTIST));
+    if (avatar_url) {
+      grl_data_set_string (GRL_DATA (rs->media),
+                           GRL_METADATA_KEY_ARTIST_AVATAR,
+                           avatar_url);
+      g_free (avatar_url);
+    }
+  }
+
+  if (author_avatar_required) {
+    avatar_url = get_avatar (grl_data_get_string (GRL_DATA (rs->media),
+                                                  GRL_METADATA_KEY_AUTHOR));
+    if (avatar_url) {
+      grl_data_set_string (GRL_DATA (rs->media),
+                           GRL_METADATA_KEY_AUTHOR_AVATAR,
+                           avatar_url);
+      g_free (avatar_url);
+    }
+  }
+
+  rs->callback (source, rs->media, rs->user_data, NULL);
+}
diff --git a/src/metadata/gravatar/grl-gravatar.h b/src/metadata/gravatar/grl-gravatar.h
new file mode 100644
index 0000000..644efdb
--- /dev/null
+++ b/src/metadata/gravatar/grl-gravatar.h
@@ -0,0 +1,74 @@
+/*
+ * Copyright (C) 2010 Igalia S.L.
+ *
+ * Contact: Iago Toral Quiroga <itoral igalia com>
+ *
+ * Authors: Juan A. Suarez Romero <jasuarez igalia 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_GRAVATAR_H_
+#define _GRL_GRAVATAR_H_
+
+#include <grilo.h>
+
+#define GRL_GRAVATAR_SOURCE_TYPE                \
+  (grl_gravatar_source_get_type ())
+
+#define GRL_GRAVATAR_SOURCE(obj)                                \
+  (G_TYPE_CHECK_INSTANCE_CAST ((obj),                           \
+                               GRL_GRAVATAR_SOURCE_TYPE,        \
+                               GrlGravatarSource))
+
+#define GRL_IS_GRAVATAR_SOURCE(obj)                             \
+  (G_TYPE_CHECK_INSTANCE_TYPE ((obj),                           \
+                               GRL_GRAVATAR_SOURCE_TYPE))
+
+#define GRL_GRAVATAR_SOURCE_CLASS(klass)                \
+  (G_TYPE_CHECK_CLASS_CAST((klass),                     \
+                           GRL_GRAVATAR_SOURCE_TYPE,    \
+                           GrlGravatarSourceClass))
+
+#define GRL_IS_GRAVATAR_SOURCE_CLASS(klass)             \
+  (G_TYPE_CHECK_CLASS_TYPE((klass),                     \
+                           GRL_GRAVATAR_SOURCE_TYPE))
+
+#define GRL_GRAVATAR_SOURCE_GET_CLASS(obj)              \
+  (G_TYPE_INSTANCE_GET_CLASS ((obj),                    \
+                              GRL_GRAVATAR_SOURCE_TYPE, \
+                              GrlGravatarSourceClass))
+
+typedef struct _GrlGravatarSource GrlGravatarSource;
+
+struct _GrlGravatarSource {
+
+  GrlMetadataSource parent;
+
+};
+
+typedef struct _GrlGravatarSourceClass GrlGravatarSourceClass;
+
+struct _GrlGravatarSourceClass {
+
+  GrlMetadataSourceClass parent_class;
+
+};
+
+GType grl_gravatar_source_get_type (void);
+
+#endif /* _GRL_GRAVATAR_H_ */
diff --git a/src/metadata/gravatar/grl-gravatar.xml b/src/metadata/gravatar/grl-gravatar.xml
new file mode 100644
index 0000000..58f60d1
--- /dev/null
+++ b/src/metadata/gravatar/grl-gravatar.xml
@@ -0,0 +1,9 @@
+<plugin>
+  <info>
+    <name>Avatar provider from Gravatar</name>
+    <description>A plugin to get avatars for artist and author fields</description>
+    <author>Igalia S.L.</author>
+    <license>LGPL</license>
+    <site>http://www.igalia.com</site>
+  </info>
+</plugin>
diff --git a/src/metadata/lastfm-albumart/Makefile.am b/src/metadata/lastfm-albumart/Makefile.am
new file mode 100644
index 0000000..ad7bc6e
--- /dev/null
+++ b/src/metadata/lastfm-albumart/Makefile.am
@@ -0,0 +1,36 @@
+#
+# Makefile.am
+#
+# Author: Juan A. Suarez Romero <jasuarez igalia com>
+#
+# Copyright (C) 2010, 2011 Igalia S.L. All rights reserved.
+
+lib_LTLIBRARIES = libgrllastfm-albumart.la
+
+libgrllastfm_albumart_la_CFLAGS =	\
+	$(DEPS_CFLAGS) 			\
+	$(GRLNET_CFLAGS) 		\
+	$(XML_CFLAGS)
+
+libgrllastfm_albumart_la_LIBADD =	\
+	$(DEPS_LIBS)			\
+	$(GRLNET_LIBS)			\
+	$(XML_LIBS)
+
+libgrllastfm_albumart_la_LDFLAGS = \
+	-module			   \
+	-avoid-version
+
+libgrllastfm_albumart_la_SOURCES = grl-lastfm-albumart.c grl-lastfm-albumart.h
+
+libdir = $(GRL_PLUGINS_DIR)
+lastfmalbumartxmldir	= $(GRL_PLUGINS_CONF_DIR)
+lastfmalbumartxml_DATA	= $(LASTFM_ALBUMART_PLUGIN_ID).xml
+
+EXTRA_DIST = $(lastfmalbumartxml_DATA)
+
+MAINTAINERCLEANFILES =	\
+	*.in	\
+	*~
+
+DISTCLEANFILES = $(MAINTAINERCLEANFILES)
diff --git a/src/metadata/lastfm-albumart/grl-lastfm-albumart.c b/src/metadata/lastfm-albumart/grl-lastfm-albumart.c
new file mode 100644
index 0000000..99825a2
--- /dev/null
+++ b/src/metadata/lastfm-albumart/grl-lastfm-albumart.c
@@ -0,0 +1,316 @@
+/*
+ * Copyright (C) 2010 Igalia S.L.
+ *
+ * Contact: Iago Toral Quiroga <itoral igalia com>
+ *
+ * Authors: Juan A. Suarez Romero <jasuarez igalia 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 <net/grl-net.h>
+#include <libxml/parser.h>
+#include <libxml/xmlmemory.h>
+#include <libxml/xpath.h>
+
+#include "grl-lastfm-albumart.h"
+
+/* ---------- Logging ---------- */
+
+#define GRL_LOG_DOMAIN_DEFAULT lastfm_albumart_log_domain
+GRL_LOG_DOMAIN_STATIC(lastfm_albumart_log_domain);
+
+/* -------- Last.FM API -------- */
+
+#define LASTFM_GET_ALBUM "http://ws.audioscrobbler.com/1.0/album/%s/%s/info.xml";
+#define LASTFM_XML_COVER "/album/coverart/medium"
+
+/* ------- Pluging Info -------- */
+
+#define PLUGIN_ID   LASTFM_ALBUMART_PLUGIN_ID
+
+#define SOURCE_ID   "grl-lastfm-albumart"
+#define SOURCE_NAME "Album art Provider from Last.FM"
+#define SOURCE_DESC "A plugin for getting album arts using Last.FM as backend"
+
+#define AUTHOR      "Igalia S.L."
+#define LICENSE     "LGPL"
+#define SITE        "http://www.igalia.com";
+
+static GrlNetWc *wc;
+
+static GrlLastfmAlbumartSource *grl_lastfm_albumart_source_new (void);
+
+static void grl_lastfm_albumart_source_finalize (GObject *object);
+
+static void grl_lastfm_albumart_source_resolve (GrlMetadataSource *source,
+                                                GrlMetadataSourceResolveSpec *rs);
+
+static const GList *grl_lastfm_albumart_source_supported_keys (GrlMetadataSource *source);
+
+static const GList *grl_lastfm_albumart_source_key_depends (GrlMetadataSource *source,
+                                                            GrlKeyID key_id);
+
+gboolean grl_lastfm_albumart_source_plugin_init (GrlPluginRegistry *registry,
+                                                 const GrlPluginInfo *plugin,
+                                                 GList *configs);
+
+
+/* =================== Last.FM-AlbumArt Plugin  =============== */
+
+gboolean
+grl_lastfm_albumart_source_plugin_init (GrlPluginRegistry *registry,
+                                        const GrlPluginInfo *plugin,
+                                        GList *configs)
+{
+  GRL_LOG_DOMAIN_INIT (lastfm_albumart_log_domain, "lastfm-albumart");
+
+  GRL_DEBUG ("grl_lastfm_albumart_source_plugin_init");
+
+  GrlLastfmAlbumartSource *source = grl_lastfm_albumart_source_new ();
+  grl_plugin_registry_register_source (registry,
+                                       plugin,
+                                       GRL_MEDIA_PLUGIN (source),
+                                       NULL);
+  return TRUE;
+}
+
+GRL_PLUGIN_REGISTER (grl_lastfm_albumart_source_plugin_init,
+                     NULL,
+                     PLUGIN_ID);
+
+/* ================== Last.FM-AlbumArt GObject ================ */
+
+static GrlLastfmAlbumartSource *
+grl_lastfm_albumart_source_new (void)
+{
+  GRL_DEBUG ("grl_lastfm_albumart_source_new");
+  return g_object_new (GRL_LASTFM_ALBUMART_SOURCE_TYPE,
+		       "source-id", SOURCE_ID,
+		       "source-name", SOURCE_NAME,
+		       "source-desc", SOURCE_DESC,
+		       NULL);
+}
+
+static void
+grl_lastfm_albumart_source_class_init (GrlLastfmAlbumartSourceClass * klass)
+{
+  GrlMetadataSourceClass *metadata_class = GRL_METADATA_SOURCE_CLASS (klass);
+  metadata_class->supported_keys = grl_lastfm_albumart_source_supported_keys;
+  metadata_class->key_depends = grl_lastfm_albumart_source_key_depends;
+  metadata_class->resolve = grl_lastfm_albumart_source_resolve;
+
+  GObjectClass *gobject_class = G_OBJECT_CLASS (klass);
+  gobject_class->finalize = grl_lastfm_albumart_source_finalize;
+}
+
+static void
+grl_lastfm_albumart_source_init (GrlLastfmAlbumartSource *source)
+{
+}
+
+G_DEFINE_TYPE (GrlLastfmAlbumartSource,
+               grl_lastfm_albumart_source,
+               GRL_TYPE_METADATA_SOURCE);
+
+static void
+grl_lastfm_albumart_source_finalize (GObject *object)
+{
+  if (wc && GRL_IS_NET_WC (wc))
+    g_object_unref (wc);
+
+  G_OBJECT_CLASS (grl_lastfm_albumart_source_parent_class)->finalize (object);
+}
+
+/* ======================= Utilities ==================== */
+
+static gchar *
+xml_get_image (const gchar *xmldata)
+{
+  xmlDocPtr doc;
+  xmlXPathContextPtr xpath_ctx;
+  xmlXPathObjectPtr xpath_res;
+  gchar *image = NULL;
+
+  doc = xmlReadMemory (xmldata, xmlStrlen ((xmlChar*) xmldata), NULL, NULL,
+                       XML_PARSE_RECOVER | XML_PARSE_NOBLANKS);
+  if (!doc) {
+    return NULL;
+  }
+
+  xpath_ctx = xmlXPathNewContext (doc);
+  if (!xpath_ctx) {
+    xmlFreeDoc (doc);
+    return NULL;
+  }
+
+  xpath_res = xmlXPathEvalExpression ((xmlChar *) LASTFM_XML_COVER,
+                                      xpath_ctx);
+  if (!xpath_res) {
+    xmlXPathFreeContext (xpath_ctx);
+    xmlFreeDoc (doc);
+    return NULL;
+  }
+
+  if (xpath_res->nodesetval->nodeTab) {
+    image =
+      (gchar *) xmlNodeListGetString (doc,
+                                      xpath_res->nodesetval->nodeTab[0]->xmlChildrenNode,
+                                      1);
+  }
+  xmlXPathFreeObject (xpath_res);
+  xmlXPathFreeContext (xpath_ctx);
+  xmlFreeDoc (doc);
+
+  return image;
+}
+
+static void
+read_done_cb (GObject *source_object,
+              GAsyncResult *res,
+              gpointer user_data)
+{
+  GrlMetadataSourceResolveSpec *rs =
+    (GrlMetadataSourceResolveSpec *) user_data;
+  GError *error = NULL;
+  GError *wc_error = NULL;
+  gchar *content = NULL;
+  gchar *image = NULL;
+
+  if (!grl_net_wc_request_finish (GRL_NET_WC (source_object),
+                              res,
+                              &content,
+                              NULL,
+                              &wc_error)) {
+    error = g_error_new (GRL_CORE_ERROR,
+                         GRL_CORE_ERROR_RESOLVE_FAILED,
+                         "Failed to connect to Last.FM: '%s'",
+                         wc_error->message);
+    rs->callback (rs->source, rs->media, rs->user_data, error);
+    g_error_free (wc_error);
+    g_error_free (error);
+
+    return;
+  }
+
+  image = xml_get_image (content);
+  if (image) {
+    grl_data_set_string (GRL_DATA (rs->media),
+                         GRL_METADATA_KEY_THUMBNAIL,
+                         image);
+    g_free (image);
+  }
+
+  rs->callback (rs->source, rs->media, rs->user_data, NULL);
+}
+
+static void
+read_url_async (const gchar *url, gpointer user_data)
+{
+  if (!wc)
+    wc = grl_net_wc_new ();
+
+  GRL_DEBUG ("Opening '%s'", url);
+  grl_net_wc_request_async (wc, url, NULL, read_done_cb, user_data);
+}
+
+/* ================== API Implementation ================ */
+
+static const GList *
+grl_lastfm_albumart_source_supported_keys (GrlMetadataSource *source)
+{
+  static GList *keys = NULL;
+
+  if (!keys) {
+    keys = grl_metadata_key_list_new (GRL_METADATA_KEY_THUMBNAIL,
+                                      NULL);
+  }
+
+  return keys;
+}
+
+static const GList *
+grl_lastfm_albumart_source_key_depends (GrlMetadataSource *source,
+                                        GrlKeyID key_id)
+{
+  static GList *deps = NULL;
+
+  if (!deps) {
+    deps = grl_metadata_key_list_new (GRL_METADATA_KEY_ARTIST,
+                                      GRL_METADATA_KEY_ALBUM,
+                                      NULL);
+  }
+
+  if (key_id == GRL_METADATA_KEY_THUMBNAIL) {
+    return deps;
+  }
+
+  return  NULL;
+}
+
+static void
+grl_lastfm_albumart_source_resolve (GrlMetadataSource *source,
+                                    GrlMetadataSourceResolveSpec *rs)
+{
+  const gchar *artist = NULL;
+  const gchar *album = NULL;
+  gchar *esc_artist = NULL;
+  gchar *esc_album = NULL;
+  gchar *url = NULL;
+
+  GRL_DEBUG ("grl_lastfm_albumart_source_resolve");
+
+  GList *iter;
+
+  /* Check that albumart is requested */
+  iter = rs->keys;
+  while (iter) {
+    if (iter->data == GRL_METADATA_KEY_THUMBNAIL) {
+      break;
+    } else {
+      iter = g_list_next (iter);
+    }
+  }
+
+  if (iter == NULL) {
+    GRL_DEBUG ("No supported key was requested");
+    rs->callback (source, rs->media, rs->user_data, NULL);
+  } else {
+    artist = grl_data_get_string (GRL_DATA (rs->media),
+                                  GRL_METADATA_KEY_ARTIST);
+
+    album = grl_data_get_string (GRL_DATA (rs->media),
+                                 GRL_METADATA_KEY_ALBUM);
+
+    if (!artist || !album) {
+      GRL_DEBUG ("Missing dependencies");
+      rs->callback (source, rs->media, rs->user_data, NULL);
+    } else {
+      esc_artist = g_uri_escape_string (artist, NULL, TRUE);
+      esc_album = g_uri_escape_string (album, NULL, TRUE);
+      url = g_strdup_printf (LASTFM_GET_ALBUM, esc_artist, esc_album);
+      read_url_async (url, rs);
+      g_free (esc_artist);
+      g_free (esc_album);
+      g_free (url);
+    }
+  }
+}
diff --git a/src/metadata/lastfm-albumart/grl-lastfm-albumart.h b/src/metadata/lastfm-albumart/grl-lastfm-albumart.h
new file mode 100644
index 0000000..43a5ab5
--- /dev/null
+++ b/src/metadata/lastfm-albumart/grl-lastfm-albumart.h
@@ -0,0 +1,74 @@
+/*
+ * Copyright (C) 2010 Igalia S.L.
+ *
+ * Contact: Iago Toral Quiroga <itoral igalia com>
+ *
+ * Authors: Juan A. Suarez Romero <jasuarez igalia 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_LASTFM_ALBUMART_SOURCE_H_
+#define _GRL_LASTFM_ALBUMART_SOURCE_H_
+
+#include <grilo.h>
+
+#define GRL_LASTFM_ALBUMART_SOURCE_TYPE         \
+  (grl_lastfm_albumart_source_get_type ())
+
+#define GRL_LASTFM_ALBUMART_SOURCE(obj)                         \
+  (G_TYPE_CHECK_INSTANCE_CAST ((obj),                           \
+                               GRL_LASTFM_ALBUMART_SOURCE_TYPE, \
+                               GrlLastfmAlbumartSource))
+
+#define GRL_IS_LASTFM_ALBUMART_SOURCE(obj)                              \
+  (G_TYPE_CHECK_INSTANCE_TYPE ((obj),                                   \
+                               GRL_LASTFM_ALBUMART_SOURCE_TYPE))
+
+#define GRL_LASTFM_ALBUMART_SOURCE_CLASS(klass)                 \
+  (G_TYPE_CHECK_CLASS_CAST((klass),                             \
+                           GRL_LASTFM_ALBUMART_SOURCE_TYPE,     \
+                           GrlLastfmAlbumartSourceClass))
+
+#define GRL_IS_LASTFM_ALBUMART_SOURCE_CLASS(klass)              \
+  (G_TYPE_CHECK_CLASS_TYPE((klass),                             \
+                           GRL_LASTFM_ALBUMART_SOURCE_TYPE))
+
+#define GRL_LASTFM_ALBUMART_SOURCE_GET_CLASS(obj)                       \
+  (G_TYPE_INSTANCE_GET_CLASS ((obj),                                    \
+                              GRL_LASTFM_ALBUMART_SOURCE_TYPE,          \
+                              GrlLastfmAlbumartSourceClass))
+
+typedef struct _GrlLastfmAlbumartSource GrlLastfmAlbumartSource;
+
+struct _GrlLastfmAlbumartSource {
+
+  GrlMetadataSource parent;
+
+};
+
+typedef struct _GrlLastfmAlbumartSourceClass GrlLastfmAlbumartSourceClass;
+
+struct _GrlLastfmAlbumartSourceClass {
+
+  GrlMetadataSourceClass parent_class;
+
+};
+
+GType grl_lastfm_albumart_source_get_type (void);
+
+#endif /* _GRL_LASTFM_ALBUMART_SOURCE_H_ */
diff --git a/src/metadata/lastfm-albumart/grl-lastfm-albumart.xml b/src/metadata/lastfm-albumart/grl-lastfm-albumart.xml
new file mode 100644
index 0000000..f2b64dd
--- /dev/null
+++ b/src/metadata/lastfm-albumart/grl-lastfm-albumart.xml
@@ -0,0 +1,9 @@
+<plugin>
+  <info>
+    <name>Album art Provider from Last.FM</name>
+    <description>A plugin for getting album arts using Last.FM as backend</description>
+    <author>Igalia S.L.</author>
+    <license>LGPL</license>
+    <site>http://www.igalia.com</site>
+  </info>
+</plugin>
diff --git a/src/metadata/local-metadata/Makefile.am b/src/metadata/local-metadata/Makefile.am
new file mode 100644
index 0000000..6840cf3
--- /dev/null
+++ b/src/metadata/local-metadata/Makefile.am
@@ -0,0 +1,34 @@
+#
+# Makefile.am
+#
+# Author: Guillaume Emont <gemont igalia com>
+#
+# Copyright (C) 2010-2011 Igalia S.L. All rights reserved.
+
+lib_LTLIBRARIES		 = libgrllocalmetadata.la
+
+libgrllocalmetadata_la_CFLAGS =	\
+	$(DEPS_CFLAGS) \
+	$(GIO_CFLAGS)
+
+libgrllocalmetadata_la_LIBADD =	\
+	$(DEPS_LIBS) \
+	$(GIO_LIBS)
+
+libgrllocalmetadata_la_LDFLAGS = \
+	-module			\
+	-avoid-version
+
+libgrllocalmetadata_la_SOURCES = grl-local-metadata.c grl-local-metadata.h
+
+libdir=$(GRL_PLUGINS_DIR)
+localmetadataxmldir	= $(GRL_PLUGINS_CONF_DIR)
+localmetadataxml_DATA	= $(LOCALMETADATA_PLUGIN_ID).xml
+
+EXTRA_DIST = $(localmetadataxml_DATA)
+
+MAINTAINERCLEANFILES	 = \
+	*.in \
+	*~
+
+DISTCLEANFILES = $(MAINTAINERCLEANFILES)
diff --git a/src/metadata/local-metadata/grl-local-metadata.c b/src/metadata/local-metadata/grl-local-metadata.c
new file mode 100644
index 0000000..a535f42
--- /dev/null
+++ b/src/metadata/local-metadata/grl-local-metadata.c
@@ -0,0 +1,262 @@
+/*
+ * Copyright (C) 2010-2011 Igalia S.L.
+ *
+ * Contact: Guillaume Emont <gemont igalia 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 <grilo.h>
+#include <gio/gio.h>
+
+#include "grl-local-metadata.h"
+
+#define GRL_LOG_DOMAIN_DEFAULT local_metadata_log_domain
+GRL_LOG_DOMAIN_STATIC(local_metadata_log_domain);
+
+#define PLUGIN_ID   LOCALMETADATA_PLUGIN_ID
+
+#define SOURCE_ID   "grl-local-metadata"
+#define SOURCE_NAME "Local Metadata Provider"
+#define SOURCE_DESC "A source providing locally available metadata"
+
+#define AUTHOR      "Igalia S.L."
+#define LICENSE     "LGPL"
+#define SITE        "http://www.igalia.com";
+
+
+static GrlLocalMetadataSource *grl_local_metadata_source_new (void);
+
+static void grl_local_metadata_source_resolve (GrlMetadataSource *source,
+                                              GrlMetadataSourceResolveSpec *rs);
+
+static const GList *grl_local_metadata_source_supported_keys (GrlMetadataSource *source);
+
+static const GList * grl_local_metadata_source_key_depends (GrlMetadataSource *source,
+                                                            GrlKeyID key_id);
+
+gboolean grl_local_metadata_source_plugin_init (GrlPluginRegistry *registry,
+                                               const GrlPluginInfo *plugin,
+                                               GList *configs);
+
+
+/* =================== GrlLocalMetadata Plugin  =============== */
+
+gboolean
+grl_local_metadata_source_plugin_init (GrlPluginRegistry *registry,
+                                      const GrlPluginInfo *plugin,
+                                      GList *configs)
+{
+  GRL_LOG_DOMAIN_INIT (local_metadata_log_domain, "local-metadata");
+
+  GRL_DEBUG ("grl_local_metadata_source_plugin_init");
+
+  GrlLocalMetadataSource *source = grl_local_metadata_source_new ();
+  grl_plugin_registry_register_source (registry,
+                                       plugin,
+                                       GRL_MEDIA_PLUGIN (source),
+                                       NULL);
+  return TRUE;
+}
+
+GRL_PLUGIN_REGISTER (grl_local_metadata_source_plugin_init,
+                     NULL,
+                     PLUGIN_ID);
+
+/* ================== GrlLocalMetadata GObject ================ */
+
+static GrlLocalMetadataSource *
+grl_local_metadata_source_new (void)
+{
+  GRL_DEBUG ("grl_local_metadata_source_new");
+  return g_object_new (GRL_LOCAL_METADATA_SOURCE_TYPE,
+		       "source-id", SOURCE_ID,
+		       "source-name", SOURCE_NAME,
+		       "source-desc", SOURCE_DESC,
+		       NULL);
+}
+
+static void
+grl_local_metadata_source_class_init (GrlLocalMetadataSourceClass * klass)
+{
+  GrlMetadataSourceClass *metadata_class = GRL_METADATA_SOURCE_CLASS (klass);
+  metadata_class->supported_keys = grl_local_metadata_source_supported_keys;
+  metadata_class->key_depends = grl_local_metadata_source_key_depends;
+  metadata_class->resolve = grl_local_metadata_source_resolve;
+}
+
+static void
+grl_local_metadata_source_init (GrlLocalMetadataSource *source)
+{
+}
+
+G_DEFINE_TYPE (GrlLocalMetadataSource,
+               grl_local_metadata_source,
+               GRL_TYPE_METADATA_SOURCE);
+
+/* ======================= Utilities ==================== */
+static void
+got_file_info (GFile *file, GAsyncResult *result,
+               GrlMetadataSourceResolveSpec *rs)
+{
+  GFileInfo *info;
+  GError *error = NULL;
+  const gchar *thumbnail_path;
+
+  GRL_DEBUG ("got_file_info");
+
+  info = g_file_query_info_finish (file, result, &error);
+  if (error)
+    goto error;
+
+  thumbnail_path =
+      g_file_info_get_attribute_byte_string (info, G_FILE_ATTRIBUTE_THUMBNAIL_PATH);
+
+
+  if (thumbnail_path) {
+    gchar *thumbnail_uri = g_filename_to_uri (thumbnail_path, NULL, &error);
+    if (error)
+      goto error;
+
+    GRL_INFO ("Got thumbnail %s for media: %s", thumbnail_uri,
+              grl_media_get_url (rs->media));
+    grl_media_set_thumbnail (rs->media, thumbnail_uri);
+    g_free (thumbnail_uri);
+
+    rs->callback (rs->source, rs->media, rs->user_data, NULL);
+  } else {
+    GRL_INFO ("Could not find thumbnail for media: %s",
+              grl_media_get_url (rs->media));
+    rs->callback (rs->source, rs->media, rs->user_data, NULL);
+  }
+
+  goto exit;
+
+error:
+    {
+      GError *new_error = g_error_new (GRL_CORE_ERROR, GRL_CORE_ERROR_RESOLVE_FAILED,
+                                       "Got error: %s", error->message);
+      rs->callback (rs->source, rs->media, rs->user_data, new_error);
+
+      g_error_free (error);
+      g_error_free (new_error);
+    }
+
+exit:
+  if (info)
+    g_object_unref (info);
+}
+
+static void
+resolve_image (GrlMetadataSourceResolveSpec *rs)
+{
+  GFile *file;
+
+  GRL_DEBUG ("resolve_image");
+
+  file = g_file_new_for_uri (grl_media_get_url (rs->media));
+
+  g_file_query_info_async (file, G_FILE_ATTRIBUTE_THUMBNAIL_PATH,
+                           G_FILE_QUERY_INFO_NONE, G_PRIORITY_DEFAULT, NULL,
+                           (GAsyncReadyCallback)got_file_info, rs);
+}
+
+static void
+resolve_album_art (GrlMetadataSourceResolveSpec *rs)
+{
+  /* FIXME: implement this, according to
+   * http://live.gnome.org/MediaArtStorageSpec */
+  GError *error;
+  error = g_error_new (GRL_CORE_ERROR, GRL_CORE_ERROR_RESOLVE_FAILED,
+    "Thumbnail resolution for GrlMediaAudio not implemented in local-metadata");
+  rs->callback (rs->source, rs->media, rs->user_data, error);
+  g_error_free (error);
+}
+
+/* ================== API Implementation ================ */
+
+static const GList *
+grl_local_metadata_source_supported_keys (GrlMetadataSource *source)
+{
+  static GList *keys = NULL;
+  if (!keys) {
+    keys = grl_metadata_key_list_new (GRL_METADATA_KEY_THUMBNAIL,
+                                      NULL);
+  }
+  return keys;
+}
+
+static const GList *
+grl_local_metadata_source_key_depends (GrlMetadataSource *source,
+                                       GrlKeyID key_id)
+{
+  static GList *deps = NULL;
+  if (!deps) {
+    deps = grl_metadata_key_list_new (GRL_METADATA_KEY_URL, NULL);
+  }
+
+  if (key_id == GRL_METADATA_KEY_THUMBNAIL)
+    return deps;
+
+  return NULL;
+}
+
+static void
+grl_local_metadata_source_resolve (GrlMetadataSource *source,
+                                  GrlMetadataSourceResolveSpec *rs)
+{
+  const gchar *url;
+  gchar *scheme;
+  GError *error = NULL;
+
+  GRL_DEBUG ("grl_local_metadata_source_resolve");
+
+  url = grl_media_get_url (rs->media);
+  scheme = g_uri_parse_scheme (url);
+
+  if (0 != g_strcmp0 (scheme, "file"))
+    error = g_error_new (GRL_CORE_ERROR, GRL_CORE_ERROR_RESOLVE_FAILED,
+                         "local-metadata needs a url in the file:// scheme");
+  else if (!g_list_find (rs->keys, GRL_METADATA_KEY_THUMBNAIL))
+    error = g_error_new (GRL_CORE_ERROR, GRL_CORE_ERROR_RESOLVE_FAILED,
+                         "local-metadata can only resolve the thumbnail key");
+
+  if (error) {
+    /* No can do! */
+    rs->callback (source, rs->media, rs->user_data, error);
+    g_error_free (error);
+    goto exit;
+  }
+
+  if (GRL_IS_MEDIA_VIDEO (rs->media)
+      || GRL_IS_MEDIA_IMAGE (rs->media)) {
+    resolve_image (rs);
+  } else if (GRL_IS_MEDIA_AUDIO (rs->media)) {
+    resolve_album_art (rs);
+  } else {
+    /* What's that media type? */
+    rs->callback (source, rs->media, rs->user_data, NULL);
+  }
+
+exit:
+  g_free (scheme);
+}
+
diff --git a/src/metadata/local-metadata/grl-local-metadata.h b/src/metadata/local-metadata/grl-local-metadata.h
new file mode 100644
index 0000000..448051e
--- /dev/null
+++ b/src/metadata/local-metadata/grl-local-metadata.h
@@ -0,0 +1,72 @@
+/*
+ * Copyright (C) 2010-2011 Igalia S.L.
+ *
+ * Contact: Guillaume Emont <gemont igalia 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_LOCAL_METADATA_SOURCE_H_
+#define _GRL_LOCAL_METADATA_SOURCE_H_
+
+#include <grilo.h>
+
+#define GRL_LOCAL_METADATA_SOURCE_TYPE           \
+  (grl_local_metadata_source_get_type ())
+
+#define GRL_LOCAL_METADATA_SOURCE(obj)                           \
+  (G_TYPE_CHECK_INSTANCE_CAST ((obj),                           \
+                               GRL_LOCAL_METADATA_SOURCE_TYPE,   \
+                               GrlLocalMetadataSource))
+
+#define GRL_IS_LOCAL_METADATA_SOURCE(obj)                        \
+  (G_TYPE_CHECK_INSTANCE_TYPE ((obj),                           \
+                               GRL_LOCAL_METADATA_SOURCE_TYPE))
+
+#define GRL_LOCAL_METADATA_SOURCE_CLASS(klass)                   \
+  (G_TYPE_CHECK_CLASS_CAST((klass),                             \
+                           GRL_LOCAL_METADATA_SOURCE_TYPE,       \
+                           GrlLocalMetadataSourceClass))
+
+#define GRL_IS_LOCAL_METADATA_SOURCE_CLASS(klass)                \
+  (G_TYPE_CHECK_CLASS_TYPE((klass),                             \
+                           GRL_LOCAL_METADATA_SOURCE_TYPE))
+
+#define GRL_LOCAL_METADATA_SOURCE_GET_CLASS(obj)                 \
+  (G_TYPE_INSTANCE_GET_CLASS ((obj),                            \
+                              GRL_LOCAL_METADATA_SOURCE_TYPE,    \
+                              GrlLocalMetadataSourceClass))
+
+typedef struct _GrlLocalMetadataSource GrlLocalMetadataSource;
+
+struct _GrlLocalMetadataSource {
+
+  GrlMetadataSource parent;
+
+};
+
+typedef struct _GrlLocalMetadataSourceClass GrlLocalMetadataSourceClass;
+
+struct _GrlLocalMetadataSourceClass {
+
+  GrlMetadataSourceClass parent_class;
+
+};
+
+GType grl_local_metadata_source_get_type (void);
+
+#endif /* _GRL_LOCAL_METADATA_SOURCE_H_ */
diff --git a/src/metadata/local-metadata/grl-local-metadata.xml b/src/metadata/local-metadata/grl-local-metadata.xml
new file mode 100644
index 0000000..073392e
--- /dev/null
+++ b/src/metadata/local-metadata/grl-local-metadata.xml
@@ -0,0 +1,9 @@
+<plugin>
+  <info>
+    <name>Local Metadata Provider</name>
+    <description>A plugin that gets simple-to-obtain metadata from the local system</description>
+    <author>Igalia S.L.</author>
+    <license>LGPL</license>
+    <site>http://www.igalia.com</site>
+  </info>
+</plugin>
diff --git a/src/metadata/metadata-store/Makefile.am b/src/metadata/metadata-store/Makefile.am
new file mode 100644
index 0000000..372f4a8
--- /dev/null
+++ b/src/metadata/metadata-store/Makefile.am
@@ -0,0 +1,34 @@
+#
+# Makefile.am
+#
+# Author: Iago Toral Quiroga <itoral igalia com>
+#
+# Copyright (C) 2010, 2011 Igalia S.L. All rights reserved.
+
+lib_LTLIBRARIES		 = libgrlmetadatastore.la
+
+libgrlmetadatastore_la_CFLAGS =	\
+	$(DEPS_CFLAGS) \
+	$(SQLITE_CFLAGS)
+
+libgrlmetadatastore_la_LIBADD =	\
+	$(DEPS_LIBS) \
+	$(SQLITE_LIBS)
+
+libgrlmetadatastore_la_LDFLAGS = \
+	-module			 \
+	-avoid-version
+
+libgrlmetadatastore_la_SOURCES = grl-metadata-store.c grl-metadata-store.h
+
+libdir=$(GRL_PLUGINS_DIR)
+metadatastorexmldir	= $(GRL_PLUGINS_CONF_DIR)
+metadatastorexml_DATA	= $(METADATA_STORE_PLUGIN_ID).xml
+
+EXTRA_DIST = $(metadatastorexml_DATA)
+
+MAINTAINERCLEANFILES	 = \
+	*.in \
+	*~
+
+DISTCLEANFILES = $(MAINTAINERCLEANFILES)
diff --git a/src/metadata/metadata-store/grl-metadata-store.c b/src/metadata/metadata-store/grl-metadata-store.c
new file mode 100644
index 0000000..f596585
--- /dev/null
+++ b/src/metadata/metadata-store/grl-metadata-store.c
@@ -0,0 +1,650 @@
+/*
+ * Copyright (C) 2010 Igalia S.L.
+ *
+ * Contact: Iago Toral Quiroga <itoral igalia 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 <grilo.h>
+#include <sqlite3.h>
+#include <string.h>
+
+#include "grl-metadata-store.h"
+
+#define GRL_METADATA_STORE_GET_PRIVATE(object)			 \
+  (G_TYPE_INSTANCE_GET_PRIVATE((object),			 \
+                               GRL_METADATA_STORE_SOURCE_TYPE,	 \
+                               GrlMetadataStorePrivate))
+
+#define GRL_LOG_DOMAIN_DEFAULT metadata_store_log_domain
+GRL_LOG_DOMAIN_STATIC(metadata_store_log_domain);
+
+#define PLUGIN_ID   METADATA_STORE_PLUGIN_ID
+
+#define SOURCE_ID   "grl-metadata-store"
+#define SOURCE_NAME "Metadata Store"
+#define SOURCE_DESC "A plugin for storing extra metadata information"
+
+#define AUTHOR      "Igalia S.L."
+#define LICENSE     "LGPL"
+#define SITE        "http://www.igalia.com";
+
+#define GRL_SQL_DB        ".grl-metadata-store"
+
+#define GRL_SQL_CREATE_TABLE_STORE			 \
+  "CREATE TABLE IF NOT EXISTS store ("			 \
+  "source_id TEXT,"					 \
+  "media_id TEXT,"					 \
+  "play_count INTEGER,"					 \
+  "rating REAL,"					 \
+  "last_position INTEGER,"				 \
+  "last_played DATE)"
+
+#define GRL_SQL_GET_METADATA				\
+  "SELECT * FROM store "				\
+  "WHERE source_id='%s' AND media_id='%s' "		\
+  "LIMIT 1"
+
+#define GRL_SQL_UPDATE_METADATA			\
+  "UPDATE store SET %s "			\
+  "WHERE source_id=? AND media_id=?"
+
+#define GRL_SQL_INSERT_METADATA			\
+  "INSERT INTO store "				\
+  "(%s source_id, media_id) VALUES "		\
+  "(%s ?, ?)"
+
+struct _GrlMetadataStorePrivate {
+  sqlite3 *db;
+};
+
+enum {
+  STORE_SOURCE_ID = 0,
+  STORE_MEDIA_ID,
+  STORE_PLAY_COUNT,
+  STORE_RATING,
+  STORE_LAST_POSITION,
+  STORE_LAST_PLAYED,
+};
+
+static GrlMetadataStoreSource *grl_metadata_store_source_new (void);
+
+static void grl_metadata_store_source_resolve (GrlMetadataSource *source,
+					       GrlMetadataSourceResolveSpec *rs);
+
+static void grl_metadata_store_source_set_metadata (GrlMetadataSource *source,
+						    GrlMetadataSourceSetMetadataSpec *sms);
+
+static const GList *grl_metadata_store_source_supported_keys (GrlMetadataSource *source);
+
+static const GList *grl_metadata_store_source_key_depends (GrlMetadataSource *source,
+							   GrlKeyID key_id);
+static const GList *grl_metadata_store_source_writable_keys (GrlMetadataSource *source);
+
+gboolean grl_metadata_store_source_plugin_init (GrlPluginRegistry *registry,
+						const GrlPluginInfo *plugin,
+						GList *configs);
+
+
+/* =================== GrlMetadataStore Plugin  =============== */
+
+gboolean
+grl_metadata_store_source_plugin_init (GrlPluginRegistry *registry,
+                                      const GrlPluginInfo *plugin,
+                                      GList *configs)
+{
+  GRL_LOG_DOMAIN_INIT (metadata_store_log_domain, "metadata-store");
+
+  GRL_DEBUG ("grl_metadata_store_source_plugin_init");
+
+  GrlMetadataStoreSource *source = grl_metadata_store_source_new ();
+  grl_plugin_registry_register_source (registry,
+                                       plugin,
+                                       GRL_MEDIA_PLUGIN (source),
+                                       NULL);
+  return TRUE;
+}
+
+GRL_PLUGIN_REGISTER (grl_metadata_store_source_plugin_init,
+                     NULL,
+                     PLUGIN_ID);
+
+/* ================== GrlMetadataStore GObject ================ */
+
+static GrlMetadataStoreSource *
+grl_metadata_store_source_new (void)
+{
+  GRL_DEBUG ("grl_metadata_store_source_new");
+  return g_object_new (GRL_METADATA_STORE_SOURCE_TYPE,
+		       "source-id", SOURCE_ID,
+		       "source-name", SOURCE_NAME,
+		       "source-desc", SOURCE_DESC,
+		       NULL);
+}
+
+static void
+grl_metadata_store_source_class_init (GrlMetadataStoreSourceClass * klass)
+{
+  GrlMetadataSourceClass *metadata_class = GRL_METADATA_SOURCE_CLASS (klass);
+  metadata_class->supported_keys = grl_metadata_store_source_supported_keys;
+  metadata_class->key_depends = grl_metadata_store_source_key_depends;
+  metadata_class->writable_keys = grl_metadata_store_source_writable_keys;
+  metadata_class->resolve = grl_metadata_store_source_resolve;
+  metadata_class->set_metadata = grl_metadata_store_source_set_metadata;
+
+  g_type_class_add_private (klass, sizeof (GrlMetadataStorePrivate));
+}
+
+static void
+grl_metadata_store_source_init (GrlMetadataStoreSource *source)
+{
+  gint r;
+  const gchar *home;
+  gchar *db_path;
+  gchar *sql_error = NULL;
+
+  source->priv = GRL_METADATA_STORE_GET_PRIVATE (source);
+
+  home = g_getenv ("HOME");
+  if (!home) {
+    GRL_WARNING ("$HOME not set, cannot open database");
+    return;
+  }
+
+  GRL_DEBUG ("Opening database connection...");
+  db_path = g_strconcat (home, G_DIR_SEPARATOR_S, GRL_SQL_DB, NULL);
+  r = sqlite3_open (db_path, &source->priv->db);
+  if (r) {
+    g_critical ("Failed to open database '%s': %s",
+		db_path, sqlite3_errmsg (source->priv->db));
+    sqlite3_close (source->priv->db);
+    return;
+  }
+  GRL_DEBUG ("  OK");
+
+  GRL_DEBUG ("Checking database tables...");
+  r = sqlite3_exec (source->priv->db, GRL_SQL_CREATE_TABLE_STORE,
+		    NULL, NULL, &sql_error);
+
+  if (r) {
+    if (sql_error) {
+      GRL_WARNING ("Failed to create database tables: %s", sql_error);
+      sqlite3_free (sql_error);
+      sql_error = NULL;
+    } else {
+      GRL_WARNING ("Failed to create database tables.");
+    }
+    sqlite3_close (source->priv->db);
+    return;
+  }
+  GRL_DEBUG ("  OK");
+
+  g_free (db_path);
+}
+
+G_DEFINE_TYPE (GrlMetadataStoreSource, grl_metadata_store_source,
+               GRL_TYPE_METADATA_SOURCE);
+
+/* ======================= Utilities ==================== */
+
+static sqlite3_stmt *
+query_metadata_store (sqlite3 *db,
+		      const gchar *source_id,
+		      const gchar *media_id)
+{
+  gint r;
+  sqlite3_stmt *sql_stmt = NULL;
+  gchar *sql;
+
+  GRL_DEBUG ("get_metadata");
+
+  sql = g_strdup_printf (GRL_SQL_GET_METADATA, source_id, media_id);
+  GRL_DEBUG ("%s", sql);
+  r = sqlite3_prepare_v2 (db, sql, strlen (sql), &sql_stmt, NULL);
+  g_free (sql);
+
+  if (r != SQLITE_OK) {
+    GRL_WARNING ("Failed to get metadata: %s", sqlite3_errmsg (db));
+    return NULL;
+  }
+
+  return sql_stmt;
+}
+
+static void
+fill_metadata (GrlMedia *media, GList *keys, sqlite3_stmt *stmt)
+{
+  GList *iter;
+  gint play_count, last_position;
+  gdouble rating;
+  gchar *last_played;
+  gint r;
+
+  while ((r = sqlite3_step (stmt)) == SQLITE_BUSY);
+
+  if (r != SQLITE_ROW) {
+    /* No info in DB for this item, bail out silently */
+    sqlite3_finalize (stmt);
+    return;
+  }
+
+  iter = keys;
+  while (iter) {
+    if (iter->data == GRL_METADATA_KEY_PLAY_COUNT) {
+      play_count = sqlite3_column_int (stmt, STORE_PLAY_COUNT);
+      grl_media_set_play_count (media, play_count);
+    } else if (iter->data == GRL_METADATA_KEY_RATING) {
+      rating = sqlite3_column_double (stmt, STORE_RATING);
+      grl_media_set_rating (media, rating, 5.00);
+    } else if (iter->data == GRL_METADATA_KEY_LAST_PLAYED) {
+      last_played = (gchar *) sqlite3_column_text (stmt, STORE_LAST_PLAYED);
+      grl_media_set_last_played (media, last_played);
+    } else if (iter->data == GRL_METADATA_KEY_LAST_POSITION) {
+      last_position = sqlite3_column_int (stmt, STORE_LAST_POSITION);
+      grl_media_set_last_position (media, last_position);
+    }
+    iter = g_list_next (iter);
+  }
+
+  sqlite3_finalize (stmt);
+}
+
+static const gchar *
+get_column_name_from_key_id (GrlKeyID key_id)
+{
+  static const gchar *col_names[] = {"rating", "last_played", "last_position",
+				     "play_count"};
+  if (key_id == GRL_METADATA_KEY_RATING) {
+    return col_names[0];
+  } else if (key_id == GRL_METADATA_KEY_LAST_PLAYED) {
+    return col_names[1];
+  } else if (key_id == GRL_METADATA_KEY_LAST_POSITION) {
+    return col_names[2];
+  } else if (key_id == GRL_METADATA_KEY_PLAY_COUNT) {
+    return col_names[3];
+  } else {
+    return NULL;
+  }
+}
+
+static gboolean
+bind_and_exec (sqlite3 *db,
+	       const gchar *sql,
+	       const gchar *source_id,
+	       const gchar *media_id,
+	       GList *col_names,
+	       GList *keys,
+	       GrlMedia *media)
+{
+  gint r;
+  const gchar *char_value;
+  gint int_value;
+  double double_value;
+  GList *iter_names, *iter_keys;
+  guint count;
+  sqlite3_stmt *stmt;
+
+  /* Create statement from sql */
+  GRL_DEBUG ("%s", sql);
+  r = sqlite3_prepare_v2 (db, sql, strlen (sql), &stmt, NULL);
+
+  if (r != SQLITE_OK) {
+    GRL_WARNING ("Failed to update metadata for '%s - %s': %s",
+                     source_id, media_id, sqlite3_errmsg (db));
+    sqlite3_finalize (stmt);
+    return FALSE;
+  }
+
+  /* Bind column values */
+  count = 1;
+  iter_names = col_names;
+  iter_keys = keys;
+  while (iter_names) {
+    if (iter_names->data) {
+      if (iter_keys->data == GRL_METADATA_KEY_RATING) {
+	double_value = grl_media_get_rating (media);
+	sqlite3_bind_double (stmt, count, double_value);
+      } else if (iter_keys->data == GRL_METADATA_KEY_PLAY_COUNT) {
+	int_value = grl_media_get_play_count (media);
+	sqlite3_bind_int (stmt, count, int_value);
+      } else if (iter_keys->data == GRL_METADATA_KEY_LAST_POSITION) {
+	int_value = grl_media_get_last_position (media);
+	sqlite3_bind_int (stmt, count, int_value);
+      } else if (iter_keys->data == GRL_METADATA_KEY_LAST_PLAYED) {
+	char_value = grl_media_get_last_played (media);
+	sqlite3_bind_text (stmt, count, char_value, -1, SQLITE_STATIC);
+      }
+      count++;
+    }
+    iter_keys = g_list_next (iter_keys);
+    iter_names = g_list_next (iter_names);
+  }
+
+  sqlite3_bind_text (stmt, count++, source_id, -1, SQLITE_STATIC);
+  sqlite3_bind_text (stmt, count++, media_id, -1, SQLITE_STATIC);
+
+  /* execute query */
+  while ((r = sqlite3_step (stmt)) == SQLITE_BUSY);
+
+  sqlite3_finalize (stmt);
+
+  return (r == SQLITE_DONE);
+}
+
+static gboolean
+prepare_and_exec_update (sqlite3 *db,
+			 const gchar *source_id,
+			 const gchar *media_id,
+			 GList *col_names,
+			 GList *keys,
+			 GrlMedia *media)
+{
+  gchar *sql;
+  gint r;
+  GList *iter_names;
+  GString *sql_buf;
+  gchar *sql_set;
+  guint count;
+
+  GRL_DEBUG ("prepare_and_exec_update");
+
+  /* Prepare sql "set" for update query */
+  count = 0;
+  sql_buf = g_string_new ("");
+  iter_names = col_names;
+  while (iter_names) {
+    gchar *col_name = (gchar *) iter_names->data;
+    if (col_name) {
+      if (count > 0) {
+	g_string_append (sql_buf, " AND ");
+      }
+      g_string_append_printf (sql_buf, "%s=?", col_name);
+      count++;
+    }
+    iter_names = g_list_next (iter_names);
+  }
+  sql_set = g_string_free (sql_buf, FALSE);
+
+  /* Execute query */
+  sql = g_strdup_printf (GRL_SQL_UPDATE_METADATA, sql_set);
+  r = bind_and_exec (db, sql, source_id, media_id, col_names, keys, media);
+  g_free (sql);
+  g_free (sql_set);
+
+  return r;
+}
+
+static gboolean
+prepare_and_exec_insert (sqlite3 *db,
+			 const gchar *source_id,
+			 const gchar *media_id,
+			 GList *col_names,
+			 GList *keys,
+			 GrlMedia *media)
+{
+  gchar *sql;
+  gint r;
+  GList *iter_names;
+  GString *sql_buf_cols, *sql_buf_values;
+  gchar *sql_cols, *sql_values;
+
+  GRL_DEBUG ("prepare_and_exec_insert");
+
+  /* Prepare sql for insert query */
+  sql_buf_cols = g_string_new ("");
+  sql_buf_values = g_string_new ("");
+  iter_names = col_names;
+  while (iter_names) {
+    gchar *col_name = (gchar *) iter_names->data;
+    if (col_name) {
+      g_string_append_printf (sql_buf_cols, "%s, ", col_name);
+      g_string_append (sql_buf_values, "?, ");
+    }
+    iter_names = g_list_next (iter_names);
+  }
+  sql_cols = g_string_free (sql_buf_cols, FALSE);
+  sql_values = g_string_free (sql_buf_values, FALSE);
+
+  /* Execute query */
+  sql = g_strdup_printf (GRL_SQL_INSERT_METADATA, sql_cols, sql_values);
+  r = bind_and_exec (db, sql, source_id, media_id, col_names, keys, media);
+  g_free (sql);
+  g_free (sql_cols);
+  g_free (sql_values);
+
+  return r;
+}
+
+static GList *
+write_keys (sqlite3 *db,
+	    const gchar *source_id,
+	    const gchar *media_id,
+	    GrlMetadataSourceSetMetadataSpec *sms,
+	    GError **error)
+{
+  GList *col_names = NULL;
+  GList *iter;
+  GList *failed_keys = NULL;
+  guint supported_keys = 0;
+  gint r;
+
+  /* Get DB column names for each key to be updated */
+  iter = sms->keys;
+  while (iter) {
+    const gchar *col_name = get_column_name_from_key_id (iter->data);
+    if (!col_name) {
+      GRL_WARNING ("Key %" GRL_KEYID_FORMAT " is not supported for "
+                       "writing, ignoring...",
+                 iter->data);
+      failed_keys = g_list_prepend (failed_keys, iter->data);
+    } else {
+      supported_keys++;
+    }
+    col_names = g_list_prepend (col_names, (gchar *) col_name);
+    iter = g_list_next (iter);
+  }
+  col_names = g_list_reverse (col_names);
+
+  if (supported_keys == 0) {
+    GRL_WARNING ("Failed to update metadata, none of the specified "
+                     "keys is writable");
+    *error = g_error_new (GRL_CORE_ERROR,
+			  GRL_CORE_ERROR_SET_METADATA_FAILED,
+			  "Failed to update metadata, "
+			  "specified keys are not writable");
+    goto done;
+  }
+
+  r = prepare_and_exec_update (db,
+			       source_id,
+			       media_id,
+			       col_names,
+			       sms->keys,
+			       sms->media);
+    
+  if (!r) {
+    GRL_WARNING ("Failed to update metadata for '%s - %s': %s",
+                     source_id, media_id, sqlite3_errmsg (db));
+    g_list_free (failed_keys);
+    failed_keys = g_list_copy (sms->keys);
+    *error = g_error_new (GRL_CORE_ERROR,
+			  GRL_CORE_ERROR_SET_METADATA_FAILED,
+			  "Failed to update metadata");
+    goto done;
+  } 
+  
+  if (sqlite3_changes (db) == 0) {
+    /* We have to create the row */
+    r = prepare_and_exec_insert (db,
+				 source_id,
+				 media_id,
+				 col_names,
+				 sms->keys,
+				 sms->media);
+  }
+
+  if (!r) {
+    GRL_WARNING ("Failed to update metadata for '%s - %s': %s",
+                     source_id, media_id, sqlite3_errmsg (db));
+    g_list_free (failed_keys);
+    failed_keys = g_list_copy (sms->keys);
+    *error = g_error_new (GRL_CORE_ERROR,
+			  GRL_CORE_ERROR_SET_METADATA_FAILED,
+			  "Failed to update metadata");
+    goto done;
+  } 
+
+ done:
+  g_list_free (col_names);
+  return failed_keys;
+}
+
+/* ================== API Implementation ================ */
+
+static const GList *
+grl_metadata_store_source_supported_keys (GrlMetadataSource *source)
+{
+  static GList *keys = NULL;
+  if (!keys) {
+    keys = grl_metadata_key_list_new (GRL_METADATA_KEY_RATING,
+                                      GRL_METADATA_KEY_PLAY_COUNT,
+                                      GRL_METADATA_KEY_LAST_PLAYED,
+                                      GRL_METADATA_KEY_LAST_POSITION,
+                                      NULL);
+  }
+  return keys;
+}
+
+static const GList *
+grl_metadata_store_source_writable_keys (GrlMetadataSource *source)
+{
+  static GList *keys = NULL;
+  if (!keys) {
+    keys = grl_metadata_key_list_new (GRL_METADATA_KEY_RATING,
+                                      GRL_METADATA_KEY_PLAY_COUNT,
+                                      GRL_METADATA_KEY_LAST_PLAYED,
+                                      GRL_METADATA_KEY_LAST_POSITION,
+                                      NULL);
+  }
+  return keys;
+}
+
+static const GList *
+grl_metadata_store_source_key_depends (GrlMetadataSource *source,
+				       GrlKeyID key_id)
+{
+  static GList *deps = NULL;
+  if (!deps) {
+    deps = grl_metadata_key_list_new (GRL_METADATA_KEY_ID, NULL);
+  }
+
+  if (key_id == GRL_METADATA_KEY_RATING ||
+      key_id == GRL_METADATA_KEY_PLAY_COUNT ||
+      key_id == GRL_METADATA_KEY_LAST_PLAYED ||
+      key_id == GRL_METADATA_KEY_LAST_POSITION) {
+    return deps;
+  }
+
+  return NULL;
+}
+
+static void
+grl_metadata_store_source_resolve (GrlMetadataSource *source,
+				   GrlMetadataSourceResolveSpec *rs)
+{
+  GRL_DEBUG ("grl_metadata_store_source_resolve");
+
+  const gchar *source_id, *media_id;
+  sqlite3_stmt *stmt;
+  GError *error = NULL;
+
+  source_id = grl_media_get_source (rs->media);
+  media_id = grl_media_get_id (rs->media);
+
+  /* We need the source id */
+  if (!source_id) {
+    GRL_WARNING ("Failed to resolve metadata: source-id not available");
+    error = g_error_new (GRL_CORE_ERROR,
+			 GRL_CORE_ERROR_RESOLVE_FAILED,
+			 "source-id not available, cannot resolve metadata.");
+    rs->callback (rs->source, rs->media, rs->user_data, error);
+    g_error_free (error);
+    return;
+  }
+
+  /* Special case for root categories */
+  if (!media_id) {
+    media_id = "";
+  }
+
+  stmt = query_metadata_store (GRL_METADATA_STORE_SOURCE (source)->priv->db,
+			       source_id, media_id);
+  if (stmt) {
+    fill_metadata (rs->media, rs->keys, stmt);
+    rs->callback (rs->source, rs->media, rs->user_data, NULL);
+  } else {
+    GRL_WARNING ("Failed to resolve metadata");
+    error = g_error_new (GRL_CORE_ERROR,
+			 GRL_CORE_ERROR_RESOLVE_FAILED,
+			 "Failed to resolve metadata.");
+    rs->callback (rs->source, rs->media, rs->user_data, error);
+    g_error_free (error);
+  }
+}
+
+static void
+grl_metadata_store_source_set_metadata (GrlMetadataSource *source,
+					GrlMetadataSourceSetMetadataSpec *sms)
+{
+  GRL_DEBUG ("grl_metadata_store_source_set_metadata");
+
+  const gchar *media_id, *source_id;
+  GError *error = NULL;
+  GList *failed_keys = NULL;
+
+  source_id = grl_media_get_source (sms->media);
+  media_id = grl_media_get_id (sms->media);
+
+  /* We need the source id */
+  if (!source_id) {
+    GRL_WARNING ("Failed to update metadata: source-id not available");
+    error = g_error_new (GRL_CORE_ERROR,
+			 GRL_CORE_ERROR_SET_METADATA_FAILED,
+			 "source-id not available, cannot update metadata.");
+    failed_keys = g_list_copy (sms->keys);
+  } else {
+    /* Special case for root categories */
+    if (!media_id) {
+      media_id = "";
+    }
+    
+    failed_keys = write_keys (GRL_METADATA_STORE_SOURCE (source)->priv->db,
+			      source_id, media_id, sms, &error);
+  }
+
+  sms->callback (sms->source, sms->media, failed_keys, sms->user_data, error);
+
+  if (error) {
+    g_error_free (error);
+  }
+  g_list_free (failed_keys);
+}
diff --git a/src/metadata/metadata-store/grl-metadata-store.h b/src/metadata/metadata-store/grl-metadata-store.h
new file mode 100644
index 0000000..47df494
--- /dev/null
+++ b/src/metadata/metadata-store/grl-metadata-store.h
@@ -0,0 +1,75 @@
+/*
+ * Copyright (C) 2010 Igalia S.L.
+ *
+ * Contact: Iago Toral Quiroga <itoral igalia 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_METADATA_STORE_SOURCE_H_
+#define _GRL_METADATA_STORE_SOURCE_H_
+
+#include <grilo.h>
+
+#define GRL_METADATA_STORE_SOURCE_TYPE           \
+  (grl_metadata_store_source_get_type ())
+
+#define GRL_METADATA_STORE_SOURCE(obj)                           \
+  (G_TYPE_CHECK_INSTANCE_CAST ((obj),                           \
+                               GRL_METADATA_STORE_SOURCE_TYPE,   \
+                               GrlMetadataStoreSource))
+
+#define GRL_IS_METADATA_STORE_SOURCE(obj)                        \
+  (G_TYPE_CHECK_INSTANCE_TYPE ((obj),                           \
+                               GRL_METADATA_STORE_SOURCE_TYPE))
+
+#define GRL_METADATA_STORE_SOURCE_CLASS(klass)                   \
+  (G_TYPE_CHECK_CLASS_CAST((klass),				 \
+                           GRL_METADATA_STORE_SOURCE_TYPE,       \
+                           GrlMetadataStoreSourceClass))
+
+#define GRL_IS_METADATA_STORE_SOURCE_CLASS(klass)		\
+  (G_TYPE_CHECK_CLASS_TYPE((klass),                             \
+                           GRL_METADATA_STORE_SOURCE_TYPE))
+
+#define GRL_METADATA_STORE_SOURCE_GET_CLASS(obj)                 \
+  (G_TYPE_INSTANCE_GET_CLASS ((obj),				 \
+                              GRL_METADATA_STORE_SOURCE_TYPE,    \
+                              GrlMetadataStoreSourceClass))
+
+typedef struct _GrlMetadataStorePrivate GrlMetadataStorePrivate;
+typedef struct _GrlMetadataStoreSource GrlMetadataStoreSource;
+
+struct _GrlMetadataStoreSource {
+
+  GrlMetadataSource parent;
+
+  /*< private >*/
+  GrlMetadataStorePrivate *priv;
+};
+
+typedef struct _GrlMetadataStoreSourceClass GrlMetadataStoreSourceClass;
+
+struct _GrlMetadataStoreSourceClass {
+
+  GrlMetadataSourceClass parent_class;
+
+};
+
+GType grl_metadata_store_source_get_type (void);
+
+#endif /* _GRL_METADATA_STORE_SOURCE_H_ */
diff --git a/src/metadata/metadata-store/grl-metadata-store.xml b/src/metadata/metadata-store/grl-metadata-store.xml
new file mode 100644
index 0000000..b8bbbb8
--- /dev/null
+++ b/src/metadata/metadata-store/grl-metadata-store.xml
@@ -0,0 +1,9 @@
+<plugin>
+  <info>
+    <name>Metadata Store</name>
+    <description>A plugin for storing extra metadata information</description>
+    <author>Igalia S.L.</author>
+    <license>LGPL</license>
+    <site>http://www.igalia.com</site>
+  </info>
+</plugin>
diff --git a/src/podcasts/Makefile.am b/src/podcasts/Makefile.am
deleted file mode 100644
index 2383042..0000000
--- a/src/podcasts/Makefile.am
+++ /dev/null
@@ -1,38 +0,0 @@
-#
-# Makefile.am
-#
-# Author: Iago Toral Quiroga <itoral igalia com>
-#
-# Copyright (C) 2010, 2011 Igalia S.L. All rights reserved.
-
-lib_LTLIBRARIES = libgrlpodcasts.la
-
-libgrlpodcasts_la_CFLAGS =	\
-	$(DEPS_CFLAGS)		\
-	$(GRLNET_CFLAGS)	\
-	$(XML_CFLAGS)		\
-	$(SQLITE_CFLAGS)
-
-libgrlpodcasts_la_LIBADD =	\
-	$(DEPS_LIBS)		\
-	$(GRLNET_LIBS)		\
-	$(XML_LIBS)		\
-	$(SQLITE_LIBS)
-
-libgrlpodcasts_la_LDFLAGS = \
-	-module		    \
-	-avoid-version
-
-libgrlpodcasts_la_SOURCES = grl-podcasts.c grl-podcasts.h
-
-libdir = $(GRL_PLUGINS_DIR)
-podcastsxmldir		= $(GRL_PLUGINS_CONF_DIR)
-podcastsxml_DATA	= $(PODCASTS_PLUGIN_ID).xml
-
-EXTRA_DIST = $(podcastsxml_DATA)
-
-MAINTAINERCLEANFILES =	\
-	*.in		\
-	*~
-
-DISTCLEANFILES = $(MAINTAINERCLEANFILES)
diff --git a/src/podcasts/TODO b/src/podcasts/TODO
deleted file mode 100644
index 12f3311..0000000
--- a/src/podcasts/TODO
+++ /dev/null
@@ -1,5 +0,0 @@
-- IGN podcasts contains items with no enclosure. These should be skipped.
-- Support more tags (like author, site, etc)
-- Parsing and inserting in the DB seems to block long enough, split the parsing to handle only
-  one element in each go.
-- Implement support for remove() -- Needs support in core first
diff --git a/src/podcasts/grl-podcasts.c b/src/podcasts/grl-podcasts.c
deleted file mode 100644
index a4d5ad3..0000000
--- a/src/podcasts/grl-podcasts.c
+++ /dev/null
@@ -1,1636 +0,0 @@
-/*
- * Copyright (C) 2010, 2011 Igalia S.L.
- *
- * Contact: Iago Toral Quiroga <itoral igalia 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 <grilo.h>
-#include <net/grl-net.h>
-#include <libxml/xpath.h>
-#include <sqlite3.h>
-#include <string.h>
-
-#include "grl-podcasts.h"
-
-#define GRL_PODCASTS_GET_PRIVATE(object)                        \
-  (G_TYPE_INSTANCE_GET_PRIVATE((object),                        \
-                               GRL_PODCASTS_SOURCE_TYPE,        \
-                               GrlPodcastsPrivate))
-
-#define GRL_ROOT_TITLE "Podcasts"
-
-/* --------- Logging  -------- */
-
-#define GRL_LOG_DOMAIN_DEFAULT podcasts_log_domain
-GRL_LOG_DOMAIN_STATIC(podcasts_log_domain);
-
-/* --- Database --- */
-
-#define GRL_SQL_DB        ".grl-podcasts"
-
-#define GRL_SQL_CREATE_TABLE_PODCASTS           \
-  "CREATE TABLE IF NOT EXISTS podcasts ("       \
-  "id    INTEGER  PRIMARY KEY AUTOINCREMENT,"   \
-  "title TEXT,"                                 \
-  "url   TEXT,"                                 \
-  "desc  TEXT,"                                 \
-  "last_refreshed DATE)"
-
-#define GRL_SQL_CREATE_TABLE_STREAMS		 \
-  "CREATE TABLE IF NOT EXISTS streams ( "        \
-  "podcast INTEGER REFERENCES podcasts (id), "   \
-  "url     TEXT, "				 \
-  "title   TEXT, "                               \
-  "length  INTEGER, "                            \
-  "mime    TEXT, "                               \
-  "date    TEXT, "                               \
-  "desc    TEXT)"
-
-#define GRL_SQL_GET_PODCASTS				\
-  "SELECT p.*, count(s.podcast <> '') "			\
-  "FROM podcasts p LEFT OUTER JOIN streams s "		\
-  "  ON p.id = s.podcast "				\
-  "GROUP BY p.id "					\
-  "LIMIT %u OFFSET %u"
-
-#define GRL_SQL_GET_PODCASTS_BY_QUERY				\
-  "SELECT p.*, count(s.podcast <> '') "				\
-  "FROM podcasts p LEFT OUTER JOIN streams s "			\
-  "  ON p.id = s.podcast "					\
-  "WHERE %s "							\
-  "GROUP BY p.id "						\
-  "LIMIT %u OFFSET %u"
-
-#define GRL_SQL_GET_PODCAST_BY_ID               \
-  "SELECT * FROM podcasts "                     \
-  "WHERE id='%s' "                              \
-  "LIMIT 1"
-
-#define GRL_SQL_STORE_PODCAST                   \
-  "INSERT INTO podcasts "                       \
-  "(url, title, desc) "                         \
-  "VALUES (?, ?, ?)"
-
-#define GRL_SQL_REMOVE_PODCAST                  \
-  "DELETE FROM podcasts "                       \
-  "WHERE id='%s'"
-
-#define GRL_SQL_REMOVE_STREAM                   \
-  "DELETE FROM streams "                        \
-  "WHERE url='%s'"
-
-#define GRL_SQL_STORE_STREAM                            \
-  "INSERT INTO streams "                                \
-  "(podcast, url, title, length, mime, date, desc) "    \
-  "VALUES (?, ?, ?, ?, ?, ?, ?)"
-
-#define GRL_SQL_DELETE_PODCAST_STREAMS          \
-  "DELETE FROM streams WHERE podcast='%s'"
-
-#define GRL_SQL_GET_PODCAST_STREAMS             \
-  "SELECT * FROM streams "                      \
-  "WHERE podcast='%s' "                         \
-  "LIMIT %u  OFFSET %u"
-
-#define GRL_SQL_GET_PODCAST_STREAMS_BY_TEXT                     \
-  "SELECT s.* "                                                 \
-  "FROM streams s LEFT OUTER JOIN podcasts p "			\
-  "  ON s.podcast = p.id "					\
-  "WHERE s.title LIKE '%%%s%%' OR s.desc LIKE '%%%s%%' "	\
-  "  OR p.title LIKE '%%%s%%' OR p.desc LIKE '%%%s%%' "         \
-  "LIMIT %u OFFSET %u"
-
-#define GRL_SQL_GET_PODCAST_STREAMS_ALL         \
-  "SELECT * FROM streams "                      \
-  "LIMIT %u OFFSET %u"
-
-#define GRL_SQL_GET_PODCAST_STREAM              \
-  "SELECT * FROM streams "                      \
-  "WHERE url='%s' "                             \
-  "LIMIT 1"
-
-#define GRL_SQL_TOUCH_PODCAST			\
-  "UPDATE podcasts "				\
-  "SET last_refreshed='%s' "			\
-  "WHERE id='%s'"
-
-/* --- Other --- */
-
-#define CACHE_DURATION (24 * 60 * 60)
-
-/* --- Plugin information --- */
-
-#define PLUGIN_ID   PODCASTS_PLUGIN_ID
-
-#define SOURCE_ID   "grl-podcasts"
-#define SOURCE_NAME "Podcasts"
-#define SOURCE_DESC "A source for browsing podcasts"
-
-#define AUTHOR      "Igalia S.L."
-#define LICENSE     "LGPL"
-#define SITE        "http://www.igalia.com";
-
-enum {
-  PODCAST_ID = 0,
-  PODCAST_TITLE,
-  PODCAST_URL,
-  PODCAST_DESC,
-  PODCAST_LAST_REFRESHED,
-  PODCAST_LAST,
-};
-
-enum {
-  STREAM_PODCAST = 0,
-  STREAM_URL,
-  STREAM_TITLE,
-  STREAM_LENGTH,
-  STREAM_MIME,
-  STREAM_DATE,
-  STREAM_DESC,
-};
-
-typedef void (*AsyncReadCbFunc) (gchar *data, gpointer user_data);
-
-typedef struct {
-  AsyncReadCbFunc callback;
-  gchar *url;
-  gpointer user_data;
-} AsyncReadCb;
-
-typedef struct {
-  gchar *id;
-  gchar *url;
-  gchar *title;
-  gchar *published;
-  gchar *duration;
-  gchar *summary;
-  gchar *mime;
-} Entry;
-
-struct _GrlPodcastsPrivate {
-  sqlite3 *db;
-  GrlNetWc *wc;
-  gboolean notify_changes;
-};
-
-typedef struct {
-  GrlMediaSource *source;
-  guint operation_id;
-  const gchar *media_id;
-  guint skip;
-  guint count;
-  const gchar *text;
-  GrlMediaSourceResultCb callback;
-  guint error_code;
-  gboolean is_query;
-  gpointer user_data;
-} OperationSpec;
-
-typedef struct {
-  OperationSpec *os;
-  xmlDocPtr doc;
-  xmlXPathContextPtr xpathCtx;
-  xmlXPathObjectPtr xpathObj;
-  guint parse_count;
-  guint parse_index;
-  guint parse_valid_index;
-  GrlMedia *last_media;
-} OperationSpecParse;
-
-static GrlPodcastsSource *grl_podcasts_source_new (void);
-
-static void grl_podcasts_source_finalize (GObject *plugin);
-
-static const GList *grl_podcasts_source_supported_keys (GrlMetadataSource *source);
-
-static void grl_podcasts_source_browse (GrlMediaSource *source,
-                                        GrlMediaSourceBrowseSpec *bs);
-static void grl_podcasts_source_search (GrlMediaSource *source,
-                                        GrlMediaSourceSearchSpec *ss);
-static void grl_podcasts_source_query (GrlMediaSource *source,
-                                       GrlMediaSourceQuerySpec *qs);
-static void grl_podcasts_source_metadata (GrlMediaSource *source,
-                                          GrlMediaSourceMetadataSpec *ms);
-static void grl_podcasts_source_store (GrlMediaSource *source,
-                                       GrlMediaSourceStoreSpec *ss);
-static void grl_podcasts_source_remove (GrlMediaSource *source,
-                                        GrlMediaSourceRemoveSpec *rs);
-static gboolean grl_podcasts_source_notify_change_start (GrlMediaSource *source,
-                                                         GError **error);
-static gboolean grl_podcasts_source_notify_change_stop (GrlMediaSource *source,
-                                                        GError **error);
-
-/* =================== Podcasts Plugin  =============== */
-
-static gboolean
-grl_podcasts_plugin_init (GrlPluginRegistry *registry,
-                          const GrlPluginInfo *plugin,
-                          GList *configs)
-{
-  GRL_LOG_DOMAIN_INIT (podcasts_log_domain, "podcasts");
-
-  GRL_DEBUG ("podcasts_plugin_init");
-
-  GrlPodcastsSource *source = grl_podcasts_source_new ();
-  grl_plugin_registry_register_source (registry,
-                                       plugin,
-                                       GRL_MEDIA_PLUGIN (source),
-                                       NULL);
-  return TRUE;
-}
-
-GRL_PLUGIN_REGISTER (grl_podcasts_plugin_init,
-                     NULL,
-                     PLUGIN_ID);
-
-/* ================== Podcasts GObject ================ */
-
-static GrlPodcastsSource *
-grl_podcasts_source_new (void)
-{
-  GRL_DEBUG ("grl_podcasts_source_new");
-  return g_object_new (GRL_PODCASTS_SOURCE_TYPE,
-		       "source-id", SOURCE_ID,
-		       "source-name", SOURCE_NAME,
-		       "source-desc", SOURCE_DESC,
-		       NULL);
-}
-
-static void
-grl_podcasts_source_class_init (GrlPodcastsSourceClass * klass)
-{
-  GObjectClass *gobject_class = G_OBJECT_CLASS (klass);
-  GrlMediaSourceClass *source_class = GRL_MEDIA_SOURCE_CLASS (klass);
-  GrlMetadataSourceClass *metadata_class = GRL_METADATA_SOURCE_CLASS (klass);
-
-  gobject_class->finalize = grl_podcasts_source_finalize;
-
-  source_class->browse = grl_podcasts_source_browse;
-  source_class->search = grl_podcasts_source_search;
-  source_class->query = grl_podcasts_source_query;
-  source_class->metadata = grl_podcasts_source_metadata;
-  source_class->store = grl_podcasts_source_store;
-  source_class->remove = grl_podcasts_source_remove;
-  source_class->notify_change_start = grl_podcasts_source_notify_change_start;
-  source_class->notify_change_stop = grl_podcasts_source_notify_change_stop;
-
-  metadata_class->supported_keys = grl_podcasts_source_supported_keys;
-
-  g_type_class_add_private (klass, sizeof (GrlPodcastsPrivate));
-}
-
-static void
-grl_podcasts_source_init (GrlPodcastsSource *source)
-{
-  gint r;
-  const gchar *home;
-  gchar *db_path;
-  gchar *sql_error = NULL;
-
-  source->priv = GRL_PODCASTS_GET_PRIVATE (source);
-
-  home = g_getenv ("HOME");
-  if (!home) {
-    GRL_WARNING ("$HOME not set, cannot open database");
-    return;
-  }
-
-  GRL_DEBUG ("Opening database connection...");
-  db_path = g_strconcat (home, G_DIR_SEPARATOR_S, GRL_SQL_DB, NULL);
-  r = sqlite3_open (db_path, &source->priv->db);
-  if (r) {
-    g_critical ("Failed to open database '%s': %s",
-		db_path, sqlite3_errmsg (source->priv->db));
-    sqlite3_close (source->priv->db);
-    return;
-  }
-  GRL_DEBUG ("  OK");
-
-  GRL_DEBUG ("Checking database tables...");
-  r = sqlite3_exec (source->priv->db, GRL_SQL_CREATE_TABLE_PODCASTS,
-		    NULL, NULL, &sql_error);
-
-  if (!r) {
-    /* TODO: if this fails, sqlite stays in an unreliable state fix that */
-    r = sqlite3_exec (source->priv->db, GRL_SQL_CREATE_TABLE_STREAMS,
-		      NULL, NULL, &sql_error);
-  }
-  if (r) {
-    if (sql_error) {
-      GRL_WARNING ("Failed to create database tables: %s", sql_error);
-      sqlite3_free (sql_error);
-      sql_error = NULL;
-    } else {
-      GRL_WARNING ("Failed to create database tables.");
-    }
-    sqlite3_close (source->priv->db);
-    return;
-  }
-  GRL_DEBUG ("  OK");
-
-  g_free (db_path);
-}
-
-G_DEFINE_TYPE (GrlPodcastsSource, grl_podcasts_source, GRL_TYPE_MEDIA_SOURCE);
-
-static void
-grl_podcasts_source_finalize (GObject *object)
-{
-  GrlPodcastsSource *source;
-
-  GRL_DEBUG ("grl_podcasts_source_finalize");
-
-  source = GRL_PODCASTS_SOURCE (object);
-
-  if (source->priv->wc)
-    g_object_unref (source->priv->wc);
-
-  sqlite3_close (source->priv->db);
-
-  G_OBJECT_CLASS (grl_podcasts_source_parent_class)->finalize (object);
-}
-
-/* ======================= Utilities ==================== */
-
-static void
-print_entry (Entry *entry)
-{
-  g_print ("Entry Information:\n");
-  g_print ("            ID: %s\n", entry->id);
-  g_print ("         Title: %s\n", entry->title);
-  g_print ("          Date: %s\n", entry->published);
-  g_print ("      Duration: %s\n", entry->duration);
-  g_print ("       Summary: %s\n", entry->summary);
-  g_print ("           URL: %s\n", entry->url);
-  g_print ("          Mime: %s\n", entry->mime);
-}
-
-static void
-free_entry (Entry *entry)
-{
-  g_free (entry->id);
-  g_free (entry->title);
-  g_free (entry->published);
-  g_free (entry->summary);
-  g_free (entry->url);
-  g_free (entry->mime);
-}
-
-static void
-read_done_cb (GObject *source_object,
-              GAsyncResult *res,
-              gpointer user_data)
-{
-  AsyncReadCb *arc = (AsyncReadCb *) user_data;
-  GError *wc_error = NULL;
-  gchar *content = NULL;
-
-  GRL_DEBUG ("  Done");
-
-  grl_net_wc_request_finish (GRL_NET_WC (source_object),
-                         res,
-                         &content,
-                         NULL,
-                         &wc_error);
-  if (wc_error) {
-    GRL_WARNING ("Failed to open '%s': %s", arc->url, wc_error->message);
-    g_error_free (wc_error);
-  } else {
-    arc->callback (content, arc->user_data);
-  }
-  g_free (arc->url);
-  g_slice_free (AsyncReadCb, arc);
-}
-
-static void
-read_url_async (GrlPodcastsSource *source,
-                const gchar *url,
-                AsyncReadCbFunc callback,
-                gpointer user_data)
-{
-  AsyncReadCb *arc;
-
-  GRL_DEBUG ("Opening async '%s'", url);
-
-  arc = g_slice_new0 (AsyncReadCb);
-  arc->url = g_strdup (url);
-  arc->callback = callback;
-  arc->user_data = user_data;
-
-  /* We would need a different Wc if we change of URL.
-   * In this case, as we don't know the previous URL,
-   * we ditch the Wc and create another. It's cheap.
-   */
-  if (!source->priv->wc)
-    g_object_unref (source->priv->wc);
-
-  source->priv->wc = grl_net_wc_new ();
-  grl_net_wc_request_async (source->priv->wc, url, NULL, read_done_cb, arc);
-}
-
-static gint
-duration_to_seconds (const gchar *str)
-{
-  gint seconds = 0;
-  gchar **parts;
-  gint i;
-  guint multiplier = 1;
-
-  if (!str || str[0] == '\0') {
-    return 0;
-  }
-
-  parts = g_strsplit (str, ":", 3);
-
-  /* Get last portion (seconds) */
-  i = 0;
-  while (parts[i]) i++;
-  if (i == 0) {
-    g_strfreev (parts);
-    return 0;
-  } else {
-    i--;
-  }
-
-  do {
-    seconds += atoi (parts[i]) * multiplier;
-    multiplier *= 60;
-    i--;
-  } while (i >= 0);
-
-  g_strfreev (parts);
-
-  return seconds;
-}
-
-static gboolean
-mime_is_video (const gchar *mime)
-{
-  return mime && strstr (mime, "video") != NULL;
-}
-
-static gboolean
-mime_is_audio (const gchar *mime)
-{
-  return mime && strstr (mime, "audio") != NULL;
-}
-
-static gchar *
-get_site_from_url (const gchar *url)
-{
-  gchar *p;
-
-  if (g_str_has_prefix (url, "file://")) {
-    return NULL;
-  }
-
-  p = strstr (url, "://");
-  if (!p) {
-    return NULL;
-  } else {
-    p += 3;
-  }
-
-  while (*p != '/') p++;
-
-  return g_strndup (url, p - url);
-}
-
-static GrlMedia *
-build_media (GrlMedia *content,
-	     gboolean is_podcast,
-	     const gchar *id,
-	     const gchar *title,
-	     const gchar *url,
-	     const gchar *desc,
-	     const gchar *mime,
-	     const gchar *date,
-	     guint duration,
-	     guint childcount)
-{
-  GrlMedia *media = NULL;
-  gchar *site;
-
-  if (content) {
-    media = content;
-  }
-
-  if (is_podcast) {
-    if (!media) {
-      media = GRL_MEDIA (grl_media_box_new ());
-    }
-
-    grl_media_set_id (media, id);
-    if (desc)
-      grl_media_set_description (media, desc);
-    grl_media_box_set_childcount (GRL_MEDIA_BOX (media), childcount);
-  } else {
-    if (!media) {
-      if (mime_is_audio (mime)) {
-	media = grl_media_audio_new ();
-      } else if (mime_is_video (mime)) {
-	media = grl_media_video_new ();
-      } else {
-	media = grl_media_new ();
-      }
-    }
-
-    grl_media_set_id (media, url);
-    if (date)
-      grl_media_set_date (media, date);
-    if (desc)
-      grl_media_set_description (media, desc);
-    if (mime)
-      grl_media_set_mime (media, mime);
-    if (duration > 0) {
-      grl_media_set_duration (media, duration);
-    }
-  }
-
-  grl_media_set_title (media, title);
-  grl_media_set_url (media, url);
-
-  site = get_site_from_url (url);
-  if (site) {
-    grl_media_set_site (media, site);
-    g_free (site);
-  }
-
-  return media;
-}
-
-static GrlMedia *
-build_media_from_entry (Entry *entry)
-{
-  GrlMedia *media;
-  gint duration;
-
-  duration = duration_to_seconds (entry->duration);
-  media = build_media (NULL, FALSE,
-		       entry->url, entry->title, entry->url,
-		       entry->summary, entry->mime, entry->published,
-		       duration, 0);
-  return media;
-}
-
-static GrlMedia *
-build_media_from_stmt (GrlMedia *content,
-		       sqlite3_stmt *sql_stmt,
-		       gboolean is_podcast)
-{
-  GrlMedia *media;
-  gchar *id;
-  gchar *title;
-  gchar *url;
-  gchar *desc;
-  gchar *mime;
-  gchar *date;
-  guint duration;
-  guint childcount;
-
-  if (is_podcast) {
-    id = (gchar *) sqlite3_column_text (sql_stmt, PODCAST_ID);
-    title = (gchar *) sqlite3_column_text (sql_stmt, PODCAST_TITLE);
-    url = (gchar *) sqlite3_column_text (sql_stmt, PODCAST_URL);
-    desc = (gchar *) sqlite3_column_text (sql_stmt, PODCAST_DESC);
-    childcount = (guint) sqlite3_column_int (sql_stmt, PODCAST_LAST);
-    media = build_media (content, is_podcast,
-			 id, title, url, desc, NULL, NULL, 0, childcount);
-  } else {
-    mime = (gchar *) sqlite3_column_text (sql_stmt, STREAM_MIME);
-    url = (gchar *) sqlite3_column_text (sql_stmt, STREAM_URL);
-    title = (gchar *) sqlite3_column_text (sql_stmt, STREAM_TITLE);
-    date = (gchar *) sqlite3_column_text (sql_stmt, STREAM_DATE);
-    desc = (gchar *) sqlite3_column_text (sql_stmt, STREAM_DESC);
-    duration = sqlite3_column_int (sql_stmt, STREAM_LENGTH);
-    media = build_media (content, is_podcast,
-			 url, title, url, desc, mime, date, duration, 0);
-  }
-
-  return media;
-}
-
-static void
-produce_podcast_contents_from_db (OperationSpec *os)
-{
-  sqlite3 *db;
-  gchar *sql;
-  sqlite3_stmt *sql_stmt = NULL;
-  GList *iter, *medias = NULL;
-  guint count = 0;
-  GrlMedia *media;
-  gint r;
-  GError *error = NULL;
-
-  GRL_DEBUG ("produce_podcast_contents_from_db");
-
-  db = GRL_PODCASTS_SOURCE (os->source)->priv->db;
-  /* Check if searching or browsing */
-  if (os->is_query) {
-    if (os->text) {
-      /* Search text */
-      sql = g_strdup_printf (GRL_SQL_GET_PODCAST_STREAMS_BY_TEXT,
-                             os->text, os->text, os->text, os->text,
-                             os->count, os->skip);
-    } else {
-      /* Return all */
-      sql = g_strdup_printf (GRL_SQL_GET_PODCAST_STREAMS_ALL,
-                             os->count, os->skip);
-    }
-  } else {
-    sql = g_strdup_printf (GRL_SQL_GET_PODCAST_STREAMS,
-                           os->media_id, os->count, os->skip);
-  }
-  GRL_DEBUG ("%s", sql);
-  r = sqlite3_prepare_v2 (db, sql, strlen (sql), &sql_stmt, NULL);
-  g_free (sql);
-
-  if (r != SQLITE_OK) {
-    GRL_WARNING ("Failed to retrieve podcast streams: %s", sqlite3_errmsg (db));
-    error = g_error_new (GRL_CORE_ERROR,
-			 os->error_code,
-			 "Failed to retrieve podcast streams");
-    os->callback (os->source, os->operation_id, NULL, 0, os->user_data, error);
-    g_error_free (error);
-    return;
-  }
-
-  while ((r = sqlite3_step (sql_stmt)) == SQLITE_BUSY);
-
-  while (r == SQLITE_ROW) {
-    media = build_media_from_stmt (NULL, sql_stmt, FALSE);
-    medias = g_list_prepend (medias, media);
-    count++;
-    r = sqlite3_step (sql_stmt);
-  }
-
-  if (r != SQLITE_DONE) {
-    GRL_WARNING ("Failed to retrive podcast streams: %s", sqlite3_errmsg (db));
-    error = g_error_new (GRL_CORE_ERROR,
-			 os->error_code,
-			 "Failed to retrieve podcast streams");
-    os->callback (os->source, os->operation_id, NULL, 0, os->user_data, error);
-    g_error_free (error);
-    sqlite3_finalize (sql_stmt);
-    return;
-  }
-
-  sqlite3_finalize (sql_stmt);
-
-  if (count > 0) {
-    medias = g_list_reverse (medias);
-    iter = medias;
-    while (iter) {
-      media = GRL_MEDIA (iter->data);
-      os->callback (os->source,
-		    os->operation_id,
-		    media,
-		    --count,
-		    os->user_data,
-		    NULL);
-      iter = g_list_next (iter);
-    }
-    g_list_free (medias);
-  } else {
-    os->callback (os->source, os->operation_id, NULL, 0, os->user_data, NULL);
-  }
-}
-
-static void
-remove_podcast_streams (sqlite3 *db, const gchar *podcast_id, GError **error)
-{
-  gchar *sql;
-  gchar *sql_error;
-  gint r;
-
-  sql = g_strdup_printf (GRL_SQL_DELETE_PODCAST_STREAMS, podcast_id);
-  GRL_DEBUG ("%s", sql);
-  r = sqlite3_exec (db, sql, NULL, NULL, &sql_error);
-  g_free (sql);
-  if (r) {
-    GRL_WARNING ("Failed to remove podcast streams cache: %s", sql_error);
-    *error = g_error_new (GRL_CORE_ERROR,
-			  GRL_CORE_ERROR_REMOVE_FAILED,
-			  "Failed to remove podcast streams");
-    sqlite3_free (error);
-  }
-}
-
-static void
-remove_podcast (GrlPodcastsSource *podcasts_source,
-                const gchar *podcast_id,
-                GError **error)
-{
-  gint r;
-  gchar *sql_error;
-  gchar *sql;
-
-  GRL_DEBUG ("remove_podcast");
-
-  remove_podcast_streams (podcasts_source->priv->db, podcast_id, error);
-  if (*error) {
-    return;
-  }
-
-  sql = g_strdup_printf (GRL_SQL_REMOVE_PODCAST, podcast_id);
-  GRL_DEBUG ("%s", sql);
-  r = sqlite3_exec (podcasts_source->priv->db, sql, NULL, NULL, &sql_error);
-  g_free (sql);
-
-  if (r != SQLITE_OK) {
-    GRL_WARNING ("Failed to remove podcast '%s': %s", podcast_id, sql_error);
-    g_set_error_literal (error,
-                         GRL_CORE_ERROR,
-                         GRL_CORE_ERROR_REMOVE_FAILED,
-                         "Failed to remove podcast");
-    sqlite3_free (sql_error);
-  } else if (podcasts_source->priv->notify_changes) {
-    grl_media_source_notify_change (GRL_MEDIA_SOURCE (podcasts_source),
-                                    NULL,
-                                    GRL_CONTENT_REMOVED,
-                                    TRUE);
-  }
-}
-
-static void
-remove_stream (GrlPodcastsSource *podcasts_source,
-               const gchar *url,
-               GError **error)
-{
-  gint r;
-  gchar *sql_error;
-  gchar *sql;
-
-  GRL_DEBUG ("remove_stream");
-
-  sql = g_strdup_printf (GRL_SQL_REMOVE_STREAM, url);
-  GRL_DEBUG ("%s", sql);
-  r = sqlite3_exec (podcasts_source->priv->db, sql, NULL, NULL, &sql_error);
-  g_free (sql);
-
-  if (r != SQLITE_OK) {
-    GRL_WARNING ("Failed to remove podcast stream '%s': %s", url, sql_error);
-    g_set_error_literal (error,
-                         GRL_CORE_ERROR,
-                         GRL_CORE_ERROR_REMOVE_FAILED,
-                         "Failed to remove podcast stream");
-    sqlite3_free (sql_error);
-  } else if (podcasts_source->priv->notify_changes) {
-    grl_media_source_notify_change (GRL_MEDIA_SOURCE (podcasts_source),
-                                    NULL,
-                                    GRL_CONTENT_REMOVED,
-                                    TRUE);
-  }
-}
-
-static void
-store_podcast (GrlPodcastsSource *podcasts_source,
-               GrlMedia *podcast,
-               GError **error)
-{
-  gint r;
-  sqlite3_stmt *sql_stmt = NULL;
-  const gchar *title;
-  const gchar *url;
-  const gchar *desc;
-  gchar *id;
-
-  GRL_DEBUG ("store_podcast");
-
-  title = grl_media_get_title (podcast);
-  url = grl_media_get_url (podcast);
-  desc = grl_media_get_description (podcast);
-
-  GRL_DEBUG ("%s", GRL_SQL_STORE_PODCAST);
-  r = sqlite3_prepare_v2 (podcasts_source->priv->db,
-			  GRL_SQL_STORE_PODCAST,
-			  strlen (GRL_SQL_STORE_PODCAST),
-			  &sql_stmt, NULL);
-  if (r != SQLITE_OK) {
-    GRL_WARNING ("Failed to store podcast '%s': %s", title,
-                 sqlite3_errmsg (podcasts_source->priv->db));
-    g_set_error (error,
-                 GRL_CORE_ERROR,
-                 GRL_CORE_ERROR_STORE_FAILED,
-                 "Failed to store podcast '%s'", title);
-    return;
-  }
-
-  sqlite3_bind_text (sql_stmt, 1, url, -1, SQLITE_STATIC);
-  sqlite3_bind_text (sql_stmt, 2, title, -1, SQLITE_STATIC);
-  if (desc) {
-    sqlite3_bind_text (sql_stmt, 3, desc, -1, SQLITE_STATIC);
-  } else {
-    sqlite3_bind_text (sql_stmt, 3, "", -1, SQLITE_STATIC);
-  }
-
-  while ((r = sqlite3_step (sql_stmt)) == SQLITE_BUSY);
-
-  if (r != SQLITE_DONE) {
-    GRL_WARNING ("Failed to store podcast '%s': %s", title,
-                 sqlite3_errmsg (podcasts_source->priv->db));
-    g_set_error (error,
-                 GRL_CORE_ERROR,
-                 GRL_CORE_ERROR_STORE_FAILED,
-                 "Failed to store podcast '%s'", title);
-    sqlite3_finalize (sql_stmt);
-    return;
-  }
-
-  sqlite3_finalize (sql_stmt);
-
-  id = g_strdup_printf ("%llu",
-                        sqlite3_last_insert_rowid (podcasts_source->priv->db));
-  grl_media_set_id (podcast, id);
-  g_free (id);
-
-  if (podcasts_source->priv->notify_changes) {
-    grl_media_source_notify_change (GRL_MEDIA_SOURCE (podcasts_source),
-                                    NULL,
-                                    GRL_CONTENT_ADDED,
-                                    FALSE);
-  }
-}
-
-static void
-store_stream (sqlite3 *db, const gchar *podcast_id, Entry *entry)
-{
-  gint r;
-  guint seconds;
-  sqlite3_stmt *sql_stmt = NULL;
-
-  if (!entry->url || entry->url[0] == '\0') {
-    GRL_DEBUG ("Podcast stream has no URL, skipping");
-    return;
-  }
-
-  seconds = duration_to_seconds (entry->duration);
-  GRL_DEBUG ("%s", GRL_SQL_STORE_STREAM);
-  r = sqlite3_prepare_v2 (db,
-			  GRL_SQL_STORE_STREAM,
-			  strlen (GRL_SQL_STORE_STREAM),
-			  &sql_stmt, NULL);
-  if (r != SQLITE_OK) {
-    GRL_WARNING ("Failed to store podcast stream '%s': %s",
-                 entry->url, sqlite3_errmsg (db));
-    return;
-  }
-
-  sqlite3_bind_text (sql_stmt, 1, podcast_id, -1, SQLITE_STATIC);
-  sqlite3_bind_text (sql_stmt, 2, entry->url, -1, SQLITE_STATIC);
-  sqlite3_bind_text (sql_stmt, 3, entry->title, -1, SQLITE_STATIC);
-  sqlite3_bind_int  (sql_stmt, 4, seconds);
-  sqlite3_bind_text (sql_stmt, 5, entry->mime, -1, SQLITE_STATIC);
-  sqlite3_bind_text (sql_stmt, 6, entry->published, -1, SQLITE_STATIC);
-  sqlite3_bind_text (sql_stmt, 7, entry->summary, -1, SQLITE_STATIC);
-
-  while ((r = sqlite3_step (sql_stmt)) == SQLITE_BUSY);
-
-  if (r != SQLITE_DONE) {
-    GRL_WARNING ("Failed to store podcast stream '%s': %s",
-                 entry->url, sqlite3_errmsg (db));
-  }
-
-  sqlite3_finalize (sql_stmt);
-}
-
-static void
-parse_entry (xmlDocPtr doc, xmlNodePtr entry, Entry *data)
-{
-  xmlNodePtr node;
-  node = entry->xmlChildrenNode;
-  while (node) {
-    if (!xmlStrcmp (node->name, (const xmlChar *) "title")) {
-      data->title =
-	(gchar *) xmlNodeListGetString (doc, node->xmlChildrenNode, 1);
-    } else if (!xmlStrcmp (node->name, (const xmlChar *) "enclosure")) {
-      data->id = (gchar *) xmlGetProp (node, (xmlChar *) "url");
-      data->url = g_strdup (data->id);
-      data->mime = (gchar *) xmlGetProp (node, (xmlChar *) "type");
-    } else if (!xmlStrcmp (node->name, (const xmlChar *) "summary")) {
-      data->summary =
-	(gchar *) xmlNodeListGetString (doc, node->xmlChildrenNode, 1);
-    } else if (!xmlStrcmp (node->name, (const xmlChar *) "pubDate")) {
-      data->published =
-	(gchar *) xmlNodeListGetString (doc, node->xmlChildrenNode, 1);
-    } else if (!xmlStrcmp (node->name, (const xmlChar *) "duration")) {
-      data->duration =
-	(gchar *) xmlNodeListGetString (doc, node->xmlChildrenNode, 1);
-    }
-    node = node->next;
-  }
-}
-
-static void
-touch_podcast (sqlite3 *db, const gchar *podcast_id)
-{
-  gint r;
-  gchar *sql, *sql_error;
-  GTimeVal now;
-  gchar *now_str;
-
-  GRL_DEBUG ("touch_podcast");
-
-  g_get_current_time (&now);
-  now_str = g_time_val_to_iso8601 (&now);
-
-  sql = g_strdup_printf (GRL_SQL_TOUCH_PODCAST, now_str, podcast_id);
-  GRL_DEBUG ("%s", sql);
-  r = sqlite3_exec (db, sql, NULL, NULL, &sql_error);
-  g_free (sql);
-
-  if (r != SQLITE_OK) {
-    GRL_WARNING ("Failed to touch podcast, '%s': %s", podcast_id, sql_error);
-    sqlite3_free (sql_error);
-    return;
-  }
-}
-
-static gboolean
-parse_entry_idle (gpointer user_data)
-{
-  OperationSpecParse *osp = (OperationSpecParse *) user_data;
-  xmlNodeSetPtr nodes;
-  guint remaining;
-  GrlMedia *media;
-
-  nodes = osp->xpathObj->nodesetval;
-
-  /* Parse entry */
-  Entry *entry = g_slice_new0 (Entry);
-  if (nodes->nodeTab) {
-    parse_entry (osp->doc, nodes->nodeTab[osp->parse_index], entry);
-  }
-  if (0) print_entry (entry);
-
-  /* Check if entry is valid */
-  if (!entry->url || entry->url[0] == '\0') {
-    GRL_DEBUG ("Podcast stream has no URL, skipping");
-  } else {
-    /* Provide results to user as fast as possible */
-    if (osp->parse_valid_index >= osp->os->skip &&
-	osp->parse_valid_index < osp->os->skip + osp->os->count) {
-      media = build_media_from_entry (entry);
-      remaining = osp->os->skip + osp->os->count - osp->parse_valid_index - 1;
-
-      /* Hack: if we emit the last result now the UI may request more results
-	 right away while we are still parsing the XML, so we keep the last
-	 result until we are done processing the whole feed, this way when
-	 the next query arrives all the stuff is stored in the database
-	 and the query can be resolved normally */
-      if (remaining > 0) {
-	osp->os->callback (osp->os->source,
-			   osp->os->operation_id,
-			   media,
-			   remaining,
-			   osp->os->user_data,
-			   NULL);
-      } else {
-	osp->last_media = media;
-      }
-    }
-
-    osp->parse_valid_index++;
-
-    /* And store stream in database cache */
-    store_stream (GRL_PODCASTS_SOURCE (osp->os->source)->priv->db,
-		  osp->os->media_id, entry);
-  }
-
-  osp->parse_index++;
-  free_entry (entry);
-
-  if (osp->parse_index >= osp->parse_count) {
-    /* Send last result */
-    osp->os->callback (osp->os->source,
-		       osp->os->operation_id,
-		       osp->last_media,
-		       0,
-		       osp->os->user_data,
-		       NULL);
-    /* Notify about changes */
-    if (GRL_PODCASTS_SOURCE (osp->os->source)->priv->notify_changes) {
-      media = grl_media_box_new ();
-      grl_media_set_id (media, osp->os->media_id);
-      grl_media_source_notify_change (osp->os->source,
-                                      media,
-                                      GRL_CONTENT_CHANGED,
-                                      FALSE);
-      g_object_unref (media);
-    }
-    g_slice_free (OperationSpec, osp->os);
-    xmlXPathFreeObject (osp->xpathObj);
-    xmlXPathFreeContext (osp->xpathCtx);
-    xmlFreeDoc (osp->doc);
-    g_slice_free (OperationSpecParse, osp);
-  }
-
-  return osp->parse_index < osp->parse_count;
-}
-
-static void
-parse_feed (OperationSpec *os, const gchar *str, GError **error)
-{
-  GrlMedia *podcast = NULL;
-  xmlDocPtr doc = NULL;
-  xmlXPathContextPtr xpathCtx = NULL;
-  xmlXPathObjectPtr xpathObj = NULL;
-  guint stream_count;
-
-  GRL_DEBUG ("parse_feed");
-
-  doc = xmlParseDoc ((xmlChar *) str);
-  if (!doc) {
-    *error = g_error_new (GRL_CORE_ERROR,
-			  os->error_code,
-			  "Failed to parse podcast contents");
-    goto free_resources;
-  }
-
-  /* Get feed stream list */
-  xpathCtx = xmlXPathNewContext (doc);
-  if (!xpathCtx) {
-    *error = g_error_new (GRL_CORE_ERROR,
-			  os->error_code,
-			  "Failed to parse podcast contents");
-    goto free_resources;
-  }
-  
-  xpathObj = xmlXPathEvalExpression ((xmlChar *) "/rss/channel/item",
-				     xpathCtx);
-  if(xpathObj == NULL) {
-    *error = g_error_new (GRL_CORE_ERROR,
-			  os->error_code,
-			  "Failed to parse podcast contents");
-    goto free_resources;
-  }
-
-  /* Feed is ok, let's process it */
-
-  /* First, remove old entries for this podcast */
-  remove_podcast_streams (GRL_PODCASTS_SOURCE (os->source)->priv->db,
-			  os->media_id, error);
-  if (*error) {
-    (*error)->code = os->error_code;
-    goto free_resources;
-  }
-
-  /* Then update the last_refreshed date of the podcast */
-  touch_podcast (GRL_PODCASTS_SOURCE (os->source)->priv->db, os->media_id);
-
-  /* If the feed contains no streams, notify and bail out */
-  stream_count = xpathObj->nodesetval ? xpathObj->nodesetval->nodeNr : 0;
-  GRL_DEBUG ("Got %d streams", stream_count);
-  
-  if (stream_count <= 0) {
-    if (GRL_PODCASTS_SOURCE (os->source)->priv->notify_changes) {
-      podcast = grl_media_box_new ();
-      grl_media_set_id (podcast, os->media_id);
-      grl_media_source_notify_change (os->source,
-                                      podcast,
-                                      GRL_CONTENT_CHANGED,
-                                      FALSE);
-      g_object_unref (podcast);
-    }
-    os->callback (os->source,
-		  os->operation_id,
-		  NULL,
-		  0,
-		  os->user_data,
-		  NULL);
-    goto free_resources;
-  }
-
-  /* Otherwise parse the streams in idle loop to prevent blocking */
-  OperationSpecParse *osp = g_slice_new0 (OperationSpecParse);
-  osp->os = os;
-  osp->doc = doc;
-  osp->xpathCtx = xpathCtx;
-  osp->xpathObj = xpathObj;
-  osp->parse_count = stream_count;
-  g_idle_add (parse_entry_idle, osp);
-  return;
-
- free_resources:
-  if (xpathObj)
-    xmlXPathFreeObject (xpathObj);
-  if (xpathCtx)
-    xmlXPathFreeContext (xpathCtx);
-  if (doc)
-    xmlFreeDoc (doc);
-}
-
-static void
-read_feed_cb (gchar *xmldata, gpointer user_data)
-{
-  GError *error = NULL;
-  OperationSpec *os = (OperationSpec *) user_data;
-
-  if (!xmldata) {
-    error = g_error_new (GRL_CORE_ERROR,
-			 GRL_CORE_ERROR_BROWSE_FAILED,
-			 "Failed to read data from podcast");
-  } else {
-    parse_feed (os, xmldata, &error);
-  }
-
-  if (error) {
-    os->callback (os->source,
-		  os->operation_id,
-		  NULL,
-		  0,
-		  os->user_data,
-		  error);
-    g_error_free (error);
-    g_slice_free (OperationSpec, os);
-  }
-}
-
-static sqlite3_stmt *
-get_podcast_info (sqlite3 *db, const gchar *podcast_id)
-{
-  gint r;
-  sqlite3_stmt *sql_stmt = NULL;
-  gchar *sql;
-
-  GRL_DEBUG ("get_podcast_info");
-
-  sql = g_strdup_printf (GRL_SQL_GET_PODCAST_BY_ID, podcast_id);
-  GRL_DEBUG ("%s", sql);
-  r = sqlite3_prepare_v2 (db, sql, strlen (sql), &sql_stmt, NULL);
-  g_free (sql);
-
-  if (r != SQLITE_OK) {
-    GRL_WARNING ("Failed to retrieve podcast '%s': %s",
-                 podcast_id, sqlite3_errmsg (db));
-    return NULL;
-  }
-
-  while ((r = sqlite3_step (sql_stmt)) == SQLITE_BUSY);
-
-  if (r == SQLITE_ROW) {
-    return sql_stmt;
-  } else {
-    GRL_WARNING ("Failed to retrieve podcast information: %s",
-                 sqlite3_errmsg (db));
-    sqlite3_finalize (sql_stmt);
-    return NULL;
-  }
-}
-
-static void
-produce_podcast_contents (OperationSpec *os)
-{
-  sqlite3_stmt *sql_stmt = NULL;
-  sqlite3 *db;
-  GError *error;
-  gchar *url;
-
-  GRL_DEBUG ("produce_podcast_contents");
-
-  /* First we get some information about the podcast */
-  db = GRL_PODCASTS_SOURCE (os->source)->priv->db;
-  sql_stmt = get_podcast_info (db, os->media_id);
-  if (sql_stmt) {
-    gchar *lr_str;
-    GTimeVal lr;
-    GTimeVal now;
-
-    /* Check if we have to refresh the podcast */
-    lr_str = (gchar *) sqlite3_column_text (sql_stmt, PODCAST_LAST_REFRESHED);
-    GRL_DEBUG ("Podcast last-refreshed: '%s'", lr_str);
-    g_time_val_from_iso8601 (lr_str ? lr_str : "", &lr);
-    g_get_current_time (&now);
-    now.tv_sec -= CACHE_DURATION;
-    if (now.tv_sec >= lr.tv_sec) {
-      /* We have to read the podcast feed again */
-      GRL_DEBUG ("Refreshing podcast '%s'...", os->media_id);
-      url = g_strdup ((gchar *) sqlite3_column_text (sql_stmt, PODCAST_URL));
-      read_url_async (GRL_PODCASTS_SOURCE (os->source), url, read_feed_cb, os);
-      g_free (url);
-    } else {
-      /* We can read the podcast entries from the database cache */
-      produce_podcast_contents_from_db (os);
-      g_slice_free (OperationSpec, os);
-    }
-    sqlite3_finalize (sql_stmt);
-  } else {
-    error = g_error_new (GRL_CORE_ERROR,
-			 os->error_code,
-			 "Failed to retrieve podcast information");
-    os->callback (os->source, os->operation_id, NULL, 0, os->user_data, error);
-    g_error_free (error);
-    g_slice_free (OperationSpec, os);
-  }
-
-}
-
-static void
-produce_podcasts (OperationSpec *os)
-{
-  gint r;
-  sqlite3_stmt *sql_stmt = NULL;
-  sqlite3 *db;
-  GrlMedia *media;
-  GError *error = NULL;
-  GList *medias = NULL;
-  guint count = 0;
-  GList *iter;
-  gchar *sql;
-
-  GRL_DEBUG ("produce_podcasts");
-
-  db = GRL_PODCASTS_SOURCE (os->source)->priv->db;
-
-  if (os->is_query) {
-    /* Query */
-    sql = g_strdup_printf (GRL_SQL_GET_PODCASTS_BY_QUERY,
-			   os->text, os->count, os->skip);
-  } else {
-    /* Browse */
-    sql = g_strdup_printf (GRL_SQL_GET_PODCASTS, os->count, os->skip);
-  }
-  GRL_DEBUG ("%s", sql);
-  r = sqlite3_prepare_v2 (db, sql, strlen (sql), &sql_stmt, NULL);
-  g_free (sql);
-
-  if (r != SQLITE_OK) {
-    GRL_WARNING ("Failed to retrieve podcasts: %s", sqlite3_errmsg (db));
-    error = g_error_new (GRL_CORE_ERROR,
-			 os->error_code,
-			 "Failed to retrieve podcasts list");
-    os->callback (os->source, os->operation_id, NULL, 0, os->user_data, error);
-    g_error_free (error);
-    goto free_resources;
-  }
-
-  while ((r = sqlite3_step (sql_stmt)) == SQLITE_BUSY);
-
-  while (r == SQLITE_ROW) {
-    media = build_media_from_stmt (NULL, sql_stmt, TRUE);
-    medias = g_list_prepend (medias, media);
-    count++;
-    r = sqlite3_step (sql_stmt);
-  }
-
-  if (r != SQLITE_DONE) {
-    GRL_WARNING ("Failed to retrieve podcasts: %s", sqlite3_errmsg (db));
-    error = g_error_new (GRL_CORE_ERROR,
-			 os->error_code,
-			 "Failed to retrieve podcasts list");
-    os->callback (os->source, os->operation_id, NULL, 0, os->user_data, error);
-    g_error_free (error);
-    goto free_resources;
-  }
-
-  if (count > 0) {
-    medias = g_list_reverse (medias);
-    iter = medias;
-    while (iter) {
-      media = GRL_MEDIA (iter->data);
-      os->callback (os->source,
-		    os->operation_id,
-		    media,
-		    --count,
-		    os->user_data,
-		    NULL);
-      iter = g_list_next (iter);
-    }
-    g_list_free (medias);
-  } else {
-    os->callback (os->source, os->operation_id, NULL, 0, os->user_data, NULL);
-  }
-
- free_resources:
-  if (sql_stmt)
-    sqlite3_finalize (sql_stmt);
-}
-
-static void
-stream_metadata (GrlMediaSourceMetadataSpec *ms)
-{
-  gint r;
-  sqlite3_stmt *sql_stmt = NULL;
-  sqlite3 *db;
-  GError *error = NULL;
-  gchar *sql;
-  const gchar *id;
-
-  GRL_DEBUG ("stream_metadata");
-
-  db = GRL_PODCASTS_SOURCE (ms->source)->priv->db;
-
-  id = grl_media_get_id (ms->media);
-  sql = g_strdup_printf (GRL_SQL_GET_PODCAST_STREAM, id);
-  GRL_DEBUG ("%s", sql);
-  r = sqlite3_prepare_v2 (db, sql, strlen (sql), &sql_stmt, NULL);
-  g_free (sql);
-
-  if (r != SQLITE_OK) {
-    GRL_WARNING ("Failed to get podcast stream: %s", sqlite3_errmsg (db));
-    error = g_error_new (GRL_CORE_ERROR,
-			 GRL_CORE_ERROR_METADATA_FAILED,
-			 "Failed to get podcast stream metadata");
-    ms->callback (ms->source, ms->media, ms->user_data, error);
-    g_error_free (error);
-    return;
-  }
-
-  while ((r = sqlite3_step (sql_stmt)) == SQLITE_BUSY);
-
-  if (r == SQLITE_ROW) {
-    build_media_from_stmt (ms->media, sql_stmt, FALSE);
-    ms->callback (ms->source, ms->media, ms->user_data, NULL);
-  } else {
-    GRL_WARNING ("Failed to get podcast stream: %s", sqlite3_errmsg (db));
-    error = g_error_new (GRL_CORE_ERROR,
-			 GRL_CORE_ERROR_METADATA_FAILED,
-			 "Failed to get podcast stream metadata");
-    ms->callback (ms->source, ms->media, ms->user_data, error);
-    g_error_free (error);
-  }
-
-  sqlite3_finalize (sql_stmt);
-}
-
-static void
-podcast_metadata (GrlMediaSourceMetadataSpec *ms)
-{
-  sqlite3_stmt *sql_stmt = NULL;
-  sqlite3 *db;
-  GError *error = NULL;
-  const gchar *id;
-
-  GRL_DEBUG ("podcast_metadata");
-
-  db = GRL_PODCASTS_SOURCE (ms->source)->priv->db;
-
-  id = grl_media_get_id (ms->media);
-  if (!id) {
-    /* Root category: special case */
-    grl_media_set_title (ms->media, GRL_ROOT_TITLE);
-    ms->callback (ms->source, ms->media, ms->user_data, NULL);
-    return;
-  }
-
-  sql_stmt = get_podcast_info (db, id);
-
-  if (sql_stmt) {
-    build_media_from_stmt (ms->media, sql_stmt, TRUE);
-    ms->callback (ms->source, ms->media, ms->user_data, NULL);
-    sqlite3_finalize (sql_stmt);
-  } else {
-    GRL_WARNING ("Failed to get podcast: %s", sqlite3_errmsg (db));
-    error = g_error_new (GRL_CORE_ERROR,
-			 GRL_CORE_ERROR_METADATA_FAILED,
-			 "Failed to get podcast metadata");
-    ms->callback (ms->source, ms->media, ms->user_data, error);
-    g_error_free (error);
-  }
-}
-
-static gboolean
-media_id_is_podcast (const gchar *id)
-{
-  return g_ascii_strtoll (id, NULL, 10) != 0;
-}
-
-/* ================== API Implementation ================ */
-
-static const GList *
-grl_podcasts_source_supported_keys (GrlMetadataSource *source)
-{
-  static GList *keys = NULL;
-  if (!keys) {
-    keys = grl_metadata_key_list_new (GRL_METADATA_KEY_ID,
-                                      GRL_METADATA_KEY_TITLE,
-                                      GRL_METADATA_KEY_URL,
-                                      GRL_METADATA_KEY_CHILDCOUNT,
-                                      GRL_METADATA_KEY_SITE,
-                                      NULL);
-  }
-  return keys;
-}
-
-static void
-grl_podcasts_source_browse (GrlMediaSource *source,
-                            GrlMediaSourceBrowseSpec *bs)
-{
-  GRL_DEBUG ("grl_podcasts_source_browse");
-
-  OperationSpec *os;
-  GrlPodcastsSource *podcasts_source;
-  GError *error = NULL;
-
-  podcasts_source = GRL_PODCASTS_SOURCE (source);
-  if (!podcasts_source->priv->db) {
-    GRL_WARNING ("Can't execute operation: no database connection.");
-    error = g_error_new (GRL_CORE_ERROR,
-			 GRL_CORE_ERROR_BROWSE_FAILED,
-			 "No database connection");
-    bs->callback (bs->source, bs->browse_id, NULL, 0, bs->user_data, error);
-    g_error_free (error);
-    return;
-  }
-
-  /* Configure browse operation */
-  os = g_slice_new0 (OperationSpec);
-  os->source = bs->source;
-  os->operation_id = bs->browse_id;
-  os->media_id = grl_media_get_id (bs->container);
-  os->count = bs->count;
-  os->skip = bs->skip;
-  os->callback = bs->callback;
-  os->user_data = bs->user_data;
-  os->error_code = GRL_CORE_ERROR_BROWSE_FAILED;
-
-  if (!os->media_id) {
-    /* Browsing podcasts list */
-    produce_podcasts (os);
-    g_slice_free (OperationSpec, os);
-  } else {
-    /* Browsing a particular podcast. We may need to parse
-       the feed (async) and in that case we will need to keep
-       os, so we do not free os here */
-    produce_podcast_contents (os);
-  }
-}
-
-static void
-grl_podcasts_source_search (GrlMediaSource *source,
-                            GrlMediaSourceSearchSpec *ss)
-{
-  GRL_DEBUG ("grl_podcasts_source_search");
-
-  GrlPodcastsSource *podcasts_source;
-  OperationSpec *os;
-  GError *error = NULL;
-
-  podcasts_source = GRL_PODCASTS_SOURCE (source);
-  if (!podcasts_source->priv->db) {
-    GRL_WARNING ("Can't execute operation: no database connection.");
-    error = g_error_new (GRL_CORE_ERROR,
-			 GRL_CORE_ERROR_QUERY_FAILED,
-			 "No database connection");
-    ss->callback (ss->source, ss->search_id, NULL, 0, ss->user_data, error);
-    g_error_free (error);
-    return;
-  }
-
-  os = g_slice_new0 (OperationSpec);
-  os->source = ss->source;
-  os->operation_id = ss->search_id;
-  os->text = ss->text;
-  os->count = ss->count;
-  os->skip = ss->skip;
-  os->callback = ss->callback;
-  os->user_data = ss->user_data;
-  os->is_query = TRUE;
-  os->error_code = GRL_CORE_ERROR_SEARCH_FAILED;
-  produce_podcast_contents_from_db (os);
-  g_slice_free (OperationSpec, os);
-}
-
-static void
-grl_podcasts_source_query (GrlMediaSource *source, GrlMediaSourceQuerySpec *qs)
-{
-  GRL_DEBUG ("grl_podcasts_source_query");
-
-  GrlPodcastsSource *podcasts_source;
-  OperationSpec *os;
-  GError *error = NULL;
-
-  podcasts_source = GRL_PODCASTS_SOURCE (source);
-  if (!podcasts_source->priv->db) {
-    GRL_WARNING ("Can't execute operation: no database connection.");
-    error = g_error_new (GRL_CORE_ERROR,
-			 GRL_CORE_ERROR_QUERY_FAILED,
-			 "No database connection");
-    qs->callback (qs->source, qs->query_id, NULL, 0, qs->user_data, error);
-    g_error_free (error);
-    return;
-  }
-
-  os = g_slice_new0 (OperationSpec);
-  os->source = qs->source;
-  os->operation_id = qs->query_id;
-  os->text = qs->query;
-  os->count = qs->count;
-  os->skip = qs->skip;
-  os->callback = qs->callback;
-  os->user_data = qs->user_data;
-  os->is_query = TRUE;
-  os->error_code = GRL_CORE_ERROR_QUERY_FAILED;
-  produce_podcasts (os);
-  g_slice_free (OperationSpec, os);
-}
-
-static void
-grl_podcasts_source_metadata (GrlMediaSource *source,
-                              GrlMediaSourceMetadataSpec *ms)
-{
-  GRL_DEBUG ("grl_podcasts_source_metadata");
-
-  GrlPodcastsSource *podcasts_source;
-  GError *error = NULL;
-  const gchar *media_id;
-
-  podcasts_source = GRL_PODCASTS_SOURCE (source);
-  if (!podcasts_source->priv->db) {
-    GRL_WARNING ("Can't execute operation: no database connection.");
-    error = g_error_new (GRL_CORE_ERROR,
-			 GRL_CORE_ERROR_METADATA_FAILED,
-			 "No database connection");
-    ms->callback (ms->source, ms->media, ms->user_data, error);
-    g_error_free (error);
-    return;
-  }
-
-  media_id = grl_media_get_id (ms->media);
-  if (!media_id || media_id_is_podcast (media_id)) {
-    podcast_metadata (ms);
-  } else {
-    stream_metadata (ms);
-  }
-}
-
-static void
-grl_podcasts_source_store (GrlMediaSource *source, GrlMediaSourceStoreSpec *ss)
-{
-  GRL_DEBUG ("grl_podcasts_source_store");
-  GError *error = NULL;
-  if (GRL_IS_MEDIA_BOX (ss->media)) {
-    error = g_error_new (GRL_CORE_ERROR,
-			 GRL_CORE_ERROR_STORE_FAILED,
-			 "Cannot create containers. Only feeds are accepted.");
-  } else {
-    store_podcast (GRL_PODCASTS_SOURCE (ss->source),
-		   ss->media,
-		   &error);
-  }
-  ss->callback (ss->source, ss->parent, ss->media, ss->user_data, error);
-  if (error) {
-    g_error_free (error);
-  }
-}
-
-static void
-grl_podcasts_source_remove (GrlMediaSource *source,
-                            GrlMediaSourceRemoveSpec *rs)
-{
-  GRL_DEBUG ("grl_podcasts_source_remove");
-  GError *error = NULL;
-  if (media_id_is_podcast (rs->media_id)) {
-    remove_podcast (GRL_PODCASTS_SOURCE (rs->source),
-		    rs->media_id, &error);
-  } else {
-    remove_stream (GRL_PODCASTS_SOURCE (rs->source),
-		   rs->media_id, &error);
-  }
-  rs->callback (rs->source, rs->media, rs->user_data, error);
-  if (error) {
-    g_error_free (error);
-  }
-}
-
-static gboolean
-grl_podcasts_source_notify_change_start (GrlMediaSource *source,
-                                         GError **error)
-{
-  GrlPodcastsSource *podcasts_source = GRL_PODCASTS_SOURCE (source);
-
-  podcasts_source->priv->notify_changes = TRUE;
-
-  return TRUE;
-}
-
-static gboolean
-grl_podcasts_source_notify_change_stop (GrlMediaSource *source,
-                                        GError **error)
-{
-  GrlPodcastsSource *podcasts_source = GRL_PODCASTS_SOURCE (source);
-
-  podcasts_source->priv->notify_changes = FALSE;
-
-  return TRUE;
-}
diff --git a/src/podcasts/grl-podcasts.h b/src/podcasts/grl-podcasts.h
deleted file mode 100644
index 039ba64..0000000
--- a/src/podcasts/grl-podcasts.h
+++ /dev/null
@@ -1,75 +0,0 @@
-/*
- * Copyright (C) 2010 Igalia S.L.
- *
- * Contact: Iago Toral Quiroga <itoral igalia 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_PODCASTS_SOURCE_H_
-#define _GRL_PODCASTS_SOURCE_H_
-
-#include <grilo.h>
-
-#define GRL_PODCASTS_SOURCE_TYPE                \
-  (grl_podcasts_source_get_type ())
-
-#define GRL_PODCASTS_SOURCE(obj)                                \
-  (G_TYPE_CHECK_INSTANCE_CAST ((obj),                           \
-                               GRL_PODCASTS_SOURCE_TYPE,        \
-                               GrlPodcastsSource))
-
-#define GRL_IS_PODCASTS_SOURCE(obj)                             \
-  (G_TYPE_CHECK_INSTANCE_TYPE ((obj),                           \
-                               GRL_PODCASTS_SOURCE_TYPE))
-
-#define GRL_PODCASTS_SOURCE_CLASS(klass)                \
-  (G_TYPE_CHECK_CLASS_CAST((klass),                     \
-                           GRL_PODCASTS_SOURCE_TYPE,    \
-                           GrlPodcastsSourceClass))
-
-#define GRL_IS_PODCASTS_SOURCE_CLASS(klass)             \
-  (G_TYPE_CHECK_CLASS_TYPE((klass),                     \
-                           GRL_PODCASTS_SOURCE_TYPE))
-
-#define GRL_PODCASTS_SOURCE_GET_CLASS(obj)              \
-  (G_TYPE_INSTANCE_GET_CLASS ((obj),                    \
-                              GRL_PODCASTS_SOURCE_TYPE, \
-                              GrlPodcastsSourceClass))
-
-typedef struct _GrlPodcastsPrivate GrlPodcastsPrivate;
-typedef struct _GrlPodcastsSource  GrlPodcastsSource;
-
-struct _GrlPodcastsSource {
-
-  GrlMediaSource parent;
-
-  /*< private >*/
-  GrlPodcastsPrivate *priv;
-};
-
-typedef struct _GrlPodcastsSourceClass GrlPodcastsSourceClass;
-
-struct _GrlPodcastsSourceClass {
-
-  GrlMediaSourceClass parent_class;
-
-};
-
-GType grl_podcasts_source_get_type (void);
-
-#endif /* _GRL_PODCASTS_SOURCE_H_ */
diff --git a/src/podcasts/grl-podcasts.xml b/src/podcasts/grl-podcasts.xml
deleted file mode 100644
index 1222f5c..0000000
--- a/src/podcasts/grl-podcasts.xml
+++ /dev/null
@@ -1,9 +0,0 @@
-<plugin>
-  <info>
-    <name>Podcasts</name>
-    <description>A plugin for browsing podcasts</description>
-    <author>Igalia S.L.</author>
-    <license>LGPL</license>
-    <site>http://www.igalia.com</site>
-  </info>
-</plugin>
diff --git a/src/shoutcast/Makefile.am b/src/shoutcast/Makefile.am
deleted file mode 100644
index 3940692..0000000
--- a/src/shoutcast/Makefile.am
+++ /dev/null
@@ -1,37 +0,0 @@
-#
-# Makefile.am
-#
-# Author: Juan A. Suarez Romero <jasuarez igalia com>
-#
-# Copyright (C) 2010, 2011 Igalia S.L. All rights reserved.
-
-lib_LTLIBRARIES		 = libgrlshoutcast.la
-
-libgrlshoutcast_la_CFLAGS =	\
-	$(DEPS_CFLAGS)		\
-	$(GRLNET_CFLAGS)	\
-	$(XML_CFLAGS)
-
-libgrlshoutcast_la_LIBADD =	\
-	$(DEPS_LIBS)		\
-	$(GRLNET_LIBS)		\
-	$(XML_LIBS)
-
-libgrlshoutcast_la_LDFLAGS = \
-	-module		     \
-	-avoid-version
-
-libgrlshoutcast_la_SOURCES	 = grl-shoutcast.c grl-shoutcast.h
-
-libdir=$(GRL_PLUGINS_DIR)
-
-shoutcastxmldir		= $(GRL_PLUGINS_CONF_DIR)
-shoutcastxml_DATA	= $(SHOUTCAST_PLUGIN_ID).xml
-
-EXTRA_DIST = $(shoutcastxml_DATA)
-
-MAINTAINERCLEANFILES =	\
-	*.in		\
-	*~
-
-DISTCLEANFILES = $(MAINTAINERCLEANFILES)
diff --git a/src/shoutcast/grl-shoutcast.c b/src/shoutcast/grl-shoutcast.c
deleted file mode 100644
index 6b08928..0000000
--- a/src/shoutcast/grl-shoutcast.c
+++ /dev/null
@@ -1,727 +0,0 @@
-/*
- * Copyright (C) 2010 Igalia S.L.
- *
- * Contact: Iago Toral Quiroga <itoral igalia com>
- *
- * Authors: Juan A. Suarez Romero <jasuarez igalia 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 <grilo.h>
-#include <net/grl-net.h>
-#include <libxml/parser.h>
-#include <libxml/xpath.h>
-
-#include "grl-shoutcast.h"
-
-#define EXPIRE_CACHE_TIMEOUT 300
-
-/* --------- Logging  -------- */
-
-#define GRL_LOG_DOMAIN_DEFAULT shoutcast_log_domain
-GRL_LOG_DOMAIN_STATIC(shoutcast_log_domain);
-
-/* ------ SHOUTcast API ------ */
-
-#define SHOUTCAST_BASE_ENTRY "http://yp.shoutcast.com";
-
-#define SHOUTCAST_GET_GENRES    SHOUTCAST_BASE_ENTRY "/sbin/newxml.phtml"
-#define SHOUTCAST_GET_RADIOS    SHOUTCAST_GET_GENRES "?genre=%s&limit=%u"
-#define SHOUTCAST_SEARCH_RADIOS SHOUTCAST_GET_GENRES "?search=%s&limit=%u"
-#define SHOUTCAST_TUNE          SHOUTCAST_BASE_ENTRY "/sbin/tunein-station.pls?id=%s"
-
-/* --- Plugin information --- */
-
-#define PLUGIN_ID   SHOUTCAST_PLUGIN_ID
-
-#define SOURCE_ID   "grl-shoutcast"
-#define SOURCE_NAME "SHOUTcast"
-#define SOURCE_DESC "A source for browsing SHOUTcast radios"
-
-#define AUTHOR      "Igalia S.L."
-#define LICENSE     "LGPL"
-#define SITE        "http://www.igalia.com";
-
-typedef struct {
-  GrlMedia *media;
-  GrlMediaSource *source;
-  GrlMediaSourceMetadataCb metadata_cb;
-  GrlMediaSourceResultCb result_cb;
-  gboolean cancelled;
-  gboolean cache;
-  gchar *filter_entry;
-  gchar *genre;
-  gint error_code;
-  gint operation_id;
-  gint to_send;
-  gpointer user_data;
-  guint count;
-  guint skip;
-  xmlDocPtr xml_doc;
-  xmlNodePtr xml_entries;
-} OperationData;
-
-static GrlNetWc *wc = NULL;
-static GCancellable *cancellable;
-static gchar *cached_page = NULL;
-static gboolean cached_page_expired = TRUE;
-
-static GrlShoutcastSource *grl_shoutcast_source_new (void);
-
-gboolean grl_shoutcast_plugin_init (GrlPluginRegistry *registry,
-                                    const GrlPluginInfo *plugin,
-                                    GList *configs);
-
-static const GList *grl_shoutcast_source_supported_keys (GrlMetadataSource *source);
-
-static void grl_shoutcast_source_metadata (GrlMediaSource *source,
-                                           GrlMediaSourceMetadataSpec *ms);
-
-static void grl_shoutcast_source_browse (GrlMediaSource *source,
-                                         GrlMediaSourceBrowseSpec *bs);
-
-static void grl_shoutcast_source_search (GrlMediaSource *source,
-                                         GrlMediaSourceSearchSpec *ss);
-
-static void grl_shoutcast_source_cancel (GrlMediaSource *source,
-                                         guint operation_id);
-
-static void read_url_async (const gchar *url, OperationData *op_data);
-
-static void grl_shoutcast_source_finalize (GObject *object);
-
-/* =================== SHOUTcast Plugin  =============== */
-
-gboolean
-grl_shoutcast_plugin_init (GrlPluginRegistry *registry,
-                           const GrlPluginInfo *plugin,
-                           GList *configs)
-{
-  GRL_LOG_DOMAIN_INIT (shoutcast_log_domain, "shoutcast");
-
-  GRL_DEBUG ("shoutcast_plugin_init");
-
-  GrlShoutcastSource *source = grl_shoutcast_source_new ();
-  grl_plugin_registry_register_source (registry,
-                                       plugin,
-                                       GRL_MEDIA_PLUGIN (source),
-                                       NULL);
-  return TRUE;
-}
-
-GRL_PLUGIN_REGISTER (grl_shoutcast_plugin_init,
-                     NULL,
-                     PLUGIN_ID);
-
-/* ================== SHOUTcast GObject ================ */
-
-static GrlShoutcastSource *
-grl_shoutcast_source_new (void)
-{
-  GRL_DEBUG ("grl_shoutcast_source_new");
-  return g_object_new (GRL_SHOUTCAST_SOURCE_TYPE,
-		       "source-id", SOURCE_ID,
-		       "source-name", SOURCE_NAME,
-		       "source-desc", SOURCE_DESC,
-		       NULL);
-}
-
-static void
-grl_shoutcast_source_class_init (GrlShoutcastSourceClass * klass)
-{
-  GrlMediaSourceClass *source_class = GRL_MEDIA_SOURCE_CLASS (klass);
-  GrlMetadataSourceClass *metadata_class = GRL_METADATA_SOURCE_CLASS (klass);
-  GObjectClass *gobject_class = G_OBJECT_CLASS (klass);
-  source_class->metadata = grl_shoutcast_source_metadata;
-  source_class->browse = grl_shoutcast_source_browse;
-  source_class->search = grl_shoutcast_source_search;
-  source_class->cancel = grl_shoutcast_source_cancel;
-  metadata_class->supported_keys = grl_shoutcast_source_supported_keys;
-  gobject_class->finalize = grl_shoutcast_source_finalize;
-}
-
-static void
-grl_shoutcast_source_init (GrlShoutcastSource *source)
-{
-}
-
-G_DEFINE_TYPE (GrlShoutcastSource, grl_shoutcast_source, GRL_TYPE_MEDIA_SOURCE);
-
-static void
-grl_shoutcast_source_finalize (GObject *object)
-{
-  if (wc && GRL_IS_NET_WC (wc))
-    g_object_unref (wc);
-
-  if (cancellable && G_IS_CANCELLABLE (cancellable))
-    g_cancellable_cancel (cancellable);
-
-  G_OBJECT_CLASS (grl_shoutcast_source_parent_class)->finalize (object);
-}
-
-/* ======================= Private ==================== */
-
-static gint
-xml_count_nodes (xmlNodePtr node)
-{
-  gint count = 0;
-
-  while (node) {
-    count++;
-    node = node->next;
-  }
-
-  return count;
-}
-
-static GrlMedia *
-build_media_from_genre (OperationData *op_data)
-{
-  GrlMedia *media;
-  gchar *genre_name;
-
-  if (op_data->media) {
-    media = op_data->media;
-  } else {
-    media = grl_media_box_new ();
-  }
-
-  genre_name = (gchar *) xmlGetProp (op_data->xml_entries,
-                                     (const xmlChar *) "name");
-
-  grl_media_set_id (media, genre_name);
-  grl_media_set_title (media, genre_name);
-  grl_data_set_string (GRL_DATA (media),
-                       GRL_METADATA_KEY_GENRE,
-                       genre_name);
-  g_free (genre_name);
-
-  return media;
-}
-
-static GrlMedia *
-build_media_from_station (OperationData *op_data)
-{
-  GrlMedia *media;
-  gchar **station_genres = NULL;
-  gchar *media_id;
-  gchar *media_url;
-  gchar *station_bitrate;
-  gchar *station_genre;
-  gchar *station_genre_field;
-  gchar *station_id;
-  gchar *station_mime;
-  gchar *station_name;
-
-  station_name = (gchar *) xmlGetProp (op_data->xml_entries,
-                                       (const xmlChar *) "name");
-  station_mime = (gchar *) xmlGetProp (op_data->xml_entries,
-                                       (const xmlChar *) "mt");
-  station_id = (gchar *) xmlGetProp (op_data->xml_entries,
-                                     (const xmlChar *) "id");
-  station_bitrate = (gchar *) xmlGetProp (op_data->xml_entries,
-                                          (const xmlChar *) "br");
-  if (op_data->media) {
-    media = op_data->media;
-  } else {
-    media = grl_media_audio_new ();
-  }
-
-  if (op_data->genre) {
-    station_genre = op_data->genre;
-  } else {
-    station_genre_field = (gchar *) xmlGetProp (op_data->xml_entries,
-                                                (const xmlChar *) "genre");
-    station_genres = g_strsplit (station_genre_field, " ", -1);
-    g_free (station_genre_field);
-    station_genre = station_genres[0];
-  }
-
-  media_id = g_strconcat (station_genre, "/", station_id, NULL);
-  media_url = g_strdup_printf (SHOUTCAST_TUNE, station_id);
-
-  grl_media_set_id (media, media_id);
-  grl_media_set_title (media, station_name);
-  grl_media_set_mime (media, station_mime);
-  grl_media_audio_set_genre (GRL_MEDIA_AUDIO (media), station_genre);
-  grl_media_set_url (media, media_url);
-  grl_media_audio_set_bitrate (GRL_MEDIA_AUDIO (media),
-                               atoi (station_bitrate));
-
-  g_free (station_name);
-  g_free (station_mime);
-  g_free (station_id);
-  g_free (station_bitrate);
-  g_free (media_id);
-  g_free (media_url);
-  if (station_genres) {
-    g_strfreev (station_genres);
-  }
-
-  return media;
-}
-
-static gboolean
-send_media (OperationData *op_data, GrlMedia *media)
-{
-  if (!op_data->cancelled) {
-    op_data->result_cb (op_data->source,
-                        op_data->operation_id,
-                        media,
-                        --op_data->to_send,
-                        op_data->user_data,
-                        NULL);
-
-    op_data->xml_entries = op_data->xml_entries->next;
-  } else {
-    op_data->result_cb (op_data->source,
-                        op_data->operation_id,
-                        NULL,
-                        0,
-                        op_data->user_data,
-                        NULL);
-  }
-
-  if (op_data->to_send == 0 || op_data->cancelled) {
-    xmlFreeDoc (op_data->xml_doc);
-    g_slice_free (OperationData, op_data);
-    return FALSE;
-  } else {
-    return TRUE;
-  }
-}
-
-static gboolean
-send_genrelist_entries (OperationData *op_data)
-{
-  return send_media (op_data,
-                     build_media_from_genre (op_data));
-}
-
-static gboolean
-send_stationlist_entries (OperationData *op_data)
-{
-  return send_media (op_data,
-                     build_media_from_station (op_data));
-}
-
-static void
-xml_parse_result (const gchar *str, OperationData *op_data)
-{
-  GError *error = NULL;
-  gboolean stationlist_result;
-  gchar *xpath_expression;
-  xmlNodePtr node;
-  xmlXPathContextPtr xpath_ctx;
-  xmlXPathObjectPtr xpath_res;
-
-  if (op_data->cancelled) {
-    op_data->result_cb (op_data->source,
-                        op_data->operation_id,
-                        NULL,
-                        0,
-                        op_data->user_data,
-                        NULL);
-    g_slice_free (OperationData, op_data);
-    return;
-  }
-
-  op_data->xml_doc = xmlReadMemory (str, xmlStrlen ((xmlChar*) str), NULL, NULL,
-                                    XML_PARSE_RECOVER | XML_PARSE_NOBLANKS);
-  if (!op_data->xml_doc) {
-    error = g_error_new (GRL_CORE_ERROR,
-                         op_data->error_code,
-                         "Failed to parse SHOUTcast's response");
-    goto finalize;
-  }
-
-  node = xmlDocGetRootElement (op_data->xml_doc);
-  if  (!node) {
-    error = g_error_new (GRL_CORE_ERROR,
-                         op_data->error_code,
-                         "Empty response from SHOUTcast");
-    goto finalize;
-  }
-
-  stationlist_result = (xmlStrcmp (node->name,
-                                   (const xmlChar *) "stationlist") == 0);
-
-  op_data->xml_entries = node->xmlChildrenNode;
-
-  /* Check if we are interesting only in updating a media (that is, a metadata()
-     operation) or just browsing/searching */
-  if (op_data->media) {
-
-    /* Search for node */
-    xpath_ctx = xmlXPathNewContext (op_data->xml_doc);
-    if (xpath_ctx) {
-      if (stationlist_result) {
-        xpath_expression = g_strdup_printf ("//station[@id = \"%s\"]",
-                                            op_data->filter_entry);
-      } else {
-        xpath_expression = g_strdup_printf ("//genre[@name = \"%s\"]",
-                                            op_data->filter_entry);
-      }
-      xpath_res = xmlXPathEvalExpression ((xmlChar *) xpath_expression,
-                                          xpath_ctx);
-      g_free (xpath_expression);
-
-      if (xpath_res && xpath_res->nodesetval->nodeTab &&
-          xpath_res->nodesetval->nodeTab[0]) {
-        op_data->xml_entries = xpath_res->nodesetval->nodeTab[0];
-        if (stationlist_result) {
-          build_media_from_station (op_data);
-        } else {
-          build_media_from_genre (op_data);
-        }
-      } else {
-        error = g_error_new (GRL_CORE_ERROR,
-                             op_data->error_code,
-                             "Can not find media '%s'",
-                             grl_media_get_id (op_data->media));
-      }
-      if (xpath_res) {
-        xmlXPathFreeObject (xpath_res);
-      }
-      xmlXPathFreeContext (xpath_ctx);
-    } else {
-      error = g_error_new (GRL_CORE_ERROR,
-                           op_data->error_code,
-                           "Can not build xpath context");
-    }
-
-    op_data->metadata_cb (
-                          op_data->source,
-                          op_data->media,
-                          op_data->user_data,
-                          error);
-    goto free_resources;
-  }
-
-  if (stationlist_result) {
-    /* First node is "tunein"; skip it */
-    op_data->xml_entries = op_data->xml_entries->next;
-  }
-
-  /* Skip elements */
-  while (op_data->xml_entries && op_data->skip > 0) {
-    op_data->xml_entries = op_data->xml_entries->next;
-    op_data->skip--;
-  }
-
-  /* Check if there are elements to send*/
-  if (!op_data->xml_entries || op_data->count == 0) {
-    goto finalize;
-  }
-
-  /* Compute how many items are to be sent */
-  op_data->to_send = xml_count_nodes (op_data->xml_entries);
-  if (op_data->to_send > op_data->count) {
-    op_data->to_send = op_data->count;
-  }
-
-  if (stationlist_result) {
-    g_idle_add ((GSourceFunc) send_stationlist_entries, op_data);
-  } else {
-    g_idle_add ((GSourceFunc) send_genrelist_entries, op_data);
-  }
-
-  return;
-
- finalize:
-  op_data->result_cb (op_data->source,
-                      op_data->operation_id,
-                      NULL,
-                      0,
-                      op_data->user_data,
-                      error);
-
- free_resources:
-  if (op_data->xml_doc) {
-    xmlFreeDoc (op_data->xml_doc);
-  }
-
-  if (op_data->filter_entry) {
-    g_free (op_data->filter_entry);
-  }
-
-  if (error) {
-    g_error_free (error);
-  }
-  g_slice_free (OperationData, op_data);
-}
-
-static gboolean
-expire_cache (gpointer user_data)
-{
-  GRL_DEBUG ("Cached page expired");
-  cached_page_expired = TRUE;
-  return FALSE;
-}
-
-static void
-read_done_cb (GObject *source_object,
-              GAsyncResult *res,
-              gpointer user_data)
-{
-  GError *error = NULL;
-  GError *wc_error = NULL;
-  OperationData *op_data = (OperationData *) user_data;
-  gboolean cache;
-  gchar *content = NULL;
-
-  if (!grl_net_wc_request_finish (GRL_NET_WC (source_object),
-                            res,
-                            &content,
-                            NULL,
-                            &wc_error)) {
-    error = g_error_new (GRL_CORE_ERROR,
-                         op_data->error_code,
-                         "Failed to connect SHOUTcast: '%s'",
-                         wc_error->message);
-    op_data->result_cb (op_data->source,
-                        op_data->operation_id,
-                        NULL,
-                        0,
-                        op_data->user_data,
-                        error);
-    g_error_free (wc_error);
-    g_error_free (error);
-    g_slice_free (OperationData, op_data);
-
-    return;
-  }
-
-  cache = op_data->cache;
-  xml_parse_result (content, op_data);
-  if (cache && cached_page_expired) {
-    GRL_DEBUG ("Caching page");
-    g_free (cached_page);
-    cached_page = g_strdup (content);
-    cached_page_expired = FALSE;
-    g_timeout_add_seconds (EXPIRE_CACHE_TIMEOUT, expire_cache, NULL);
-  }
-}
-
-static gboolean
-read_cached_page (OperationData *op_data)
-{
-  xml_parse_result (cached_page, op_data);
-  return FALSE;
-}
-
-static void
-read_url_async (const gchar *url, OperationData *op_data)
-{
-  if (op_data->cache && !cached_page_expired) {
-    GRL_DEBUG ("Using cached page");
-    g_idle_add ((GSourceFunc) read_cached_page, op_data);
-  } else {
-    if (!wc)
-      wc = grl_net_wc_new ();
-
-    cancellable = g_cancellable_new ();
-    grl_net_wc_request_async (wc, url, cancellable, read_done_cb, op_data);
-  }
-}
-
-/* ================== API Implementation ================ */
-
-static const GList *
-grl_shoutcast_source_supported_keys (GrlMetadataSource *source)
-{
-  static GList *keys = NULL;
-  if (!keys) {
-    keys = grl_metadata_key_list_new (GRL_METADATA_KEY_BITRATE,
-                                      GRL_METADATA_KEY_GENRE,
-                                      GRL_METADATA_KEY_ID,
-                                      GRL_METADATA_KEY_MIME,
-                                      GRL_METADATA_KEY_TITLE,
-                                      GRL_METADATA_KEY_URL,
-                                      NULL);
-  }
-  return keys;
-}
-
-static void
-grl_shoutcast_source_metadata (GrlMediaSource *source,
-                               GrlMediaSourceMetadataSpec *ms)
-{
-  const gchar *media_id;
-  gchar **id_tokens;
-  gchar *url = NULL;
-  OperationData *data = NULL;
-
-  /* Unfortunately, shoutcast does not have an API to get information about a
-     station.  Thus, steps done to obtain the Content must be repeated. For
-     instance, if we have a Media with id "Pop/1321", it means that it is
-     station #1321 that was obtained after browsing "Pop" category. Thus we have
-     repeat the Pop browsing and get the result with station id 1321. If it
-     doesn't exist (think in results obtained from a search), we do nothing */
-
-  media_id = grl_media_get_id (ms->media);
-
-  /* Check if we need to report about root category */
-  if (!media_id) {
-    grl_media_set_title (ms->media, "SHOUTcast");
-  } else {
-    data = g_slice_new0 (OperationData);
-    data->source = source;
-    data->count = 1;
-    data->metadata_cb = ms->callback;
-    data->user_data = ms->user_data;
-    data->error_code = GRL_CORE_ERROR_METADATA_FAILED;
-    data->media = ms->media;
-
-    id_tokens = g_strsplit (media_id, "/", -1);
-
-    /* Check if Content is a media */
-    if (id_tokens[1]) {
-      data->filter_entry = g_strdup (id_tokens[1]);
-
-      /* Check if result is from a previous search */
-      if (id_tokens[0][0] == '?') {
-        url = g_strdup_printf (SHOUTCAST_SEARCH_RADIOS,
-                               id_tokens[0]+1,
-                               G_MAXINT);
-      } else {
-        url = g_strdup_printf (SHOUTCAST_GET_RADIOS,
-                               id_tokens[0],
-                               G_MAXINT);
-      }
-    } else {
-      data->filter_entry = g_strdup (id_tokens[0]);
-      data->cache = TRUE;
-      url = g_strdup (SHOUTCAST_GET_GENRES);
-    }
-
-    g_strfreev (id_tokens);
-  }
-
-  if (url) {
-    read_url_async (url, data);
-    g_free (url);
-  } else {
-    ms->callback (ms->source, ms->media, ms->user_data, NULL);
-  }
-}
-
-static void
-grl_shoutcast_source_browse (GrlMediaSource *source,
-                             GrlMediaSourceBrowseSpec *bs)
-{
-  OperationData *data;
-  const gchar *container_id;
-  gchar *url;
-
-  GRL_DEBUG ("grl_shoutcast_source_browse");
-
-  data = g_slice_new0 (OperationData);
-  data->source = source;
-  data->operation_id = bs->browse_id;
-  data->result_cb = bs->callback;
-  data->skip = bs->skip;
-  data->count = bs->count;
-  data->user_data = bs->user_data;
-  data->error_code = GRL_CORE_ERROR_BROWSE_FAILED;
-
-  container_id = grl_media_get_id (bs->container);
-
-  /* If it's root category send list of genres; else send list of radios */
-  if (!container_id) {
-    data->cache = TRUE;
-    url = g_strdup (SHOUTCAST_GET_GENRES);
-  } else {
-    url = g_strdup_printf (SHOUTCAST_GET_RADIOS,
-                           container_id,
-                           bs->skip + bs->count);
-    data->genre = g_strdup (container_id);
-  }
-
-  grl_media_source_set_operation_data (source, bs->browse_id, data);
-
-  read_url_async (url, data);
-
-  g_free (url);
-}
-
-static void
-grl_shoutcast_source_search (GrlMediaSource *source,
-                             GrlMediaSourceSearchSpec *ss)
-{
-  GError *error;
-  OperationData *data;
-  gchar *url;
-
-  /* Check if there is text to search */
-  if (!ss->text || ss->text[0] == '\0') {
-    error = g_error_new (GRL_CORE_ERROR,
-                         GRL_CORE_ERROR_SEARCH_FAILED,
-                         "Search text not specified");
-    ss->callback (ss->source,
-                  ss->search_id,
-                  NULL,
-                  0,
-                  ss->user_data,
-                  error);
-
-    g_error_free (error);
-    return;
-  }
-
-  data = g_slice_new0 (OperationData);
-  data->source = source;
-  data->operation_id = ss->search_id;
-  data->result_cb = ss->callback;
-  data->skip = ss->skip;
-  data->count = ss->count;
-  data->user_data = ss->user_data;
-  data->error_code = GRL_CORE_ERROR_SEARCH_FAILED;
-
-  grl_media_source_set_operation_data (source, ss->search_id, data);
-
-  url = g_strdup_printf (SHOUTCAST_SEARCH_RADIOS,
-                         ss->text,
-                         ss->skip + ss->count);
-
-  read_url_async (url, data);
-
-  g_free (url);
-}
-
-static void
-grl_shoutcast_source_cancel (GrlMediaSource *source, guint operation_id)
-{
-  OperationData *op_data;
-
-  GRL_DEBUG ("grl_shoutcast_source_cancel");
-
-  if (cancellable && G_IS_CANCELLABLE (cancellable))
-    g_cancellable_cancel (cancellable);
-  cancellable = NULL;
-
-  op_data = (OperationData *) grl_media_source_get_operation_data (source, operation_id);
-
-  if (op_data) {
-    op_data->cancelled = TRUE;
-  }
-}
diff --git a/src/shoutcast/grl-shoutcast.h b/src/shoutcast/grl-shoutcast.h
deleted file mode 100644
index 741fac0..0000000
--- a/src/shoutcast/grl-shoutcast.h
+++ /dev/null
@@ -1,74 +0,0 @@
-/*
- * Copyright (C) 2010 Igalia S.L.
- *
- * Contact: Iago Toral Quiroga <itoral igalia com>
- *
- * Authors: Juan A. Suarez Romero <jasuarez igalia 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_SHOUTCAST_SOURCE_H_
-#define _GRL_SHOUTCAST_SOURCE_H_
-
-#include <grilo.h>
-
-#define GRL_SHOUTCAST_SOURCE_TYPE                 \
-  (grl_shoutcast_source_get_type ())
-
-#define GRL_SHOUTCAST_SOURCE(obj)                         \
-  (G_TYPE_CHECK_INSTANCE_CAST ((obj),                   \
-                               GRL_SHOUTCAST_SOURCE_TYPE, \
-                               GrlShoutcastSource))
-
-#define GRL_IS_SHOUTCAST_SOURCE(obj)                              \
-  (G_TYPE_CHECK_INSTANCE_TYPE ((obj),                           \
-                               GRL_SHOUTCAST_SOURCE_TYPE))
-
-#define GRL_SHOUTCAST_SOURCE_CLASS(klass)                 \
-  (G_TYPE_CHECK_CLASS_CAST((klass),                     \
-                           GRL_SHOUTCAST_SOURCE_TYPE,     \
-                           GrlShoutcastSourceClass))
-
-#define GRL_IS_SHOUTCAST_SOURCE_CLASS(klass)              \
-  (G_TYPE_CHECK_CLASS_TYPE((klass),                     \
-                           GRL_SHOUTCAST_SOURCE_TYPE))
-
-#define GRL_SHOUTCAST_SOURCE_GET_CLASS(obj)               \
-  (G_TYPE_INSTANCE_GET_CLASS ((obj),                    \
-                              GRL_SHOUTCAST_SOURCE_TYPE,  \
-                              GrlShoutcastSourceClass))
-
-typedef struct _GrlShoutcastSource GrlShoutcastSource;
-
-struct _GrlShoutcastSource {
-
-  GrlMediaSource parent;
-
-};
-
-typedef struct _GrlShoutcastSourceClass GrlShoutcastSourceClass;
-
-struct _GrlShoutcastSourceClass {
-
-  GrlMediaSourceClass parent_class;
-
-};
-
-GType grl_shoutcast_source_get_type (void);
-
-#endif /* _GRL_SHOUTCAST_SOURCE_H_ */
diff --git a/src/shoutcast/grl-shoutcast.xml b/src/shoutcast/grl-shoutcast.xml
deleted file mode 100644
index 2f9e916..0000000
--- a/src/shoutcast/grl-shoutcast.xml
+++ /dev/null
@@ -1,9 +0,0 @@
-<plugin>
-  <info>
-    <name>SHOUTcast</name>
-    <description>A plugin for browsing SHOUTcast radios</description>
-    <author>Igalia S.L.</author>
-    <license>LGPL</license>
-    <site>http://www.igalia.com</site>
-  </info>
-</plugin>
diff --git a/src/tracker/Makefile.am b/src/tracker/Makefile.am
deleted file mode 100644
index 5510f5a..0000000
--- a/src/tracker/Makefile.am
+++ /dev/null
@@ -1,36 +0,0 @@
-#
-# Makefile.am
-#
-# Author: Juan A. Suarez Romero <jasuarez igalia com>
-#
-# Copyright (C) 2011 Igalia S.L. All rights reserved.
-
-lib_LTLIBRARIES		 = libgrltracker.la
-
-libgrltracker_la_CFLAGS =	\
-	$(DEPS_CFLAGS)		\
-	$(TRACKER_SPARQL_CFLAGS)
-
-libgrltracker_la_LIBADD =	\
-	$(DEPS_LIBS)		\
-	$(TRACKER_SPARQL_LIBS)
-
-libgrltracker_la_LDFLAGS = \
-	-module		   \
-	-avoid-version
-
-libgrltracker_la_SOURCES = 	\
-	grl-tracker.c 		\
-	grl-tracker.h
-
-libdir = $(GRL_PLUGINS_DIR)
-trackerxmldir = $(GRL_PLUGINS_CONF_DIR)
-trackerxml_DATA	= $(TRACKER_PLUGIN_ID).xml
-
-EXTRA_DIST = $(trackerxml_DATA)
-
-MAINTAINERCLEANFILES =	\
-	*.in		\
-	*~
-
-DISTCLEANFILES = $(MAINTAINERCLEANFILES)
diff --git a/src/tracker/grl-tracker.c b/src/tracker/grl-tracker.c
deleted file mode 100644
index b07627f..0000000
--- a/src/tracker/grl-tracker.c
+++ /dev/null
@@ -1,1511 +0,0 @@
-/*
- * Copyright (C) 2011 Igalia S.L.
- * Copyright (C) 2011 Intel Corporation.
- *
- * Contact: Iago Toral Quiroga <itoral igalia com>
- *
- * Authors: Juan A. Suarez Romero <jasuarez igalia 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 <grilo.h>
-#include <string.h>
-#include <gio/gio.h>
-#include <tracker-sparql.h>
-
-#include "grl-tracker.h"
-
-/* --------- Logging  -------- */
-
-#define GRL_LOG_DOMAIN_DEFAULT tracker_log_domain
-GRL_LOG_DOMAIN_STATIC(tracker_log_domain);
-
-/* ------- Definitions ------- */
-
-#define MEDIA_TYPE "grilo-media-type"
-
-#define RDF_TYPE_ALBUM  "nmm#MusicAlbum"
-#define RDF_TYPE_ARTIST "nmm#Artist"
-#define RDF_TYPE_AUDIO  "nfo#Audio"
-#define RDF_TYPE_MUSIC  "nmm#MusicPiece"
-#define RDF_TYPE_IMAGE  "nmm#Photo"
-#define RDF_TYPE_VIDEO  "nmm#Video"
-#define RDF_TYPE_BOX    "grilo#Box"
-
-/* ---- Plugin information --- */
-
-#define PLUGIN_ID   TRACKER_PLUGIN_ID
-
-#define SOURCE_ID   "grl-tracker"
-#define SOURCE_NAME "Tracker"
-#define SOURCE_DESC "A plugin for searching multimedia content using Tracker"
-
-#define AUTHOR      "Igalia S.L."
-#define LICENSE     "LGPL"
-#define SITE        "http://www.igalia.com";
-
-enum {
-  METADATA,
-  BROWSE,
-  QUERY,
-  SEARCH
-};
-
-/* --- Other --- */
-
-#define TRACKER_MOUNTED_DATASOURCES_START                    \
-  "SELECT nie:dataSource(?urn) AS ?datasource "              \
-  "(SELECT nie:url(tracker:mountPoint(?ds)) "                \
-  "WHERE { ?urn nie:dataSource ?ds  }) "                     \
-  "(SELECT GROUP_CONCAT(tracker:isMounted(?ds), \",\") "     \
-  "WHERE { ?urn nie:dataSource ?ds  }) "                     \
-  "WHERE { ?urn a nfo:FileDataObject . FILTER (?urn IN ("
-
-#define TRACKER_MOUNTED_DATASOURCES_END " ))} GROUP BY (?datasource)"
-
-#define TRACKER_DATASOURCES_REQUEST                                     \
-  "SELECT ?urn nie:dataSource(?urn) AS ?source "                        \
-  "(SELECT GROUP_CONCAT(nie:url(tracker:mountPoint(?ds)), \",\") "      \
-  "WHERE { ?urn nie:dataSource ?ds  }) "                                \
-  "WHERE { "                                                            \
-  "?urn tracker:available ?tr . "                                       \
-  "?source a tracker:Volume . "                                         \
-  "FILTER (bound(nie:dataSource(?urn))) "                               \
-  "} "                                                                  \
-  "GROUP BY (?source)"
-
-#define TRACKER_QUERY_REQUEST                                         \
-  "SELECT rdf:type(?urn) %s "                                         \
-  "WHERE { %s . %s } "                                                \
-  "ORDER BY DESC(nfo:fileLastModified(?urn)) "                        \
-  "OFFSET %i "                                                        \
-  "LIMIT %i"
-
-#define TRACKER_SEARCH_REQUEST                   \
-  "SELECT rdf:type(?urn) %s "                    \
-  "WHERE "                                       \
-  "{ "                                           \
-  "?urn a nfo:Media . "                          \
-  "?urn tracker:available ?tr . "                \
-  "?urn fts:match '%s' . "                       \
-  "%s "                                          \
-  "} "                                           \
-  "ORDER BY DESC(nfo:fileLastModified(?urn)) "   \
-  "OFFSET %i "                                   \
-  "LIMIT %i"
-
-#define TRACKER_SEARCH_ALL_REQUEST               \
-  "SELECT rdf:type(?urn) %s "                    \
-  "WHERE "                                       \
-  "{ "                                           \
-  "?urn a nfo:Media . "                          \
-  "?urn tracker:available ?tr . "                \
-  "%s "                                          \
-  "} "                                           \
-  "ORDER BY DESC(nfo:fileLastModified(?urn)) "   \
-  "OFFSET %i "                                   \
-  "LIMIT %i"
-
-#define TRACKER_BROWSE_CATEGORY_REQUEST                                 \
-  "SELECT rdf:type(?urn) %s "                                           \
-  "WHERE "                                                              \
-  "{ "                                                                  \
-  "?urn a %s . "                                                        \
-  "?urn tracker:available ?tr . "                                       \
-  "%s "                                                                 \
-  "} "                                                                  \
-  "ORDER BY DESC(nfo:fileLastModified(?urn)) "                          \
-  "OFFSET %i "                                                          \
-  "LIMIT %i"
-
-#define TRACKER_METADATA_REQUEST                                    \
-  "SELECT %s "							    \
-  "WHERE { ?urn a nie:DataObject . FILTER (?urn = <%s>) }"
-
-typedef struct {
-  GrlKeyID     grl_key;
-  const gchar *sparql_key_name;
-  const gchar *sparql_key_attr;
-  const gchar *sparql_key_flavor;
-} tracker_grl_sparql_t;
-
-typedef struct {
-  gboolean in_use;
-
-  GHashTable *updated_items;
-  GList *updated_items_list;
-  GList *updated_items_iter;
-
-  /* GList *updated_sources; */
-
-  TrackerSparqlCursor *cursor;
-} tracker_evt_update_t;
-
-struct OperationSpec {
-  GrlMediaSource         *source;
-  GrlTrackerSourcePriv   *priv;
-  guint                   operation_id;
-  GCancellable           *cancel_op;
-  const GList            *keys;
-  guint                   skip;
-  guint                   count;
-  guint                   current;
-  GrlMediaSourceResultCb  callback;
-  gpointer                user_data;
-  TrackerSparqlCursor    *cursor;
-};
-
-enum {
-  PROP_0,
-  PROP_TRACKER_CONNECTION,
-};
-
-struct _GrlTrackerSourcePriv {
-  TrackerSparqlConnection *tracker_connection;
-
-  GHashTable *operations;
-
-  gchar *tracker_datasource;
-};
-
-#define GRL_TRACKER_SOURCE_GET_PRIVATE(object)		\
-  (G_TYPE_INSTANCE_GET_PRIVATE((object),                \
-                               GRL_TRACKER_SOURCE_TYPE,	\
-                               GrlTrackerSourcePriv))
-
-static GrlTrackerSource *grl_tracker_source_new (TrackerSparqlConnection *connection);
-
-static void grl_tracker_source_set_property (GObject      *object,
-                                             guint         propid,
-                                             const GValue *value,
-                                             GParamSpec   *pspec);
-
-static void grl_tracker_source_constructed (GObject *object);
-
-static void grl_tracker_source_finalize (GObject *object);
-
-gboolean grl_tracker_plugin_init (GrlPluginRegistry *registry,
-                                  const GrlPluginInfo *plugin,
-                                  GList *configs);
-
-static const GList *grl_tracker_source_supported_keys (GrlMetadataSource *source);
-
-static void grl_tracker_source_query (GrlMediaSource *source,
-                                      GrlMediaSourceQuerySpec *qs);
-
-static void grl_tracker_source_metadata (GrlMediaSource *source,
-                                         GrlMediaSourceMetadataSpec *ms);
-
-static void grl_tracker_source_search (GrlMediaSource *source,
-                                       GrlMediaSourceSearchSpec *ss);
-
-static void grl_tracker_source_browse (GrlMediaSource *source,
-                                       GrlMediaSourceBrowseSpec *bs);
-
-static void grl_tracker_source_cancel (GrlMediaSource *source,
-                                       guint operation_id);
-
-static gchar *get_tracker_source_name (const gchar *uri,
-                                       const gchar *datasource);
-
-static void setup_key_mappings (void);
-
-/* ===================== Globals  ================= */
-
-static GHashTable *grl_to_sparql_mapping = NULL;
-static GHashTable *sparql_to_grl_mapping = NULL;
-
-static GVolumeMonitor *volume_monitor = NULL;
-static TrackerSparqlConnection *tracker_connection = NULL;
-static gboolean tracker_per_device_source = FALSE;
-static const GrlPluginInfo *tracker_grl_plugin;
-static guint tracker_dbus_signal_id = 0;
-
-/* =================== Tracker Plugin  =============== */
-
-static tracker_evt_update_t *
-tracker_evt_update_new (void)
-{
-  tracker_evt_update_t *evt = g_slice_new0 (tracker_evt_update_t);
-
-  evt->updated_items = g_hash_table_new (g_direct_hash, g_direct_equal);
-
-  return evt;
-}
-
-static void
-tracker_evt_update_free (tracker_evt_update_t *evt)
-{
-  if (!evt)
-    return;
-
-  g_hash_table_destroy (evt->updated_items);
-  g_list_free (evt->updated_items_list);
-
-  if (evt->cursor != NULL)
-    g_object_unref (evt->cursor);
-
-  g_slice_free (tracker_evt_update_t, evt);
-}
-
-static void
-grl_tracker_add_source (GrlTrackerSource *source)
-{
-  grl_plugin_registry_register_source (grl_plugin_registry_get_default (),
-                                       tracker_grl_plugin,
-                                       GRL_MEDIA_PLUGIN (source),
-                                       NULL);
-}
-
-static void
-grl_tracker_del_source (GrlTrackerSource *source)
-{
-  grl_plugin_registry_unregister_source (grl_plugin_registry_get_default (),
-                                         GRL_MEDIA_PLUGIN (source),
-                                         NULL);
-}
-
-static void
-tracker_evt_update_process_item_cb (GObject              *object,
-                                    GAsyncResult         *result,
-                                    tracker_evt_update_t *evt)
-{
-  const gchar *datasource, *uri;
-  gboolean source_mounted;
-  gchar *source_name = NULL;
-  GrlMediaPlugin *plugin;
-  GError *tracker_error = NULL;
-
-  GRL_DEBUG ("%s", __FUNCTION__);
-
-  if (!tracker_sparql_cursor_next_finish (evt->cursor,
-                                          result,
-                                          &tracker_error)) {
-    if (tracker_error != NULL) {
-      GRL_DEBUG ("\terror in parsing : %s", tracker_error->message);
-      g_error_free (tracker_error);
-    } else {
-      GRL_DEBUG ("\tend of parsing :)");
-    }
-
-    tracker_evt_update_free (evt);
-    return;
-  }
-
-  datasource = tracker_sparql_cursor_get_string (evt->cursor, 0, NULL);
-  uri = tracker_sparql_cursor_get_string (evt->cursor, 1, NULL);
-  source_mounted = tracker_sparql_cursor_get_boolean (evt->cursor, 2);
-
-  plugin = grl_plugin_registry_lookup_source (grl_plugin_registry_get_default (),
-                                              datasource);
-
-  GRL_DEBUG ("\tdatasource=%s uri=%s mounted=%i plugin=%p",
-             datasource, uri, source_mounted, plugin);
-
-  if (source_mounted) {
-    if (plugin == NULL) {
-      source_name = get_tracker_source_name (uri, datasource);
-
-      plugin = g_object_new (GRL_TRACKER_SOURCE_TYPE,
-                             "source-id", datasource,
-                             "source-name", source_name,
-                             "source-desc", SOURCE_DESC,
-                             "tracker-connection", tracker_connection,
-                             NULL);
-      grl_tracker_add_source (GRL_TRACKER_SOURCE (plugin));
-      g_free (source_name);
-    } else {
-      GRL_DEBUG ("\tChanges on source %p / %s", plugin, datasource);
-    }
-  } else if (!source_mounted && plugin != NULL) {
-    grl_tracker_del_source (GRL_TRACKER_SOURCE (plugin));
-  }
-
-  tracker_sparql_cursor_next_async (evt->cursor, NULL,
-                                    (GAsyncReadyCallback) tracker_evt_update_process_item_cb,
-                                    (gpointer) evt);
-}
-
-static void
-tracker_evt_update_process_cb (GObject              *object,
-                               GAsyncResult         *result,
-                               tracker_evt_update_t *evt)
-{
-  GError *tracker_error = NULL;
-
-  GRL_DEBUG ("%s", __FUNCTION__);
-
-  evt->cursor = tracker_sparql_connection_query_finish (tracker_connection,
-                                                        result, NULL);
-
-  if (tracker_error != NULL) {
-    GRL_WARNING ("Could not execute sparql query: %s", tracker_error->message);
-
-    g_error_free (tracker_error);
-    tracker_evt_update_free (evt);
-    return;
-  }
-
-  tracker_sparql_cursor_next_async (evt->cursor, NULL,
-                                    (GAsyncReadyCallback) tracker_evt_update_process_item_cb,
-                                    (gpointer) evt);
-}
-
-static void
-tracker_evt_update_process (tracker_evt_update_t *evt)
-{
-  GString *request_str = g_string_new (TRACKER_MOUNTED_DATASOURCES_START);
-
-  GRL_DEBUG ("%s", __FUNCTION__);
-
-  evt->updated_items_iter = evt->updated_items_list;
-  g_string_append_printf (request_str, "%i",
-                          GPOINTER_TO_INT (evt->updated_items_iter->data));
-  evt->updated_items_iter = evt->updated_items_iter->next;
-
-  while (evt->updated_items_iter != NULL) {
-    g_string_append_printf (request_str, ", %i",
-                            GPOINTER_TO_INT (evt->updated_items_iter->data));
-    evt->updated_items_iter = evt->updated_items_iter->next;
-  }
-
-  g_string_append (request_str, TRACKER_MOUNTED_DATASOURCES_END);
-
-  GRL_DEBUG ("\trequest : %s", request_str->str);
-
-  tracker_sparql_connection_query_async (tracker_connection,
-                                         request_str->str,
-                                         NULL,
-                                         (GAsyncReadyCallback) tracker_evt_update_process_cb,
-                                         evt);
-  g_string_free (request_str, TRUE);
-}
-
-static void
-tracker_dbus_signal_cb (GDBusConnection *connection,
-                        const gchar     *sender_name,
-                        const gchar     *object_path,
-                        const gchar     *interface_name,
-                        const gchar     *signal_name,
-                        GVariant        *parameters,
-                        gpointer         user_data)
-
-{
-  gchar *class_name;
-  gint graph = 0, subject = 0, predicate = 0, object = 0, subject_state;
-  GVariantIter *iter1, *iter2;
-  tracker_evt_update_t *evt = tracker_evt_update_new ();
-
-  GRL_DEBUG ("%s", __FUNCTION__);
-
-  g_variant_get (parameters, "(&sa(iiii)a(iiii))", &class_name, &iter1, &iter2);
-
-  GRL_DEBUG ("\tTracker update event for class=%s ins=%lu del=%lu",
-             class_name,
-             (unsigned long) g_variant_iter_n_children (iter1),
-             (unsigned long) g_variant_iter_n_children (iter2));
-
-  while (g_variant_iter_loop (iter1, "(iiii)", &graph,
-                              &subject, &predicate, &object)) {
-    subject_state = GPOINTER_TO_INT (g_hash_table_lookup (evt->updated_items,
-                                                          GSIZE_TO_POINTER (subject)));
-
-    if (subject_state == 0) {
-      g_hash_table_insert (evt->updated_items,
-                           GSIZE_TO_POINTER (subject),
-                           GSIZE_TO_POINTER (1));
-      evt->updated_items_list = g_list_append (evt->updated_items_list,
-                                               GSIZE_TO_POINTER (subject));
-    } else if (subject_state == 2)
-      evt->updated_items_list = g_list_append (evt->updated_items_list,
-                                               GSIZE_TO_POINTER (subject));
-  }
-  g_variant_iter_free (iter1);
-
-
-  while (g_variant_iter_loop (iter2, "(iiii)", &graph,
-                              &subject, &predicate, &object)) {
-    subject_state = GPOINTER_TO_INT (g_hash_table_lookup (evt->updated_items,
-                                                          GSIZE_TO_POINTER (subject)));
-
-    if (subject_state == 0) {
-      g_hash_table_insert (evt->updated_items,
-                           GSIZE_TO_POINTER (subject),
-                           GSIZE_TO_POINTER (1));
-      evt->updated_items_list = g_list_append (evt->updated_items_list,
-                                              GSIZE_TO_POINTER (subject));
-    } else if (subject_state == 2)
-      evt->updated_items_list = g_list_append (evt->updated_items_list,
-                                              GSIZE_TO_POINTER (subject));
-  }
-  g_variant_iter_free (iter2);
-
-  evt->updated_items_iter = evt->updated_items_list;
-
-  GRL_DEBUG ("\t%u elements updated", g_hash_table_size (evt->updated_items));
-  GRL_DEBUG ("\t%u elements updated (list)",
-             g_list_length (evt->updated_items_list));
-
-  tracker_evt_update_process (evt);
-}
-
-static void
-tracker_dbus_start_watch (void)
-{
-  GDBusConnection *connection = g_bus_get_sync (G_BUS_TYPE_SESSION, NULL, NULL);
-
-  tracker_dbus_signal_id = g_dbus_connection_signal_subscribe (connection,
-                                                               TRACKER_DBUS_SERVICE,
-                                                               TRACKER_DBUS_INTERFACE_RESOURCES,
-                                                               "GraphUpdated",
-                                                               TRACKER_DBUS_OBJECT_RESOURCES,
-                                                               NULL,
-                                                               G_DBUS_SIGNAL_FLAGS_NONE,
-                                                               tracker_dbus_signal_cb,
-                                                               NULL,
-                                                               NULL);
-}
-
-static void
-tracker_get_datasource_cb (GObject             *object,
-                           GAsyncResult        *result,
-                           TrackerSparqlCursor *cursor)
-{
-  const gchar *datasource, *uri;
-  gchar *source_name;
-  GError *tracker_error = NULL;
-  GrlTrackerSource *source;
-
-  GRL_DEBUG ("%s", __FUNCTION__);
-
-  if (!tracker_sparql_cursor_next_finish (cursor, result, &tracker_error)) {
-    if (tracker_error == NULL) {
-      GRL_DEBUG ("\tEnd of parsing of devices");
-    } else {
-      GRL_DEBUG ("\tError while parsing devices: %s", tracker_error->message);
-      g_error_free (tracker_error);
-    }
-    g_object_unref (G_OBJECT (cursor));
-    return;
-  }
-
-  datasource = tracker_sparql_cursor_get_string (cursor, 1, NULL);
-  uri = tracker_sparql_cursor_get_string (cursor, 2, NULL);
-  source = GRL_TRACKER_SOURCE (grl_plugin_registry_lookup_source (grl_plugin_registry_get_default (),
-                                                                  datasource));
-
-  if (source == NULL) {
-    source_name = get_tracker_source_name (uri, datasource); /* TODO: get a better name */
-
-    GRL_DEBUG ("\tnew datasource: urn=%s uri=%s\n", datasource, uri);
-
-    source = g_object_new (GRL_TRACKER_SOURCE_TYPE,
-                           "source-id", datasource,
-                           "source-name", source_name,
-                           "source-desc", SOURCE_DESC,
-                           "tracker-connection", tracker_connection,
-                           NULL);
-    grl_plugin_registry_register_source (grl_plugin_registry_get_default (),
-                                         tracker_grl_plugin,
-                                         GRL_MEDIA_PLUGIN (source),
-                                         NULL);
-    g_free (source_name);
-  }
-
-  tracker_sparql_cursor_next_async (cursor, NULL,
-                                    (GAsyncReadyCallback) tracker_get_datasource_cb,
-                                    cursor);
-}
-
-static void
-tracker_get_datasources_cb (GObject      *object,
-                            GAsyncResult *result,
-                            gpointer      data)
-{
-  TrackerSparqlCursor *cursor;
-
-  GRL_DEBUG ("%s", __FUNCTION__);
-
-  cursor = tracker_sparql_connection_query_finish (tracker_connection,
-                                                   result, NULL);
-
-  tracker_sparql_cursor_next_async (cursor, NULL,
-                                    (GAsyncReadyCallback) tracker_get_datasource_cb,
-                                    cursor);
-}
-
-static void
-tracker_get_connection_cb (GObject             *object,
-                           GAsyncResult        *res,
-                           const GrlPluginInfo *plugin)
-{
-  /* GrlTrackerSource *source; */
-
-  GRL_DEBUG ("%s", __FUNCTION__);
-
-  tracker_connection = tracker_sparql_connection_get_finish (res, NULL);
-
-  if (tracker_connection != NULL) {
-    if (tracker_per_device_source == TRUE) {
-      /* Let's discover available data sources. */
-      GRL_DEBUG ("per device source mode");
-
-      tracker_dbus_start_watch ();
-
-      volume_monitor = g_volume_monitor_get ();
-
-      tracker_sparql_connection_query_async (tracker_connection,
-                                             TRACKER_DATASOURCES_REQUEST,
-                                             NULL,
-                                             (GAsyncReadyCallback) tracker_get_datasources_cb,
-                                             NULL);
-    } else {
-      /* One source to rule them all. */
-      grl_tracker_add_source (grl_tracker_source_new (tracker_connection));
-    }
-  }
-}
-
-gboolean
-grl_tracker_plugin_init (GrlPluginRegistry *registry,
-                         const GrlPluginInfo *plugin,
-                         GList *configs)
-{
-  GrlConfig *config;
-  gint config_count;
-
-  GRL_LOG_DOMAIN_INIT (tracker_log_domain, "tracker");
-
-  GRL_DEBUG ("%s", __FUNCTION__);
-
-  tracker_grl_plugin = plugin;
-
-  if (!configs) {
-    GRL_WARNING ("Configuration not provided! Using default configuration.");
-  } else {
-    config_count = g_list_length (configs);
-    if (config_count > 1) {
-      GRL_WARNING ("Provided %i configs, but will only use one", config_count);
-    }
-
-    config = GRL_CONFIG (configs->data);
-
-    tracker_per_device_source = grl_config_get_boolean (config,
-                                                        "per-device-source");
-  }
-
-  tracker_sparql_connection_get_async (NULL,
-                                       (GAsyncReadyCallback) tracker_get_connection_cb,
-                                       (gpointer) plugin);
-  return TRUE;
-}
-
-GRL_PLUGIN_REGISTER (grl_tracker_plugin_init,
-                     NULL,
-                     PLUGIN_ID);
-
-/* ================== Tracker GObject ================ */
-
-static GrlTrackerSource *
-grl_tracker_source_new (TrackerSparqlConnection *connection)
-{
-  GRL_DEBUG ("%s", __FUNCTION__);
-
-  return g_object_new (GRL_TRACKER_SOURCE_TYPE,
-                       "source-id", SOURCE_ID,
-                       "source-name", SOURCE_NAME,
-                       "source-desc", SOURCE_DESC,
-                       "tracker-connection", connection,
-                       NULL);
-}
-
-G_DEFINE_TYPE (GrlTrackerSource, grl_tracker_source, GRL_TYPE_MEDIA_SOURCE);
-
-static void
-grl_tracker_source_class_init (GrlTrackerSourceClass * klass)
-{
-  GrlMediaSourceClass    *source_class   = GRL_MEDIA_SOURCE_CLASS (klass);
-  GrlMetadataSourceClass *metadata_class = GRL_METADATA_SOURCE_CLASS (klass);
-  GObjectClass           *g_class        = G_OBJECT_CLASS (klass);
-
-  source_class->query    = grl_tracker_source_query;
-  source_class->metadata = grl_tracker_source_metadata;
-  source_class->search   = grl_tracker_source_search;
-  source_class->browse   = grl_tracker_source_browse;
-  source_class->cancel   = grl_tracker_source_cancel;
-
-  metadata_class->supported_keys = grl_tracker_source_supported_keys;
-
-  g_class->finalize     = grl_tracker_source_finalize;
-  g_class->set_property = grl_tracker_source_set_property;
-  g_class->constructed  = grl_tracker_source_constructed;
-
-  g_object_class_install_property (g_class,
-                                   PROP_TRACKER_CONNECTION,
-                                   g_param_spec_object ("tracker-connection",
-                                                        "tracker connection",
-                                                        "A Tracker connection",
-                                                        TRACKER_SPARQL_TYPE_CONNECTION,
-                                                        G_PARAM_WRITABLE
-                                                        | G_PARAM_CONSTRUCT_ONLY
-                                                        | G_PARAM_STATIC_NAME));
-
-  g_type_class_add_private (klass, sizeof (GrlTrackerSourcePriv));
-
-  setup_key_mappings ();
-}
-
-static void
-grl_tracker_source_init (GrlTrackerSource *source)
-{
-  GrlTrackerSourcePriv *priv = GRL_TRACKER_SOURCE_GET_PRIVATE (source);
-
-  source->priv = priv;
-
-  priv->operations = g_hash_table_new (g_direct_hash, g_direct_equal);
-}
-
-static void
-grl_tracker_source_constructed (GObject *object)
-{
-  GrlTrackerSourcePriv *priv = GRL_TRACKER_SOURCE_GET_PRIVATE (object);
-
-  if (tracker_per_device_source)
-    g_object_get (object, "source-id", &priv->tracker_datasource, NULL);
-}
-
-static void
-grl_tracker_source_finalize (GObject *object)
-{
-  GrlTrackerSource *self;
-
-  self = GRL_TRACKER_SOURCE (object);
-  if (self->priv->tracker_connection)
-    g_object_unref (self->priv->tracker_connection);
-
-  G_OBJECT_CLASS (grl_tracker_source_parent_class)->finalize (object);
-}
-
-static void
-grl_tracker_source_set_property (GObject      *object,
-                                 guint         propid,
-                                 const GValue *value,
-                                 GParamSpec   *pspec)
-
-{
-  GrlTrackerSourcePriv *priv = GRL_TRACKER_SOURCE_GET_PRIVATE (object);
-
-  switch (propid) {
-  case PROP_TRACKER_CONNECTION:
-    if (priv->tracker_connection != NULL)
-      g_object_unref (G_OBJECT (priv->tracker_connection));
-    priv->tracker_connection = g_object_ref (g_value_get_object (value));
-    break;
-
-  default:
-    G_OBJECT_WARN_INVALID_PROPERTY_ID (object, propid, pspec);
-  }
-}
-
-/* ======================= Utilities ==================== */
-
-static gchar *
-get_tracker_source_name (const gchar *uri, const gchar *datasource)
-{
-  gchar *source_name = NULL;
-  GList *mounts, *mount;
-  GFile *file;
-
-  if (uri != NULL) {
-    mounts = g_volume_monitor_get_mounts (volume_monitor);
-    file = g_file_new_for_uri (uri);
-
-    mount = mounts;
-    while (mount != NULL) {
-      GFile *m_file = g_mount_get_root (G_MOUNT (mount->data));
-
-      if (g_file_equal (m_file, file)) {
-        gchar *m_name = g_mount_get_name (G_MOUNT (mount->data));
-        g_object_unref (G_OBJECT (m_file));
-        source_name = g_strdup_printf ("%s %s", SOURCE_NAME, m_name);
-        g_free (m_name);
-        break;
-      }
-      g_object_unref (G_OBJECT (m_file));
-
-      mount = mount->next;
-    }
-    g_list_foreach (mounts, (GFunc) g_object_unref, NULL);
-    g_list_free (mounts);
-    g_object_unref (G_OBJECT (file));
-
-    if (source_name == NULL)
-      source_name = g_strdup_printf  ("%s %s", SOURCE_NAME, datasource);
-  } else {
-    source_name = g_strdup (SOURCE_NAME " Local");
-  }
-
-  return source_name;
-}
-
-static gchar *
-build_flavored_key (gchar *key, const gchar *flavor)
-{
-  gint i = 0;
-
-  while (key[i] != '\0') {
-    if (!g_ascii_isalnum (key[i])) {
-      key[i] = '_';
-     }
-    i++;
-  }
-
-  return g_strdup_printf ("%s_%s", key, flavor);
-}
-
-static void
-insert_key_mapping (GrlKeyID     grl_key,
-                    const gchar *sparql_key_attr,
-                    const gchar *sparql_key_flavor)
-{
-  tracker_grl_sparql_t *assoc = g_slice_new0 (tracker_grl_sparql_t);
-  GList *assoc_list = g_hash_table_lookup (grl_to_sparql_mapping, grl_key);
-  gchar *canon_name = g_strdup (g_param_spec_get_name (grl_key));
-
-  assoc->grl_key           = grl_key;
-  assoc->sparql_key_name   = build_flavored_key (canon_name, sparql_key_flavor);
-  assoc->sparql_key_attr   = sparql_key_attr;
-  assoc->sparql_key_flavor = sparql_key_flavor;
-
-  assoc_list = g_list_append (assoc_list, assoc);
-
-  g_hash_table_insert (grl_to_sparql_mapping, grl_key, assoc_list);
-  g_hash_table_insert (sparql_to_grl_mapping,
-                       (gpointer) assoc->sparql_key_name,
-                       assoc);
-  g_hash_table_insert (sparql_to_grl_mapping,
-                       (gpointer) g_param_spec_get_name (G_PARAM_SPEC (grl_key)),
-                       assoc);
-
-  g_free (canon_name);
-}
-
-static void
-setup_key_mappings (void)
-{
-  grl_to_sparql_mapping = g_hash_table_new (g_direct_hash, g_direct_equal);
-  sparql_to_grl_mapping = g_hash_table_new (g_str_hash, g_str_equal);
-
-  insert_key_mapping (GRL_METADATA_KEY_ALBUM,
-                      "nmm:albumTitle(nmm:musicAlbum(?urn))",
-                      "audio");
-
-  insert_key_mapping (GRL_METADATA_KEY_ARTIST,
-                      "nmm:artistName(nmm:performer(?urn))",
-                      "audio");
-
-  insert_key_mapping (GRL_METADATA_KEY_AUTHOR,
-                      "nmm:artistName(nmm:performer(?urn))",
-                      "audio");
-
-  insert_key_mapping (GRL_METADATA_KEY_BITRATE,
-                      "nfo:averageBitrate(?urn)",
-                      "audio");
-
-  insert_key_mapping (GRL_METADATA_KEY_CHILDCOUNT,
-                      "nfo:entryCounter(?urn)",
-                      "directory");
-
-  insert_key_mapping (GRL_METADATA_KEY_DATE,
-                      "nfo:fileLastModified(?urn)",
-                      "file");
-
-  insert_key_mapping (GRL_METADATA_KEY_DURATION,
-                      "nfo:duration(?urn)",
-                      "audio");
-
-  insert_key_mapping (GRL_METADATA_KEY_FRAMERATE,
-                      "nfo:frameRate(?urn)",
-                      "video");
-
-  insert_key_mapping (GRL_METADATA_KEY_HEIGHT,
-                      "nfo:height(?urn)",
-                      "video");
-
-  insert_key_mapping (GRL_METADATA_KEY_ID,
-                      "?urn",
-                      "file");
-
-  insert_key_mapping (GRL_METADATA_KEY_LAST_PLAYED,
-                      "nfo:fileLastAccessed(?urn)",
-                      "file");
-
-  insert_key_mapping (GRL_METADATA_KEY_MIME,
-                      "nie:mimeType(?urn)",
-                      "file");
-
-  insert_key_mapping (GRL_METADATA_KEY_SITE,
-                      "nie:url(?urn)",
-                      "file");
-
-  insert_key_mapping (GRL_METADATA_KEY_TITLE,
-                      "nie:title(?urn)",
-                      "audio");
-
-  insert_key_mapping (GRL_METADATA_KEY_TITLE,
-                      "nfo:fileName(?urn)",
-                      "file");
-
-  insert_key_mapping (GRL_METADATA_KEY_URL,
-                      "nie:url(?urn)",
-                      "file");
-
-  insert_key_mapping (GRL_METADATA_KEY_WIDTH,
-                      "nfo:width(?urn)",
-                      "video");
-}
-
-static tracker_grl_sparql_t *
-get_mapping_from_sparql (const gchar *key)
-{
-  return (tracker_grl_sparql_t *) g_hash_table_lookup (sparql_to_grl_mapping,
-                                                       key);
-}
-
-static GList *
-get_mapping_from_grl (const GrlKeyID key)
-{
-  return (GList *) g_hash_table_lookup (grl_to_sparql_mapping, key);
-}
-
-static struct OperationSpec *
-tracker_operation_initiate (GrlMediaSource *source,
-                            GrlTrackerSourcePriv *priv,
-                            guint operation_id)
-{
-  struct OperationSpec *os = g_slice_new0 (struct OperationSpec);
-
-  os->source       = source;
-  os->priv         = priv;
-  os->operation_id = operation_id;
-  os->cancel_op    = g_cancellable_new ();
-
-  g_hash_table_insert (priv->operations, GSIZE_TO_POINTER (operation_id), os);
-
-  return os;
-}
-
-static void
-tracker_operation_terminate (struct OperationSpec *os)
-{
-  if (os == NULL)
-    return;
-
-  g_hash_table_remove (os->priv->operations,
-                       GSIZE_TO_POINTER (os->operation_id));
-
-  g_object_unref (G_OBJECT (os->cursor));
-  g_object_unref (G_OBJECT (os->cancel_op));
-  g_slice_free (struct OperationSpec, os);
-}
-
-static gchar *
-get_select_string (GrlMediaSource *source, const GList *keys)
-{
-  const GList *key = keys;
-  GString *gstr = g_string_new ("");
-  GList *assoc_list;
-  tracker_grl_sparql_t *assoc;
-
-  while (key != NULL) {
-    assoc_list = get_mapping_from_grl ((GrlKeyID) key->data);
-    while (assoc_list != NULL) {
-      assoc = (tracker_grl_sparql_t *) assoc_list->data;
-      if (assoc != NULL) {
-        g_string_append_printf (gstr, "%s AS %s",
-                                assoc->sparql_key_attr,
-                                assoc->sparql_key_name);
-        g_string_append (gstr, " ");
-      }
-      assoc_list = assoc_list->next;
-    }
-    key = key->next;
-  }
-
-  return g_string_free (gstr, FALSE);
-}
-
-/* Builds an appropriate GrlMedia based on ontology type returned by tracker, or
-   NULL if unknown */
-static GrlMedia *
-build_grilo_media (const gchar *rdf_type)
-{
-  GrlMedia *media = NULL;
-  gchar **rdf_single_type;
-  int i;
-
-  if (!rdf_type) {
-    return NULL;
-  }
-
-  /* As rdf_type can be formed by several types, split them */
-  rdf_single_type = g_strsplit (rdf_type, ",", -1);
-  i = g_strv_length (rdf_single_type) - 1;
-
-  while (!media && i >= 0) {
-    if (g_str_has_suffix (rdf_single_type[i], RDF_TYPE_MUSIC)) {
-      media = grl_media_audio_new ();
-    } else if (g_str_has_suffix (rdf_single_type[i], RDF_TYPE_VIDEO)) {
-      media = grl_media_video_new ();
-    } else if (g_str_has_suffix (rdf_single_type[i], RDF_TYPE_IMAGE)) {
-      media = grl_media_image_new ();
-    } else if (g_str_has_suffix (rdf_single_type[i], RDF_TYPE_ARTIST)) {
-      media = grl_media_box_new ();
-    } else if (g_str_has_suffix (rdf_single_type[i], RDF_TYPE_ALBUM)) {
-      media = grl_media_box_new ();
-    } else if (g_str_has_suffix (rdf_single_type[i], RDF_TYPE_BOX)) {
-      media = grl_media_box_new ();
-    }
-    i--;
-  }
-
-  g_strfreev (rdf_single_type);
-
-  return media;
-}
-
-static void
-fill_grilo_media_from_sparql (GrlMedia            *media,
-                              TrackerSparqlCursor *cursor,
-                              gint                 column)
-{
-  const gchar *sparql_key = tracker_sparql_cursor_get_variable_name (cursor, column);
-  tracker_grl_sparql_t *assoc = get_mapping_from_sparql (sparql_key);;
-  union {
-    gint int_val;
-    gdouble double_val;
-    const gchar *str_val;
-  } val;
-
-  if (assoc == NULL)
-    return;
-
-  GRL_DEBUG ("\tSetting media prop (col=%i/var=%s/prop=%s) %s",
-             column,
-             sparql_key,
-             g_param_spec_get_name (G_PARAM_SPEC (assoc->grl_key)),
-             tracker_sparql_cursor_get_string (cursor, column, NULL));
-
-  if (tracker_sparql_cursor_is_bound (cursor, column) == FALSE) {
-    GRL_DEBUG ("\t\tDropping, no data");
-    return;
-  }
-
-  if (grl_data_has_key (GRL_DATA (media), assoc->grl_key)) {
-    GRL_DEBUG ("\t\tDropping, already here");
-    return;
-  }
-
-  switch (G_PARAM_SPEC (assoc->grl_key)->value_type) {
-  case G_TYPE_STRING:
-    val.str_val = tracker_sparql_cursor_get_string (cursor, column, NULL);
-    if (val.str_val != NULL)
-      grl_data_set_string (GRL_DATA (media), assoc->grl_key, val.str_val);
-    break;
-
-  case G_TYPE_INT:
-    val.int_val = tracker_sparql_cursor_get_integer (cursor, column);
-    grl_data_set_int (GRL_DATA (media), assoc->grl_key, val.int_val);
-    break;
-
-  case G_TYPE_FLOAT:
-    val.double_val = tracker_sparql_cursor_get_double (cursor, column);
-    grl_data_set_float (GRL_DATA (media), assoc->grl_key, (gfloat) val.double_val);
-    break;
-
-  default:
-    GRL_DEBUG ("\t\tUnexpected data type");
-    break;
-  }
-}
-
-static void
-tracker_query_result_cb (GObject              *source_object,
-                         GAsyncResult         *result,
-                         struct OperationSpec *operation)
-{
-  gint         col;
-  const gchar *sparql_type;
-  GError      *tracker_error = NULL, *error = NULL;
-  GrlMedia    *media;
-
-  GRL_DEBUG ("%s", __FUNCTION__);
-
-  if (g_cancellable_is_cancelled (operation->cancel_op)) {
-    GRL_DEBUG ("\tOperation %u cancelled", operation->operation_id);
-    operation->callback (operation->source,
-                         operation->operation_id,
-                         NULL, 0,
-                         operation->user_data, NULL);
-    tracker_operation_terminate (operation);
-
-    return;
-  }
-
-  if (!tracker_sparql_cursor_next_finish (operation->cursor,
-                                          result,
-                                          &tracker_error)) {
-    if (tracker_error != NULL) {
-      GRL_DEBUG ("\terror in parsing : %s", tracker_error->message);
-
-      error = g_error_new (GRL_CORE_ERROR,
-                           GRL_CORE_ERROR_BROWSE_FAILED,
-                           "Failed to start browse action : %s",
-                           tracker_error->message);
-
-      operation->callback (operation->source,
-                           operation->operation_id,
-                           NULL, 0,
-                           operation->user_data, error);
-
-      g_error_free (error);
-      g_error_free (tracker_error);
-    } else {
-      GRL_DEBUG ("\tend of parsing :)");
-
-      /* Only emit this last one if more result than expected */
-      if (operation->count > 1)
-        operation->callback (operation->source,
-                             operation->operation_id,
-                             NULL, 0,
-                             operation->user_data, NULL);
-    }
-
-    tracker_operation_terminate (operation);
-    return;
-  }
-
-  sparql_type = tracker_sparql_cursor_get_string (operation->cursor, 0, NULL);
-
-  GRL_DEBUG ("Parsing line %i of type %s", operation->current, sparql_type);
-
-  media = build_grilo_media (sparql_type);
-
-  if (media != NULL) {
-    for (col = 1 ;
-         col < tracker_sparql_cursor_get_n_columns (operation->cursor) ;
-         col++) {
-      fill_grilo_media_from_sparql (media, operation->cursor, col);
-    }
-
-    operation->callback (operation->source,
-                         operation->operation_id,
-                         media,
-                         --operation->count,
-                         operation->user_data,
-                         NULL);
-  }
-
-  /* Schedule the next line to parse */
-  operation->current++;
-  if (operation->count < 1)
-        tracker_operation_terminate (operation);
-  else
-    tracker_sparql_cursor_next_async (operation->cursor, operation->cancel_op,
-                                      (GAsyncReadyCallback) tracker_query_result_cb,
-                                      (gpointer) operation);
-}
-
-static void
-tracker_query_cb (GObject              *source_object,
-                  GAsyncResult         *result,
-                  struct OperationSpec *operation)
-{
-  GError *tracker_error = NULL, *error = NULL;
-
-  GRL_DEBUG ("%s", __FUNCTION__);
-
-  operation->cursor =
-    tracker_sparql_connection_query_finish (operation->priv->tracker_connection,
-                                            result, &tracker_error);
-
-  if (tracker_error) {
-    GRL_WARNING ("Could not execute sparql query: %s", tracker_error->message);
-
-    error = g_error_new (GRL_CORE_ERROR,
-			 GRL_CORE_ERROR_BROWSE_FAILED,
-			 "Failed to start browse action : %s",
-                         tracker_error->message);
-
-    operation->callback (operation->source, operation->operation_id, NULL, 0,
-                         operation->user_data, error);
-
-    g_error_free (tracker_error);
-    g_error_free (error);
-    g_slice_free (struct OperationSpec, operation);
-
-    return;
-  }
-
-  /* Start parsing results */
-  operation->current = 0;
-  tracker_sparql_cursor_next_async (operation->cursor, NULL,
-                                    (GAsyncReadyCallback) tracker_query_result_cb,
-                                    (gpointer) operation);
-}
-
-static void
-tracker_metadata_cb (GObject                    *source_object,
-                     GAsyncResult               *result,
-                     GrlMediaSourceMetadataSpec *ms)
-{
-  GrlTrackerSourcePriv *priv = GRL_TRACKER_SOURCE_GET_PRIVATE (ms->source);
-  gint                  col;
-  GError               *tracker_error = NULL, *error = NULL;
-  TrackerSparqlCursor  *cursor;
-
-  GRL_DEBUG ("%s", __FUNCTION__);
-
-  cursor = tracker_sparql_connection_query_finish (priv->tracker_connection,
-                                                   result, &tracker_error);
-
-  if (tracker_error) {
-    GRL_WARNING ("Could not execute sparql query: %s", tracker_error->message);
-
-    error = g_error_new (GRL_CORE_ERROR,
-			 GRL_CORE_ERROR_BROWSE_FAILED,
-			 "Failed to start browse action : %s",
-                         tracker_error->message);
-
-    ms->callback (ms->source, NULL, ms->user_data, error);
-
-    g_error_free (tracker_error);
-    g_error_free (error);
-
-    goto end_operation;
-  }
-
-
-  tracker_sparql_cursor_next (cursor, NULL, NULL);
-
-  /* Translate Sparql result into Grilo result */
-  for (col = 0 ; col < tracker_sparql_cursor_get_n_columns (cursor) ; col++) {
-    fill_grilo_media_from_sparql (ms->media, cursor, col);
-  }
-
-  ms->callback (ms->source, ms->media, ms->user_data, NULL);
-
- end_operation:
-  if (cursor)
-    g_object_unref (G_OBJECT (cursor));
-}
-
-static gchar *
-tracker_source_get_device_constraint (GrlTrackerSourcePriv *priv)
-{
-  if (priv->tracker_datasource == NULL)
-    return g_strdup ("");
-
-  return g_strdup_printf ("?urn nie:dataSource <%s> .",
-                          priv->tracker_datasource);
-}
-
-/* ================== API Implementation ================ */
-
-static const GList *
-grl_tracker_source_supported_keys (GrlMetadataSource *source)
-{
-  return grl_plugin_registry_get_metadata_keys (grl_plugin_registry_get_default ());
-}
-
-/**
- * Query is a SPARQL query.
- *
- * Columns must be named with the Grilo key name that the column
- * represent. Unnamed or unknown columns will be ignored.
- *
- * First column must be the media type, and it does not need to be named.  It
- * must match with any value supported in rdf:type() property, or
- * grilo#Box. Types understood are:
- *
- * <itemizedlist>
- *   <listitem>
- *     <para>
- *       <literal>nmm#MusicPiece</literal>
- *     </para>
- *   </listitem>
- *   <listitem>
- *     <para>
- *       <literal>nmm#Video</literal>
- *     </para>
- *   </listitem>
- *   <listitem>
- *     <para>
- *       <literal>nmm#Photo</literal>
- *     </para>
- *   </listitem>
- *   <listitem>
- *     <para>
- *       <literal>nmm#Artist</literal>
- *     </para>
- *   </listitem>
- *   <listitem>
- *     <para>
- *       <literal>nmm#MusicAlbum</literal>
- *     </para>
- *   </listitem>
- *   <listitem>
- *     <para>
- *       <literal>grilo#Box</literal>
- *     </para>
- *   </listitem>
- * </itemizedlist>
- *
- * An example for searching all songs:
- *
- * <informalexample>
- *   <programlisting>
- *     SELECT rdf:type(?song)
- *            ?song            AS id
- *            nie:title(?song) AS title
- *            nie:url(?song)   AS url
- *     WHERE { ?song a nmm:MusicPiece }
- *   </programlisting>
- * </informalexample>
- *
- * Alternatively, we can use a partial SPARQL query: just specify the sentence
- * in the WHERE part. In this case, "?urn" is the ontology concept to be used in
- * the clause.
- *
- * An example of such partial query:
- *
- * <informalexample>
- *   <programlisting>
- *     ?urn a nfo:Media
- *   </programlisting>
- * </informalexample>
- *
- * In this case, all data required to build a full SPARQL query will be get from
- * the query spec.
- */
-static void
-grl_tracker_source_query (GrlMediaSource *source,
-                          GrlMediaSourceQuerySpec *qs)
-{
-  GError               *error = NULL;
-  GrlTrackerSourcePriv *priv  = GRL_TRACKER_SOURCE_GET_PRIVATE (source);
-  gchar                *constraint;
-  gchar                *sparql_final;
-  gchar                *sparql_select;
-  struct OperationSpec *os;
-
-  GRL_DEBUG ("%s: id=%u", __FUNCTION__, qs->query_id);
-
-  if (!qs->query || qs->query[0] == '\0') {
-    error = g_error_new_literal (GRL_CORE_ERROR,
-                                 GRL_CORE_ERROR_QUERY_FAILED,
-                                 "Empty query");
-    goto send_error;
-  }
-
-  /* Check if it is a full sparql query */
-  if (g_ascii_strncasecmp (qs->query, "select ", 7) != 0) {
-    constraint = tracker_source_get_device_constraint (priv);
-    sparql_select = get_select_string (source, qs->keys);
-    sparql_final = g_strdup_printf (TRACKER_QUERY_REQUEST,
-                                    sparql_select,
-                                    qs->query,
-                                    constraint,
-                                    qs->skip,
-                                    qs->count);
-    g_free (constraint);
-    g_free (qs->query);
-    g_free (sparql_select);
-    qs->query = sparql_final;
-    grl_tracker_source_query (source, qs);
-    return;
-  }
-
-  GRL_DEBUG ("select : %s", qs->query);
-
-  os = tracker_operation_initiate (source, priv, qs->query_id);
-  os->keys         = qs->keys;
-  os->skip         = qs->skip;
-  os->count        = qs->count;
-  os->callback     = qs->callback;
-  os->user_data    = qs->user_data;
-
-  tracker_sparql_connection_query_async (priv->tracker_connection,
-                                         qs->query,
-                                         os->cancel_op,
-                                         (GAsyncReadyCallback) tracker_query_cb,
-                                         os);
-
-  return;
-
- send_error:
-  qs->callback (qs->source, qs->query_id, NULL, 0, qs->user_data, error);
-  g_error_free (error);
-}
-
-static void
-grl_tracker_source_metadata (GrlMediaSource *source,
-                             GrlMediaSourceMetadataSpec *ms)
-{
-  GrlTrackerSourcePriv *priv = GRL_TRACKER_SOURCE_GET_PRIVATE (source);
-  gchar                *sparql_select, *sparql_final;
-
-  GRL_DEBUG ("%s: id=%i", __FUNCTION__, ms->metadata_id);
-
-  sparql_select = get_select_string (source, ms->keys);
-  sparql_final = g_strdup_printf (TRACKER_METADATA_REQUEST, sparql_select,
-                                  grl_media_get_id (ms->media));
-
-  GRL_DEBUG ("select: '%s'", sparql_final);
-
-  tracker_sparql_connection_query_async (priv->tracker_connection,
-                                         sparql_final,
-                                         NULL,
-                                         (GAsyncReadyCallback) tracker_metadata_cb,
-                                         ms);
-
-  if (sparql_select != NULL)
-    g_free (sparql_select);
-  if (sparql_final != NULL)
-    g_free (sparql_final);
-}
-
-static void
-grl_tracker_source_search (GrlMediaSource *source, GrlMediaSourceSearchSpec *ss)
-{
-  GrlTrackerSourcePriv *priv  = GRL_TRACKER_SOURCE_GET_PRIVATE (source);
-  gchar                *constraint;
-  gchar                *sparql_select;
-  gchar                *sparql_final;
-  struct OperationSpec *os;
-
-  GRL_DEBUG ("%s: id=%u", __FUNCTION__, ss->search_id);
-
-  constraint = tracker_source_get_device_constraint (priv);
-  sparql_select = get_select_string (source, ss->keys);
-  if (!ss->text || ss->text[0] == '\0') {
-    /* Search all */
-    sparql_final = g_strdup_printf (TRACKER_SEARCH_ALL_REQUEST, sparql_select,
-                                    constraint, ss->skip, ss->count);
-  } else {
-    sparql_final = g_strdup_printf (TRACKER_SEARCH_REQUEST, sparql_select,
-                                    ss->text, constraint, ss->skip, ss->count);
-  }
-
-  GRL_DEBUG ("select: '%s'", sparql_final);
-
-  os = tracker_operation_initiate (source, priv, ss->search_id);
-  os->keys         = ss->keys;
-  os->skip         = ss->skip;
-  os->count        = ss->count;
-  os->callback     = ss->callback;
-  os->user_data    = ss->user_data;
-
-  tracker_sparql_connection_query_async (priv->tracker_connection,
-                                         sparql_final,
-                                         os->cancel_op,
-                                         (GAsyncReadyCallback) tracker_query_cb,
-                                         os);
-
-  g_free (constraint);
-  g_free (sparql_select);
-  g_free (sparql_final);
-}
-
-static void
-grl_tracker_source_browse (GrlMediaSource *source,
-                           GrlMediaSourceBrowseSpec *bs)
-{
-  GrlTrackerSourcePriv *priv  = GRL_TRACKER_SOURCE_GET_PRIVATE (source);
-  gchar                *constraint;
-  gchar                *sparql_select;
-  gchar                *sparql_final;
-  struct OperationSpec *os;
-  GrlMedia             *media;
-
-  GRL_DEBUG ("%s: id=%u", __FUNCTION__, bs->browse_id);
-
-  if ((bs->container == NULL || grl_media_get_id (bs->container) == NULL)) {
-    /* Hardcoded categories */
-    media = grl_media_box_new ();
-    grl_media_set_title (media, "Music");
-    grl_media_set_id (media, "nmm:MusicPiece");
-    bs->callback (bs->source, bs->browse_id, media, 2, bs->user_data, NULL);
-
-    media = grl_media_box_new ();
-    grl_media_set_title (media, "Photo");
-    grl_media_set_id (media, "nmm:Photo");
-    bs->callback (bs->source, bs->browse_id, media, 1, bs->user_data, NULL);
-
-    media = grl_media_box_new ();
-    grl_media_set_title (media, "Video");
-    grl_media_set_id (media, "nmm:Video");
-    bs->callback (bs->source, bs->browse_id, media, 0, bs->user_data, NULL);
-    return;
-  }
-
-  constraint = tracker_source_get_device_constraint (priv);
-  sparql_select = get_select_string (bs->source, bs->keys);
-  sparql_final = g_strdup_printf (TRACKER_BROWSE_CATEGORY_REQUEST,
-                                  sparql_select,
-                                  grl_media_get_id (bs->container),
-                                  constraint,
-                                  bs->skip, bs->count);
-
-  GRL_DEBUG ("select: '%s'", sparql_final);
-
-  os = tracker_operation_initiate (source, priv, bs->browse_id);
-  os->keys         = bs->keys;
-  os->skip         = bs->skip;
-  os->count        = bs->count;
-  os->callback     = bs->callback;
-  os->user_data    = bs->user_data;
-
-  tracker_sparql_connection_query_async (priv->tracker_connection,
-                                         sparql_final,
-                                         os->cancel_op,
-                                         (GAsyncReadyCallback) tracker_query_cb,
-                                         os);
-
-  g_free (constraint);
-  g_free (sparql_select);
-  g_free (sparql_final);
-}
-
-static void
-grl_tracker_source_cancel (GrlMediaSource *source, guint operation_id)
-{
-  GrlTrackerSourcePriv *priv = GRL_TRACKER_SOURCE_GET_PRIVATE (source);
-  struct OperationSpec *os;
-
-  GRL_DEBUG ("%s: id=%u", __FUNCTION__, operation_id);
-
-  os = g_hash_table_lookup (priv->operations, GSIZE_TO_POINTER (operation_id));
-
-  if (os != NULL)
-    g_cancellable_cancel (os->cancel_op);
-}
diff --git a/src/tracker/grl-tracker.h b/src/tracker/grl-tracker.h
deleted file mode 100644
index ddf46ce..0000000
--- a/src/tracker/grl-tracker.h
+++ /dev/null
@@ -1,78 +0,0 @@
-/*
- * Copyright (C) 2011 Igalia S.L.
- *
- * Contact: Iago Toral Quiroga <itoral igalia com>
- *
- * Authors: Juan A. Suarez Romero <jasuarez igalia 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_TRACKER_H_
-#define _GRL_TRACKER_H_
-
-#include <grilo.h>
-
-#define GRL_TRACKER_SOURCE_TYPE                 \
-  (grl_tracker_source_get_type ())
-
-#define GRL_TRACKER_SOURCE(obj)                         \
-  (G_TYPE_CHECK_INSTANCE_CAST ((obj),                   \
-                               GRL_TRACKER_SOURCE_TYPE, \
-                               GrlTrackerSource))
-
-#define GRL_IS_TRACKER_SOURCE(obj)                              \
-  (G_TYPE_CHECK_INSTANCE_TYPE ((obj),                           \
-                               GRL_TRACKER_SOURCE_TYPE))
-
-#define GRL_TRACKER_SOURCE_CLASS(klass)                 \
-  (G_TYPE_CHECK_CLASS_CAST((klass),                     \
-                           GRL_TRACKER_SOURCE_TYPE,     \
-                           GrlTrackerSourceClass))
-
-#define GRL_IS_TRACKER_SOURCE_CLASS(klass)              \
-  (G_TYPE_CHECK_CLASS_TYPE((klass),                     \
-                           GRL_TRACKER_SOURCE_TYPE))
-
-#define GRL_TRACKER_SOURCE_GET_CLASS(obj)               \
-  (G_TYPE_INSTANCE_GET_CLASS ((obj),                    \
-                              GRL_TRACKER_SOURCE_TYPE,  \
-                              GrlTrackerSourceClass))
-
-typedef struct _GrlTrackerSource GrlTrackerSource;
-typedef struct _GrlTrackerSourcePriv GrlTrackerSourcePriv;
-
-struct _GrlTrackerSource {
-
-  GrlMediaSource parent;
-
-  /*< private >*/
-  GrlTrackerSourcePriv *priv;
-
-};
-
-typedef struct _GrlTrackerSourceClass GrlTrackerSourceClass;
-
-struct _GrlTrackerSourceClass {
-
-  GrlMediaSourceClass parent_class;
-
-};
-
-GType grl_tracker_source_get_type (void);
-
-#endif /* _GRL_TRACKER_H_ */
diff --git a/src/tracker/grl-tracker.xml b/src/tracker/grl-tracker.xml
deleted file mode 100644
index 20f7bb6..0000000
--- a/src/tracker/grl-tracker.xml
+++ /dev/null
@@ -1,9 +0,0 @@
-<plugin>
-  <info>
-    <name>Tracker</name>
-    <description>A plugin for searching multimedia content using Tracker</description>
-    <author>Igalia S.L.</author>
-    <license>LGPL</license>
-    <site>http://www.igalia.com</site>
-  </info>
-</plugin>
diff --git a/src/upnp/Makefile.am b/src/upnp/Makefile.am
deleted file mode 100644
index b6b09bd..0000000
--- a/src/upnp/Makefile.am
+++ /dev/null
@@ -1,46 +0,0 @@
-#
-# Makefile.am
-#
-# Author: Iago Toral Quiroga <itoral igalia com>
-#
-# Copyright (C) 2010, 2011 Igalia S.L. All rights reserved.
-
-lib_LTLIBRARIES	= libgrlupnp.la
-
-libgrlupnp_la_CFLAGS =		\
-	$(DEPS_CFLAGS)		\
-	$(GUPNP_CFLAGS)		\
-	$(GUPNPAV_CFLAGS)	\
-	$(GTHREAD_CFLAGS)	\
-	$(XML_CFLAGS)
-
-libgrlupnp_la_LIBADD =	\
-	$(DEPS_LIBS)	\
-	$(GUPNP_LIBS)	\
-	$(GUPNPAV_LIBS)	\
-	$(GTHREAD_LIBS)	\
-	$(XML_LIBS)
-
-libgrlupnp_la_LDFLAGS = \
-	-module		\
-	-avoid-version
-
-libgrlupnp_la_CFLAGS +=	\
-	$(XML_CFLAGS)
-
-libgrlupnp_la_LIBADD +=	\
-	$(XML_LIBS)
-
-libgrlupnp_la_SOURCES = grl-upnp.c grl-upnp.h
-
-libdir = $(GRL_PLUGINS_DIR)
-upnpxmldir	= $(GRL_PLUGINS_CONF_DIR)
-upnpxml_DATA	= $(UPNP_PLUGIN_ID).xml
-
-EXTRA_DIST = $(upnpxml_DATA)
-
-MAINTAINERCLEANFILES =	\
-	*.in		\
-	*~
-
-DISTCLEANFILES = $(MAINTAINERCLEANFILES)
diff --git a/src/upnp/grl-upnp.c b/src/upnp/grl-upnp.c
deleted file mode 100644
index f024b53..0000000
--- a/src/upnp/grl-upnp.c
+++ /dev/null
@@ -1,1356 +0,0 @@
-/*
- * Copyright (C) 2010, 2011 Igalia S.L.
- * Copyright (C) 2011 Intel Corporation.
- *
- * This component is based on Maemo's mafw-upnp-source source code.
- *
- * Contact: Iago Toral Quiroga <itoral igalia 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 <grilo.h>
-#include <libgupnp/gupnp.h>
-#include <libgupnp-av/gupnp-av.h>
-#include <string.h>
-#include <stdlib.h>
-#include <libxml/tree.h>
-
-#include "grl-upnp.h"
-
-#define GRL_UPNP_GET_PRIVATE(object)                                    \
-  (G_TYPE_INSTANCE_GET_PRIVATE((object), GRL_UPNP_SOURCE_TYPE, GrlUpnpPrivate))
-
-/* --------- Logging  -------- */
-
-#define GRL_LOG_DOMAIN_DEFAULT upnp_log_domain
-GRL_LOG_DOMAIN_STATIC(upnp_log_domain);
-
-/* --- Plugin information --- */
-
-#define PLUGIN_ID   UPNP_PLUGIN_ID
-
-#define SOURCE_ID_TEMPLATE    "grl-upnp-%s"
-#define SOURCE_NAME_TEMPLATE  "UPnP - %s"
-#define SOURCE_DESC_TEMPLATE  "A source for browsing the UPnP server '%s'"
-
-#define AUTHOR      "Igalia S.L."
-#define LICENSE     "LGPL"
-#define SITE        "http://www.igalia.com";
-
-/* --- Other --- */
-
-#ifndef CONTENT_DIR_SERVICE
-#define CONTENT_DIR_SERVICE "urn:schemas-upnp-org:service:ContentDirectory"
-#endif
-
-#define UPNP_SEARCH_SPEC				\
-  "dc:title contains \"%s\" or "			\
-  "upnp:album contains \"%s\" or "			\
-  "upnp:artist contains \"%s\""
-
-#define UPNP_SEARCH_ALL                         \
-  "upnp:class derivedfrom \"object.item\""
-
-struct _GrlUpnpPrivate {
-  GUPnPDeviceProxy* device;
-  GUPnPServiceProxy* service;
-  GUPnPControlPoint *cp;
-  gboolean search_enabled;
-  gchar *upnp_name;
-};
-
-struct OperationSpec {
-  GrlMediaSource *source;
-  guint operation_id;
-  GList *keys;
-  guint skip;
-  guint count;
-  GrlMediaSourceResultCb callback;
-  gpointer user_data;
-};
-
-struct SourceInfo {
-  gchar *source_id;
-  gchar *source_name;
-  GUPnPDeviceProxy* device;
-  GUPnPServiceProxy* service;
-  GrlPluginInfo *plugin;
-};
-
-static void setup_key_mappings (void);
-
-static gchar *build_source_id (const gchar *udn);
-
-static GrlUpnpSource *grl_upnp_source_new (const gchar *id, const gchar *name);
-
-gboolean grl_upnp_plugin_init (GrlPluginRegistry *registry,
-                               const GrlPluginInfo *plugin,
-                               GList *configs);
-
-static void grl_upnp_source_finalize (GObject *plugin);
-
-static const GList *grl_upnp_source_supported_keys (GrlMetadataSource *source);
-
-static void grl_upnp_source_browse (GrlMediaSource *source,
-                                    GrlMediaSourceBrowseSpec *bs);
-
-static void grl_upnp_source_search (GrlMediaSource *source,
-                                    GrlMediaSourceSearchSpec *ss);
-
-static void grl_upnp_source_query (GrlMediaSource *source,
-                                   GrlMediaSourceQuerySpec *qs);
-
-static void grl_upnp_source_metadata (GrlMediaSource *source,
-                                      GrlMediaSourceMetadataSpec *ms);
-
-static GrlSupportedOps grl_upnp_source_supported_operations (GrlMetadataSource *source);
-
-static gboolean grl_upnp_source_notify_change_start (GrlMediaSource *source,
-                                                     GError **error);
-
-static gboolean grl_upnp_source_notify_change_stop (GrlMediaSource *source,
-                                                    GError **error);
-
-static void context_available_cb (GUPnPContextManager *context_manager,
-				  GUPnPContext *context,
-				  gpointer user_data);
-static void device_available_cb (GUPnPControlPoint *cp,
-				 GUPnPDeviceProxy *device,
-				 gpointer user_data);
-static void device_unavailable_cb (GUPnPControlPoint *cp,
-				   GUPnPDeviceProxy *device,
-				   gpointer user_data);
-
-/* ===================== Globals  ================= */
-
-static GHashTable *key_mapping = NULL;
-static GHashTable *filter_key_mapping = NULL;
-static GUPnPContextManager *context_manager = NULL;
-
-/* =================== UPnP Plugin  =============== */
-
-gboolean
-grl_upnp_plugin_init (GrlPluginRegistry *registry,
-                      const GrlPluginInfo *plugin,
-                      GList *configs)
-{
-  GRL_LOG_DOMAIN_INIT (upnp_log_domain, "upnp");
-
-  GRL_DEBUG ("grl_upnp_plugin_init");
-
-  /* libsoup needs this */
-  if (!g_thread_supported()) {
-    g_thread_init (NULL);
-  }
-
-  context_manager = gupnp_context_manager_new (NULL, 0);
-  g_signal_connect (context_manager,
-                    "context-available",
-                    G_CALLBACK (context_available_cb),
-                    (gpointer)plugin);
-
-  return TRUE;
-}
-
-static void
-grl_upnp_plugin_deinit (void)
-{
-  GRL_DEBUG ("grl_upnp_plugin_deinit");
-
-  if (context_manager != NULL) {
-    g_object_unref (context_manager);
-    context_manager = NULL;
-  }
-}
-
-GRL_PLUGIN_REGISTER (grl_upnp_plugin_init,
-                     grl_upnp_plugin_deinit,
-                     PLUGIN_ID);
-
-/* ================== UPnP GObject ================ */
-
-G_DEFINE_TYPE (GrlUpnpSource, grl_upnp_source, GRL_TYPE_MEDIA_SOURCE);
-
-static GrlUpnpSource *
-grl_upnp_source_new (const gchar *source_id, const gchar *name)
-{
-  gchar *source_name, *source_desc;
-  GrlUpnpSource *source;
-
-  GRL_DEBUG ("grl_upnp_source_new");
-  source_name = g_strdup_printf (SOURCE_NAME_TEMPLATE, name);
-  source_desc = g_strdup_printf (SOURCE_DESC_TEMPLATE, name);
-
-  source = g_object_new (GRL_UPNP_SOURCE_TYPE,
-			 "source-id", source_id,
-			 "source-name", source_name,
-			 "source-desc", source_desc,
-			 NULL);
-
-  source->priv->upnp_name = g_strdup (name);
-
-  g_free (source_name);
-  g_free (source_desc);
-
-  return source;
-}
-
-static void
-grl_upnp_source_class_init (GrlUpnpSourceClass * klass)
-{
-  GObjectClass *gobject_class = G_OBJECT_CLASS (klass);
-  GrlMediaSourceClass *source_class = GRL_MEDIA_SOURCE_CLASS (klass);
-  GrlMetadataSourceClass *metadata_class = GRL_METADATA_SOURCE_CLASS (klass);
-
-  gobject_class->finalize = grl_upnp_source_finalize;
-
-  metadata_class->supported_keys = grl_upnp_source_supported_keys;
-  metadata_class->supported_operations = grl_upnp_source_supported_operations;
-
-  source_class->browse = grl_upnp_source_browse;
-  source_class->search = grl_upnp_source_search;
-  source_class->query = grl_upnp_source_query;
-  source_class->metadata = grl_upnp_source_metadata;
-  source_class->notify_change_start = grl_upnp_source_notify_change_start;
-  source_class->notify_change_stop = grl_upnp_source_notify_change_stop;
-
-  g_type_class_add_private (klass, sizeof (GrlUpnpPrivate));
-
-  setup_key_mappings ();
-}
-
-static void
-grl_upnp_source_init (GrlUpnpSource *source)
-{
-  source->priv = GRL_UPNP_GET_PRIVATE (source);
-}
-
-static void
-grl_upnp_source_finalize (GObject *object)
-{
-  GrlUpnpSource *source;
-
-  GRL_DEBUG ("grl_upnp_source_finalize");
-
-  source = GRL_UPNP_SOURCE (object);
-
-  g_object_unref (source->priv->device);
-  g_object_unref (source->priv->service);
-  g_free (source->priv->upnp_name);
-
-  G_OBJECT_CLASS (grl_upnp_source_parent_class)->finalize (object);
-}
-
-/* ======================= Utilities ==================== */
-
-static gchar *
-build_source_id (const gchar *udn)
-{
-  return g_strdup_printf (SOURCE_ID_TEMPLATE, udn);
-}
-
-static void
-free_source_info (struct SourceInfo *info)
-{
-  g_free (info->source_id);
-  g_free (info->source_name);
-  g_object_unref (info->device);
-  g_object_unref (info->service);
-  g_slice_free (struct SourceInfo, info);
-}
-
-static void
-container_changed_cb (GUPnPServiceProxy *proxy,
-                      const char *variable,
-                      GValue *value,
-                      gpointer user_data)
-{
-  GrlMedia *container;
-  GrlMediaSource *source = GRL_MEDIA_SOURCE (user_data);
-  gchar **tokens;
-  gint i = 0;
-
-  GRL_DEBUG (__func__);
-
-  /* Value is a list of pairs (id, number), where "id" is the container id */
-  tokens = g_strsplit (g_value_get_string (value), ",", -1);
-  while (tokens[i]) {
-    container = grl_media_box_new ();
-    grl_media_set_id (container, tokens[i]);
-    grl_media_source_notify_change (source,
-                                    container,
-                                    GRL_CONTENT_CHANGED,
-                                    FALSE);
-    g_object_unref (container);
-    i += 2;
-  }
-  g_strfreev (tokens);
-}
-
-static void
-gupnp_search_caps_cb (GUPnPServiceProxy *service,
-		      GUPnPServiceProxyAction *action,
-		      gpointer user_data)
-{
-  GError *error = NULL;
-  gchar *caps = NULL;
-  gchar *name;
-  GrlUpnpSource *source;
-  gchar *source_id;
-  GrlPluginRegistry *registry;
-  struct SourceInfo *source_info;
-  gboolean result;
-
-  result =
-    gupnp_service_proxy_end_action (service, action, &error,
-				    "SearchCaps", G_TYPE_STRING, &caps,
-				    NULL);
-  if (!result) {
-    GRL_WARNING ("Failed to execute GetSearchCaps operation");
-    if (error) {
-      GRL_WARNING ("Reason: %s", error->message);
-      g_error_free (error);
-    }
-  }
-
-  source_info = (struct SourceInfo *) user_data;
-  name = source_info->source_name;
-  source_id = source_info->source_id;
-
-  registry = grl_plugin_registry_get_default ();
-  if (grl_plugin_registry_lookup_source (registry, source_id)) {
-    GRL_DEBUG ("A source with id '%s' is already registered. Skipping...",
-               source_id);
-    goto free_resources;
-  }
-
-  source = grl_upnp_source_new (source_id, name);
-  source->priv->device = g_object_ref (source_info->device);
-  source->priv->service = g_object_ref (source_info->service);
-
-  GRL_DEBUG ("Search caps for source '%s': '%s'", name, caps);
-
-  if (caps && caps[0] != '\0') {
-    GRL_DEBUG ("Setting search enabled for source '%s'", name );
-    source->priv->search_enabled = TRUE;
-  } else {
-    GRL_DEBUG ("Setting search disabled for source '%s'", name );
-  }
-
-  grl_plugin_registry_register_source (registry,
-                                       source_info->plugin,
-                                       GRL_MEDIA_PLUGIN (source),
-                                       NULL);
-
- free_resources:
-  free_source_info (source_info);
-}
-
-static void
-context_available_cb (GUPnPContextManager *context_manager,
-		      GUPnPContext *context,
-		      gpointer user_data)
-{
-  GUPnPControlPoint *cp;
-
-  GRL_DEBUG ("%s", __func__);
-
-  cp = gupnp_control_point_new (context, "urn:schemas-upnp-org:device:MediaServer:1");
-  g_signal_connect (cp,
-		    "device-proxy-available",
-		    G_CALLBACK (device_available_cb),
-		    user_data);
-  g_signal_connect (cp,
-		    "device-proxy-unavailable",
-		    G_CALLBACK (device_unavailable_cb),
-		    NULL);
-
-  gssdp_resource_browser_set_active (GSSDP_RESOURCE_BROWSER (cp), TRUE);
-
-  /* Let context manager take care of the control point life cycle */
-  gupnp_context_manager_manage_control_point (context_manager, cp);
-  g_object_unref (cp);
-}
-
-static void
-device_available_cb (GUPnPControlPoint *cp,
-		     GUPnPDeviceProxy *device,
-		     gpointer user_data)
-{
-  gchar* name;
-  const gchar* udn;
-  const char *type;
-  GUPnPServiceInfo *service;
-  GrlPluginRegistry *registry;
-  gchar *source_id;
-
-  GRL_DEBUG ("device_available_cb");
-
-  type = gupnp_device_info_get_device_type (GUPNP_DEVICE_INFO (device));
-  GRL_DEBUG ("  type: %s", type);
-
-  service = gupnp_device_info_get_service (GUPNP_DEVICE_INFO (device),
-					   CONTENT_DIR_SERVICE);
-  if (!service) {
-    GRL_DEBUG ("Device does not provide required service, ignoring...");
-    return;
-  }
-
-  udn = gupnp_device_info_get_udn (GUPNP_DEVICE_INFO (device));
-  GRL_DEBUG ("   udn: %s ", udn);
-
-  name = gupnp_device_info_get_friendly_name (GUPNP_DEVICE_INFO (device));
-  GRL_DEBUG ("  name: %s", name);
-
-  registry = grl_plugin_registry_get_default ();
-  source_id = build_source_id (udn);
-  if (grl_plugin_registry_lookup_source (registry, source_id)) {
-    GRL_DEBUG ("A source with id '%s' is already registered. Skipping...",
-               source_id);
-    goto free_resources;
-  }
-
-  /* We got a valid UPnP source */
-  /* Now let's check if it supports search operations before registering */
-  struct SourceInfo *source_info = g_slice_new0 (struct SourceInfo);
-  source_info->source_id = g_strdup (source_id);
-  source_info->source_name = g_strdup (name);
-  source_info->device = g_object_ref (device);
-  source_info->service = g_object_ref (service);
-  source_info->plugin = (GrlPluginInfo *) user_data;
-
-  if (!gupnp_service_proxy_begin_action (GUPNP_SERVICE_PROXY (service),
-					 "GetSearchCapabilities",
-					 gupnp_search_caps_cb,
-					 source_info,
-					 NULL)) {
-    GrlUpnpSource *source = grl_upnp_source_new (source_id, name);
-    GRL_WARNING ("Failed to start GetCapabilitiesSearch action");
-    GRL_DEBUG ("Setting search disabled for source '%s'", name );
-    registry = grl_plugin_registry_get_default ();
-    grl_plugin_registry_register_source (registry,
-                                         source_info->plugin,
-                                         GRL_MEDIA_PLUGIN (source),
-                                         NULL);
-    free_source_info (source_info);
-  }
-
- free_resources:
-  g_object_unref (service);
-  g_free (source_id);
-}
-
-static void
-device_unavailable_cb (GUPnPControlPoint *cp,
-		       GUPnPDeviceProxy *device,
-		       gpointer user_data)
-{
-  const gchar* udn;
-  GrlMediaPlugin *source;
-  GrlPluginRegistry *registry;
-  gchar *source_id;
-
-  GRL_DEBUG ("device_unavailable_cb");
-
-  udn = gupnp_device_info_get_udn (GUPNP_DEVICE_INFO (device));
-  GRL_DEBUG ("   udn: %s ", udn);
-
-  registry = grl_plugin_registry_get_default ();
-  source_id = build_source_id (udn);
-  source = grl_plugin_registry_lookup_source (registry, source_id);
-  if (!source) {
-    GRL_DEBUG ("No source registered with id '%s', ignoring", source_id);
-  } else {
-    grl_plugin_registry_unregister_source (registry, source, NULL);
-  }
-
-  g_free (source_id);
-}
-
-const static gchar *
-get_upnp_key (const GrlKeyID key_id)
-{
-  return g_hash_table_lookup (key_mapping, key_id);
-}
-
-const static gchar *
-get_upnp_key_for_filter (const GrlKeyID key_id)
-{
-  return g_hash_table_lookup (filter_key_mapping, key_id);
-}
-
-static gchar *
-get_upnp_filter (const GList *keys)
-{
-  GString *filter;
-  GList *iter;
-  gchar *upnp_key;
-  guint first = TRUE;
-
-  filter = g_string_new ("");
-  iter = (GList *) keys;
-  while (iter) {
-    upnp_key =
-      (gchar *) get_upnp_key_for_filter (iter->data);
-    if (upnp_key) {
-      if (!first) {
-	g_string_append (filter, ",");
-      }
-      g_string_append (filter, upnp_key);
-      first = FALSE;
-    }
-    iter = g_list_next (iter);
-  }
-
-  return g_string_free (filter, FALSE);
-}
-
-static gchar *
-get_upnp_search (const gchar *text)
-{
-  if (text) {
-    return g_strdup_printf (UPNP_SEARCH_SPEC, text, text, text);
-  } else {
-    return g_strdup (UPNP_SEARCH_ALL);
-  }
-}
-
-static void
-setup_key_mappings (void)
-{
-  /* For key_mapping we only have to set mapping for keys that
-     are not handled directly with the corresponding fw key
-     (see ket_valur_for_key) */
-  key_mapping = g_hash_table_new (g_direct_hash, g_direct_equal);
-  filter_key_mapping = g_hash_table_new (g_direct_hash, g_direct_equal);
-
-  g_hash_table_insert (key_mapping, GRL_METADATA_KEY_TITLE, "title");
-  g_hash_table_insert (key_mapping, GRL_METADATA_KEY_ARTIST, "artist");
-  g_hash_table_insert (key_mapping, GRL_METADATA_KEY_ALBUM, "album");
-  g_hash_table_insert (key_mapping, GRL_METADATA_KEY_GENRE, "genre");
-  g_hash_table_insert (key_mapping, GRL_METADATA_KEY_URL, "res");
-  g_hash_table_insert (key_mapping, GRL_METADATA_KEY_DATE, "modified");
-  g_hash_table_insert (filter_key_mapping, GRL_METADATA_KEY_TITLE, "title");
-  g_hash_table_insert (filter_key_mapping, GRL_METADATA_KEY_URL, "res");
-  g_hash_table_insert (filter_key_mapping, GRL_METADATA_KEY_DATE, "modified");
-  g_hash_table_insert (filter_key_mapping, GRL_METADATA_KEY_ARTIST, "upnp:artist");
-  g_hash_table_insert (filter_key_mapping, GRL_METADATA_KEY_ALBUM, "upnp:album");
-  g_hash_table_insert (filter_key_mapping, GRL_METADATA_KEY_GENRE, "upnp:genre");
-  g_hash_table_insert (filter_key_mapping, GRL_METADATA_KEY_DURATION, "res@duration");
-  g_hash_table_insert (filter_key_mapping, GRL_METADATA_KEY_DATE, "modified");
-}
-
-static gchar *
-didl_res_get_protocol_info (xmlNode* res_node, gint field)
-{
-  gchar* pinfo;
-  gchar* value;
-  gchar** array;
-
-  pinfo = (gchar *) xmlGetProp (res_node, (const xmlChar *) "protocolInfo");
-  if (pinfo == NULL) {
-    return NULL;
-  }
-
-  /* 0:protocol, 1:network, 2:mime-type and 3:additional info. */
-  array = g_strsplit (pinfo, ":", 4);
-  g_free(pinfo);
-  if (g_strv_length (array) < 4) {
-    value = NULL;
-  } else {
-    value = g_strdup (array[field]);
-  }
-
-  g_strfreev (array);
-
-  return value;
-}
-
-static GList *
-didl_get_supported_resources (GUPnPDIDLLiteObject *didl)
-{
-  GList *properties, *node;
-  xmlNode *xml_node;
-  gchar *protocol;
-
-  properties = gupnp_didl_lite_object_get_properties (didl, "res");
-
-  node = properties;
-  while (node) {
-    xml_node = (xmlNode *) node->data;
-    if (!xml_node) {
-      node = properties = g_list_delete_link (properties, node);
-      continue;
-    }
-
-    protocol = didl_res_get_protocol_info (xml_node, 0);
-    if (protocol && strcmp (protocol, "http-get") != 0) {
-      node = properties = g_list_delete_link (properties, node);
-      g_free (protocol);
-      continue;
-    }
-    g_free (protocol);
-    node = g_list_next (node);
-  }
-
-  return properties;
-}
-
-static gint
-didl_h_mm_ss_to_int (const gchar *time)
-{
-  guint len = 0;
-  guint i = 0;
-  guint head = 0;
-  guint tail = 0;
-  int result = 0;
-  gchar* tmp = NULL;
-  gboolean has_hours = FALSE;
-
-  if (!time) {
-    return -1;
-  }
-
-  len = strlen (time);
-  tmp = g_new0 (gchar, sizeof (gchar) * len);
-
-  /* Find the first colon (it can be anywhere) and also count the
-   * amount of colons to know if there are hours or not */
-  for (i = 0; i < len; i++) {
-    if (time[i] == ':') {
-      if (tail != 0) {
-	has_hours = TRUE;
-      } else {
-	tail = i;
-      }
-    }
-  }
-
-  if (tail > len || head > tail) {
-    g_free (tmp);
-    return -1;
-  }
-
-  /* Hours */
-  if (has_hours == TRUE) {
-    memcpy (tmp, time + head, tail - head);
-    tmp[tail - head + 1] = '\0';
-    result += 3600 * atoi (tmp);
-    /* The next colon should be exactly 2 chars right */
-    head = tail + 1;
-    tail = head + 2;
-  } else {
-    /* The format is now exactly MM:SS */
-    head = 0;
-    tail = 2;
-  }
-
-  /* Bail out if tail goes too far or head is bigger than tail */
-  if (tail > len || head > tail) {
-    g_free (tmp);
-    return -1;
-  }
-
-  /* Minutes */
-  memcpy (tmp, time + head, tail - head);
-  tmp[2] = '\0';
-  result += 60 * atoi (tmp);
-
-  /* The next colon should again be exactly 2 chars right */
-  head = tail + 1;
-  tail = head + 2;
-
-  /* Bail out if tail goes too far or head is bigger than tail */
-  if (tail > len || head > tail) {
-    g_free (tmp);
-    return -1;
-  }
-
-  /* Extract seconds */
-  memcpy (tmp, time + head, tail - head);
-  tmp[2] = '\0';
-  result += atoi(tmp);
-  g_free (tmp);
-
-  return result;
-
-}
-
-static gboolean
-is_image (xmlNode *node)
-{
-  gchar *mime_type;
-  gboolean ret;
-
-  mime_type = didl_res_get_protocol_info (node, 2);
-  ret = g_str_has_prefix (mime_type, "image/");
-
-  g_free (mime_type);
-  return ret;
-}
-
-static gboolean
-is_http_get (xmlNode *node)
-{
-  gboolean ret;
-  gchar *protocol;
-
-  protocol = didl_res_get_protocol_info (node, 0);
-  ret = g_str_has_prefix (protocol, "http-get");
-
-  g_free (protocol);
-  return ret;
-}
-
-static gboolean
-has_thumbnail_marker (xmlNode *node)
-{
-  gchar *dlna_stuff;
-  gboolean ret;
-
-  dlna_stuff = didl_res_get_protocol_info (node, 3);
-  ret = strstr("JPEG_TN", dlna_stuff) != NULL;
-
-  g_free (dlna_stuff);
-  return ret;
-}
-
-static gchar *
-get_thumbnail (GList *nodes)
-{
-  GList *element;
-  gchar *val = NULL;
-  guint counter = 0;
-
-  /* chose, depending on availability, the first with DLNA.ORG_PN=JPEG_TN, or
-   * the last http-get with mimetype image/something if there is more than one
-   * http-get.
-   * This covers at least mediatomb and rygel.
-   * This could be improved by handling resolution and/or size */
-
-  for (element=nodes; element; element=g_list_next (element)) {
-    xmlNode *node = (xmlNode *)element->data;
-
-    if (is_http_get (node)) {
-      counter++;
-      if (is_image (node)) {
-        if (val)
-          g_free (val);
-        val = (gchar *) xmlNodeGetContent (node);
-
-        if (has_thumbnail_marker (node))  /* that's definitely it! */
-          return val;
-      }
-    }
-  }
-
-  if (val && counter == 1) {
-    /* There was only one element with http-get protocol: that's the uri of the
-     * media itself, not a thumbnail */
-    g_free (val);
-    val = NULL;
-  }
-
-  return val;
-}
-
-static gchar *
-get_value_for_key (GrlKeyID key_id,
-                   GUPnPDIDLLiteObject *didl,
-                   GList *props)
-{
-  GList* list;
-  gchar* val = NULL;
-  const gchar* upnp_key;
-
-  xmlNode *didl_node = gupnp_didl_lite_object_get_xml_node (didl);
-  upnp_key = get_upnp_key (key_id);
-
-  if (key_id == GRL_METADATA_KEY_CHILDCOUNT) {
-    val = (gchar *) xmlGetProp (didl_node,
-                                (const xmlChar *) "childCount");
-  } else if (key_id == GRL_METADATA_KEY_MIME && props) {
-    val = didl_res_get_protocol_info ((xmlNode *) props->data, 2);
-  } else if (key_id == GRL_METADATA_KEY_DURATION && props) {
-    val = (gchar *) xmlGetProp ((xmlNodePtr) props->data,
-                                (const xmlChar *) "duration");
-  } else if (key_id == GRL_METADATA_KEY_URL && props) {
-    val = (gchar *) xmlNodeGetContent ((xmlNode *) props->data);
-  } else if (key_id == GRL_METADATA_KEY_THUMBNAIL && props) {
-    val = g_strdup (gupnp_didl_lite_object_get_album_art (didl));
-    if (!val)
-      val = get_thumbnail (props);
-  } else if (upnp_key) {
-    list = gupnp_didl_lite_object_get_properties (didl, upnp_key);
-    if (list) {
-      val = (gchar *) xmlNodeGetContent ((xmlNode*) list->data);
-      g_list_free (list);
-    } else if (props && props->data) {
-      val = (gchar *) xmlGetProp ((xmlNodePtr) props->data,
-                                  (const xmlChar *) upnp_key);
-    }
-  }
-
-  return val;
-}
-
-static void
-set_metadata_value (GrlData *data,
-                    GrlKeyID key_id,
-                    const gchar *value)
-{
-  if (key_id == GRL_METADATA_KEY_DURATION) {
-    gint duration = didl_h_mm_ss_to_int (value);
-    if (duration >= 0) {
-      grl_data_set_int (data, GRL_METADATA_KEY_DURATION, duration);
-    }
-  } else if (key_id == GRL_METADATA_KEY_CHILDCOUNT && value) {
-    grl_data_set_int (data, GRL_METADATA_KEY_CHILDCOUNT, atoi (value));
-  } else {
-    grl_data_set_string (data, key_id, value);
-  }
-}
-
-static GrlMedia *
-build_media_from_didl (GrlMedia *content,
-                       GUPnPDIDLLiteObject *didl_node,
-                       GList *keys)
-{
-  const gchar *id;
-  const gchar *class;
-
-  GrlMedia *media = NULL;
-  GList *didl_props;
-  GList *iter;
-
-  GRL_DEBUG ("build_media_from_didl");
-
-  if (content) {
-    media = content;
-  } else {
-
-    if (GUPNP_IS_DIDL_LITE_CONTAINER (didl_node)) {
-      media = grl_media_box_new ();
-    } else {
-      if (!media) {
-        class = gupnp_didl_lite_object_get_upnp_class (didl_node);
-        if (class) {
-          if (g_str_has_prefix (class, "object.item.audioItem")) {
-            media = grl_media_audio_new ();
-          } else if (g_str_has_prefix (class, "object.item.videoItem")) {
-            media = grl_media_video_new ();
-          } else if (g_str_has_prefix (class, "object.item.imageItem")) {
-            media = grl_media_image_new ();
-          } else {
-            media = grl_media_new ();
-          }
-        } else {
-          media = grl_media_new ();
-        }
-      }
-    }
-  }
-
-  id = gupnp_didl_lite_object_get_id (didl_node);
-  /* Root category's id is always NULL */
-  if (g_strcmp0 (id, "0") == 0) {
-    grl_media_set_id (media, NULL);
-  } else {
-    grl_media_set_id (media, id);
-  }
-
-  didl_props = didl_get_supported_resources (didl_node);
-
-  iter = keys;
-  while (iter) {
-    gchar *value = get_value_for_key (iter->data, didl_node, didl_props);
-    if (value) {
-      set_metadata_value (GRL_DATA (media), iter->data, value);
-    }
-    iter = g_list_next (iter);
-  }
-
-  g_list_free (didl_props);
-
-  return media;
-}
-
-static void
-gupnp_browse_result_cb (GUPnPDIDLLiteParser *parser,
-			GUPnPDIDLLiteObject *didl,
-			gpointer user_data)
-{
-  GrlMedia *media;
-  struct OperationSpec *os = (struct OperationSpec *) user_data;
-  if (gupnp_didl_lite_object_get_id (didl)) {
-    media = build_media_from_didl (NULL, didl, os->keys);
-    os->callback (os->source,
-		  os->operation_id,
-		  media,
-		  --os->count,
-		  os->user_data,
-		  NULL);
-  }
-}
-
-static void
-gupnp_browse_cb (GUPnPServiceProxy *service,
-		 GUPnPServiceProxyAction *action,
-		 gpointer user_data)
-{
-  GError *error = NULL;
-  gchar *didl = NULL;
-  guint returned = 0;
-  guint matches = 0;
-  gboolean result;
-  struct OperationSpec *os;
-  GUPnPDIDLLiteParser *didl_parser;
-
-  GRL_DEBUG ("gupnp_browse_cb");
-
-  os = (struct OperationSpec *) user_data;
-  didl_parser = gupnp_didl_lite_parser_new ();
-
-  result =
-    gupnp_service_proxy_end_action (service, action, &error,
-				    "Result", G_TYPE_STRING, &didl,
-				    "NumberReturned", G_TYPE_UINT, &returned,
-				    "TotalMatches", G_TYPE_UINT, &matches,
-				    NULL);
-
-  if (!result) {
-    GRL_WARNING ("Operation (browse, search or query) failed");
-    os->callback (os->source, os->operation_id, NULL, 0, os->user_data, error);
-    if (error) {
-      GRL_WARNING ("  Reason: %s", error->message);
-      g_error_free (error);
-    }
-
-    goto free_resources;
-  }
-
-  if (!didl || !returned) {
-    GRL_DEBUG ("Got no results");
-    os->callback (os->source, os->operation_id, NULL, 0, os->user_data, NULL);
-
-    goto free_resources;
-  }
-
-  /* Use os->count to emit "remaining" information */
-  if (os->count > returned) {
-    os->count = returned;
-  }
-
-  g_signal_connect (G_OBJECT (didl_parser),
-                    "object-available",
-                    G_CALLBACK (gupnp_browse_result_cb),
-                    os);
-  gupnp_didl_lite_parser_parse_didl (didl_parser,
-                                     didl,
-                                     &error);
-  if (error) {
-    GRL_WARNING ("Failed to parse DIDL result: %s", error->message);
-    os->callback (os->source, os->operation_id, NULL, 0, os->user_data, error);
-    g_error_free (error);
-
-    goto free_resources;
-  }
-
- free_resources:
-  g_slice_free (struct OperationSpec, os);
-  g_free (didl);
-  g_object_unref (didl_parser);
-}
-
-static void
-gupnp_metadata_result_cb (GUPnPDIDLLiteParser *parser,
-			  GUPnPDIDLLiteObject *didl,
-			  gpointer user_data)
-{
-  GrlMediaSourceMetadataSpec *ms = (GrlMediaSourceMetadataSpec *) user_data;
-  if (gupnp_didl_lite_object_get_id (didl)) {
-    build_media_from_didl (ms->media, didl, ms->keys);
-    ms->callback (ms->source, ms->media, ms->user_data, NULL);
-  }
-}
-
-static void
-gupnp_metadata_cb (GUPnPServiceProxy *service,
-		   GUPnPServiceProxyAction *action,
-		   gpointer user_data)
-{
-  GError *error = NULL;
-  gchar *didl = NULL;
-  gboolean result;
-  GrlMediaSourceMetadataSpec *ms;
-  GUPnPDIDLLiteParser *didl_parser;
-
-  GRL_DEBUG ("gupnp_metadata_cb");
-
-  ms = (GrlMediaSourceMetadataSpec *) user_data;
-  didl_parser = gupnp_didl_lite_parser_new ();
-
-  result =
-    gupnp_service_proxy_end_action (service, action, &error,
-				    "Result", G_TYPE_STRING, &didl,
-				    NULL);
-
-  if (!result) {
-    GRL_WARNING ("Metadata operation failed");
-    ms->callback (ms->source, ms->media, ms->user_data, error);
-    if (error) {
-      GRL_WARNING ("  Reason: %s", error->message);
-      g_error_free (error);
-    }
-
-    goto free_resources;
-  }
-
-  if (!didl) {
-    GRL_DEBUG ("Got no metadata");
-    ms->callback (ms->source, ms->media,  ms->user_data, NULL);
-
-    goto free_resources;
-  }
-
-  g_signal_connect (G_OBJECT (didl_parser),
-                    "object-available",
-                    G_CALLBACK (gupnp_metadata_result_cb),
-                    ms);
-  gupnp_didl_lite_parser_parse_didl (didl_parser,
-                                     didl,
-                                     &error);
-  if (error) {
-    GRL_WARNING ("Failed to parse DIDL result: %s", error->message);
-    ms->callback (ms->source, ms->media, ms->user_data, error);
-    g_error_free (error);
-    goto free_resources;
-  }
-
- free_resources:
-  g_free (didl);
-  g_object_unref (didl_parser);
-}
-
-/* ================== API Implementation ================ */
-
-static const GList *
-grl_upnp_source_supported_keys (GrlMetadataSource *source)
-{
-  static GList *keys = NULL;
-  if (!keys) {
-    keys = grl_metadata_key_list_new (GRL_METADATA_KEY_ID,
-                                      GRL_METADATA_KEY_TITLE,
-                                      GRL_METADATA_KEY_URL,
-                                      GRL_METADATA_KEY_MIME,
-                                      GRL_METADATA_KEY_DATE,
-                                      GRL_METADATA_KEY_DURATION,
-                                      GRL_METADATA_KEY_ARTIST,
-                                      GRL_METADATA_KEY_ALBUM,
-                                      GRL_METADATA_KEY_GENRE,
-                                      GRL_METADATA_KEY_CHILDCOUNT,
-                                      GRL_METADATA_KEY_THUMBNAIL,
-                                      NULL);
-  }
-  return keys;
-}
-
-static void
-grl_upnp_source_browse (GrlMediaSource *source, GrlMediaSourceBrowseSpec *bs)
-{
-  GUPnPServiceProxyAction* action;
-  gchar *upnp_filter;
-  gchar *container_id;
-  GError *error = NULL;
-  struct OperationSpec *os;
-
-  GRL_DEBUG ("grl_upnp_source_browse");
-
-  upnp_filter = get_upnp_filter (bs->keys);
-  GRL_DEBUG ("filter: '%s'", upnp_filter);
-
-  os = g_slice_new0 (struct OperationSpec);
-  os->source = bs->source;
-  os->operation_id = bs->browse_id;
-  os->keys = bs->keys;
-  os->skip = bs->skip;
-  os->count = bs->count;
-  os->callback = bs->callback;
-  os->user_data = bs->user_data;
-
-  container_id = (gchar *) grl_media_get_id (bs->container);
-  if (!container_id) {
-    container_id = "0";
-  }
-
-  action =
-    gupnp_service_proxy_begin_action (GRL_UPNP_SOURCE (source)->priv->service,
-				      "Browse", gupnp_browse_cb,
-                                      os,
-				      "ObjectID", G_TYPE_STRING,
-                                      container_id,
-				      "BrowseFlag", G_TYPE_STRING,
-                                      "BrowseDirectChildren",
-				      "Filter", G_TYPE_STRING,
-                                      upnp_filter,
-				      "StartingIndex", G_TYPE_UINT,
-                                      bs->skip,
-				      "RequestedCount", G_TYPE_UINT,
-                                      bs->count,
-				      "SortCriteria", G_TYPE_STRING,
-                                      "",
-				      NULL);
-  if (!action) {
-    error = g_error_new (GRL_CORE_ERROR,
-			 GRL_CORE_ERROR_BROWSE_FAILED,
-			 "Failed to start browse action");
-    bs->callback (bs->source, bs->browse_id, NULL, 0, bs->user_data, error);
-    g_error_free (error);
-    g_slice_free (struct OperationSpec, os);
-  }
-
-  g_free (upnp_filter);
-}
-
-static void
-grl_upnp_source_search (GrlMediaSource *source, GrlMediaSourceSearchSpec *ss)
-{
-  GUPnPServiceProxyAction* action;
-  gchar *upnp_filter;
-  GError *error = NULL;
-  gchar *upnp_search;
-  struct OperationSpec *os;
-
-  GRL_DEBUG ("grl_upnp_source_search");
-
-  upnp_filter = get_upnp_filter (ss->keys);
-  GRL_DEBUG ("filter: '%s'", upnp_filter);
-
-  upnp_search = get_upnp_search (ss->text);
-  GRL_DEBUG ("search: '%s'", upnp_search);
-
-  os = g_slice_new0 (struct OperationSpec);
-  os->source = ss->source;
-  os->operation_id = ss->search_id;
-  os->keys = ss->keys;
-  os->skip = ss->skip;
-  os->count = ss->count;
-  os->callback = ss->callback;
-  os->user_data = ss->user_data;
-
-  action =
-    gupnp_service_proxy_begin_action (GRL_UPNP_SOURCE (source)->priv->service,
-				      "Search", gupnp_browse_cb,
-                                      os,
-				      "ContainerID", G_TYPE_STRING,
-                                      "0",
-				      "SearchCriteria", G_TYPE_STRING,
-                                      upnp_search,
-				      "Filter", G_TYPE_STRING,
-                                      upnp_filter,
-				      "StartingIndex", G_TYPE_UINT,
-                                      ss->skip,
-				      "RequestedCount", G_TYPE_UINT,
-                                      ss->count,
-				      "SortCriteria", G_TYPE_STRING,
-                                      "",
-				      NULL);
-  if (!action) {
-    error = g_error_new (GRL_CORE_ERROR,
-			 GRL_CORE_ERROR_SEARCH_FAILED,
-			 "Failed to start browse action");
-    ss->callback (ss->source, ss->search_id, NULL, 0, ss->user_data, error);
-    g_error_free (error);
-    g_slice_free (struct OperationSpec, os);
-  }
-
-  g_free (upnp_filter);
-  g_free (upnp_search);
-}
-
-/*
- * Query format is the UPnP ContentDirectory SearchCriteria format, e.g.
- * 'upnp:artist contains "Rick Astley" and
- *  (upnp:class derivedfrom "object.item.audioItem")'
- *
- * Note that we don't guarantee or check that the server actually
- * supports the given criteria. Offering the searchcaps as
- * additional metadata to clients that _really_ are interested might
- * be useful.
- */
-static void
-grl_upnp_source_query (GrlMediaSource *source, GrlMediaSourceQuerySpec *qs)
-{
-  GUPnPServiceProxyAction* action;
-  gchar *upnp_filter;
-  GError *error = NULL;
-  struct OperationSpec *os;
-
-  GRL_DEBUG (__func__);
-
-  upnp_filter = get_upnp_filter (qs->keys);
-  GRL_DEBUG ("filter: '%s'", upnp_filter);
-
-  GRL_DEBUG ("query: '%s'", qs->query);
-
-  os = g_slice_new0 (struct OperationSpec);
-  os->source = qs->source;
-  os->operation_id = qs->query_id;
-  os->keys = qs->keys;
-  os->skip = qs->skip;
-  os->count = qs->count;
-  os->callback = qs->callback;
-  os->user_data = qs->user_data;
-
-  action =
-    gupnp_service_proxy_begin_action (GRL_UPNP_SOURCE (source)->priv->service,
-				      "Search", gupnp_browse_cb, os,
-				      "ContainerID", G_TYPE_STRING,
-				      "0",
-				      "SearchCriteria", G_TYPE_STRING,
-				      qs->query,
-				      "Filter", G_TYPE_STRING,
-				      upnp_filter,
-				      "StartingIndex", G_TYPE_UINT,
-				      qs->skip,
-				      "RequestedCount", G_TYPE_UINT,
-				      qs->count,
-				      "SortCriteria", G_TYPE_STRING,
-				      "",
-				      NULL);
-  if (!action) {
-    error = g_error_new (GRL_CORE_ERROR,
-			 GRL_CORE_ERROR_QUERY_FAILED,
-			 "Failed to start query action");
-    qs->callback (qs->source, qs->query_id, NULL, 0, qs->user_data, error);
-    g_error_free (error);
-    g_slice_free (struct OperationSpec, os);
-  }
-
-  g_free (upnp_filter);
-}
-
-static void
-grl_upnp_source_metadata (GrlMediaSource *source,
-                          GrlMediaSourceMetadataSpec *ms)
-{
-  GUPnPServiceProxyAction* action;
-  gchar *upnp_filter;
-  gchar *id;
-  GError *error = NULL;
-
-  GRL_DEBUG ("grl_upnp_source_metadata");
-
-  upnp_filter = get_upnp_filter (ms->keys);
-
-  GRL_DEBUG ("filter: '%s'", upnp_filter);
-
-  id = (gchar *) grl_media_get_id (ms->media);
-  if (!id) {
-    grl_media_set_title (ms->media, GRL_UPNP_SOURCE (source)->priv->upnp_name);
-    id = "0";
-  }
-
-  action =
-    gupnp_service_proxy_begin_action (GRL_UPNP_SOURCE (source)->priv->service,
-				      "Browse", gupnp_metadata_cb,
-                                      ms,
-				      "ObjectID", G_TYPE_STRING,
-                                      id,
-				      "BrowseFlag", G_TYPE_STRING,
-                                      "BrowseMetadata",
-				      "Filter", G_TYPE_STRING,
-                                      upnp_filter,
-				      "StartingIndex", G_TYPE_UINT,
-                                      0,
-				      "RequestedCount", G_TYPE_UINT,
-                                      0,
-				      "SortCriteria", G_TYPE_STRING,
-                                      "",
-				      NULL);
-  if (!action) {
-    error = g_error_new (GRL_CORE_ERROR,
-			 GRL_CORE_ERROR_METADATA_FAILED,
-			 "Failed to start metadata action");
-    ms->callback (ms->source, ms->media, ms->user_data, error);
-    g_error_free (error);
-  }
-
-  g_free (upnp_filter);
-}
-
-static GrlSupportedOps
-grl_upnp_source_supported_operations (GrlMetadataSource *metadata_source)
-{
-  GrlSupportedOps caps;
-  GrlUpnpSource *source;
-
-  /* Some sources may support search() while other not, so we rewrite
-     supported_operations() to take that into account.
-     See also note in grl_upnp_source_query() */
-
-  source = GRL_UPNP_SOURCE (metadata_source);
-  caps = GRL_OP_BROWSE | GRL_OP_METADATA | GRL_OP_NOTIFY_CHANGE;
-  if (source->priv->search_enabled)
-    caps = caps | GRL_OP_SEARCH | GRL_OP_QUERY;
-
-  return caps;
-}
-
-static gboolean
-grl_upnp_source_notify_change_start (GrlMediaSource *source,
-                                     GError **error)
-{
-  GrlUpnpSource *upnp_source = GRL_UPNP_SOURCE (source);
-
-  if (!gupnp_service_proxy_add_notify (upnp_source->priv->service,
-                                       "ContainerUpdateIDs",
-                                       G_TYPE_STRING,
-                                       container_changed_cb,
-                                       source)) {
-    g_set_error (error,
-                 GRL_CORE_ERROR,
-                 GRL_CORE_ERROR_NOTIFY_CHANGED_FAILED,
-                 "Unable to listen for changes in %s",
-                 grl_metadata_source_get_id (GRL_METADATA_SOURCE (source)));
-    return FALSE;
-  }
-  gupnp_service_proxy_set_subscribed (upnp_source->priv->service, TRUE);
-
-  return TRUE;
-}
-
-
-static gboolean
-grl_upnp_source_notify_change_stop (GrlMediaSource *source,
-                                    GError **error)
-{
-  GrlUpnpSource *upnp_source = GRL_UPNP_SOURCE (source);
-
-  gupnp_service_proxy_set_subscribed (upnp_source->priv->service, FALSE);
-  gupnp_service_proxy_remove_notify (upnp_source->priv->service,
-                                     "ContainerUpdateIDs",
-                                     container_changed_cb,
-                                     source);
-
-  return TRUE;
-}
diff --git a/src/upnp/grl-upnp.h b/src/upnp/grl-upnp.h
deleted file mode 100644
index e0bc748..0000000
--- a/src/upnp/grl-upnp.h
+++ /dev/null
@@ -1,75 +0,0 @@
-/*
- * Copyright (C) 2010 Igalia S.L.
- *
- * Contact: Iago Toral Quiroga <itoral igalia 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_UPNP_SOURCE_H_
-#define _GRL_UPNP_SOURCE_H_
-
-#include <grilo.h>
-
-#define GRL_UPNP_SOURCE_TYPE                    \
-  (grl_upnp_source_get_type ())
-
-#define GRL_UPNP_SOURCE(obj)                            \
-  (G_TYPE_CHECK_INSTANCE_CAST ((obj),                   \
-                               GRL_UPNP_SOURCE_TYPE,    \
-                               GrlUpnpSource))
-
-#define GRL_IS_UPNP_SOURCE(obj)                         \
-  (G_TYPE_CHECK_INSTANCE_TYPE ((obj),                   \
-                               GRL_UPNP_SOURCE_TYPE))
-
-#define GRL_UPNP_SOURCE_CLASS(klass)                    \
-  (G_TYPE_CHECK_CLASS_CAST((klass),                     \
-                           GRL_UPNP_SOURCE_TYPE,        \
-                           GrlUpnpSourceClass))
-
-#define GRL_IS_UPNP_SOURCE_CLASS(klass)                 \
-  (G_TYPE_CHECK_CLASS_TYPE((klass),                     \
-                           GRL_UPNP_SOURCE_TYPE))
-
-#define GRL_UPNP_SOURCE_GET_CLASS(obj)                  \
-  (G_TYPE_INSTANCE_GET_CLASS ((obj),                    \
-                              GRL_UPNP_SOURCE_TYPE,     \
-                              GrlUpnpSourceClass))
-
-typedef struct _GrlUpnpPrivate GrlUpnpPrivate;
-typedef struct _GrlUpnpSource  GrlUpnpSource;
-
-struct _GrlUpnpSource {
-
-  GrlMediaSource parent;
-
-  /*< private >*/
-  GrlUpnpPrivate *priv;
-};
-
-typedef struct _GrlUpnpSourceClass GrlUpnpSourceClass;
-
-struct _GrlUpnpSourceClass {
-
-  GrlMediaSourceClass parent_class;
-
-};
-
-GType grl_upnp_source_get_type (void);
-
-#endif /* _GRL_UPNP_SOURCE_H_ */
diff --git a/src/upnp/grl-upnp.xml b/src/upnp/grl-upnp.xml
deleted file mode 100644
index 3617b98..0000000
--- a/src/upnp/grl-upnp.xml
+++ /dev/null
@@ -1,9 +0,0 @@
-<plugin>
-  <info>
-    <name>UPnP</name>
-    <description>A plugin for browsing UPnP servers</description>
-    <author>Igalia S.L.</author>
-    <license>LGPL</license>
-    <site>http://www.igalia.com</site>
-  </info>
-</plugin>
diff --git a/src/vimeo/Makefile.am b/src/vimeo/Makefile.am
deleted file mode 100644
index b810218..0000000
--- a/src/vimeo/Makefile.am
+++ /dev/null
@@ -1,42 +0,0 @@
-#
-# Makefile.am
-#
-# Author: Joaquim Rocha <jrocha igalia com>
-#
-# Copyright (C) 2010, 2011 Igalia S.L. All rights reserved.
-
-lib_LTLIBRARIES		 = libgrlvimeo.la
-
-libgrlvimeo_la_CFLAGS =	\
-	$(DEPS_CFLAGS)		\
-	$(XML_CFLAGS)		\
-	$(GTHREAD_CFLAGS)	\
-	$(LIBSOUP_CFLAGS)
-
-libgrlvimeo_la_LIBADD =	\
-	$(DEPS_LIBS)		\
-	$(XML_LIBS)		\
-	$(GTHREAD_LIBS)		\
-	$(LIBSOUP_LIBS)
-
-libgrlvimeo_la_LDFLAGS = \
-	-module		 \
-	-avoid-version
-
-libgrlvimeo_la_SOURCES =	\
-	grl-vimeo.c		\
-	grl-vimeo.h		\
-	gvimeo.c		\
-	gvimeo.h
-
-libdir=$(GRL_PLUGINS_DIR)
-vimeoxmldir	= $(GRL_PLUGINS_CONF_DIR)
-vimeoxml_DATA	= $(VIMEO_PLUGIN_ID).xml
-
-EXTRA_DIST = $(vimeoxml_DATA)
-
-MAINTAINERCLEANFILES =	\
-	*.in		\
-	*~
-
-DISTCLEANFILES = $(MAINTAINERCLEANFILES)
diff --git a/src/vimeo/grl-vimeo.c b/src/vimeo/grl-vimeo.c
deleted file mode 100644
index a235217..0000000
--- a/src/vimeo/grl-vimeo.c
+++ /dev/null
@@ -1,413 +0,0 @@
-/*
- * Copyright (C) 2010 Igalia S.L.
- * Copyright (C) 2011 Intel Corporation.
- *
- * Contact: Iago Toral Quiroga <itoral igalia com>
- *
- * Authors: Joaquim Rocha <jrocha igalia 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 <grilo.h>
-#include <string.h>
-#include <stdlib.h>
-#include <errno.h>
-
-#include "grl-vimeo.h"
-#include "gvimeo.h"
-
-#define GRL_VIMEO_SOURCE_GET_PRIVATE(object)                           \
-  (G_TYPE_INSTANCE_GET_PRIVATE((object),                                \
-                               GRL_VIMEO_SOURCE_TYPE,                  \
-                               GrlVimeoSourcePrivate))
-
-/* --------- Logging  -------- */
-
-#define GRL_LOG_DOMAIN_DEFAULT vimeo_log_domain
-GRL_LOG_DOMAIN_STATIC(vimeo_log_domain);
-
-/* --- Plugin information --- */
-
-#define PLUGIN_ID   VIMEO_PLUGIN_ID
-
-#define SOURCE_ID   "grl-vimeo"
-#define SOURCE_NAME "Vimeo"
-#define SOURCE_DESC "A source for browsing and searching Vimeo videos"
-
-#define AUTHOR      "Igalia S.L."
-#define LICENSE     "LGPL"
-#define SITE        "http://www.igalia.com";
-
-typedef struct {
-  GrlMediaSourceSearchSpec *ss;
-  gint offset;
-  gint page;
-} SearchData;
-
-struct _GrlVimeoSourcePrivate {
-  GVimeo *vimeo;
-};
-
-static GrlVimeoSource *grl_vimeo_source_new (void);
-
-gboolean grl_vimeo_plugin_init (GrlPluginRegistry *registry,
-				const GrlPluginInfo *plugin,
-				GList *configs);
-
-static const GList *grl_vimeo_source_supported_keys (GrlMetadataSource *source);
-
-static void grl_vimeo_source_metadata (GrlMediaSource *source,
-				       GrlMediaSourceMetadataSpec *ss);
-
-static void grl_vimeo_source_search (GrlMediaSource *source,
-				     GrlMediaSourceSearchSpec *ss);
-
-/* =================== Vimeo Plugin  =============== */
-
-gboolean
-grl_vimeo_plugin_init (GrlPluginRegistry *registry,
-                        const GrlPluginInfo *plugin,
-                        GList *configs)
-{
-  gchar *vimeo_key;
-  gchar *vimeo_secret;
-  GrlConfig *config;
-  gint config_count;
-  gboolean init_result = FALSE;
-  GrlVimeoSource *source;
-
-  GRL_LOG_DOMAIN_INIT (vimeo_log_domain, "vimeo");
-
-  GRL_DEBUG ("vimeo_plugin_init");
-
-  if (!g_thread_supported ()) {
-    g_thread_init (NULL);
-  }
-
-  if (!configs) {
-    GRL_WARNING ("Missing configuration");
-    return FALSE;
-  }
-
-  config_count = g_list_length (configs);
-  if (config_count > 1) {
-    GRL_WARNING ("Provided %d configs, but will only use one", config_count);
-  }
-
-  config = GRL_CONFIG (configs->data);
-
-  vimeo_key = grl_config_get_api_key (config);
-  vimeo_secret = grl_config_get_api_secret (config);
-
-  if (!vimeo_key || !vimeo_secret) {
-    GRL_WARNING ("Required configuration keys not set up");
-    goto go_out;
-  }
-
-  source = grl_vimeo_source_new ();
-  source->priv->vimeo = g_vimeo_new (vimeo_key, vimeo_secret);
-
-  grl_plugin_registry_register_source (registry,
-                                       plugin,
-                                       GRL_MEDIA_PLUGIN (source),
-                                       NULL);
-  init_result = TRUE;
-
- go_out:
-
-  if (vimeo_key != NULL)
-    g_free (vimeo_key);
-  if (vimeo_secret != NULL)
-    g_free (vimeo_secret);
-
-  return init_result;
-}
-
-GRL_PLUGIN_REGISTER (grl_vimeo_plugin_init,
-                     NULL,
-                     PLUGIN_ID);
-
-/* ================== Vimeo GObject ================ */
-
-static GrlVimeoSource *
-grl_vimeo_source_new (void)
-{
-  GRL_DEBUG ("grl_vimeo_source_new");
-
-  return g_object_new (GRL_VIMEO_SOURCE_TYPE,
-                       "source-id", SOURCE_ID,
-                       "source-name", SOURCE_NAME,
-                       "source-desc", SOURCE_DESC,
-                       NULL);
-}
-
-static void
-grl_vimeo_source_class_init (GrlVimeoSourceClass * klass)
-{
-  GrlMediaSourceClass *source_class = GRL_MEDIA_SOURCE_CLASS (klass);
-  GrlMetadataSourceClass *metadata_class = GRL_METADATA_SOURCE_CLASS (klass);
-
-  source_class->metadata = grl_vimeo_source_metadata;
-  source_class->search = grl_vimeo_source_search;
-  metadata_class->supported_keys = grl_vimeo_source_supported_keys;
-
-  g_type_class_add_private (klass, sizeof (GrlVimeoSourcePrivate));
-}
-
-static void
-grl_vimeo_source_init (GrlVimeoSource *source)
-{
-  source->priv = GRL_VIMEO_SOURCE_GET_PRIVATE (source);
-}
-
-G_DEFINE_TYPE (GrlVimeoSource, grl_vimeo_source, GRL_TYPE_MEDIA_SOURCE);
-
-/* ======================= Utilities ==================== */
-
-static gint
-str_to_gint (gchar *str)
-{
-  gint number;
-
-  errno = 0;
-  number = (gint) g_ascii_strtod (str, NULL);
-  if (errno == 0)
-  {
-    return number;
-  }
-  return 0;
-}
-
-static void
-update_media (GrlMedia *media, GHashTable *video)
-{
-  gchar *str;
-
-  str = g_hash_table_lookup (video, VIMEO_VIDEO_ID);
-  if (str)
-  {
-    grl_media_set_id (media, str);
-  }
-
-  str = g_hash_table_lookup (video, VIMEO_VIDEO_TITLE);
-  if (str)
-  {
-    grl_media_set_title (media, str);
-  }
-
-  str = g_hash_table_lookup (video, VIMEO_VIDEO_DESCRIPTION);
-  if (str)
-  {
-    grl_media_set_description (media, str);
-  }
-
-  str = g_hash_table_lookup (video, VIMEO_VIDEO_DURATION);
-  if (str)
-  {
-    grl_media_set_duration (media, str_to_gint (str));
-  }
-
-  str = g_hash_table_lookup (video, VIMEO_VIDEO_OWNER_NAME);
-  if (str)
-  {
-    grl_media_set_author (media, str);
-  }
-
-  str = g_hash_table_lookup (video, VIMEO_VIDEO_UPLOAD_DATE);
-  if (str)
-  {
-    grl_media_set_date (media, str);
-  }
-
-  str = g_hash_table_lookup (video, VIMEO_VIDEO_THUMBNAIL);
-  if (str)
-  {
-    grl_media_set_thumbnail (media, str);
-  }
-
-  str = g_hash_table_lookup (video, VIMEO_VIDEO_WIDTH);
-  if (str)
-  {
-    grl_media_video_set_width (GRL_MEDIA_VIDEO (media), str_to_gint (str));
-  }
-
-  str = g_hash_table_lookup (video, VIMEO_VIDEO_HEIGHT);
-  if (str)
-  {
-    grl_media_video_set_height (GRL_MEDIA_VIDEO (media), str_to_gint (str));
-  }
-}
-
-
-static void
-search_cb (GVimeo *vimeo, GList *video_list, gpointer user_data)
-{
-  GrlMedia *media = NULL;
-  SearchData *sd = (SearchData *) user_data;
-  gint count = sd->ss->count;
-  gchar *media_type;
-
-  /* Go to offset element */
-  video_list = g_list_nth (video_list, sd->offset);
-
-  /* No more elements can be sent */
-  if (!video_list) {
-    sd->ss->callback (sd->ss->source,
-                      sd->ss->search_id,
-                      NULL,
-                      0,
-                      sd->ss->user_data,
-                      NULL);
-    g_slice_free (SearchData, sd);
-    return;
-  }
-
-  while (video_list && count)
-  {
-    media_type = g_hash_table_lookup (video_list->data, "title");
-    if (media_type) {
-      media = grl_media_video_new ();
-    }
-
-    if (media)
-    {
-      update_media (media, video_list->data);
-      sd->ss->callback (sd->ss->source,
-			sd->ss->search_id,
-			media,
-			sd->ss->count == 1? 0: -1,
-			sd->ss->user_data,
-			NULL);
-    }
-    video_list = g_list_next (video_list);
-
-    if (--count)
-      sd->ss->count = count;
-
-    media = NULL;
-  }
-
-  /* Get more elements */
-  if (count)
-  {
-    sd->offset = 0;
-    sd->page++;
-    g_vimeo_videos_search (vimeo, sd->ss->text, sd->page, search_cb, sd);
-  }
-  else
-  {
-    g_slice_free (SearchData, sd);
-  }
-}
-
-static void
-video_get_play_url_cb (gchar *url, gpointer user_data)
-{
-  GrlMediaSourceMetadataSpec *ms = (GrlMediaSourceMetadataSpec *) user_data;
-
-  if (url)
-  {
-    grl_media_set_url (ms->media, url);
-  }
-
-  ms->callback (ms->source, ms->media, ms->user_data, NULL);
-}
-
-/* ================== API Implementation ================ */
-
-static const GList *
-grl_vimeo_source_supported_keys (GrlMetadataSource *source)
-{
-  static GList *keys = NULL;
-  if (!keys) {
-    keys = grl_metadata_key_list_new (GRL_METADATA_KEY_ID,
-				      GRL_METADATA_KEY_TITLE,
-				      GRL_METADATA_KEY_DESCRIPTION,
-				      GRL_METADATA_KEY_URL,
-				      GRL_METADATA_KEY_AUTHOR,
-                                      GRL_METADATA_KEY_DATE,
-				      GRL_METADATA_KEY_THUMBNAIL,
-				      GRL_METADATA_KEY_DURATION,
-				      GRL_METADATA_KEY_WIDTH,
-				      GRL_METADATA_KEY_HEIGHT,
-				      NULL);
-  }
-  return keys;
-}
-
-static void
-grl_vimeo_source_metadata (GrlMediaSource *source,
-			   GrlMediaSourceMetadataSpec *ms)
-{
-  gint id;
-  const gchar *id_str;
-
-  if (!ms->media || (id_str = grl_media_get_id (ms->media)) == NULL)
-  {
-    ms->callback (ms->source, ms->media, ms->user_data, NULL);
-    return;
-  }
-
-  errno = 0;
-  id = (gint) g_ascii_strtod (id_str, NULL);
-  if (errno != 0)
-  {
-    return;
-  }
-
-  g_vimeo_video_get_play_url (GRL_VIMEO_SOURCE (source)->priv->vimeo,
-			      id,
-			      video_get_play_url_cb,
-			      ms);
-}
-
-static void
-grl_vimeo_source_search (GrlMediaSource *source,
-			 GrlMediaSourceSearchSpec *ss)
-{
-  SearchData *sd;
-  GError *error;
-  gint per_page;
-  GVimeo *vimeo = GRL_VIMEO_SOURCE (source)->priv->vimeo;
-
-  if (!ss->text) {
-    /* Vimeo does not support searching all */
-    error =
-      g_error_new_literal (GRL_CORE_ERROR,
-                           GRL_CORE_ERROR_SEARCH_NULL_UNSUPPORTED,
-                           "Unable to execute search: non NULL search text is required");
-    ss->callback (ss->source, ss->search_id, NULL, 0, ss->user_data, error);
-    g_error_free (error);
-    return;
-  }
-
-  /* Compute items per page and page offset */
-  per_page = CLAMP (1 + ss->skip + ss->count, 0, 100);
-  g_vimeo_set_per_page (vimeo, per_page);
-
-  sd = g_slice_new (SearchData);
-  sd->page = 1 + (ss->skip / per_page);
-  sd->offset = ss->skip % per_page;
-  sd->ss = ss;
-
-  g_vimeo_videos_search (vimeo, ss->text, sd->page, search_cb, sd);
-}
diff --git a/src/vimeo/grl-vimeo.h b/src/vimeo/grl-vimeo.h
deleted file mode 100644
index fde4f36..0000000
--- a/src/vimeo/grl-vimeo.h
+++ /dev/null
@@ -1,77 +0,0 @@
-/*
- * Copyright (C) 2010 Igalia S.L.
- *
- * Contact: Iago Toral Quiroga <itoral igalia com>
- *
- * Authors: Joaquim Rocha <jrocha igalia 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_VIMEO_SOURCE_H_
-#define _GRL_VIMEO_SOURCE_H_
-
-#include <grilo.h>
-
-#define GRL_VIMEO_SOURCE_TYPE                  \
-  (grl_vimeo_source_get_type ())
-
-#define GRL_VIMEO_SOURCE(obj)                          \
-  (G_TYPE_CHECK_INSTANCE_CAST ((obj),                   \
-                               GRL_VIMEO_SOURCE_TYPE,  \
-                               GrlVimeoSource))
-
-#define GRL_IS_VIMEO_SOURCE(obj)                       \
-  (G_TYPE_CHECK_INSTANCE_TYPE ((obj),                   \
-                               GRL_VIMEO_SOURCE_TYPE))
-
-#define GRL_VIMEO_SOURCE_CLASS(klass)                  \
-  (G_TYPE_CHECK_CLASS_CAST((klass),                     \
-                           GRL_VIMEO_SOURCE_TYPE,      \
-                           GrlVimeoSourceClass))
-
-#define GRL_IS_VIMEO_SOURCE_CLASS(klass)               \
-  (G_TYPE_CHECK_CLASS_TYPE((klass),                     \
-                           GRL_VIMEO_SOURCE_TYPE))
-
-#define GRL_VIMEO_SOURCE_GET_CLASS(obj)                \
-  (G_TYPE_INSTANCE_GET_CLASS ((obj),                    \
-                              GRL_VIMEO_SOURCE_TYPE,   \
-                              GrlVimeoSourceClass))
-
-typedef struct _GrlVimeoSource        GrlVimeoSource;
-typedef struct _GrlVimeoSourcePrivate GrlVimeoSourcePrivate;
-
-struct _GrlVimeoSource {
-
-  GrlMediaSource parent;
-
-  /*< private >*/
-  GrlVimeoSourcePrivate *priv;
-};
-
-typedef struct _GrlVimeoSourceClass GrlVimeoSourceClass;
-
-struct _GrlVimeoSourceClass {
-
-  GrlMediaSourceClass parent_class;
-
-};
-
-GType grl_vimeo_source_get_type (void);
-
-#endif /* _GRL_VIMEO_SOURCE_H_ */
diff --git a/src/vimeo/grl-vimeo.xml b/src/vimeo/grl-vimeo.xml
deleted file mode 100644
index b80c9d9..0000000
--- a/src/vimeo/grl-vimeo.xml
+++ /dev/null
@@ -1,9 +0,0 @@
-<plugin>
-  <info>
-    <name>Vimeo</name>
-    <description>A plugin for searching Vimeo videos</description>
-    <author>Igalia S.L.</author>
-    <license>LGPL</license>
-    <site>http://www.igalia.com</site>
-  </info>
-</plugin>
diff --git a/src/vimeo/gvimeo.c b/src/vimeo/gvimeo.c
deleted file mode 100644
index 3b7f2fc..0000000
--- a/src/vimeo/gvimeo.c
+++ /dev/null
@@ -1,517 +0,0 @@
-/*
- * Copyright (C) 2010 Igalia S.L.
- *
- * Contact: Iago Toral Quiroga <itoral igalia com>
- *
- * Authors: Joaquim Rocha <jrocha igalia 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 <gcrypt.h>
-#include <libsoup/soup.h>
-#include "gvimeo.h"
-#include <libxml/parser.h>
-#include <libxml/xpath.h>
-
-#define G_VIMEO_GET_PRIVATE(object)             \
-  (G_TYPE_INSTANCE_GET_PRIVATE((object),        \
-                               G_VIMEO_TYPE,    \
-                               GVimeoPrivate))
-
-#define PLUGIN_USER_AGENT             "Grilo Vimeo Plugin"
-
-#define VIMEO_ENDPOINT                "http://vimeo.com/api/rest/v2";
-#define VIMEO_VIDEO_LOAD_URL          "http://vimeo.com/moogaloop/load/clip:";
-#define VIMEO_VIDEO_PLAY_URL          "http://vimeo.com/moogaloop/play/clip:";
-
-#define VIMEO_VIDEO_SEARCH_METHOD     "vimeo.videos.search"
-#define VIMEO_API_OAUTH_SIGN_METHOD   "HMAC-SHA1"
-#define VIMEO_API_OAUTH_SIGNATURE_PARAM "&oauth_signature=%s"
-
-#define VIMEO_VIDEO_SEARCH					\
-  "full_response=yes"						\
-  "&method=%s"							\
-  "&oauth_consumer_key=%s"					\
-  "&oauth_nonce=%s"						\
-  "&oauth_signature_method=" VIMEO_API_OAUTH_SIGN_METHOD	\
-  "&oauth_timestamp=%s"						\
-  "&oauth_token="						\
-  "&page=%d"							\
-  "&per_page=%d"						\
-  "&query=%s"
-
-typedef struct {
-  GVimeo *vimeo;
-  GVimeoVideoSearchCb search_cb;
-  gpointer user_data;
-} GVimeoVideoSearchData;
-
-typedef struct {
-  GVimeo *vimeo;
-  gint video_id;
-  GVimeoURLCb callback;
-  gpointer user_data;
-} GVimeoVideoURLData;
-
-struct _GVimeoPrivate {
-  gchar *api_key;
-  gchar *auth_token;
-  gchar *auth_secret;
-  gint per_page;
-  SoupSession *async_session;
-};
-
-enum InfoType {SIMPLE, EXTENDED};
-
-typedef struct {
-  enum InfoType type;
-  gchar *name;
-} VideoInfo;
-
-static VideoInfo video_info[] = {{SIMPLE, VIMEO_VIDEO_TITLE},
-				 {SIMPLE, VIMEO_VIDEO_DESCRIPTION},
-				 {SIMPLE, VIMEO_VIDEO_UPLOAD_DATE},
-				 {SIMPLE, VIMEO_VIDEO_WIDTH},
-				 {SIMPLE, VIMEO_VIDEO_HEIGHT},
-				 {SIMPLE, VIMEO_VIDEO_OWNER},
-				 {SIMPLE, VIMEO_VIDEO_URL},
-				 {SIMPLE, VIMEO_VIDEO_THUMBNAIL},
-				 {SIMPLE, VIMEO_VIDEO_DURATION},
-				 {EXTENDED, VIMEO_VIDEO_OWNER}};
-
-static void g_vimeo_finalize (GObject *object);
-static gchar * encode_uri (const gchar *uri);
-
-/* -------------------- GOBJECT -------------------- */
-
-G_DEFINE_TYPE (GVimeo, g_vimeo, G_TYPE_OBJECT);
-
-static void
-g_vimeo_class_init (GVimeoClass *klass)
-{
-  GObjectClass *gobject_class = G_OBJECT_CLASS (klass);
-  gobject_class->finalize = g_vimeo_finalize;
-
-  g_type_class_add_private (klass, sizeof (GVimeoPrivate));
-}
-
-static void
-g_vimeo_init (GVimeo *vimeo)
-{
-  vimeo->priv = G_VIMEO_GET_PRIVATE (vimeo);
-  vimeo->priv->per_page = 50;
-  vimeo->priv->async_session = soup_session_async_new ();
-}
-
-static void
-g_vimeo_finalize (GObject *object)
-{
-  GVimeo *vimeo = G_VIMEO (object);
-  g_free (vimeo->priv->api_key);
-  g_free (vimeo->priv->auth_secret);
-  g_object_unref (vimeo->priv->async_session);
-
-  G_OBJECT_CLASS (g_vimeo_parent_class)->finalize (object);
-}
-
-GVimeo *
-g_vimeo_new (const gchar *api_key, const gchar *auth_secret)
-{
-  GVimeo *vimeo = g_object_new (G_VIMEO_TYPE, NULL);
-  vimeo->priv->api_key = g_strdup (api_key);
-  vimeo->priv->auth_secret = g_strdup (auth_secret);
-
-  return vimeo;
-}
-
-/* -------------------- PRIVATE API -------------------- */
-
-static gchar *
-get_timestamp (void)
-{
-  time_t t = time (NULL);
-  return g_strdup_printf ("%d", (gint) t);
-}
-
-static gchar *
-get_nonce (void)
-{
-  gchar *timestamp = get_timestamp();
-  guint rnd_number = g_random_int ();
-  gchar *rnd_str = g_strdup_printf ("%d_%s", rnd_number, timestamp);
-  gchar *nonce = g_compute_checksum_for_string (G_CHECKSUM_MD5, rnd_str, -1);
-  g_free (timestamp);
-  g_free (rnd_str);
-
-  return nonce;
-}
-
-static gchar *
-get_videos_search_params (GVimeo *vimeo, const gchar *text, gint page) {
-  gchar *encoded_text = encode_uri (text);
-  gchar *timestamp = get_timestamp ();
-  gchar *nonce = get_nonce ();
-  gchar *params = g_strdup_printf (VIMEO_VIDEO_SEARCH,
-				   VIMEO_VIDEO_SEARCH_METHOD,
-				   vimeo->priv->api_key,
-				   nonce,
-				   timestamp,
-				   page,
-				   vimeo->priv->per_page,
-				   encoded_text);
-  g_free (timestamp);
-  g_free (nonce);
-  g_free (encoded_text);
-
-  return params;
-}
-
-static gchar *
-sign_string (gchar *message, gchar *key)
-{
-  gchar *signed_message = NULL;
-  gcry_md_hd_t digest_obj;
-  unsigned char *hmac_digest;
-  guint digest_len;
-
-  gcry_md_open(&digest_obj,
-	       GCRY_MD_SHA1,
-	       GCRY_MD_FLAG_SECURE | GCRY_MD_FLAG_HMAC);
-  gcry_md_setkey(digest_obj, key, strlen (key));
-  gcry_md_write (digest_obj, message, strlen (message));
-  gcry_md_final (digest_obj);
-  hmac_digest = gcry_md_read (digest_obj, 0);
-
-  digest_len = gcry_md_get_algo_dlen (GCRY_MD_SHA1);
-  signed_message = g_base64_encode (hmac_digest, digest_len);
-
-  gcry_md_close (digest_obj);
-
-  return signed_message;
-}
-
-static gboolean
-result_is_correct (xmlNodePtr node)
-{
-  gboolean correct = FALSE;
-  xmlChar *stat;
-
-  if (xmlStrcmp (node->name, (const xmlChar *) "rsp") == 0)
-  {
-    stat = xmlGetProp (node, (const xmlChar *) "stat");
-    if (stat && xmlStrcmp (stat, (const xmlChar *) "ok") == 0)
-    {
-      correct = TRUE;
-      xmlFree (stat);
-    }
-  }
-
-  return correct;
-}
-
-static void
-add_node (xmlNodePtr node, GHashTable *video)
-{
-  xmlAttrPtr attr;
-
-  for (attr = node->properties; attr != NULL; attr = attr->next)
-  {
-    g_hash_table_insert (video,
-                         g_strconcat ((const gchar *) node->name,
-                                      "_",
-                                      (const gchar *) attr->name,
-                                      NULL),
-                         (gchar *) xmlGetProp (node, attr->name));
-  }
-}
-
-static xmlNodePtr
-xpath_get_node (xmlXPathContextPtr context, gchar *xpath_expr)
-{
-  xmlNodePtr node = NULL;
-  xmlXPathObjectPtr xpath_obj;
-  xpath_obj = xmlXPathEvalExpression ((xmlChar *) xpath_expr, context);
-
-  if (xpath_obj && xpath_obj->nodesetval->nodeTab)
-  {
-    node = xpath_obj->nodesetval->nodeTab[0];
-  }
-  xmlXPathFreeObject (xpath_obj);
-
-  return node;
-}
-
-static gchar *
-get_node_text (xmlXPathContextPtr context, gchar *xpath_expr)
-{
-  xmlNodePtr node;
-  gchar *node_text = NULL;
-
-  node = xpath_get_node (context, xpath_expr);
-  if (node)
-  {
-    node_text = (gchar *) xmlNodeGetContent (node);
-  }
-
-  return node_text;
-}
-
-static GHashTable *
-get_video (xmlNodePtr node)
-{
-  gint i;
-  gint array_length;
-  xmlXPathContextPtr context;
-  gchar *video_id;
-  GHashTable *video = g_hash_table_new_full (g_str_hash,
-                                             g_str_equal,
-                                             g_free,
-                                             g_free);
-
-  /* Adds the video node's properties */
-  add_node (node, video);
-
-  context = xmlXPathNewContext (node->doc);
-  video_id = (gchar *) xmlGetProp (node, (xmlChar *) "id");
-
-  array_length = G_N_ELEMENTS (video_info);
-  for (i = 0; i < array_length; i++)
-  {
-    /* Look for the wanted information only under the current video */
-    gchar *xpath_name = g_strdup_printf ("//video[@id=%s]//%s",
-					 video_id,
-					 video_info[i].name);
-    xmlNodePtr info_node = xpath_get_node (context, xpath_name);
-    if (info_node)
-    {
-      if (video_info[i].type == EXTENDED) {
-	add_node (info_node, video);
-      }
-      else
-      {
-	g_hash_table_insert (video,
-			     g_strdup ((const gchar *) info_node->name),
-			     (gchar *) xmlNodeGetContent (info_node));
-      }
-    }
-    g_free (xpath_name);
-  }
-  g_free (video_id);
-
-  xmlXPathFreeContext (context);
-
-  return video;
-}
-
-
-static void
-process_video_search_result (const gchar *xml_result, gpointer user_data)
-{
-  xmlDocPtr doc;
-  xmlNodePtr node;
-  GList *video_list = NULL;
-  GVimeoVideoSearchData *data = (GVimeoVideoSearchData *) user_data;
-
-  doc = xmlReadMemory (xml_result,
-		       xmlStrlen ((xmlChar *) xml_result),
-		       NULL,
-		       NULL,
-		       XML_PARSE_RECOVER | XML_PARSE_NOBLANKS);
-  node = xmlDocGetRootElement (doc);
-
-  /* Check result is ok */
-  if (!node || !result_is_correct (node))
-  {
-    data->search_cb (data->vimeo, NULL, data->user_data);
-  }
-  else
-  {
-    node = node->xmlChildrenNode;
-
-    /* Now we're at "video pages" node */
-    node = node->xmlChildrenNode;
-    while (node)
-    {
-      video_list = g_list_prepend (video_list, get_video (node));
-      node = node->next;
-    }
-
-    data->search_cb (data->vimeo, g_list_reverse (video_list), data->user_data);
-    g_list_foreach (video_list, (GFunc) g_hash_table_unref, NULL);
-    g_list_free (video_list);
-  }
-  g_slice_free (GVimeoVideoSearchData, data);
-  xmlFreeDoc (doc);
-}
-
-static void
-search_videos_complete_cb (SoupSession *session,
-			   SoupMessage *message,
-			   gpointer *data)
-{
-  GVimeoVideoSearchCb *search_data = (GVimeoVideoSearchCb *) data;
-  process_video_search_result (message->response_body->data, search_data);
-}
-
-static gchar *
-get_play_url_from_vimeo_xml (const gchar *xml, gint video_id)
-{
-  xmlDocPtr doc = xmlRecoverDoc ((xmlChar *) xml);
-  xmlXPathContextPtr context = xmlXPathNewContext (doc);
-  gchar *request_signature = get_node_text (context,
-					    "/xml/request_signature[1]");
-  gchar *signature_expires = get_node_text (context,
-					    "/xml/request_signature_expires[1]");
-
-  gchar *url = g_strdup_printf ("%s%d/%s/%s/?q=sd",
-				VIMEO_VIDEO_PLAY_URL,
-			        video_id,
-				request_signature,
-				signature_expires);
-
-  g_free (request_signature);
-  g_free (signature_expires);
-  xmlXPathFreeContext (context);
-  xmlFreeDoc (doc);
-
-  return url;
-}
-
-static void
-get_video_play_url_complete_cb (SoupSession *session,
-				SoupMessage *message,
-				gpointer *data)
-{
-  GVimeoVideoURLData *url_data;
-  gchar *url;
-
-  if (message->response_body == NULL)
-  {
-    return;
-  }
-
-  url_data = (GVimeoVideoURLData *) data;
-  url =  get_play_url_from_vimeo_xml (message->response_body->data,
-				      url_data->video_id);
-  url_data->callback (url, url_data->user_data);
-  g_slice_free (GVimeoVideoURLData, url_data);
-}
-
-static gchar *
-encode_uri (const gchar *uri)
-{
-  return soup_uri_encode (uri, "%!*'();:@&=+$,/?#[] ");
-}
-
-static gchar *
-build_request (GVimeo *vimeo, const gchar *query, gint page)
-{
-  gchar *params;
-  gchar *endpoint_encoded;
-  gchar *key;
-  gchar *escaped_str;
-  gchar *tmp_str;
-  gchar *signature;
-
-  g_return_val_if_fail (G_IS_VIMEO (vimeo), NULL);
-
-  params = get_videos_search_params (vimeo, query, page);
-  endpoint_encoded = encode_uri (VIMEO_ENDPOINT);
-  key = g_strdup_printf ("%s&", vimeo->priv->auth_secret);
-  escaped_str = encode_uri (params);
-  tmp_str = g_strdup_printf ("GET&%s&%s", endpoint_encoded, escaped_str);
-  signature = sign_string (tmp_str, key);
-  g_free (escaped_str);
-  g_free (tmp_str);
-  escaped_str = encode_uri (signature);
-  tmp_str = g_strdup_printf ("%s?%s" VIMEO_API_OAUTH_SIGNATURE_PARAM,
-			     VIMEO_ENDPOINT,
-			     params,
-			     escaped_str);
-
-  g_free (endpoint_encoded);
-  g_free (params);
-  g_free (key);
-  g_free (escaped_str);
-  g_free (signature);
-
-  return tmp_str;
-}
-
-/* -------------------- PUBLIC API -------------------- */
-
-void
-g_vimeo_set_per_page (GVimeo *vimeo, gint per_page)
-{
-  g_return_if_fail (G_IS_VIMEO (vimeo));
-  vimeo->priv->per_page = per_page;
-}
-
-void
-g_vimeo_videos_search (GVimeo *vimeo,
-		       const gchar *text,
-		       gint page,
-		       GVimeoVideoSearchCb callback,
-		       gpointer user_data)
-{
-  SoupMessage *message;
-  GVimeoVideoSearchData *search_data;
-  gchar *request;
-
-  g_return_if_fail (G_IS_VIMEO (vimeo));
-
-  request = build_request (vimeo, text, page);
-  search_data = g_slice_new (GVimeoVideoSearchData);
-  search_data->vimeo = vimeo;
-  search_data->search_cb = callback;
-  search_data->user_data = user_data;
-
-  message = soup_message_new ("GET", request);
-  soup_session_queue_message (vimeo->priv->async_session,
-			      message,
-			      (SoupSessionCallback) search_videos_complete_cb,
-			      search_data);
-  g_free (request);
-}
-
-void
-g_vimeo_video_get_play_url (GVimeo *vimeo,
-			    gint id,
-			    GVimeoURLCb callback,
-			    gpointer user_data)
-{
-  GVimeoVideoURLData *data;
-  gchar *url = g_strdup_printf ("%s%d",
-				VIMEO_VIDEO_LOAD_URL,
-				id);
-  SoupMessage *message = soup_message_new ("GET", url);
-  SoupMessageHeaders *headers = message->request_headers;
-  soup_message_headers_append (headers, "User-Agent", PLUGIN_USER_AGENT);
-
-  data = g_slice_new (GVimeoVideoURLData);
-  data->video_id = id;
-  data->vimeo = vimeo;
-  data->callback = callback;
-  data->user_data = user_data;
-
-  soup_session_queue_message (vimeo->priv->async_session,
-			      message,
-			      (SoupSessionCallback) get_video_play_url_complete_cb,
-			      data);
-  g_free (url);
-}
diff --git a/src/vimeo/gvimeo.h b/src/vimeo/gvimeo.h
deleted file mode 100644
index 34e17a3..0000000
--- a/src/vimeo/gvimeo.h
+++ /dev/null
@@ -1,112 +0,0 @@
-/*
- * Copyright (C) 2010 Igalia S.L.
- *
- * Contact: Iago Toral Quiroga <itoral igalia com>
- *
- * Authors: Joaquim Rocha <jrocha igalia 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 _G_VIMEO_H_
-#define _G_VIMEO_H_
-
-#include <glib-object.h>
-
-#define G_VIMEO_TYPE                           \
-  (g_vimeo_get_type ())
-
-#define G_VIMEO(obj)                                   \
-  (G_TYPE_CHECK_INSTANCE_CAST ((obj),                   \
-                               G_VIMEO_TYPE,           \
-                               GVimeo))
-
-#define G_IS_VIMEO(obj)                                \
-  (G_TYPE_CHECK_INSTANCE_TYPE ((obj),                   \
-                               G_VIMEO_TYPE))
-
-#define G_VIMEO_CLASS(klass)                           \
-  (G_TYPE_CHECK_CLASS_CAST((klass),                     \
-                           G_VIMEO_TYPE,               \
-                           GVimeoClass))
-
-#define G_IS_VIMEO_CLASS(klass)                        \
-  (G_TYPE_CHECK_CLASS_TYPE((klass),                     \
-                           G_VIMEO_TYPE))
-
-#define G_VIMEO_GET_CLASS(obj)                         \
-  (G_TYPE_INSTANCE_GET_CLASS ((obj),                    \
-                              G_VIMEO_TYPE,            \
-                              GVimeoClass))
-
-#define VIMEO_VIDEO "video"
-#define VIMEO_VIDEO_ID VIMEO_VIDEO "_id"
-#define VIMEO_VIDEO_TITLE "title"
-#define VIMEO_VIDEO_DESCRIPTION "description"
-#define VIMEO_VIDEO_URL "url"
-#define VIMEO_VIDEO_UPLOAD_DATE "upload_date"
-#define VIMEO_VIDEO_WIDTH "width"
-#define VIMEO_VIDEO_HEIGHT "height"
-#define VIMEO_VIDEO_DURATION "duration"
-#define VIMEO_VIDEO_OWNER "owner"
-#define VIMEO_VIDEO_THUMBNAIL "thumbnail"
-
-#define VIMEO_VIDEO_OWNER_NAME VIMEO_VIDEO_OWNER "_realname"
-
-typedef struct _GVimeo        GVimeo;
-typedef struct _GVimeoPrivate GVimeoPrivate;
-
-struct _GVimeo {
-
-  GObject parent;
-
-  /*< private >*/
-  GVimeoPrivate *priv;
-};
-
-typedef struct _GVimeoClass GVimeoClass;
-
-struct _GVimeoClass {
-
-  GObjectClass parent_class;
-
-};
-
-typedef void (*GVimeoVideoSearchCb) (GVimeo *vimeo,
-				     GList *videolist,
-				     gpointer user_data);
-
-typedef void (*GVimeoURLCb) (gchar *url, gpointer user_data);
-
-GType g_vimeo_get_type (void);
-
-GVimeo *g_vimeo_new (const gchar *api_key, const gchar *auth_secret);
-
-void g_vimeo_video_get_play_url (GVimeo *vimeo,
-				 gint id,
-				 GVimeoURLCb callback,
-				 gpointer user_data);
-
-void g_vimeo_set_per_page (GVimeo *vimeo, gint per_page);
-
-void g_vimeo_videos_search (GVimeo *vimeo,
-			    const gchar *text,
-			    gint page,
-			    GVimeoVideoSearchCb callback,
-			    gpointer user_data);
-
-#endif /* _G_VIMEO_H_ */
diff --git a/src/youtube/Makefile.am b/src/youtube/Makefile.am
deleted file mode 100644
index 2384bb8..0000000
--- a/src/youtube/Makefile.am
+++ /dev/null
@@ -1,40 +0,0 @@
-#
-# Makefile.am
-#
-# Author: Iago Toral Quiroga <itoral igalia com>
-#
-# Copyright (C) 2010, 2011 Igalia S.L. All rights reserved.
-
-lib_LTLIBRARIES = libgrlyoutube.la
-
-libgrlyoutube_la_CFLAGS =	\
-	$(DEPS_CFLAGS)		\
-	$(GRLNET_CFLAGS)	\
-	$(XML_CFLAGS) 		\
-	$(GTHREAD_CFLAGS)	\
-	$(GDATA_CFLAGS)
-
-libgrlyoutube_la_LIBADD =	\
-	$(DEPS_LIBS)		\
-	$(GRLNET_LIBS)		\
-	$(XML_LIBS) 		\
-	$(GTHREAD_LIBS)		\
-	$(GDATA_LIBS)
-
-libgrlyoutube_la_LDFLAGS = \
-	-module		   \
-	-avoid-version
-
-libgrlyoutube_la_SOURCES = grl-youtube.c grl-youtube.h
-
-libdir = $(GRL_PLUGINS_DIR)
-youtubexmldir	= $(GRL_PLUGINS_CONF_DIR)
-youtubexml_DATA	= $(YOUTUBE_PLUGIN_ID).xml
-
-EXTRA_DIST = $(youtubexml_DATA)
-
-MAINTAINERCLEANFILES	 = \
-	*.in \
-	*~
-
-DISTCLEANFILES = $(MAINTAINERCLEANFILES)
diff --git a/src/youtube/TODO b/src/youtube/TODO
deleted file mode 100644
index 409835c..0000000
--- a/src/youtube/TODO
+++ /dev/null
@@ -1,6 +0,0 @@
-- Consider using libgdata.
-- Fix get_metadata() for standard feeds.
-- Consider implementing store() -> video upload.
-- Consider implementing configuration (personal account)
-  -> Could add new category for browsing (my videos)
-  -> would enable store() and remove() operations.
diff --git a/src/youtube/grl-youtube.c b/src/youtube/grl-youtube.c
deleted file mode 100644
index c6dfd2e..0000000
--- a/src/youtube/grl-youtube.c
+++ /dev/null
@@ -1,1630 +0,0 @@
-/*
- * Copyright (C) 2010 Igalia S.L.
- * Copyright (C) 2011 Intel Corporation.
- *
- * Contact: Iago Toral Quiroga <itoral igalia 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 <grilo.h>
-#include <net/grl-net.h>
-#include <gdata/gdata.h>
-#include <string.h>
-
-#include "grl-youtube.h"
-
-enum {
-  PROP_0,
-  PROP_SERVICE,
-};
-
-#define GRL_YOUTUBE_SOURCE_GET_PRIVATE(object)            \
-  (G_TYPE_INSTANCE_GET_PRIVATE((object),                  \
-                               GRL_YOUTUBE_SOURCE_TYPE,   \
-                               GrlYoutubeSourcePriv))
-
-/* --------- Logging  -------- */
-
-#define GRL_LOG_DOMAIN_DEFAULT youtube_log_domain
-GRL_LOG_DOMAIN_STATIC(youtube_log_domain);
-
-/* ----- Root categories ---- */
-
-#define YOUTUBE_ROOT_NAME       "Youtube"
-
-#define ROOT_DIR_FEEDS_INDEX      0
-#define ROOT_DIR_CATEGORIES_INDEX 1
-
-#define YOUTUBE_FEEDS_ID        "standard-feeds"
-#define YOUTUBE_FEEDS_NAME      "Standard feeds"
-
-#define YOUTUBE_CATEGORIES_ID   "categories"
-#define YOUTUBE_CATEGORIES_NAME "Categories"
-#define YOUTUBE_CATEGORIES_URL  "http://gdata.youtube.com/schemas/2007/categories.cat";
-
-/* ----- Feeds categories ---- */
-
-#define YOUTUBE_TOP_RATED_ID         (YOUTUBE_FEEDS_ID "/0")
-#define YOUTUBE_TOP_RATED_NAME       "Top Rated"
-
-#define YOUTUBE_TOP_FAVS_ID          (YOUTUBE_FEEDS_ID "/1")
-#define YOUTUBE_TOP_FAVS_NAME        "Top Favorites"
-
-#define YOUTUBE_MOST_VIEWED_ID       (YOUTUBE_FEEDS_ID "/2")
-#define YOUTUBE_MOST_VIEWED_NAME     "Most Viewed"
-
-#define YOUTUBE_MOST_POPULAR_ID      (YOUTUBE_FEEDS_ID "/3")
-#define YOUTUBE_MOST_POPULAR_NAME    "Most Popular"
-
-#define YOUTUBE_MOST_RECENT_ID       (YOUTUBE_FEEDS_ID "/4")
-#define YOUTUBE_MOST_RECENT_NAME     "Most Recent"
-
-#define YOUTUBE_MOST_DISCUSSED_ID    (YOUTUBE_FEEDS_ID "/5")
-#define YOUTUBE_MOST_DISCUSSED_NAME  "Most Discussed"
-
-#define YOUTUBE_MOST_LINKED_ID       (YOUTUBE_FEEDS_ID "/6")
-#define YOUTUBE_MOST_LINKED_NAME     "Most Linked"
-
-#define YOUTUBE_MOST_RESPONDED_ID    (YOUTUBE_FEEDS_ID "/7")
-#define YOUTUBE_MOST_RESPONDED_NAME  "Most Responded"
-
-#define YOUTUBE_FEATURED_ID          (YOUTUBE_FEEDS_ID "/8")
-#define YOUTUBE_FEATURED_NAME        "Recently Featured"
-
-#define YOUTUBE_MOBILE_ID            (YOUTUBE_FEEDS_ID "/9")
-#define YOUTUBE_MOBILE_NAME          "Watch On Mobile"
-
-/* --- Other --- */
-
-#define YOUTUBE_MAX_CHUNK       50
-
-#define YOUTUBE_VIDEO_INFO_URL  "http://www.youtube.com/get_video_info?video_id=%s";
-#define YOUTUBE_VIDEO_URL       "http://www.youtube.com/get_video?video_id=%s&t=%s&asv=";
-#define YOUTUBE_CATEGORY_URL    "http://gdata.youtube.com/feeds/api/videos/-/%s?&start-index=%s&max-results=%s";
-#define YOUTUBE_WATCH_URL       "http://www.youtube.com/watch?v=";
-
-#define YOUTUBE_VIDEO_MIME      "application/x-shockwave-flash"
-#define YOUTUBE_SITE_URL        "www.youtube.com"
-
-
-/* --- Plugin information --- */
-
-#define PLUGIN_ID   YOUTUBE_PLUGIN_ID
-
-#define SOURCE_ID   "grl-youtube"
-#define SOURCE_NAME "Youtube"
-#define SOURCE_DESC "A source for browsing and searching Youtube videos"
-
-#define AUTHOR      "Igalia S.L."
-#define LICENSE     "LGPL"
-#define SITE        "http://www.igalia.com";
-
-/* --- Data types --- */
-
-typedef void (*AsyncReadCbFunc) (gchar *data, gpointer user_data);
-
-typedef void (*BuildMediaFromEntryCbFunc) (GrlMedia *media, gpointer user_data);
-
-typedef struct {
-  gchar *id;
-  gchar *name;
-  guint count;
-} CategoryInfo;
-
-typedef struct {
-  GrlMediaSource *source;
-  guint operation_id;
-  const gchar *container_id;
-  GList *keys;
-  GrlMetadataResolutionFlags flags;
-  guint skip;
-  guint count;
-  GrlMediaSourceResultCb callback;
-  gpointer user_data;
-  guint error_code;
-  CategoryInfo *category_info;
-  guint emitted;
-  guint matches;
-  guint ref_count;
-} OperationSpec;
-
-typedef struct {
-  GDataService *service;
-  CategoryInfo *category_info;
-} CategoryCountCb;
-
-typedef struct {
-  AsyncReadCbFunc callback;
-  gchar *url;
-  gpointer user_data;
-} AsyncReadCb;
-
-typedef struct {
-  GrlMedia *media;
-  BuildMediaFromEntryCbFunc callback;
-  gpointer user_data;
-} SetMediaUrlAsyncReadCb;
-
-typedef enum {
-  YOUTUBE_MEDIA_TYPE_ROOT,
-  YOUTUBE_MEDIA_TYPE_FEEDS,
-  YOUTUBE_MEDIA_TYPE_CATEGORIES,
-  YOUTUBE_MEDIA_TYPE_FEED,
-  YOUTUBE_MEDIA_TYPE_CATEGORY,
-  YOUTUBE_MEDIA_TYPE_VIDEO,
-} YoutubeMediaType;
-
-struct _GrlYoutubeSourcePriv {
-  GDataService *service;
-
-  GrlNetWc *wc;
-};
-
-#define YOUTUBE_CLIENT_ID "grilo"
-
-static GrlYoutubeSource *grl_youtube_source_new (const gchar *api_key,
-						 const gchar *client_id);
-
-static void grl_youtube_source_set_property (GObject *object,
-                                             guint propid,
-                                             const GValue *value,
-                                             GParamSpec *pspec);
-static void grl_youtube_source_finalize (GObject *object);
-
-gboolean grl_youtube_plugin_init (GrlPluginRegistry *registry,
-                                  const GrlPluginInfo *plugin,
-                                  GList *configs);
-
-static const GList *grl_youtube_source_supported_keys (GrlMetadataSource *source);
-
-static const GList *grl_youtube_source_slow_keys (GrlMetadataSource *source);
-
-static void grl_youtube_source_search (GrlMediaSource *source,
-                                       GrlMediaSourceSearchSpec *ss);
-
-static void grl_youtube_source_browse (GrlMediaSource *source,
-                                       GrlMediaSourceBrowseSpec *bs);
-
-static void grl_youtube_source_metadata (GrlMediaSource *source,
-                                         GrlMediaSourceMetadataSpec *ms);
-
-static gboolean grl_youtube_test_media_from_uri (GrlMediaSource *source,
-						 const gchar *uri);
-
-static void grl_youtube_get_media_from_uri (GrlMediaSource *source,
-					    GrlMediaSourceMediaFromUriSpec *mfus);
-
-static void build_directories (GDataService *service);
-static void compute_feed_counts (GDataService *service);
-static void compute_category_counts (GDataService *service);
-
-/* ==================== Global Data  ================= */
-
-guint root_dir_size = 2;
-CategoryInfo root_dir[] = {
-  {YOUTUBE_FEEDS_ID,      YOUTUBE_FEEDS_NAME,      10},
-  {YOUTUBE_CATEGORIES_ID, YOUTUBE_CATEGORIES_NAME,  0},
-  {NULL, NULL, 0}
-};
-
-CategoryInfo feeds_dir[] = {
-  {YOUTUBE_TOP_RATED_ID,      YOUTUBE_TOP_RATED_NAME,       0},
-  {YOUTUBE_TOP_FAVS_ID,       YOUTUBE_TOP_FAVS_NAME,        0},
-  {YOUTUBE_MOST_VIEWED_ID,    YOUTUBE_MOST_VIEWED_NAME,     0},
-  {YOUTUBE_MOST_POPULAR_ID,   YOUTUBE_MOST_POPULAR_NAME,    0},
-  {YOUTUBE_MOST_RECENT_ID,    YOUTUBE_MOST_RECENT_NAME,     0},
-  {YOUTUBE_MOST_DISCUSSED_ID, YOUTUBE_MOST_DISCUSSED_NAME,  0},
-  {YOUTUBE_MOST_LINKED_ID,    YOUTUBE_MOST_LINKED_NAME,     0},
-  {YOUTUBE_MOST_RESPONDED_ID, YOUTUBE_MOST_RESPONDED_NAME,  0},
-  {YOUTUBE_FEATURED_ID,       YOUTUBE_FEATURED_NAME,        0},
-  {YOUTUBE_MOBILE_ID,         YOUTUBE_MOBILE_NAME,          0},
-  {NULL, NULL, 0}
-};
-
-CategoryInfo *categories_dir = NULL;
-
-static GrlYoutubeSource *ytsrc = NULL;
-
-/* =================== Youtube Plugin  =============== */
-
-gboolean
-grl_youtube_plugin_init (GrlPluginRegistry *registry,
-                         const GrlPluginInfo *plugin,
-                         GList *configs)
-{
-  gchar *api_key;
-  GrlConfig *config;
-  gint config_count;
-
-  GRL_LOG_DOMAIN_INIT (youtube_log_domain, "youtube");
-
-  GRL_DEBUG ("youtube_plugin_init");
-
-  if (!configs) {
-    GRL_WARNING ("Configuration not provided! Cannot configure plugin.");
-    return FALSE;
-  }
-
-  config_count = g_list_length (configs);
-  if (config_count > 1) {
-    GRL_WARNING ("Provided %d configs, but will only use one", config_count);
-  }
-
-  config = GRL_CONFIG (configs->data);
-  api_key = grl_config_get_api_key (config);
-  if (!api_key) {
-    GRL_WARNING ("Missing API Key, cannot configure Youtube plugin");
-    return FALSE;
-  }
-
-  /* libgdata needs this */
-  if (!g_thread_supported()) {
-    g_thread_init (NULL);
-  }
-
-  GrlYoutubeSource *source =
-    grl_youtube_source_new (api_key, YOUTUBE_CLIENT_ID);
-
-  grl_plugin_registry_register_source (registry,
-                                       plugin,
-                                       GRL_MEDIA_PLUGIN (source),
-                                       NULL);
-
-  g_free (api_key);
-
-  return TRUE;
-}
-
-GRL_PLUGIN_REGISTER (grl_youtube_plugin_init,
-                     NULL,
-                     PLUGIN_ID);
-
-/* ================== Youtube GObject ================ */
-
-G_DEFINE_TYPE (GrlYoutubeSource, grl_youtube_source, GRL_TYPE_MEDIA_SOURCE);
-
-static GrlYoutubeSource *
-grl_youtube_source_new (const gchar *api_key, const gchar *client_id)
-{
-  GRL_DEBUG ("grl_youtube_source_new");
-
-  GrlYoutubeSource *source;
-  GDataYouTubeService *service;
-
-  service = gdata_youtube_service_new (api_key, client_id);
-  if (!service) {
-    GRL_WARNING ("Failed to initialize gdata service");
-    return NULL;
-  }
-
-  /* Use auto-split mode because Youtube fails for queries
-     that request more than YOUTUBE_MAX_CHUNK results */
-  source = GRL_YOUTUBE_SOURCE (g_object_new (GRL_YOUTUBE_SOURCE_TYPE,
-					     "source-id", SOURCE_ID,
-					     "source-name", SOURCE_NAME,
-					     "source-desc", SOURCE_DESC,
-					     "auto-split-threshold",
-					     YOUTUBE_MAX_CHUNK,
-                                             "yt-service", service,
-					     NULL));
-
-  ytsrc = source;
-
-  /* Build browse content hierarchy:
-      - Query Youtube for available categories
-      - Compute category childcounts
-      We only need to do this once */
-  if (!categories_dir) {
-    build_directories (GDATA_SERVICE (service));
-  }
-
-  return source;
-}
-
-static void
-grl_youtube_source_class_init (GrlYoutubeSourceClass * klass)
-{
-  GrlMediaSourceClass *source_class = GRL_MEDIA_SOURCE_CLASS (klass);
-  GrlMetadataSourceClass *metadata_class = GRL_METADATA_SOURCE_CLASS (klass);
-  GObjectClass *gobject_class = G_OBJECT_CLASS (klass);
-  source_class->search = grl_youtube_source_search;
-  source_class->browse = grl_youtube_source_browse;
-  source_class->metadata = grl_youtube_source_metadata;
-  source_class->test_media_from_uri = grl_youtube_test_media_from_uri;
-  source_class->media_from_uri = grl_youtube_get_media_from_uri;
-  metadata_class->supported_keys = grl_youtube_source_supported_keys;
-  metadata_class->slow_keys = grl_youtube_source_slow_keys;
-  gobject_class->set_property = grl_youtube_source_set_property;
-  gobject_class->finalize = grl_youtube_source_finalize;
-
-  g_object_class_install_property (gobject_class,
-                                   PROP_SERVICE,
-                                   g_param_spec_object ("yt-service",
-                                                        "youtube-service",
-                                                        "gdata youtube service object",
-                                                        GDATA_TYPE_YOUTUBE_SERVICE,
-                                                        G_PARAM_WRITABLE
-                                                        | G_PARAM_CONSTRUCT_ONLY
-                                                        | G_PARAM_STATIC_NAME));
-
-  g_type_class_add_private (klass, sizeof (GrlYoutubeSourcePriv));
-}
-
-static void
-grl_youtube_source_init (GrlYoutubeSource *source)
-{
-  source->priv = GRL_YOUTUBE_SOURCE_GET_PRIVATE (source);
-}
-
-static void
-grl_youtube_source_set_property (GObject *object,
-                                 guint propid,
-                                 const GValue *value,
-                                 GParamSpec *pspec)
-
-{
-  switch (propid) {
-  case PROP_SERVICE: {
-    GrlYoutubeSource *self;
-    self = GRL_YOUTUBE_SOURCE (object);
-    self->priv->service = g_value_get_object (value);
-    break;
-  }
-  default:
-    G_OBJECT_WARN_INVALID_PROPERTY_ID (object, propid, pspec);
-  }
-}
-
-static void
-grl_youtube_source_finalize (GObject *object)
-{
-  GrlYoutubeSource *self;
-
-  self = GRL_YOUTUBE_SOURCE (object);
-
-  if (self->priv->wc)
-    g_object_unref (self->priv->wc);
-
-  if (self->priv->service)
-    g_object_unref (self->priv->service);
-
-  G_OBJECT_CLASS (grl_youtube_source_parent_class)->finalize (object);
-}
-
-/* ======================= Utilities ==================== */
-
-static OperationSpec *
-operation_spec_new ()
-{
-  GRL_DEBUG ("Allocating new spec");
-  OperationSpec *os =  g_slice_new0 (OperationSpec);
-  os->ref_count = 1;
-  return os;
-}
-
-static void
-operation_spec_unref (OperationSpec *os)
-{
-  os->ref_count--;
-  if (os->ref_count == 0) {
-    g_slice_free (OperationSpec, os);
-    GRL_DEBUG ("freeing spec");
-  }
-}
-
-static void
-operation_spec_ref (OperationSpec *os)
-{
-  GRL_DEBUG ("Reffing spec");
-  os->ref_count++;
-}
-
-inline static GrlNetWc *
-get_wc ()
-{
-  if (ytsrc && !ytsrc->priv->wc)
-    ytsrc->priv->wc = grl_net_wc_new ();
-
-  return ytsrc->priv->wc;
-}
-
-static void
-read_done_cb (GObject *source_object,
-              GAsyncResult *res,
-              gpointer user_data)
-{
-  AsyncReadCb *arc = (AsyncReadCb *) user_data;
-  GError *wc_error = NULL;
-  gchar *content = NULL;
-
-  grl_net_wc_request_finish (GRL_NET_WC (source_object),
-                         res,
-                         &content,
-                         NULL,
-                         &wc_error);
-  if (wc_error) {
-    GRL_WARNING ("Failed to open '%s': %s", arc->url, wc_error->message);
-    arc->callback (NULL, arc->user_data);
-    g_error_free (wc_error);
-  } else {
-    arc->callback (content, arc->user_data);
-  }
-  g_free (arc->url);
-  g_slice_free (AsyncReadCb, arc);
-}
-
-static void
-read_url_async (const gchar *url,
-                AsyncReadCbFunc callback,
-                gpointer user_data)
-{
-  AsyncReadCb *arc;
-
-  arc = g_slice_new0 (AsyncReadCb);
-  arc->url = g_strdup (url);
-  arc->callback = callback;
-  arc->user_data = user_data;
-
-  GRL_DEBUG ("Opening async '%s'", url);
-  grl_net_wc_request_async (get_wc (),
-                        url,
-                        NULL,
-                        read_done_cb,
-                        arc);
-}
-
-static void
-set_media_url_async_read_cb (gchar *data, gpointer user_data)
-{
-  SetMediaUrlAsyncReadCb *cb_data = (SetMediaUrlAsyncReadCb *) user_data;
-  gchar *url = NULL;
-  GMatchInfo *match_info = NULL;
-  static GRegex *regex = NULL;
-
-  if (!data) {
-    goto done;
-  }
-
-  if (regex == NULL) {
-    regex = g_regex_new (".*&fmt_url_map=([^&]+)&", G_REGEX_OPTIMIZE, 0, NULL);
-  }
-
-  /* Check if we find the url mapping */
-  g_regex_match (regex, data, 0, &match_info);
-  if (g_match_info_matches (match_info) == TRUE) {
-    gchar *url_map_escaped, *url_map;
-    gchar **mappings;
-
-    url_map_escaped = g_match_info_fetch (match_info, 1);
-    url_map = g_uri_unescape_string (url_map_escaped, NULL);
-    g_free (url_map_escaped);
-
-    mappings = g_strsplit (url_map, ",", 0);
-    g_free (url_map);
-
-    if (mappings != NULL) {
-      /* TODO: We get the URL from the first format available.
-       * We should provide the list of available urls or let the user
-       * configure preferred formats
-       */
-      gchar **mapping = g_strsplit (mappings[0], "|", 2);
-      url = g_strdup (mapping[1]);
-      g_strfreev (mapping);
-    }
-  } else {
-    GRL_DEBUG ("Format array not found, using token workaround");
-    gchar *token_start;
-    gchar *token_end;
-    gchar *token;
-    const gchar *video_id;
-
-    token_start = g_strrstr (data, "&token=");
-    if (!token_start) {
-      goto done;
-    }
-    token_start += 7;
-    token_end = strstr (token_start, "&");
-    token = g_strndup (token_start, token_end - token_start);
-
-    video_id = grl_media_get_id (cb_data->media);
-    url = g_strdup_printf (YOUTUBE_VIDEO_URL, video_id, token);
-    g_free (token);
-  }
-
- done:
-  if (url) {
-    grl_media_set_url (cb_data->media, url);
-    g_free (url);
-  }
-
-  cb_data->callback (cb_data->media, cb_data->user_data);
-
-  g_free (cb_data);
-}
-
-static void
-set_media_url (GrlMedia *media,
-	       BuildMediaFromEntryCbFunc callback,
-	       gpointer user_data)
-{
-  const gchar *video_id;
-  gchar *video_info_url;
-  SetMediaUrlAsyncReadCb *set_media_url_async_read_data;
-
-  /* The procedure to get the video url is:
-   * 1) Read the video info URL using the video id (async operation)
-   * 2) In the video info page, there should be an array of supported formats
-   *    and their corresponding URLs, right now we just use the first one we get.
-   *    (see set_media_url_async_read_cb).
-   *    TODO: we should be able to provide various urls or at least
-   *          select preferred formats via configuration
-   *    TODO: we should set mime-type accordingly to the format selected
-   * 3) As a workaround in case no format array is found we get the video token
-   *    and figure out the url of the video using the video id and the token.
-   */
-
-  video_id = grl_media_get_id (media);
-  video_info_url = g_strdup_printf (YOUTUBE_VIDEO_INFO_URL, video_id);
-
-  set_media_url_async_read_data = g_new0 (SetMediaUrlAsyncReadCb, 1);
-  set_media_url_async_read_data->media = media;
-  set_media_url_async_read_data->callback = callback;
-  set_media_url_async_read_data->user_data = user_data;
-
-  read_url_async (video_info_url,
-		  set_media_url_async_read_cb,
-		  set_media_url_async_read_data);
-
-  g_free (video_info_url);
-}
-
-static void
-build_media_from_entry (GrlMedia *content,
-			GDataEntry *entry,
-			const GList *keys,
-			BuildMediaFromEntryCbFunc callback,
-			gpointer user_data)
-{
-  GDataYouTubeVideo *video;
-  GrlMedia *media;
-  GList *iter;
-  gboolean need_url = FALSE;
-
-  if (!content) {
-    media = grl_media_video_new ();
-  } else {
-    media = content;
-  }
-
-  video = GDATA_YOUTUBE_VIDEO (entry);
-
-  /* Make sure we set the media id in any case */
-  if (!grl_media_get_id (media)) {
-    grl_media_set_id (media, gdata_youtube_video_get_video_id (video));
-  }
-
-  iter = (GList *) keys;
-  while (iter) {
-    if (iter->data == GRL_METADATA_KEY_TITLE) {
-      grl_media_set_title (media, gdata_entry_get_title (entry));
-    } else if (iter->data == GRL_METADATA_KEY_DESCRIPTION) {
-      grl_media_set_description (media,
-				 gdata_youtube_video_get_description (video));
-    } else if (iter->data == GRL_METADATA_KEY_THUMBNAIL) {
-      GList *thumb_list;
-      thumb_list = gdata_youtube_video_get_thumbnails (video);
-      if (thumb_list) {
-        GDataMediaThumbnail *thumbnail;
-        thumbnail = GDATA_MEDIA_THUMBNAIL (thumb_list->data);
-        grl_media_set_thumbnail (media,
-                                 gdata_media_thumbnail_get_uri (thumbnail));
-      }
-    } else if (iter->data == GRL_METADATA_KEY_DATE) {
-      GTimeVal date;
-      gchar *date_str;
-#ifdef GDATA_API_SUBJECT_TO_CHANGE
-      gint64 published = gdata_entry_get_published (entry);
-      date.tv_sec = (glong) published;
-#else
-      gdata_entry_get_published (entry, &date);
-#endif
-      if (date.tv_sec != 0 || date.tv_usec != 0) {
-        date_str = g_time_val_to_iso8601 (&date);
-        grl_media_set_date (media, date_str);
-        g_free (date_str);
-      }
-    } else if (iter->data == GRL_METADATA_KEY_DURATION) {
-      grl_media_set_duration (media, gdata_youtube_video_get_duration (video));
-    } else if (iter->data == GRL_METADATA_KEY_MIME) {
-      grl_media_set_mime (media, YOUTUBE_VIDEO_MIME);
-    } else if (iter->data == GRL_METADATA_KEY_SITE) {
-      grl_media_set_site (media, gdata_youtube_video_get_player_uri (video));
-    } else if (iter->data == GRL_METADATA_KEY_EXTERNAL_URL) {
-      grl_media_set_external_url (media,
-				  gdata_youtube_video_get_player_uri (video));
-    } else if (iter->data == GRL_METADATA_KEY_RATING) {
-      gdouble average;
-      gdata_youtube_video_get_rating (video, NULL, NULL, NULL, &average);
-      grl_media_set_rating (media, average, 5.00);
-    } else if (iter->data == GRL_METADATA_KEY_URL) {
-      /* This needs another query and will be resolved asynchronously p Q*/
-      need_url = TRUE;
-    } else if (iter->data == GRL_METADATA_KEY_EXTERNAL_PLAYER) {
-      GDataYouTubeContent *youtube_content;
-      youtube_content =
-	gdata_youtube_video_look_up_content (video,
-					     "application/x-shockwave-flash");
-      if (youtube_content != NULL) {
-	GDataMediaContent *content = GDATA_MEDIA_CONTENT (youtube_content);
-	grl_media_set_external_player (media,
-				       gdata_media_content_get_uri (content));
-      }
-    }
-    iter = g_list_next (iter);
-  }
-
-  if (need_url) {
-    /* URL resolution is async */
-    set_media_url (media, callback, user_data);
-  } else {
-    callback (media, user_data);
-  }
-}
-
-static void
-parse_categories (xmlDocPtr doc, xmlNodePtr node, GDataService *service)
-{
-  GRL_DEBUG ("parse_categories");
-
-  guint total = 0;
-  GList *all = NULL, *iter;
-  CategoryInfo *cat_info;
-  gchar *id;
-  guint index = 0;
-
-  while (node) {
-    cat_info = g_slice_new (CategoryInfo);
-    id = (gchar *) xmlGetProp (node, (xmlChar *) "term");
-    cat_info->id = g_strconcat (YOUTUBE_CATEGORIES_ID, "/", id, NULL);
-    cat_info->name = (gchar *) xmlGetProp (node, (xmlChar *) "label");
-    all = g_list_prepend (all, cat_info);
-    g_free (id);
-    node = node->next;
-    total++;
-    GRL_DEBUG ("  Found category: '%d - %s'", index++, cat_info->name);
-  }
-
-  if (all) {
-    root_dir[ROOT_DIR_CATEGORIES_INDEX].count = total;
-    categories_dir = g_new0 (CategoryInfo, total + 1);
-    iter = all;
-    do {
-      cat_info = (CategoryInfo *) iter->data;
-      categories_dir[total - 1].id = cat_info->id ;
-      categories_dir[total - 1].name = cat_info->name;
-      categories_dir[total - 1].count = 0;
-      total--;
-      g_slice_free (CategoryInfo, cat_info);
-      iter = g_list_next (iter);
-    } while (iter);
-    g_list_free (all);
-
-    compute_category_counts (service);
-  }
-}
-
-static void
-build_categories_directory_read_cb (gchar *xmldata, gpointer user_data)
-{
-  xmlDocPtr doc;
-  xmlNodePtr node;
-
-  if (!xmldata) {
-    g_critical ("Failed to build category directory (1)");
-    return;
-  }
-
-  doc = xmlReadMemory (xmldata, strlen (xmldata), NULL, NULL,
-                       XML_PARSE_RECOVER | XML_PARSE_NOBLANKS);
-  if (!doc) {
-    g_critical ("Failed to build category directory (2)");
-    goto free_resources;
-  }
-
-  node = xmlDocGetRootElement (doc);
-  if (!node) {
-    g_critical ("Failed to build category directory (3)");
-    goto free_resources;
-  }
-
-  if (xmlStrcmp (node->name, (const xmlChar *) "categories")) {
-    g_critical ("Failed to build category directory (4)");
-    goto free_resources;
-  }
-
-  node = node->xmlChildrenNode;
-  if (!node) {
-    g_critical ("Failed to build category directory (5)");
-    goto free_resources;
-  }
-
-  parse_categories (doc, node, GDATA_SERVICE (user_data));
-
- free_resources:
-  xmlFreeDoc (doc);
-}
-
-static gint
-get_feed_type_from_id (const gchar *feed_id)
-{
-  gchar *tmp;
-  gchar *test;
-  gint feed_type;
-
-  tmp = g_strrstr (feed_id, "/");
-  if (!tmp) {
-    return -1;
-  }
-  tmp++;
-
-  feed_type = strtol (tmp, &test, 10);
-  if (*test != '\0') {
-    return -1;
-  }
-
-  return feed_type;
-}
-
-static const gchar *
-get_category_term_from_id (const gchar *category_id)
-{
-  gchar *term;
-  term = g_strrstr (category_id, "/");
-  if (!term) {
-    return NULL;
-  }
-  return ++term;
-}
-
-static gint
-get_category_index_from_id (const gchar *category_id)
-{
-  gint i;
-
-  for (i=0; i<root_dir[ROOT_DIR_CATEGORIES_INDEX].count; i++) {
-    if (!strcmp (categories_dir[i].id, category_id)) {
-      return i;
-    }
-  }
-  return -1;
-}
-
-static void
-item_count_cb (GObject *object, GAsyncResult *result, CategoryCountCb *cc)
-{
-  GRL_DEBUG ("item_count_cb");
-
-  GDataFeed *feed;
-  GError *error = NULL;
-
-  feed = gdata_service_query_finish (GDATA_SERVICE (cc->service),
-				     result, &error);
-  if (error) {
-    GRL_WARNING ("Failed to compute count for category '%s': %s",
-                 cc->category_info->id, error->message);
-    g_error_free (error);
-  } else if (feed) {
-    cc->category_info->count = gdata_feed_get_total_results (feed);
-    GRL_DEBUG ("Category '%s' - childcount: '%u'",
-               cc->category_info->id, cc->category_info->count);
-  }
-
-  if (feed) {
-    g_object_unref (feed);
-  }
-  g_slice_free (CategoryCountCb, cc);
-}
-
-static void
-compute_category_counts (GDataService *service)
-{
-  gint i;
-
-  GRL_DEBUG ("compute_category_counts");
-
-  for (i=0; i<root_dir[ROOT_DIR_CATEGORIES_INDEX].count; i++) {
-    GRL_DEBUG ("Computing chilcount for category '%s'", categories_dir[i].id);
-    GDataQuery *query = gdata_query_new_with_limits (NULL, 0, 1);
-    const gchar *category_term =
-      get_category_term_from_id (categories_dir[i].id);
-    gdata_query_set_categories (query, category_term);
-    CategoryCountCb *cc = g_slice_new (CategoryCountCb);
-    cc->service = service;
-    cc->category_info = &categories_dir[i];
-    gdata_youtube_service_query_videos_async (GDATA_YOUTUBE_SERVICE (service),
-					      query,
-					      NULL, NULL, NULL,
-					      (GAsyncReadyCallback) item_count_cb,
-					      cc);
-    g_object_unref (query);
-  }
-}
-
-static void
-compute_feed_counts (GDataService *service)
-{
-  gint i;
-  GRL_DEBUG ("compute_feed_counts");
-
-  for (i=0; i<root_dir[ROOT_DIR_FEEDS_INDEX].count; i++) {
-    GRL_DEBUG ("Computing chilcount for feed '%s'", feeds_dir[i].id);
-    gint feed_type = get_feed_type_from_id (feeds_dir[i].id);
-    GDataQuery *query = gdata_query_new_with_limits (NULL, 0, 1);
-    CategoryCountCb *cc = g_slice_new (CategoryCountCb);
-    cc->service = service;
-    cc->category_info = &feeds_dir[i];
-    gdata_youtube_service_query_standard_feed_async (GDATA_YOUTUBE_SERVICE (service),
-						     feed_type,
-						     query,
-						     NULL, NULL, NULL,
-						     (GAsyncReadyCallback) item_count_cb,
-						     cc);
-    g_object_unref (query);
-  }
-}
-
-static void
-build_media_from_entry_metadata_cb (GrlMedia *media, gpointer user_data)
-{
-  GrlMediaSourceMetadataSpec *ms = (GrlMediaSourceMetadataSpec *) user_data;
-  ms->callback (ms->source, media, ms->user_data, NULL);
-}
-
-static void
-build_media_from_entry_search_cb (GrlMedia *media, gpointer user_data)
-{
-  /*
-   * TODO: Async resolution of URL messes (or could mess) with the sorting,
-   * If we want to ensure a particular sorting or implement sorting
-   * mechanisms we should add code to handle that here so we emit items in
-   * the right order and not just when we got the URL resolved (would
-   * damage response time though).
-   */
-  OperationSpec *os = (OperationSpec *) user_data;
-  guint remaining;
-
-  if (os->emitted < os->count) {
-    remaining = os->count - os->emitted - 1;
-    os->callback (os->source,
-		  os->operation_id,
-		  media,
-		  remaining,
-		  os->user_data,
-		  NULL);
-    if (remaining == 0) {
-      GRL_DEBUG ("Unreffing spec in build_media_from_entry_search_cb");
-      operation_spec_unref (os);
-    } else {
-      os->emitted++;
-    }
-  }
-}
-
-static void
-build_directories (GDataService *service)
-{
-  GRL_DEBUG ("build_drectories");
-
-  /* Parse category list from Youtube and compute category counts */
-  read_url_async (YOUTUBE_CATEGORIES_URL,
-                  build_categories_directory_read_cb,
-                  service);
-
-  /* Compute feed counts */
-  compute_feed_counts (service);
-}
-
-static void
-metadata_cb (GObject *object,
-	     GAsyncResult *result,
-	     gpointer user_data)
-{
-  GRL_DEBUG ("metadata_cb");
-
-  GError *error = NULL;
-  GrlYoutubeSource *source;
-  GDataEntry *video;
-  GDataService *service;
-  GrlMediaSourceMetadataSpec *ms = (GrlMediaSourceMetadataSpec *) user_data;
-
-  source = GRL_YOUTUBE_SOURCE (ms->source);
-  service = GDATA_SERVICE (source->priv->service);
-
-#ifdef GDATA_API_SUBJECT_TO_CHANGE
-  video = gdata_service_query_single_entry_finish (service, result, &error);
-#else
-  video =
-    GDATA_ENTRY (gdata_youtube_service_query_single_video_finish
-                   (GDATA_YOUTUBE_SERVICE (service), result, &error));
-#endif
-  if (error) {
-    error->code = GRL_CORE_ERROR_METADATA_FAILED;
-    ms->callback (ms->source, ms->media, ms->user_data, error);
-    g_error_free (error);
-  } else {
-    build_media_from_entry (ms->media, video, ms->keys,
-			    build_media_from_entry_metadata_cb, ms);
-  }
-
-  if (video) {
-    g_object_unref (video);
-  }
-}
-
-static void
-search_progress_cb (GDataEntry *entry,
-		    guint index,
-		    guint count,
-		    gpointer user_data)
-{
-  OperationSpec *os = (OperationSpec *) user_data;
-  if (index < count) {
-    /* Keep track of the items we got here. Due to the asynchronous
-     * nature of build_media_from_entry(), when search_cb is invoked
-     * we have to check if we got as many results as we requested or
-     * not, and handle that situation properly */
-    os->matches++;
-    build_media_from_entry (NULL, entry, os->keys,
-			    build_media_from_entry_search_cb, os);
-  } else {
-    GRL_WARNING ("Invalid index/count received grom libgdata, ignoring result");
-  }
-
-  /* The entry will be freed when freeing the feed in search_cb */
-}
-
-static void
-search_cb (GObject *object, GAsyncResult *result, OperationSpec *os)
-{
-  GRL_DEBUG ("search_cb");
-
-  GDataFeed *feed;
-  GError *error = NULL;
-  gboolean need_extra_unref = FALSE;
-  GrlYoutubeSource *source = GRL_YOUTUBE_SOURCE (os->source);
-
-  feed = gdata_service_query_finish (source->priv->service, result, &error);
-  if (!error && feed) {
-    /* If we are browsing a category, update the count for it */
-    if (os->category_info) {
-      os->category_info->count = gdata_feed_get_total_results (feed);
-    }
-
-    /* Check if we got as many results as we requested */
-    if (os->matches < os->count) {
-      os->count = os->matches;
-      /* In case we are resolving URLs asynchronously, from now on
-       * results will be sent with appropriate remaining, but it can
-       * also be the case that we have sent all the results already
-       * and the last one was sent with remaining>0, in that case
-       * we should send a finishing message now. */
-      if (os->emitted == os->count) {
-	GRL_DEBUG ("sending finishing message");
-	os->callback (os->source, os->operation_id,
-		      NULL, 0, os->user_data, NULL);
-	need_extra_unref = TRUE;
-      }
-    }
-  } else {
-    if (!error) {
-      error = g_error_new (GRL_CORE_ERROR,
-			   os->error_code,
-			   "Failed to obtain feed from Youtube");
-    } else {
-      error->code = os->error_code;
-    }
-    os->callback (os->source, os->operation_id, NULL, 0, os->user_data, error);
-    g_error_free (error);
-    need_extra_unref = TRUE;
-  }
-
-  if (feed)
-    g_object_unref (feed);
-
-  GRL_DEBUG ("Unreffing spec in search_cb");
-  operation_spec_unref (os);
-  if (need_extra_unref) {
-    /* We did not free the spec in the emission callback, do it here */
-    GRL_DEBUG ("need extra spec unref in search_cb");
-    operation_spec_unref (os);
-  }
-}
-
-static gboolean
-is_category_container (const gchar *container_id)
-{
-  return g_str_has_prefix (container_id, YOUTUBE_CATEGORIES_ID "/");
-}
-
-static gboolean
-is_feeds_container (const gchar *container_id)
-{
-  return g_str_has_prefix (container_id, YOUTUBE_FEEDS_ID "/");
-}
-
-static YoutubeMediaType
-classify_media_id (const gchar *media_id)
-{
-  if (!media_id) {
-    return YOUTUBE_MEDIA_TYPE_ROOT;
-  } else if (!strcmp (media_id, YOUTUBE_FEEDS_ID)) {
-    return YOUTUBE_MEDIA_TYPE_FEEDS;
-  } else if (!strcmp (media_id, YOUTUBE_CATEGORIES_ID)) {
-    return YOUTUBE_MEDIA_TYPE_CATEGORIES;
-  } else if (is_category_container (media_id)) {
-    return YOUTUBE_MEDIA_TYPE_CATEGORY;
-  } else if (is_feeds_container (media_id)) {
-    return YOUTUBE_MEDIA_TYPE_FEED;
-  } else {
-    return YOUTUBE_MEDIA_TYPE_VIDEO;
-  }
-}
-
-static void
-set_category_childcount (GDataService *service,
-			 GrlMediaBox *content,
-                         CategoryInfo *dir,
-                         guint index)
-{
-  gint childcount;
-  gboolean set_childcount = TRUE;
-  const gchar *container_id;
-
-  container_id = grl_media_get_id (GRL_MEDIA (content));
-
-  if (dir == NULL) {
-    /* Special case: we want childcount of root category */
-    childcount = root_dir_size;
-  } else if (!strcmp (dir[index].id, YOUTUBE_FEEDS_ID)) {
-    childcount = root_dir[ROOT_DIR_FEEDS_INDEX].count;
-  } else if (!strcmp (dir[index].id, YOUTUBE_CATEGORIES_ID)) {
-    childcount = root_dir[ROOT_DIR_CATEGORIES_INDEX].count;
-  } else if (is_feeds_container (container_id)) {
-    gint feed_index = get_feed_type_from_id (container_id);
-    if (feed_index >= 0) {
-      childcount = feeds_dir[feed_index].count;
-    } else {
-      set_childcount = FALSE;
-    }
-  } else if (is_category_container (container_id)) {
-    gint cat_index = get_category_index_from_id (container_id);
-    if (cat_index >= 0) {
-      childcount = categories_dir[cat_index].count;
-    } else {
-      set_childcount = FALSE;
-    }
-  } else {
-    set_childcount = FALSE;
-  }
-
-  if (set_childcount) {
-    grl_media_box_set_childcount (content, childcount);
-  }
-}
-
-static GrlMedia *
-produce_container_from_directory (GDataService *service,
-				  GrlMedia *media,
-				  CategoryInfo *dir,
-				  guint index)
-{
-  GrlMedia *content;
-
-  if (!media) {
-    /* Create mode */
-    content = grl_media_box_new ();
-  } else {
-    /* Update mode */
-    content = media;
-  }
-
-  if (!dir) {
-    grl_media_set_id (content, NULL);
-    grl_media_set_title (content, YOUTUBE_ROOT_NAME);
-  } else {
-    grl_media_set_id (content, dir[index].id);
-    grl_media_set_title (content, dir[index].name);
-  }
-  grl_media_set_site (content, YOUTUBE_SITE_URL);
-  set_category_childcount (service, GRL_MEDIA_BOX (content), dir, index);
-
-  return content;
-}
-
-static void
-produce_from_directory (CategoryInfo *dir, guint dir_size, OperationSpec *os)
-{
-  GRL_DEBUG ("produce_from_directory");
-
-  guint index, remaining;
-
-  /* Youtube's first index is 1, but the directories start at 0 */
-  os->skip--;
-
-  if (os->skip >= dir_size) {
-    /* No results */
-    os->callback (os->source,
-		  os->operation_id,
-		  NULL,
-		  0,
-		  os->user_data,
-		  NULL);
-    operation_spec_unref (os);
-  } else {
-    index = os->skip;
-    remaining = MIN (dir_size - os->skip, os->count);
-
-    do {
-      GDataService *service = GRL_YOUTUBE_SOURCE (os->source)->priv->service;
-
-      GrlMedia *content =
-	produce_container_from_directory (service, NULL, dir, index);
-
-      remaining--;
-      index++;
-
-      os->callback (os->source,
-		    os->operation_id,
-		    content,
-		    remaining,
-		    os->user_data,
-		    NULL);
-
-      if (remaining == 0) {
-	operation_spec_unref (os);
-      }
-    } while (remaining > 0);
-  }
-}
-
-static void
-produce_from_feed (OperationSpec *os)
-{
-  GError *error = NULL;
-  gint feed_type;
-  GDataQuery *query;
-  GDataService *service;
-
-  feed_type = get_feed_type_from_id (os->container_id);
-
-  if (feed_type < 0) {
-    error = g_error_new (GRL_CORE_ERROR,
-			 GRL_CORE_ERROR_BROWSE_FAILED,
-			 "Invalid feed id: %s", os->container_id);
-    os->callback (os->source,
-		  os->operation_id,
-		  NULL,
-		  0,
-		  os->user_data,
-		  error);
-    g_error_free (error);
-    operation_spec_unref (os);
-    return;
-  }
-  /* OPERATION_SPEC_REF_RATIONALE
-   * Depending on wether the URL has been requested, metadata resolution
-   * for each item in the result set may or may not be asynchronous.
-   * We cannot free the spec in search_cb because that may be called
-   * before the asynchronous URL resolution is finished, and we cannot
-   * do it in build_media_from_entry_search_cb either, because in the
-   * synchronous case (when we do not request URL) search_cb will
-   * be invoked after it.
-   * Thus, the solution is to increase the reference count here and
-   * have both places unreffing the spec, that way, no matter which
-   * is invoked last, the spec will be freed only once. */
-  operation_spec_ref (os);
-
-  service = GRL_YOUTUBE_SOURCE (os->source)->priv->service;
-  query = gdata_query_new_with_limits (NULL , os->skip, os->count);
-  os->category_info = &feeds_dir[feed_type];
-  gdata_youtube_service_query_standard_feed_async (GDATA_YOUTUBE_SERVICE (service),
-                                                   feed_type,
-                                                   query,
-                                                   NULL,
-                                                   search_progress_cb,
-                                                   os,
-                                                   (GAsyncReadyCallback) search_cb,
-                                                   os);
-  g_object_unref (query);
-}
-
-static void
-produce_from_category (OperationSpec *os)
-{
-  GError *error = NULL;
-  GDataQuery *query;
-  GDataService *service;
-  const gchar *category_term;
-  gint category_index;
-
-  category_term = get_category_term_from_id (os->container_id);
-  category_index = get_category_index_from_id (os->container_id);
-
-  if (!category_term) {
-    error = g_error_new (GRL_CORE_ERROR,
-			 GRL_CORE_ERROR_BROWSE_FAILED,
-			 "Invalid category id: %s", os->container_id);
-    os->callback (os->source,
-		  os->operation_id,
-		  NULL,
-		  0,
-		  os->user_data,
-		  error);
-    g_error_free (error);
-    operation_spec_unref (os);
-    return;
-  }
-
-  /* Look for OPERATION_SPEC_REF_RATIONALE for details */
-  operation_spec_ref (os);
-
-  service = GRL_YOUTUBE_SOURCE (os->source)->priv->service;
-  query = gdata_query_new_with_limits (NULL , os->skip, os->count);
-  os->category_info = &categories_dir[category_index];
-  gdata_query_set_categories (query, category_term);
-  gdata_youtube_service_query_videos_async (GDATA_YOUTUBE_SERVICE (service),
-					    query,
-					    NULL,
-					    search_progress_cb,
-					    os,
-					    (GAsyncReadyCallback) search_cb,
-					    os);
-  g_object_unref (query);
-}
-
-static gchar *
-get_video_id_from_url (const gchar *url)
-{
-  gchar *marker, *end, *video_id;
-
-  if (url == NULL) {
-    return NULL;
-  }
-
-  marker = strstr (url, YOUTUBE_WATCH_URL);
-  if (!marker) {
-    return NULL;
-  }
-
-  marker += strlen (YOUTUBE_WATCH_URL);
-
-  end = marker;
-  while (*end != '\0' && *end != '&') {
-    end++;
-  }
-
-  video_id = g_strndup (marker, end - marker);
-
-  return video_id;
-}
-
-static void
-build_media_from_entry_media_from_uri_cb (GrlMedia *media, gpointer user_data)
-{
-  GrlMediaSourceMediaFromUriSpec *mfus =
-    (GrlMediaSourceMediaFromUriSpec *) user_data;
-  mfus->callback (mfus->source, media, mfus->user_data, NULL);
-}
-
-static void
-media_from_uri_cb (GObject *object, GAsyncResult *result, gpointer user_data)
-{
-  GError *error = NULL;
-  GrlYoutubeSource *source;
-  GDataEntry *video;
-  GDataService *service;
-  GrlMediaSourceMediaFromUriSpec *mfus =
-    (GrlMediaSourceMediaFromUriSpec *) user_data;
-
-  source = GRL_YOUTUBE_SOURCE (mfus->source);
-  service = GDATA_SERVICE (source->priv->service);
-
-#ifdef GDATA_API_SUBJECT_TO_CHANGE
-  video = gdata_service_query_single_entry_finish (service, result, &error);
-#else
-  video =
-    GDATA_ENTRY (gdata_youtube_service_query_single_video_finish
-		 (GDATA_YOUTUBE_SERVICE (service), result, &error));
-#endif
-
-  if (error) {
-    error->code = GRL_CORE_ERROR_MEDIA_FROM_URI_FAILED;
-    mfus->callback (mfus->source, NULL, mfus->user_data, error);
-    g_error_free (error);
-  } else {
-    build_media_from_entry (NULL, video, mfus->keys,
-			    build_media_from_entry_media_from_uri_cb,
-			    mfus);
-  }
-
-  if (video) {
-    g_object_unref (video);
-  }
-}
-
-/* ================== API Implementation ================ */
-
-static const GList *
-grl_youtube_source_supported_keys (GrlMetadataSource *source)
-{
-  static GList *keys = NULL;
-  if (!keys) {
-    keys = grl_metadata_key_list_new (GRL_METADATA_KEY_ID,
-                                      GRL_METADATA_KEY_TITLE,
-                                      GRL_METADATA_KEY_URL,
-				      GRL_METADATA_KEY_EXTERNAL_URL,
-                                      GRL_METADATA_KEY_DESCRIPTION,
-                                      GRL_METADATA_KEY_DURATION,
-                                      GRL_METADATA_KEY_DATE,
-                                      GRL_METADATA_KEY_THUMBNAIL,
-                                      GRL_METADATA_KEY_MIME,
-                                      GRL_METADATA_KEY_CHILDCOUNT,
-                                      GRL_METADATA_KEY_SITE,
-                                      GRL_METADATA_KEY_RATING,
-				      GRL_METADATA_KEY_EXTERNAL_PLAYER,
-                                      NULL);
-  }
-  return keys;
-}
-
-static const GList *
-grl_youtube_source_slow_keys (GrlMetadataSource *source)
-{
-  static GList *keys = NULL;
-  if (!keys) {
-    keys = grl_metadata_key_list_new (GRL_METADATA_KEY_URL,
-                                      NULL);
-  }
-  return keys;
-}
-
-static void
-grl_youtube_source_search (GrlMediaSource *source,
-                           GrlMediaSourceSearchSpec *ss)
-{
-  OperationSpec *os;
-  GDataQuery *query;
-
-  GRL_DEBUG ("grl_youtube_source_search (%u, %u)", ss->skip, ss->count);
-
-  os = operation_spec_new ();
-  os->source = source;
-  os->operation_id = ss->search_id;
-  os->keys = ss->keys;
-  os->skip = ss->skip + 1;
-  os->count = ss->count;
-  os->callback = ss->callback;
-  os->user_data = ss->user_data;
-  os->error_code = GRL_CORE_ERROR_SEARCH_FAILED;
-
-  /* Look for OPERATION_SPEC_REF_RATIONALE for details */
-  operation_spec_ref (os);
-
-  query = gdata_query_new_with_limits (ss->text, os->skip, os->count);
-  gdata_youtube_service_query_videos_async (GDATA_YOUTUBE_SERVICE (GRL_YOUTUBE_SOURCE (source)->priv->service),
-					    query,
-					    NULL,
-					    search_progress_cb,
-					    os,
-					    (GAsyncReadyCallback) search_cb,
-					    os);
-  g_object_unref (query);
-}
-
-static void
-grl_youtube_source_browse (GrlMediaSource *source,
-                           GrlMediaSourceBrowseSpec *bs)
-{
-  OperationSpec *os;
-  const gchar *container_id;
-
-  GRL_DEBUG ("grl_youtube_source_browse: %s", grl_media_get_id (bs->container));
-
-  container_id = grl_media_get_id (bs->container);
-
-  os = operation_spec_new ();
-  os->source = bs->source;
-  os->operation_id = bs->browse_id;
-  os->container_id = container_id;
-  os->keys = bs->keys;
-  os->flags = bs->flags;
-  os->skip = bs->skip + 1;
-  os->count = bs->count;
-  os->callback = bs->callback;
-  os->user_data = bs->user_data;
-  os->error_code = GRL_CORE_ERROR_BROWSE_FAILED;
-
-  switch (classify_media_id (container_id))
-    {
-    case YOUTUBE_MEDIA_TYPE_ROOT:
-      produce_from_directory (root_dir, root_dir_size, os);
-      break;
-    case YOUTUBE_MEDIA_TYPE_FEEDS:
-      produce_from_directory (feeds_dir,
-			      root_dir[ROOT_DIR_FEEDS_INDEX].count, os);
-      break;
-    case YOUTUBE_MEDIA_TYPE_CATEGORIES:
-      produce_from_directory (categories_dir,
-			      root_dir[ROOT_DIR_CATEGORIES_INDEX].count, os);
-      break;
-    case YOUTUBE_MEDIA_TYPE_FEED:
-      produce_from_feed (os);
-      break;
-    case YOUTUBE_MEDIA_TYPE_CATEGORY:
-      produce_from_category (os);
-      break;
-    default:
-      g_assert_not_reached ();
-      break;
-    }
-}
-
-static void
-grl_youtube_source_metadata (GrlMediaSource *source,
-                             GrlMediaSourceMetadataSpec *ms)
-{
-  YoutubeMediaType media_type;
-  const gchar *id;
-  GDataService *service;
-  GError *error = NULL;
-  GrlMedia *media = NULL;
-
-  GRL_DEBUG ("grl_youtube_source_metadata");
-
-  id = grl_media_get_id (ms->media);
-  media_type = classify_media_id (id);
-  service = GRL_YOUTUBE_SOURCE (source)->priv->service;
-
-  switch (media_type) {
-  case YOUTUBE_MEDIA_TYPE_ROOT:
-    media = produce_container_from_directory (service, ms->media, NULL, 0);
-    break;
-  case YOUTUBE_MEDIA_TYPE_FEEDS:
-    media = produce_container_from_directory (service, ms->media, root_dir, 0);
-    break;
-  case YOUTUBE_MEDIA_TYPE_CATEGORIES:
-    media = produce_container_from_directory (service, ms->media, root_dir, 1);
-    break;
-  case YOUTUBE_MEDIA_TYPE_FEED:
-    {
-      gint index = get_feed_type_from_id (id);
-      if (index >= 0) {
-	media = produce_container_from_directory (service, ms->media, feeds_dir,
-						  index);
-      } else {
-	error = g_error_new (GRL_CORE_ERROR,
-			     GRL_CORE_ERROR_METADATA_FAILED,
-			     "Invalid feed id");
-      }
-    }
-    break;
-  case YOUTUBE_MEDIA_TYPE_CATEGORY:
-    {
-      gint index = get_category_index_from_id (id);
-      if (index >= 0) {
-	media = produce_container_from_directory (service, ms->media,
-						  categories_dir, index);
-      } else {
-	error = g_error_new (GRL_CORE_ERROR,
-			     GRL_CORE_ERROR_METADATA_FAILED,
-			     "Invalid category id");
-      }
-    }
-    break;
-  case YOUTUBE_MEDIA_TYPE_VIDEO:
-  default:
-#ifdef GDATA_API_SUBJECT_TO_CHANGE
-    {
-      gchar *entryid = g_strconcat ("tag:youtube.com,2008:video:", id, NULL);
-      gdata_service_query_single_entry_async (service,
-                                              entryid,
-                                              NULL,
-                                              GDATA_TYPE_YOUTUBE_VIDEO,
-                                              NULL,
-                                              metadata_cb,
-                                              ms);
-      g_free (entryid);
-    }
-#else
-    gdata_youtube_service_query_single_video_async (GDATA_YOUTUBE_SERVICE (service),
-                                                    NULL,
-                                                    id,
-                                                    NULL,
-                                                    metadata_cb,
-                                                    ms);
-#endif
-    break;
-  }
-
-  if (error) {
-    ms->callback (ms->source, ms->media, ms->user_data, error);
-    g_error_free (error);
-  } else if (media) {
-    ms->callback (ms->source, ms->media, ms->user_data, NULL);
-  }
-}
-
-static gboolean
-grl_youtube_test_media_from_uri (GrlMediaSource *source, const gchar *uri)
-{
-  GRL_DEBUG ("grl_youtube_test_media_from_uri");
-
-  gchar *video_id;
-  gboolean ok;
-
-  video_id = get_video_id_from_url (uri);
-  ok = (video_id != NULL);
-  g_free (video_id);
-  return ok;
-}
-
-static void
-grl_youtube_get_media_from_uri (GrlMediaSource *source,
-				 GrlMediaSourceMediaFromUriSpec *mfus)
-{
-  GRL_DEBUG ("grl_youtube_get_media_from_uri");
-
-  gchar *video_id;
-  GError *error;
-  GDataService *service;
-
-  video_id = get_video_id_from_url (mfus->uri);
-  if (video_id == NULL) {
-    error = g_error_new (GRL_CORE_ERROR,
-			 GRL_CORE_ERROR_MEDIA_FROM_URI_FAILED,
-			 "Cannot create media from '%s'", mfus->uri);
-    mfus->callback (source, NULL, mfus->user_data, error);
-    g_error_free (error);
-    return;
-  }
-
-  service = GRL_YOUTUBE_SOURCE (source)->priv->service;
-
-#ifdef GDATA_API_SUBJECT_TO_CHANGE
-  gchar *entry_id = g_strconcat ("tag:youtube.com,2008:video:", video_id, NULL);
-  gdata_service_query_single_entry_async (service,
-					  entry_id,
-					  NULL,
-					  GDATA_TYPE_YOUTUBE_VIDEO,
-					  NULL,
-					  media_from_uri_cb,
-					  mfus);
-  g_free (entry_id);
-#else
-  gdata_youtube_service_query_single_video_async (GDATA_YOUTUBE_SERVICE (service),
-						  NULL,
-						  video_id,
-						  NULL,
-						  media_from_uri_cb,
-						  mfus);
-#endif
-}
diff --git a/src/youtube/grl-youtube.h b/src/youtube/grl-youtube.h
deleted file mode 100644
index dc88589..0000000
--- a/src/youtube/grl-youtube.h
+++ /dev/null
@@ -1,74 +0,0 @@
-/*
- * Copyright (C) 2010 Igalia S.L.
- *
- * Contact: Iago Toral Quiroga <itoral igalia 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_YOUTUBE_SOURCE_H_
-#define _GRL_YOUTUBE_SOURCE_H_
-
-#include <grilo.h>
-
-#define GRL_YOUTUBE_SOURCE_TYPE                 \
-  (grl_youtube_source_get_type ())
-
-#define GRL_YOUTUBE_SOURCE(obj)                         \
-  (G_TYPE_CHECK_INSTANCE_CAST ((obj),                   \
-                               GRL_YOUTUBE_SOURCE_TYPE, \
-                               GrlYoutubeSource))
-
-#define GRL_IS_YOUTUBE_SOURCE(obj)                              \
-  (G_TYPE_CHECK_INSTANCE_TYPE ((obj),                           \
-                               GRL_YOUTUBE_SOURCE_TYPE))
-
-#define GRL_YOUTUBE_SOURCE_CLASS(klass)                 \
-  (G_TYPE_CHECK_CLASS_CAST((klass),                     \
-                           GRL_YOUTUBE_SOURCE_TYPE,     \
-                           GrlYoutubeSourceClass))
-
-#define GRL_IS_YOUTUBE_SOURCE_CLASS(klass)              \
-  (G_TYPE_CHECK_CLASS_TYPE((klass),                     \
-                           GRL_YOUTUBE_SOURCE_TYPE))
-
-#define GRL_YOUTUBE_SOURCE_GET_CLASS(obj)               \
-  (G_TYPE_INSTANCE_GET_CLASS ((obj),                    \
-                              GRL_YOUTUBE_SOURCE_TYPE,  \
-                              GrlYoutubeSourceClass))
-
-typedef struct _GrlYoutubeSource GrlYoutubeSource;
-typedef struct _GrlYoutubeSourcePriv GrlYoutubeSourcePriv;
-
-struct _GrlYoutubeSource {
-
-  GrlMediaSource parent;
-  GrlYoutubeSourcePriv *priv;
-
-};
-
-typedef struct _GrlYoutubeSourceClass GrlYoutubeSourceClass;
-
-struct _GrlYoutubeSourceClass {
-
-  GrlMediaSourceClass parent_class;
-
-};
-
-GType grl_youtube_source_get_type (void);
-
-#endif /* _GRL_YOUTUBE_SOURCE_H_ */
diff --git a/src/youtube/grl-youtube.xml b/src/youtube/grl-youtube.xml
deleted file mode 100644
index b874678..0000000
--- a/src/youtube/grl-youtube.xml
+++ /dev/null
@@ -1,9 +0,0 @@
-<plugin>
-  <info>
-    <name>YouTube</name>
-    <description>A plugin for browsing and searching Youtube videos</description>
-    <author>Igalia S.L.</author>
-    <license>LGPL</license>
-    <site>http://www.igalia.com</site>
-  </info>
-</plugin>
-- 
1.7.1



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