[grilo-plugins] dleyna: Plugin to access DLNA contents using the dLeyna DBus service



commit 4990fa374259aa15c9511ad210a6bf07de5bdcc4
Author: Emanuele Aina <emanuele aina collabora com>
Date:   Mon Oct 14 18:28:22 2013 +0200

    dleyna: Plugin to access DLNA contents using the dLeyna DBus service
    
    This plugin uses the dleyna-server service to interact with DMS (DLNA
    Media Server, eg. Rygel) through an high level DBus API and it is meant
    to deprecate the UPnP plugin.
    
    It implements all the functionalities exposed by GrlSource: resolve(),
    browse(), search(), query(), store(), store_metadata(), remove(),
    cancel() and change notifications.
    
    This adds a soft-dependency on the deyna-renderer DBus service: if the
    service is not activatable no DLNA server sources will be shown.
    
    The required GLib version has been bumped to 2.36 to use GTask.
    
    https://bugzilla.gnome.org/show_bug.cgi?id=707346
    
    Signed-off-by: Emanuele Aina <emanuele aina collabora com>

 configure.ac                                       |   63 +-
 m4/ax_python_module.m4                             |   49 +
 po/POTFILES.in                                     |    1 +
 src/Makefile.am                                    |    6 +-
 src/dleyna/.gitignore                              |    3 +
 src/dleyna/Makefile.am                             |   86 +
 src/dleyna/com.intel.dLeynaServer.Manager.xml      |   34 +
 src/dleyna/com.intel.dLeynaServer.MediaDevice.xml  |  132 ++
 src/dleyna/grl-dleyna-server.c                     |  424 +++++
 src/dleyna/grl-dleyna-server.h                     |   90 ++
 src/dleyna/grl-dleyna-servers-manager.c            |  253 +++
 src/dleyna/grl-dleyna-servers-manager.h            |   70 +
 src/dleyna/grl-dleyna-source.c                     | 1682 ++++++++++++++++++++
 src/dleyna/grl-dleyna-source.h                     |   75 +
 src/dleyna/grl-dleyna.c                            |  137 ++
 src/dleyna/grl-dleyna.xml                          |   10 +
 src/dleyna/org.gnome.UPnP.MediaServer2.xml         |  200 +++
 src/local-metadata/grl-local-metadata.c            |    2 +
 tests/.gitignore                                   |    4 +
 tests/Makefile.am                                  |    5 +
 tests/dleyna/Makefile.am                           |   64 +
 tests/dleyna/data/helloworld.txt                   |    1 +
 tests/dleyna/dbusmock/dleyna-server-mock           |   10 +
 .../dleyna/dbusmock/dleyna-server-mock.service.in  |    3 +
 tests/dleyna/dbusmock/dleynamanager.py             |   76 +
 tests/dleyna/dbusmock/dleynamediacontainer.py      |  118 ++
 tests/dleyna/dbusmock/dleynamediadevice.py         |  156 ++
 tests/dleyna/dbusmock/dleynamediaobject.py         |   57 +
 tests/dleyna/dbusmock/items.py                     |  125 ++
 tests/dleyna/test_dleyna.c                         |  467 ++++++
 tests/dleyna/test_dleyna_utils.c                   |  139 ++
 tests/dleyna/test_dleyna_utils.h                   |   58 +
 32 files changed, 4598 insertions(+), 2 deletions(-)
---
diff --git a/configure.ac b/configure.ac
index 78a1791..b5ef16b 100644
--- a/configure.ac
+++ b/configure.ac
@@ -87,7 +87,7 @@ fi
 # HARD DEPENDENCIES
 # ----------------------------------------------------------
 
-PKG_CHECK_MODULES([DEPS], glib-2.0 >= 2.34      \
+PKG_CHECK_MODULES([DEPS], glib-2.0 >= 2.36      \
                          gobject-2.0   \
                          gmodule-2.0   \
                          ${GRL_NAME} >= 0.2.10)
@@ -120,6 +120,8 @@ PKG_CHECK_MODULES(XML, libxml-2.0, HAVE_XML=yes, HAVE_XML=no)
 
 PKG_CHECK_MODULES(GIO, gio-2.0, HAVE_GIO=yes, HAVE_GIO=no)
 
+PKG_CHECK_MODULES(GIO_UNIX, gio-unix-2.0, HAVE_GIO_UNIX=yes, HAVE_GIO_UNIX=no)
+
 PKG_CHECK_MODULES(GSSDP, gssdp-1.0, HAVE_GSSDP=yes, HAVE_GSSDP=no)
 
 PKG_CHECK_MODULES(GUPNP, gupnp-1.0 >= 0.13, HAVE_GUPNP=yes, HAVE_GUPNP=no)
@@ -179,6 +181,8 @@ PKG_CHECK_MODULES([TRACKER_SPARQL], [ ${trackerpkg} ],
                                     HAVE_TRACKER_SPARQL=yes,
                                     HAVE_TRACKER_SPARQL=no)
 
+GDBUS_CODEGEN=`${PKG_CONFIG} --variable=gdbus_codegen gio-2.0`
+AC_SUBST(GDBUS_CODEGEN)
 
 # ----------------------------------------------------------
 # CHECK GOA
@@ -1173,6 +1177,60 @@ DEPS_LOCALMETADATA_LIBS="$DEPS_LIBS $GIO_LIBS $MEDIAART_LIBS"
 AC_SUBST(DEPS_LOCALMETADATA_LIBS)
 
 # ----------------------------------------------------------
+# BUILD DLEYNA PLUGIN
+# ----------------------------------------------------------
+
+AC_ARG_ENABLE(dleyna,
+        AC_HELP_STRING([--enable-dleyna],
+                [enable DLNA plugin using the dLeyna DBus services (default: yes)]),
+                [
+                  case "$enableval" in
+                     yes)
+                        if test "x$HAVE_GIO" = "xno"; then
+                           AC_MSG_ERROR([GIO not found, install it or use --disable-dleyna])
+                        fi
+                        if test "x$HAVE_GIO_UNIX" = "xno"; then
+                           AC_MSG_ERROR([GIO Unix Extensions not found, install it or use --disable-dleyna])
+                        fi
+                        ;;
+                  esac
+
+                ],
+                [
+                  if test "x$HAVE_GIO" = "xyes" -a "x$HAVE_GIO_UNIX" = "xyes"; then
+                     enable_dleyna=yes
+                  else
+                     enable_dleyna=no
+                  fi
+                ])
+
+AM_CONDITIONAL([DLEYNA_PLUGIN], [test "x$enable_dleyna" = "xyes"])
+GRL_PLUGINS_ALL="$GRL_PLUGINS_ALL dleyna"
+if test "x$enable_dleyna" = "xyes"
+then
+       GRL_PLUGINS_ENABLED="$GRL_PLUGINS_ENABLED dleyna"
+
+        AX_PYTHON_MODULE([dbusmock])
+        AS_IF([test "$HAVE_PYMOD_DBUSMOCK" = "yes"],,
+              AC_MSG_WARN([[
+***
+*** No python-dbusmock module detected, tests for the dLeyna plugin will be disabled.
+***]]))
+fi
+
+AM_CONDITIONAL([DLEYNA_TESTS_RUN_DBUSMOCK],[test "$HAVE_PYMOD_DBUSMOCK" = "yes"])
+
+DLEYNA_PLUGIN_ID="grl-dleyna"
+AC_SUBST(DLEYNA_PLUGIN_ID)
+AC_DEFINE_UNQUOTED([DLEYNA_PLUGIN_ID], ["$DLEYNA_PLUGIN_ID"], [dLeyna DLNA plugin ID])
+
+DEPS_DLEYNA_CFLAGS="$DEPS_CFLAGS $GIO_UNIX_CFLAGS"
+AC_SUBST(DEPS_DLEYNA_CFLAGS)
+DEPS_DLEYNA_LIBS="$DEPS_LIBS $GIO_UNIX_LIBS"
+AC_SUBST(DEPS_DLEYNA_LIBS)
+
+
+# ----------------------------------------------------------
 # BUILD DMAP PLUGIN
 # ----------------------------------------------------------
 
