commit 5d8835b69b0351a2d5da4e28253cb4dc8b5d1f5a
Author: George Sedov <radist.morse@gmail.com>
Date:   Wed Mar 4 13:17:20 2015 +0300

    Add support for the VK.com music provider

diff --git a/Makefile.am b/Makefile.am
index 3113453..48a1527 100644
--- a/Makefile.am
+++ b/Makefile.am
@@ -58,6 +58,7 @@ DISTCHECK_CONFIGURE_FLAGS = --enable-apple-trailers \
                             --enable-tmdb \
                             --enable-tracker \
                             --enable-vimeo \
+                            --enable-vkontakte \
                             --enable-youtube

 distclean-local:
diff --git a/configure.ac b/configure.ac
index 80e3786..7b45627 100644
--- a/configure.ac
+++ b/configure.ac
@@ -915,6 +915,57 @@ DEPS_VIMEO_LIBS="$DEPS_LIBS $GRLNET_LIBS $XML_LIBS $TOTEM_PL_PARSER_LIBS"
 AC_SUBST(DEPS_VIMEO_LIBS)

 # ----------------------------------------------------------
+# BUILD VKONTAKTE PLUGIN
+# ----------------------------------------------------------
+
+AC_ARG_ENABLE(vkontakte,
+        AC_HELP_STRING([--enable-vkontakte],
+                [enable VKontakte plugin (default: auto)]),
+        [
+                case "$enableval" in
+                     yes)
+                        if test "x$HAVE_GRLNET" = "xno"; then
+                           AC_MSG_ERROR([grilo-net-0.2 >= 0.2.2 not found, install it or use --disable-vkontakte])
+                        fi
+                        if test "x$HAVE_JSON_GLIB" = "xno"; then
+                           AC_MSG_ERROR([json-glib-1.0 not found, install it or use --disable-vkontakte])
+                        fi
+                        ;;
+                esac
+        ],
+        [
+                if test "x$HAVE_GRLNET" = "xyes" -a "x$HAVE_JSON_GLIB" = "xyes"; then
+                   enable_vkontakte=yes
+                else
+                   enable_vkontakte=no
+                fi
+        ])
+
+AM_CONDITIONAL([VKONTAKTE_PLUGIN], [test "x$enable_vkontakte" = "xyes"])
+GRL_PLUGINS_ALL="$GRL_PLUGINS_ALL vkontakte"
+if test "x$enable_vkontakte" = "xyes"
+then
+	GRL_PLUGINS_ENABLED="$GRL_PLUGINS_ENABLED vkontakte"
+fi
+
+VKONTAKTE_PLUGIN_ID="grl-vkontakte"
+AC_SUBST(VKONTAKTE_PLUGIN_ID)
+AC_DEFINE_UNQUOTED([VKONTAKTE_PLUGIN_ID], ["$VKONTAKTE_PLUGIN_ID"], [VKontakte plugin ID])
+
+DEPS_VKONTAKTE_CFLAGS="$DEPS_CFLAGS $JSON_CFLAGS $GRLNET_CFLAGS"
+if test "x$enable_goa" = "xyes"; then
+	DEPS_VKONTAKTE_CFLAGS="$DEPS_VKONTAKTE_CFLAGS $GOA_CFLAGS"
+fi
+AC_SUBST(DEPS_VKONTAKTE_CFLAGS)
+
+
+DEPS_VKONTAKTE_LIBS="$DEPS_LIBS $JSON_LIBS $GRLNET_LIBS"
+if test "x$enable_goa" = "xyes"; then
+	DEPS_VKONTAKTE_LIBS="$DEPS_VKONTAKTE_LIBS $GOA_LIBS"
+fi
+AC_SUBST(DEPS_VKONTAKTE_LIBS)
+
+# ----------------------------------------------------------
 # BUILD GRAVATAR PLUGIN
 # ----------------------------------------------------------

