[grilo] pls: Add new Grilo Playlist lib



commit 50e231075122405bad866191c829470c2f100467
Author: Mateu Batle <mateu batle collabora com>
Date:   Thu Sep 12 19:01:35 2013 +0200

    pls: Add new Grilo Playlist lib
    
    This utility library allows both plugins and applications to read
    playlists as if they were containers, using an API similar to the
    Sources.
    
    With additional fixes from Juan A. Suarez Romero and
    Bastien Nocera.
    
    https://bugzilla.gnome.org/show_bug.cgi?id=695303
    
    Signed-off-by: Bastien Nocera <hadess hadess net>
    Signed-off-by: Juan A. Suarez Romero <jasuarez igalia com>

 Makefile.am                  |    4 +
 configure.ac                 |   37 ++
 doc/grilo/grilo-docs.sgml    |    5 +
 doc/grilo/grilo-sections.txt |   10 +
 examples/Makefile.am         |    5 +-
 examples/browsing-pls.c      |  246 ++++++++
 grilo-pls-0.2.pc.in          |   15 +
 grilo-pls-uninstalled.pc.in  |   15 +
 libs/Makefile.am             |    6 +-
 libs/pls/Makefile.am         |   70 +++
 libs/pls/grl-pls.c           | 1372 ++++++++++++++++++++++++++++++++++++++++++
 libs/pls/grl-pls.h           |   79 +++
 po/POTFILES.in               |    1 +
 13 files changed, 1863 insertions(+), 2 deletions(-)
---
diff --git a/Makefile.am b/Makefile.am
index c05000b..33aadd6 100644
--- a/Makefile.am
+++ b/Makefile.am
@@ -24,6 +24,10 @@ if BUILD_GRILO_NET
 pkgconfig_DATA += grilo-net-0.2.pc
 endif
 
+if BUILD_GRILO_PLS
+pkgconfig_DATA += grilo-pls-0.2.pc
+endif
+
 dist_man1_MANS = grl-inspect.1
 
 MAINTAINERCLEANFILES = \
diff --git a/configure.ac b/configure.ac
index 55149d8..3974292 100644
--- a/configure.ac
+++ b/configure.ac
@@ -44,6 +44,12 @@ GRLNET_VERSION=0.2.3
 AC_SUBST(GRLNET_VERSION)
 AC_DEFINE_UNQUOTED(GRLNET_VERSION, "$GRLNET_VERSION", [Grilo Net library version])
 
+# Grilo Pls library
+
+GRLPLS_VERSION=0.2.0
+AC_SUBST(GRLPLS_VERSION)
+AC_DEFINE_UNQUOTED(GRLPLS_VERSION, "$GRLPLS_VERSION", [Grilo Pls library version])
+
 # ----------------------------------------------------------
 # LIBTOOL VERSIONING
 # ----------------------------------------------------------
@@ -53,9 +59,11 @@ AC_DEFINE_UNQUOTED(GRLNET_VERSION, "$GRLNET_VERSION", [Grilo Net library version
 
 GRL_LT_VERSION=5:0:4
 GRLNET_LT_VERSION=1:5:1
+GRLPLS_LT_VERSION=0:0:0
 
 AC_SUBST([GRL_LT_VERSION])
 AC_SUBST([GRLNET_LT_VERSION])
+AC_SUBST([GRLPLS_LT_VERSION])
 
 # ----------------------------------------------------------
 # ENVIRONMENT CONFIGURATION
@@ -195,6 +203,30 @@ AM_CONDITIONAL(BUILD_GRILO_NET, test "x$HAVE_LIBSOUP" = "xyes")
 AM_CONDITIONAL(BUILD_GRILO_NET_WITH_DEPRECATED_REQUESTER, test "x$HAVE_LIBSOUP_REQUESTER_DEPRECATED" = 
"xyes")
 
 # ----------------------------------------------------------
+# PLS LIBRARY
+# ----------------------------------------------------------
+
+PKG_CHECK_MODULES(TOTEM_PL_PARSER, totem-plparser >= 3.4.1, HAVE_TOTEM_PL_PARSER=yes, 
HAVE_TOTEM_PL_PARSER=no)
+
+AC_ARG_ENABLE([grl_pls],
+        AS_HELP_STRING([--enable-grl-pls],
+                [Enable Grilo Pls library (default: auto)]),
+        [
+                case "$enableval" in
+                     yes)
+                        if test "x$HAVE_TOTEM_PL_PARSER" = "xno"; then
+                           AC_MSG_ERROR([totem-pl-parser not found, install it or use --disable-grl-pls])
+                        fi
+                        ;;
+                     no)
+                        HAVE_TOTEM_PL_PARSER=no
+                        ;;
+                esac
+        ])
+
+AM_CONDITIONAL(BUILD_GRILO_PLS, test "x$HAVE_TOTEM_PL_PARSER" = "xyes")
+
+# ----------------------------------------------------------
 # DEBUG SUPPORT
 # ----------------------------------------------------------
 
@@ -299,6 +331,10 @@ if test "x$HAVE_LIBSOUP" = "xyes"; then
    AC_CONFIG_FILES([grilo-net-uninstalled.pc grilo-net-0.2.pc])
 fi
 