@@ -1329,6 +1387,7 @@ AC_CONFIG_FILES([
   src/apple-trailers/Makefile
   src/bliptv/Makefile
   src/bookmarks/Makefile
+  src/dleyna/Makefile
   src/dmap/Makefile
   src/filesystem/Makefile
   src/flickr/Makefile
@@ -1354,6 +1413,8 @@ AC_CONFIG_FILES([
   tests/Makefile
   tests/apple-trailers/Makefile
   tests/bliptv/Makefile
+  tests/dleyna/Makefile
+  tests/dleyna/dbusmock/dleyna-server-mock.service
   tests/local-metadata/Makefile
   tests/tmdb/Makefile
   tests/vimeo/Makefile
diff --git a/m4/ax_python_module.m4 b/m4/ax_python_module.m4
new file mode 100644
index 0000000..3afc404
--- /dev/null
+++ b/m4/ax_python_module.m4
@@ -0,0 +1,49 @@
+# ===========================================================================
+#     http://www.gnu.org/software/autoconf-archive/ax_python_module.html
+# ===========================================================================
+#
+# SYNOPSIS
+#
+#   AX_PYTHON_MODULE(modname[, fatal])
+#
+# DESCRIPTION
+#
+#   Checks for Python module.
+#
+#   If fatal is non-empty then absence of a module will trigger an error.
+#
+# LICENSE
+#
+#   Copyright (c) 2008 Andrew Collier
+#
+#   Copying and distribution of this file, with or without modification, are
+#   permitted in any medium without royalty provided the copyright notice
+#   and this notice are preserved. This file is offered as-is, without any
+#   warranty.
+
+#serial 6
+
+AU_ALIAS([AC_PYTHON_MODULE], [AX_PYTHON_MODULE])
+AC_DEFUN([AX_PYTHON_MODULE],[
+    if test -z $PYTHON;
+    then
+        PYTHON="python"
+    fi
+    PYTHON_NAME=`basename $PYTHON`
+    AC_MSG_CHECKING($PYTHON_NAME module: $1)
+       $PYTHON -c "import $1" 2>/dev/null
+       if test $? -eq 0;
+       then
+               AC_MSG_RESULT(yes)
+               eval AS_TR_CPP(HAVE_PYMOD_$1)=yes
+       else
+               AC_MSG_RESULT(no)
+               eval AS_TR_CPP(HAVE_PYMOD_$1)=no
+               #
+               if test -n "$2"
+               then
+                       AC_MSG_ERROR(failed to find required module $1)
+                       exit 1
+               fi
+       fi
+])
diff --git a/po/POTFILES.in b/po/POTFILES.in
index 63b8d67..cc47f0f 100644
--- a/po/POTFILES.in
+++ b/po/POTFILES.in
@@ -1,6 +1,7 @@
 src/apple-trailers/grl-apple-trailers.c
 src/bliptv/grl-bliptv.c
 src/bookmarks/grl-bookmarks.c
+src/dleyna/grl-dleyna-source.c
 src/dmap/grl-dmap.c
 src/dmap/simple-dmap-db.c
 src/filesystem/grl-filesystem.c
diff --git a/src/Makefile.am b/src/Makefile.am
index dbaa29b..7b4f309 100644
--- a/src/Makefile.am
+++ b/src/Makefile.am
@@ -22,6 +22,10 @@ if BOOKMARKS_PLUGIN
 SUBDIRS += bookmarks
 endif
 
+if DLEYNA_PLUGIN
+SUBDIRS += dleyna
+endif
+
 if DMAP_PLUGIN
 SUBDIRS += dmap
 endif
@@ -107,7 +111,7 @@ SUBDIRS += freebox
 endif
 
 DIST_SUBDIRS = \
-   apple-trailers bliptv bookmarks dmap filesystem flickr freebox gravatar jamendo \
+   apple-trailers bliptv bookmarks dleyna dmap filesystem flickr freebox gravatar jamendo \
    lastfm-albumart local-metadata lua-factory magnatune metadata-store optical-media   \
    pocket podcasts raitv shoutcast tmdb tracker upnp vimeo youtube
 
diff --git a/src/dleyna/.gitignore b/src/dleyna/.gitignore
new file mode 100644
index 0000000..499de87
--- /dev/null
+++ b/src/dleyna/.gitignore
@@ -0,0 +1,3 @@
+/grl-dleyna-proxy-manager.[ch]
+/grl-dleyna-proxy-mediadevice.[ch]
+/grl-dleyna-proxy-mediaserver2.[ch]
diff --git a/src/dleyna/Makefile.am b/src/dleyna/Makefile.am
new file mode 100644
index 0000000..d099123
--- /dev/null
+++ b/src/dleyna/Makefile.am
@@ -0,0 +1,86 @@
+#
+# Makefile.am
+#
+# Copyright (C) 2010, 2011 Igalia S.L. All rights reserved.
+# Copyright (C) 2013 Intel Corp. All rights reserved.
+
+ext_LTLIBRARIES        = libgrldleyna.la
+
+libgrldleyna_la_CFLAGS =            \
+       $(DEPS_DLEYNA_CFLAGS)        \
+       -DG_LOG_DOMAIN=\"GrlDleyna\" \
+       -DLOCALEDIR=\"$(localedir)\"
+
+libgrldleyna_la_LIBADD =    \
+       $(DEPS_DLEYNA_LIBS)
+
+libgrldleyna_la_LDFLAGS = \
+       -no-undefined     \
+       -module           \
+       -avoid-version
+
+dbus_built_sources = \
+       grl-dleyna-proxy-mediaserver2.h \
+       grl-dleyna-proxy-mediaserver2.c \
+       grl-dleyna-proxy-mediadevice.h \
+       grl-dleyna-proxy-mediadevice.c \
+       grl-dleyna-proxy-manager.h \
+       grl-dleyna-proxy-manager.c \
+       $(NULL)
+
+BUILT_SOURCES = $(dbus_built_sources)
+
+libgrldleyna_la_SOURCES =            \
+       $(dbus_built_sources)        \
+       grl-dleyna.c                 \
+       grl-dleyna-source.c          \
+       grl-dleyna-source.h          \
+       grl-dleyna-server.c          \
+       grl-dleyna-server.h          \
+       grl-dleyna-servers-manager.c \
+       grl-dleyna-servers-manager.h \
+       $(NULL)
+
+extdir         = $(GRL_PLUGINS_DIR)
+dleynaxmldir   = $(GRL_PLUGINS_DIR)
+dleynaxml_DATA = $(DLEYNA_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: libgrldleyna.la
+       $(AM_V_at)cp -f $(srcdir)/$(dleynaxml_DATA) $(builddir)/.libs/
+
+all-local: copy-xml-to-libs-dir
+
+grl-dleyna-proxy-mediaserver2.h grl-dleyna-proxy-mediaserver2.c: org.gnome.UPnP.MediaServer2.xml
+       $(AM_V_GEN)${GDBUS_CODEGEN} \
+               --c-namespace GrlDleyna \
+               --generate-c-code grl-dleyna-proxy-mediaserver2 \
+               --interface-prefix org.gnome.UPnP. \
+               $^
+
+grl-dleyna-proxy-manager.h grl-dleyna-proxy-manager.c: com.intel.dLeynaServer.Manager.xml
+       $(AM_V_GEN)${GDBUS_CODEGEN} \
+               --c-namespace GrlDleyna \
+               --generate-c-code grl-dleyna-proxy-manager \
+               --interface-prefix com.intel.dLeynaServer. \
+               $^
+
+grl-dleyna-proxy-mediadevice.h grl-dleyna-proxy-mediadevice.c: com.intel.dLeynaServer.MediaDevice.xml
+       $(AM_V_GEN)${GDBUS_CODEGEN} \
+               --c-namespace GrlDleyna \
+               --generate-c-code grl-dleyna-proxy-mediadevice \
+               --interface-prefix com.intel.dLeynaServer. \
+               $^
+
+EXTRA_DIST =                                   \
+       com.intel.dLeynaServer.Manager.xml     \
+       com.intel.dLeynaServer.MediaDevice.xml \
+       org.gnome.UPnP.MediaServer2.xml        \
+       $(dleynaxml_DATA)
+
+MAINTAINERCLEANFILES = \
+       *.in            \
+       *~
+
+DISTCLEANFILES = $(MAINTAINERCLEANFILES)
diff --git a/src/dleyna/com.intel.dLeynaServer.Manager.xml b/src/dleyna/com.intel.dLeynaServer.Manager.xml
new file mode 100644
index 0000000..7e8810f
--- /dev/null
+++ b/src/dleyna/com.intel.dLeynaServer.Manager.xml
@@ -0,0 +1,34 @@
+<!DOCTYPE node PUBLIC "-//freedesktop//DTD D-BUS Object Introspection 1.0//EN"
+                      "http://www.freedesktop.org/standards/dbus/1.0/introspect.dtd";>
+<node>
+  <interface name="com.intel.dLeynaServer.Manager">
+    <method name="GetVersion">
+      <arg type="s" name="Version" direction="out">
+      </arg>
+    </method>
+    <method name="Release">
+    </method>
+    <method name="GetServers">
+      <arg type="ao" name="Servers" direction="out">
+      </arg>
+    </method>
+    <method name="Rescan">
+    </method>
+    <method name="SetProtocolInfo">
+      <arg type="s" name="ProtocolInfo" direction="in">
+      </arg>
+    </method>
+    <method name="PreferLocalAddresses">
+      <arg type="b" name="Prefer" direction="in">
+      </arg>
+    </method>
+    <signal name="FoundServer">
+      <arg type="o" name="Path">
+      </arg>
+    </signal>
+    <signal name="LostServer">
+      <arg type="o" name="Path">
+      </arg>
+    </signal>
+  </interface>
+</node>
diff --git a/src/dleyna/com.intel.dLeynaServer.MediaDevice.xml 
b/src/dleyna/com.intel.dLeynaServer.MediaDevice.xml
new file mode 100644
index 0000000..49db20c
--- /dev/null
+++ b/src/dleyna/com.intel.dLeynaServer.MediaDevice.xml
@@ -0,0 +1,132 @@
+<!DOCTYPE node PUBLIC "-//freedesktop//DTD D-BUS Object Introspection 1.0//EN"
+                      "http://www.freedesktop.org/standards/dbus/1.0/introspect.dtd";>
+<node>
+  <interface name="com.intel.dLeynaServer.MediaDevice">
+    <method name="UploadToAnyContainer">
+      <arg type="s" name="DisplayName" direction="in">
+      </arg>
+      <arg type="s" name="FilePath" direction="in">
+      </arg>
+      <arg type="u" name="UploadId" direction="out">
+      </arg>
+      <arg type="o" name="Path" direction="out">
+      </arg>
+    </method>
+    <method name="GetUploadStatus">
+      <arg type="u" name="UploadId" direction="in">
+      </arg>
+      <arg type="s" name="UploadStatus" direction="out">
+      </arg>
+      <arg type="t" name="Length" direction="out">
+      </arg>
+      <arg type="t" name="Total" direction="out">
+      </arg>
+    </method>
+    <method name="GetUploadIDs">
+      <arg type="au" name="Total" direction="out">
+      </arg>
+    </method>
+    <method name="CancelUpload">
+      <arg type="u" name="UploadId" direction="in">
+      </arg>
+    </method>
+    <method name="CreateContainerInAnyContainer">
+      <arg type="s" name="DisplayName" direction="in">
+      </arg>
+      <arg type="s" name="Type" direction="in">
+      </arg>
+      <arg type="as" name="ChildTypes" direction="in">
+      </arg>
+      <arg type="o" name="Path" direction="out">
+      </arg>
+    </method>
+    <method name="Cancel">
+    </method>
+    <method name="CreatePlaylistInAnyContainer">
+      <arg type="s" name="Title" direction="in">
+      </arg>
+      <arg type="s" name="Creator" direction="in">
+      </arg>
+      <arg type="s" name="Genre" direction="in">
+      </arg>
+      <arg type="s" name="Description" direction="in">
+      </arg>
+      <arg type="ao" name="PlaylistItems" direction="in">
+      </arg>
+      <arg type="u" name="UploadId" direction="out">
+      </arg>
+      <arg type="o" name="Path" direction="out">
+      </arg>
+    </method>
+    <signal name="ContainerUpdateIDs">
+      <arg type="a(ou)" name="ContainerPathsIDs">
+      </arg>
+    </signal>
+    <method name="BrowseObjects">
+      <arg type="ao" name="Objects" direction="in">
+      </arg>
+      <arg type="as" name="Filter" direction="in">
+      </arg>
+      <arg type="aa{sv}" name="Children" direction="out">
+      </arg>
+    </method>
+    <signal name="LastChange">
+      <arg type="a(sv)" name="StateEvent">
+      </arg>
+    </signal>
+    <signal name="Changed">
+      <arg type="aa{sv}" name="ChangedObjects">
+      </arg>
+    </signal>
+    <signal name="UploadUpdate">
+      <arg type="u" name="UploadId">
+      </arg>
+      <arg type="s" name="UploadStatus">
+      </arg>
+      <arg type="t" name="Length">
+      </arg>
+      <arg type="t" name="Total">
+      </arg>
+    </signal>
+    <property type="s" name="Location" access="read">
+    </property>
+    <property type="s" name="UDN" access="read">
+    </property>
+    <property type="s" name="DeviceType" access="read">
+    </property>
+    <property type="s" name="FriendlyName" access="read">
+    </property>
+    <property type="s" name="Manufacturer" access="read">
+    </property>
+    <property type="s" name="ManufacturerUrl" access="read">
+    </property>
+    <property type="s" name="ModelDescription" access="read">
+    </property>
+    <property type="s" name="ModelName" access="read">
+    </property>
+    <property type="s" name="ModelNumber" access="read">
+    </property>
+    <property type="s" name="ModelURL" access="read">
+    </property>
+    <property type="s" name="SerialNumber" access="read">
+    </property>
+    <property type="s" name="PresentationURL" access="read">
+    </property>
+    <property type="s" name="IconURL" access="read">
+    </property>
+    <property type="a{sv}" name="DLNACaps" access="read">
+    </property>
+    <property type="as" name="SearchCaps" access="read">
+    </property>
+    <property type="as" name="SortCaps" access="read">
+    </property>
+    <property type="as" name="SortExtCaps" access="read">
+    </property>
+    <property type="a(ssao)" name="FeatureList" access="read">
+    </property>
+    <property type="u" name="SystemUpdateID" access="read">
+    </property>
+    <property type="s" name="ServiceResetToken" access="read">
+    </property>
+  </interface>
+</node>
diff --git a/src/dleyna/grl-dleyna-server.c b/src/dleyna/grl-dleyna-server.c
new file mode 100644
index 0000000..73d8d00
--- /dev/null
+++ b/src/dleyna/grl-dleyna-server.c
@@ -0,0 +1,424 @@
+/*
+ * Copyright © 2013 Intel Corporation. All rights reserved.
+ *
+ * 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, either 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, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "config.h"
+
+#include "grl-dleyna-server.h"
+
+#include <grilo.h>
+
+typedef enum
+{
+  INIT_NOTHING   = 0,
+  INIT_DEVICE    = 1 << 0,
+  INIT_OBJECT    = 1 << 1,
+  INIT_CONTAINER = 1 << 2,
+  INIT_READY     = INIT_DEVICE | INIT_OBJECT | INIT_CONTAINER
+} InitStatus;
+
+struct _GrlDleynaServerPrivate
+{
+  GBusType bus_type;
+  GDBusProxyFlags flags;
+  gchar *object_path;
+  gchar *well_known_name;
+
+  GrlDleynaMediaDevice *media_device;
+  GrlDleynaMediaObject2 *media_object;
+  GrlDleynaMediaContainer2 *media_container;
+
+  InitStatus init_status;
+};
+
+enum
+{
+  PROP_0,
+  PROP_BUS_TYPE,
+  PROP_WELL_KNOWN_NAME,
+  PROP_FLAGS,
+  PROP_OBJECT_PATH
+};
+
+static void grl_dleyna_server_async_initable_iface_init (GAsyncInitableIface *iface);
+
+G_DEFINE_TYPE_WITH_CODE (GrlDleynaServer, grl_dleyna_server, G_TYPE_OBJECT,
+                         G_IMPLEMENT_INTERFACE (G_TYPE_ASYNC_INITABLE, 
grl_dleyna_server_async_initable_iface_init))
+
+static void
+grl_dleyna_server_dispose (GObject *object)
+{
+  GrlDleynaServer *self = GRL_DLEYNA_SERVER (object);
+  GrlDleynaServerPrivate *priv = self->priv;
+
+  g_clear_object (&priv->media_device);
+  g_clear_object (&priv->media_object);
+  g_clear_object (&priv->media_container);
+
+  G_OBJECT_CLASS (grl_dleyna_server_parent_class)->dispose (object);
+}
+
+static void
+grl_dleyna_server_finalize (GObject *object)
+{
+  GrlDleynaServer *self = GRL_DLEYNA_SERVER (object);
+  GrlDleynaServerPrivate *priv = self->priv;
+
+  g_free (priv->well_known_name);
+  g_free (priv->object_path);
+
+  G_OBJECT_CLASS (grl_dleyna_server_parent_class)->finalize (object);
+}
+
+static void
+grl_dleyna_server_get_property (GObject    *object,
+                                guint       prop_id,
+                                GValue     *value,
+                                GParamSpec *pspec)
+{
+  GrlDleynaServer *self = GRL_DLEYNA_SERVER (object);
+  GrlDleynaServerPrivate *priv = self->priv;
+
+  switch (prop_id) {
+    case PROP_FLAGS:
+      g_value_set_flags (value, priv->flags);
+      break;
+
+    case PROP_BUS_TYPE:
+      g_value_set_enum (value, priv->bus_type);
+      break;
+
+    case PROP_WELL_KNOWN_NAME:
+      g_value_set_string (value, priv->well_known_name);
+      break;
+
+    case PROP_OBJECT_PATH:
+      g_value_set_string (value, priv->object_path);
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+      break;
+  }
+}
+
+static void
+grl_dleyna_server_set_property (GObject      *object,
+                                guint         prop_id,
+                                const GValue *value,
+                                GParamSpec   *pspec)
+{
+  GrlDleynaServer *self = GRL_DLEYNA_SERVER (object);
+  GrlDleynaServerPrivate *priv = self->priv;
+
+  switch (prop_id) {
+    case PROP_FLAGS:
+      priv->flags = g_value_get_flags (value);
+      break;
+
+    case PROP_BUS_TYPE:
+      priv->bus_type = g_value_get_enum (value);
+      break;
+
+    case PROP_WELL_KNOWN_NAME:
+      priv->well_known_name = g_value_dup_string (value);
+      break;
+
+    case PROP_OBJECT_PATH:
+      priv->object_path = g_value_dup_string (value);
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+      break;
+  }
+}
+
+static void
+grl_dleyna_server_init (GrlDleynaServer *self)
+{
+  GrlDleynaServerPrivate *priv;
+
+  self->priv = priv = G_TYPE_INSTANCE_GET_PRIVATE (self, GRL_TYPE_DLEYNA_SERVER,
+                                                   GrlDleynaServerPrivate);
+}
+
+static void
+grl_dleyna_server_class_init (GrlDleynaServerClass *class)
+{
+  GObjectClass *gobject_class = G_OBJECT_CLASS (class);
+
+  gobject_class->dispose = grl_dleyna_server_dispose;
+  gobject_class->finalize = grl_dleyna_server_finalize;
+  gobject_class->get_property = grl_dleyna_server_get_property;
+  gobject_class->set_property = grl_dleyna_server_set_property;
+
+  g_object_class_install_property (gobject_class,
+                                   PROP_FLAGS,
+                                   g_param_spec_flags ("flags",
+                                                       "Flags",
+                                                       "Proxy flags",
+                                                       G_TYPE_DBUS_PROXY_FLAGS,
+                                                       G_DBUS_PROXY_FLAGS_NONE,
+                                                       G_PARAM_READABLE |
+                                                       G_PARAM_WRITABLE |
+                                                       G_PARAM_CONSTRUCT_ONLY |
+                                                       G_PARAM_STATIC_NAME |
+                                                       G_PARAM_STATIC_BLURB |
+                                                       G_PARAM_STATIC_NICK));
+
+  g_object_class_install_property (gobject_class,
+                                   PROP_BUS_TYPE,
+                                   g_param_spec_enum ("bus-type",
+                                                      "Bus Type",
+                                                      "The bus to connect to, defaults to the session one",
+                                                      G_TYPE_BUS_TYPE,
+                                                      G_BUS_TYPE_SESSION,
+                                                      G_PARAM_WRITABLE |
+                                                      G_PARAM_CONSTRUCT_ONLY |
+                                                      G_PARAM_STATIC_NAME |
+                                                      G_PARAM_STATIC_BLURB |
+                                                      G_PARAM_STATIC_NICK));
+
+  g_object_class_install_property (gobject_class,
+                                   PROP_WELL_KNOWN_NAME,
+                                   g_param_spec_string ("well-known-name",
+                                                        "Well-Known Name",
+                                                        "The well-known name of the service",
+                                                        NULL,
+                                                        G_PARAM_READABLE |
+                                                        G_PARAM_WRITABLE |
+                                                        G_PARAM_CONSTRUCT_ONLY |
+                                                        G_PARAM_STATIC_NAME |
+                                                        G_PARAM_STATIC_BLURB |
+                                                        G_PARAM_STATIC_NICK));
+
+  g_object_class_install_property (gobject_class,
+                                   PROP_OBJECT_PATH,
+                                   g_param_spec_string ("object-path",
+                                                        "object-path",
+                                                        "The object path the proxy is for",
+                                                        NULL,
+                                                        G_PARAM_WRITABLE |
+                                                        G_PARAM_CONSTRUCT_ONLY |
+                                                        G_PARAM_STATIC_NAME |
+                                                        G_PARAM_STATIC_BLURB |
+                                                        G_PARAM_STATIC_NICK));
+
+  g_type_class_add_private (class, sizeof (GrlDleynaServerPrivate));
+}
+
+void
+grl_dleyna_server_new_for_bus (GBusType             bus_type,
+                               GDBusProxyFlags      flags,
+                               const gchar         *well_known_name,
+                               const gchar         *object_path,
+                               GCancellable        *cancellable,
+                               GAsyncReadyCallback  callback,
+                               gpointer             user_data)
+{
+  g_async_initable_new_async (GRL_TYPE_DLEYNA_SERVER, G_PRIORITY_DEFAULT, cancellable,
+                              callback, user_data, "flags", flags, "bus-type", bus_type,
+                              "well-known-name", well_known_name, "object-path", object_path, NULL);
+}
+
+/* grl_dleyna_server_init_check_complete:
+ *
+ * Check that all the async subtasks have completed and return the result.
+ *
+ * Note that if the caller does not take ownership of the object (eg. by not
+ * calling g_task_propagate_pointer() or in case of errors) this may lead to
+ * the object being disposed.
+ */
+static gboolean
+grl_dleyna_server_init_check_complete (GrlDleynaServer *self,
+                                       GTask           *init_task)
+{
+  GError *error;
+
+  g_return_val_if_fail (g_task_is_valid (init_task, self), TRUE);
+
+  if (self->priv->init_status != INIT_READY) {
+    return FALSE;
+  }
+
+  error = g_task_get_task_data (init_task);
+
+  if (error != NULL) {
+    g_task_return_error (init_task, error);
+  }
+  else {
+    g_task_return_boolean (init_task, TRUE);
+  }
+
+  g_object_unref (init_task);
+
+  return TRUE;
+}
+
+static void
+grl_dleyna_server_media_device_proxy_new_cb (GObject      *source_object,
+                                             GAsyncResult *res,
+                                             gpointer      user_data)
+{
+  GTask *init_task = G_TASK (user_data);
+  GrlDleynaServer *self;
+  GError *error = NULL;
+
+  self = GRL_DLEYNA_SERVER (g_task_get_source_object (init_task));
+
+  self->priv->init_status |= INIT_DEVICE;
+  self->priv->media_device = grl_dleyna_media_device_proxy_new_for_bus_finish (res, &error);
+
+  if (error != NULL) {
+    GRL_WARNING ("Unable to load the MediaDevice interface: %s", error->message);
+    g_task_set_task_data (init_task, error, (GDestroyNotify) g_error_free);
+  }
+
+  grl_dleyna_server_init_check_complete (self, init_task);
+}
+
+static void
+grl_dleyna_server_media_object2_proxy_new_cb (GObject      *source_object,
+                                              GAsyncResult *res,
+                                              gpointer      user_data)
+{
+  GTask *init_task = G_TASK (user_data);
+  GrlDleynaServer *self;
+  GError *error = NULL;
+
+  self = GRL_DLEYNA_SERVER (g_task_get_source_object (init_task));
+
+  self->priv->init_status |= INIT_OBJECT;
+  self->priv->media_object = grl_dleyna_media_object2_proxy_new_for_bus_finish (res, &error);
+
+  if (error != NULL) {
+    GRL_WARNING ("Unable to load the MediaObjetc2 interface: %s", error->message);
+    g_task_set_task_data (init_task, error, (GDestroyNotify) g_error_free);
+  }
+
+  grl_dleyna_server_init_check_complete (self, init_task);
+}
+
+static void
+grl_dleyna_server_media_container2_proxy_new_cb (GObject      *source_object,
+                                                 GAsyncResult *res,
+                                                 gpointer      user_data)
+{
+  GTask *init_task = G_TASK (user_data);
+  GrlDleynaServer *self;
+  GError *error = NULL;
+
+  self = GRL_DLEYNA_SERVER (g_task_get_source_object (init_task));
+
+  self->priv->init_status |= INIT_CONTAINER;
+  self->priv->media_container = grl_dleyna_media_container2_proxy_new_for_bus_finish (res, &error);
+
+  if (error != NULL) {
+    GRL_WARNING ("Unable to load the MediaContainer2 interface: %s", error->message);
+    g_task_set_task_data (init_task, error, (GDestroyNotify) g_error_free);
+  }
+
+  grl_dleyna_server_init_check_complete (self, init_task);
+}
+
+static void
+grl_dleyna_server_init_async (GAsyncInitable      *initable,
+                              int                  io_priority,
+                              GCancellable        *cancellable,
+                              GAsyncReadyCallback  callback,
+                              gpointer             user_data)
+{
+  GrlDleynaServer *self = GRL_DLEYNA_SERVER (initable);
+  GrlDleynaServerPrivate *priv = self->priv;
+  GTask *init_task;
+
+  init_task = g_task_new (initable, cancellable, callback, user_data);
+  g_task_set_priority (init_task, io_priority);
+
+  /* Load all three DBus interface proxies asynchronously and then complete
+   * the initialization when all of them are ready */
+  grl_dleyna_media_device_proxy_new_for_bus (priv->bus_type, priv->flags, priv->well_known_name,
+                                             priv->object_path, cancellable,
+                                             grl_dleyna_server_media_device_proxy_new_cb, init_task);
+  grl_dleyna_media_object2_proxy_new_for_bus (priv->bus_type, priv->flags, priv->well_known_name,
+                                              priv->object_path, cancellable,
+                                              grl_dleyna_server_media_object2_proxy_new_cb, init_task);
+  grl_dleyna_media_container2_proxy_new_for_bus (priv->bus_type, priv->flags, priv->well_known_name,
+                                                 priv->object_path, cancellable,
+                                                 grl_dleyna_server_media_container2_proxy_new_cb, init_task);
+}
+
+static gboolean
+grl_dleyna_server_init_finish (GAsyncInitable *initable,
+                               GAsyncResult   *result,
+                               GError        **error)
+{
+  g_return_val_if_fail (g_task_is_valid (result, G_OBJECT (initable)), FALSE);
+
+  return g_task_propagate_pointer (G_TASK (result), error) != NULL;
+}
+
+GrlDleynaServer*
+grl_dleyna_server_new_for_bus_finish (GAsyncResult *result,
+                                      GError      **error)
+{
+  GObject *object, *source_object;
+  GError *err = NULL;
+
+  source_object = g_async_result_get_source_object (result);
+  object = g_async_initable_new_finish (G_ASYNC_INITABLE (source_object), result, &err);
+  g_object_unref (source_object);
+
+  if (err != NULL) {
+    g_clear_object (&object);
+    g_propagate_error (error, err);
+    return NULL;
+  }
+
+  return GRL_DLEYNA_SERVER (object);
+}
+
+static void
+grl_dleyna_server_async_initable_iface_init (GAsyncInitableIface *iface)
+{
+  iface->init_async = grl_dleyna_server_init_async;
+  iface->init_finish = grl_dleyna_server_init_finish;
+}
+
+const gchar *
+grl_dleyna_server_get_object_path (GrlDleynaServer *self)
+{
+  return self->priv->object_path;
+}
+
+GrlDleynaMediaDevice *
+grl_dleyna_server_get_media_device (GrlDleynaServer *self)
+{
+  return self->priv->media_device;
+}
+
+GrlDleynaMediaObject2 *
+grl_dleyna_server_get_media_object (GrlDleynaServer *self)
+{
+  return self->priv->media_object;
+}
+
+GrlDleynaMediaContainer2 *
+grl_dleyna_server_get_media_container (GrlDleynaServer *self)
+{
+  return self->priv->media_container;
+}
diff --git a/src/dleyna/grl-dleyna-server.h b/src/dleyna/grl-dleyna-server.h
new file mode 100644
index 0000000..d282e49
--- /dev/null
+++ b/src/dleyna/grl-dleyna-server.h
@@ -0,0 +1,90 @@
+/*
+ * Copyright © 2013 Intel Corporation. All rights reserved.
+ *
+ * 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, either 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, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef GRL_DLEYNA_SERVER_H
+#define GRL_DLEYNA_SERVER_H
+
+#include <glib-object.h>
+#include <gio/gio.h>
+
+#include "grl-dleyna-proxy-mediadevice.h"
+#include "grl-dleyna-proxy-mediaserver2.h"
+
+G_BEGIN_DECLS
+
+#define GRL_TYPE_DLEYNA_SERVER (grl_dleyna_server_get_type ())
+
+#define GRL_DLEYNA_SERVER(obj) \
+  (G_TYPE_CHECK_INSTANCE_CAST ((obj), \
+   GRL_TYPE_DLEYNA_SERVER, GrlDleynaServer))
+
+#define GRL_DLEYNA_SERVER_CLASS(klass) \
+  (G_TYPE_CHECK_CLASS_CAST ((klass), \
+   GRL_TYPE_DLEYNA_SERVER, GrlDleynaServerClass))
+
+#define GRL_IS_DLEYNA_SERVER(obj) \
+  (G_TYPE_CHECK_INSTANCE_TYPE ((obj), \
+   GRL_TYPE_DLEYNA_SERVER))
+
+#define GRL_IS_DLEYNA_SERVER_CLASS(klass) \
+  (G_TYPE_CHECK_CLASS_TYPE ((klass), \
+   GRL_TYPE_DLEYNA_SERVER))
+
+#define GRL_DLEYNA_SERVER_GET_CLASS(obj) \
+  (G_TYPE_INSTANCE_GET_CLASS ((obj), \
+   GRL_TYPE_DLEYNA_SERVER, GrlDleynaServerClass))
+
+typedef struct _GrlDleynaServer        GrlDleynaServer;
+typedef struct _GrlDleynaServerClass   GrlDleynaServerClass;
+typedef struct _GrlDleynaServerPrivate GrlDleynaServerPrivate;
+
+struct _GrlDleynaServer
+{
+  GObject parent_instance;
+  GrlDleynaServerPrivate *priv;
+};
+
+struct _GrlDleynaServerClass
+{
+  GObjectClass parent_class;
+};
+
+GType                    grl_dleyna_server_get_type            (void) G_GNUC_CONST;
+
+void                     grl_dleyna_server_new_for_bus         (GBusType            bus_type,
+                                                                GDBusProxyFlags     flags,
+                                                                const gchar        *name,
+                                                                const gchar        *object_path,
+                                                                GCancellable       *cancellable,
+                                                                GAsyncReadyCallback callback,
+                                                                gpointer            user_data);
+
+GrlDleynaServer          *grl_dleyna_server_new_for_bus_finish  (GAsyncResult      *res,
+                                                                 GError           **error);
+
+const gchar              *grl_dleyna_server_get_object_path     (GrlDleynaServer   *server);
+
+GrlDleynaMediaDevice     *grl_dleyna_server_get_media_device    (GrlDleynaServer   *server);
+
+GrlDleynaMediaObject2    *grl_dleyna_server_get_media_object    (GrlDleynaServer   *server);
+
+GrlDleynaMediaContainer2 *grl_dleyna_server_get_media_container (GrlDleynaServer   *server);
+
+
+G_END_DECLS
+
+#endif /* GRL_DLEYNA_SERVER_H */
diff --git a/src/dleyna/grl-dleyna-servers-manager.c b/src/dleyna/grl-dleyna-servers-manager.c
new file mode 100644
index 0000000..28fb6a4
--- /dev/null
+++ b/src/dleyna/grl-dleyna-servers-manager.c
@@ -0,0 +1,253 @@
+/*
+ * Copyright © 2013 Intel Corporation. All rights reserved.
+ *
+ * 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, either 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, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "config.h"
+
+#include "grl-dleyna-servers-manager.h"
+
+#include <gio/gio.h>
+#include <grilo.h>
+
+#include "grl-dleyna-proxy-manager.h"
+#include "grl-dleyna-server.h"
+
+#define GRL_LOG_DOMAIN_DEFAULT dleyna_log_domain
+GRL_LOG_DOMAIN_EXTERN(dleyna_log_domain);
+
+struct _GrlDleynaServersManagerPrivate
+{
+  GrlDleynaManager *proxy;
+  GHashTable *servers;
+
+  gboolean got_error;
+};
+
+enum
+{
+  SERVER_FOUND,
+  SERVER_LOST,
+  LAST_SIGNAL
+};
+
+static guint signals[LAST_SIGNAL] = { 0 };
+
+static GObject *grl_dleyna_servers_manager_singleton = NULL;
+
+G_DEFINE_TYPE (GrlDleynaServersManager, grl_dleyna_servers_manager, G_TYPE_OBJECT)
+
+static void
+grl_dleyna_servers_manager_dispose (GObject *object)
+{
+  GrlDleynaServersManager *self = GRL_DLEYNA_SERVERS_MANAGER (object);
+  GrlDleynaServersManagerPrivate *priv = self->priv;
+
+  g_clear_object (&priv->proxy);
+  g_clear_pointer (&priv->servers, g_hash_table_unref);
+
+  G_OBJECT_CLASS (grl_dleyna_servers_manager_parent_class)->dispose (object);
+}
+
+static void
+grl_dleyna_servers_manager_server_new_cb (GObject      *source_object,
+                                          GAsyncResult *res,
+                                          gpointer      user_data)
+{
+  GrlDleynaServersManager *self = GRL_DLEYNA_SERVERS_MANAGER (user_data);
+  GrlDleynaServersManagerPrivate *priv = self->priv;
+  GrlDleynaServer *server;
+  GrlDleynaMediaDevice *device;
+  const gchar *object_path;
+  GError *error = NULL;
+
+  GRL_DEBUG (G_STRFUNC);
+  server = grl_dleyna_server_new_for_bus_finish (res, &error);
+  if (error != NULL) {
+    GRL_WARNING ("Unable to load server object: %s", error->message);
+    g_error_free (error);
+    return;
+  }
+
+  device = grl_dleyna_server_get_media_device (server);
+  object_path = grl_dleyna_server_get_object_path (server);
+  GRL_DEBUG ("%s '%s' %s %s", G_STRFUNC,
+             grl_dleyna_media_device_get_friendly_name (device),
+             grl_dleyna_media_device_get_udn (device),
+             object_path);
+  g_hash_table_insert (priv->servers, (gpointer) object_path, server);
+  g_signal_emit (self, signals[SERVER_FOUND], 0, server);
+}
+
+static void
+grl_dleyna_servers_manager_server_found_cb (GrlDleynaServersManager *self,
+                                            const gchar             *object_path,
+                                            gpointer                *data)
+{
+  grl_dleyna_server_new_for_bus (G_BUS_TYPE_SESSION, G_DBUS_PROXY_FLAGS_NONE,
+                                 "com.intel.dleyna-server", object_path, NULL,
+                                 grl_dleyna_servers_manager_server_new_cb, self);
+}
+
+static void
+grl_dleyna_servers_manager_server_lost_cb (GrlDleynaServersManager *self,
+                                           const gchar             *object_path,
+                                           gpointer                *data)
+{
+  GrlDleynaServersManagerPrivate *priv = self->priv;
+  GrlDleynaServer *server;
+  GrlDleynaMediaDevice *device;
+
+  server = GRL_DLEYNA_SERVER (g_hash_table_lookup (priv->servers, object_path));
+  g_return_if_fail (server != NULL);
+
+  g_hash_table_steal (priv->servers, object_path);
+  device = grl_dleyna_server_get_media_device (server);
+  GRL_DEBUG ("%s '%s' %s %s", G_STRFUNC,
+             grl_dleyna_media_device_get_friendly_name (device),
+             grl_dleyna_media_device_get_udn (device),
+             object_path);
+  g_signal_emit (self, signals[SERVER_LOST], 0, server);
+  g_object_unref (server);
+}
+
+static void
+grl_dleyna_servers_manager_proxy_get_servers_cb (GObject      *source_object,
+                                                 GAsyncResult *res,
+                                                 gpointer      user_data)
+{
+  GrlDleynaServersManager *self = user_data;
+  GrlDleynaServersManagerPrivate *priv = self->priv;
+  gchar **object_paths, **path;
+  GError *error = NULL;
+
+  grl_dleyna_manager_call_get_servers_finish (priv->proxy, &object_paths, res, &error);
+  if (error != NULL) {
+    GRL_WARNING ("Unable to fetch the list of available servers: %s", error->message);
+    g_error_free (error);
+    priv->got_error = TRUE;
+    return;
+  }
+
+  for (path = object_paths; *path != NULL; path++) {
+    grl_dleyna_servers_manager_server_found_cb (self, *path, NULL);
+  }
+
+  g_strfreev (object_paths);
+
+  /* Release the ref taken in grl_dleyna_manager_proxy_new_for_bus() */
+  g_object_unref (self);
+}
+
+static void
+grl_dleyna_servers_manager_proxy_new_cb (GObject      *source_object,
+                                         GAsyncResult *res,
+                                         gpointer      user_data)
+{
+  GrlDleynaServersManager *self = user_data;
+  GrlDleynaServersManagerPrivate *priv = self->priv;
+  GError *error = NULL;
+
+  priv->proxy = grl_dleyna_manager_proxy_new_for_bus_finish (res, &error);
+  if (error != NULL) {
+    GRL_WARNING ("Unable to connect to the dLeynaRenderer.Manager DBus object: %s", error->message);
+    g_error_free (error);
+    priv->got_error = TRUE;
+    return;
+  }
+
+  GRL_DEBUG ("%s DLNA servers manager initialized", G_STRFUNC);
+
+  g_object_connect (priv->proxy,
+                    "swapped-object-signal::found-server", grl_dleyna_servers_manager_server_found_cb, self,
+                    "swapped-object-signal::lost-server", grl_dleyna_servers_manager_server_lost_cb, self,
+                    NULL);
+
+  grl_dleyna_manager_call_get_servers (priv->proxy, NULL,
+                                       grl_dleyna_servers_manager_proxy_get_servers_cb, self);
+}
+
+static GObject *
+grl_dleyna_servers_manager_constructor (GType                  type,
+                                        guint                  n_construct_params,
+                                        GObjectConstructParam *construct_params)
+{
+  if (grl_dleyna_servers_manager_singleton != NULL) {
+    return g_object_ref (grl_dleyna_servers_manager_singleton);
+  }
+
+  grl_dleyna_servers_manager_singleton =
+      G_OBJECT_CLASS (grl_dleyna_servers_manager_parent_class)->constructor (type, n_construct_params,
+                                                                             construct_params);
+
+  g_object_add_weak_pointer (grl_dleyna_servers_manager_singleton, (gpointer) 
&grl_dleyna_servers_manager_singleton);
+
+  return grl_dleyna_servers_manager_singleton;
+}
+
+static void
+grl_dleyna_servers_manager_init (GrlDleynaServersManager *self)
+{
+  GrlDleynaServersManagerPrivate *priv;
+
+  self->priv = priv = G_TYPE_INSTANCE_GET_PRIVATE (self, GRL_TYPE_DLEYNA_SERVERS_MANAGER,
+                                                   GrlDleynaServersManagerPrivate);
+
+  grl_dleyna_manager_proxy_new_for_bus (G_BUS_TYPE_SESSION, G_DBUS_PROXY_FLAGS_NONE,
+                                        "com.intel.dleyna-server", "/com/intel/dLeynaServer", NULL,
+                                        grl_dleyna_servers_manager_proxy_new_cb, g_object_ref (self));
+  priv->servers = g_hash_table_new_full (g_str_hash, g_str_equal, NULL, g_object_unref);
+}
+
+static void
+grl_dleyna_servers_manager_class_init (GrlDleynaServersManagerClass *class)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (class);
+
+  object_class->constructor = grl_dleyna_servers_manager_constructor;
+  object_class->dispose = grl_dleyna_servers_manager_dispose;
+
+  signals[SERVER_FOUND] = g_signal_new ("server-found", G_TYPE_FROM_CLASS (class),
+                                        G_SIGNAL_RUN_LAST, 0, NULL, NULL,
+                                        g_cclosure_marshal_VOID__OBJECT,
+                                        G_TYPE_NONE, 1, GRL_TYPE_DLEYNA_SERVER);
+
+  signals[SERVER_LOST] = g_signal_new ("server-lost", G_TYPE_FROM_CLASS (class),
+                                       G_SIGNAL_RUN_LAST, 0, NULL, NULL,
+                                       g_cclosure_marshal_VOID__OBJECT,
+                                       G_TYPE_NONE, 1, GRL_TYPE_DLEYNA_SERVER);
+
+  g_type_class_add_private (class, sizeof (GrlDleynaServersManagerPrivate));
+}
+
+GrlDleynaServersManager *
+grl_dleyna_servers_manager_dup_singleton (void)
+{
+  GRL_DEBUG (G_STRFUNC);
+  return g_object_new (GRL_TYPE_DLEYNA_SERVERS_MANAGER, NULL);
+}
+
+gboolean
+grl_dleyna_servers_manager_is_available (void)
+{
+  GrlDleynaServersManager *self;
+
+  if (grl_dleyna_servers_manager_singleton == NULL)
+    return FALSE;
+
+  self = GRL_DLEYNA_SERVERS_MANAGER (grl_dleyna_servers_manager_singleton);
+
+  return self->priv->got_error == FALSE;
+}
diff --git a/src/dleyna/grl-dleyna-servers-manager.h b/src/dleyna/grl-dleyna-servers-manager.h
new file mode 100644
index 0000000..0c0ea60
--- /dev/null
+++ b/src/dleyna/grl-dleyna-servers-manager.h
@@ -0,0 +1,70 @@
+/*
+ * Copyright © 2013 Intel Corporation. All rights reserved.
+ *
+ * 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, either 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, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef GRL_DLEYNA_SERVERS_MANAGER_H
+#define GRL_DLEYNA_SERVERS_MANAGER_H
+
+#include <glib-object.h>
+
+G_BEGIN_DECLS
+
+#define GRL_TYPE_DLEYNA_SERVERS_MANAGER (grl_dleyna_servers_manager_get_type ())
+
+#define GRL_DLEYNA_SERVERS_MANAGER(obj) \
+  (G_TYPE_CHECK_INSTANCE_CAST ((obj), \
+   GRL_TYPE_DLEYNA_SERVERS_MANAGER, GrlDleynaServersManager))
+
+#define GRL_DLEYNA_SERVERS_MANAGER_CLASS(klass) \
+  (G_TYPE_CHECK_CLASS_CAST ((klass), \
+   GRL_TYPE_DLEYNA_SERVERS_MANAGER, GrlDleynaServersManagerClass))
+
+#define GRL_IS_DLEYNA_SERVERS_MANAGER(obj) \
+  (G_TYPE_CHECK_INSTANCE_TYPE ((obj), \
+   GRL_TYPE_DLEYNA_SERVERS_MANAGER))
+
+#define GRL_IS_DLEYNA_SERVERS_MANAGER_CLASS(klass) \
+  (G_TYPE_CHECK_CLASS_TYPE ((klass), \
+   GRL_TYPE_DLEYNA_SERVERS_MANAGER))
+
+#define GRL_DLEYNA_SERVERS_MANAGER_GET_CLASS(obj) \
+  (G_TYPE_INSTANCE_GET_CLASS ((obj), \
+   GRL_TYPE_DLEYNA_SERVERS_MANAGER, GrlDleynaServersManagerClass))
+
+typedef struct _GrlDleynaServersManager        GrlDleynaServersManager;
+typedef struct _GrlDleynaServersManagerClass   GrlDleynaServersManagerClass;
+typedef struct _GrlDleynaServersManagerPrivate GrlDleynaServersManagerPrivate;
+
+struct _GrlDleynaServersManager
+{
+  GObject parent_instance;
+  GrlDleynaServersManagerPrivate *priv;
+};
+
+struct _GrlDleynaServersManagerClass
+{
+  GObjectClass parent_class;
+};
+
+GType                    grl_dleyna_servers_manager_get_type      (void) G_GNUC_CONST;
+
+GrlDleynaServersManager *grl_dleyna_servers_manager_dup_singleton (void);
+
+gboolean                 grl_dleyna_servers_manager_is_available  (void);
+
+G_END_DECLS
+
+#endif /* GRL_DLEYNA_SERVERS_MANAGER_H */
diff --git a/src/dleyna/grl-dleyna-source.c b/src/dleyna/grl-dleyna-source.c
new file mode 100644
index 0000000..ee8ccce
--- /dev/null
+++ b/src/dleyna/grl-dleyna-source.c
@@ -0,0 +1,1682 @@
+/*
+ * Copyright (C) 2010, 2011 Igalia S.L.
+ * Copyright (C) 2011 Intel Corporation.
+ * Copyright (C) 2013 Intel Corporation.
+ *
+ * This component is based on the grl-upnp source code.
+ *
+ * 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 "config.h"
+
+#include "grl-dleyna-source.h"
+
+#include <grilo.h>
+#include <glib/gi18n-lib.h>
+
+#define SOURCE_ID_TEMPLATE    "grl-dleyna-%s"
+#define SOURCE_DESC_TEMPLATE  _("A source for browsing the DLNA server '%s'")
+
+#define MEDIA_ID_PREFIX "dleyna:"
+#define MEDIA_ID_PREFIX_LENGTH 7
+
+#define DLEYNA_DBUS_NAME "com.intel.dleyna-server"
+
+#define DLEYNA_SEARCH_SPEC                \
+  "(DisplayName contains \"%s\" or "      \
+  "Album contains \"%s\" or "             \
+  "Artist contains \"%s\")"
+
+#define DLEYNA_BROWSE_SPEC                \
+  "Parent = \"%s\""
+
+#define DLEYNA_TYPE_FILTER_AUDIO          \
+  "Type derivedfrom \"audio\" or Type derivedfrom \"music\""
+
+#define DLEYNA_TYPE_FILTER_VIDEO          \
+  "Type derivedfrom \"video\""
+
+#define DLEYNA_TYPE_FILTER_IMAGE          \
+  "Type derivedfrom \"image\""
+
+#define DLEYNA_TYPE_FILTER_CONTAINER      \
+  "Type derivedfrom \"container\""
+
+#define GRL_LOG_DOMAIN_DEFAULT dleyna_log_domain
+GRL_LOG_DOMAIN_EXTERN(dleyna_log_domain);
+
+struct _GrlDleynaSourcePrivate {
+  GrlDleynaServer *server;
+  GHashTable *uploads;
+  gboolean search_enabled;
+  gboolean browse_filtered_enabled;
+};
+
+enum {
+  PROP_0,
+  PROP_SERVER
+};
+
+typedef enum {
+  DLEYNA_CHANGE_TYPE_ADD = 1,
+  DLEYNA_CHANGE_TYPE_MOD = 2,
+  DLEYNA_CHANGE_TYPE_DEL = 3,
+  DLEYNA_CHANGE_TYPE_DONE = 4,
+  DLEYNA_CHANGE_TYPE_CONTAINER = 5
+} DleynaChangeType;
+
+#define GRL_DLEYNA_GET_PRIVATE(object)                                    \
+  (G_TYPE_INSTANCE_GET_PRIVATE((object), GRL_DLEYNA_SOURCE_TYPE, GrlDleynaSourcePrivate))
+
+/* ================== Prototypes ================== */
+
+static void            grl_dleyna_source_dispose              (GObject *object);
+static void            grl_dleyna_source_set_property         (GObject *object,
+                                                               guint prop_id,
+                                                               const GValue *value,
+                                                               GParamSpec *pspec);
+static void            grl_dleyna_source_get_property         (GObject *object,
+                                                               guint prop_id,
+                                                               GValue *value,
+                                                               GParamSpec *pspec);
+static GrlCaps *       grl_dleyna_source_get_caps             (GrlSource *source,
+                                                               GrlSupportedOps operation);
+static const GList *   grl_dleyna_source_supported_keys       (GrlSource *source);
+static const GList *   grl_dleyna_source_writable_keys        (GrlSource *source);
+static GrlSupportedOps grl_dleyna_source_supported_operations (GrlSource *source);
+static void            grl_dleyna_source_resolve              (GrlSource *source,
+                                                               GrlSourceResolveSpec *rs);
+static void            grl_dleyna_source_browse               (GrlSource *source,
+                                                               GrlSourceBrowseSpec *bs);
+static void            grl_dleyna_source_search               (GrlSource *source,
+                                                               GrlSourceSearchSpec *ss);
+static void            grl_dleyna_source_query                (GrlSource *source,
+                                                               GrlSourceQuerySpec *qs);
+static void            grl_dleyna_source_store                (GrlSource *source,
+                                                               GrlSourceStoreSpec *ss);
+static void            grl_dleyna_source_store_metadata       (GrlSource *source,
+                                                               GrlSourceStoreMetadataSpec *sms);
+static void            grl_dleyna_source_remove               (GrlSource *source,
+                                                               GrlSourceRemoveSpec *rs);
+static void            grl_dleyna_source_cancel               (GrlSource *source,
+                                                               guint operation_id);
+static gboolean        grl_dleyna_source_notify_change_start  (GrlSource *source,
+                                                               GError **error);
+static gboolean        grl_dleyna_source_notify_change_stop   (GrlSource *source,
+                                                               GError **error);
+static void            grl_dleyna_source_upload_destroy       (GrlSourceStoreSpec *ss);
+static void            grl_dleyna_source_set_server           (GrlDleynaSource *source,
+                                                               GrlDleynaServer *server);
+
+/* ================== GObject API implementation ================== */
+
+G_DEFINE_TYPE (GrlDleynaSource, grl_dleyna_source, GRL_TYPE_SOURCE)
+
+static void
+grl_dleyna_source_class_init (GrlDleynaSourceClass * klass)
+{
+  GObjectClass *gobject_class = G_OBJECT_CLASS (klass);
+  GrlSourceClass *source_class = GRL_SOURCE_CLASS (klass);
+
+  gobject_class->dispose = grl_dleyna_source_dispose;
+  gobject_class->get_property = grl_dleyna_source_get_property;
+  gobject_class->set_property = grl_dleyna_source_set_property;
+
+  source_class->get_caps = grl_dleyna_source_get_caps;
+  source_class->supported_keys = grl_dleyna_source_supported_keys;
+  source_class->writable_keys = grl_dleyna_source_writable_keys;
+  source_class->supported_operations = grl_dleyna_source_supported_operations;
+  source_class->resolve = grl_dleyna_source_resolve;
+  source_class->browse = grl_dleyna_source_browse;
+  source_class->search = grl_dleyna_source_search;
+  source_class->query = grl_dleyna_source_query;
+  source_class->store = grl_dleyna_source_store;
+  source_class->store_metadata = grl_dleyna_source_store_metadata;
+  source_class->remove = grl_dleyna_source_remove;
+  source_class->cancel = grl_dleyna_source_cancel;
+  source_class->notify_change_start = grl_dleyna_source_notify_change_start;
+  source_class->notify_change_stop = grl_dleyna_source_notify_change_stop;
+
+  g_object_class_install_property (gobject_class,
+                                   PROP_SERVER,
+                                   g_param_spec_object ("server",
+                                                        "Server",
+                                                        "The DLNA Media Server (DMS) this source refer to.",
+                                                        GRL_TYPE_DLEYNA_SERVER,
+                                                        G_PARAM_WRITABLE |
+                                                        G_PARAM_CONSTRUCT_ONLY |
+                                                        G_PARAM_STATIC_NAME |
+                                                        G_PARAM_STATIC_BLURB |
+                                                        G_PARAM_STATIC_NICK));
+
+  g_type_class_add_private (klass, sizeof (GrlDleynaSourcePrivate));
+}
+
+static void
+grl_dleyna_source_init (GrlDleynaSource *source)
+{
+  source->priv = GRL_DLEYNA_GET_PRIVATE (source);
+  source->priv->uploads = g_hash_table_new_full (g_direct_hash, g_direct_equal, NULL,
+                                                 (GDestroyNotify)grl_dleyna_source_upload_destroy);
+}
+
+static void
+grl_dleyna_source_dispose (GObject *object)
+{
+  GrlDleynaSource *source = GRL_DLEYNA_SOURCE (object);
+
+  g_clear_object (&source->priv->server);
+
+  G_OBJECT_CLASS (grl_dleyna_source_parent_class)->dispose (object);
+}
+
+static void
+grl_dleyna_source_set_property (GObject *object,
+                                guint prop_id,
+                                const GValue *value,
+                                GParamSpec *pspec)
+{
+  GrlDleynaSource *source = GRL_DLEYNA_SOURCE (object);
+
+  switch (prop_id) {
+    case PROP_SERVER:
+      grl_dleyna_source_set_server (source, GRL_DLEYNA_SERVER (g_value_get_object (value)));
+      break;
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+      break;
+  }
+}
+
+static void
+grl_dleyna_source_get_property (GObject *object,
+                                guint prop_id,
+                                GValue *value,
+                                GParamSpec *pspec)
+{
+  GrlDleynaSource *source = GRL_DLEYNA_SOURCE (object);
+
+  switch (prop_id) {
+    case PROP_SERVER:
+      g_value_set_object (value, source->priv->server);
+      break;
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+      break;
+  }
+}
+
+/* ================== Private functions ================== */
+
+static GError *
+grl_dleyna_source_convert_error (GError *src,
+                                 gint code)
+{
+  GError *dst;
+  dst = g_error_new_literal (GRL_CORE_ERROR, code, src->message);
+  g_error_free (src);
+  return dst;
+}
+
+static const gchar *
+grl_dleyna_source_media_get_object_path_from_id (const gchar *id)
+{
+  g_return_val_if_fail (g_str_has_prefix(id, "dleyna:"), NULL);
+  return id + MEDIA_ID_PREFIX_LENGTH;
+}
+
+static const gchar *
+grl_dleyna_source_media_get_object_path (GrlMedia *media)
+{
+  const gchar *id;
+
+  if (media == NULL) {
+    return NULL;
+  }
+
+  id = grl_media_get_id (media);
+  if (id == NULL) {
+    return NULL;
+  }
+
+  return grl_dleyna_source_media_get_object_path_from_id (id);
+}
+
+static void
+grl_dleyna_source_media_set_id_from_object_path (GrlMedia    *media,
+                                                 const gchar *object_path)
+{
+  gchar *id;
+
+  id = g_strdup_printf (MEDIA_ID_PREFIX "%s", object_path);
+  grl_media_set_id (media, id);
+  g_free (id);
+}
+
+static void
+grl_dleyna_source_upload_destroy (GrlSourceStoreSpec *ss)
+{
+  GError *error;
+
+  error = g_error_new_literal (GRL_CORE_ERROR, GRL_CORE_ERROR_STORE_FAILED,
+                               _("Upload failed, target source destroyed"));
+  ss->callback (ss->source, ss->media, NULL, ss->user_data, error);
+  g_error_free (error);
+}
+
+static void
+grl_dleyna_source_update_caps_cb (GObject    *gobject,
+                                  GParamSpec *pspec,
+                                  gpointer    user_data)
+{
+  GrlDleynaSource *source = GRL_DLEYNA_SOURCE (gobject);
+  GrlDleynaMediaDevice *device = GRL_DLEYNA_MEDIA_DEVICE (user_data);
+  const gchar * const *search_caps;
+
+  search_caps = grl_dleyna_media_device_get_search_caps (device);
+  if (search_caps == NULL) {
+    GRL_DEBUG ("%s caps:NULL", G_STRFUNC);
+    source->priv->search_enabled = FALSE;
+    source->priv->browse_filtered_enabled = FALSE;
+  }
+  else if (g_strv_length ((gchar **) search_caps) == 1 && g_strcmp0 ("*", search_caps[0]) == 0) {
+    GRL_DEBUG ("%s caps:*", G_STRFUNC);
+    source->priv->search_enabled = TRUE;
+    source->priv->browse_filtered_enabled = TRUE;
+  }
+  else {
+    gchar **cap;
+    gboolean type, displayname, album, artist, parent;
+    type = displayname = album = artist = parent = FALSE;
+
+    GRL_DEBUG ("%s caps:", G_STRFUNC);
+    for (cap = (gchar **) search_caps; *cap != NULL; cap++) {
+      GRL_DEBUG ("  %s", *cap);
+      type = type || (g_strcmp0(*cap, "Type") == 0);
+      displayname = displayname || (g_strcmp0(*cap, "DisplayName") == 0);
+      album = album || (g_strcmp0(*cap, "Album") == 0);
+      artist = artist || (g_strcmp0(*cap, "Artist") == 0);
+      parent = parent || (g_strcmp0(*cap, "Parent") == 0);
+    }
+
+    source->priv->search_enabled = type && (displayname || album || artist);
+    source->priv->browse_filtered_enabled = type && parent;
+  }
+
+  GRL_DEBUG ("%s %s search:%d filtered:%d", G_STRFUNC, grl_source_get_id (GRL_SOURCE (source)),
+             source->priv->search_enabled, source->priv->browse_filtered_enabled);
+}
+
+static void
+grl_dleyna_source_store_upload_completed (GrlSourceStoreSpec *ss,
+                                          const gchar        *object_path,
+                                          GError             *error)
+{
+  GList *failed_keys;
+
+  GRL_DEBUG ("%s", G_STRFUNC);
+
+  if (error != NULL) {
+    GRL_WARNING ("%s error:%s", G_STRFUNC, error->message);
+    error = grl_dleyna_source_convert_error (error, GRL_CORE_ERROR_STORE_FAILED);
+    ss->callback (ss->source, ss->media, NULL, ss->user_data, error);
+    g_error_free (error);
+    return;
+  }
+
+  if (object_path != NULL) {
+    grl_dleyna_source_media_set_id_from_object_path (ss->media, object_path);
+  }
+
+  failed_keys = grl_data_get_keys (GRL_DATA (ss->media));
+  failed_keys = g_list_remove (failed_keys, GINT_TO_POINTER (GRL_METADATA_KEY_URL));
+  failed_keys = g_list_remove (failed_keys, GINT_TO_POINTER (GRL_METADATA_KEY_ID));
+  failed_keys = g_list_remove (failed_keys, GINT_TO_POINTER (GRL_METADATA_KEY_TITLE));
+
+  ss->callback (ss->source, ss->media, failed_keys, ss->user_data, error);
+
+  g_list_free (failed_keys);
+}
+
+static void
+grl_dleyna_source_store_upload_update_cb (GrlDleynaSource      *self,
+                                          guint                 upload_id,
+                                          const gchar          *upload_status,
+                                          guint64               length,
+                                          guint64               total,
+                                          GrlDleynaMediaDevice *mediadevice)
+{
+  GrlSourceStoreSpec *ss;
+  GError *error = NULL;
+
+  ss = g_hash_table_lookup (self->priv->uploads, GUINT_TO_POINTER (upload_id));
+
+  /* Upload status notification for an upload started by someone else, ignore */
+  if (ss == NULL) {
+    return;
+  }
+
+  /* Prevent grl_dleyna_source_upload_destroy() from being called */
+  g_hash_table_steal (self->priv->uploads, GUINT_TO_POINTER (upload_id));
+
+  if (!g_str_equal (upload_status, "COMPLETED")) {
+    error = g_error_new (GRL_CORE_ERROR, GRL_CORE_ERROR_STORE_FAILED,
+                          _("Upload failed, '%s', transferred %"G_GUINT64_FORMAT" bytes of 
%"G_GUINT64_FORMAT" bytes"),
+                          upload_status, length, total);
+    GRL_WARNING ("%s error:%s", G_STRFUNC, error->message);
+  }
+
+  grl_dleyna_source_store_upload_completed (ss, NULL, error);
+}
+
+static void
+grl_dleyna_source_set_server (GrlDleynaSource *source,
+                              GrlDleynaServer *server)
+{
+  GrlDleynaMediaDevice *device;
+
+  GRL_DEBUG (G_STRFUNC);
+
+  g_return_if_fail (source->priv->server == NULL);
+
+  source->priv->server = g_object_ref (server);
+
+  device = grl_dleyna_server_get_media_device (server);
+
+  g_signal_connect_object (device, "notify::search-caps", G_CALLBACK (grl_dleyna_source_update_caps_cb),
+                           source, G_CONNECT_SWAPPED);
+  grl_dleyna_source_update_caps_cb (G_OBJECT (source), NULL, device);
+
+  g_signal_connect_object (device, "upload-update", G_CALLBACK(grl_dleyna_source_store_upload_update_cb),
+                           source, G_CONNECT_SWAPPED);
+}
+
+static void
+properties_add_for_key (GPtrArray *properties,
+                        const GrlKeyID key_id)
+{
+  switch (key_id)
+    {
+    case GRL_METADATA_KEY_ID:
+      g_ptr_array_add (properties, "UDN");
+    case GRL_METADATA_KEY_TITLE:
+      g_ptr_array_add (properties, "DisplayName");
+      break;
+    case GRL_METADATA_KEY_URL:
+      g_ptr_array_add (properties, "URLs");
+      break;
+    case GRL_METADATA_KEY_AUTHOR:
+      g_ptr_array_add (properties, "Creator");
+      break;
+    case GRL_METADATA_KEY_ARTIST:
+      g_ptr_array_add (properties, "Artist");
+      break;
+    case GRL_METADATA_KEY_ALBUM:
+      g_ptr_array_add (properties, "Album");
+      break;
+    case GRL_METADATA_KEY_GENRE:
+      g_ptr_array_add (properties, "Genre");
+      break;
+    case GRL_METADATA_KEY_DURATION:
+      g_ptr_array_add (properties, "Duration");
+      break;
+    case GRL_METADATA_KEY_TRACK_NUMBER:
+      g_ptr_array_add (properties, "TrackNumber");
+      break;
+    case GRL_METADATA_KEY_MIME:
+      g_ptr_array_add (properties, "MIMEType");
+      break;
+    case GRL_METADATA_KEY_WIDTH:
+      g_ptr_array_add (properties, "Width");
+      break;
+    case GRL_METADATA_KEY_HEIGHT:
+      g_ptr_array_add (properties, "Height");
+      break;
+    case GRL_METADATA_KEY_BITRATE:
+      g_ptr_array_add (properties, "Bitrate");
+      break;
+    case GRL_METADATA_KEY_CHILDCOUNT:
+      g_ptr_array_add (properties, "ChildCount");
+      break;
+    case GRL_METADATA_KEY_THUMBNAIL:
+      g_ptr_array_add (properties, "AlbumArtURL");
+      break;
+    case GRL_METADATA_KEY_PUBLICATION_DATE:
+      g_ptr_array_add (properties, "Date");
+      break;
+    default:
+      GRL_WARNING ("%s ignored non-supported key %s", G_STRFUNC, GRL_METADATA_KEY_GET_NAME (key_id));
+    }
+}
+
+static void
+media_set_property (GrlMedia *media,
+                    const gchar *key,
+                    GVariant *value)
+{
+  const gchar *s;
+  gint i;
+
+  if (g_strcmp0 (key, "Path") == 0) {
+    s = g_variant_get_string (value, NULL);
+    grl_dleyna_source_media_set_id_from_object_path (media, s);
+  }
+  else if (g_strcmp0 (key, "DisplayName") == 0) {
+    s = g_variant_get_string (value, NULL);
+    grl_media_set_title (media, s);
+  }
+  else if (g_strcmp0 (key, "URLs") == 0 && g_variant_n_children (value) > 0) {
+    g_variant_get_child (value, 0, "&s", &s);
+    grl_media_set_url (media, s);
+  }
+  else if (g_strcmp0 (key, "MIMEType") == 0) {
+    s = g_variant_get_string (value, NULL);
+    grl_media_set_mime (media, s);
+  }
+  else if (g_strcmp0 (key, "Duration") == 0) {
+    i = g_variant_get_int32 (value);
+    grl_media_set_duration (media, i);
+  }
+  else if (g_strcmp0 (key, "Author") == 0) {
+    s = g_variant_get_string (value, NULL);
+    grl_media_set_author (media, s);
+  }
+  else if (g_strcmp0 (key, "Artist") == 0) {
+    s = g_variant_get_string (value, NULL);
+    if (GRL_IS_MEDIA_AUDIO (media)) {
+      grl_media_audio_set_artist (GRL_MEDIA_AUDIO (media), s);
+    }
+  }
+  else if (g_strcmp0 (key, "Album") == 0) {
+    s = g_variant_get_string (value, NULL);
+    if (GRL_IS_MEDIA_AUDIO (media)) {
+      grl_media_audio_set_album (GRL_MEDIA_AUDIO (media), s);
+    }
+  }
+  else if (g_strcmp0 (key, "Genre") == 0) {
+    s = g_variant_get_string (value, NULL);
+    if (GRL_IS_MEDIA_AUDIO (media)) {
+      grl_media_audio_set_genre (GRL_MEDIA_AUDIO (media), s);
+    }
+  }
+  else if (g_strcmp0 (key, "TrackNumber") == 0) {
+    i = g_variant_get_int32 (value);
+    if (GRL_IS_MEDIA_AUDIO (media)) {
+      grl_media_audio_set_track_number (GRL_MEDIA_AUDIO (media), i);
+    }
+  }
+  else if (g_strcmp0 (key, "ChildCount") == 0) {
+    guint32 count = g_variant_get_uint32 (value);
+    if (GRL_IS_MEDIA_BOX (media)) {
+      grl_media_box_set_childcount (GRL_MEDIA_BOX (media), count);
+    }
+  }
+  else if (g_strcmp0 (key, "Width") == 0) {
+    i = g_variant_get_int32 (value);
+    if (GRL_IS_MEDIA_VIDEO (media)) {
+      grl_media_video_set_width (GRL_MEDIA_VIDEO (media), i);
+    }
+    if (GRL_IS_MEDIA_IMAGE (media)) {
+      grl_media_image_set_width (GRL_MEDIA_IMAGE (media), i);
+    }
+  }
+  else if (g_strcmp0 (key, "Height") == 0) {
+    i = g_variant_get_int32 (value);
+    if (GRL_IS_MEDIA_VIDEO (media)) {
+      grl_media_video_set_height (GRL_MEDIA_VIDEO (media), i);
+    }
+    if (GRL_IS_MEDIA_IMAGE (media)) {
+      grl_media_image_set_height (GRL_MEDIA_IMAGE (media), i);
+    }
+  }
+  else if (g_strcmp0 (key, "Bitrate") == 0) {
+    i = g_variant_get_int32 (value);
+    if (GRL_IS_MEDIA_AUDIO (media)) {
+      grl_media_audio_set_bitrate (GRL_MEDIA_AUDIO (media), i);
+    }
+  }
+  else if (g_strcmp0 (key, "AlbumArtURL") == 0) {
+    s = g_variant_get_string (value, NULL);
+    grl_media_add_thumbnail (media, s);
+  }
+  else if (g_strcmp0 (key, "Date") == 0) {
+    GDate date;
+    s = g_variant_get_string (value, NULL);
+    g_date_set_parse (&date, s);
+    if (g_date_valid (&date)) {
+      GDateTime *datetime;
+      datetime = g_date_time_new_utc (date.year, date.month, date.day, 0, 0, 0);
+      grl_media_set_publication_date (media, datetime);
+      g_date_time_unref (datetime);
+    }
+  }
+}
+
+static GrlMedia *
+populate_media_from_variant (GrlMedia *media,
+                             GVariant *variant)
+{
+  GVariantIter iter;
+  const gchar *key;
+  GVariant *value;
+
+  g_variant_iter_init (&iter, variant);
+  while (g_variant_iter_next (&iter, "{&sv}", &key, &value)) {
+    media_set_property (media, key, value);
+    g_variant_unref (value);
+  }
+
+  return media;
+}
+
+static GrlMedia *
+build_media_from_variant (GVariant *variant)
+{
+  GrlMedia *media;
+  const gchar *type = NULL;
+
+  g_variant_lookup (variant, "Type", "&s", &type);
+
+  if (type == NULL) {
+    media = grl_media_new ();
+  }
+  else if (g_str_has_prefix (type, "container")) {
+    media = grl_media_box_new ();
+  }
+  /* Workaround https://github.com/01org/dleyna-server/issues/101 */
+  else if (g_str_has_prefix (type, "album") ||
+           g_str_has_prefix (type, "person") ||
+           g_str_has_prefix (type, "genre")) {
+    media = grl_media_box_new ();
+  }
+  else if (g_str_has_prefix (type, "audio") ||
+           g_str_has_prefix (type, "music")) {
+    media = grl_media_audio_new ();
+  }
+  else if (g_str_has_prefix (type, "video")) {
+    media = grl_media_video_new ();
+  }
+  else if (g_str_has_prefix (type, "image")) {
+    media = grl_media_image_new ();
+  }
+  else {
+    media = grl_media_new ();
+  }
+
+  populate_media_from_variant (media, variant);
+
+  return media;
+}
+
+static gboolean
+variant_set_property (GVariantBuilder *builder,
+                      GrlMedia *media,
+                      GrlKeyID key_id)
+{
+  gchar *s;
+
+  switch (key_id) {
+    case GRL_METADATA_KEY_TITLE:
+      g_variant_builder_add_parsed (builder, "{'DisplayName', <%s>}",
+                                    grl_media_get_title (media));
+      return TRUE;
+    case GRL_METADATA_KEY_ARTIST:
+      if (GRL_IS_MEDIA_AUDIO (media))
+        g_variant_builder_add_parsed (builder, "{'Artist', <%s>}",
+                                      grl_media_audio_get_artist (GRL_MEDIA_AUDIO (media)));
+      return TRUE;
+    case GRL_METADATA_KEY_ALBUM:
+      if (GRL_IS_MEDIA_AUDIO (media))
+        g_variant_builder_add_parsed (builder, "{'Album', <%s>}",
+                                      grl_media_audio_get_album (GRL_MEDIA_AUDIO (media)));
+      return TRUE;
+    case GRL_METADATA_KEY_GENRE:
+      if (GRL_IS_MEDIA_AUDIO (media))
+        g_variant_builder_add_parsed (builder, "{'Genre', <%s>}",
+                                      grl_media_audio_get_genre (GRL_MEDIA_AUDIO (media)));
+      return TRUE;
+    case GRL_METADATA_KEY_TRACK_NUMBER:
+      if (GRL_IS_MEDIA_AUDIO (media))
+        g_variant_builder_add_parsed (builder, "{'TrackNumber', <%i>}",
+                                      grl_media_audio_get_track_number (GRL_MEDIA_AUDIO (media)));
+      return TRUE;
+    case GRL_METADATA_KEY_AUTHOR:
+      g_variant_builder_add_parsed (builder, "{'Creator', <%s>}",
+                                    grl_media_get_author (media));
+      return TRUE;
+    case GRL_METADATA_KEY_PUBLICATION_DATE:
+      s = g_date_time_format (grl_media_get_publication_date (media), "%F");
+      g_variant_builder_add_parsed (builder, "{'Date', <%s>}", s);
+      g_free (s);
+      return TRUE;
+    default:
+      GRL_WARNING ("%s ignored non-writable key %s", G_STRFUNC, GRL_METADATA_KEY_GET_NAME (key_id));
+      return FALSE;
+    }
+}
+
+static GVariant *
+build_variant_from_media (GrlMedia  *media,
+                          GList     *keys,
+                          GPtrArray *delete_keys)
+{
+  GVariantBuilder *builder;
+  GList *iter;
+
+  builder = g_variant_builder_new (G_VARIANT_TYPE_VARDICT);
+  for (iter = keys; iter != NULL; iter = g_list_next (iter)) {
+    GrlKeyID key = GRLPOINTER_TO_KEYID (iter->data);
+    if (grl_data_has_key (GRL_DATA (media), key)) {
+      variant_set_property (builder, media, key);
+    }
+    else {
+      properties_add_for_key (delete_keys, key);
+    }
+  }
+
+  return g_variant_builder_end (builder);
+}
+
+static void
+grl_dleyna_source_results (GrlSource *source,
+                           GError *error,
+                           gint code,
+                           GVariant *results,
+                           guint operation_id,
+                           GrlSourceResultCb callback,
+                           gpointer user_data)
+{
+  GVariantIter iter;
+  GVariant *item;
+  gsize remaining;
+
+  GRL_DEBUG (G_STRFUNC);
+
+  if (error != NULL) {
+    GRL_WARNING ("%s error:%s", G_STRFUNC, error->message);
+    error = grl_dleyna_source_convert_error (error, code);
+    callback (source, operation_id, NULL, 0, user_data, error);
+    g_error_free (error);
+    return;
+  }
+
+  remaining = g_variant_n_children (results);
+  if (remaining == 0) {
+    GRL_DEBUG ("%s no results", G_STRFUNC);
+    callback (source, operation_id, NULL, 0, user_data, NULL);
+    return;
+  }
+
+  g_variant_iter_init (&iter, results);
+  while ((item = g_variant_iter_next_value (&iter))) {
+    GrlMedia *media;
+    media = build_media_from_variant (item);
+    GRL_DEBUG ("%s %s", G_STRFUNC, grl_media_get_id (media));
+    callback (source, operation_id, media, --remaining, user_data, NULL);
+    g_variant_unref (item);
+  }
+}
+
+static gchar const **
+build_properties_filter (const GList *keys)
+{
+  GPtrArray *filter;
+
+  filter = g_ptr_array_new ();
+  g_ptr_array_add (filter, "Path"); /* always retrieve the items' DBus path */
+  g_ptr_array_add (filter, "Type"); /* and their object type */
+  while (keys != NULL) {
+    properties_add_for_key (filter, GRLPOINTER_TO_KEYID (keys->data));
+    keys = g_list_next (keys);
+  }
+  g_ptr_array_add (filter, NULL); /* nul-terminate the strvector */
+
+  return (gchar const **) g_ptr_array_free (filter, FALSE);
+}
+
+static gchar *
+build_type_filter_query (GrlTypeFilter type_filter)
+{
+  GString *filter;
+  gboolean append_or = FALSE;
+
+  if (type_filter == GRL_TYPE_FILTER_ALL) {
+    return NULL;
+  }
+
+  filter = g_string_new ("( ");
+
+  if (type_filter & GRL_TYPE_FILTER_AUDIO) {
+    filter = g_string_append (filter, DLEYNA_TYPE_FILTER_AUDIO);
+    append_or = TRUE;
+  }
+
+  if (type_filter & GRL_TYPE_FILTER_VIDEO) {
+    if (append_or) {
+      filter = g_string_append (filter, " or ");
+    }
+    filter = g_string_append (filter, DLEYNA_TYPE_FILTER_VIDEO);
+    append_or = TRUE;
+  }
+
+  if (type_filter & GRL_TYPE_FILTER_IMAGE) {
+    if (append_or) {
+      filter = g_string_append (filter, " or ");
+    }
+    filter = g_string_append (filter, DLEYNA_TYPE_FILTER_IMAGE);
+  }
+
+  filter = g_string_append (filter, " )");
+
+  return g_string_free (filter, FALSE);
+}
+
+static gchar *
+build_search_query (GrlTypeFilter types,
+                    const gchar *text)
+{
+  gchar *type_filter, *props_filter, *full_filter;
+
+  type_filter = build_type_filter_query (types);
+
+  if (text != NULL) {
+    props_filter = g_strdup_printf (DLEYNA_SEARCH_SPEC, text, text, text);
+  }
+  else {
+    props_filter = NULL;
+  }
+
+  if (text != NULL && type_filter != NULL) {
+    full_filter = g_strdup_printf ("%s and %s", type_filter, props_filter);
+  }
+  else if (type_filter == NULL) {
+    full_filter = g_strdup (props_filter);
+  }
+  else {
+    full_filter = g_strdup ("*");
+  }
+
+  g_free (type_filter);
+  g_free (props_filter);
+
+  return full_filter;
+}
+
+static gchar *
+build_browse_query (GrlTypeFilter types,
+                    const gchar *container_id)
+{
+  gchar *type_filter, *container_filter, *full_filter;
+
+  g_return_val_if_fail (container_id != NULL, NULL);
+
+  type_filter = build_type_filter_query (types);
+  container_filter = g_strdup_printf (DLEYNA_BROWSE_SPEC, container_id);
+
+  if (type_filter != NULL) {
+    full_filter = g_strdup_printf ("(%s or %s) and %s", DLEYNA_TYPE_FILTER_CONTAINER, type_filter, 
container_filter);
+  }
+  else {
+    full_filter = g_strdup (container_filter);
+  }
+
+  g_free (type_filter);
+  g_free (container_filter);
+
+  return full_filter;
+}
+
+static void
+grl_dleyna_source_resolve_browse_objects_cb (GObject      *object,
+                                             GAsyncResult *res,
+                                             gpointer      user_data)
+{
+  GrlDleynaMediaDevice *device = GRL_DLEYNA_MEDIA_DEVICE (object);
+  GrlSourceResolveSpec *rs = user_data;
+  GVariant *results, *dict, *item_error;
+  GError *error = NULL;
+
+  GRL_DEBUG (G_STRFUNC);
+  grl_dleyna_media_device_call_browse_objects_finish (device, &results, res, &error);
+
+  if (error != NULL) {
+    GRL_WARNING ("%s error:%s", G_STRFUNC, error->message);
+    error = grl_dleyna_source_convert_error (error, GRL_CORE_ERROR_RESOLVE_FAILED);
+    rs->callback (rs->source, rs->operation_id, rs->media, rs->user_data, error);
+    g_error_free (error);
+    return;
+  }
+
+  dict = g_variant_get_child_value (results, 0);
+
+  /* Handle per-object errors */
+  item_error = g_variant_lookup_value (dict, "Error", G_VARIANT_TYPE_VARDICT);
+  if (item_error != NULL) {
+    gint32 id = 0;
+    gchar *message = NULL;
+
+    g_variant_lookup (item_error, "ID", "i", &id);
+    g_variant_lookup (item_error, "Message", "&s", &message);
+    GRL_WARNING ("%s item error id:%d \"%s\"", G_STRFUNC, id, message);
+    error = g_error_new (GRL_CORE_ERROR, GRL_CORE_ERROR_RESOLVE_FAILED,
+                        _("Failed to retrieve item properties (BrowseObjects error %d: %s)"), id, message);
+    rs->callback (rs->source, rs->operation_id, rs->media, rs->user_data, error);
+    g_error_free (error);
+    return;
+  }
+
+  populate_media_from_variant (rs->media, dict);
+  rs->callback (rs->source, rs->operation_id, rs->media, rs->user_data, NULL);
+}
+
+static void
+grl_dleyna_source_search_search_objects_cb (GObject      *object,
+                                            GAsyncResult *res,
+                                            gpointer      user_data)
+{
+  GrlDleynaMediaContainer2 *container = GRL_DLEYNA_MEDIA_CONTAINER2 (object);
+  GrlSourceSearchSpec *ss = user_data;
+  GVariant *results;
+  GError *error = NULL;
+
+  GRL_DEBUG (G_STRFUNC);
+  grl_dleyna_media_container2_call_search_objects_finish (container, &results, res, &error);
+  grl_dleyna_source_results (ss->source, error, GRL_CORE_ERROR_SEARCH_FAILED, results, ss->operation_id, 
ss->callback, ss->user_data);
+}
+
+static void
+grl_dleyna_source_query_search_objects_cb (GObject      *object,
+                                           GAsyncResult *res,
+                                           gpointer      user_data)
+{
+  GrlDleynaMediaContainer2 *container = GRL_DLEYNA_MEDIA_CONTAINER2 (object);
+  GrlSourceQuerySpec *qs = user_data;
+  GVariant *results;
+  GError *error = NULL;
+
+  GRL_DEBUG (G_STRFUNC);
+  grl_dleyna_media_container2_call_search_objects_finish (container, &results, res, &error);
+  grl_dleyna_source_results (qs->source, error, GRL_CORE_ERROR_QUERY_FAILED, results, qs->operation_id, 
qs->callback, qs->user_data);
+}
+
+static void
+grl_dleyna_source_browse_list_children_cb (GObject      *object,
+                                           GAsyncResult *res,
+                                           gpointer      user_data)
+{
+  GrlDleynaMediaContainer2 *container = GRL_DLEYNA_MEDIA_CONTAINER2 (object);
+  GrlSourceBrowseSpec *bs = user_data;
+  GVariant *children;
+  GError *error = NULL;
+
+  GRL_DEBUG (G_STRFUNC);
+  grl_dleyna_media_container2_call_list_children_finish (container, &children, res, &error);
+  grl_dleyna_source_results (bs->source, error, GRL_CORE_ERROR_BROWSE_FAILED, children, bs->operation_id, 
bs->callback, bs->user_data);
+}
+
+static void
+grl_dleyna_source_browse_search_objects_cb (GObject      *object,
+                                            GAsyncResult *res,
+                                            gpointer      user_data)
+{
+  GrlDleynaMediaContainer2 *container = GRL_DLEYNA_MEDIA_CONTAINER2 (object);
+  GrlSourceBrowseSpec *bs = user_data;
+  GVariant *children;
+  GError *error = NULL;
+
+  GRL_DEBUG (G_STRFUNC);
+  grl_dleyna_media_container2_call_search_objects_finish (container, &children, res, &error);
+  grl_dleyna_source_results (bs->source, error, GRL_CORE_ERROR_BROWSE_FAILED, children, bs->operation_id, 
bs->callback, bs->user_data);
+}
+
+static void
+grl_dleyna_source_store_upload_wait_for_completion (GrlSourceStoreSpec *ss,
+                                                    guint               upload_id,
+                                                    gchar              *object_path,
+                                                    GError             *error)
+{
+  GrlDleynaSource *self = GRL_DLEYNA_SOURCE (ss->source);
+
+  GRL_DEBUG (G_STRFUNC);
+
+  if (error != NULL) {
+    GRL_WARNING ("%s error:%s", G_STRFUNC, error->message);
+    error = grl_dleyna_source_convert_error (error, GRL_CORE_ERROR_STORE_FAILED);
+    ss->callback (ss->source, ss->media, NULL, ss->user_data, error);
+    g_error_free (error);
+    return;
+  }
+
+  g_hash_table_insert (self->priv->uploads, GUINT_TO_POINTER (upload_id), ss);
+
+  grl_dleyna_source_media_set_id_from_object_path (ss->media, object_path);
+  g_free (object_path);
+}
+
+static void
+grl_dleyna_source_store_create_container_in_any_container_cb (GObject      *object,
+                                                              GAsyncResult *res,
+                                                              gpointer      user_data)
+{
+  GrlDleynaMediaDevice *device = GRL_DLEYNA_MEDIA_DEVICE (object);
+  GrlSourceStoreSpec *ss = user_data;
+  gchar *object_path = NULL;
+  GError *error = NULL;
+
+  GRL_DEBUG (G_STRFUNC);
+
+  grl_dleyna_media_device_call_create_container_in_any_container_finish (device, &object_path, res, &error);
+  grl_dleyna_source_store_upload_completed (ss, object_path, error);
+  g_free (object_path);
+}
+
+static void
+grl_dleyna_source_store_upload_to_any_container_cb (GObject      *object,
+                                                    GAsyncResult *res,
+                                                    gpointer      user_data)
+{
+  GrlDleynaMediaDevice *device = GRL_DLEYNA_MEDIA_DEVICE (object);
+  GrlSourceStoreSpec *ss = user_data;
+  gchar *object_path = NULL;
+  guint upload_id;
+  GError *error = NULL;
+
+  GRL_DEBUG (G_STRFUNC);
+
+  grl_dleyna_media_device_call_upload_to_any_container_finish (device, &upload_id, &object_path, res, 
&error);
+  grl_dleyna_source_store_upload_wait_for_completion (ss, upload_id, object_path, error);
+}
+
+static void
+grl_dleyna_source_store_create_container_cb (GObject      *object,
+                                             GAsyncResult *res,
+                                             gpointer      user_data)
+{
+  GrlDleynaMediaContainer2 *container = GRL_DLEYNA_MEDIA_CONTAINER2 (object);
+  GrlSourceStoreSpec *ss = user_data;
+  gchar *object_path = NULL;
+  GError *error = NULL;
+
+  GRL_DEBUG (G_STRFUNC);
+
+  grl_dleyna_media_container2_call_create_container_finish (container, &object_path, res, &error);
+  grl_dleyna_source_store_upload_completed (ss, object_path, error);
+  g_free (object_path);
+}
+
+static void
+grl_dleyna_source_store_upload_cb (GObject      *object,
+                                   GAsyncResult *res,
+                                   gpointer      user_data)
+{
+  GrlDleynaMediaContainer2 *container = GRL_DLEYNA_MEDIA_CONTAINER2 (object);
+  GrlSourceStoreSpec *ss = user_data;
+  gchar *object_path = NULL;
+  guint upload_id;
+  GError *error = NULL;
+
+  GRL_DEBUG (G_STRFUNC);
+
+  grl_dleyna_media_container2_call_upload_finish (container, &upload_id, &object_path, res, &error);
+  grl_dleyna_source_store_upload_wait_for_completion (ss, upload_id, object_path, error);
+}
+
+static void
+grl_dleyna_source_store_metadata_update_cb (GObject      *obj,
+                                            GAsyncResult *res,
+                                            gpointer      user_data)
+{
+  GrlDleynaMediaObject2 *object = GRL_DLEYNA_MEDIA_OBJECT2 (obj);
+  GrlSourceStoreMetadataSpec *sms = user_data;
+  GList *failed_keys;
+  const GList *w;
+  GError *error = NULL;
+
+  GRL_DEBUG ("%s", G_STRFUNC);
+
+  grl_dleyna_media_object2_call_update_finish (object, res, &error);
+
+  if (error != NULL) {
+    GRL_WARNING ("%s error:%s", G_STRFUNC, error->message);
+    error = grl_dleyna_source_convert_error (error, GRL_CORE_ERROR_STORE_FAILED);
+    sms->callback (sms->source, sms->media, NULL, sms->user_data, error);
+    g_error_free (error);
+    return;
+  }
+
+  /* Drop from the set of keys to be stored the writable ones */
+  failed_keys = g_list_copy (sms->keys);
+  for (w = grl_dleyna_source_writable_keys (sms->source); w != NULL; w = g_list_next (w))
+    failed_keys = g_list_remove (failed_keys, w->data);
+
+  sms->callback (sms->source, sms->media, failed_keys, sms->user_data, NULL);
+  g_list_free (failed_keys);
+}
+
+static void
+grl_dleyna_source_remove_delete_cb (GObject      *obj,
+                                    GAsyncResult *res,
+                                    gpointer      user_data)
+{
+  GrlDleynaMediaObject2 *object = GRL_DLEYNA_MEDIA_OBJECT2 (obj);
+  GrlSourceRemoveSpec *rs = user_data;
+  GError *error = NULL;
+
+  GRL_DEBUG ("%s", G_STRFUNC);
+
+  grl_dleyna_media_object2_call_delete_finish (object, res, &error);
+  if (error != NULL) {
+    GRL_WARNING ("%s error:%s", G_STRFUNC, error->message);
+    error = grl_dleyna_source_convert_error (error, GRL_CORE_ERROR_REMOVE_FAILED);
+  }
+
+  rs->callback (rs->source, rs->media, rs->user_data, error);
+  g_clear_error (&error);
+}
+
+static void
+grl_dleyna_source_changed_cb (GrlDleynaSource *self,
+                              GVariant *changes,
+                              gpointer user_data)
+{
+  GPtrArray *changed_medias = NULL;
+  GVariantIter iter;
+  GVariant *change, *next;
+  guint32 change_type, next_change_type;
+  GrlSourceChangeType grl_change_type;
+  gboolean location_unknown;
+
+  GRL_DEBUG (G_STRFUNC);
+
+  g_variant_iter_init (&iter, changes);
+  next = g_variant_iter_next_value (&iter);
+  while (next != NULL) {
+    GrlMedia *media;
+
+    change = next;
+    next = g_variant_iter_next_value (&iter);
+
+    if (!g_variant_lookup (change, "ChangeType", "u", &change_type)) {
+      GRL_WARNING ("Missing mandatory ChangeType property in the Changed(aa{sv}) DBus signal");
+      continue;
+    }
+
+    /* make sure to flush the notification array if the next element
+      * has no ChangeType property (which would be bug, as it is mandatory) */
+    next_change_type = G_MAXUINT;
+    if (next != NULL) {
+      g_variant_lookup (next, "ChangeType", "u", &next_change_type);
+    }
+
+    switch ((DleynaChangeType)change_type) {
+      case DLEYNA_CHANGE_TYPE_ADD:
+        grl_change_type = GRL_CONTENT_ADDED;
+        location_unknown = FALSE;
+        break;
+      case DLEYNA_CHANGE_TYPE_MOD:
+        grl_change_type = GRL_CONTENT_CHANGED;
+        location_unknown = FALSE;
+        break;
+      case DLEYNA_CHANGE_TYPE_DEL:
+        grl_change_type = GRL_CONTENT_REMOVED;
+        location_unknown = FALSE;
+        break;
+      case DLEYNA_CHANGE_TYPE_CONTAINER:
+        grl_change_type = GRL_CONTENT_CHANGED;
+        location_unknown = TRUE;
+        break;
+      case DLEYNA_CHANGE_TYPE_DONE:
+        continue;
+      default:
+        GRL_WARNING ("%s ignore change type %d", G_STRFUNC, change_type);
+        continue;
+    }
+
+    if (changed_medias == NULL) {
+      changed_medias = g_ptr_array_new ();
+    }
+
+    media = build_media_from_variant (change);
+    g_ptr_array_add (changed_medias, media);
+
+    /* Flush the notifications when reaching the last element or when the
+      * next one has a different change type */
+    if (next == NULL || next_change_type != change_type) {
+      grl_source_notify_change_list (GRL_SOURCE (self), changed_medias, grl_change_type, location_unknown);
+      changed_medias = NULL;
+    }
+  }
+}
+
+/* ================== Internal functions ================== */
+
+gchar *
+grl_dleyna_source_build_id (const gchar *udn)
+{
+  return g_strdup_printf (SOURCE_ID_TEMPLATE, udn);
+}
+
+GrlDleynaSource *
+grl_dleyna_source_new (GrlDleynaServer *server)
+{
+  GrlDleynaSource *source;
+  GrlDleynaMediaDevice *device;
+  const gchar *friendly_name, *udn, *icon_url;
+  gchar *id, *desc;
+  GIcon *icon = NULL;
+
+  GRL_DEBUG (G_STRFUNC);
+
+  device = grl_dleyna_server_get_media_device (server);
+
+  friendly_name = grl_dleyna_media_device_get_friendly_name (device);
+  udn = grl_dleyna_media_device_get_udn (device);
+  id = grl_dleyna_source_build_id (udn);
+  desc = g_strdup_printf (SOURCE_DESC_TEMPLATE, friendly_name);
+
+  icon_url = grl_dleyna_media_device_get_icon_url (device);
+  if (icon_url != NULL) {
+    GFile *file;
+    file = g_file_new_for_uri (icon_url);
+    icon = g_file_icon_new (file);
+    g_object_unref (file);
+  }
+
+  source = g_object_new (GRL_DLEYNA_SOURCE_TYPE, "server", server, "source-id", id,
+                         "source-name", friendly_name, "source-desc", desc,
+                         "source-icon", icon, NULL);
+  g_free (id);
+  g_free (desc);
+
+  return source;
+}
+
+/* ================== Grilo API implementation ================== */
+
+static GrlCaps *
+grl_dleyna_source_get_caps (GrlSource *source,
+                            GrlSupportedOps operation)
+{
+  static GrlCaps *caps = NULL;
+  static GrlCaps *caps_browse = NULL;
+
+  if (caps == NULL) {
+    caps = grl_caps_new ();
+    if (GRL_DLEYNA_SOURCE (source)->priv->search_enabled) {
+      grl_caps_set_type_filter (caps, GRL_TYPE_FILTER_ALL);
+    }
+  }
+
+  if (caps_browse == NULL) {
+    caps_browse = grl_caps_new ();
+    if (GRL_DLEYNA_SOURCE (source)->priv->browse_filtered_enabled) {
+      grl_caps_set_type_filter (caps_browse, GRL_TYPE_FILTER_ALL);
+    }
+  }
+
+  return (operation == GRL_OP_BROWSE) ? caps_browse: caps;
+}
+
+static const GList *
+grl_dleyna_source_supported_keys (GrlSource *source)
+{
+  static GList *keys = NULL;
+
+  if (keys == NULL) {
+    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_DURATION,
+                                      GRL_METADATA_KEY_ARTIST,
+                                      GRL_METADATA_KEY_ALBUM,
+                                      GRL_METADATA_KEY_GENRE,
+                                      GRL_METADATA_KEY_CHILDCOUNT,
+                                      GRL_METADATA_KEY_THUMBNAIL,
+                                      GRL_METADATA_KEY_TRACK_NUMBER,
+                                      GRL_METADATA_KEY_AUTHOR,
+                                      GRL_METADATA_KEY_WIDTH,
+                                      GRL_METADATA_KEY_HEIGHT,
+                                      GRL_METADATA_KEY_BITRATE,
+                                      GRL_METADATA_KEY_PUBLICATION_DATE,
+                                      NULL);
+  }
+
+  return keys;
+}
+
+static const GList *
+grl_dleyna_source_writable_keys (GrlSource *source)
+{
+  static GList *keys = NULL;
+
+  if (keys == NULL) {
+    keys = grl_metadata_key_list_new (GRL_METADATA_KEY_TITLE,
+                                      GRL_METADATA_KEY_ARTIST,
+                                      GRL_METADATA_KEY_ALBUM,
+                                      GRL_METADATA_KEY_GENRE,
+                                      GRL_METADATA_KEY_TRACK_NUMBER,
+                                      GRL_METADATA_KEY_AUTHOR,
+                                      GRL_METADATA_KEY_PUBLICATION_DATE,
+                                      NULL);
+  }
+
+  return keys;
+}
+
+static GrlSupportedOps
+grl_dleyna_source_supported_operations (GrlSource *source)
+{
+  GrlDleynaSource *self = GRL_DLEYNA_SOURCE (source);
+  GrlSupportedOps caps;
+
+  caps = GRL_OP_BROWSE | GRL_OP_RESOLVE | GRL_OP_STORE | GRL_OP_STORE_PARENT |
+         GRL_OP_STORE_METADATA | GRL_OP_REMOVE | GRL_OP_NOTIFY_CHANGE;
+  if (self->priv->search_enabled) {
+    caps = caps | GRL_OP_SEARCH | GRL_OP_QUERY;
+  }
+
+  return caps;
+}
+
+static void
+grl_dleyna_source_resolve (GrlSource *source,
+                           GrlSourceResolveSpec *rs)
+{
+  GrlDleynaSource *self = GRL_DLEYNA_SOURCE (source);
+  GrlDleynaMediaDevice *device;
+  GCancellable *cancellable;
+  GPtrArray *filter;
+  GList *iter;
+  gchar const *media_id;
+  gchar const *object_path;
+  gchar const *object_paths[] = { NULL, NULL };
+
+  GRL_DEBUG (G_STRFUNC);
+
+  media_id = grl_media_get_id (rs->media);
+
+  /* assume root container if no id has been specified */
+  if (media_id == NULL) {
+    GrlDleynaMediaContainer2 *root;
+    root = grl_dleyna_server_get_media_container (self->priv->server);
+    object_path = g_dbus_proxy_get_object_path (G_DBUS_PROXY (root));
+    grl_dleyna_source_media_set_id_from_object_path (rs->media, object_path);
+  }
+
+  device = grl_dleyna_server_get_media_device (self->priv->server);
+  object_path = grl_dleyna_source_media_get_object_path (rs->media);
+  object_paths[0] = object_path;
+
+  /* discard media from different servers */
+  if (!g_str_has_prefix (object_path, grl_dleyna_server_get_object_path (self->priv->server))) {
+    rs->callback (rs->source, rs->operation_id, rs->media, rs->user_data, NULL);
+    return;
+  }
+
+  cancellable = g_cancellable_new ();
+  grl_operation_set_data_full (rs->operation_id, cancellable, g_object_unref);
+
+  filter = g_ptr_array_new ();
+  for (iter = rs->keys; iter != NULL; iter = g_list_next (iter)) {
+    properties_add_for_key (filter, GRLPOINTER_TO_KEYID (iter->data));
+  }
+  g_ptr_array_add (filter, NULL); /* nul-terminate the strvector */
+
+  grl_dleyna_media_device_call_browse_objects (device, object_paths,
+                                               (const gchar * const*)filter->pdata, cancellable,
+                                               grl_dleyna_source_resolve_browse_objects_cb, rs);
+  g_ptr_array_unref (filter);
+}
+
+static void
+grl_dleyna_source_browse (GrlSource *source,
+                          GrlSourceBrowseSpec *bs)
+{
+  GrlDleynaSource *self = GRL_DLEYNA_SOURCE (source);
+  GrlDleynaMediaContainer2 *container, *root;
+  GDBusConnection *connection;
+  GCancellable *cancellable;
+  GrlTypeFilter type_filter;
+  gchar const *object_path;
+  gchar const **filter;
+  guint offset, limit;
+  GError *error = NULL;
+
+  GRL_DEBUG (G_STRFUNC);
+
+  cancellable = g_cancellable_new ();
+  grl_operation_set_data_full (bs->operation_id, cancellable, g_object_unref);
+
+  root = grl_dleyna_server_get_media_container (self->priv->server);
+  connection = g_dbus_proxy_get_connection (G_DBUS_PROXY (root));
+  filter = build_properties_filter (bs->keys);
+  offset = grl_operation_options_get_skip (bs->options);
+  /* Grilo uses -1 to say "no limit" while dLeyna expect 0 for the same purpose */
+  limit = MAX (0, grl_operation_options_get_count (bs->options));
+
+  object_path = grl_dleyna_source_media_get_object_path (bs->container);
+  if (object_path == NULL) {
+    object_path = g_dbus_proxy_get_object_path (G_DBUS_PROXY (root));
+  }
+
+  /* This does not block as we don't load properties nor connect to signals*/
+  container = grl_dleyna_media_container2_proxy_new_sync (connection,
+                                                          G_DBUS_PROXY_FLAGS_DO_NOT_LOAD_PROPERTIES |
+                                                          G_DBUS_PROXY_FLAGS_DO_NOT_CONNECT_SIGNALS,
+                                                          DLEYNA_DBUS_NAME, object_path, cancellable, 
&error);
+  if (error != NULL) {
+    grl_dleyna_source_results (bs->source, error, GRL_CORE_ERROR_BROWSE_FAILED, NULL, bs->operation_id, 
bs->callback, bs->user_data);
+    goto out;
+  }
+
+  /* invoke SearchObjects instead of ListChildren if we need to filter by type */
+  type_filter = grl_operation_options_get_type_filter (bs->options);
+  if (type_filter != GRL_TYPE_FILTER_ALL) {
+    gchar *query;
+
+    query = build_browse_query (type_filter, object_path);
+    GRL_DEBUG ("%s browse:%s", G_STRFUNC, query);
+    grl_dleyna_media_container2_call_search_objects (container, query, offset, limit, filter, cancellable,
+                                                     grl_dleyna_source_browse_search_objects_cb, bs);
+    g_free (query);
+  }
+  else {
+    grl_dleyna_media_container2_call_list_children (container, offset, limit, filter, cancellable,
+                                                    grl_dleyna_source_browse_list_children_cb, bs);
+  }
+
+out:
+  g_object_unref (container);
+  g_free (filter);
+}
+
+static void
+grl_dleyna_source_search (GrlSource *source,
+                          GrlSourceSearchSpec *ss)
+{
+  GrlDleynaSource *self = GRL_DLEYNA_SOURCE (source);
+  GrlDleynaMediaContainer2 *root;
+  GCancellable *cancellable;
+  gchar const **filter;
+  gchar *query;
+  guint skip, count;
+
+  GRL_DEBUG (G_STRFUNC);
+
+  cancellable = g_cancellable_new ();
+  grl_operation_set_data_full (ss->operation_id, cancellable, g_object_unref);
+
+  skip = grl_operation_options_get_skip (ss->options);
+  /* Grilo uses -1 to say "no limit" while dLeyna expect 0 for the same purpose */
+  count = MAX (0, grl_operation_options_get_count (ss->options));
+  filter = build_properties_filter (ss->keys);
+  query = build_search_query (grl_operation_options_get_type_filter (ss->options), ss->text);
+
+  GRL_DEBUG ("%s query:'%s'", G_STRFUNC, query);
+  root = grl_dleyna_server_get_media_container (self->priv->server);
+  grl_dleyna_media_container2_call_search_objects (root, query, skip, count, filter,
+                                                   cancellable, grl_dleyna_source_search_search_objects_cb, 
ss);
+  g_free (filter);
+  g_free (query);
+}
+
+/*
+ * Query format is the org.gnome.UPnP.MediaContainer2.SearchObjects search
+ * criteria format, e.g.
+ * 'Artist contains "Rick Astley" and
+ *  (Type derivedfrom "audio") or (Type derivedfrom "music")'
+ *
+ * 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_dleyna_source_query (GrlSource *source,
+                         GrlSourceQuerySpec *qs)
+{
+  GrlDleynaSource *self = GRL_DLEYNA_SOURCE (source);
+  GrlDleynaMediaContainer2 *root;
+  GCancellable *cancellable;
+  gchar const **filter;
+  guint skip, count;
+
+  GRL_DEBUG (G_STRFUNC);
+
+  cancellable = g_cancellable_new ();
+  grl_operation_set_data_full (qs->operation_id, cancellable, g_object_unref);
+
+  skip = grl_operation_options_get_skip (qs->options);
+  /* Grilo uses -1 to say "no limit" while dLeyna expect 0 for the same purpose */
+  count = MAX (0, grl_operation_options_get_count (qs->options));
+  filter = build_properties_filter (qs->keys);
+  root = grl_dleyna_server_get_media_container (self->priv->server);
+  grl_dleyna_media_container2_call_search_objects (root, qs->query, skip, count, filter,
+                                                   cancellable, grl_dleyna_source_query_search_objects_cb, 
qs);
+  g_free (filter);
+}
+
+static void
+grl_dleyna_source_store (GrlSource *source,
+                         GrlSourceStoreSpec *ss)
+{
+  GrlDleynaSource *self = GRL_DLEYNA_SOURCE (source);
+  GrlDleynaMediaContainer2 *container;
+  GrlDleynaMediaDevice *device;
+  const gchar *container_object_path;
+  const gchar *url;
+  const gchar * const child_types[] = { "*", NULL };
+  gchar *title = NULL;
+  gchar *filename = NULL;
+  GError *error = NULL;
+
+  GRL_DEBUG (G_STRFUNC);
+
+  title = g_strdup (grl_media_get_title (ss->media));
+
+  if (!GRL_IS_MEDIA_BOX (ss->media)) {
+    url = grl_media_get_url (ss->media);
+    if (url == NULL) {
+      error = g_error_new (GRL_CORE_ERROR, GRL_CORE_ERROR_STORE_FAILED,
+                          _("Upload failed, URL missing on the media object to be transferred"));
+      GRL_WARNING ("%s error:%s", G_STRFUNC, error->message);
+      ss->callback (ss->source, ss->media, NULL, ss->user_data, error);
+      goto out;
+    }
+
+    filename = g_filename_from_uri (url, NULL, &error);
+    if (error != NULL) {
+      GRL_WARNING ("%s error:%s", G_STRFUNC, error->message);
+      error = grl_dleyna_source_convert_error (error, GRL_CORE_ERROR_STORE_FAILED);
+      ss->callback (ss->source, ss->media, NULL, ss->user_data, error);
+      goto out;
+    }
+
+    if (title == NULL) {
+      title = g_path_get_basename (filename);
+    }
+  }
+
+  device = grl_dleyna_server_get_media_device (self->priv->server);
+  container_object_path = grl_dleyna_source_media_get_object_path (GRL_MEDIA (ss->parent));
+  if (container_object_path == NULL) {
+    /* If no container has been explicitly requested, let the DMS choose the
+      * appropriate storage location automatically */
+    if (GRL_IS_MEDIA_BOX (ss->media)) {
+      grl_dleyna_media_device_call_create_container_in_any_container (device, title, "container", 
child_types, NULL,
+                                                                      
grl_dleyna_source_store_create_container_in_any_container_cb, ss);
+    }
+    else {
+      grl_dleyna_media_device_call_upload_to_any_container (device, title, filename, NULL,
+                                                            
grl_dleyna_source_store_upload_to_any_container_cb, ss);
+    }
+  }
+  else {
+    GDBusConnection *connection;
+
+    connection = g_dbus_proxy_get_connection (G_DBUS_PROXY (device));
+    /* This does not block as we don't load properties nor connect to signals*/
+    container = grl_dleyna_media_container2_proxy_new_sync (connection,
+                                                            G_DBUS_PROXY_FLAGS_DO_NOT_LOAD_PROPERTIES |
+                                                            G_DBUS_PROXY_FLAGS_DO_NOT_CONNECT_SIGNALS,
+                                                            DLEYNA_DBUS_NAME, container_object_path, NULL,
+                                                            &error);
+    if (error != NULL) {
+      GRL_WARNING ("%s error:%s", G_STRFUNC, error->message);
+      error = grl_dleyna_source_convert_error (error, GRL_CORE_ERROR_STORE_FAILED);
+      ss->callback (ss->source, ss->media, NULL, ss->user_data, error);
+      goto out;
+    }
+
+    if (GRL_IS_MEDIA_BOX (ss->media)) {
+      grl_dleyna_media_container2_call_create_container (container, title, "container", child_types, NULL,
+                                                         grl_dleyna_source_store_create_container_cb, ss);
+    }
+    else {
+      grl_dleyna_media_container2_call_upload (container, title, filename, NULL,
+                                               grl_dleyna_source_store_upload_cb, ss);
+    }
+    g_object_unref (container);
+  }
+
+out:
+  g_clear_error (&error);
+  g_free (title);
+  g_free (filename);
+}
+
+static void
+grl_dleyna_source_store_metadata (GrlSource *source,
+                                  GrlSourceStoreMetadataSpec *sms)
+{
+  GrlDleynaSource *self = GRL_DLEYNA_SOURCE (source);
+  GrlDleynaMediaDevice *device;
+  GrlDleynaMediaObject2 *object;
+  GDBusConnection *connection;
+  GVariant *metadata;
+  GPtrArray *delete_keys;
+  const gchar *object_path;
+  GError *error = NULL;
+
+  GRL_DEBUG ("%s", G_STRFUNC);
+
+  device = grl_dleyna_server_get_media_device (self->priv->server);
+  connection = g_dbus_proxy_get_connection (G_DBUS_PROXY (device));
+  object_path = grl_dleyna_source_media_get_object_path (sms->media);
+
+  /* This does not block as we don't load properties nor connect to signals*/
+  object = grl_dleyna_media_object2_proxy_new_sync (connection,
+                                                    G_DBUS_PROXY_FLAGS_DO_NOT_LOAD_PROPERTIES |
+                                                    G_DBUS_PROXY_FLAGS_DO_NOT_CONNECT_SIGNALS,
+                                                    DLEYNA_DBUS_NAME, object_path, NULL, &error);
+  if (error != NULL) {
+    GRL_WARNING ("%s error:%s", G_STRFUNC, error->message);
+    error = grl_dleyna_source_convert_error (error, GRL_CORE_ERROR_STORE_METADATA_FAILED);
+    sms->callback (sms->source, sms->media, NULL, sms->user_data, error);
+    goto out;
+  }
+
+  delete_keys = g_ptr_array_new_with_free_func (g_free);
+  metadata = build_variant_from_media (sms->media, sms->keys, delete_keys);
+  g_ptr_array_add (delete_keys, NULL); /* Make sure it is NULL-terminated */
+  grl_dleyna_media_object2_call_update (object, metadata, (const gchar * const *) delete_keys->pdata,
+                                        NULL, grl_dleyna_source_store_metadata_update_cb, sms);
+  g_ptr_array_unref (delete_keys);
+
+out:
+  g_clear_error (&error);
+  g_object_unref (object);
+}
+
+static void
+grl_dleyna_source_remove (GrlSource *source,
+                          GrlSourceRemoveSpec *rs)
+{
+  GrlDleynaSource *self = GRL_DLEYNA_SOURCE (source);
+  GrlDleynaMediaDevice *device;
+  GrlDleynaMediaObject2 *object;
+  GDBusConnection *connection;
+  const gchar *object_path;
+  GError *error = NULL;
+
+  GRL_DEBUG ("%s", G_STRFUNC);
+
+  device = grl_dleyna_server_get_media_device (self->priv->server);
+  connection = g_dbus_proxy_get_connection (G_DBUS_PROXY (device));
+  object_path = grl_dleyna_source_media_get_object_path_from_id (rs->media_id);
+
+  /* This does not block as we don't load properties nor connect to signals*/
+  object = grl_dleyna_media_object2_proxy_new_sync (connection,
+                                                    G_DBUS_PROXY_FLAGS_DO_NOT_LOAD_PROPERTIES |
+                                                    G_DBUS_PROXY_FLAGS_DO_NOT_CONNECT_SIGNALS,
+                                                    DLEYNA_DBUS_NAME, object_path, NULL, &error);
+  if (error != NULL) {
+    GRL_WARNING ("%s error:%s", G_STRFUNC, error->message);
+    error = grl_dleyna_source_convert_error (error, GRL_CORE_ERROR_REMOVE_FAILED);
+    rs->callback (rs->source, rs->media, rs->user_data, error);
+    g_error_free (error);
+    return;
+  }
+
+  grl_dleyna_media_object2_call_delete (object, NULL, grl_dleyna_source_remove_delete_cb, rs);
+  g_object_unref (object);
+}
+
+static void
+grl_dleyna_source_cancel (GrlSource *source,
+                          guint operation_id)
+{
+  GCancellable *cancellable;
+
+  GRL_DEBUG ( G_STRFUNC);
+
+  cancellable = grl_operation_get_data (operation_id);
+  if (cancellable != NULL) {
+    g_cancellable_cancel (cancellable);
+  }
+}
+
+static gboolean
+grl_dleyna_source_notify_change_start (GrlSource *source,
+                                       GError **error)
+{
+  GrlDleynaSource *self = GRL_DLEYNA_SOURCE (source);
+  GrlDleynaMediaDevice *device;
+
+  GRL_DEBUG (G_STRFUNC);
+  device = grl_dleyna_server_get_media_device (self->priv->server);
+  g_signal_connect_object (device, "changed", G_CALLBACK (grl_dleyna_source_changed_cb),
+                           self, G_CONNECT_SWAPPED);
+
+  return TRUE;
+}
+
+static gboolean
+grl_dleyna_source_notify_change_stop (GrlSource *source,
+                                      GError **error)
+{
+  GrlDleynaSource *self = GRL_DLEYNA_SOURCE (source);
+  GrlDleynaMediaDevice *device;
+
+  GRL_DEBUG (G_STRFUNC);
+  device = grl_dleyna_server_get_media_device (self->priv->server);
+  g_signal_handlers_disconnect_by_func (device, grl_dleyna_source_changed_cb, self);
+
+  return TRUE;
+}
diff --git a/src/dleyna/grl-dleyna-source.h b/src/dleyna/grl-dleyna-source.h
new file mode 100644
index 0000000..e8f353a
--- /dev/null
+++ b/src/dleyna/grl-dleyna-source.h
@@ -0,0 +1,75 @@
+/*
+ * Copyright (C) 2010 Igalia S.L.
+ * Copyright (C) 2013 Intel Corp.
+ *
+ * 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_DLEYNA_SOURCE_H_
+#define _GRL_DLEYNA_SOURCE_H_
+
+#include <grilo.h>
+
+#include "grl-dleyna-server.h"
+
+#define GRL_DLEYNA_SOURCE_TYPE                          \
+  (grl_dleyna_source_get_type ())
+
+#define GRL_DLEYNA_SOURCE(obj)                          \
+  (G_TYPE_CHECK_INSTANCE_CAST ((obj),                   \
+                               GRL_DLEYNA_SOURCE_TYPE,  \
+                               GrlDleynaSource))
+
+#define GRL_IS_DLEYNA_SOURCE(obj)                       \
+  (G_TYPE_CHECK_INSTANCE_TYPE ((obj),                   \
+                               GRL_DLEYNA_SOURCE_TYPE))
+
+#define GRL_DLEYNA_SOURCE_CLASS(klass)                  \
+  (G_TYPE_CHECK_CLASS_CAST((klass),                     \
+                           GRL_DLEYNA_SOURCE_TYPE,      \
+                           GrlDleynaSourceClass))
+
+#define GRL_IS_DLEYNA_SOURCE_CLASS(klass)               \
+  (G_TYPE_CHECK_CLASS_TYPE((klass),                     \
+                           GRL_DLEYNA_SOURCE_TYPE))
+
+#define GRL_DLEYNA_SOURCE_GET_CLASS(obj)                \
+  (G_TYPE_INSTANCE_GET_CLASS ((obj),                    \
+                              GRL_DLEYNA_SOURCE_TYPE,   \
+                              GrlDleynaSourceClass))
+
+typedef struct _GrlDleynaSourcePrivate GrlDleynaSourcePrivate;
+typedef struct _GrlDleynaSource GrlDleynaSource;
+
+struct _GrlDleynaSource {
+  GrlSource parent;
+
+  /*< private >*/
+  GrlDleynaSourcePrivate *priv;
+};
+
+typedef struct _GrlDleynaSourceClass GrlDleynaSourceClass;
+
+struct _GrlDleynaSourceClass {
+  GrlSourceClass parent_class;
+};
+
+GType             grl_dleyna_source_get_type (void);
+gchar *           grl_dleyna_source_build_id (const gchar *udn);
+GrlDleynaSource * grl_dleyna_source_new      (GrlDleynaServer *server);
+
+#endif /* _GRL_DLEYNA_SOURCE_H_ */
diff --git a/src/dleyna/grl-dleyna.c b/src/dleyna/grl-dleyna.c
new file mode 100644
index 0000000..3af3b56
--- /dev/null
+++ b/src/dleyna/grl-dleyna.c
@@ -0,0 +1,137 @@
+/*
+ * Copyright (C) 2010, 2011 Igalia S.L.
+ * Copyright (C) 2011 Intel Corporation.
+ * Copyright (C) 2013 Intel Corporation.
+ *
+ * This component is based on the grl-upnp source code.
+ *
+ * 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 "config.h"
+
+#include <grilo.h>
+#include <glib/gi18n-lib.h>
+
+#include "grl-dleyna-source.h"
+#include "grl-dleyna-servers-manager.h"
+
+#define GRL_LOG_DOMAIN_DEFAULT dleyna_log_domain
+GRL_LOG_DOMAIN(dleyna_log_domain);
+
+#define PLUGIN_ID   DLEYNA_PLUGIN_ID
+
+/* Globals */
+static GrlDleynaServersManager *servers = NULL;
+
+
+static void
+server_found_cb (GrlDleynaServersManager *serversmgr,
+                 GrlDleynaServer         *server,
+                 gpointer                *user_data)
+{
+  GrlPlugin *plugin = GRL_PLUGIN (user_data);
+  GrlDleynaMediaDevice *device;
+  GrlSource *source;
+  GrlRegistry *registry;
+  GError *error = NULL;
+
+  GRL_DEBUG (G_STRFUNC);
+  device = grl_dleyna_server_get_media_device (server);
+  GRL_DEBUG ("%s udn: %s ", G_STRFUNC, grl_dleyna_media_device_get_udn (device));
+
+  registry = grl_registry_get_default ();
+
+  source = GRL_SOURCE (grl_dleyna_source_new (server));
+  GRL_DEBUG ("%s id: %s ", G_STRFUNC, grl_source_get_id (source));
+  grl_registry_register_source (registry, plugin, GRL_SOURCE (source), &error);
+
+  if (error != NULL) {
+    GRL_WARNING ("Failed to register source for DLNA device %s: %s",
+                 grl_dleyna_media_device_get_udn (device), error->message);
+    g_error_free (error);
+  }
+}
+
+static void
+server_lost_cb (GrlDleynaServersManager *serversmgr,
+                GrlDleynaServer         *server,
+                gpointer                *user_data)
+{
+  GrlDleynaMediaDevice *device;
+  GrlSource *source;
+  GrlRegistry *registry;
+  const gchar* udn;
+  gchar *source_id;
+
+  GRL_DEBUG (G_STRFUNC);
+  device = grl_dleyna_server_get_media_device (server);
+  udn = grl_dleyna_media_device_get_udn (device);
+  GRL_DEBUG ("%s udn: %s ", G_STRFUNC, udn);
+
+  registry = grl_registry_get_default ();
+  source_id = grl_dleyna_source_build_id (udn);
+
+  GRL_DEBUG ("%s id: %s ", G_STRFUNC, source_id);
+
+  source = grl_registry_lookup_source (registry, source_id);
+  if (source != NULL) {
+    GError *error = NULL;
+    GRL_DEBUG ("%s unregistered %s", G_STRFUNC, source_id);
+    grl_registry_unregister_source (registry, source, &error);
+    if (error != NULL) {
+      GRL_WARNING ("Failed to unregister source %s: %s", udn, error->message);
+      g_error_free (error);
+    }
+  }
+
+  g_free (source_id);
+}
+
+static gboolean
+grl_dleyna_plugin_init (GrlRegistry *registry,
+                        GrlPlugin *plugin,
+                        GList *configs)
+{
+  GRL_LOG_DOMAIN_INIT (dleyna_log_domain, "dleyna");
+
+  GRL_DEBUG (G_STRFUNC);
+
+  /* Initialize i18n */
+  bindtextdomain (GETTEXT_PACKAGE, LOCALEDIR);
+  bind_textdomain_codeset (GETTEXT_PACKAGE, "UTF-8");
+
+  servers = grl_dleyna_servers_manager_dup_singleton ();
+  g_signal_connect_object (servers, "server-found", G_CALLBACK (server_found_cb), plugin, 0);
+  g_signal_connect_object (servers, "server-lost", G_CALLBACK (server_lost_cb), plugin, 0);
+
+  /* Not immensely useful, since most of the errors will be detected only when
+   * the underlying async DBus calls will complete. */
+  return grl_dleyna_servers_manager_is_available ();
+}
+
+static void
+grl_dleyna_plugin_deinit (GrlPlugin *plugin)
+{
+  GRL_DEBUG (G_STRFUNC);
+
+  g_clear_object (&servers);
+}
+
+GRL_PLUGIN_REGISTER (grl_dleyna_plugin_init,
+                     grl_dleyna_plugin_deinit,
+                     PLUGIN_ID);
diff --git a/src/dleyna/grl-dleyna.xml b/src/dleyna/grl-dleyna.xml
new file mode 100644
index 0000000..5c50019
--- /dev/null
+++ b/src/dleyna/grl-dleyna.xml
@@ -0,0 +1,10 @@
+<plugin>
+  <info>
+    <name>dLeyna</name>
+    <module>libgrldleyna</module>
+    <description>A plugin for browsing DLNA servers</description>
+    <author>Intel Corp.</author>
+    <license>LGPL</license>
+    <site>https://01.org/dleyna</site>
+  </info>
+</plugin>
diff --git a/src/dleyna/org.gnome.UPnP.MediaServer2.xml b/src/dleyna/org.gnome.UPnP.MediaServer2.xml
new file mode 100644
index 0000000..a08b096
--- /dev/null
+++ b/src/dleyna/org.gnome.UPnP.MediaServer2.xml
@@ -0,0 +1,200 @@
+<!DOCTYPE node PUBLIC "-//freedesktop//DTD D-BUS Object Introspection 1.0//EN"
+                      "http://www.freedesktop.org/standards/dbus/1.0/introspect.dtd";>
+<node>
+  <interface name="org.gnome.UPnP.MediaObject2">
+    <method name="Delete">
+    </method>
+    <method name="Update">
+      <arg type="a{sv}" name="ToAddUpdate" direction="in">
+      </arg>
+      <arg type="as" name="ToDelete" direction="in">
+      </arg>
+    </method>
+    <method name="GetMetaData">
+      <arg type="s" name="MetaData" direction="out">
+      </arg>
+    </method>
+    <property type="o" name="Parent" access="read">
+    </property>
+    <property type="s" name="Type" access="read">
+    </property>
+    <property type="o" name="Path" access="read">
+    </property>
+    <property type="s" name="DisplayName" access="read">
+    </property>
+    <property type="s" name="Creator" access="read">
+    </property>
+    <property type="b" name="Restricted" access="read">
+    </property>
+    <property type="a{sb}" name="DLNAManaged" access="read">
+    </property>
+    <property type="u" name="ObjectUpdateID" access="read">
+    </property>
+  </interface>
+  <interface name="org.gnome.UPnP.MediaContainer2">
+    <method name="ListChildren">
+      <arg type="u" name="Offset" direction="in">
+      </arg>
+      <arg type="u" name="Max" direction="in">
+      </arg>
+      <arg type="as" name="Filter" direction="in">
+      </arg>
+      <arg type="aa{sv}" name="Children" direction="out">
+      </arg>
+    </method>
+    <method name="ListChildrenEx">
+      <arg type="u" name="Offset" direction="in">
+      </arg>
+      <arg type="u" name="Max" direction="in">
+      </arg>
+      <arg type="as" name="Filter" direction="in">
+      </arg>
+      <arg type="s" name="SortBy" direction="in">
+      </arg>
+      <arg type="aa{sv}" name="Children" direction="out">
+      </arg>
+    </method>
+    <method name="ListContainers">
+      <arg type="u" name="Offset" direction="in">
+      </arg>
+      <arg type="u" name="Max" direction="in">
+      </arg>
+      <arg type="as" name="Filter" direction="in">
+      </arg>
+      <arg type="aa{sv}" name="Children" direction="out">
+      </arg>
+    </method>
+    <method name="ListContainersEx">
+      <arg type="u" name="Offset" direction="in">
+      </arg>
+      <arg type="u" name="Max" direction="in">
+      </arg>
+      <arg type="as" name="Filter" direction="in">
+      </arg>
+      <arg type="s" name="SortBy" direction="in">
+      </arg>
+      <arg type="aa{sv}" name="Children" direction="out">
+      </arg>
+    </method>
+    <method name="ListItems">
+      <arg type="u" name="Offset" direction="in">
+      </arg>
+      <arg type="u" name="Max" direction="in">
+      </arg>
+      <arg type="as" name="Filter" direction="in">
+      </arg>
+      <arg type="aa{sv}" name="Children" direction="out">
+      </arg>
+    </method>
+    <method name="ListItemsEx">
+      <arg type="u" name="Offset" direction="in">
+      </arg>
+      <arg type="u" name="Max" direction="in">
+      </arg>
+      <arg type="as" name="Filter" direction="in">
+      </arg>
+      <arg type="s" name="SortBy" direction="in">
+      </arg>
+      <arg type="aa{sv}" name="Children" direction="out">
+      </arg>
+    </method>
+    <method name="SearchObjects">
+      <arg type="s" name="Query" direction="in">
+      </arg>
+      <arg type="u" name="Offset" direction="in">
+      </arg>
+      <arg type="u" name="Max" direction="in">
+      </arg>
+      <arg type="as" name="Filter" direction="in">
+      </arg>
+      <arg type="aa{sv}" name="Children" direction="out">
+      </arg>
+    </method>
+    <method name="SearchObjectsEx">
+      <arg type="s" name="Query" direction="in">
+      </arg>
+      <arg type="u" name="Offset" direction="in">
+      </arg>
+      <arg type="u" name="Max" direction="in">
+      </arg>
+      <arg type="as" name="Filter" direction="in">
+      </arg>
+      <arg type="s" name="SortBy" direction="in">
+      </arg>
+      <arg type="aa{sv}" name="Children" direction="out">
+      </arg>
+      <arg type="u" name="TotalItems" direction="out">
+      </arg>
+    </method>
+    <method name="Upload">
+      <arg type="s" name="DisplayName" direction="in">
+      </arg>
+      <arg type="s" name="FilePath" direction="in">
+      </arg>
+      <arg type="u" name="UploadId" direction="out">
+      </arg>
+      <arg type="o" name="Path" direction="out">
+      </arg>
+    </method>
+    <method name="CreateContainer">
+      <arg type="s" name="DisplayName" direction="in">
+      </arg>
+      <arg type="s" name="Type" direction="in">
+      </arg>
+      <arg type="as" name="ChildTypes" direction="in">
+      </arg>
+      <arg type="o" name="Path" direction="out">
+      </arg>
+    </method>
+    <method name="CreatePlaylist">
+      <arg type="s" name="Title" direction="in">
+      </arg>
+      <arg type="s" name="Creator" direction="in">
+      </arg>
+      <arg type="s" name="Genre" direction="in">
+      </arg>
+      <arg type="s" name="Description" direction="in">
+      </arg>
+      <arg type="ao" name="PlaylistItems" direction="in">
+      </arg>
+      <arg type="u" name="UploadId" direction="out">
+      </arg>
+      <arg type="o" name="Path" direction="out">
+      </arg>
+    </method>
+    <method name="GetCompatibleResource">
+      <arg type="s" name="ProtocolInfo" direction="in">
+      </arg>
+      <arg type="as" name="Filter" direction="in">
+      </arg>
+      <arg type="a{sv}" name="Properties" direction="out">
+      </arg>
+    </method>
+    <property type="u" name="ChildCount" access="read">
+    </property>
+    <property type="b" name="Searchable" access="read">
+    </property>
+    <property type="a(sb)" name="CreateClasses" access="read">
+    </property>
+    <property type="u" name="ContainerUpdateID" access="read">
+    </property>
+    <property type="u" name="TotalDeletedChildCount" access="read">
+    </property>
+    <property type="aa{sv}" name="Resources" access="read">
+    </property>
+    <property type="as" name="URLs" access="read">
+    </property>
+    <property type="s" name="MIMEType" access="read">
+    </property>
+    <property type="s" name="DLNAProfile" access="read">
+    </property>
+    <property type="a{sb}" name="DLNAConversion" access="read">
+    </property>
+    <property type="a{sb}" name="DLNAOperation" access="read">
+    </property>
+    <property type="a{sb}" name="DLNAFlags" access="read">
+    </property>
+    <property type="x" name="Size" access="read">
+    </property>
+  </interface>
+</node>
diff --git a/src/local-metadata/grl-local-metadata.c b/src/local-metadata/grl-local-metadata.c
index 18837ba..e797c10 100644
--- a/src/local-metadata/grl-local-metadata.c
+++ b/src/local-metadata/grl-local-metadata.c
@@ -688,6 +688,8 @@ has_compatible_media_url (GrlMedia *media)
 
     if (g_str_has_prefix (source, "grl-upnp-uuid:"))
       return FALSE;
+    if (g_str_has_prefix (source, "grl-dleyna-uuid:"))
+      return FALSE;
   }
 
   url = grl_media_get_url (media);