@@ -1442,6 +1493,7 @@ AC_CONFIG_FILES([
   src/tmdb/Makefile
   src/tracker/Makefile
   src/vimeo/Makefile
+  src/vkontakte/Makefile
   src/youtube/Makefile
   tests/Makefile
   tests/apple-trailers/Makefile
@@ -1453,6 +1505,7 @@ AC_CONFIG_FILES([
   tests/thetvdb/Makefile
   tests/tmdb/Makefile
   tests/vimeo/Makefile
+  tests/vkontakte/Makefile
 ])

 AC_OUTPUT
diff --git a/help/C/grilo-plugins.xml b/help/C/grilo-plugins.xml
index ee3d710..c692e4f 100644
--- a/help/C/grilo-plugins.xml
+++ b/help/C/grilo-plugins.xml
@@ -156,6 +156,11 @@
 <para>This is a Grilo plugin for Vimeo. Its plugin ID is <literal>"grl-vimeo"</literal></para>
 </sect1>

+<sect1 id="sec-plugin-vkontakte">
+<title>VKontakte</title>
+<para>This is a Grilo plugin for VKontakte. Its plugin ID is <literal>"grl-vkontakte"</literal></para>
+</sect1>
+
 <sect1 id="sec-plugin-youtube">
 <title>YouTube</title>
 <para>This is a Grilo plugin for YouTube. Its plugin ID is <literal>"grl-youtube"</literal></para>
diff --git a/po/POTFILES.in b/po/POTFILES.in
index c373a8e..c7c4774 100644
--- a/po/POTFILES.in
+++ b/po/POTFILES.in
@@ -26,4 +26,5 @@ src/tracker/grl-tracker-source-api.c
 src/tracker/grl-tracker-source-priv.h
 src/tracker/grl-tracker-utils.c
 src/vimeo/grl-vimeo.c
+src/vkontakte/grl-vkontakte.c
 src/youtube/grl-youtube.c
diff --git a/src/Makefile.am b/src/Makefile.am
index 67be649..e9ab701 100644
--- a/src/Makefile.am
+++ b/src/Makefile.am
@@ -102,6 +102,10 @@ if VIMEO_PLUGIN
 SUBDIRS += vimeo
 endif

+if VKONTAKTE_PLUGIN
+SUBDIRS += vkontakte
+endif
+
 if YOUTUBE_PLUGIN
 SUBDIRS += youtube
 endif
@@ -117,7 +121,7 @@ endif
 DIST_SUBDIRS = \
    apple-trailers bliptv bookmarks dleyna dmap filesystem flickr freebox gravatar jamendo \
    lastfm-albumart local-metadata lua-factory magnatune metadata-store opensubtitles \
-   optical-media pocket podcasts raitv shoutcast thetvdb tmdb tracker vimeo youtube
+   optical-media pocket podcasts raitv shoutcast thetvdb tmdb tracker vimeo vkontakte youtube

 MAINTAINERCLEANFILES =	\
         *.in		\
diff --git a/src/vkontakte/Makefile.am b/src/vkontakte/Makefile.am
new file mode 100644
index 0000000..acad8a5
--- /dev/null
+++ b/src/vkontakte/Makefile.am
@@ -0,0 +1,45 @@
+#
+# Makefile.am
+#
+# Author: George Sedov <radist.morse@gmail.com>
+#
+# Copyright (C) 2015 George Sedov. All rights reserved.
+
+include $(top_srcdir)/gtester.mk
+
+ext_LTLIBRARIES = libgrlvkontakte.la
+
+libgrlvkontakte_la_CFLAGS =	\
+	$(DEPS_VKONTAKTE_CFLAGS)	\
+	-DG_LOG_DOMAIN=\"GrlVkontakte\" \
+	-DLOCALEDIR=\"$(localedir)\"
+
+libgrlvkontakte_la_LIBADD =	\
+	$(DEPS_VKONTAKTE_LIBS)
+
+libgrlvkontakte_la_LDFLAGS = \
+	-no-undefined	    \
+	-module		    \
+	-avoid-version
+
+libgrlvkontakte_la_SOURCES = grl-vkontakte.c grl-vkontakte.h
+
+extdir				= $(GRL_PLUGINS_DIR)
+vkontaktexmldir	= $(GRL_PLUGINS_DIR)
+vkontaktexml_DATA	= $(VKONTAKTE_PLUGIN_ID).xml
+
+# This lets us test the plugin without installing it,
+# because grilo expects the .so and .xml files to be in
+# the same directory:
+copy-xml-to-libs-dir: libgrlvkontakte.la
+	cp -f $(srcdir)/$(vkontaktexml_DATA) $(builddir)/.libs/
+
+all-local: copy-xml-to-libs-dir
+
+EXTRA_DIST += $(vkontaktexml_DATA)
+
+MAINTAINERCLEANFILES =	\
+	*.in		\
+	*~
+
+DISTCLEANFILES = $(MAINTAINERCLEANFILES)
diff --git a/src/vkontakte/grl-vkontakte.c b/src/vkontakte/grl-vkontakte.c
new file mode 100644
index 0000000..96f32d2
--- /dev/null
+++ b/src/vkontakte/grl-vkontakte.c
@@ -0,0 +1,704 @@
+/*
+ * Copyright (C) 2015 George Sedov.
+ *
+ * Contact: George Sedov <radist.morse@gmail.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 <json-glib/json-glib.h>
+#include <glib/gi18n-lib.h>
+#include <grilo.h>
+#include <net/grl-net.h>
+#include <string.h>
+
+#ifdef GOA_ENABLED
+#define GOA_API_IS_SUBJECT_TO_CHANGE
+#include <goa/goa.h>
+#define GOA_VK_NAME "vk"
+#endif
+
+#include "grl-vkontakte.h"
+
+#define GRL_VKONTAKTE_SOURCE_GET_PRIVATE(object)          \
+  (G_TYPE_INSTANCE_GET_PRIVATE((object),                  \
+                               GRL_VKONTAKTE_SOURCE_TYPE, \
+                               GrlVkontakteSourcePrivate))
+
+/* --------- Logging  -------- */
+
+#define GRL_LOG_DOMAIN_DEFAULT vkontakte_log_domain
+GRL_LOG_DOMAIN (vkontakte_log_domain);
+
+/* --- URLs --- */
+
+#define VK_API_VER                   "5.28"
+#define URL_VK_BASE                  "https://api.vk.com/method/"
+#define URL_VK_BROWSE                URL_VK_BASE "audio.get?format=json&access_token=%s&v=" VK_API_VER
+#define URL_VK_SEARCH                URL_VK_BASE "audio.search?format=json&q=%s&count=%d&offset=%d&access_token=%s&v=" VK_API_VER
+
+#define SEARCH_MAX  100
+
+/* --- Plugin information --- */
+
+#define SOURCE_ID       "grl-vkontakte"
+#define SOURCE_NAME     _("VK.com")
+#define SOURCE_DESC     _("A source for browsing and searching VK music")
+
+/* --- GOA names, personalized --- */
+
+#define PERSONAL_SOURCE_ID      "grl-vk-%s"
+/* Translators: %s is a name of the account holder, e.g. "VK.com (John Smith)" */
+#define PERSONAL_SOURCE_NAME    _("VK.com (%s)")
+
+struct _GrlVkontakteSourcePrivate {
+  gchar *token;
+  GrlNetWc *grlwc;
+};
+
+struct _GrlSourceReqSpec {
+  GrlSource *source;
+  guint operation_id;
+  GrlSourceResultCb callback;
+  gpointer user_data;
+};
+typedef struct _GrlSourceReqSpec GrlSourceReqSpec;
+
+#ifdef GOA_ENABLED
+struct _GrlVkGoaData {
+  GrlPlugin *plugin;
+  GrlRegistry *registry;
+  GHashTable *sources;
+};
+typedef struct _GrlVkGoaData GrlVkGoaData;
+#endif
+
+/* ================ Forward Declarations ================ */
+
+static GrlVkontakteSource *grl_vkontakte_source_new (GrlPlugin *plugin,
+                                                     const gchar *vk_username,
+                                                     const gchar *vk_token,
+                                                     const gchar *account_id);
+
+static void grl_vkontakte_source_finalize (GObject *object);
+
+static const GList *grl_vkontakte_source_supported_keys (GrlSource *source);
+
+static void grl_vkontakte_source_search (GrlSource *source,
+                                         GrlSourceSearchSpec *ss);
+
+static void grl_vkontakte_source_browse (GrlSource *source,
+                                         GrlSourceBrowseSpec *bs);
+
+#ifdef GOA_ENABLED
+static void grl_vkontakte_goa_init (GObject *source_object,
+                                    GAsyncResult *res,
+                                    gpointer user_data);
+#endif /* GOA_ENABLED */
+
+/* ================== Vkontakte Plugin  ================= */
+
+static gboolean
+grl_vkontakte_plugin_init (GrlRegistry *registry,
+                           GrlPlugin *plugin,
+                           GList *configs)
+{
+  GrlConfig *config;
+  gchar *vk_token;
+  gchar *vk_username;
+
+  GRL_LOG_DOMAIN_INIT (vkontakte_log_domain, "vkontakte");
+
+  /* Initialize i18n */
+  bindtextdomain (GETTEXT_PACKAGE, LOCALEDIR);
+  bind_textdomain_codeset (GETTEXT_PACKAGE, "UTF-8");
+
+  GRL_DEBUG ("Parse provided configs");
+  while (configs) {
+    config = GRL_CONFIG (configs->data);
+    vk_token = grl_config_get_api_token (config);
+    vk_username = grl_config_get_username (config);
+    if (vk_token) {
+      GrlVkontakteSource *source = grl_vkontakte_source_new (plugin, vk_username, vk_token, NULL);
+
+      grl_registry_register_source (registry,
+                                    plugin,
+                                    GRL_SOURCE (source),
+                                    NULL);
+
+      GRL_DEBUG ("Added source from the provided config");
+    } else {
+      if (vk_username)
+        GRL_INFO ("Required API token not provided. "
+                  " Cannot create source for %s", vk_username);
+      else
+        GRL_INFO ("Required API token not provided. "
+                  " Cannot create source");
+    }
+
+    configs = g_list_next (configs);
+  }
+
+#ifdef GOA_ENABLED
+  GrlVkGoaData *user_data = g_slice_new (GrlVkGoaData);
+  user_data->plugin = plugin;
+  user_data->registry = registry;
+
+  goa_client_new (NULL, grl_vkontakte_goa_init, user_data);
+#endif /* GOA_ENABLED */
+
+  return TRUE;
+}
+
+GRL_PLUGIN_REGISTER (grl_vkontakte_plugin_init, NULL, SOURCE_ID);
+
+/* ================== Vkontakte GObject ================= */
+
+G_DEFINE_TYPE (GrlVkontakteSource, grl_vkontakte_source, GRL_TYPE_SOURCE);
+
+static GrlVkontakteSource *
+grl_vkontakte_source_new (GrlPlugin *plugin,
+                          const gchar *vk_username,
+                          const gchar *vk_token,
+                          const gchar *account_id)
+{
+  GObject *object;
+  GrlVkontakteSource *source;
+  gchar* source_id;
+  gchar* source_name;
+  gchar* source_desc;
+
+  GRL_DEBUG ("vkontakte_source_new");
+
+  if (account_id)
+    source_id = g_strdup_printf (PERSONAL_SOURCE_ID, account_id);
+  else
+    source_id = g_strdup (SOURCE_ID);
+
+  source_desc = g_strdup (SOURCE_DESC);
+  if (vk_username)
+    source_name = g_strdup_printf (PERSONAL_SOURCE_NAME, vk_username);
+  else
+    source_name = g_strdup (SOURCE_NAME);
+
+  object = g_object_new (GRL_VKONTAKTE_SOURCE_TYPE,
+                         "source-id", source_id,
+                         "source-name", source_name,
+                         "source-desc", source_desc,
+                         "supported-media", GRL_MEDIA_TYPE_AUDIO,
+                         NULL);
+
+  source = GRL_VKONTAKTE_SOURCE (object);
+
+  source->priv->token = g_strdup (vk_token);
+  source->priv->grlwc = grl_net_wc_new ();
+
+  g_clear_pointer (&source_id, g_free);
+  g_clear_pointer (&source_name, g_free);
+  g_clear_pointer (&source_desc, g_free);
+
+  return source;
+}
+
+static void
+grl_vkontakte_source_class_init (GrlVkontakteSourceClass * klass)
+{
+  GObjectClass *gobject_class = G_OBJECT_CLASS (klass);
+  GrlSourceClass *source_class = GRL_SOURCE_CLASS (klass);
+
+  gobject_class->finalize = grl_vkontakte_source_finalize;
+
+  source_class->supported_keys = grl_vkontakte_source_supported_keys;
+  source_class->search = grl_vkontakte_source_search;
+  source_class->browse = grl_vkontakte_source_browse;
+
+  g_type_class_add_private (klass, sizeof (GrlVkontakteSourcePrivate));
+}
+
+static void
+grl_vkontakte_source_init (GrlVkontakteSource *source)
+{
+  GRL_DEBUG ("vkontakte_source_init");
+
+  source->priv = GRL_VKONTAKTE_SOURCE_GET_PRIVATE (source);
+
+  grl_source_set_auto_split_threshold (GRL_SOURCE (source), SEARCH_MAX);
+}
+
+static void
+grl_vkontakte_source_finalize (GObject *object)
+{
+  GRL_DEBUG ("grl_vkontakte_source_finalize");
+
+  GrlVkontakteSource *source = GRL_VKONTAKTE_SOURCE (object);
+
+  g_clear_pointer (&source->priv->token, g_free);
+  g_object_unref (source->priv->grlwc);
+
+  G_OBJECT_CLASS (grl_vkontakte_source_parent_class)->finalize (object);
+}
+
+/* =============== Forward Util Declaration============== */
+
+static void
+vk_req_async_cb (GObject *source_object,
+                 GAsyncResult *res,
+                 gpointer user_data);
+
+static void
+vk_parse_media_cb (JsonArray *array,
+                   guint index_,
+                   JsonNode *element_node,
+                   gpointer user_data);
+
+/* ================== API Implementation ================ */
+
+static const GList *
+grl_vkontakte_source_supported_keys (GrlSource *source)
+{
+  static GList *keys = NULL;
+  if (!keys) {
+    keys = grl_metadata_key_list_new (GRL_METADATA_KEY_ID,
+                                      GRL_METADATA_KEY_ARTIST,
+                                      GRL_METADATA_KEY_DURATION,
+                                      GRL_METADATA_KEY_TITLE,
+                                      GRL_METADATA_KEY_URL,
+                                      GRL_METADATA_KEY_INVALID);
+  }
+  return keys;
+}
+
+static void
+grl_vkontakte_source_search (GrlSource *source, GrlSourceSearchSpec *ss)
+{
+  gchar *vk_http_search;
+  gchar *escaped_req;
+  GrlSourceReqSpec *rs = g_slice_new (GrlSourceReqSpec);
+  rs->source = ss->source;
+  rs->operation_id = ss->operation_id;
+  rs->callback = ss->callback;
+  rs->user_data = ss->user_data;
+
+  escaped_req = g_uri_escape_string (ss->text, NULL, TRUE);
+  vk_http_search = g_strdup_printf (URL_VK_SEARCH,
+                                    escaped_req,
+                                    grl_operation_options_get_count (ss->options),
+                                    grl_operation_options_get_skip (ss->options),
+                                    GRL_VKONTAKTE_SOURCE (source)->priv->token);
+  g_free (escaped_req);
+  grl_net_wc_request_async (GRL_VKONTAKTE_SOURCE (source)->priv->grlwc,
+                            vk_http_search,
+                            NULL,
+                            vk_req_async_cb,
+                            rs);
+}
+
+static void
+grl_vkontakte_source_browse (GrlSource *source, GrlSourceBrowseSpec *bs)
+{
+  gchar *vk_http_browse;
+  GrlSourceReqSpec *rs = g_slice_new (GrlSourceReqSpec);
+  rs->source = bs->source;
+  rs->operation_id = bs->operation_id;
+  rs->callback = bs->callback;
+  rs->user_data = bs->user_data;
+
+  vk_http_browse = g_strdup_printf (URL_VK_BROWSE,
+                                    GRL_VKONTAKTE_SOURCE (source)->priv->token);
+
+  grl_net_wc_request_async (GRL_VKONTAKTE_SOURCE (source)->priv->grlwc,
+                            vk_http_browse,
+                            NULL,
+                            vk_req_async_cb,
+                            rs);
+}
+
+/* ======================= Utilities ==================== */
+
+static inline const gchar *
+string_or(const gchar *first, const gchar *second)
+{
+    return first ? first : second;
+}
+
+#ifdef GOA_ENABLED
+static void
+grl_vkontakte_goa_update (GoaClient *client,
+                          GoaObject *object,
+                          gpointer   user_data)
+{
+  GoaAccount *acc = goa_object_peek_account (object);
+  const gchar *account_id = goa_account_get_id (acc);
+  gboolean enabled = !(goa_account_get_chat_disabled (acc)); /* TODO: make it music */
+  GrlVkGoaData *grl_data = (GrlVkGoaData *) user_data;
+
+  if (g_strcmp0 (goa_account_get_provider_type (acc), GOA_VK_NAME) != 0)
+    return;
+
+  GRL_DEBUG("GOA update. id: %s, status: %s", account_id, enabled?"enabled":"disabled");
+
+  /* Is plugin enabled while source is not registered? We should create the source. */
+  if (enabled && !g_hash_table_contains (grl_data->sources, account_id)) {
+    const gchar *vk_username = goa_account_get_presentation_identity (acc);
+    gchar *vk_token = NULL;
+    GError *error = NULL;
+    GrlVkontakteSource *source;
+    gint expires_in;
+
+    if (!goa_oauth2_based_call_get_access_token_sync (goa_object_peek_oauth2_based (object),
+                                                      &vk_token,
+                                                      &expires_in,
+                                                      NULL,
+                                                      &error)) {
+      GRL_WARNING ("Can't get VK token for %s: %s\n", vk_username, error->message);
+      g_error_free (error);
+      return;
+    }
+
+    source = grl_vkontakte_source_new (grl_data->plugin, vk_username, vk_token, account_id);
+
+    if (grl_registry_register_source (grl_data->registry,
+                                      grl_data->plugin,
+                                      GRL_SOURCE (source),
+                                      &error)) {
+      g_hash_table_insert (grl_data->sources, g_strdup (account_id), source);
+      GRL_DEBUG ("GOA update. registered view for id: %s", account_id);
+    } else {
+      GRL_ERROR ("Unable to register VK source for %s: %s\n", account_id, error->message);
+      g_error_free (error);
+      return;
+    }
+  }
+
+  /* Is plugin disabled while source is registered? We should remove the source. */
+  if (!enabled && g_hash_table_contains (grl_data->sources, account_id)) {
+    GError *error = NULL;
+    GrlVkontakteSource *source = (GrlVkontakteSource *)g_hash_table_lookup (grl_data->sources, account_id);
+
+    if (grl_registry_unregister_source (grl_data->registry,
+                                        GRL_SOURCE (source),
+                                        &error)) {
+      g_hash_table_remove (grl_data->sources, account_id);
+      GRL_DEBUG ("GOA update. removed view for id: %s", account_id);
+    } else {
+      GRL_ERROR ("Unable to unregister VK source for %s: %s\n", account_id, error->message);
+      g_error_free (error);
+      return;
+    }
+  }
+}
+
+static void
+grl_vkontakte_goa_remove (GoaClient *client,
+                          GoaObject *object,
+                          gpointer   user_data)
+{
+  GoaAccount *acc = goa_object_peek_account (object);
+  const gchar *account_id = goa_account_get_id (acc);
+  GError *error = NULL;
+  GrlVkontakteSource *source;
+  GrlVkGoaData *grl_data = (GrlVkGoaData *) user_data;
+
+  if (g_strcmp0 (goa_account_get_provider_type (acc), GOA_VK_NAME) != 0)
+    return;
+
+  GRL_DEBUG ("GOA remove. id: %s", account_id);
+  source = (GrlVkontakteSource *)g_hash_table_lookup (grl_data->sources, account_id);
+
+  if (!source)
+    return;
+
+  if (grl_registry_unregister_source (grl_data->registry,
+                                      GRL_SOURCE (source),
+                                      &error)) {
+    g_hash_table_remove (grl_data->sources, account_id);
+    GRL_DEBUG ("GOA update. removed view for id: %s", account_id);
+  } else {
+    GRL_ERROR ("Unable to unregister VK source for %s: %s\n", account_id, error->message);
+    g_error_free (error);
+    return;
+  }
+}
+
+static void
+grl_vkontakte_goa_init (GObject *source_object,
+                        GAsyncResult *res,
+                        gpointer user_data)
+{
+  GError *error = NULL;
+  GList *tmp;
+  GList *acc_list;
+  GList *vk_acc_list = NULL;
+  GrlVkGoaData *grl_data = (GrlVkGoaData *) user_data;
+
+  GoaClient *cl = goa_client_new_finish (res, &error);
+
+  if (error) {
+    GRL_MESSAGE ("Can't connect to GOA: %s\n", error->message);
+    g_error_free(error);
+    return;
+  }
+
+  acc_list = goa_client_get_accounts (cl);
+  for (tmp = acc_list; tmp != NULL; tmp = tmp->next) {
+    GoaAccount *acc = goa_object_peek_account (tmp->data);
+
+    if (g_strcmp0 (goa_account_get_provider_type (acc), GOA_VK_NAME) == 0)
+      vk_acc_list = g_list_append (vk_acc_list, tmp->data);
+    else
+      g_object_unref(acc);
+  }
+
+  g_list_free (acc_list);
+
+  grl_data->sources = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, NULL);
+
+  g_signal_connect (cl, "account-added", G_CALLBACK (grl_vkontakte_goa_update), user_data);
+  g_signal_connect (cl, "account-removed", G_CALLBACK (grl_vkontakte_goa_remove), user_data);
+  g_signal_connect (cl, "account-changed", G_CALLBACK (grl_vkontakte_goa_update), user_data);
+
+  for (tmp = vk_acc_list; tmp != NULL; tmp = tmp->next) {
+    /* the initial add of the accounts should be done manually, we can re-use the update function */
+    grl_vkontakte_goa_update (cl, (GoaObject *)tmp->data, user_data);
+    g_object_unref (tmp->data);
+  }
+  g_list_free (vk_acc_list);
+}
+#endif /* GOA_ENABLED */
+
+static GError *
+parse_vk_error (JsonNode *error_node)
+{
+  JsonObject *error;
+  gint err_code;
+  const gchar *err_msg;
+
+  JsonArray *request_params;
+  JsonObject *param;
+  JsonGenerator* json_gen;
+  gchar *debug_str;
+
+
+  if (!JSON_NODE_HOLDS_OBJECT(error_node))
+    return g_error_new (GRL_CORE_ERROR,
+                        GRL_CORE_ERROR_SEARCH_FAILED,
+                        "%s",
+                        "VK server sent an incorrect error JSON");
+
+  error = json_node_get_object (error_node);
+
+  /* {"error_code":####,"error_msg":"####","request_params":[{"key":"####","value":"####"}, ... ]} */
+
+  err_code = json_object_get_int_member (error, "error_code");
+  err_msg = json_object_get_string_member (error, "error_msg");
+  switch (err_code) {
+  case 2:  /* app is switched off */
+  case 5:  /* token access revoked */
+  case 7:  /* wrong app priveleges */
+  case 14: /* captcha required */
+  case 17: /* validation needed */
+    /* These errors indicate some problems with the OAuth2 token */
+    /* On receiving this error the client should notify the user */
+    return g_error_new (GRL_CORE_ERROR,
+                        GRL_CORE_ERROR_AUTHENTICATION_TOKEN,
+                        "%s",
+                        err_msg);
+  case 6:  /* too many requests per second */
+  case 9:  /* too many similar requests */
+  case 10: /* internal error */
+    /* these errors we can just happily ignore, they will go away eventually */
+    GRL_WARNING ("VK returned an error: code %d, %s", err_code, err_msg);
+    return NULL;
+  case 3:  /* wrong method */
+  case 8:  /* wrong request */
+  case 23: /* this method was disabled */
+  case 100: /* one or several parameters are wrong */
+  case 101: /* wrong API ID */
+    /* these errors indicate that we are royally screwed */
+    /* we should dump all the possible info from the error for debug */
+    error_node = json_node_copy (error_node);
+    error = json_node_get_object (error_node);
+    request_params = json_object_get_array_member (error, "request_params");
+
+    if (request_params) {
+      /* As tokens are private, we should remove it from the list of parameters before printing */
+      for (int i = 0; i < json_array_get_length (request_params); i++) {
+        param = json_array_get_object_element (request_params, i);
+        if (param != NULL && g_strcmp0 (json_object_get_string_member (param, "key"), "access_token") == 0) {
+            json_array_remove_element (request_params, i);
+            break; /* for */
+        }
+      }
+    }
+    json_gen = json_generator_new ();
+    json_generator_set_root (json_gen, error_node);
+    debug_str = json_generator_to_data (json_gen, NULL);
+
+    GRL_DEBUG ("Unhandled error from the VK server.\nError content:\n%s", debug_str);
+
+    g_free (debug_str);
+    g_object_unref (json_gen);
+    json_node_free (error_node);
+
+    return g_error_new (GRL_CORE_ERROR,
+                        GRL_CORE_ERROR_SEARCH_FAILED,
+                        "%s",
+                        err_msg);
+  }
+  /* All other errors do not require any special treatment */
+  return g_error_new (GRL_CORE_ERROR,
+                      GRL_CORE_ERROR_SEARCH_FAILED,
+                      "%s",
+                      err_msg);
+}
+
+static void
+vk_req_async_cb (GObject *source_object,
+                 GAsyncResult *res,
+                 gpointer user_data)
+{
+  gchar *content = NULL;
+  gsize reslen = -1;
+  GError *error = NULL;
+
+  JsonParser *parser = NULL;
+  JsonNode   *root;
+  JsonObject *json_obj;
+  JsonArray  *json_array;
+
+  GrlSourceReqSpec *rs = (GrlSourceReqSpec *) user_data;
+
+  if (!grl_net_wc_request_finish (GRL_NET_WC (source_object),
+                                  res,
+                                  &content,
+                                  &reslen,
+                                  &error)) {
+    GRL_DEBUG ("Couldn't retrieve data from VK server");
+    goto out;
+  }
+
+  parser = json_parser_new ();
+
+  if (!json_parser_load_from_data (parser,
+                                   content,
+                                   reslen,
+                                   &error)) {
+    GRL_DEBUG ("VK sent a non-JSON response:\n%s",content);
+    goto out;
+  }
+
+  /* {"response":{"count":####,"items":[{<media item>}, ... ]}} */
+  /* {"error":{<error>}} */
+
+
+  root = json_parser_get_root (parser);
+  if (!JSON_NODE_HOLDS_OBJECT(root)) {
+    error = g_error_new (GRL_CORE_ERROR,
+                         GRL_CORE_ERROR_SEARCH_FAILED,
+                         "%s",
+                         "VK server sent an incorrect JSON");
+    goto out;
+  }
+
+  json_obj = json_node_get_object (root);
+  if (json_object_has_member (json_obj, "error")) {
+    error = parse_vk_error(json_object_get_member(json_obj, "error"));
+    goto out;
+  }
+
+  json_obj = json_object_get_object_member (json_obj, "response");
+  if (!json_obj) {
+    error = g_error_new (GRL_CORE_ERROR,
+                         GRL_CORE_ERROR_SEARCH_FAILED,
+                         "%s",
+                         "VK server sent an incorrect JSON");
+    goto out;
+  }
+
+  if (json_object_get_int_member (json_obj, "count") == 0) {
+    /* No results */
+    goto out;
+  }
+
+  json_array = json_object_get_array_member (json_obj, "items");
+  if (!json_array) {
+    error = g_error_new (GRL_CORE_ERROR,
+                         GRL_CORE_ERROR_SEARCH_FAILED,
+                         "%s",
+                         "VK server sent an incorrect JSON");
+    goto out;
+  }
+
+  json_array_foreach_element (json_array,
+                              vk_parse_media_cb,
+                              user_data);
+
+out:
+  /* Finish the search operation */
+  rs->callback (rs->source, rs->operation_id,
+                NULL, 0, rs->user_data, error);
+
+  g_slice_free (GrlSourceReqSpec, rs);
+  g_object_unref (parser);
+}
+
+static void
+vk_parse_media_cb (JsonArray *array,
+                   guint index_,
+                   JsonNode *element_node,
+                   gpointer user_data)
+{
+  JsonObject *json_obj;
+  GrlSourceReqSpec *rs = (GrlSourceReqSpec *) user_data;
+  GrlMedia *media;
+  const gchar *url;
+  gchar *id;
+
+  if (!JSON_NODE_HOLDS_OBJECT(element_node)) {
+    GRL_WARNING ("Unhandled media JSON in VK responce");
+    GRL_DEBUG ("Got %s instead of JsonObject", json_node_type_name (element_node));
+    return;
+  }
+
+  /* {"id":####,"owner_id":####,"artist":"####","title":"####","duration":####,"url":"####","lyrics_id":####,"genre_id":####} */
+  json_obj = json_node_get_object (element_node);
+  url = json_object_get_string_member (json_obj, "url");
+  if (!url) {
+    GRL_WARNING ("media in VK response doesn't contain url");
+    return;
+  }
+  media = grl_media_audio_new ();
+  id = g_strdup_printf("audio_%lx", (guint64) json_object_get_int_member (json_obj, "id"));
+  grl_media_set_id (media, id);
+  grl_media_audio_set_artist (GRL_MEDIA_AUDIO(media), string_or (json_object_get_string_member (json_obj, "artist"), _("Unknown Artist")));
+  grl_media_set_title (media, string_or (json_object_get_string_member (json_obj, "title"), _("Unknown Title")));
+  grl_media_set_duration (media, json_object_get_int_member (json_obj, "duration"));
+  grl_media_set_url (media, url);
+
+  g_free (id);
+
+  rs->callback (rs->source,
+                rs->operation_id,
+                media,
+                GRL_SOURCE_REMAINING_UNKNOWN,
+                rs->user_data,
+                NULL);
+}
diff --git a/src/vkontakte/grl-vkontakte.h b/src/vkontakte/grl-vkontakte.h
new file mode 100644
index 0000000..955f2d4
--- /dev/null
+++ b/src/vkontakte/grl-vkontakte.h
@@ -0,0 +1,73 @@
+/*
+ * Copyright (C) 2015 George Sedov.
+ *
+ * Contact: George Sedov <radist.morse@gmail.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_VKONTAKTE_H_
+#define _GRL_VKONTAKTE_H_
+
+#include <grilo.h>
+
+#define GRL_VKONTAKTE_SOURCE_TYPE                        \
+  (grl_vkontakte_source_get_type())
+
+#define GRL_VKONTAKTE_SOURCE(obj)                        \
+  (G_TYPE_CHECK_INSTANCE_CAST((obj),                     \
+                              GRL_VKONTAKTE_SOURCE_TYPE, \
+                              GrlVkontakteSource))
+
+#define GRL_VKONTAKTE_SOURCE_CLASS(klass)                \
+  (G_TYPE_CHECK_CLASS_CAST((klass),                      \
+                           GRL_VKONTAKTE_SOURCE_TYPE,    \
+                           GrlVkontakteSourceClass))
+
+#define GRL_IS_VKONTAKTE_SOURCE(obj)                     \
+  (G_TYPE_CHECK_INSTANCE_TYPE ((obj),                    \
+                           GRL_VKONTAKTE_SOURCE_TYPE))
+
+#define GRL_IS_VKONTAKTE_SOURCE_CLASS(klass)             \
+  (G_TYPE_CHECK_CLASS_TYPE((klass),                      \
+                           GRL_VKONTAKTE_SOURCE_TYPE))
+
+#define GRL_VKONTAKTE_SOURCE_GET_CLASS(obj)              \
+  (G_TYPE_INSTANCE_GET_CLASS((obj),                      \
+                              GRL_VKONTAKTE_SOURCE_TYPE, \
+                              GrlVkontakteSourceClass))
+
+/* plugin's log domain */
+GRL_LOG_DOMAIN_EXTERN(vkontakte_log_domain);
+
+typedef struct _GrlVkontakteSource        GrlVkontakteSource;
+typedef struct _GrlVkontakteSourcePrivate GrlVkontakteSourcePrivate;
+
+struct _GrlVkontakteSource {
+  GrlSource parent;
+  GrlVkontakteSourcePrivate *priv;
+};
+
+typedef struct _GrlVkontakteSourceClass GrlVkontakteSourceClass;
+
+struct _GrlVkontakteSourceClass {
+  GrlSourceClass parent_class;
+};
+
+GType grl_vkontakte_source_get_type(void);
+
+#endif /* _GRL_VKONTAKTE_H_ */
diff --git a/src/vkontakte/grl-vkontakte.xml b/src/vkontakte/grl-vkontakte.xml
new file mode 100644
index 0000000..1d075b3
--- /dev/null
+++ b/src/vkontakte/grl-vkontakte.xml
@@ -0,0 +1,10 @@
+<plugin>
+  <info>
+    <name>Vkontakte</name>
+    <module>libgrlvkontakte</module>
+    <description>A plugin for searching music</description>
+    <author>George Sedov</author>
+    <license>LGPL</license>
+    <site>https://wiki.gnome.org/Projects/Grilo</site>
+  </info>
+</plugin>
diff --git a/tests/Makefile.am b/tests/Makefile.am
index 8230b5d..bd7348e 100644
--- a/tests/Makefile.am
+++ b/tests/Makefile.am
@@ -41,6 +41,10 @@ if VIMEO_PLUGIN
 SUBDIRS += vimeo
 endif

+if VKONTAKTE_PLUGIN
+SUBDIRS += vkontakte
+endif
+
 DIST_SUBDIRS =    \
    apple-trailers \
    bliptv         \
@@ -49,7 +53,8 @@ DIST_SUBDIRS =    \
    local-metadata \
    tmdb           \
    thetvdb        \
-   vimeo
+   vimeo          \
+   vkontakte

 MAINTAINERCLEANFILES =  \
    *.in                 \
diff --git a/tests/vkontakte/Makefile.am b/tests/vkontakte/Makefile.am
new file mode 100644
index 0000000..6238884
--- /dev/null
+++ b/tests/vkontakte/Makefile.am
@@ -0,0 +1,37 @@
+#
+# Makefile.am
+#
+
+include $(top_srcdir)/gtester.mk
+
+TEST_PROGS +=	\
+   test_vkontakte
+
+test_vkontakte_defines =                                              \
+   -DVK_PLUGIN_PATH=\""$(abs_top_builddir)/src/vkontakte/.libs/"\" \
+   -DVK_DATA_PATH=\""$(abs_top_srcdir)/tests/vkontakte/data/"\"
+
+test_vkontakte_SOURCES =	\
+	test_vk.c
+
+test_vkontakte_LDADD =	\
+	@DEPS_LIBS@
+
+test_vkontakte_CFLAGS =	\
+	@DEPS_CFLAGS@		\
+	$(test_vkontakte_defines)
+
+# Distribute the tests data:
+dist_noinst_DATA =           \
+   data/network-data.ini     \
+   data/testsearch.data      \
+   data/testemptysearch.data \
+   data/tokenerror.data
+
+noinst_PROGRAMS = $(TEST_PROGS)
+
+MAINTAINERCLEANFILES =  \
+   *.in                 \
+   *~
+
+DISTCLEANFILES = $(MAINTAINERCLEANFILES)
diff --git a/tests/vkontakte/data/network-data.ini b/tests/vkontakte/data/network-data.ini
new file mode 100644
index 0000000..154d210
--- /dev/null
+++ b/tests/vkontakte/data/network-data.ini
@@ -0,0 +1,15 @@
+[default]
+version=1
+ignored-parameters=v;count;offset;
+
+[https://api.vk.com/method/audio.search?format=json&q=testquery&access_token=TEST_VK_TOKEN]
+data=testsearch.data
+
+[https://api.vk.com/method/audio.get?format=json&access_token=TEST_VK_TOKEN]
+data=testsearch.data
+
+[https://api.vk.com/method/audio.search?format=json&q=testemptyquery&access_token=TEST_VK_TOKEN]
+data=testemptysearch.data
+
+[https://api.vk.com/method/audio.search?format=json&q=testerrorquery&access_token=TEST_VK_TOKEN]
+data=tokenerror.data
diff --git a/tests/vkontakte/data/testemptysearch.data b/tests/vkontakte/data/testemptysearch.data
new file mode 100644
index 0000000..20fb2bf
--- /dev/null
+++ b/tests/vkontakte/data/testemptysearch.data
@@ -0,0 +1 @@
+{"response":{"count":0,"items":[]}}
diff --git a/tests/vkontakte/data/testsearch.data b/tests/vkontakte/data/testsearch.data
new file mode 100644
index 0000000..7c18427
--- /dev/null
+++ b/tests/vkontakte/data/testsearch.data
@@ -0,0 +1 @@
+{"response":{"count":123456,"items":[{"id":123456,"owner_id":123456,"artist":"Foo","title":"Bar","duration":123,"url":"https:\/\/psv4.vk.me\/c4679\/u2055938\/audios\/1234567.mp3?extra=12345678","lyrics_id":123456,"genre_id":18},{"id":1234567,"owner_id":123456,"artist":"FooFoo","title":"BarBar","duration":123,"url":"https:\/\/psv4.vk.me\/c536113\/u230033305\/audios\/123456.mp3?extra=123456","lyrics_id":123456,"album_id":123456,"genre_id":8}]}}
diff --git a/tests/vkontakte/data/tokenerror.data b/tests/vkontakte/data/tokenerror.data
new file mode 100644
index 0000000..e972a09
--- /dev/null
+++ b/tests/vkontakte/data/tokenerror.data
@@ -0,0 +1 @@
+{"error":{"error_code":5,"error_msg":"User authorization failed: user revoke access for this token.","request_params":[{"key":"oauth","value":"1"},{"key":"method","value":"audio.get"},{"key":"v","value":"5.28"},{"key":"format","value":"json"},{"key":"access_token","value":"my-very-secret-token"}]}}
diff --git a/tests/vkontakte/test_vk.c b/tests/vkontakte/test_vk.c
new file mode 100644
index 0000000..70f7203
--- /dev/null
+++ b/tests/vkontakte/test_vk.c
@@ -0,0 +1,259 @@
+/*
+ *
+ * Author: George Sedov <radist.morse@gmail.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 <grilo.h>
+
+#define VK_ID "grl-vkontakte"
+#define VK_TOKEN      "TEST_VK_TOKEN"
+
+#define SEARCH_LINE "testquery"
+#define EMPTY_SEARCH_LINE "testemptyquery"
+#define ERROR_SEARCH_LINE "testerrorquery"
+
+static void
+test_setup (void)
+{
+  GError *error = NULL;
+  GrlConfig *config;
+  GrlRegistry *registry;
+
+  registry = grl_registry_get_default ();
+
+  config = grl_config_new (VK_ID, NULL);
+  grl_config_set_api_token (config, VK_TOKEN);
+  grl_config_set_username (config, "VK Test");
+  grl_registry_add_config (registry, config, NULL);
+
+  grl_registry_load_all_plugins (registry, &error);
+  g_assert_no_error (error);
+}
+
+static void
+test_search_normal (void)
+{
+  GError *error = NULL;
+  GList *medias;
+  GrlMedia *media;
+  GrlOperationOptions *options;
+  GrlRegistry *registry;
+  GrlSource *source;
+
+  registry = grl_registry_get_default ();
+  source = grl_registry_lookup_source (registry, VK_ID);
+  g_assert (source);
+  options = grl_operation_options_new (NULL);
+  grl_operation_options_set_count (options, 2);
+  grl_operation_options_set_flags (options, GRL_RESOLVE_FAST_ONLY);
+  g_assert (options);
+
+  medias = grl_source_search_sync (source,
+                                   SEARCH_LINE,
+                                   grl_source_supported_keys (source),
+                                   options,
+                                   &error);
+
+  g_assert_cmpint (g_list_length(medias), ==, 2);
+  g_assert_no_error (error);
+
+  media = g_list_nth_data (medias, 0);
+
+  g_assert (GRL_IS_MEDIA_AUDIO (media));
+  g_assert_cmpstr (grl_media_get_id (media),
+                   ==,
+                   "audio_1e240");
+  g_assert_cmpstr (grl_media_audio_get_artist (GRL_MEDIA_AUDIO (media)),
+                   ==,
+                   "Foo");
+  g_assert_cmpstr (grl_media_get_title (media),
+                   ==,
+                   "Bar");
+  g_assert_cmpint (grl_media_get_duration (media),
+                   ==,
+                   123);
+
+  media = g_list_nth_data (medias, 1);
+
+  g_assert (GRL_IS_MEDIA_AUDIO (media));
+  g_assert_cmpstr (grl_media_get_id (media),
+                   ==,
+                   "audio_12d687");
+  g_assert_cmpstr (grl_media_audio_get_artist (GRL_MEDIA_AUDIO (media)),
+                   ==,
+                   "FooFoo");
+  g_assert_cmpstr (grl_media_get_title (media),
+                   ==,
+                   "BarBar");
+  g_assert_cmpint (grl_media_get_duration (media),
+                   ==,
+                   123);
+
+
+  g_list_free_full (medias, g_object_unref);
+  g_object_unref (options);
+}
+
+static void
+test_search_empty (void)
+{
+  GError *error = NULL;
+  GList *medias;
+  GrlOperationOptions *options;
+  GrlRegistry *registry;
+  GrlSource *source;
+
+  registry = grl_registry_get_default ();
+  source = grl_registry_lookup_source (registry, VK_ID);
+  g_assert (source);
+  options = grl_operation_options_new (NULL);
+  grl_operation_options_set_count (options, 2);
+  grl_operation_options_set_flags (options, GRL_RESOLVE_FAST_ONLY);
+  g_assert (options);
+
+  medias = grl_source_search_sync (source,
+                                   EMPTY_SEARCH_LINE,
+                                   grl_source_supported_keys (source),
+                                   options,
+                                   &error);
+
+  g_assert_cmpint (g_list_length(medias), ==, 0);
+  g_assert_no_error (error);
+
+  g_object_unref (options);
+}
+
+static void
+test_browse (void)
+{
+  GError *error = NULL;
+  GList *medias;
+  GrlMedia *media;
+  GrlOperationOptions *options;
+  GrlRegistry *registry;
+  GrlSource *source;
+
+  registry = grl_registry_get_default ();
+  source = grl_registry_lookup_source (registry, VK_ID);
+  g_assert (source);
+  options = grl_operation_options_new (NULL);
+  grl_operation_options_set_count (options, 2);
+  grl_operation_options_set_flags (options, GRL_RESOLVE_FAST_ONLY);
+  g_assert (options);
+
+  medias = grl_source_browse_sync (source,
+                                   NULL,
+                                   grl_source_supported_keys (source),
+                                   options,
+                                   &error);
+
+  g_assert_cmpint (g_list_length(medias), ==, 2);
+  g_assert_no_error (error);
+
+  media = g_list_nth_data (medias, 0);
+
+  g_assert (GRL_IS_MEDIA_AUDIO (media));
+  g_assert_cmpstr (grl_media_get_id (media),
+                   ==,
+                   "audio_1e240");
+  g_assert_cmpstr (grl_media_audio_get_artist (GRL_MEDIA_AUDIO (media)),
+                   ==,
+                   "Foo");
+  g_assert_cmpstr (grl_media_get_title (media),
+                   ==,
+                   "Bar");
+  g_assert_cmpint (grl_media_get_duration (media),
+                   ==,
+                   123);
+
+  media = g_list_nth_data (medias, 1);
+
+  g_assert (GRL_IS_MEDIA_AUDIO (media));
+  g_assert_cmpstr (grl_media_get_id (media),
+                   ==,
+                   "audio_12d687");
+  g_assert_cmpstr (grl_media_audio_get_artist (GRL_MEDIA_AUDIO (media)),
+                   ==,
+                   "FooFoo");
+  g_assert_cmpstr (grl_media_get_title (media),
+                   ==,
+                   "BarBar");
+  g_assert_cmpint (grl_media_get_duration (media),
+                   ==,
+                   123);
+
+
+  g_list_free_full (medias, g_object_unref);
+  g_object_unref (options);
+}
+
+static void
+test_error (void)
+{
+  GError *error = NULL;
+  GList *medias;
+  GrlOperationOptions *options;
+  GrlRegistry *registry;
+  GrlSource *source;
+
+  registry = grl_registry_get_default ();
+  source = grl_registry_lookup_source (registry, VK_ID);
+  g_assert (source);
+  options = grl_operation_options_new (NULL);
+  grl_operation_options_set_count (options, 2);
+  grl_operation_options_set_flags (options, GRL_RESOLVE_FAST_ONLY);
+  g_assert (options);
+
+  medias = grl_source_search_sync (source,
+                                   ERROR_SEARCH_LINE,
+                                   grl_source_supported_keys (source),
+                                   options,
+                                   &error);
+
+  g_assert_cmpint (g_list_length(medias), ==, 0);
+  g_assert_error  (error,
+                   GRL_CORE_ERROR,
+                   GRL_CORE_ERROR_SEARCH_FAILED);
+
+  g_object_unref (options);
+}
+
+int
+main (int argc, char **argv)
+{
+  g_setenv ("GRL_PLUGIN_PATH", VK_PLUGIN_PATH, TRUE);
+  g_setenv ("GRL_PLUGIN_LIST", VK_ID, TRUE);
+  g_setenv ("GRL_NET_MOCKED", VK_DATA_PATH "network-data.ini", TRUE);
+
+  grl_init (&argc, &argv);
+  g_test_init (&argc, &argv, NULL);
+
+  test_setup ();
+
+  g_test_add_func ("/vkontakte/search/normal", test_search_normal);
+  g_test_add_func ("/vkontakte/search/empty",  test_search_empty);
+  g_test_add_func ("/vkontakte/browse",        test_browse);
+  g_test_add_func ("/vkontakte/error",         test_error);
+
+  gint result = g_test_run ();
+
+  grl_deinit ();
+
+  return result;
+}