diff options
-rw-r--r-- | Makefile.am | 4 | ||||
-rw-r--r-- | configure.ac | 37 | ||||
-rw-r--r-- | doc/grilo/grilo-docs.sgml | 5 | ||||
-rw-r--r-- | doc/grilo/grilo-sections.txt | 10 | ||||
-rw-r--r-- | examples/Makefile.am | 5 | ||||
-rw-r--r-- | examples/browsing-pls.c | 246 | ||||
-rw-r--r-- | grilo-pls-0.2.pc.in | 15 | ||||
-rw-r--r-- | grilo-pls-uninstalled.pc.in | 15 | ||||
-rw-r--r-- | libs/Makefile.am | 6 | ||||
-rw-r--r-- | libs/pls/Makefile.am | 70 | ||||
-rw-r--r-- | libs/pls/grl-pls.c | 1372 | ||||
-rw-r--r-- | libs/pls/grl-pls.h | 79 | ||||
-rw-r--r-- | 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 |