+if test "x$HAVE_TOTEM_PL_PARSER" = "xyes"; then
+   AC_CONFIG_FILES([grilo-pls-uninstalled.pc grilo-pls-0.2.pc])
+fi
+
 AC_CONFIG_FILES([
   Makefile
   grilo-uninstalled.pc
@@ -311,6 +347,7 @@ AC_CONFIG_FILES([
   tests/python/util.py
   libs/Makefile
   libs/net/Makefile
+  libs/pls/Makefile
   tools/Makefile
   tools/grilo-test-ui/Makefile
   tools/grilo-inspect/Makefile
diff --git a/doc/grilo/grilo-docs.sgml b/doc/grilo/grilo-docs.sgml
index aae5de6..7054e69 100644
--- a/doc/grilo/grilo-docs.sgml
+++ b/doc/grilo/grilo-docs.sgml
@@ -127,6 +127,11 @@
         <title>Grilo Net Classes</title>
         <xi:include href="xml/grl-net-wc.xml"/>
       </chapter>
+
+      <chapter id="grilo-pls">
+        <title>Grilo Playlist Functions</title>
+        <xi:include href="xml/grl-pls.xml"/>
+      </chapter>
     </reference>
 
   <index id="api-index-full">
diff --git a/doc/grilo/grilo-sections.txt b/doc/grilo/grilo-sections.txt
index c99e9fd..55edc41 100644
--- a/doc/grilo/grilo-sections.txt
+++ b/doc/grilo/grilo-sections.txt
@@ -769,3 +769,13 @@ grl_net_wc_get_type
 <SUBSECTION Private>
 GrlNetWcPrivate
 </SECTION>
+
+<SECTION>
+<FILE>grl-pls</FILE>
+<TITLE>GrlPls</TITLE>
+grl_pls_mime_is_playlist
+grl_pls_file_is_playlist
+grl_pls_media_is_playlist
+grl_pls_browse
+grl_pls_browse_sync
+</SECTION>
diff --git a/examples/Makefile.am b/examples/Makefile.am
index 2e95ff4..2432e5d 100644
--- a/examples/Makefile.am
+++ b/examples/Makefile.am
@@ -1,11 +1,14 @@
 AM_CFLAGS = $(DEPS_CFLAGS) -I$(top_srcdir)/src -I$(top_srcdir)/src/data
 LDADD = $(DEPS_LIBS) $(top_builddir)/src/lib GRL_NAME@.la
 
-noinst_PROGRAMS = browsing configuring-plugins efficient-metadata-resolution   \
+noinst_PROGRAMS = browsing browsing-pls configuring-plugins efficient-metadata-resolution      \
        loading-plugins multivalues searching
 
 browsing_SOURCES = browsing.c
 
+browsing_pls_SOURCES = browsing-pls.c
+browsing_pls_LDADD = $(LDADD) $(top_builddir)/libs/pls/libgrlpls- GRL_MAJORMINOR@.la
+
 configuring_plugins_SOURCES = configuring-plugins.c
 
 efficient_metadata_resolution_SOURCES = efficient-metadata-resolution.c
diff --git a/examples/browsing-pls.c b/examples/browsing-pls.c
new file mode 100644
index 0000000..9f6ad33
--- /dev/null
+++ b/examples/browsing-pls.c
@@ -0,0 +1,246 @@
+/*
+ * Browsing in Grilo.
+ * Shows the first BROWSE_CHUNK_SIZE elements of each browsable source
+ *
+ * XXX: No pagination yet! See grilo-test-ui. It's somewhat complicated.
+ */
+
+#include <grilo.h>
+#include <gio/gio.h>
+#include <glib.h>
+#include <glib/gprintf.h>
+#include "../libs/pls/grl-pls.h"
+
+#define GRL_LOG_DOMAIN_DEFAULT  example_log_domain
+GRL_LOG_DOMAIN_STATIC(example_log_domain);
+
+#define BROWSE_CHUNK_SIZE 10
+
+static void source_browser (gpointer data,
+                            gpointer user_data);
+static void element_browser (gpointer data,
+                             gpointer user_data);
+
+static void
+element_browser (gpointer data,
+                 gpointer user_data)
+{
+  GrlMedia *media = GRL_MEDIA (data);
+  GrlSource *source = GRL_SOURCE (user_data);
+
+  /* Check if we got a valid media object as some plugins may call the callback
+     with a NULL media under certain circumstances (for example when they
+     cannot estimate the number of remaining results and they find suddenly they
+     don't have any more results to send) */
+  if (!media) {
+    g_debug ("Media element is NULL!");
+    goto out;
+  }
+
+  const gchar *title = grl_media_get_title (media);
+
+  /* If the media is a container (box), that means we will browse it again */
+  if (GRL_IS_MEDIA_BOX (media)) {
+    guint childcount = grl_media_box_get_childcount (GRL_MEDIA_BOX (media));
+    g_debug ("\t Got '%s' (container with %d elements)", title, childcount);
+
+    source_browser (source, media);
+  } else {
+    const gchar *url = grl_media_get_url (media);
+    const gchar *mime = grl_media_get_mime (media);
+    GDateTime *date = grl_media_get_modification_date (media);
+    time_t rawdate = g_date_time_to_unix(date);
+    g_printf ("\t Got '%s', of type '%s', ctime is '%s'\n", title, mime, ctime(&rawdate));
+    g_printf ("\t\t URL: %s\n", url);
+  }
+
+out:
+  g_object_unref (media);
+}
+
+static void
+source_browser (gpointer data,
+                gpointer user_data)
+{
+  GrlSource *source = GRL_SOURCE (data);
+  GrlMedia *media = GRL_MEDIA (user_data);
+  GList *media_elements;
+  GError *error = NULL;
+  GList *keys;
+  GrlOperationOptions *options;
+  GrlCaps *caps;
+
+  keys = grl_metadata_key_list_new (GRL_METADATA_KEY_TITLE,
+                                    GRL_METADATA_KEY_URL,
+                                    GRL_METADATA_KEY_MODIFICATION_DATE,
+                                    GRL_METADATA_KEY_MIME,
+                                    GRL_METADATA_KEY_CHILDCOUNT,
+                                    NULL);
+
+  g_debug ("Detected new source available: '%s'",
+          grl_source_get_name (source));
+
+  if (!(grl_source_supported_operations (source) & GRL_OP_BROWSE))
+    goto out;
+
+  g_debug ("Browsing source: %s", grl_source_get_name (source));
+  /* Here is how you can browse a source, you have to provide:
+     1) The source you want to browse contents from.
+     2) The container object you want to browse (NULL for the root container)
+     3) A list of metadata keys we are interested in.
+     4) Options to control certain aspects of the browse operation.
+     5) A callback that the framework will invoke for each available result
+     6) User data for the callback
+     It returns an operation identifier that you can use to match results
+     with the corresponding request (we ignore it here) */
+
+  caps = grl_source_get_caps (source, GRL_OP_BROWSE);
+  options = grl_operation_options_new (caps);
+  grl_operation_options_set_count (options, BROWSE_CHUNK_SIZE);
+  grl_operation_options_set_flags (options, GRL_RESOLVE_IDLE_RELAY);
+  media_elements = grl_pls_browse_sync (GRL_SOURCE (source),
+                                        media, keys,
+                                        options,
+                                        NULL,
+                                        &error);
+  if (!media_elements) {
+    g_debug ("No elements found for source: %s!",
+             grl_source_get_name (source));
+    goto out;
+  }
+
+  if (error)
+    g_error ("Failed to browse source: %s", error->message);
+
+  g_list_foreach (media_elements, element_browser, source);
+
+out:
+  g_list_free (keys);
+  g_object_unref (options);
+}
+
+static void
+load_plugins (gchar* playlist)
+{
+  GrlRegistry *registry;
+  GrlSource *source;
+  GError *error = NULL;
+  GList *keys;
+  GrlOperationOptions *options;
+  GrlCaps *caps;
+  GrlMedia* media;
+  gboolean pls_media;
+  const gchar *mime;
+
+  registry = grl_registry_get_default ();
+
+  /* Load plugin */
+  if (!grl_registry_load_plugin_by_id (registry, "grl-filesystem", &error))
+    g_error ("Failed to load plugin: %s", error->message);
+
+  source = grl_registry_lookup_source (registry, "grl-filesystem");
+  if (!source)
+    g_error ("Unable to load grl-filesystem plugin");
+
+  if (!(grl_source_supported_operations (source) & GRL_OP_MEDIA_FROM_URI))
+    g_error ("Unable to get media from URI");
+
+  keys = grl_metadata_key_list_new (GRL_METADATA_KEY_TITLE, GRL_METADATA_KEY_URL, GRL_METADATA_KEY_MIME, 
NULL);
+  if (!keys)
+    g_error ("Unable to create key list");
+
+  caps = grl_source_get_caps (source, GRL_OP_MEDIA_FROM_URI);
+  if (!caps)
+    g_error ("Unable to get source caps");
+
+  options = grl_operation_options_new (caps);
+  if (!options)
+    g_error ("Unable to create operation options");
+
+  media = grl_source_get_media_from_uri_sync (source, playlist, keys, options, &error);
+  if (!media)
+    g_error ("Unable to get GrlMedia for playlist %s", playlist);
+
+  g_object_unref (options);
+
+  mime = grl_media_get_mime (media);
+
+  pls_media = grl_pls_media_is_playlist (media);
+
+  g_printf("Got Media for %s - mime=%s\n", playlist, mime);
+  g_printf("\tgrl_pls_media_is_playlist = %d\n", pls_media);
+
+  if (pls_media) {
+    source_browser (source, media);
+  }
+
+  g_object_unref (media);
+  g_object_unref (source);
+}
+
+static void
+config_plugins (gchar* chosen_test_path)
+{
+  GrlRegistry *registry;
+  GrlConfig *config;
+
+  registry = grl_registry_get_default ();
+
+  /* Configure plugin */
+  config = grl_config_new ("grl-filesystem", "Filesystem");
+  grl_config_set_string (config, "base-path", chosen_test_path);
+  grl_registry_add_config (registry, config, NULL);
+
+  g_printf ("config_plugin with %s\n", chosen_test_path);
+}
+
+gint
+main (int     argc,
+      gchar  *argv[])
+{
+  gchar *chosen_test_path;
+  gchar *file_uri;
+  GError *error = NULL;
+
+  grl_init (&argc, &argv);
+  GRL_LOG_DOMAIN_INIT (example_log_domain, "example");
+
+  if (argc != 2) {
+    g_printf ("Usage: %s <path to browse>\n", argv[0]);
+    return 1;
+  }
+
+  chosen_test_path = argv[1];
+  GFile *file = g_file_new_for_path (chosen_test_path);
+  if (!file) {
+    g_printf ("Invalid file/directory %s\n", argv[1]);
+    return 1;
+  }
+
+  GFileInfo *info = g_file_query_info (file,
+               G_FILE_ATTRIBUTE_STANDARD_TYPE,
+               0,
+               NULL,
+               &error);
+  if (!info) {
+    g_printf ("Invalid file/directory information\n");
+    return 1;
+  }
+
+  if (g_file_info_get_file_type (info) != G_FILE_TYPE_REGULAR) {
+    return 1;
+  }
+
+  gchar *dirname = g_path_get_dirname(chosen_test_path);
+  config_plugins (dirname);
+  g_free (dirname);
+
+  file_uri = g_filename_to_uri (chosen_test_path, NULL, &error);
+
+  g_object_unref (file);
+  g_object_unref (info);
+  load_plugins (file_uri);
+  g_free (file_uri);
+
+  return 0;
+}
diff --git a/grilo-pls-0.2.pc.in b/grilo-pls-0.2.pc.in
new file mode 100644
index 0000000..c25b0a2
--- /dev/null
+++ b/grilo-pls-0.2.pc.in
@@ -0,0 +1,15 @@
+prefix= prefix@
+exec_prefix= exec_prefix@
+libdir= libdir@
+includedir= includedir@/@GRL_NAME@
+datarootdir=${prefix}/share
+datadir=${datarootdir}
+girdir= INTROSPECTION_GIRDIR@
+typelibdir= INTROSPECTION_TYPELIBDIR@
+
+Name: Grilo playlist library
+Description: Grilo playlist utility
+Requires: @GRL_NAME@
+Version: @GRLPLS_VERSION@
+Libs: -L${libdir} -lgrlpls-0.2
+Cflags: -I${includedir}
diff --git a/grilo-pls-uninstalled.pc.in b/grilo-pls-uninstalled.pc.in
new file mode 100644
index 0000000..cbab596
--- /dev/null
+++ b/grilo-pls-uninstalled.pc.in
@@ -0,0 +1,15 @@
+# the standard variables don't make sense for an uninstalled copy
+prefix=
+exec_prefix=
+libdir=
+includedir=
+girdir= abs_top_builddir@/libs/
+typelibdir= abs_top_builddir@/libs
+
+Name: Grilo playlist library
+Description: Grilo playlist utility
+Requires: @GRL_NAME@
+Version: @GRLPLS_VERSION@
+
+Libs: @abs_top_builddir@/libs/pls/libgrlpls-0.2.la
+Cflags: -I abs_top_srcdir@/libs -I abs_top_builddir@/libs
diff --git a/libs/Makefile.am b/libs/Makefile.am
index 1c70fbf..dbb585a 100644
--- a/libs/Makefile.am
+++ b/libs/Makefile.am
@@ -11,6 +11,10 @@ if BUILD_GRILO_NET
 SUBDIRS += net
 endif
 
-DIST_SUBDIRS = net
+if BUILD_GRILO_PLS
+SUBDIRS += pls
+endif
+
+DIST_SUBDIRS = net pls
 
 -include $(top_srcdir)/git.mk
diff --git a/libs/pls/Makefile.am b/libs/pls/Makefile.am
new file mode 100644
index 0000000..3362137
--- /dev/null
+++ b/libs/pls/Makefile.am
@@ -0,0 +1,70 @@
+#
+# Makefile.am
+#
+# Author:  Mateu Batle <mateu batle collabora com>
+#
+# Copyright (C) 2013 Collabora Ltd. All rights reserved.
+
+
+lib_LTLIBRARIES = libgrlpls- GRL_MAJORMINOR@.la
+
+libgrlpls_ GRL_MAJORMINOR@_la_DEPENDENCIES =   \
+       $(top_builddir)/src/lib GRL_NAME@.la
+
+libgrlpls_ GRL_MAJORMINOR@_la_SOURCES = \
+       grl-pls.c
+
+libgrlpls_ GRL_MAJORMINOR@_la_CFLAGS = \
+       -I $(top_srcdir)/src            \
+       -I $(top_srcdir)/src/data       \
+       -DLOCALEDIR=\"$(localedir)\" \
+       $(DEPS_CFLAGS)                  \
+       $(TOTEM_PL_PARSER_CFLAGS)
+
+libgrlpls_ GRL_MAJORMINOR@_la_LIBADD = \
+       $(top_builddir)/src/lib GRL_NAME@.la    \
+       $(DEPS_LIBS)                            \
+       $(TOTEM_PL_PARSER_LIBS)
+
+libgrlpls_ GRL_MAJORMINOR@_la_LDFLAGS =        \
+       -version-info $(GRLPLS_LT_VERSION)              \
+       -no-undefined
+
+libgrlpls_ GRL_MAJORMINOR@includedir = \
+       $(includedir)/@GRL_NAME@/pls
+
+libgrlpls_ GRL_MAJORMINOR@include_HEADERS =    \
+       grl-pls.h
+
+CLEANFILES = *.gir
+
+# introspection support
+if HAVE_INTROSPECTION
+-include $(INTROSPECTION_MAKEFILE)
+gir_headers = $(patsubst %,$(srcdir)/%, $(libgrlpls_ GRL_MAJORMINOR@include_HEADERS))
+gir_sources = $(patsubst %,$(srcdir)/%, $(libgrlpls_ GRL_MAJORMINOR@_la_SOURCES))
+
+INTROSPECTION_GIRS =
+INTROSPECTION_SCANNER_ARGS = --warn-all
+
+introspection_sources =        \
+       $(gir_headers)  \
+       $(gir_sources)
+
+GrlPls- GRL_MAJORMINOR@.gir: libgrlpls- GRL_MAJORMINOR@.la
+GrlPls_ GRL_MAJORMINOR_NORM@_gir_INCLUDES = GObject-2.0 Gio-2.0 Grl- GRL_MAJORMINOR@
+GrlPls_ GRL_MAJORMINOR_NORM@_gir_CFLAGS = -I $(top_srcdir)/src \
+       -I $(top_srcdir)/src/data -I $(top_srcdir)/libs
+GrlPls_ GRL_MAJORMINOR_NORM@_gir_LIBS = libgrlpls- GRL_MAJORMINOR@.la \
+       $(top_builddir)/src/lib GRL_NAME@.la
+GrlPls_ GRL_MAJORMINOR_NORM@_gir_FILES = $(introspection_sources)
+INTROSPECTION_GIRS += GrlPls- GRL_MAJORMINOR@.gir
+
+girdir = @INTROSPECTION_GIRDIR@
+gir_DATA = $(INTROSPECTION_GIRS)
+
+typelibdir = @INTROSPECTION_TYPELIBDIR@
+typelib_DATA = $(INTROSPECTION_GIRS:.gir=.typelib)
+
+CLEANFILES += $(dist_gir_DATA) $(typelib_DATA)
+endif
diff --git a/libs/pls/grl-pls.c b/libs/pls/grl-pls.c
new file mode 100644
index 0000000..0ae4a49
--- /dev/null
+++ b/libs/pls/grl-pls.c
@@ -0,0 +1,1372 @@
+/*
+ * Copyright (C) 2013 Collabora Ltd.
+ *
+ * Author: Mateu Batle Sastre <mateu batle collabora com>
+ *         Bastien Nocera <hadess hadess net>
+ *
+ * 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
+ *
+ */
+
+/**
+ * SECTION:grl-pls
+ * @short_description: playlist handling functions
+ *
+ * Grilo only deals with audio, video or image content, but not with
+ * playlists. This library allow to identify playlists and browse into them
+ * exposing playlist entries as GrlMedia objects.
+ */
+
+#ifdef HAVE_CONFIG_H
+#include "config.h"
+#endif
+
+#include "grl-pls.h"
+
+#include "grl-operation-priv.h"
+#include "grl-sync-priv.h"
+
+#include <gio/gio.h>
+#include <glib/gi18n-lib.h>
+#include <grilo.h>
+#include <stdlib.h>
+#include <string.h>
+#include <totem-pl-parser.h>
+#include <totem-pl-parser-mini.h>
+
+/* --------- Constants -------- */
+
+#define GRL_DATA_PRIV_PLS_IS_PLAYLIST   "priv:pls:is_playlist"
+#define GRL_DATA_PRIV_PLS_VALID_ENTRIES "priv:pls:valid_entries"
+
+typedef enum {
+  GRL_PLS_IS_PLAYLIST_FALSE = -1,
+  GRL_PLS_IS_PLAYLIST_UNKNOWN = 0,
+  GRL_PLS_IS_PLAYLIST_TRUE = 1
+} _GrlPlsIsPlaylist;
+
+/* --------- Logging  -------- */
+
+#define GRL_LOG_DOMAIN_DEFAULT libpls_log_domain
+GRL_LOG_DOMAIN_STATIC(libpls_log_domain);
+
+/* -------- File info ------- */
+
+#ifndef G_FILE_ATTRIBUTE_THUMBNAIL_IS_VALID
+#define G_FILE_ATTRIBUTE_THUMBNAIL_IS_VALID "thumbnail::is-valid"
+#endif
+
+#define FILE_ATTRIBUTES                         \
+  G_FILE_ATTRIBUTE_STANDARD_NAME ","            \
+  G_FILE_ATTRIBUTE_STANDARD_DISPLAY_NAME ","    \
+  G_FILE_ATTRIBUTE_STANDARD_CONTENT_TYPE ","    \
+  G_FILE_ATTRIBUTE_STANDARD_TYPE ","            \
+  G_FILE_ATTRIBUTE_STANDARD_IS_HIDDEN ","       \
+  G_FILE_ATTRIBUTE_TIME_MODIFIED ","            \
+  G_FILE_ATTRIBUTE_THUMBNAIL_PATH ","           \
+  G_FILE_ATTRIBUTE_THUMBNAILING_FAILED ","      \
+  G_FILE_ATTRIBUTE_THUMBNAIL_IS_VALID
+
+/* -------- Data structures ------- */
+
+struct _GrlPlsPrivate {
+  gpointer user_data;
+  GCancellable *cancellable;
+  GrlPlsFilterFunc filter_func;
+  GPtrArray *entries;
+};
+
+struct OperationState {
+  GrlSource *source;
+  guint operation_id;
+  gboolean cancelled;
+  gboolean completed;
+  gboolean started;
+  GrlSourceBrowseSpec *bs;
+};
+
+/* -------- Prototypes ------- */
+
+static void
+grl_pls_cancel_cb (struct OperationState *op_state);
+static GrlMedia*
+grl_media_new_from_pls_entry (const gchar *uri,
+                              GHashTable *metadata);
+
+/* -------- Variables ------- */
+
+static GHashTable *operations = NULL;
+
+/* -------- Functions ------- */
+
+static void
+grl_pls_private_free (struct _GrlPlsPrivate *priv)
+{
+  g_return_if_fail (priv);
+
+  if (priv->cancellable) {
+    g_object_unref (priv->cancellable);
+    priv->cancellable = NULL;
+  }
+
+  g_free (priv);
+}
+
+static void
+grl_source_browse_spec_free (GrlSourceBrowseSpec *spec)
+{
+  if (spec->source) {
+    g_object_unref (spec->source);
+    spec->source = NULL;
+  }
+
+  if (spec->container) {
+    g_object_unref (spec->container);
+    spec->container = NULL;
+  }
+
+  if (spec->keys) {
+    /* TODO */
+    spec->keys = NULL;
+  }
+
+  if (spec->options) {
+    g_object_unref (spec->options);
+    spec->options = NULL;
+  }
+
+  if (spec->user_data) {
+    struct _GrlPlsPrivate *priv = (struct _GrlPlsPrivate *) spec->user_data;
+    grl_pls_private_free (priv);
+  }
+
+  g_free (spec);
+}
+
+static void
+grl_pls_entries_array_free (GPtrArray *entries)
+{
+  g_return_if_fail (entries);
+
+  g_ptr_array_free (entries, TRUE);
+}
+
+static void
+grl_pls_valid_entries_ptrarray_free (GPtrArray *valid_entries)
+{
+  g_return_if_fail (valid_entries);
+
+  g_ptr_array_free (valid_entries, TRUE);
+}
+
+static void
+grl_pls_init (void)
+{
+  static gboolean initialized = FALSE;
+
+  if (!initialized) {
+    GRL_LOG_DOMAIN_INIT (libpls_log_domain, "pls");
+
+    operations = g_hash_table_new_full (g_direct_hash, g_direct_equal,
+                                        NULL,
+                                        (GDestroyNotify) grl_source_browse_spec_free);
+
+    initialized = TRUE;
+  }
+}
+
+static gboolean
+mime_is_video (const gchar *mime)
+{
+  return g_content_type_is_a (mime, "video/*");
+}
+
+static gboolean
+mime_is_audio (const gchar *mime)
+{
+  return g_content_type_is_a (mime, "audio/*");
+}
+
+static gboolean
+mime_is_image (const gchar *mime)
+{
+  return g_content_type_is_a (mime, "image/*");
+}
+
+static void
+operation_state_free (struct OperationState *op_state)
+{
+  g_return_if_fail (op_state);
+
+  GRL_DEBUG ("%s (%p)", __FUNCTION__, op_state);
+
+  g_object_unref (op_state->source);
+  g_free (op_state);
+}
+
+/*
+ * operation_set_finished:
+ *
+ * Sets operation as finished (we have already emitted the last result
+ * to the user).
+ */
+static void
+operation_set_finished (guint operation_id)
+{
+  GRL_DEBUG ("%s (%d)", __FUNCTION__, operation_id);
+
+  grl_operation_remove (operation_id);
+}
+
+/*
+ * operation_set_completed:
+ *
+ * Sets the operation as completed (we have already received the last
+ * result in the relay cb. If it is finsihed it is also completed).
+ */
+static void
+operation_set_completed (guint operation_id)
+{
+  struct OperationState *op_state;
+
+  GRL_DEBUG ("%s (%d)", __FUNCTION__, operation_id);
+
+  op_state = grl_operation_get_private_data (operation_id);
+
+  if (op_state) {
+    op_state->completed = TRUE;
+  }
+}
+
+/*
+ * operation_is_completed:
+ *
+ * Checks if operation is completed (we have already received the last
+ * result in the relay cb. A finished operation is also a completed
+ * operation).
+ */
+static gboolean
+operation_is_completed (guint operation_id)
+{
+  struct OperationState *op_state;
+
+  op_state = grl_operation_get_private_data (operation_id);
+
+  return !op_state || op_state->completed;
+}
+
+/*
+ * operation_set_cancelled:
+ *
+ * Sets the operation as cancelled (a valid operation, i.e., not
+ * finished, was cancelled)
+ */
+static void
+operation_set_cancelled (guint operation_id)
+{
+  struct OperationState *op_state;
+
+  GRL_DEBUG ("%s (%d)", __FUNCTION__, operation_id);
+
+  op_state = grl_operation_get_private_data (operation_id);
+
+  if (op_state) {
+    op_state->cancelled = TRUE;
+  }
+}
+
+/*
+ * operation_is_cancelled:
+ *
+ * Checks if operation is cancelled (a valid operation that was
+ * cancelled).
+ */
+static gboolean
+operation_is_cancelled (guint operation_id)
+{
+  struct OperationState *op_state;
+
+  op_state = grl_operation_get_private_data (operation_id);
+
+  return op_state && op_state->cancelled;
+}
+
+/*
+ * operation_set_ongoing:
+ *
+ * Sets the operation as ongoing (operation is valid, not finished, not started
+ * and not cancelled)
+ */
+static void
+operation_set_ongoing (GrlSource *source, guint operation_id, GrlSourceBrowseSpec *bs)
+{
+  struct OperationState *op_state;
+
+  g_return_if_fail (source);
+
+  GRL_DEBUG ("%s (%d)", __FUNCTION__, operation_id);
+
+  op_state = g_new0 (struct OperationState, 1);
+  op_state->source = g_object_ref (source);
+  op_state->operation_id = operation_id;
+  op_state->bs = bs;
+
+  grl_operation_set_private_data (operation_id,
+                                  op_state,
+                                  (GrlOperationCancelCb) grl_pls_cancel_cb,
+                                  (GDestroyNotify) operation_state_free);
+}
+
+/*
+ * operation_is_ongoing:
+ *
+ * Checks if operation is ongoing (operation is valid, and it is not
+ * finished nor cancelled).
+ */
+static gboolean
+operation_is_ongoing (guint operation_id)
+{
+  struct OperationState *op_state;
+
+  op_state = grl_operation_get_private_data (operation_id);
+
+  return op_state && !op_state->cancelled;
+}
+
+static void
+grl_pls_cancel_cb (struct OperationState *op_state)
+{
+  struct _GrlPlsPrivate *priv;
+
+  g_return_if_fail (op_state);
+
+  GRL_DEBUG ("%s (%p)", __FUNCTION__, op_state);
+
+  if (!operation_is_ongoing (op_state->operation_id)) {
+    GRL_DEBUG ("Tried to cancel invalid or already cancelled operation. "
+               "Skipping...");
+    return;
+  }
+
+  operation_set_cancelled (op_state->operation_id);
+
+  /* Cancel the totem playlist parsing operation */
+  priv = (struct _GrlPlsPrivate *) op_state->bs->user_data;
+  if (priv && !g_cancellable_is_cancelled (priv->cancellable)) {
+    g_cancellable_cancel (priv->cancellable);
+  }
+}
+
+/**
+ * grl_pls_mime_is_playlist:
+ * @mime: mime type of the playlist
+ *
+ * Check if mime type corresponds to a playlist or not.
+ * This is quick to determine, but it does not offer full guarantees.
+ *
+ * Returns: %TRUE if mime type is a playlist recognized mime type
+ *
+ */
+static gboolean
+grl_pls_mime_is_playlist (const gchar *mime)
+{
+  grl_pls_init();
+
+  GRL_DEBUG ("%s (\"%s\")", __FUNCTION__, mime);
+
+  g_return_val_if_fail (mime, FALSE);
+
+  return g_str_has_prefix (mime, "audio/x-ms-asx") ||
+         g_str_has_prefix (mime, "audio/mpegurl") ||
+         g_str_has_prefix (mime, "audio/x-mpegurl") ||
+         g_str_has_prefix (mime, "audio/x-scpls");
+}
+
+static gboolean
+grl_pls_file_is_playlist (const gchar *uri)
+{
+  char *filename;
+  gboolean ret;
+
+  grl_pls_init();
+
+  GRL_DEBUG ("%s (\"%s\")", __FUNCTION__, uri);
+
+  g_return_val_if_fail (uri, FALSE);
+
+  filename = g_filename_from_uri (uri, NULL, NULL);
+  if (!filename)
+    return FALSE;
+
+  ret = totem_pl_parser_can_parse_from_filename (filename, FALSE);
+  g_free (filename);
+  return ret;
+}
+
+/**
+ * grl_pls_media_is_playlist:
+ * @media: GrlMedia
+ *
+ * Check if a file identified by GrlMedia object is a playlist or not.
+ * This function does blocking I/O.
+ *
+ * Returns: %TRUE if a GrlMedia is recognized as a playlist.
+ *
+ */
+gboolean
+grl_pls_media_is_playlist (GrlMedia *media)
+{
+  const gchar *playlist_url;
+  gpointer ptr;
+  _GrlPlsIsPlaylist is_pls;
+
+  grl_pls_init();
+
+  GRL_DEBUG ("%s (\"%p\") id=%s", __FUNCTION__, media,
+      media ? grl_media_get_id(media) : NULL);
+
+  g_return_val_if_fail (media, FALSE);
+
+  is_pls = GPOINTER_TO_INT (g_object_get_data (G_OBJECT (media), GRL_DATA_PRIV_PLS_IS_PLAYLIST));
+  if (is_pls != GRL_PLS_IS_PLAYLIST_UNKNOWN) {
+    GRL_DEBUG ("%s : using cached value = %d", __FUNCTION__, (is_pls == GRL_PLS_IS_PLAYLIST_TRUE));
+    return (is_pls == GRL_PLS_IS_PLAYLIST_TRUE);
+  }
+
+  playlist_url = grl_media_get_url (media);
+  if (!playlist_url) {
+    GRL_DEBUG ("%s: no URL found", __FUNCTION__);
+    return FALSE;
+  }
+
+  is_pls = grl_pls_file_is_playlist (playlist_url) ?
+          GRL_PLS_IS_PLAYLIST_TRUE : GRL_PLS_IS_PLAYLIST_FALSE;
+
+  ptr = GINT_TO_POINTER (is_pls);
+  g_object_set_data (G_OBJECT (media), GRL_DATA_PRIV_PLS_IS_PLAYLIST, ptr);
+  GRL_DEBUG ("%s : caching value = %d", __FUNCTION__, is_pls);
+
+  return (is_pls == GRL_PLS_IS_PLAYLIST_TRUE);
+}
+
+static void
+grl_pls_playlist_entry_parsed_cb (TotemPlParser *parser,
+                                  const gchar *uri,
+                                  GHashTable *metadata,
+                                  gpointer user_data)
+{
+  GrlSourceBrowseSpec *bs = (GrlSourceBrowseSpec *) user_data;
+  struct _GrlPlsPrivate *priv;
+  GrlMedia *media;
+  GError *_error;
+
+  priv = bs->user_data;
+
+  GRL_DEBUG ("%s (parser=%p, uri=\"%s\", metadata=%p, user_data=%p)",
+      __FUNCTION__, parser, uri, metadata, user_data);
+
+  g_return_if_fail (TOTEM_IS_PL_PARSER (parser));
+  g_return_if_fail (uri);
+  g_return_if_fail (metadata);
+  g_return_if_fail (user_data);
+  g_return_if_fail (bs->user_data);
+
+  priv = (struct _GrlPlsPrivate *) bs->user_data;
+
+  /* Ignore elements after operation has completed */
+  if (operation_is_completed (bs->operation_id)) {
+    GRL_WARNING ("Entry parsed after playlist completed for operation %d",
+                 bs->operation_id);
+    return;
+  }
+
+  /* Check if cancelled */
+  if (operation_is_cancelled (bs->operation_id)) {
+    GRL_DEBUG ("Operation was cancelled, skipping result until getting the last one");
+    /* Wait for the last element */
+    _error = g_error_new (GRL_CORE_ERROR,
+                          GRL_CORE_ERROR_OPERATION_CANCELLED,
+                          _("Operation was cancelled"));
+    bs->callback (bs->source, bs->operation_id, NULL, 0, priv->user_data, _error);
+    g_error_free (_error);
+    return;
+  }
+
+  media = grl_media_new_from_pls_entry (uri, metadata);
+  if (priv->filter_func != NULL)
+    media = (priv->filter_func) (bs->source, media, priv->user_data);
+
+  if (media && priv->entries) {
+    GRL_DEBUG ("New playlist entry: URI=%s", uri);
+    g_ptr_array_add (priv->entries, media);
+  } else {
+    GRL_DEBUG ("Ignored playlist entry: URI=%s", uri);
+  }
+}
+
+static GrlMedia*
+grl_media_new_from_pls_entry (const gchar *uri,
+                              GHashTable *metadata)
+{
+  GFile *file;
+  GrlOperationOptions *options;
+  GrlMedia *media;
+  const gchar *title, *thumbnail;
+  const gchar *description, *mimetype;
+  const gchar *duration_ms;
+
+  GRL_DEBUG ("%s (\"%s\")", __FUNCTION__, uri);
+
+  g_return_val_if_fail (uri, NULL);
+
+  file = g_file_new_for_uri (uri);
+  options = grl_operation_options_new (NULL);
+  grl_operation_options_set_flags (options, GRL_RESOLVE_FAST_ONLY);
+  media = grl_pls_file_to_media (NULL, file, NULL, FALSE, options);
+  g_object_unref (options);
+  g_object_unref (file);
+
+  title = g_hash_table_lookup (metadata, TOTEM_PL_PARSER_FIELD_TITLE);
+  if (title)
+    grl_media_set_title (media, title);
+  duration_ms = g_hash_table_lookup (metadata, TOTEM_PL_PARSER_FIELD_DURATION_MS);
+  if (duration_ms != NULL) {
+    grl_media_set_duration (media, g_ascii_strtoll (duration_ms, NULL, -1) / 1000);
+  } else {
+    gint64 duration;
+
+    duration = totem_pl_parser_parse_duration (g_hash_table_lookup (metadata, 
TOTEM_PL_PARSER_FIELD_DURATION), FALSE);
+    if (duration > 0)
+      grl_media_set_duration (media, duration);
+  }
+  thumbnail = g_hash_table_lookup (metadata, TOTEM_PL_PARSER_FIELD_IMAGE_URI);
+  if (thumbnail)
+    grl_media_set_thumbnail (media, thumbnail);
+  description = g_hash_table_lookup (metadata, TOTEM_PL_PARSER_FIELD_DESCRIPTION);
+  if (description)
+    grl_media_set_description (media, description);
+  mimetype = g_hash_table_lookup (metadata, TOTEM_PL_PARSER_FIELD_CONTENT_TYPE);
+  if (mimetype)
+    grl_media_set_mime (media, mimetype);
+
+  if (GRL_IS_MEDIA_AUDIO(media)) {
+    GrlMediaAudio *audio = GRL_MEDIA_AUDIO(media);
+    grl_media_audio_set_album (audio, g_hash_table_lookup (metadata, TOTEM_PL_PARSER_FIELD_ALBUM));
+    grl_media_audio_set_artist (audio, g_hash_table_lookup (metadata, TOTEM_PL_PARSER_FIELD_AUTHOR));
+    grl_media_audio_set_genre (audio, g_hash_table_lookup (metadata, TOTEM_PL_PARSER_FIELD_GENRE));
+  }
+
+  return media;
+}
+
+static gint
+grl_pls_browse_report_error (GrlSourceBrowseSpec *bs, const gchar *message)
+{
+  struct _GrlPlsPrivate *priv = (struct _GrlPlsPrivate *) bs->user_data;
+
+  GError *error = g_error_new_literal (GRL_CORE_ERROR,
+                                       GRL_CORE_ERROR_BROWSE_FAILED,
+                                       message);
+  bs->callback (bs->source, bs->operation_id, bs->container, 0, priv->user_data, error);
+  g_error_free (error);
+
+  return FALSE;
+}
+
+static gboolean
+grl_pls_browse_report_results (GrlSourceBrowseSpec *bs)
+{
+  guint skip;
+  guint count;
+  guint remaining;
+  GPtrArray *valid_entries;
+  struct _GrlPlsPrivate *priv;
+  gboolean called_from_plugin;
+
+  GRL_DEBUG ("%s (bs=%p)", __FUNCTION__, bs);
+
+  g_return_val_if_fail (bs, FALSE);
+  g_return_val_if_fail (bs->container, FALSE);
+  g_return_val_if_fail (bs->options, FALSE);
+  g_return_val_if_fail (bs->operation_id, FALSE);
+  g_return_val_if_fail (bs->user_data, FALSE);
+
+  priv = bs->user_data;
+
+  valid_entries = g_object_get_data (G_OBJECT (bs->container),
+      GRL_DATA_PRIV_PLS_VALID_ENTRIES);
+  if (valid_entries) {
+    skip = grl_operation_options_get_skip (bs->options);
+    if (skip > valid_entries->len)
+      skip = valid_entries->len;
+
+    count = grl_operation_options_get_count (bs->options);
+    if (skip + count > valid_entries->len)
+      count = valid_entries->len - skip;
+
+    remaining = MIN (valid_entries->len - skip, count);
+  } else {
+    skip = 0;
+    count = 0;
+    remaining = 0;
+  }
+
+  GRL_DEBUG ("%s, skip: %d, count: %d, remaining: %d, num entries: %d",
+             __FUNCTION__, skip, count, remaining, valid_entries->len);
+
+  if (remaining) {
+    int i;
+
+    for (i = 0;i < count;i++) {
+      GrlMedia *content;
+
+      content = g_ptr_array_index (valid_entries, skip + i);
+      g_object_ref (content);
+      remaining--;
+      bs->callback (bs->source,
+               bs->operation_id,
+               content,
+               remaining,
+               priv->user_data,
+               NULL);
+      GRL_DEBUG ("callback called source=%p id=%d content=%p remaining=%d user_data=%p",
+          bs->source, bs->operation_id, content, remaining, priv->user_data);
+    }
+  } else {
+    bs->callback (bs->source,
+             bs->operation_id,
+             NULL,
+             0,
+             priv->user_data,
+             NULL);
+  }
+
+  called_from_plugin = g_hash_table_lookup (operations,
+      GUINT_TO_POINTER (bs->operation_id)) == NULL;
+
+  if (!called_from_plugin) {
+    operation_set_completed (bs->operation_id);
+    operation_set_finished (bs->operation_id);
+    g_hash_table_remove (operations, GUINT_TO_POINTER (bs->operation_id));
+  }
+
+  return FALSE;
+}
+
+static void
+grl_pls_playlist_parse_cb (GObject *object,
+                           GAsyncResult *result,
+                           gpointer user_data)
+{
+  TotemPlParser *parser = (TotemPlParser *) object;
+  TotemPlParserResult retval;
+  GrlSourceBrowseSpec *bs = (GrlSourceBrowseSpec *) user_data;
+  struct _GrlPlsPrivate *priv;
+  GError *error = NULL;
+  guint i;
+  GPtrArray *valid_entries;
+
+  GRL_DEBUG ("%s (object=%p, result=%p, user_data=%p)", __FUNCTION__, object, result, user_data);
+
+  g_return_if_fail (object);
+  g_return_if_fail (result);
+  g_return_if_fail (bs);
+  g_return_if_fail (bs->operation_id);
+  g_return_if_fail (bs->container);
+  g_return_if_fail (bs->user_data);
+
+  priv = bs->user_data;
+
+  retval = totem_pl_parser_parse_finish (parser, result, &error);
+  if (retval != TOTEM_PL_PARSER_RESULT_SUCCESS) {
+    if (retval == TOTEM_PL_PARSER_RESULT_ERROR) {
+      if (!g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED))
+        GRL_ERROR ("Playlist parsing failed, retval=%d code=%d msg=%s", retval, error->code, error->message);
+      g_error_free (error);
+    }
+    return;
+  }
+
+  valid_entries = g_object_get_data (G_OBJECT (bs->container), GRL_DATA_PRIV_PLS_VALID_ENTRIES);
+
+  /* process all entries to see which ones are valid */
+  for (i = 0;i < priv->entries->len;i++) {
+    struct GrlMedia *media;
+    media = g_ptr_array_index (priv->entries, i);
+    g_ptr_array_add (valid_entries, g_object_ref (media));
+  }
+
+  /* at this point we can free entries, not used anymore */
+  grl_pls_entries_array_free (priv->entries);
+  priv->entries = NULL;
+
+  if (GRL_IS_MEDIA_BOX (bs->container)) {
+    GrlMediaBox *box = GRL_MEDIA_BOX (bs->container);
+    grl_media_box_set_childcount (box, valid_entries->len);
+  }
+
+  grl_pls_browse_report_results (bs);
+}
+
+static gboolean
+check_options (GrlSource *source,
+               GrlSupportedOps operation,
+               GrlOperationOptions *options)
+{
+  if (grl_operation_options_get_count (options) == 0)
+    return FALSE;
+
+  /* Check only if the source supports the operation */
+  if (grl_source_supported_operations (source) & operation) {
+    GrlCaps *caps;
+    caps = grl_source_get_caps (source, operation);
+    return grl_operation_options_obey_caps (options, caps, NULL, NULL);
+  } else {
+    return TRUE;
+  }
+}
+
+static void
+multiple_result_async_cb (GrlSource *source,
+                          guint op_id,
+                          GrlMedia *media,
+                          guint remaining,
+                          gpointer user_data,
+                          const GError *error)
+{
+  GrlDataSync *ds = (GrlDataSync *) user_data;
+
+  GRL_DEBUG (__FUNCTION__);
+
+  if (error) {
+    ds->error = g_error_copy (error);
+
+    /* Free previous results */
+    g_list_foreach (ds->data, (GFunc) g_object_unref, NULL);
+    g_list_free (ds->data);
+
+    ds->data = NULL;
+    ds->complete = TRUE;
+    return;
+  }
+
+  if (media) {
+    ds->data = g_list_prepend (ds->data, media);
+  }
+
+  if (remaining == 0) {
+    ds->data = g_list_reverse (ds->data);
+    ds->complete = TRUE;
+  }
+}
+
+/**
+ * grl_pls_browse_by_spec:
+ * @source: a source
+ * @filter_func: (scope async): A filter function
+ * @bs: a GrlSourceBrowseSpec structure with details of the browsing operation
+ *
+ * Browse into a playlist. The playlist entries are
+ * returned via the bs->callback function as GrlMedia objects.
+ * This function is more suitable to be called from plugins, which by
+ * design get the GrlSourceBrowseSpec already filled in.
+ *
+ * The bs->playlist provided could be of any GrlMedia class,
+ * as long as its URI points to a valid playlist file.
+ *
+ * This function is asynchronous.
+ *
+ * See #grl_pls_browse() and #grl_source_browse() function for additional
+ * information and sample code.
+ *
+ */
+void
+grl_pls_browse_by_spec (GrlSource *source,
+                        GrlPlsFilterFunc filter_func,
+                        GrlSourceBrowseSpec *bs)
+{
+  TotemPlParser *parser;
+  const char *playlist_url;
+  struct _GrlPlsPrivate *priv;
+  GPtrArray *valid_entries;
+
+  grl_pls_init();
+
+  GRL_DEBUG (__FUNCTION__);
+
+  g_return_if_fail (GRL_IS_SOURCE (source));
+  g_return_if_fail (GRL_IS_MEDIA (bs->container));
+  g_return_if_fail (GRL_IS_OPERATION_OPTIONS (bs->options));
+  g_return_if_fail (bs->callback != NULL);
+  g_return_if_fail (grl_source_supported_operations (bs->source) &
+                    GRL_OP_BROWSE);
+  g_return_if_fail (check_options (source, GRL_OP_BROWSE, bs->options));
+
+  priv = g_new0 (struct _GrlPlsPrivate, 1);
+  priv->user_data = bs->user_data;
+  priv->cancellable = g_cancellable_new ();
+  priv->filter_func = filter_func;
+  bs->user_data = priv;
+
+  playlist_url = grl_media_get_url (bs->container);
+  if (!playlist_url) {
+    GRL_DEBUG ("%s : Unable to get URL from Media %p", __FUNCTION__, bs->container);
+    grl_pls_browse_report_error (bs, "Unable to get URL from Media");
+    return;
+  }
+
+  /* check if we have the entries cached or not */
+  valid_entries = g_object_get_data (G_OBJECT (bs->container), GRL_DATA_PRIV_PLS_VALID_ENTRIES);
+  if (valid_entries) {
+    GRL_DEBUG ("%s : using cached data bs=%p", __FUNCTION__, bs);
+    g_idle_add ((GSourceFunc) grl_pls_browse_report_results, bs);
+    return;
+  }
+
+  priv->entries = g_ptr_array_new_with_free_func (g_object_unref);
+  valid_entries = g_ptr_array_new_with_free_func (g_object_unref);
+
+  parser = totem_pl_parser_new ();
+
+  g_object_set_data_full (G_OBJECT (bs->container),
+      GRL_DATA_PRIV_PLS_VALID_ENTRIES,
+      valid_entries,
+      (GDestroyNotify) grl_pls_valid_entries_ptrarray_free);
+
+  /*
+   * disable-unsafe: if %TRUE the parser will not parse unsafe locations,
+   * such as local devices and local files if the playlist isn't local.
+   * This is useful if the library is parsing a playlist from a remote
+   * location such as a website. */
+  g_object_set (parser,
+                "recurse", FALSE,
+                "disable-unsafe", TRUE,
+                NULL);
+  g_signal_connect (G_OBJECT (parser),
+                    "entry-parsed",
+                    G_CALLBACK (grl_pls_playlist_entry_parsed_cb),
+                    bs);
+
+  totem_pl_parser_parse_async (parser,
+                               playlist_url,
+                               FALSE,
+                               priv->cancellable,
+                               grl_pls_playlist_parse_cb,
+                               bs);
+
+  g_object_unref (parser);
+}
+
+/**
+ * grl_pls_browse:
+ * @source: a source
+ * @playlist: a playlist
+ * @keys: (element-type GrlKeyID): the #GList of
+ * #GrlKeyID<!-- -->s to request
+ * @options: options wanted for that operation
+ * @filter_func: (scope async): A filter function
+ * @callback: (scope notified): the user defined callback
+ * @user_data: the user data to pass in the callback
+ *
+ * Browse into a playlist. The playlist entries are
+ * returned via the @callback function as GrlMedia objects.
+ * This function imitates the API and way of working of
+ * #grl_source_browse.
+ *
+ * The @playlist provided could be of any GrlMedia class,
+ * as long as its URI points to a valid playlist file.
+ *
+ * This function is asynchronous.
+ *
+ * See #grl_source_browse() function for additional information
+ * and sample code.
+ *
+ * Returns: the operation identifier
+ *
+ */
+guint
+grl_pls_browse (GrlSource *source,
+                GrlMedia *playlist,
+                const GList *keys,
+                GrlOperationOptions *options,
+                GrlPlsFilterFunc filter_func,
+                GrlSourceResultCb callback,
+                gpointer userdata)
+{
+  GrlSourceBrowseSpec *bs;
+
+  grl_pls_init();
+
+  GRL_DEBUG (__FUNCTION__);
+
+  g_return_val_if_fail (GRL_IS_SOURCE (source), 0);
+  g_return_val_if_fail (GRL_IS_MEDIA (playlist), 0);
+  g_return_val_if_fail (GRL_IS_OPERATION_OPTIONS (options), 0);
+  g_return_val_if_fail (callback != NULL, 0);
+  g_return_val_if_fail (grl_source_supported_operations (source) &
+                        GRL_OP_BROWSE, 0);
+  g_return_val_if_fail (check_options (source, GRL_OP_BROWSE, options), 0);
+
+  bs = g_new0 (GrlSourceBrowseSpec, 1);
+
+  bs->source = g_object_ref (source);
+  bs->container = g_object_ref (playlist);
+  /* TODO: what to do with keys */
+  bs->keys = NULL;
+  bs->options = grl_operation_options_copy (options);
+  bs->callback = callback;
+  bs->user_data = userdata;
+  bs->operation_id = grl_operation_generate_id ();
+
+  g_hash_table_insert (operations, GUINT_TO_POINTER (bs->operation_id), bs);
+
+  operation_set_ongoing (source, bs->operation_id, bs);
+
+  grl_pls_browse_by_spec (source, filter_func, bs);
+
+  return bs->operation_id;
+}
+
+/**
+ * grl_pls_browse_sync:
+ * @source: a source
+ * @playlist: a playlist
+ * @keys: (element-type GrlKeyID): the #GList of
+ * #GrlKeyID<!-- -->s to request
+ * @filter_func: (scope async): A filter function
+ * @options: options wanted for that operation
+ * @error: a #GError, or @NULL
+ *
+ * Browse into a playlist. The playlist entries are
+ * returned via the @callback function as GrlMedia objects.
+ * This function imitates the API and way of working of
+ * #grl_source_browse_sync.
+ *
+ * The filter function @filter_func will be used for plugins
+ * or applications to be able to refuse particular entries from
+ * being listed.
+ *
+ * If a %NULL filter function is passed, the media will be added
+ * with only the metadata coming from the playlist included.
+ *
+ * This function is synchronous.
+ *
+ * See #grl_source_browse_sync() function for additional information
+ * and sample code.
+ *
+ * Returns: (element-type Grl.Media) (transfer full): a #GList with #GrlMedia
+ * elements. After use g_object_unref() every element and g_list_free() the
+ * list.
+ *
+ */
+GList *
+grl_pls_browse_sync (GrlSource *source,
+                     GrlMedia *playlist,
+                     const GList *keys,
+                     GrlOperationOptions *options,
+                     GrlPlsFilterFunc filter_func,
+                     GError **error)
+{
+  GrlDataSync *ds;
+  GList *result;
+
+  grl_pls_init();
+
+  GRL_DEBUG (__FUNCTION__);
+
+  ds = g_slice_new0 (GrlDataSync);
+
+  if (grl_pls_browse (source,
+                      playlist,
+                      keys,
+                      options,
+                      filter_func,
+                      multiple_result_async_cb,
+                      ds))
+    grl_wait_for_async_operation_complete (ds);
+
+  if (ds->error)
+    g_propagate_error (error, ds->error);
+
+  result = (GList *) ds->data;
+  g_slice_free (GrlDataSync, ds);
+
+  return result;
+}
+
+static gboolean
+mime_is_media (const gchar *mime, GrlTypeFilter filter)
+{
+  if (!mime)
+    return FALSE;
+  if (!strcmp (mime, "inode/directory"))
+    return TRUE;
+  if (filter & GRL_TYPE_FILTER_AUDIO &&
+      mime_is_audio (mime))
+    return TRUE;
+  if (filter & GRL_TYPE_FILTER_VIDEO &&
+      mime_is_video (mime))
+    return TRUE;
+  if (filter & GRL_TYPE_FILTER_IMAGE &&
+      mime_is_image (mime))
+    return TRUE;
+  return FALSE;
+}
+
+static gboolean
+file_is_valid_content (GFileInfo *info, gboolean fast, GrlOperationOptions *options)
+{
+  const gchar *mime;
+  const gchar *mime_filter = NULL;
+  GValue *mime_filter_value = NULL;
+  GValue *min_date_value = NULL;
+  GValue *max_date_value = NULL;
+  GDateTime *min_date = NULL;
+  GDateTime *max_date = NULL;
+  GDateTime *file_date = NULL;
+  GrlTypeFilter type_filter;
+  gboolean is_media = TRUE;
+  GFileType type;
+
+  /* Ignore hidden files */
+  if (g_file_info_get_is_hidden (info)) {
+      is_media = FALSE;
+      goto end;
+  }
+
+  type = g_file_info_get_file_type (info);
+
+  /* Directories are always accepted */
+  if (type == G_FILE_TYPE_DIRECTORY) {
+    goto end;
+  }
+
+  type_filter = options? grl_operation_options_get_type_filter (options): GRL_TYPE_FILTER_ALL;
+
+  /* In fast mode we do not check mime-types, any non-hidden file is accepted */
+  if (fast) {
+    if (type_filter == GRL_TYPE_FILTER_NONE) {
+      is_media = FALSE;
+    }
+    goto end;
+  }
+
+  /* Filter by type */
+  mime = g_file_info_get_content_type (info);
+  if (!mime_is_media (mime, type_filter)) {
+    is_media = FALSE;
+    goto end;
+  }
+
+  /* Filter by mime */
+  mime_filter_value =
+    options? grl_operation_options_get_key_filter (options,
+                                                   GRL_METADATA_KEY_MIME): NULL;
+  if (mime_filter_value) {
+    mime_filter = g_value_get_string (mime_filter_value);
+  }
+
+  if (mime_filter && g_strcmp0 (mime, mime_filter) != 0) {
+    is_media = FALSE;
+    goto end;
+  }
+
+  /* Filter by date */
+  if (options) {
+    grl_operation_options_get_key_range_filter (options,
+                                                GRL_METADATA_KEY_MODIFICATION_DATE,
+                                                &min_date_value,
+                                                &max_date_value);
+  }
+
+  if (min_date_value) {
+    min_date = g_date_time_ref (g_value_get_boxed (min_date_value));
+  }
+  if (max_date_value) {
+    max_date = g_date_time_ref (g_value_get_boxed (max_date_value));
+  }
+
+  if (min_date || max_date) {
+    GTimeVal time = {0,};
+
+    g_file_info_get_modification_time (info, &time);
+    file_date = g_date_time_new_from_timeval_utc (&time);
+  }
+
+  if (min_date && file_date && g_date_time_compare (min_date, file_date) > 0) {
+    is_media = FALSE;
+    goto end;
+  }
+
+  if (max_date && file_date && g_date_time_compare (max_date, file_date) < 0) {
+    is_media = FALSE;
+    goto end;
+  }
+
+ end:
+  if (file_date)
+    g_date_time_unref (file_date);
+  if (min_date)
+    g_date_time_unref (min_date);
+  if (max_date)
+    g_date_time_unref (max_date);
+  return is_media;
+}
+
+static void
+set_container_childcount (GFile               *file,
+                          GrlMedia            *media,
+                          GrlOperationOptions *options)
+{
+  GFileEnumerator *e;
+  GFileInfo *info;
+  GError *error = NULL;
+  gint count = 0;
+  char *uri;
+
+  /* in fast mode we don't compute  mime-types because it is slow,
+     so we can only check if the directory is totally empty (no subdirs,
+     and no files), otherwise we just say we do not know the actual
+     childcount */
+  if (grl_operation_options_get_flags (options) & GRL_RESOLVE_FAST_ONLY) {
+    grl_media_box_set_childcount (GRL_MEDIA_BOX (media),
+                                  GRL_METADATA_KEY_CHILDCOUNT_UNKNOWN);
+    return;
+  }
+
+  /* Open directory */
+  uri = g_file_get_uri (file);
+  GRL_DEBUG ("Opening directory '%s' for childcount", uri);
+  g_free (uri);
+  e = g_file_enumerate_children (file,
+                                 FILE_ATTRIBUTES,
+                                 G_FILE_QUERY_INFO_NONE,
+                                 NULL,
+                                 &error);
+  if (!e) {
+    GRL_DEBUG ("Failed to open directory: %s", error->message);
+    g_error_free (error);
+    return;
+  }
+
+  /* Count valid entries */
+  count = 0;
+  while ((info = g_file_enumerator_next_file (e, NULL, NULL)) != NULL) {
+    if (file_is_valid_content (info, FALSE, options))
+      count++;
+    g_object_unref (info);
+  }
+
+  g_object_unref (e);
+
+  grl_media_box_set_childcount (GRL_MEDIA_BOX (media), count);
+}
+
+static void
+set_media_id_from_file (GrlMedia *media,
+                        GFile    *file)
+{
+  char *uri;
+
+  uri = g_file_get_uri (file);
+  grl_media_set_id (media, uri);
+  g_free (uri);
+}
+
+/**
+ * grl_pls_file_to_media:
+ * @content: an existing #GrlMedia for the file, or %NULL
+ * @file: a #GFile pointing to the file or directory in question
+ * @info: an existing #GFileInfo, or %NULL
+ * @handle_pls: Whether playlists should be handled as containers
+ * @options: a #GrlOperationOptions representing the options to apply
+ *   to this operation.
+ *
+ * This function will update (if @content is non-%NULL) or create a
+ * GrlMedia and populate it with information from @info.
+ *
+ * If @info is %NULL, a call to g_file_query_info() will be made.
+ *
+ * This function is useful for plugins that browse the local filesystem
+ * and want to easily create GrlMedia from filesystem information.
+ *
+ * Returns: (transfer full): a new #GrlMedia.
+ *
+ */
+GrlMedia *
+grl_pls_file_to_media (GrlMedia            *content,
+                       GFile               *file,
+                       GFileInfo           *info,
+                       gboolean             handle_pls,
+                       GrlOperationOptions *options)
+{
+  GrlMedia *media = NULL;
+  gchar *str;
+  gchar *extension;
+  const gchar *mime;
+  gboolean thumb_failed, thumb_is_valid;
+  GError *error = NULL;
+  gboolean is_pls = FALSE;
+
+  g_return_val_if_fail (file != NULL, NULL);
+  g_return_val_if_fail (options != NULL, NULL);
+
+  grl_pls_init ();
+
+  if (!info) {
+    if (!g_file_has_uri_scheme (file, "http") &&
+        !g_file_has_uri_scheme (file, "https"))
+      info = g_file_query_info (file,
+                                FILE_ATTRIBUTES,
+                                0,
+                                NULL,
+                                &error);
+  } else {
+    info = g_object_ref (info);
+  }
+
+  /* Update mode */
+  if (content)
+    media = content;
+
+  if (info == NULL) {
+    char *uri;
+
+    uri = g_file_get_uri (file);
+    GRL_DEBUG ("Failed to get info for file '%s': %s", uri,
+               error ? error->message : "No details");
+    g_free (uri);
+
+    if (!media) {
+      media = grl_media_new ();
+      set_media_id_from_file (media, file);
+    }
+
+    /* Title */
+    str = g_file_get_basename (file);
+
+    /* Remove file extension */
+    extension = g_strrstr (str, ".");
+    if (extension) {
+      *extension = '\0';
+    }
+
+    grl_media_set_title (media, str);
+    g_clear_error (&error);
+    g_free (str);
+  } else {
+    mime = g_file_info_get_content_type (info);
+
+    if (!media) {
+      if (g_file_info_get_file_type (info) == G_FILE_TYPE_DIRECTORY) {
+        media = GRL_MEDIA (grl_media_box_new ());
+      } else if (handle_pls && grl_pls_mime_is_playlist (mime)) {
+        media = GRL_MEDIA (grl_media_box_new ());
+        is_pls = TRUE;
+      } else if (mime_is_video (mime)) {
+        media = grl_media_video_new ();
+      } else if (mime_is_audio (mime)) {
+        media = grl_media_audio_new ();
+      } else if (mime_is_image (mime)) {
+        media = grl_media_image_new ();
+      } else {
+        media = grl_media_new ();
+      }
+      set_media_id_from_file (media, file);
+    }
+
+    if (!GRL_IS_MEDIA_BOX (media)) {
+      grl_media_set_mime (media, mime);
+    }
+
+    /* Title */
+    str = g_strdup (g_file_info_get_display_name (info));
+
+    /* Remove file extension */
+    if (!GRL_IS_MEDIA_BOX (media) || is_pls) {
+      extension = g_strrstr (str, ".");
+      if (extension) {
+        *extension = '\0';
+      }
+    }
+
+    grl_media_set_title (media, str);
+    g_free (str);
+
+    /* Date */
+    GTimeVal time;
+    GDateTime *date_time;
+    g_file_info_get_modification_time (info, &time);
+    date_time = g_date_time_new_from_timeval_utc (&time);
+    grl_media_set_modification_date (media, date_time);
+    g_date_time_unref (date_time);
+
+    /* Thumbnail */
+    thumb_failed =
+      g_file_info_get_attribute_boolean (info,
+                                         G_FILE_ATTRIBUTE_THUMBNAILING_FAILED);
+    thumb_is_valid = TRUE;
+    if (g_file_info_has_attribute (info, G_FILE_ATTRIBUTE_THUMBNAIL_IS_VALID))
+      thumb_is_valid =
+        g_file_info_get_attribute_boolean (info,
+                                           G_FILE_ATTRIBUTE_THUMBNAIL_IS_VALID);
+
+    if (!thumb_failed && thumb_is_valid) {
+      const gchar *thumb =
+        g_file_info_get_attribute_byte_string (info,
+                                               G_FILE_ATTRIBUTE_THUMBNAIL_PATH);
+      if (thumb) {
+        gchar *thumb_uri = g_filename_to_uri (thumb, NULL, NULL);
+        if (thumb_uri) {
+          grl_media_set_thumbnail (media, thumb_uri);
+          g_free (thumb_uri);
+        }
+      }
+    }
+
+    g_object_unref (info);
+  }
+
+  /* URL */
+  str = g_file_get_uri (file);
+  grl_media_set_url (media, str);
+  g_free (str);
+
+  /* Childcount */
+  if (GRL_IS_MEDIA_BOX (media) && !is_pls)
+    set_container_childcount (file, media, options);
+
+  return media;
+}
+
+/**
+ * grl_pls_get_file_attributes:
+ *
+ * Returns the list of attributes to pass to
+ * g_file_query_info() to make it possible to
+ * populate a GrlMedia using grl_pls_file_to_media().
+ *
+ * Do not free the result of this function.
+ *
+ * Returns: (transfer none): a string containing the
+ * list of attributes.
+ *
+ */
+const char *
+grl_pls_get_file_attributes (void)
+{
+  return FILE_ATTRIBUTES;
+}
diff --git a/libs/pls/grl-pls.h b/libs/pls/grl-pls.h
new file mode 100644
index 0000000..6809d1b
--- /dev/null
+++ b/libs/pls/grl-pls.h
@@ -0,0 +1,79 @@
+/*
+ * Copyright (C) 2013 Collabora Ltd.
+ *
+ * Authors: Mateu Batle Sastre <mateu batle collabora com>
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public License
+ * as published by the Free Software Foundation; version 2.1 of
+ * the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
+ * 02110-1301 USA
+ *
+ */
+
+#ifndef _GRL_PLS_H_
+#define _GRL_PLS_H_
+
+#include <grilo.h>
+#include <gio/gio.h>
+
+G_BEGIN_DECLS
+
+/**
+ * GrlPlsFilterFunc:
+ * @source: the #GrlSource the browse call came from
+ * @media: a #GrlMedia to operate on
+ * @user_data: user data passed to the browse call
+ *
+ * Callback type to filter, or modify #GrlMedia created
+ * when parsing a playlist using one of grl_pls_browse(),
+ * grl_pls_browse_sync() or grl_pls_browse_by_spec().
+ *
+ * Returns: %NULL to not add this entry to the results,
+ *   or a new #GrlMedia populated with metadata of your choice.
+ */
+typedef GrlMedia * (*GrlPlsFilterFunc) (GrlSource *source,
+                                        GrlMedia  *media,
+                                        gpointer   user_data);
+
+gboolean grl_pls_media_is_playlist (GrlMedia *media);
+
+void grl_pls_browse_by_spec (GrlSource *source,
+                             GrlPlsFilterFunc filter_func,
+                             GrlSourceBrowseSpec *bs);
+
+guint grl_pls_browse (GrlSource *source,
+                      GrlMedia *playlist,
+                      const GList *keys,
+                      GrlOperationOptions *options,
+                      GrlPlsFilterFunc filter_func,
+                      GrlSourceResultCb callback,
+                      gpointer user_data);
+
+GList *grl_pls_browse_sync (GrlSource *source,
+                            GrlMedia *playlist,
+                            const GList *keys,
+                            GrlOperationOptions *options,
+                            GrlPlsFilterFunc filter_func,
+                            GError **error);
+
+GrlMedia * grl_pls_file_to_media (GrlMedia            *content,
+                                  GFile               *file,
+                                  GFileInfo           *info,
+                                  gboolean             handle_pls,
+                                  GrlOperationOptions *options);
+
+const char * grl_pls_get_file_attributes (void);
+
+G_END_DECLS
+
+#endif /* _GRL_PLS_H_ */
diff --git a/po/POTFILES.in b/po/POTFILES.in
index 8d0b471..83ff461 100644
--- a/po/POTFILES.in
+++ b/po/POTFILES.in
@@ -1,5 +1,6 @@
 libs/net/grl-net-mock.c
 libs/net/grl-net-wc.c
+libs/pls/grl-pls.c
 src/grilo.c
 src/grl-multiple.c
 src/grl-registry.c


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