summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorIago Toral Quiroga <itoral@igalia.com>2011-06-16 13:35:58 +0200
committerJuan A. Suarez Romero <jasuarez@igalia.com>2011-06-20 09:32:09 +0000
commite0ea1c12b49a4a35125b0c6a705b2c6f4970b65e (patch)
tree2b551fb35a61a813678d6374d89f04bccd8f60eb
parentaab5fbd52bca97915622e4407ca15eb922291806 (diff)
doc: Added a chapter on plugin development.
-rw-r--r--doc/grilo/Makefile.am5
-rw-r--r--doc/grilo/grilo-docs.sgml19
-rw-r--r--doc/grilo/quick-start-plugins-media-sources.xml1157
-rw-r--r--doc/grilo/quick-start-plugins-metadata-sources.xml392
-rw-r--r--doc/grilo/quick-start-plugins-testing.xml99
5 files changed, 1671 insertions, 1 deletions
diff --git a/doc/grilo/Makefile.am b/doc/grilo/Makefile.am
index d002cb2..d533007 100644
--- a/doc/grilo/Makefile.am
+++ b/doc/grilo/Makefile.am
@@ -73,7 +73,10 @@ HTML_IMAGES=
# Extra SGML files that are included by $(DOC_MAIN_SGML_FILE).
# e.g. content_files=running.sgml building.sgml changes-2.0.sgml
content_files=overview.xml \
- quick-start-using-grilo.xml
+ quick-start-using-grilo.xml \
+ quick-start-plugins-media-sources.xml \
+ quick-start-plugins-metadata-sources.xml \
+ quick-start-plugins-testing.xml
# SGML files where gtk-doc abbrevations (#GtkWidget) are expanded
# These files must be listed here *and* in content_files
diff --git a/doc/grilo/grilo-docs.sgml b/doc/grilo/grilo-docs.sgml
index 6a499a9..60c9f59 100644
--- a/doc/grilo/grilo-docs.sgml
+++ b/doc/grilo/grilo-docs.sgml
@@ -22,10 +22,29 @@
<reference>
<title>Quick Start</title>
+
<chapter>
<title>Using Grilo</title>
<xi:include href="quick-start-using-grilo.xml"/>
</chapter>
+
+ <chapter>
+ <title>Writing plugins for Grilo</title>
+ <section id="quick-start-writing-plugins">
+ <section id="media-source-plugins">
+ <title>Media Source plugins</title>
+ <xi:include href="quick-start-plugins-media-sources.xml"/>
+ </section>
+ <section id="metadata-source-plugins">
+ <title>Metadata Source plugins</title>
+ <xi:include href="quick-start-plugins-metadata-sources.xml"/>
+ </section>
+ <section id="testing-plugins">
+ <title>Testing your plugins</title>
+ <xi:include href="quick-start-plugins-testing.xml"/>
+ </section>
+ </section>
+ </chapter>
</reference>
<reference>
diff --git a/doc/grilo/quick-start-plugins-media-sources.xml b/doc/grilo/quick-start-plugins-media-sources.xml
new file mode 100644
index 0000000..63afe61
--- /dev/null
+++ b/doc/grilo/quick-start-plugins-media-sources.xml
@@ -0,0 +1,1157 @@
+<?xml version="1.0"?>
+<!DOCTYPE refentry PUBLIC "-//OASIS//DTD DocBook XML V4.3//EN"
+ "http://www.oasis-open.org/docbook/xml/4.3/docbookx.dtd" [
+<!ENTITY % local.common.attrib "xmlns:xi CDATA #FIXED 'http://www.w3.org/2003/XInclude'">
+]>
+
+<section>
+<section id="media-source-plugins-intro">
+ <title>Introduction</title>
+
+ <para>
+ Media Source plugins provide access to media content. Examples
+ of Media Source plugins are the Jamendo or UPnP plugins, which
+ give access to content offered by Jamendo or content available
+ on UPnP servers respectively.
+ </para>
+
+ <para>
+ Usually, clients interact with these plugins in various ways:
+ <itemizedlist>
+ <listitem>
+ <emphasis>Search.</emphasis>
+ Users can instruct the media provider to search for
+ content that matches certain keywords. This is how people
+ typically interact with services like YouTube, for example.
+ </listitem>
+ <listitem>
+ <emphasis>Browse.</emphasis>
+ Users navigate through a fixed hierarchy of
+ categorized content interactively. This is how people
+ typically interact with UPnP services, for example.
+ </listitem>
+ <listitem>
+ <emphasis>Query.</emphasis>
+ Some times services provide features that
+ are too specific to be transported to a generic,
+ cross-service API. An example of this could be certain
+ search filtering options. Queries allow users to
+ interact with services using service-specific language
+ that can be used to exploit these features.
+ </listitem>
+ <listitem>
+ <emphasis>Metadata.</emphasis>
+ Users can request additional information (metadata)
+ for a specific media item served by a media provider through
+ a previous browse, search or query operation that was configured
+ to retrieve only partial metadata (typically for optimization
+ purposes). Metadata operations are usually used when showing
+ detailed information about specific media items.
+ </listitem>
+ <listitem>
+ <emphasis>Store.</emphasis>
+ Some media providers allow (or even require) users
+ to push content to them. This is how people interact with
+ Podcasts for example, they "store" the feeds they are
+ interested in following first.
+ </listitem>
+ <listitem>
+ <emphasis>Remove.</emphasis>
+ The opposite to the Store operation, used to remove
+ content from the source.
+ </listitem>
+ </itemizedlist>
+ </para>
+</section>
+
+<section id="media-source-plugins-basics">
+ <title>Registering Media Source Plugins</title>
+
+ <para>
+ Grilo plugins must use the macro GRL_PLUGIN_REGISTER, which
+ defines the entry and exit points of the plugin (called
+ when the plugin is loaded and unloaded respectively) as well
+ as its plugin identifier (a string identifying the plugin).
+ </para>
+
+ <para>
+ The plugin identifier will be used by clients to identify
+ the plugin when interacting with the plugin registry API. See
+ the <link linkend="GrlPluginRegistry">GrlPluginRegistry</link>
+ API reference for more details.
+ </para>
+
+ <para>
+ The plugin initialization function is mandatory.
+ the plugin deinitialization function is optional.
+ </para>
+
+ <para>
+ Usually the plugin initialization function will create
+ at least one GrlMediaSource instance and register it
+ using grl_plugin_registry_register_source.
+ </para>
+
+ <para>
+ A GrlMediaSource instance represents a particular source
+ of media. Usually each plugin would spawn just one media
+ source, but some plugins may spawn multiple media sources.
+ For example, a UPnP plugin spawning one media source object
+ for each UPnP server discovered.
+ </para>
+
+ <para>
+ Users can query the registry for available media sources and
+ then use the GrlMediaSource API to interact with them.
+ </para>
+
+ <para>
+ If the plugin requires configuration this should be processed
+ during the plugin initialization function, which should return
+ TRUE upon successful initialization or FALSE otherwise.
+ </para>
+
+ <para>
+ The parameter "configs" of the plugin initialization function
+ provides available configuration information provided by the
+ user for this plugin, if any. This parameter is a list of
+ GrlConfig objects. Usually there would be only one GrlConfig
+ object in the list, but there might be more in the cases of
+ plugins spawning multiple media sources that require different
+ configuration options.
+ </para>
+
+ <programlisting role="C">
+ <![CDATA[
+gboolean
+grl_foo_plugin_init (GrlPluginRegistry *registry,
+ const GrlPluginInfo *plugin,
+ GList *configs)
+{
+ gchar *api_key;
+ GrlConfig *config;
+
+ config = GRL_CONFIG (configs->data);
+
+ api_key = grl_config_get_api_key (config);
+ if (!api_key) {
+ GRL_INFO ("Missing API Key, cannot load plugin");
+ return FALSE;
+ }
+
+ GrlFooSource *source = grl_foo_source_new (api_key);
+ grl_plugin_registry_register_source (registry,
+ plugin,
+ GRL_MEDIA_PLUGIN (source),
+ NULL);
+ g_free (api_key);
+ return TRUE;
+}
+
+GRL_PLUGIN_REGISTER (grl_foo_plugin_init, NULL, "grl-foo");
+]]>
+ </programlisting>
+
+ <para>
+ The next step is to implement the plugin code, for that
+ Media Source plugins must extend the
+ <link linkend="GrlMediaSource">GrlMediaSource</link> class.
+ </para>
+
+ <para>
+ In typical GObject fashion, developers should use the G_DEFINE_TYPE macro,
+ and then provide the class initialization function
+ (grl_foo_source_class_init in the example below) and the instance
+ initialization function (grl_foo_source_init in the example below).
+ A constructor function, although not mandatory, is usually nice to
+ have (grl_foo_source_new in the example below).
+ </para>
+
+ <para>
+ When creating a new GrlMediaSource instance, a few properties
+ should be provided:
+ <itemizedlist>
+ <listitem>
+ <emphasis>source-id:</emphasis> An identifier for the source object.
+ This is not the same as the plugin identifier (remember that a plugin
+ can spawn multiple media source objects). This identifier can be
+ used by clients when interacting with available media sources
+ through the plugin registry API. See
+ the <link linkend="GrlPluginRegistry">GrlPluginRegistry</link>
+ API reference for more details.
+ </listitem>
+ <listitem>
+ <emphasis>source-name:</emphasis> A name for the source object
+ (typically the name that clients would show in the user interface).
+ </listitem>
+ <listitem>
+ <emphasis>source-desc</emphasis>: A description of the media source.
+ </listitem>
+ </itemizedlist>
+ </para>
+
+ <para>
+ In the class initialization function the plugin developer should
+ provide implementations for the operations that the plugin will
+ support. Most operations are optional, but for media sources
+ at least one of Search, Browse and Query are expected to be
+ implemented. Store and Remove are optional. Metadata is expected
+ to be implemented, just like supported_keys. Slow_keys is optional.
+ </para>
+
+ <programlisting role="C">
+ <![CDATA[
+/* Foo class initialization code */
+static void
+grl_foo_source_class_init (GrlFooSourceClass * klass)
+{
+ GObjectClass *gobject_class = G_OBJECT_CLASS (klass);
+ GrlMediaSourceClass *source_class = GRL_MEDIA_SOURCE_CLASS (klass);
+ GrlMetadataSourceClass *metadata_class = GRL_METADATA_SOURCE_CLASS (klass);
+
+ metadata_class->supported_keys = grl_foo_source_supported_keys;
+ metadata_class->slow_keys = grl_foo_source_supported_keys;
+
+ source_class->browse = grl_foo_source_browse;
+ source_class->search = grl_foo_source_search;
+ source_class->query = grl_foo_source_query;
+ source_class->store = grl_foo_source_store;
+ source_class->remove = grl_foo_source_remove;
+ source_class->metadata = grl_foo_source_metadata;
+}
+
+/* Foo instance initialization code */
+static void
+grl_foo_source_init (GrlFooSource *source)
+{
+ /* Here you would initialize 'source', which is an instance
+ of this class type. */
+ source->api_key = NULL;
+}
+
+/* GrlFooSource constructor */
+static GrlFooSource *
+grl_foo_source_new (const gchar *api_key)
+{
+ GrlFooSource *source;
+
+ source = GRL_FOO_SOURCE (g_object_new (GRL_FOO_SOURCE_TYPE,
+ "source-id", "grl-foo",
+ "source-name", "Foo",
+ "source-desc", "Foo media provider",
+ NULL));
+ source->api_key = g_strdup (api_key);
+ return source;
+}
+
+G_DEFINE_TYPE (GrlFooSource, grl_foo_source, GRL_TYPE_MEDIA_SOURCE);
+]]>
+ </programlisting>
+</section>
+
+<section id="media-source-plugins-supported-keys">
+ <title>Implementing Supported Keys</title>
+
+ <para>
+ An implementation for the "supported_keys" method is mandatory for
+ the plugin to work.
+ </para>
+
+ <para>
+ This method is declarative, and it only has to return a list of
+ metadata keys that the plugin supports, that is, it is a declaration
+ of the metadata that the plugin can provide for the media content
+ that it exposes.
+ </para>
+
+ <programlisting role="C">
+ <![CDATA[
+static void
+grl_foo_source_class_init (GrlFooSourceClass * klass)
+{
+ GrlMetadataSourceClass *metadata_class = GRL_METADATA_SOURCE_CLASS (klass);
+ metadata_class->supported_keys = grl_foo_source_supported_keys;
+}
+
+static const GList *
+grl_foo_source_supported_keys (GrlMetadataSource *source)
+{
+ static GList *keys = NULL;
+ if (!keys) {
+ keys = grl_metadata_key_list_new (GRL_METADATA_KEY_ID,
+ GRL_METADATA_KEY_TITLE,
+ GRL_METADATA_KEY_URL,
+ GRL_METADATA_KEY_THUMBNAIL,
+ GRL_METADATA_KEY_MIME,
+ GRL_METADATA_KEY_ARTIST,
+ GRL_METADATA_KEY_DURATION,
+ NULL);
+ }
+ return keys;
+}
+]]>
+ </programlisting>
+</section>
+
+<section id="media-source-plugins-slow-keys">
+ <title>Implementing Slow Keys</title>
+
+ <para>
+ Implementation of the "slow_keys" method is optional, but in some
+ cases it can help to improve performance remarkably.
+ </para>
+
+ <para>
+ This method is intended to provide the framework with information
+ on metadata that is particularly expensive for the framework
+ to retrieve. The framework (or the plugin users) can then
+ use this information to remove this metadata from their requests
+ when performance is important. This is, again, a declarative
+ interface providing a list of keys.
+ </para>
+
+ <para>
+ If the plugin does not provide an implementation for "slow_keys"
+ the framework assumes that all keys are equally expensive to
+ retrieve and will not perform optimizations in any case.
+ </para>
+
+ <programlisting role="C">
+ <![CDATA[
+static void
+grl_foo_source_class_init (GrlFooSourceClass * klass)
+{
+ GrlMetadataSourceClass *metadata_class = GRL_METADATA_SOURCE_CLASS (klass);
+ metadata_class->slow_keys = grl_foo_source_slow_keys;
+}
+
+static const GList *
+grl_foo_source_slow_keys (GrlMetadataSource *source)
+{
+ static GList *keys = NULL;
+ if (!keys) {
+ keys = grl_metadata_key_list_new (GRL_METADATA_KEY_URL,
+ NULL);
+ }
+ return keys;
+}
+]]>
+ </programlisting>
+</section>
+
+<section id="media-source-plugins-search">
+ <title>Implementing Search</title>
+
+ <para>
+ Implementation of the "search" method is optional, but at least one
+ of Search, Browse and Query are expected to be implemented.
+ </para>
+
+ <para>
+ This method implements text based searches, retrieving media
+ that matches the text keywords provided by the user.
+ </para>
+
+ <para>
+ Typically, the way this method operates is like this:
+ <itemizedlist>
+ <listitem>Plugin checks the input parameters and encodes the
+ search operation as expected by the service provider
+ (that could be a SQL query, a HTTP request, etc)</listitem>
+ <listitem>Plugin executes the search on the backend. Typically this
+ involves some kind blocking operation (networking, disk access, etc)
+ that should be executed asynchronously when possible.</listitem>
+ <listitem>Plugin receives the results from the media provider.
+ For each result received the plugin creates a
+ GrlMedia object encapsulating the metadata obtained for
+ that particular match.</listitem>
+ <listitem>Plugin sends the GrlMedia objects back to the client
+ one by one by invoking the user provided callback.</listitem>
+ </itemizedlist>
+ </para>
+
+ <para>
+ Below you can see some source code that illustrates this process:
+ </para>
+
+ <programlisting role="C">
+ <![CDATA[
+/* In this example we assume a media provider that can be
+ queried over http, and that provides its results in xml format */
+
+static void
+grl_foo_source_class_init (GrlFooSourceClass * klass)
+{
+ GrlMediaSourceClass *media_class = GRL_MEDIA_SOURCE_CLASS (klass);
+ media_class->search = grl_foo_source_search;
+}
+
+static void
+foo_execute_search_async_cb (gchar *xml, GrlMediaSourceSearchSpec *ss)
+{
+ GrlMedia *media;
+ gint count;
+
+ count = count_results (xml);
+
+ if (count == 0) {
+ /* Signal "no results" */
+ ss->callback (ss->source, ss->operation_id,
+ NULL, 0, ss->user_data, NULL);
+ } else {
+ /* parse_next parses the next media item in the XML
+ and creates a GrlMedia instance with the data extracted */
+ while (media = parse_next (xml))
+ ss->callback (ss->source, /* Source emitting the data */
+ ss->operation_id, /* Operation identifier */
+ media, /* Media that matched the query */
+ --count, /* Remaining count */
+ ss->user_data, /* User data for the callback */
+ NULL); /* GError instance (if an error occurred) */
+ }
+}
+
+static void
+grl_foo_source_search (GrlMediaSource *source, GrlMediaSourceSearchSpec *ss)
+{
+ gchar *foo_http_search:
+
+ foo_http_search =
+ g_strdup_printf("http://media.foo.com?text=%s&offset=%d&count=%d",
+ ss->text, ss->skip, ss->count);
+
+ /* This executes an async http query and then invokes
+ foo_execute_search_async_cb with the response */
+ foo_execute_search_async (foo_http_search, ss);
+}
+]]>
+ </programlisting>
+
+ <para>
+ Please, check <link linkend="media-source-plugins-common-considerations">
+ Common considerations for Search, Browse and Query implementations</link>
+ for more information on how to implement Search operations properly.
+ </para>
+
+ <para>
+ Examples of plugins implementing Search functionality are
+ grl-jamendo, grl-youtube or grl-vimeo among others.
+ </para>
+</section>
+
+<section id="media-source-plugins-browse">
+ <title>Implementing Browse</title>
+
+ <para>
+ Implementation of the "browse" method is optional, but at least one
+ of Search, Browse and Query are expected to be implemented.
+ </para>
+
+ <para>
+ Browsing is an interactive process, where users navigate by exploring
+ these boxes exposed by the media source in hierarchical form. The idea
+ of browsing a media source is the same as browsing a file system.
+ </para>
+
+ <para>
+ The signature and way of operation of the Browse operation is the same
+ as in the Search operation with one difference: instead of a text
+ parameter with the search keywords, it receives a GrlMedia object
+ representing the container (box) the user wants to browse.
+ </para>
+
+ <para>
+ For the most part, plugin developers that write Browse implementations
+ should consider the same rules and guidelines explained for
+ Search operations.
+ </para>
+
+ <para>
+ Below you can see some source code that illustrates this process:
+ </para>
+
+ <programlisting role="C">
+ <![CDATA[
+/* In this example we assume a media provider that can be queried over
+ http, providing results in XML format. The media provider organizes
+ content according to a list of categories. */
+
+static void
+grl_foo_source_class_init (GrlFooSourceClass * klass)
+{
+ GrlMediaSourceClass *media_class = GRL_MEDIA_SOURCE_CLASS (klass);
+ media_class->browse = grl_foo_source_browse;
+}
+
+static void
+foo_execute_categories_async_cb (gchar *xml, GrlMediaSourceBrowseSpec *bs)
+{
+ GrlMedia *media;
+ gint count;
+
+ count = count_results (xml);
+
+ if (count == 0) {
+ /* Signal "no results" */
+ bs->callback (bs->source, bs->operation_id,
+ NULL, 0, bs->user_data, NULL);
+ } else {
+ /* parse_next parses the next category item in the XML
+ and creates a GrlMedia instance with the data extracted,
+ which should be of type GrlMediaBox */
+ while (media = parse_next_cat (xml))
+ bs->callback (bs->source, /* Source emitting the data */
+ bs->operation_id, /* Operation identifier */
+ media, /* The category (box) */
+ --count, /* Remaining count */
+ bs->user_data, /* User data for the callback */
+ NULL); /* GError instance (if an error occurred) */
+ }
+}
+
+static void
+foo_execute_media_async_cb (gchar *xml, GrlMediaSourceBrowseSpec *os)
+{
+ GrlMedia *media;
+ gint count;
+
+ count = count_results (xml);
+
+ if (count == 0) {
+ /* Signal "no results" */
+ bs->callback (bs->source, bs->operation_id,
+ NULL, 0, bs->user_data, NULL);
+ } else {
+ /* parse_next parses the next media item in the XML
+ and creates a GrlMedia instance with the data extracted,
+ which should be of type GrlMediaImage, GrlMediaAudio or
+ GrlMediaVideo */
+ while (media = parse_next_media (xml))
+ os->callback (os->source, /* Source emitting the data */
+ os->operation_id, /* Operation identifier */
+ media, /* Media that matched the query */
+ --count, /* Remaining count */
+ os->user_data, /* User data for the callback */
+ NULL); /* GError instance (if an error occurred) */
+ }
+}
+
+static void
+grl_foo_source_browse (GrlMediaSource *source, GrlMediaSourceBrowseSpec *bs)
+{
+ gchar *foo_http_browse:
+
+ /* We use the names of the categories as their media identifiers */
+ box_id = grl_media_get_id (bs->container);
+
+ if (!box_id) {
+ /* Browsing the root box, the result must be the list of
+ categories provided by the service */
+ foo_http_browse =
+ g_strdup_printf("http://media.foo.com/category_list",
+ os->skip, os->count);
+ /* This executes an async http query and then invokes
+ foo_execute_categories_async_cb with the response */
+ foo_execute_categories_async (foo_http_browse, os);
+ } else {
+ /* Browsing a specific category */
+ foo_http_browse =
+ g_strdup_printf("http://media.foo.com/content/%s?offset=%d&count=%d",
+ box_id, os->skip, os->count);
+ /* This executes an async http query and then invokes
+ foo_execute_browse_async_cb with the response */
+ foo_execute_media_async (foo_http_browse, os);
+ }
+}
+]]>
+ </programlisting>
+
+ <para>
+ Some considerations that plugin developers should take into account:
+ <itemizedlist>
+ <listitem>
+ In the example we are assuming that the content hierarchy only has two
+ levels, the first level exposes a list of categories (each one exposed
+ as a GrlMediaBox object so the user knows they can be browsed again), and
+ then a second level with the contents within these categories, that we
+ assume are all media items, although in real life they could very well
+ be more GrlMediaBox objects, leading to more complex hierarchies.
+ </listitem>
+ <listitem>
+ GrlMediaBox objects returned by a browse operation can be browsed
+ by clients in future Browse operations.
+ </listitem>
+ <listitem>
+ The input parameter that informs the plugin about the box that
+ should be browsed (bs->container) is of type GrlMediaBox. The
+ plugin developer must map that to something the media provider
+ understands. Typically, when GrlMedia objects are returned from
+ a plugin to the client, they are created so their "id"
+ property (grl_media_set_id) can be used for this purpose,
+ identifying these media resources uniquely in the context of
+ the media provider.
+ </listitem>
+ <listitem>
+ A GrlMediaBox object with NULL id always represents the root
+ box/category in the content hierarchy exposed by the plugin.
+ </listitem>
+ </itemizedlist>
+ </para>
+
+ <para>
+ Please, check <link linkend="media-source-plugins-common-considerations">
+ Common considerations for Search, Browse and Query implementations</link>
+ for more information on how to implement Browse operations properly.
+ </para>
+
+ <para>
+ Examples of plugins implementing browse functionality are
+ grl-jamendo, grl-filesystem or grl-upnp among others.
+ </para>
+
+</section>
+
+<section id="media-source-plugins-query">
+ <title>Implementing Query</title>
+
+ <para>
+ Implementation of the "query" method is optional, but at least one
+ of Search, Browse and Query are expected to be implemented.
+ </para>
+
+ <para>
+ This method provides plugin developers with means to expose
+ service-specific functionality that cannot be achieved
+ through regular Browse and Search operations.
+ </para>
+
+ <para>
+ This method operates just like the Search method, but the text
+ parameter does not represent a list of keywords to search for,
+ instead, its meaning is plugin specific and defined by the plugin
+ developer.
+ </para>
+
+ <para>
+ Normally, Query implementations involve parsing and decoding this
+ input string into something meaningful for the media provider
+ (a specific operation with its parameters).
+ </para>
+
+ <para>
+ Usually, Query implementations are intended to provide advanced
+ filtering capabilities and similar features that make use of
+ specific features of the service that cannot be
+ exposed through more service agnostic APIs, like Search or
+ Browse. For example, a plugin that provides media content
+ stored in a database can implement Query to give users the
+ possibility to execute SQL queries directly, by encoding the
+ SQL commands in this input string, giving a lot of flexibility
+ in how they access the content stored in the database in
+ exchange for writing plugin-specific code in the application.
+ </para>
+
+ <para>
+ The example below shows the case of a plugin implementing
+ Query to let the user specify filters directly in SQL
+ format for additional flexibility.
+ </para>
+
+ <programlisting role="C">
+ <![CDATA[
+static void
+grl_foo_source_class_init (GrlFooSourceClass * klass)
+{
+ GrlMediaSourceClass *media_class = GRL_MEDIA_SOURCE_CLASS (klass);
+ media_class->query = grl_foo_source_query;
+}
+
+static void
+grl_foo_source_query (GrlMediaSource *source, GrlMediaSourceQuerySpec *qs)
+{
+ const gchar *sql_filter;
+ GList *results;
+ GrlMedia *media;
+ gint count;
+
+ /* qs->text is expected to contain a suitable SQL filter */
+ sql_query = prepare_sql_with_custom_filter (qs->text, qs->skip, qs->count);
+
+ /* Execute the resulting SQL query, which incorporates
+ the filter provided by the user */
+ results = execute_sql (sql_query);
+
+ /* For each result obtained, invoke the user callback as usual */
+ count = g_list_length (results);
+
+ if (count == 0) {
+ /* Signal "no results" */
+ qs->callback (qs->source, qs->operation_id,
+ NULL, 0, qs->user_data, NULL);
+ } else {
+ while (media = next_result (&results))
+ qs->callback (qs->source, /* Source emitting the data */
+ qs->operation_id, /* Operation identifier */
+ media, /* Media that matched the query */
+ --count, /* Remaining count */
+ qs->user_data, /* User data for the callback */
+ NULL); /* GError instance (if an error occurred) */
+ }
+}
+]]>
+ </programlisting>
+
+ <para>
+ Please, check <link linkend="media-source-plugins-common-considerations">
+ Common considerations for Search, Browse and Query implementations</link>
+ for more information on how to implement Query operations properly.
+ </para>
+
+ <para>
+ Examples of plugins implementing Query are grl-jamendo,
+ grl-upnp or grl-bookmarks among others.
+ </para>
+</section>
+
+<section id="media-source-plugins-common-considerations">
+ <title>Common considerations for Search, Browse and Query implementations</title>
+
+ <para>
+ <itemizedlist>
+ <listitem>Making operations synchronous would block the client
+ application while the operation is executed, so providing
+ a non-blocking implementation is mostly mandatory for most practical
+ purposes.</listitem>
+ <listitem>
+ Grilo invokes plugin operations in idle callbacks to ensure that control
+ is returned to the client as soon as possible. Still, plugin developers
+ are encouraged to write efficient code that avoids blocking as much as
+ possible, since this good practice will make applications behave
+ smoother, granting a much better user experience. Use of threads in
+ plugin code is not recommended, instead, splitting the work to do in
+ chunks using the idle loop is encouraged.
+ </listitem>
+ <listitem>Creating GrlMedia instances is easy, depending on the type of
+ media you should instantiate one of the GrlMedia subclasses (GrlMediaImage,
+ GrlMediaVideo, GrlMediaAudio or GrlMediaBox), and then use the API to
+ set the corresponding data. Check the <link linkend="GrlData">GrlData</link>
+ hierarchy in the API reference for more details.
+ </listitem>
+ <listitem>The remaining count parameter present in the callbacks is intended
+ to provide the client with an <emphasis>estimation</emphasis> of how many
+ more results will come after the current one as part of the same operation.</listitem>
+ <listitem>Finalization of the operation must <emphasis>always</emphasis>
+ be signaled by invoking the user callback with remaining count set to 0,
+ even on error conditions.
+ </listitem>
+ <listitem>Plugin developers must ensure that all operations
+ end by invoking the user callback with the remaining count parameter
+ set to 0, and that this is done only once per operation. This
+ behavior is expected and must be guaranteed by the plugin developer.</listitem>
+ <listitem>
+ Once the user callback has been invoked with the remaining count
+ parameter set to 0, the operations is considered finished and the
+ plugin developer must <emphasis>never</emphasis>
+ invoke the user callback again for that operation again.
+ </listitem>
+ <listitem>
+ In case of error, the plugin developer must invoke the user
+ callback like this:
+ <itemizedlist>
+ <listitem>Set the last parameter to a non-NULL GError instance.</listitem>
+ <listitem>Set the media parameter to NULL.</listitem>
+ <listitem>Set the remaining count parameter to 0.</listitem>
+ </itemizedlist>
+ The plugin developer is responsible for releasing the error once
+ the user callback is invoked.
+ </listitem>
+ <listitem>It is possible to finalize the operation with a NULL
+ media and remaining count set to 0 if that is convenient for the
+ plugin developer.
+ </listitem>
+ <listitem>
+ Returned GrlMedia objects are owned by the client and should not be
+ freed by the plugin.
+ </listitem>
+ <listitem>
+ The list of metadata information requested by the client is
+ available in the "keys" field of the Spec structure. Typically plugin
+ developers don't have to care about the list of keys requested and
+ would just resolve all metadata available. The only situation in which
+ the plugin developer should check the specific list of keys requested
+ is when there are keys that are particularly expensive to
+ resolve, in these cases the plugin should only resolve these keys if
+ the user has indeed requested that information.
+ </listitem>
+ </itemizedlist>
+ </para>
+</section>
+
+
+<section id="media-source-plugins-metadata">
+ <title>Implementing Metadata</title>
+
+ <para>
+ Implementation of the "metadata" method is not mandatory
+ but would be usually expected by application developers.
+ </para>
+
+ <para>
+ The purpose of the metadata method is to provide additional
+ metadata for GrlMedia objects produced by the media source.
+ </para>
+
+ <para>
+ Typically, the use case for Metadata operations is applications
+ obtaining a list of GrlMedia objects by executing a Browse,
+ Search or Query operation , requesting limited metadata (for
+ performance reasons), and then requesting additional metadata
+ for specific items selected by the user.
+ </para>
+
+ <programlisting role="C">
+ <![CDATA[
+static void
+grl_foo_source_class_init (GrlFooSourceClass * klass)
+{
+ GrlMediaSourceClass *media_class = GRL_MEDIA_SOURCE_CLASS (klass);
+ media_class->metadata = grl_foo_source_metadata;
+}
+
+static void
+foo_media_info_cb (gchar *xml, GrlMediaSourceMetadataSpec *ms)
+{
+ /* This resolves metadata for keys "ms->keys" from "xml" and
+ stores them in "ms->media" */
+ resolve_metadata_from_xml (ms->media, ms->keys, xml);
+
+ /* Send updated ms->media back to the user */
+ ms->callback (ms->source, ms->metadata_id, ms->media, ms->user_data, NULL);
+}
+
+static void
+grl_foo_source_metadata (GrlMediaSource *source, GrlMediaSourceMetadataSpec *ms)
+{
+ const gchar *media_id;
+
+ media_id = grl_media_get_id (ms->media);
+
+ foo_media_info =
+ g_strdup_printf("http://media.foo.com/media-info/%s", media_id);
+
+ /* This executes an async http query and then invokes
+ foo_metadata_cb with the response */
+ foo_execute_metadata_async (foo_media_info, ms);
+}
+]]>
+ </programlisting>
+
+ <para>
+ Some considerations that plugin developers should take into account:
+ <itemizedlist>
+ <listitem>
+ Clients invoke this method passing the GrlMedia object that
+ they want to update (ms->media). Plugin developers should resolve the
+ requested metadata (ms->keys) and store it in that GrlMedia
+ object.
+ </listitem>
+ <listitem>
+ Just like in other APIs, implementation of this method is expected
+ to be asynchronous to avoid blocking the user code.
+ </listitem>
+ </itemizedlist>
+ </para>
+
+ <para>
+ Examples of plugins implementing Metadata are grl-youtube,
+ grl-upnp or grl-jamendo among others.
+ </para>
+</section>
+
+
+<section id="media-source-plugins-store">
+ <title>Implementing Store</title>
+
+ <para>
+ Implementation of the "store" method is optional.
+ </para>
+
+ <para>
+ The Store method is used to push new content to the media source.
+ </para>
+
+ <programlisting role="C">
+ <![CDATA[
+static void
+grl_foo_source_class_init (GrlFooSourceClass * klass)
+{
+ GrlMediaSourceClass *media_class = GRL_MEDIA_SOURCE_CLASS (klass);
+ media_class->store = grl_foo_source_store;
+}
+
+static void
+grl_foo_source_store (GrlMediaSource *source, GrlMediaSourceStoreSpec *ss)
+{
+ const gchar *title;
+ const gchar *uri;
+ const gchar *parent_id;
+ guint row_id;
+
+ /* We get the id of the parent container where we want
+ to put the new content */
+ parent_id = grl_media_get_id (GRL_MEDIA (parent));
+
+ /* We get he metadata of the media we want to push, in this case
+ only URI and Title */
+ uri = grl_media_get_uri ();
+ title = grl_media_get_title ();
+
+ /* Push the data to the media provider (in this case a database) */
+ row_id = run_sql_insert (parent_id, uri, title);
+
+ /* Set the media id in the GrlMedia object */
+ grl_media_set_id (ss->media, row_id_to_media_id (row_id));
+
+ /* Inform the user that the operation is done (NULL error means everything
+ was ok */
+ ss->callback (ss->source, ss->parent, ss->media, ss->user_data, NULL);
+}
+]]>
+ </programlisting>
+
+ <para>
+ Some considerations that plugin developers should take into account:
+ <itemizedlist>
+ <listitem>
+ After successfully storing the media, the method should assign
+ a proper media id to it before invoking the user callback.
+ </listitem>
+ </itemizedlist>
+ </para>
+
+ <para>
+ Examples of plugins implementing Store are grl-bookmarks or
+ grl-podcasts.
+ </para>
+</section>
+
+
+<section id="media-source-plugins-remove">
+ <title>Implementing Remove</title>
+
+ <para>
+ Implementation of the "remove" method is optional.
+ </para>
+
+ <para>
+ The Remove method is used to remove content from the media source.
+ </para>
+
+ <programlisting role="C">
+ <![CDATA[
+static void
+grl_foo_source_class_init (GrlFooSourceClass * klass)
+{
+ GrlMediaSourceClass *media_class = GRL_MEDIA_SOURCE_CLASS (klass);
+ media_class->remove = grl_foo_source_remove;
+}
+
+static void
+grl_foo_source_remove (GrlMediaSource *source, GrlMediaSourceRemoveSpec *rs)
+{
+ /* Remove the data from the media provider (in this case a database) */
+ run_sql_delete (ss->media_id);
+
+ /* Inform the user that the operation is done (NULL error means everything
+ was ok */
+ rs->callback (rs->source, rs->media, rs->user_data, NULL);
+}
+]]>
+ </programlisting>
+
+ <para>
+ Examples of plugins implementing Remove are grl-bookmarks or
+ grl-podcasts.
+ </para>
+</section>
+
+<section id="media-source-plugins-media-from-uri">
+ <title>Implementing Media from URI</title>
+
+ <para>Implementation of the "media_from_uri" method is optional.</para>
+
+ <para>
+ Some times clients have access to the URI of the media, and they
+ want to retrieve metadata for it. A couple of examples where this may
+ come in handy: A file system browser that needs to obtain additional
+ metadata for a particular media item located in the filesystem. A browser
+ plugin that can obtain additional metadata for a media item given its
+ URL. In these cases we know the URI of the media, but we need to
+ create a GrlMedia object representing it.
+ </para>
+
+ <para>
+ Plugins that want to support URI to GrlMedia conversions must implement
+ the "test_media_from_uri" and "media_from_uri" methods.
+ </para>
+
+ <para>
+ The method "test_media_from_uri" should return TRUE if, upon inspection of
+ the media URI, the plugin decides that it can convert it to a GrlMedia
+ object. For example, a YouTube plugin would check that the URI of the media
+ is a valid YouTube URL. This method is asynchronous and should not block.
+ If the plugin cannot decide if it can or cannot convert the URI to a
+ GrlMedia object by inspecting the URI without doing blocking operations,
+ it should return TRUE. This method is used to discard efficiently plugins
+ that cannot resolve the media.
+ </para>
+
+ <para>
+ The method "media_from_uri" is used to do the actual conversion from the
+ URI to the GrlMedia object.
+ </para>
+
+ <programlisting role="C">
+ <![CDATA[
+static void
+grl_foo_source_class_init (GrlFooSourceClass * klass)
+{
+ GrlMediaSourceClass *media_class = GRL_MEDIA_SOURCE_CLASS (klass);
+ media_class->test_media_from_uri = grl_foo_source_test_media_from_uri;
+ media_class->media_from_uri = grl_foo_source_media_from_uri;
+}
+
+static gboolean
+grl_filesystem_test_media_from_uri (GrlMediaSource *source,
+ const gchar *uri)
+{
+ if (strstr (uri, "http://media.foo.com/media-info/") == uri) {
+ return TRUE;
+ }
+ return FALSE;
+}
+
+static void
+grl_filesystem_media_from_uri (GrlMediaSource *source,
+ GrlMediaSourceMediaFromUriSpec *mfus)
+{
+ gchar *media_id;
+ GrlMedia *media;
+
+ media_id = get_media_id_from_uri (mfus->uri);
+ media = create_media_from_id (media_id);
+ mfus->callback (source, mfus->media_from_uri_id, media, mfus->user_data, NULL);
+ g_free (media_id);
+}
+]]>
+ </programlisting>
+
+ <para>
+ Some considerations that plugin developers should take into account:
+ <itemizedlist>
+ <listitem>
+ Typically "media_from_uri" involves a blocking operation, and hence
+ its implementation should be asynchronous.
+ </listitem>
+ </itemizedlist>
+
+ <para>
+ Examples of plugins implementing "media_from_uri" are grl-filesystem
+ or grl-youtube.
+ </para>
+ </para>
+</section>
+
+<section id="media-source-plugins-change_notification">
+ <title>Notifying changes</title>
+
+ <para>
+ Source can signal clients when available media content has been
+ changed. This is an optional feature.
+ </para>
+
+ <para>
+ Plugins supporting content change notification must implement
+ "notify_change_start" and "notify_change_stop", which let the
+ user start or stop content change notification at will.
+ </para>
+
+ <para>
+ Once users have activated notifications by invoking
+ "notify_change_start", media sources should communicate
+ any changes detected by calling grl_media_source_notify_change_list
+ with a list of the media items changed.
+ </para>
+
+ <para>
+ Upon calling "notify_changes_stop" the plugin must stop
+ communicating changes until "notify_changes_start" is
+ called again.
+ </para>
+
+ <programlisting role="C">
+ <![CDATA[
+static void
+grl_foo_source_class_init (GrlFooSourceClass * klass)
+{
+ GrlMediaSourceClass *media_class = GRL_MEDIA_SOURCE_CLASS (klass);
+ media_class->notify_change_start = grl_foo_source_notify_change_start;
+ media_class->notify_change_stop = grl_foo_source_notify_change_stop;
+}
+
+static void
+content_changed_cb (GList *changes)
+{
+ GPtrArray *changed_medias;
+
+ changed_medias = g_ptr_array_sized_new (g_list_length (changes));
+ while (media = next_media_from_changes (&changes)) {
+ g_ptr_array_add (changed_medias, media);
+ }
+
+ grl_media_source_notify_change_list (source,
+ changed_medias,
+ GRL_CONTENT_CHANGED,
+ FALSE);
+}
+
+static gboolean
+grl_foo_source_notify_change_start (GrlMediaSource *source,
+ GError **error)
+{
+ GrlFooMediaSource *foo_source;
+
+ /* Listen to changes in the media content provider */
+ foo_source = GRL_FOO_MEDIA_SOURCE (source);
+ foo_source->listener_id = foo_subscribe_listener_new (content_changed_cb);
+
+ return TRUE;
+}
+
+static gboolean
+grl_foo_source_notify_change_stop (GrlMediaSource *source,
+ GError **error)
+{
+ GrlFooMediaSource *foo_source;
+
+ /* Stop listening to changes in the media content provider */
+ foo_source = GRL_FOO_MEDIA_SOURCE (source);
+ foo_listener_destroy (foo_source->listener_id);
+
+ return TRUE;
+}
+]]>
+ </programlisting>
+
+ <para>
+ Please check the
+ <link linkend="GrlMediaSource">GrlMediaSource</link> API reference
+ for more details on how grl_media_source_notify_change_list
+ should be used.
+ </para>
+
+ <para>Examples of plugins implementing change notification are
+ gr-upnp and grl-tracker among others
+ </para>
+</section>
+</section>
diff --git a/doc/grilo/quick-start-plugins-metadata-sources.xml b/doc/grilo/quick-start-plugins-metadata-sources.xml
new file mode 100644
index 0000000..53d0c04
--- /dev/null
+++ b/doc/grilo/quick-start-plugins-metadata-sources.xml
@@ -0,0 +1,392 @@
+<?xml version="1.0"?>
+<!DOCTYPE refentry PUBLIC "-//OASIS//DTD DocBook XML V4.3//EN"
+ "http://www.oasis-open.org/docbook/xml/4.3/docbookx.dtd" [
+<!ENTITY % local.common.attrib "xmlns:xi CDATA #FIXED 'http://www.w3.org/2003/XInclude'">
+]>
+
+
+<section>
+ <section id="metadata-source-plugins-intro">
+ <title>Introduction</title>
+
+ <para>
+ Metadata source plugins provide access to additional metadata information.
+ </para>
+
+ <para>
+ Unlike media sources, metadata sources do not provide access to media content,
+ but additional metadata information about content that was provided by
+ media sources.
+ </para>
+
+ <para>
+ An example of a metadata source would be one which is able to provide
+ thumbnail information for local audio content from an online service.
+ </para>
+
+ <para>
+ Media sources extend GrlMetadataSource, so they are also metadata
+ sources.
+ </para>
+
+ <para>
+ Typically, users interact with metadata sources to:
+ <itemizedlist>
+ <listitem>Resolve additional metadata for a particular media item.</listitem>
+ <listitem>Update metadata for a particular media item.</listitem>
+ </itemizedlist>
+ </para>
+ </section>
+
+ <section id="metadata-source-plugins-basics">
+ <title>Registering the plugin</title>
+
+ <para>
+ Registering a new metadata source plugin is done by following the same
+ procedure as for media source plugins, except that they must extend
+ GrlMetadataSource. Please, check
+ <link linkend="media-source-plugins-basics">Registering Media Source Plugins</link>
+ for details.
+ </para>
+
+ <para>
+ Metadata source plugins must also implement "supported_keys", and optionally
+ "slow_keys". Please check
+ <link linkend="media-source-plugins-supported-keys">Implementing Supported Keys</link>
+ and
+ <link linkend="media-source-plugins-slow-keys">Implementing Slow Keys</link>
+ respectively for further details.
+ </para>
+ </section>
+
+
+ <section id="metadata-source-plugins-resolve">
+ <title>Implementing Resolve</title>
+
+ <para>
+ An implementation of the "resolve" method is mandatory for metadata
+ plugins to work.
+ </para>
+
+ <para>
+ Resolve operations are issued in order to grab additional information
+ on a given media (GrlMedia).
+ </para>
+
+ <para>
+ Typically, implementing Resolve implies inspecting the metadata
+ already known for that media and use that information to gain access
+ to new information. For example, a plugin can use the artist and album
+ information of a given GrlMediaAudio item to obtain additional information,
+ like the album cover thumbnail.
+ </para>
+
+ <para>
+ Plugins implementing "resolve" must also implement "may_resolve". The
+ purpose of this method is to analyze if the GrlMedia contains enough
+ metadata to enable the plugin to extract the additional metadata
+ requested.
+ </para>
+
+ <programlisting role="C">
+ <![CDATA[
+/* In this example we assume a plugin that can resolve thumbnail
+ information for audio items given that we have artist and album
+ information available */
+
+static void
+grl_foo_source_class_init (GrlFooSourceClass * klass)
+{
+ GrlMetadataSourceClass *metadata_class = GRL_METADATA_SOURCE_CLASS (klass);
+ metadata_class->may_resolve = grl_foo_source_may_resolve;
+ metadata_class->resolve = grl_foo_source_resolve;
+}
+
+static gboolean
+grl_foo_source_may_resolve (GrlMetadataSource *source,
+ GrlMedia *media,
+ GrlKeyID key_id,
+ GList **missing_keys)
+{
+ gboolean needs_artist = FALSE;
+ gboolean needs_album = FALSE;
+
+ /* We only support thumbnail resolution */
+ if (key_id != GRL_METADATA_KEY_THUMBNAIL)
+ return FALSE;
+
+ /* We only support audio items */
+ if (media) {
+ if (!GRL_IS_MEDIA_AUDIO (media))
+ return FALSE;
+
+ /* We need artist information available */
+ if (grl_media_audio_get_artist (GRL_MEDIA_AUDIO (media)) == NULL) {
+ if (missing_keys)
+ *missing_keys = g_list_add (*missing_keys,
+ GRLKEYID_TO_POINTER (GRL_METADATA_KEY_ARTIST));
+ needs_artist = TRUE;
+ }
+
+ /* We need album information available */
+ if (grl_media_audio_get_album (GRL_MEDIA_AUDIO (media)) == NULL)) {
+ if (missing_keys)
+ *missing_keys = g_list_add (*missing_keys,
+ GRLKEYID_TO_POINTER (GRL_METADATA_KEY_ALBUM));
+ needs_album = TRUE;
+ }
+ }
+
+ if (needs_album || needs_artist)
+ return FALSE;
+
+ return TRUE;
+}
+
+static void
+grl_foo_source_resolve (GrlMetadataSource *source,
+ GrlMetadataSourceResolveSpec *rs)
+{
+ const gchar *album;
+ const gchar *artist,
+ gchar *thumb_uri;
+ const GError *error = NULL;
+
+ if (contains_key (rs->keys, GRL_METADATA_KEY_THUMBNAIL) {
+ artist = grl_media_audio_get_artist (GRL_MEDIA_AUDIO (rs->media));
+ album = grl_media_audio_get_album (GRL_MEDIA_AUDIO (rs->media));
+ if (artist && album) {
+ thumb_uri = resolve_thumb_uri (artist, album);
+ grl_media_set_thumbnail (rs->media, thumb_uri);
+ } else {
+ error = g_error_new (GRL_CORE_ERROR,
+ GRL_CORE_ERROR_RESOLVE_FAILED,
+ "Can't resolve thumbnail, artist and album not known");
+ }
+ } else {
+ error = g_error_new (GRL_CORE_ERROR,
+ GRL_CORE_ERROR_RESOLVE_FAILED,
+ "Can't resolve requested keys");
+ }
+
+ rs->callback (source, rs->resolve_id, rs->media, rs->user_data, error);
+
+ if (error)
+ g_error_free (error);
+}
+]]>
+ </programlisting>
+
+ <para>
+ Some considerations that plugin developers should take into account:
+ <itemizedlist>
+ <listitem>
+ The method "may_resolve" is synchronous, should be fast and
+ never block. If the plugin cannot confirm if it can resolve the
+ metadata requested without doing blocking operations then it should
+ return TRUE. Then, when "resolve" is invoked further checking
+ can be done.
+ </listitem>
+ <listitem>
+ Typically "resolve" involves a blocking operation, and hence
+ its implementation should be asynchronous.
+ </listitem>
+ </itemizedlist>
+ </para>
+
+ <para>
+ Examples of plugins implementing "resolve" are grl-lastfm-albumart
+ or grl-local-metadata among others.
+ </para>
+ </section>
+
+ <section id="metadata-source-plugins-set-metadata">
+ <title>Implementing Set Metadata</title>
+
+ <para>
+ Implementing "set_metadata" is optional.
+ </para>
+
+ <para>
+ Some plugins may provide users with the option of updating the metadata
+ available for specific media items. For example, a plugin may store user
+ metadata like the last time that a certain media resource was played
+ or its play count. These metadata properties do not make sense if
+ applications do not have means to change and update their values.
+ </para>
+
+ <para>
+ Plugins that support this feature must implement two methods:
+ <itemizedlist>
+ <listitem>
+ <emphasis>writable_keys:</emphasis> just like "supported_keys"
+ or "slow_keys", it is a declarative method, intended to provide
+ information on what keys supported by the plugin are writable, that is,
+ their values can be changed by the user.
+ </listitem>
+ <listitem>
+ <emphasis>set_metadata:</emphasis> which is the method used
+ by clients to update metadata values for specific keys.
+ </listitem>
+ </itemizedlist>
+ </para>
+
+ <programlisting role="C">
+ <![CDATA[
+static void
+grl_foo_source_class_init (GrlFooSourceClass * klass)
+{
+ GrlMetadataSourceClass *metadata_class = GRL_METADATA_SOURCE_CLASS (klass);
+ metadata_class->writable_keys = grl_foo_source_writable_keys;
+ metadata_class->set_metadata = grl_foo_source_set_metadata;
+}
+
+static const GList *
+grl_foo_source_writable_keys (GrlMetadataSource *source)
+{
+ static GList *keys = NULL;
+ if (!keys) {
+ keys = grl_metadata_key_list_new (GRL_METADATA_KEY_RATING,
+ GRL_METADATA_KEY_PLAY_COUNT,
+ GRL_METADATA_KEY_LAST_PLAYED,
+ NULL);
+ }
+ return keys;
+}
+
+static void
+grl_foo_source_set_metadata (GrlMetadataSource *source,
+ GrlMetadataSourceSetMetadataSpec *sms)
+{
+ GList *iter;
+ const gchar *media_id;
+ GList *failed_keys = NULL;
+
+ /* 'sms->media' contains the media with updated values */
+ media_id = grl_media_get_id (sms->media);
+
+ /* Go through all the keys that need update ('sms->keys'), take
+ the new values (from 'sms->media') and update them in the
+ media provider */
+ iter = sms->keys;
+ while (iter) {
+ GrlKeyID key = GRLPOINTER_TO_KEYID (iter->data);
+ if (!foo_update_value_for_key (sms->media, key)) {
+ /* Save a list with keys that we failed to update */
+ failed_keys = g_list_prepend (failed_keys, iter->data);
+ }
+ iter = g_list_next (iter);
+ }
+
+ /* We are done, invoke user callback to signal client */
+ sms->callback (sms->source, sms->media, failed_keys, sms->user_data, NULL);
+ g_list_free (failed_keys);
+}
+]]>
+ </programlisting>
+
+ <para>
+ Some considerations that plugin developers should take into account:
+ <itemizedlist>
+ <listitem>
+ Typically, updating metadata keys in the media provider would involve
+ one or more blocking operations, so asynchronous implementations
+ of "set_metadata" should be considered.
+ </listitem>
+ <listitem>
+ Some media providers may allow for the possibility of updating
+ multiple keys in just one operation.
+ </listitem>
+ <listitem>
+ The user callback for "set_metadata" receives a list with all the keys
+ that failed to be updated, which the plugin should free after calling
+ the user callback.
+ </listitem>
+ </itemizedlist>
+ </para>
+
+ <para>
+ Examples of plugins implementing "set_metadata" are grl-metadata-store or
+ grl-tracker.
+ </para>
+ </section>
+
+ <section id="metadata-source-plugins-cancel">
+ <title>Cancelling ongoing operations</title>
+
+ <para>
+ Implementing the "cancel" method is optional. This method provided means
+ for application developers to cancel ongoing operations on metadata
+ sources (and hence, also in media sources).
+ </para>
+
+ <para>
+ The "cancel" method receives the identifier of the operation to be
+ cancelled.
+ </para>
+
+ <para>
+ Typically, plugin developers would implement cancellation support
+ by storing relevant information for the cancellation process
+ along with the operation data when this is started, and then
+ retrieving this information when a cancellation request is received.
+ </para>
+
+ <para>
+ Grilo provides plugin developers with API to attach arbitrary data
+ to a certain operation given its identifier. These APIs are:
+ <itemizedlist>
+ <listitem>grl_metadata_source_set_operation_data</listitem>
+ <listitem>grl_metadata_source_get_operation_data</listitem>
+ </itemizedlist>
+ See the API reference documentation for
+ <link linkend="GrlMetadataSource">GrlMetadataSource</link> for
+ more details.
+ </para>
+
+ <programlisting role="C">
+ <![CDATA[
+static void
+grl_foo_source_class_init (GrlFooSourceClass * klass)
+{
+ GrlMediaSourceClass *media_class = GRL_MEDIA_SOURCE_CLASS (klass);
+ GrlMetadataSourceClass *metadata_class = GRL_METADATA_SOURCE_CLASS (klass);
+
+ media_class->search = grl_foo_source_search;
+ metadata_class->cancel = grl_foo_source_cancel;
+}
+
+static void
+grl_foo_source_search (GrlMediaSource *source,
+ GrlMediaSourceSearchSpec *ss)
+{
+ ...
+ gint op_handler = foo_service_search_start (ss->text, ...);
+ grl_metadata_source_set_operation_data (GRL_METADATA_SOURCE (source),
+ ss->operation_id,
+ GINT_TO_POINTER (op_handler));
+ ...
+}
+
+static void
+grl_foo_source_cancel (GrlMetadataSource *source, guint operation_id)
+{
+ gint op_handler;
+
+ op_handler =
+ GPOINTER_TO_INT (grl_metadata_source_get_operation_data (source,
+ operation_id));
+ if (op_handler > 0) {
+ foo_service_search_cancel (op_handler);
+ }
+}
+]]>
+ </programlisting>
+
+ <para>
+ Some examples of plugins implementing cancellation support are
+ grl-youtube, grl-jamendo or grl-filesystem, among others.
+ </para>
+ </section>
+</section>
+
diff --git a/doc/grilo/quick-start-plugins-testing.xml b/doc/grilo/quick-start-plugins-testing.xml
new file mode 100644
index 0000000..2f6c9ab
--- /dev/null
+++ b/doc/grilo/quick-start-plugins-testing.xml
@@ -0,0 +1,99 @@
+<?xml version="1.0"?>
+<!DOCTYPE refentry PUBLIC "-//OASIS//DTD DocBook XML V4.3//EN"
+ "http://www.oasis-open.org/docbook/xml/4.3/docbookx.dtd" [
+<!ENTITY % local.common.attrib "xmlns:xi CDATA #FIXED 'http://www.w3.org/2003/XInclude'">
+]>
+
+<section id="media-source-testing-plugins">
+ <title>Testing your plugins</title>
+
+ <para>
+ Grilo ships a GTK+ test user interface called <emphasis>grilo-test-ui</emphasis>
+ that can be used to test new plugins. This simple playground application can be
+ found in the 'grilo' core source code under tools/grilo-test-ui/. If you have
+ Grilo installed on your system, you may have this application installed as
+ well.
+ </para>
+
+ <para>
+ This application loads plugins from the default plugin installation directory
+ in your system or, alternatively, by inspecting the GRL_PLUGIN_PATH environment
+ variable, which can be set to contain a list of directories where Grilo
+ should look for plugins.
+ </para>
+
+ <para>
+ Once the plugin library is visible to Grilo one only has to start the
+ grilo-test-ui application and it will load it along with other Grilo
+ plugins available in the system.
+ </para>
+
+ <para>
+ In case there is some problem with the initialization of the plugin it should
+ be logged on the console. Remember that you can control the amount of
+ logging done by Grilo through the GRL_DEBUG environment variable. You
+ may want to set this variable to do full logging, in which case
+ you should type this in your console:
+ </para>
+
+ <programlisting>
+$ export GRL_DEBUG="*:*"
+ </programlisting>
+
+ <para>
+ If you want to focus only on logging the plugin loading process, configure
+ Grilo to log full details from the plugin registry module alone
+ by doing this instead:
+ </para>
+
+ <programlisting>
+$ export GRL_DEBUG="plugin-registry:*"
+ </programlisting>
+
+ <para>
+ In case your plugin has been loaded successfully you should see something like
+ this in the log:
+ </para>
+
+ <programlisting>
+(lt-grilo-test-ui:14457): Grilo-DEBUG: [plugin-registry] grl-plugin-registry.c:188: Plugin rank [plugin-id]' : 0
+(lt-grilo-test-ui:14457): Grilo-DEBUG: [plugin-registry] grl-plugin-registry.c:476: New source available: [source-id]
+(lt-grilo-test-ui:14457): Grilo-DEBUG: [plugin-registry] grl-plugin-registry.c:683: Loaded plugin '[plugin-id]' from '[plugin-file-absolute-path.so]'
+ </programlisting>
+
+ <para>
+ If your plugin is a Media Source (not a Metadata Source) you should be able
+ to see it in the user interface of grilo-test-ui like this:
+ <itemizedlist>
+ <listitem>
+ If the plugin implements Browse you should see the media source objects
+ spawned by the plugin in the list shown in the main view. You can
+ browse the plugin by double-clicking on any of its sources.
+ </listitem>
+ <listitem>
+ If the plugin implements Search you should see the media source objects
+ spawned by the plugin in the combo box next to the "Search" button.
+ You can now search content by selecting the media source you want to test
+ in the combo, inputting a search text in the text entry right next to it
+ and clicking the Search button.
+ </listitem>
+ <listitem>
+ If the plugin implements query you should see the media source objects
+ spawned by the plugin in the combo box next to the "Query" button.
+ You can now query content by selecting the media source you want to test
+ in the combo, inputting the plugin-specific query string in the text
+ entry right next to it and clicking the Query button.
+ </listitem>
+ </itemizedlist>
+ </para>
+
+ <para>
+ If your plugin is a Metadata Source then you should test it by doing
+ a Browse, Search or Query operation in some other Media Source available
+ and then click on any of the media items showed as result. By doing this
+ grilo-test-ui will execute a Metadata operation which would use any
+ available metadata plugins to gather as much information as possible.
+ Available metadata obtained for the selected item will be shown in the
+ right pane for users to inspect.
+ </para>
+</section>