diff --git a/tests/.gitignore b/tests/.gitignore
index 727dad9..ba30ff2 100644
--- a/tests/.gitignore
+++ b/tests/.gitignore
@@ -2,8 +2,12 @@ test
 test_apple_trailers
 test_bliptv
 test_local_metadata
+test_dleyna
 test_tmdb_preconditions
 test_tmdb_missing_configuration
 test_tmdb_fast_resolution
 test_tmdb_fast_resolution_by_id
 test_tmdb_full_resolution
+/dleyna/dbusmock/dleyna-server-mock.service
+*.pyc
+__pycache__
diff --git a/tests/Makefile.am b/tests/Makefile.am
index 6adb930..5ffcba1 100644
--- a/tests/Makefile.am
+++ b/tests/Makefile.am
@@ -17,6 +17,10 @@ if BLIPTV_PLUGIN
 SUBDIRS += bliptv
 endif
 
+if DLEYNA_PLUGIN
+SUBDIRS += dleyna
+endif
+
 if LOCALMETADATA_PLUGIN
 SUBDIRS += local-metadata
 endif
@@ -32,6 +36,7 @@ endif
 DIST_SUBDIRS =    \
    apple-trailers \
    bliptv         \
+   dleyna         \
    local-metadata \
    tmdb           \
    vimeo
