summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--Makefile.am4
-rw-r--r--configure.ac37
-rw-r--r--doc/grilo/grilo-docs.sgml5
-rw-r--r--doc/grilo/grilo-sections.txt10
-rw-r--r--examples/Makefile.am5
-rw-r--r--examples/browsing-pls.c246
-rw-r--r--grilo-pls-0.2.pc.in15
-rw-r--r--grilo-pls-uninstalled.pc.in15
-rw-r--r--libs/Makefile.am6
-rw-r--r--libs/pls/Makefile.am70
-rw-r--r--libs/pls/grl-pls.c1372
-rw-r--r--libs/pls/grl-pls.h79
-rw-r--r--po/POTFILES.in1
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