diff --git a/tests/dleyna/Makefile.am b/tests/dleyna/Makefile.am
new file mode 100644
index 0000000..e761354
--- /dev/null
+++ b/tests/dleyna/Makefile.am
@@ -0,0 +1,64 @@
+#
+# Makefile.am
+#
+# Authors: Iago Toral <itoral igalia com>
+#          Emanuele Aina <emanuele aina collabora com>
+#
+# Copyright (C) 2010-2013 Igalia S.L.
+# Copyright (C) 2013 Intel Corp.
+
+include $(top_srcdir)/gtester.mk
+
+AM_CPPFLAGS = \
+       @DEPS_CFLAGS@ \
+       @GIO_CFLAGS@
+
+noinst_PROGRAMS = \
+       test_dleyna
+
+if DLEYNA_TESTS_RUN_DBUSMOCK
+TEST_PROGS += \
+       test_dleyna
+endif
+
+# Let the .c source code know about these paths, even when sourcedir!=builddir,
+# such as during make distcheck.
+#
+# We could set GRL_PLUGIN_PATH here in TESTS_ENVIRONMENT, specifying all the
+# paths (separated by ':') to all the plugins, but it is cleaner to restrict
+# the test to just one plugin by doing it in the code.
+# Note that the dleyna plugin Makefile.am copies the .xml file into .libs so we
+# can use the plugin.
+
+test_dleyna_defines = \
+       -DGRILO_PLUGINS_TESTS_DLEYNA_SERVICES_PATH=\""$(abs_top_builddir)/tests/dleyna/dbusmock"\" \
+       -DGRILO_PLUGINS_TESTS_DLEYNA_PLUGIN_PATH=\""$(abs_top_builddir)/src/dleyna/.libs/"\" \
+       -DGRILO_PLUGINS_TESTS_DLEYNA_DATA_PATH=\""$(abs_top_srcdir)/tests/dleyna/data/"\"
+
+test_dleyna_SOURCES = \
+       test_dleyna.c \
+       test_dleyna_utils.c \
+       test_dleyna_utils.h
+
+test_dleyna_LDADD = \
+       @DEPS_LIBS@
+
+test_dleyna_CFLAGS = \
+       -DPREFIX=$(prefix) \
+       $(test_dleyna_defines)
+
+EXTRA_DIST += \
+       data/helloworld.txt \
+       dbusmock/dleyna-server-mock \
+       dbusmock/dleyna-server-mock.service.in \
+       dbusmock/dleynamanager.py \
+       dbusmock/dleynamediacontainer.py \
+       dbusmock/dleynamediadevice.py \
+       dbusmock/dleynamediaobject.py \
+       dbusmock/items.py
+
+clean-local:
+       -rm -rf dbusmock/__pycache__
+
+CLEANFILES = \
+       dbusmock/*.pyc
diff --git a/tests/dleyna/data/helloworld.txt b/tests/dleyna/data/helloworld.txt
new file mode 100644
index 0000000..980a0d5
--- /dev/null
+++ b/tests/dleyna/data/helloworld.txt
@@ -0,0 +1 @@
+Hello World!
diff --git a/tests/dleyna/dbusmock/dleyna-server-mock b/tests/dleyna/dbusmock/dleyna-server-mock
new file mode 100755
index 0000000..855df46
--- /dev/null
+++ b/tests/dleyna/dbusmock/dleyna-server-mock
@@ -0,0 +1,10 @@
+#!/usr/bin/env python
+
+import os
+
+# Assume the templates are in the same dir as this script
+os.chdir(os.path.dirname(__file__))
+
+# DbusMock does not seem to like to be torn down after each fixture, set
+# logfile to /dev/null as we don't use it
+os.execvp('python', ['python', '-m', 'dbusmock', '-t', 'dleynamanager.py', '--logfile', '/dev/null'])
diff --git a/tests/dleyna/dbusmock/dleyna-server-mock.service.in 
b/tests/dleyna/dbusmock/dleyna-server-mock.service.in
new file mode 100644
index 0000000..b075a9c
--- /dev/null
+++ b/tests/dleyna/dbusmock/dleyna-server-mock.service.in
@@ -0,0 +1,3 @@
+[D-BUS Service]
+Name=com.intel.dleyna-server
+Exec= abs_top_srcdir@/tests/dleyna/dbusmock/dleyna-server-mock
diff --git a/tests/dleyna/dbusmock/dleynamanager.py b/tests/dleyna/dbusmock/dleynamanager.py
new file mode 100644
index 0000000..8d4418c
--- /dev/null
+++ b/tests/dleyna/dbusmock/dleynamanager.py
@@ -0,0 +1,76 @@
+# -*- coding: utf-8 -*-
+'''dLeyna Server Manager mock template'''
+
+# This program is free software; you can redistribute it and/or modify it under
+# the terms of the GNU Lesser General Public License as published by the Free
+# Software Foundation; either version 3 of the License, or (at your option) any
+# later version.  See http://www.gnu.org/copyleft/lgpl.html for the full text
+# of the license.
+
+__author__ = 'Emanuele Aina'
+__email__ = 'emanuele aina collabora com'
+__copyright__ = 'Copyright © 2013 Intel Corp. All rights reserved.'
+__license__ = 'LGPL 3+'
+
+import copy, dbus, uuid
+from dbusmock import MOCK_IFACE, get_object
+from items import ITEMS, filter_properties, MEDIA_DEVICE_PROPERTIES, MEDIA_OBJECT2_PROPERTIES, 
MEDIA_CONTAINER2_PROPERTIES
+
+BUS_NAME = 'com.intel.dleyna-server'
+MAIN_OBJ = '/com/intel/dLeynaServer'
+MAIN_IFACE = 'com.intel.dLeynaServer.Manager'
+SYSTEM_BUS = False
+
+def load(mock, parameters):
+    mock.AddMethods(MAIN_IFACE, [
+        ('GetServers', '', 'ao', 'ret = self.servers'),
+        ('GetVersion', '', 's', 'ret = "0.2.0"'),
+        ('PreferLocalAddresses', 'b', '', ''),
+        ('Release', '', '', ''),
+        ('Rescan', '', '', ''),
+        ('SetProtocolInfo', 's', '', ''),
+      ])
+    mock.AddProperties(MAIN_IFACE, dbus.Dictionary({
+        'Foo': 'bar',
+      }, signature='sv'))
+    mock.next_server_id = 0
+    mock.servers = []
+
+
+ dbus service method(MOCK_IFACE,
+                     in_signature='', out_signature='o')
+def AddServer(self):
+    root = '/com/intel/dLeynaServer/server/%d' % (self.next_server_id,)
+
+    # Pre-process the items tree anchoring paths to the new root object
+    items = copy.deepcopy(ITEMS)
+    for item in items:
+        item['Path'] = dbus.ObjectPath(item['Path'].replace('{root}', root))
+        item['Parent'] = dbus.ObjectPath(item['Parent'].replace('{root}', root))
+
+    for item in items:
+        path = item['Path']
+        self.AddObject(path, 'org.gnome.UPnP.MediaObject2', {}, [])
+        obj = get_object(path)
+        obj.items = items
+
+        if path == root:
+            item['FriendlyName'] = 'Mock Server <#{0}>'.format(self.next_server_id)
+            item['UDN'] = str(uuid.uuid5(uuid.UUID('9123ef5c-f083-11e2-8000-000000000000'), 
str(self.next_server_id)))
+            obj.AddTemplate("dleynamediadevice.py", filter_properties (item, MEDIA_DEVICE_PROPERTIES))
+        obj.AddTemplate("dleynamediaobject.py", filter_properties(item, MEDIA_OBJECT2_PROPERTIES))
+        obj.AddTemplate("dleynamediacontainer.py", filter_properties(item, MEDIA_CONTAINER2_PROPERTIES))
+
+    self.servers.append(root)
+    self.EmitSignal(MAIN_IFACE, 'FoundServer', 'o', [root])
+
+    self.next_server_id += 1
+    return path
+
+
+ dbus service method(MOCK_IFACE,
+                     in_signature='o', out_signature='')
+def DropServer(self, path):
+    self.servers.remove(path)
+    self.RemoveObject(path);
+    self.EmitSignal(MAIN_IFACE, 'LostServer', 'o', [path])
diff --git a/tests/dleyna/dbusmock/dleynamediacontainer.py b/tests/dleyna/dbusmock/dleynamediacontainer.py
new file mode 100644
index 0000000..747911f
--- /dev/null
+++ b/tests/dleyna/dbusmock/dleynamediacontainer.py
@@ -0,0 +1,118 @@
+# -*- coding: utf-8 -*-
+'''dLeyna Media Container mock template'''
+
+# This program is free software; you can redistribute it and/or modify it under
+# the terms of the GNU Lesser General Public License as published by the Free
+# Software Foundation; either version 3 of the License, or (at your option) any
+# later version.  See http://www.gnu.org/copyleft/lgpl.html for the full text
+# of the license.
+
+__author__ = 'Emanuele Aina'
+__email__ = 'emanuele aina collabora com'
+__copyright__ = 'Copyright © 2013 Intel Corp. All rights reserved.'
+__license__ = 'LGPL 3+'
+
+import dbus, os
+from dbusmock import MOCK_IFACE, get_object
+from items import ITEMS, filter_properties, find_item, find_root, MEDIA_OBJECT2_PROPERTIES, 
MEDIA_CONTAINER2_PROPERTIES, CHANGE_TYPES
+from gi.repository import GLib
+from dleynamediadevice import MAIN_IFACE as MEDIA_DEVICE_IFACE
+
+MAIN_IFACE = 'org.gnome.UPnP.MediaContainer2'
+
+def load(mock, parameters):
+    mock.AddMethods(MAIN_IFACE, [
+        ('CreatePlaylist', 'ssssao', '', ''),
+        ('GetCompatibleResource', 'sas', 'a{sv}', ''),
+        ('ListChildrenEx', 'uuass', 'aa{sv}', 'ret = []'),
+        ('ListContainersEx', 'uuass', 'aa{sv}', 'ret = []'),
+        ('ListContainers', 'uuas', 'aa{sv}', 'ret = []'),
+        ('ListItemsEx', 'uuass', 'aa{sv}', 'ret = []'),
+        ('ListItems', 'uuas', 'aa{sv}', 'ret = []'),
+        ('SearchObjectsEx', 'suuass', 'aa{sv}u', 'ret = []'),
+        ('SearchObjects', 'suuas', 'aa{sv}u', 'ret = []'),
+      ])
+    mock.AddProperties(MAIN_IFACE, dbus.Dictionary(parameters, signature='sv'))
+    mock.next_upload_id = 0
+
+ dbus service method(MAIN_IFACE,
+                     in_signature='ssas',
+                     out_signature='o')
+def CreateContainer(self, display_name, container_type, child_types):
+    upload_id = self.next_upload_id
+    self.next_upload_id += 1
+
+    path = "{0}/up{1:03}".format(self.__dbus_object_path__, upload_id)
+    upload = {
+        'ChildCount': dbus.UInt32(0),
+        'DisplayName': display_name,
+        'Parent': self.__dbus_object_path__,
+        'Path': path,
+        'Type': 'container',
+        'TypeEx': container_type,
+      }
+    self.items.append(upload)
+
+    self.AddObject(path, 'org.gnome.UPnP.MediaObject2', {}, [])
+    obj = get_object(path)
+    obj.items = self.items
+    obj.AddTemplate("dleynamediaobject.py", filter_properties(upload, MEDIA_OBJECT2_PROPERTIES))
+    obj.AddTemplate("dleynamediacontainer.py", filter_properties(upload, MEDIA_CONTAINER2_PROPERTIES))
+
+    device = get_object(find_root(self.items, self.__dbus_object_path__)['Path'])
+    device.queue_change ({
+        'ChangeType': CHANGE_TYPES['Add'],
+        'Path': path
+    })
+
+    return (path)
+
+ dbus service method(MAIN_IFACE,
+                     in_signature='uuas',
+                     out_signature='aa{sv}')
+def ListChildren(self, offset, count, prop_filter):
+    children = [i for i in self.items if i['Parent'] == self.__dbus_object_path__]
+    if count:
+        children = children[offset:offset+count]
+    else:
+        children = children[offset:]
+    return [filter_properties(i, prop_filter) for i in children]
+
+
+ dbus service method(MAIN_IFACE,
+                     in_signature='ss',
+                     out_signature='uo')
+def Upload(self, display_name, filename):
+    upload_id = self.next_upload_id
+    self.next_upload_id += 1
+
+    path = "{0}/up{1:03}".format(self.__dbus_object_path__, upload_id)
+    size = os.stat(filename).st_size
+    upload = {
+        'DisplayName': display_name,
+        'Parent': self.__dbus_object_path__,
+        'Path': path,
+        'UploadFilename': filename,
+        'UploadId': upload_id,
+        'UploadSize': size,
+      }
+    self.items.append(upload)
+
+    self.AddObject(path, 'org.gnome.UPnP.MediaObject2', {}, [])
+    obj = get_object(path)
+    obj.items = self.items
+    obj.AddTemplate("dleynamediaobject.py", filter_properties(upload, MEDIA_OBJECT2_PROPERTIES))
+
+    device = get_object(find_root(self.items, self.__dbus_object_path__)['Path'])
+
+    def upload_completed():
+        device.EmitSignal(MEDIA_DEVICE_IFACE, 'UploadUpdate', 'ustt', (upload_id, 'COMPLETED', size, size))
+        device.queue_change ({
+            'ChangeType': CHANGE_TYPES['Add'],
+            'Path': path
+        })
+    GLib.idle_add(upload_completed)
+
+    item = find_item(self.items, self.__dbus_object_path__)
+    item['ChildCount'] += 1
+    return (upload_id, path)
diff --git a/tests/dleyna/dbusmock/dleynamediadevice.py b/tests/dleyna/dbusmock/dleynamediadevice.py
new file mode 100644
index 0000000..da15d18
--- /dev/null
+++ b/tests/dleyna/dbusmock/dleynamediadevice.py
@@ -0,0 +1,156 @@
+# -*- coding: utf-8 -*-
+'''dLeyna Media Device mock template'''
+
+# This program is free software; you can redistribute it and/or modify it under
+# the terms of the GNU Lesser General Public License as published by the Free
+# Software Foundation; either version 3 of the License, or (at your option) any
+# later version.  See http://www.gnu.org/copyleft/lgpl.html for the full text
+# of the license.
+
+__author__ = 'Emanuele Aina'
+__email__ = 'emanuele aina collabora com'
+__copyright__ = 'Copyright © 2013 Intel Corp. All rights reserved.'
+__license__ = 'LGPL 3+'
+
+import dbus, os
+from dbusmock import MOCK_IFACE, get_object
+from items import find_item, find_root, filter_properties, MEDIA_OBJECT2_PROPERTIES, 
MEDIA_CONTAINER2_PROPERTIES, CHANGE_TYPES
+from gi.repository import GLib
+
+MAIN_IFACE = 'com.intel.dLeynaServer.MediaDevice'
+
+def queue_change(self, change):
+    self.changes_id += 1
+
+    if self.changes is None:
+        return
+
+    # return only the parent if we want to emulate ContainerUpdateID-generated
+    # change notifications
+    if not self.changes_detailed:
+        parent = find_item(self.items, change['Path'])
+        change = filter_properties(parent, 'Path Type TypeEx'.split())
+        change['ChangeType'] = CHANGE_TYPES['Container']
+        change['UpdateID'] = self.changes_id
+
+    self.changes += (change,)
+
+def load(mock, parameters):
+    mock.AddMethods(MAIN_IFACE, [
+        ('Cancel', '', '', ''),
+        ('CancelUpload', 'u', '', ''),
+        ('CreatePlaylistInAnyContainer', 'ssssao', 'uo', ''),
+        ('GetUploadIDs', '', 'au', ''),
+        ('GetUploadStatus', 'u', 'stt', ''),
+      ])
+    mock.AddProperties(MAIN_IFACE, dbus.Dictionary(parameters, signature='sv'))
+    mock.next_upload_id = 0
+    mock.changes = None
+    mock.changes_id = 0
+    mock.changes_detailed = True
+    # make queue_change() a real method of the mock object
+    setattr(mock, 'queue_change', queue_change.__get__(mock, mock.__class__))
+
+ dbus service method(MAIN_IFACE,
+                     in_signature='aoas',
+                     out_signature='aa{sv}')
+def BrowseObjects(self, object_paths, props):
+    results = []
+    for object_path in object_paths:
+        item = find_item(self.items, object_path)
+        if item:
+            result = filter_properties(item, props)
+        else:
+            result = {
+                'Error': dbus.Dictionary({
+                    'ID': dbus.Int32(42),
+                    'Message': 'Object not found'}, 'sv') }
+        results.append(result)
+    return results
+
+ dbus service method(MAIN_IFACE,
+                     in_signature='ssas',
+                     out_signature='o')
+def CreateContainerInAnyContainer(self, display_name, container_type, child_types):
+    upload_id = self.next_upload_id
+    self.next_upload_id += 1
+
+    path = '{0}/any{1:03}'.format(self.__dbus_object_path__, upload_id)
+    upload = {
+        'ChildCount': dbus.UInt32(0),
+        'DisplayName': display_name,
+        'Parent': self.__dbus_object_path__,
+        'Path': path,
+        'Type': 'container',
+        'TypeEx': container_type,
+      }
+    self.items.append(upload)
+
+    self.AddObject(path, 'org.gnome.UPnP.MediaObject2', {}, [])
+    obj = get_object(path)
+    obj.items = self.items
+    obj.AddTemplate('dleynamediaobject.py', filter_properties(upload, MEDIA_OBJECT2_PROPERTIES))
+    obj.AddTemplate('dleynamediacontainer.py', filter_properties(upload, MEDIA_CONTAINER2_PROPERTIES))
+
+    self.queue_change ({
+        'ChangeType': CHANGE_TYPES['Add'],
+        'Path': path
+    })
+
+    return (path)
+
+ dbus service method(MOCK_IFACE,
+                     in_signature='', out_signature='')
+def FlushChanges(self):
+    """Emit the currently queued changes"""
+    if not self.changes:
+        return
+    self.EmitSignal(MAIN_IFACE, 'Changed', 'aa{sv}', [self.changes])
+    self.changes = []
+
+ dbus service method(MOCK_IFACE,
+                     in_signature='bb', out_signature='')
+def QueueChanges(self, enabled, detailed):
+    """Queue a change to be later emitted on FlushChanges()"""
+    self.changes_detailed = detailed
+    if not enabled:
+        self.changes = None
+    if enabled and self.changes is None:
+        self.changes = []
+
+ dbus service method(MAIN_IFACE,
+                     in_signature='ss',
+                     out_signature='uo')
+def UploadToAnyContainer(self, display_name, filename):
+    upload_id = self.next_upload_id
+    self.next_upload_id += 1
+
+    path = '{0}/any{1:03}'.format(self.__dbus_object_path__, upload_id)
+    size = os.stat(filename).st_size
+    upload = {
+        'DisplayName': display_name,
+        'Parent': self.__dbus_object_path__,
+        'Path': path,
+        'Type': 'item.unclassified',
+        'UploadFilename': filename,
+        'UploadId': upload_id,
+        'UploadSize': size,
+      }
+    self.items.append(upload)
+
+    self.AddObject(path, 'org.gnome.UPnP.MediaObject2', {}, [])
+    obj = get_object(path)
+    obj.items = self.items
+    obj.AddTemplate('dleynamediaobject.py', filter_properties(upload, MEDIA_OBJECT2_PROPERTIES))
+
+    device = get_object(find_root(self.items, self.__dbus_object_path__)['Path'])
+
+    def upload_completed():
+        device.EmitSignal(MAIN_IFACE, 'UploadUpdate', 'ustt', (upload_id, 'COMPLETED', size, size))
+        self.queue_change ({
+            'ChangeType': CHANGE_TYPES['Add'],
+            'Path': path
+        })
+    GLib.idle_add(upload_completed)
+
+    return (upload_id, path)
diff --git a/tests/dleyna/dbusmock/dleynamediaobject.py b/tests/dleyna/dbusmock/dleynamediaobject.py
new file mode 100644
index 0000000..8d5b2df
--- /dev/null
+++ b/tests/dleyna/dbusmock/dleynamediaobject.py
@@ -0,0 +1,57 @@
+# -*- coding: utf-8 -*-
+'''dLeyna Media Object mock template'''
+
+# This program is free software; you can redistribute it and/or modify it under
+# the terms of the GNU Lesser General Public License as published by the Free
+# Software Foundation; either version 3 of the License, or (at your option) any
+# later version.  See http://www.gnu.org/copyleft/lgpl.html for the full text
+# of the license.
+
+__author__ = 'Emanuele Aina'
+__email__ = 'emanuele aina collabora com'
+__copyright__ = 'Copyright © 2013 Intel Corp. All rights reserved.'
+__license__ = 'LGPL 3+'
+
+import dbus
+from dbusmock import MOCK_IFACE, get_object
+from items import find_item, find_root, CHANGE_TYPES
+
+MAIN_IFACE = 'org.gnome.UPnP.MediaObject2'
+
+def load(mock, parameters):
+    mock.AddMethods(MAIN_IFACE, [
+        ('GetMetaData', '', 's', ''),
+      ])
+    mock.AddProperties(MAIN_IFACE, dbus.Dictionary(parameters, signature='sv'))
+
+ dbus service method(MAIN_IFACE,
+                     in_signature='a{sv}as',
+                     out_signature='')
+def Update(self, to_add_update, to_delete):
+    item = find_item(self.items, self.__dbus_object_path__)
+    for prop in to_delete:
+        del item[prop]
+    for prop, val in to_add_update.items():
+        item[prop] = val
+
+    device = get_object(find_root(self.items, self.__dbus_object_path__)['Path'])
+    device.queue_change ({
+        'ChangeType': CHANGE_TYPES['Mod'],
+        'Path': self.__dbus_object_path__
+    })
+
+ dbus service method(MAIN_IFACE,
+                     in_signature='',
+                     out_signature='')
+def Delete(self):
+    device = get_object(find_root(self.items, self.__dbus_object_path__)['Path'])
+    device.queue_change ({
+        'ChangeType': CHANGE_TYPES['Del'],
+        'Path': self.__dbus_object_path__
+    })
+
+    item = find_item(self.items, self.__dbus_object_path__)
+    self.items.remove(item)
+
+    parent = find_item(self.items, item['Parent'])
+    parent['ChildCount'] -= 1
diff --git a/tests/dleyna/dbusmock/items.py b/tests/dleyna/dbusmock/items.py
new file mode 100644
index 0000000..dba2a62
--- /dev/null
+++ b/tests/dleyna/dbusmock/items.py
@@ -0,0 +1,125 @@
+# -*- coding: utf-8 -*-
+'''dLeyna DMS fake content hierarchy'''
+
+# This program is free software; you can redistribute it and/or modify it under
+# the terms of the GNU Lesser General Public License as published by the Free
+# Software Foundation; either version 3 of the License, or (at your option) any
+# later version.  See http://www.gnu.org/copyleft/lgpl.html for the full text
+# of the license.
+
+__author__ = 'Emanuele Aina'
+__email__ = 'emanuele aina collabora com'
+__copyright__ = 'Copyright © 2013 Intel Corp. All rights reserved.'
+__license__ = 'LGPL 3+'
+
+import dbus
+
+def filter_properties(obj, prop_filter):
+    return dict(i for i in obj.items() if i[0] in prop_filter)
+
+def find_item(items, path):
+    item = [i for i in items if i['Path'] == path]
+    if item:
+        return item[0]
+    return None
+
+def find_root(items, path):
+    item = find_item(items, path)
+    while item and item['Parent'] != path:
+        path = item['Parent']
+        item = find_item(items, path)
+    return item
+
+MEDIA_DEVICE_PROPERTIES = ['DeviceType', 'UDN', 'FriendlyName', 'IconURL', 'Manufacturer', 'ManufacturerUrl',
+                           'ModelDescription', 'ModelName', 'ModelNumber', 'SerialNumber', 'PresentationURL',
+                           'DLNACaps', 'SearchCaps', 'SortCaps', 'SortExtCaps', 'FeatureList', 
'ServiceResetToken',
+                           'SystemUpdateID' ]
+MEDIA_OBJECT2_PROPERTIES = [ 'Parent', 'Type', 'TypeEx', 'Path', 'DisplayName' ]
+MEDIA_CONTAINER2_PROPERTIES = [ 'ChildCount', 'ItemCount', 'ContainerCount', 'Searchable', 'Resources', 
'URLs' ]
+
+CHANGE_TYPES = {
+    'Add': dbus.UInt32(1),
+    'Mod': dbus.UInt32(2),
+    'Del': dbus.UInt32(3),
+    'Done': dbus.UInt32(4),
+    'Container': dbus.UInt32(5) }
+
+ITEMS = [
+    { 'DisplayName': 'The Root',
+      'SearchCaps': ['*'],
+      'SystemUpdateID': dbus.UInt32(0),
+      'Path':   '{root}',
+      'Parent': '{root}',
+      'Type':   'container',
+      'TypeEx': 'container',
+      'URLs': [ 'http://127.0.0.1:4242/root/DIDL_S.xml' ] },
+    { 'DisplayName': 'Music',
+      'Path':   '{root}/1',
+      'Parent': '{root}',
+      'Type':   'container',
+      'TypeEx': 'container' },
+    { 'DisplayName': 'A song',
+      'Path':   '{root}/11',
+      'Parent': '{root}/1',
+      'Type':   'music',
+      'TypeEx': 'music' },
+    { 'DisplayName': 'Another song, with funny chars: < >\'"{1}?#%s❤;',
+      'Path':   '{root}/12',
+      'Parent': '{root}/1',
+      'Type':   'music',
+      'TypeEx': 'music' },
+    { 'DisplayName': 'Video',
+      'Path':   '{root}/2',
+      'Parent': '{root}',
+      'Type':   'container',
+      'TypeEx': 'container' },
+    { 'DisplayName': 'This is a movie clip',
+      'Path':   '{root}/21',
+      'Parent': '{root}/2',
+      'Type':   'video',
+      'TypeEx': 'video' },
+    { 'DisplayName': 'Stuff',
+      'Path':   '{root}/3',
+      'Parent': '{root}',
+      'Type':   'container',
+      'TypeEx': 'container',
+      'URLs': [ 'http://127.0.0.1:4242/stuff/DIDL_S.xml' ] },
+    { 'DisplayName': 'A non-media file',
+      'Path':   '{root}/31',
+      'Parent': '{root}/3',
+      'Type':   'item.unclassified',
+      'TypeEx': 'item' },
+    { 'DisplayName': 'An empty sub container',
+      'Path':   '{root}/32',
+      'Parent': '{root}/3',
+      'Type':   'container',
+      'TypeEx': 'container' },
+    { 'DisplayName': 'A picture.jpg',
+      'Path':   '{root}/33',
+      'Parent': '{root}/3',
+      'Type':   'image.photo',
+      'TypeEx': 'image.photo',
+      'URLs': [ 'http://127.0.0.1:4242/stuff/picture.jpg' ] },
+    { 'DisplayName': 'Another picture.jpg',
+      'Path':   '{root}/34',
+      'Parent': '{root}/3',
+      'Type':   'image.photo',
+      'TypeEx': 'image.photo',
+      'URLs': [ 'http://127.0.0.1:4242/stuff/picture2.jpg' ] },
+    { 'DisplayName': 'Another sub container',
+      'Path':   '{root}/35',
+      'Parent': '{root}/3',
+      'Type':   'container',
+      'TypeEx': 'container' },
+    { 'DisplayName': 'A nested file',
+      'Path':   '{root}/351',
+      'Parent': '{root}/35',
+      'Type':   'item.unclassified',
+      'TypeEx': 'item' },
+]
+
+# Populate initial ChildCounts
+for item in ITEMS:
+    if item['Type'].startswith('container'):
+        item['ChildCount'] = dbus.UInt32(len([i for i in ITEMS if i != item
+            and i['Parent'] == item['Path']]))
diff --git a/tests/dleyna/test_dleyna.c b/tests/dleyna/test_dleyna.c
new file mode 100644
index 0000000..8773366
--- /dev/null
+++ b/tests/dleyna/test_dleyna.c
@@ -0,0 +1,467 @@
+/*
+ * Copyright © 2013 Intel Corporation. All rights reserved.
+ *
+ * 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, either 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, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "test_dleyna_utils.h"
+
+typedef struct {
+  GPtrArray          *changed_medias;
+  GrlSourceChangeType change_type;
+  gboolean            location_unknown;
+} ChangedContentData;
+
+static ChangedContentData *
+changed_content_data_new (GPtrArray          *changed_medias,
+                          GrlSourceChangeType change_type,
+                          gboolean            location_unknown)
+{
+  ChangedContentData *ccd = g_new0 (ChangedContentData, 1);
+  ccd->changed_medias = g_ptr_array_ref (changed_medias);
+  ccd->change_type = change_type;
+  ccd->location_unknown = location_unknown;
+  return ccd;
+}
+
+static void
+changed_content_data_free (gpointer data)
+{
+  ChangedContentData *ccd = data;
+  g_ptr_array_unref (ccd->changed_medias);
+  g_free (ccd);
+}
+
+static void
+main_loop_quit_on_source_cb (GrlRegistry       *registry,
+                             GrlSource         *source,
+                             TestDleynaFixture *fixture)
+{
+  g_assert (source != NULL);
+  g_assert (GRL_IS_SOURCE (source));
+
+  test_dleyna_main_loop_quit(fixture);
+}
+
+static void
+assert_source (TestDleynaFixture *fixture,
+               gchar             *id,
+               gchar             *name)
+{
+  GrlSource *source;
+
+  source = grl_registry_lookup_source (fixture->registry, id);
+
+  g_assert (source != NULL);
+  g_assert (GRL_IS_SOURCE (source));
+  g_assert_cmpstr (grl_source_get_name (GRL_SOURCE (source)), ==, name);
+}
+
+/**
+ * test_discovery:
+ *
+ * Test that sources are correctly added/removed when DLNA servers are detected.
+ */
+static void
+test_discovery (TestDleynaFixture *fixture,
+                gconstpointer      user_data)
+{
+  g_signal_connect (fixture->registry, "source-added", G_CALLBACK (main_loop_quit_on_source_cb), fixture);
+  g_signal_connect (fixture->registry, "source-removed", G_CALLBACK (main_loop_quit_on_source_cb), fixture);
+
+  test_dleyna_add_server (fixture);
+  test_dleyna_main_loop_run (fixture, 5);
+  assert_source (fixture, "grl-dleyna-c50bf388-042a-5326-af4b-6969e1bbc860", "Mock Server <#0>");
+
+  test_dleyna_add_server (fixture);
+  test_dleyna_main_loop_run (fixture, 5);
+  assert_source (fixture, "grl-dleyna-bcc9541d-7ccb-5fa7-93bc-0fe9038bc333", "Mock Server <#1>");
+
+  test_dleyna_drop_server (fixture, "/com/intel/dLeynaServer/server/1");
+  test_dleyna_main_loop_run (fixture, 5);
+  g_assert (grl_registry_lookup_source (fixture->registry, 
"grl-dleyna-bcc9541d-7ccb-5fa7-93bc-0fe9038bc333") == NULL);
+  g_assert (grl_registry_lookup_source (fixture->registry, 
"grl-dleyna-c50bf388-042a-5326-af4b-6969e1bbc860") != NULL);
+
+  test_dleyna_drop_server (fixture, "/com/intel/dLeynaServer/server/0");
+  test_dleyna_main_loop_run (fixture, 5);
+  g_assert (grl_registry_lookup_source (fixture->registry, 
"grl-dleyna-c50bf388-042a-5326-af4b-6969e1bbc860") == NULL);
+}
+
+static void
+test_browse (TestDleynaFixture *fixture,
+             gconstpointer      user_data)
+{
+  GrlSource *source;
+  GrlOperationOptions *options;
+  GrlCaps *caps;
+  GrlMedia *media, *container;
+  GList *keys, *results;
+  GError *error = NULL;
+
+  g_signal_connect (fixture->registry, "source-added", G_CALLBACK (main_loop_quit_on_source_cb), fixture);
+  test_dleyna_add_server (fixture);
+  test_dleyna_main_loop_run (fixture, 5);
+
+  source = grl_registry_lookup_source (fixture->registry, "grl-dleyna-c50bf388-042a-5326-af4b-6969e1bbc860");
+
+  caps = grl_source_get_caps (source, GRL_OP_BROWSE);
+  g_assert (grl_caps_get_type_filter (caps) & GRL_TYPE_FILTER_VIDEO);
+  g_assert (grl_caps_get_type_filter (caps) & GRL_TYPE_FILTER_AUDIO);
+  g_assert (grl_caps_get_type_filter (caps) & GRL_TYPE_FILTER_IMAGE);
+
+  /* Try with default options and no keys */
+  options = grl_operation_options_new (NULL);
+  results = grl_source_browse_sync (source, NULL, NULL, options, &error);
+  g_assert_no_error (error);
+  g_assert_cmpint (g_list_length (results), ==, 4);
+  g_list_free_full (results, g_object_unref);
+
+  /* Ask for title, URL and child count */
+  keys = grl_metadata_key_list_new (GRL_METADATA_KEY_TITLE, GRL_METADATA_KEY_URL, 
GRL_METADATA_KEY_CHILDCOUNT, NULL);
+  results = grl_source_browse_sync (source, NULL, keys, options, &error);
+  g_assert_no_error (error);
+  g_assert_cmpint (g_list_length (results), ==, 4);
+  media = GRL_MEDIA (results->data);
+  g_assert (GRL_IS_MEDIA_BOX (media));
+  g_assert_cmpstr (grl_media_get_id (media), ==, "dleyna:/com/intel/dLeynaServer/server/0");
+  g_assert_cmpstr (grl_media_get_title (media), ==, "The Root");
+  g_assert_cmpstr (grl_media_get_url (media), ==, "http://127.0.0.1:4242/root/DIDL_S.xml";);
+  g_assert_cmpint (grl_media_box_get_childcount (GRL_MEDIA_BOX (media)), ==, 3);
+  media = GRL_MEDIA (results->next->next->next->data);
+  g_assert (GRL_IS_MEDIA_BOX (media));
+  g_assert_cmpstr (grl_media_get_id (media), ==, "dleyna:/com/intel/dLeynaServer/server/0/3");
+  g_assert_cmpstr (grl_media_get_title (media), ==, "Stuff");
+  g_assert_cmpstr (grl_media_get_url (media), ==, "http://127.0.0.1:4242/stuff/DIDL_S.xml";);
+  g_assert_cmpint (grl_media_box_get_childcount (GRL_MEDIA_BOX (media)), ==, 5);
+  container = g_object_ref (media); /* Keep a ref for the subsequent test */
+  g_list_free_full (results, g_object_unref);
+
+  /* Try from a container */
+  keys = grl_metadata_key_list_new (GRL_METADATA_KEY_TITLE, GRL_METADATA_KEY_URL, 
GRL_METADATA_KEY_CHILDCOUNT, NULL);
+  results = grl_source_browse_sync (source, container, keys, options, &error);
+  g_assert_no_error (error);
+  g_assert_cmpint (g_list_length (results), ==, 5);
+  media = GRL_MEDIA (results->next->next->data);
+  g_assert (GRL_IS_MEDIA_IMAGE (media));
+  g_assert_cmpstr (grl_media_get_id (media), ==, "dleyna:/com/intel/dLeynaServer/server/0/33");
+  g_assert_cmpstr (grl_media_get_title (media), ==, "A picture.jpg");
+  g_assert_cmpstr (grl_media_get_url (media), ==, "http://127.0.0.1:4242/stuff/picture.jpg";);
+  g_list_free (keys);
+  g_object_unref (container);
+  g_list_free_full (results, g_object_unref);
+
+  g_object_unref (caps);
+  g_object_unref (options);
+}
+
+static void
+test_store (TestDleynaFixture *fixture,
+            gconstpointer      user_data)
+{
+  GrlSource *source;
+  GrlMedia *media, *container;
+  GError *error = NULL;
+
+  g_signal_connect (fixture->registry, "source-added", G_CALLBACK (main_loop_quit_on_source_cb), fixture);
+  test_dleyna_add_server (fixture);
+  test_dleyna_main_loop_run (fixture, 5);
+
+  source = grl_registry_lookup_source (fixture->registry, "grl-dleyna-c50bf388-042a-5326-af4b-6969e1bbc860");
+
+  media = grl_media_new ();
+  grl_media_set_url (media, "file://" GRILO_PLUGINS_TESTS_DLEYNA_DATA_PATH "/helloworld.txt");
+
+  /* Let the DMS choose the right container */
+  grl_source_store_sync (source, NULL, media, GRL_WRITE_NORMAL, &error);
+  g_assert_no_error (error);
+  g_assert_cmpstr (grl_media_get_id (media), ==, "dleyna:/com/intel/dLeynaServer/server/0/any000");
+
+  /* Try again explicitly choosing a container */
+  container = grl_media_box_new ();
+  grl_media_set_id (container, "dleyna:/com/intel/dLeynaServer/server/0/3");
+
+  grl_source_store_sync (source, GRL_MEDIA_BOX (container), media, GRL_WRITE_NORMAL, &error);
+  g_assert_no_error (error);
+  g_assert_cmpstr (grl_media_get_id (media), ==, "dleyna:/com/intel/dLeynaServer/server/0/3/up000");
+
+  g_object_unref (media);
+
+  /* Create a container, letting the DMS to choose the parent */
+  media = GRL_MEDIA (grl_media_box_new ());
+  grl_media_set_title (media, "New container");
+
+  grl_source_store_sync (source, NULL, media, GRL_WRITE_NORMAL, &error);
+  g_assert_no_error (error);
+  g_assert_cmpstr (grl_media_get_id (media), ==, "dleyna:/com/intel/dLeynaServer/server/0/any001");
+
+  /* Again, but explictly choosing the parent */
+  grl_source_store_sync (source, GRL_MEDIA_BOX (container), media, GRL_WRITE_NORMAL, &error);
+  g_assert_no_error (error);
+  g_assert_cmpstr (grl_media_get_id (media), ==, "dleyna:/com/intel/dLeynaServer/server/0/3/up001");
+
+  g_object_unref (media);
+  g_object_unref (container);
+}
+
+static void
+test_store_metadata (TestDleynaFixture *fixture,
+                     gconstpointer      user_data)
+{
+  GrlSource *source;
+  GrlMedia *media, *container;
+  GList *keys;
+  GError *error = NULL;
+
+  g_signal_connect (fixture->registry, "source-added", G_CALLBACK (main_loop_quit_on_source_cb), fixture);
+  test_dleyna_add_server (fixture);
+  test_dleyna_main_loop_run (fixture, 5);
+
+  source = grl_registry_lookup_source (fixture->registry, "grl-dleyna-c50bf388-042a-5326-af4b-6969e1bbc860");
+
+  media = grl_media_new ();
+  grl_media_set_url (media, "file://" GRILO_PLUGINS_TESTS_DLEYNA_DATA_PATH "/helloworld.txt");
+  grl_media_set_author (media, "Tizio Caio Sempronio");
+
+  container = grl_media_box_new ();
+  grl_media_set_id (container, "dleyna:/com/intel/dLeynaServer/server/0/3");
+
+  grl_source_store_sync (source, GRL_MEDIA_BOX (container), media, GRL_WRITE_FULL, &error);
+  g_assert_no_error (error);
+  g_assert_cmpstr (grl_media_get_id (media), ==, "dleyna:/com/intel/dLeynaServer/server/0/3/up000");
+
+  keys = grl_metadata_key_list_new (GRL_METADATA_KEY_TITLE, NULL);
+  grl_media_set_title (media, "Hello World");
+  grl_source_store_metadata_sync (source, media, keys, GRL_WRITE_NORMAL, &error);
+  g_assert_no_error (error);
+  g_list_free (keys);
+
+  g_object_unref (media);
+  g_object_unref (container);
+}
+
+static void
+test_resolve (TestDleynaFixture *fixture,
+              gconstpointer      user_data)
+{
+  GrlSource *source;
+  GrlOperationOptions *options;
+  GrlMedia *media;
+  GList *keys;
+  GError *error = NULL;
+
+  g_signal_connect (fixture->registry, "source-added", G_CALLBACK (main_loop_quit_on_source_cb), fixture);
+  test_dleyna_add_server (fixture);
+  test_dleyna_main_loop_run (fixture, 5);
+
+  source = grl_registry_lookup_source (fixture->registry, "grl-dleyna-c50bf388-042a-5326-af4b-6969e1bbc860");
+
+  options = grl_operation_options_new (NULL);
+  keys = grl_metadata_key_list_new (GRL_METADATA_KEY_ID, GRL_METADATA_KEY_TITLE, GRL_METADATA_KEY_URL, 
GRL_METADATA_KEY_CHILDCOUNT, NULL);
+
+  media = grl_media_new ();
+  grl_source_resolve_sync (source, media, keys, options, &error);
+  g_assert_no_error (error);
+
+  g_assert_cmpstr (grl_media_get_id (media), ==, "dleyna:/com/intel/dLeynaServer/server/0");
+  g_assert_cmpstr (grl_media_get_title (media), ==, "The Root");
+  g_assert_cmpstr (grl_media_get_url (media), ==, "http://127.0.0.1:4242/root/DIDL_S.xml";);
+  g_object_unref (media);
+
+  media = grl_media_new ();
+  grl_media_set_id (media, "dleyna:/com/intel/dLeynaServer/server/0/33");
+
+  grl_source_resolve_sync (source, media, keys, options, &error);
+  g_assert_no_error (error);
+
+  g_assert_cmpstr (grl_media_get_title (media), ==, "A picture.jpg");
+  g_assert_cmpstr (grl_media_get_url (media), ==, "http://127.0.0.1:4242/stuff/picture.jpg";);
+  g_object_unref (media);
+
+  media = grl_media_new ();
+  grl_media_set_id (media, "dleyna:/com/intel/dLeynaServer/server/0/does_not_exists");
+
+  keys = grl_metadata_key_list_new (GRL_METADATA_KEY_TITLE, GRL_METADATA_KEY_URL, 
GRL_METADATA_KEY_CHILDCOUNT, NULL);
+  grl_source_resolve_sync (source, media, keys, options, &error);
+  g_assert_error (error, GRL_CORE_ERROR, GRL_CORE_ERROR_RESOLVE_FAILED);
+
+  g_object_unref (media);
+  g_list_free (keys);
+}
+
+static void
+test_remove (TestDleynaFixture *fixture,
+             gconstpointer      user_data)
+{
+  GrlSource *source;
+  GrlMedia *media;
+  GError *error = NULL;
+
+  g_signal_connect (fixture->registry, "source-added", G_CALLBACK (main_loop_quit_on_source_cb), fixture);
+  test_dleyna_add_server (fixture);
+  test_dleyna_main_loop_run (fixture, 5);
+
+  source = grl_registry_lookup_source (fixture->registry, "grl-dleyna-c50bf388-042a-5326-af4b-6969e1bbc860");
+
+  media = grl_media_new ();
+  grl_media_set_id (media, "dleyna:/com/intel/dLeynaServer/server/0/33");
+
+  grl_source_remove_sync (source, media, &error);
+  g_assert_no_error (error);
+
+  g_object_unref (media);
+}
+
+static void
+main_loop_quit_on_content_changed_cb (GrlSource          *source,
+                                      GPtrArray          *changed_medias,
+                                      GrlSourceChangeType change_type,
+                                      gboolean            location_unknown,
+                                      TestDleynaFixture  *fixture)
+{
+  ChangedContentData *ccd;
+
+  ccd = changed_content_data_new (changed_medias, change_type, location_unknown);
+  g_ptr_array_add (fixture->results, ccd);
+
+  test_dleyna_main_loop_quit(fixture);
+}
+
+static void
+test_notifications (TestDleynaFixture *fixture,
+                    gconstpointer      user_data)
+{
+  GrlSource *source;
+  GrlMedia *media, *container;
+  GList *updated_keys, *failing_keys;
+  ChangedContentData *ccd;
+  GError *error = NULL;
+
+  g_signal_connect (fixture->registry, "source-added", G_CALLBACK (main_loop_quit_on_source_cb), fixture);
+  fixture->results = g_ptr_array_new_with_free_func (changed_content_data_free);
+
+  test_dleyna_add_server (fixture);
+  test_dleyna_main_loop_run (fixture, 5);
+
+  source = grl_registry_lookup_source (fixture->registry, "grl-dleyna-c50bf388-042a-5326-af4b-6969e1bbc860");
+
+  test_dleyna_queue_changes (fixture, "/com/intel/dLeynaServer/server/0", TRUE, TRUE);
+  grl_source_notify_change_start (source, &error);
+  g_assert_no_error (error);
+
+  g_signal_connect (source, "content-changed", G_CALLBACK (main_loop_quit_on_content_changed_cb), fixture);
+
+  media = grl_media_new ();
+  grl_media_set_id (media, "dleyna:/com/intel/dLeynaServer/server/0/33");
+
+  grl_source_remove_sync (source, media, &error);
+  g_assert_no_error (error);
+  g_object_unref (media);
+
+  media = grl_media_new ();
+  grl_media_set_url (media, "file://" GRILO_PLUGINS_TESTS_DLEYNA_DATA_PATH "/helloworld.txt");
+
+  container = grl_media_box_new ();
+  grl_media_set_id (container, "dleyna:/com/intel/dLeynaServer/server/0/32");
+
+  grl_source_store_sync (source, GRL_MEDIA_BOX (container), media, GRL_WRITE_NORMAL, &error);
+  g_assert_no_error (error);
+  g_assert_cmpstr (grl_media_get_id (media), ==, "dleyna:/com/intel/dLeynaServer/server/0/32/up000");
+  grl_source_store_sync (source, GRL_MEDIA_BOX (container), media, GRL_WRITE_NORMAL, &error);
+  g_assert_no_error (error);
+  g_assert_cmpstr (grl_media_get_id (media), ==, "dleyna:/com/intel/dLeynaServer/server/0/32/up001");
+
+  grl_media_set_title (media, "helloworld-2.txt");
+  updated_keys = grl_metadata_key_list_new (GRL_METADATA_KEY_TITLE, NULL);
+  failing_keys = grl_source_store_metadata_sync (source, media, updated_keys, GRL_WRITE_NORMAL, &error);
+  g_assert_no_error (error);
+  g_list_free (updated_keys);
+  g_assert (failing_keys == NULL);
+
+  g_object_unref (media);
+  g_object_unref (container);
+
+  test_dleyna_flush_changes (fixture, "/com/intel/dLeynaServer/server/0");
+
+  test_dleyna_main_loop_run (fixture, 5);
+
+  g_assert_cmpint (fixture->results->len, ==, 3);
+
+  ccd = g_ptr_array_index (fixture->results, 0);
+  g_assert_cmpint (ccd->changed_medias->len, ==, 1);
+  g_assert_cmpint (ccd->change_type, ==, GRL_CONTENT_REMOVED);
+  g_assert (!ccd->location_unknown);
+  media = g_ptr_array_index (ccd->changed_medias, 0);
+  g_assert_cmpstr (grl_media_get_id (media), ==, "dleyna:/com/intel/dLeynaServer/server/0/33");
+
+  ccd = g_ptr_array_index (fixture->results, 1);
+
+  g_assert_cmpint (ccd->changed_medias->len, ==, 2);
+  g_assert_cmpint (ccd->change_type, ==, GRL_CONTENT_ADDED);
+  g_assert (!ccd->location_unknown);
+  media = g_ptr_array_index (ccd->changed_medias, 0);
+  g_assert_cmpstr (grl_media_get_id (media), ==, "dleyna:/com/intel/dLeynaServer/server/0/32/up000");
+  media = g_ptr_array_index (ccd->changed_medias, 1);
+  g_assert_cmpstr (grl_media_get_id (media), ==, "dleyna:/com/intel/dLeynaServer/server/0/32/up001");
+
+  ccd = g_ptr_array_index (fixture->results, 2);
+
+  g_assert_cmpint (ccd->changed_medias->len, ==, 1);
+  g_assert_cmpint (ccd->change_type, ==, GRL_CONTENT_CHANGED);
+  g_assert (!ccd->location_unknown);
+  media = g_ptr_array_index (ccd->changed_medias, 0);
+  g_assert_cmpstr (grl_media_get_id (media), ==, "dleyna:/com/intel/dLeynaServer/server/0/32/up001");
+
+  grl_source_notify_change_stop (source, &error);
+  g_assert_no_error (error);
+}
+
+int
+main(int argc, char **argv)
+{
+  g_setenv ("GRL_PLUGIN_PATH", GRILO_PLUGINS_TESTS_DLEYNA_PLUGIN_PATH, TRUE);
+  g_setenv ("GRL_PLUGIN_LIST", DLEYNA_PLUGIN_ID, TRUE);
+
+  grl_init (&argc, &argv);
+  g_test_init (&argc, &argv, NULL);
+
+  /* Since we want to test error conditions we have to make sure that
+   * G_LOG_LEVEL_WARNING is not considered fatal. In test_dleyna_setup() we
+   * also make sure that warning messages are not printed by default. */
+  g_log_set_always_fatal (G_LOG_LEVEL_CRITICAL | G_LOG_FATAL_MASK);
+
+#if !GLIB_CHECK_VERSION(2,32,0)
+  g_thread_init (NULL);
+#endif
+
+#if !GLIB_CHECK_VERSION(2,35,0)
+  g_type_init ();
+#endif
+
+  g_test_add ("/dleyna/discovery", TestDleynaFixture, NULL,
+      test_dleyna_setup, test_discovery, test_dleyna_shutdown);
+  g_test_add ("/dleyna/browse", TestDleynaFixture, NULL,
+      test_dleyna_setup, test_browse, test_dleyna_shutdown);
+  g_test_add ("/dleyna/resolve", TestDleynaFixture, NULL,
+      test_dleyna_setup, test_resolve, test_dleyna_shutdown);
+  g_test_add ("/dleyna/store", TestDleynaFixture, NULL,
+      test_dleyna_setup, test_store, test_dleyna_shutdown);
+  g_test_add ("/dleyna/store-metadata", TestDleynaFixture, NULL,
+      test_dleyna_setup, test_store_metadata, test_dleyna_shutdown);
+  g_test_add ("/dleyna/remove", TestDleynaFixture, NULL,
+      test_dleyna_setup, test_remove, test_dleyna_shutdown);
+  g_test_add ("/dleyna/notifications", TestDleynaFixture, NULL,
+      test_dleyna_setup, test_notifications, test_dleyna_shutdown);
+
+  return g_test_run ();
+}
diff --git a/tests/dleyna/test_dleyna_utils.c b/tests/dleyna/test_dleyna_utils.c
new file mode 100644
index 0000000..dc616cb
--- /dev/null
+++ b/tests/dleyna/test_dleyna_utils.c
@@ -0,0 +1,139 @@
+/*
+ * Copyright © 2013 Intel Corporation. All rights reserved.
+ *
+ * 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, either 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, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "test_dleyna_utils.h"
+
+void
+test_dleyna_setup (TestDleynaFixture *fixture,
+                   gconstpointer      user_data)
+{
+  GError *error = NULL;
+
+  fixture->main_loop = g_main_loop_new (NULL, FALSE);
+
+  fixture->dbus = g_test_dbus_new (G_TEST_DBUS_NONE);
+  g_test_dbus_add_service_dir (fixture->dbus, GRILO_PLUGINS_TESTS_DLEYNA_SERVICES_PATH);
+  g_test_dbus_up (fixture->dbus);
+
+  fixture->connection = g_bus_get_sync (G_BUS_TYPE_SESSION, NULL, &error);
+  g_assert_no_error (error);
+
+  fixture->registry = grl_registry_get_default ();
+  grl_registry_load_plugin_by_id (fixture->registry, DLEYNA_PLUGIN_ID, &error);
+  g_assert_no_error (error);
+
+  /* Do not print warning messages, we want to test error conditions without
+   * generating too much noise. */
+  if (g_getenv ("GRL_DEBUG") == NULL) {
+    grl_log_configure ("dleyna:error");
+  }
+}
+
+void
+test_dleyna_shutdown (TestDleynaFixture *fixture,
+                      gconstpointer      user_data)
+{
+  GError *error = NULL;
+
+  g_clear_pointer (&fixture->main_loop, g_main_loop_unref);
+  g_clear_pointer (&fixture->results, g_ptr_array_unref);
+
+  grl_registry_unload_plugin (fixture->registry, DLEYNA_PLUGIN_ID, &error);
+  g_assert_no_error (error);
+
+  g_signal_handlers_disconnect_by_data (fixture->registry, fixture);
+
+  g_clear_object (&fixture->connection);
+
+  g_test_dbus_down (fixture->dbus);
+  g_clear_object (&fixture->dbus);
+}
+
+static gboolean
+timeout (gpointer user_data)
+{
+    g_assert_not_reached ();
+}
+
+void
+test_dleyna_main_loop_run (TestDleynaFixture *fixture,
+                           gint seconds)
+{
+  fixture->timeout_id = g_timeout_add_seconds (5, timeout, NULL);
+  g_main_loop_run (fixture->main_loop);
+}
+
+void
+test_dleyna_main_loop_quit (TestDleynaFixture *fixture)
+{
+  if (fixture->timeout_id != 0) {
+    g_source_remove (fixture->timeout_id);
+    fixture->timeout_id = 0;
+  }
+  g_main_loop_quit (fixture->main_loop);
+}
+
+void
+test_dleyna_add_server (TestDleynaFixture *fixture)
+{
+  GError *error = NULL;
+
+  g_dbus_connection_call_sync (fixture->connection, "com.intel.dleyna-server", "/com/intel/dLeynaServer",
+                               "org.freedesktop.DBus.Mock", "AddServer", NULL, NULL,
+                               G_DBUS_CALL_FLAGS_NONE, -1, NULL, &error);
+  g_assert_no_error (error);
+}
+
+void
+test_dleyna_drop_server (TestDleynaFixture *fixture,
+                         gchar             *object_path)
+{
+  GError *error = NULL;
+
+  g_dbus_connection_call_sync (fixture->connection, "com.intel.dleyna-server", "/com/intel/dLeynaServer",
+                               "org.freedesktop.DBus.Mock", "DropServer", g_variant_new ("(s)", object_path),
+                               NULL, G_DBUS_CALL_FLAGS_NONE, -1, NULL, &error);
+  g_assert_no_error (error);
+}
+
+void
+test_dleyna_queue_changes (TestDleynaFixture *fixture,
+                           gchar             *device_path,
+                           gboolean           enabled,
+                           gboolean           detailed)
+{
+  GVariant *params;
+  GError *error = NULL;
+
+  params = g_variant_new ("(bb)", enabled, detailed);
+  g_dbus_connection_call_sync (fixture->connection, "com.intel.dleyna-server", device_path,
+                               "org.freedesktop.DBus.Mock", "QueueChanges", params, NULL,
+                               G_DBUS_CALL_FLAGS_NONE, -1, NULL, &error);
+  g_assert_no_error (error);
+}
+
+void
+test_dleyna_flush_changes (TestDleynaFixture *fixture,
+                           gchar             *device_path)
+{
+  GError *error = NULL;
+
+  g_dbus_connection_call_sync (fixture->connection, "com.intel.dleyna-server", device_path,
+                               "org.freedesktop.DBus.Mock", "FlushChanges", NULL, NULL,
+                               G_DBUS_CALL_FLAGS_NONE, -1, NULL, &error);
+  g_assert_no_error (error);
+}
diff --git a/tests/dleyna/test_dleyna_utils.h b/tests/dleyna/test_dleyna_utils.h
new file mode 100644
index 0000000..c1c3c8f
--- /dev/null
+++ b/tests/dleyna/test_dleyna_utils.h
@@ -0,0 +1,58 @@
+/*
+ * Copyright © 2013 Intel Corporation. All rights reserved.
+ *
+ * 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, either 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, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef _GRL_DLEYNA_TEST_UTILS_H_
+#define _GRL_DLEYNA_TEST_UTILS_H_
+
+#include <gio/gio.h>
+#include <grilo.h>
+
+#define DLEYNA_PLUGIN_ID "grl-dleyna"
+
+typedef struct {
+  GTestDBus       *dbus;
+  GDBusConnection *connection;
+  GrlRegistry     *registry;
+  GMainLoop       *main_loop;
+  guint            timeout_id;
+
+  GPtrArray       *results; /* used to store results from callbacks */
+} TestDleynaFixture;
+
+void     test_dleyna_setup                 (TestDleynaFixture *fixture,
+                                            gconstpointer      user_data);
+
+void     test_dleyna_shutdown              (TestDleynaFixture *fixture,
+                                            gconstpointer      user_data);
+
+void     test_dleyna_main_loop_run         (TestDleynaFixture *fixture,
+                                            gint               seconds);
+
+void     test_dleyna_main_loop_quit        (TestDleynaFixture *fixture);
+
+void     test_dleyna_add_server            (TestDleynaFixture *fixture);
+
+void     test_dleyna_drop_server           (TestDleynaFixture *fixture,
+                                            gchar             *object_path);
+void     test_dleyna_queue_changes         (TestDleynaFixture *fixture,
+                                            gchar             *device_path,
+                                            gboolean           enabled,
+                                            gboolean           detailed);
+void     test_dleyna_flush_changes         (TestDleynaFixture *fixture,
+                                            gchar             *device_path);
+
+#endif /* _GRL_DLEYNA_TEST_UTILS_H_ */


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