Writing applications with Grilo

This section intends to be a starting point for developers interested in getting started with Grilo, and as such it does not try to be a comprehensive tutorial covering all topics and features of Grilo, instead, this is intends to focus on typical use case scenarios and code examples to put the developer on the right track.

Basic concepts

Understanding Grilo

Grilo provides a high-level API that application developers can use to interact with heterogeneous media providers using an homogeneous language, reducing remarkably the effort required to integrate media content from multiple sources.

This high-level, provider-independent API is then implemented for all the providers supported by Grilo by means of plugins. These plugins encapsulate all the provider-specific details, effectively hiding all the technical tidbits and particularities of specific media providers from application developers.

There are two kinds of plugins in Grilo:

  • Media Sources, which provide access to media content that can be browsed or searched.
  • Metadata Sources, which provide additional metadata about media content obtained through media sources.

Management of available plugins is done through GrlRegistry. This object exposes API to load plugins and retrieve available media source and metadata source objects.

Some plugins may require certain configuration information in order to work properly. Examples of configuration information that may be needed include API keys, service credentials, etc. Please check the section Configuring your plugins for more details on this subject.

You can instruct Grilo to load all available plugins via grl_registry_load_all_plugins(). This will look into the default installation directory for the plugins and also the paths included via the environment variable GRL_PLUGIN_PATH. Please check section Running Grilo based programs for more details on this subject.

Users can also load plugins using their plugin identifiers. These plugin identifier strings can be obtained by running the tool grl-inspect.

Users have two options to gain access to the media source and metadata source objects loaded by the plugin registry:

For more information on how to use the plugin registry, please check GrlRegistry or any of the code examples below.

Media source and metadata source objects are the objects application developers will be interacting with for most of their Grilo work. These objects provide developers with a well known interface (API) to interact with media content that is provider-independent. Typically, each media source object provides access to content delivered by one particular media provider.

Media Sources

Media Source objects provide access to media content. Application developers would usually interact with these objects issuing Browse, Search or Query operations on them. The purpose of these operations is explained below:

  • Search: Instructs the media source to look for media matching specific keywords. Example: YouTube search.
  • Browse: Navigates through a hierarchy of content exposed by the media provider. Example: Filesystem browsing.
  • Query: An advanced search mode that uses service-specific commands to exploit features specific to a particular media provider.
  • Metadata: Obtain (more) metadata information for a particular media resource.
  • Store and Remove: Push and remove content from the media provider.

Metadata Sources

Metadata Source objects provide means to acquire additional metadata that was not provided by the original media source that delivered the media. Application developers would usually interact with metadata sources via Resolve or SetMetadata operations, which are described below:

  • Resolve: Acquire additional metadata for a particular media resource.
  • SetMetadata: Update metadata information available for a particular media resource.

Asynchronous APIs

Most of the APIs exposed by Media Source and Metadata Source objects are asynchronous. The reason for this is that these operations usually involve I/O which would block the user interface if executed synchronously, specially for the more heavy operations like Browse or Search, leading to unresponsive GUIs and bad user experience.

For this reason, most of the APIs in Grilo accept a user callback and user data parameters that are used to hand over the result of the operation to the client asynchronously.

In cases where asynchrony is not needed, for example for programs that do not have a graphical user interface and are designed to run in the background, Grilo also provides synchronous versions of some of the APIs, but for situations other than this, use of the asynchronous APIs is strongly encouraged.

For operations that can produce multiple results, like Browse, Search or Query, callbacks are invoked once per matching result. For operations that are expected to produce a single result, like Metadata or Resolve, callbacks are invoked just once.

In any case, clients should always expect the result callback to be invoked at least once after an asynchronous operation has been issued, even if the operation fails or is cancelled (in which case the error parameter of the callback will be set to a non-NULL value).

Operation identifiers

Some Media Source and Metadata Source operations will return an operation identifier that serves various purposes:

  • Identify the operation within the framework.
  • Match asynchronously results with the issuing operations.
  • Provide an operation identifier that can be used to cancel the operation.

For example, issuing an asynchronous Search operation on a Media Source object would synchronously return an operation identifier. The developer can store this identifier and use it to cancel.

Also, if the operation was not cancelled, the results callback will be invoked at least once (actually, for the case of a Browse operation is will be invoked once per matching result) handing the results of the operation to the application. Each time the results callback is invoked, the operation identifier will be provided so that the callback can identify the operation that triggered the result (there can be multiple operations ongoing simultaneously).

Media objects

As discussed before, Media Source objects provide means to access media content exposed by media providers through operations like Search, Browse or Query. Media content is represented in Grilo via GrlMedia objects that contain metadata information related with a particular media resource.

There are various subclasses of GrlMedia, which provide specific APIs to get and set metadata associated with specific media types, like video, audio or image resources.

Typically, these GrlMedia objects will be obtained by issuing Search, Browse or Query operations on Media Source objects, which produce these objects as results. Upon reception, clients would usually use the API exposed by these objects to collect the information of interest.

Grilo also supports the concept of media container, represented by a container-type GrlMedia class. These objects represent categories or folders that contain other media objects (including maybe more container objects), allowing tree-like hierarchies of content, like the ones would usually traverse during Browse operations.

Please, check the GrlMedia API reference for more information on this subject.

Metadata keys

Metadata keys identify pieces of information (metadata). They are often an input parameter for many operations in Grilo, with the purpose of specifying what pieces of information are relevant for a particular operations.

Grilo has a list of predefined supported keys. For each of these keys, Grilo provides definitions (identifiers) that clients can use to refer to specific pieces of information. Some examples are GRL_METADATA_KEY_TITLE or GRL_METADATA_KEY_ARTIST.

For a complete list of all System Keys, please see all METADATA_KEY_* constants.

Some plugins can also introduce additional metadata keys known as User Keys. The difference with the System Keys is that these do not have definitions available, and hence clients must query the plugin registry to know their identifiers first using grl_registry_lookup_metadata_key().

Typical use cases explained

The following sections will guide the reader through some of the typical use cases of Grilo, using code examples to make the ideas even more clear.

Configuring your plugins

For some plugins to work properly, it is required that the user (or application developer) provides certain configuration. For example, some plugins may require a username and a password, others may require an API token to gain access to the underlying media provider.

These configuration options could be mandatory (without them the plugin cannot operate and will fail to load), or optional, in which case they are intended to allow users to tweak certain aspects of how a particular plugin should work.

An example of a mandatory configuration is in the Youtube plugin: in order for it to work a valid API key must be provided. It is the responsibility of the user (or the application developer) to get that key (usually registering in Youtube and applying for an application key) and provide it to the plugin.

In order to know what configuration options are available for a certain plugin, users/developers must check the plugin documentation.

Here is a small program illustrating how to configure a plugin form your application:

/*
 * Configuring plugins in Grilo.
 * Shows how to configure the Youtube plugin.
 */

#include <grilo.h>

#define GRL_LOG_DOMAIN_DEFAULT  example_log_domain
GRL_LOG_DOMAIN_STATIC(example_log_domain);

static void
source_added_cb (GrlRegistry *registry, GrlSource *source, gpointer user_data)
{
  /* If the Youtube plugin is installed, you should see it here now! */
  g_debug ("Detected new source available: '%s'",
           grl_source_get_name (source));
}

static void
load_plugins (void)
{
  GrlRegistry *registry;
  GError *error = NULL;

  registry = grl_registry_get_default ();

  g_signal_connect (registry, "source-added",
                    G_CALLBACK (source_added_cb), NULL);

  if (!grl_registry_load_all_plugins (registry, TRUE, &error)) {
    g_error ("Failed to load plugins: %s", error->message);
  }
}

static void
configure_plugins (void)
{
  GrlConfig *config;
  GrlRegistry *registry;

  /* Let's configure only the Youtube plugin (only requires an API key) */
  config = grl_config_new ("grl-youtube", NULL);
  grl_config_set_api_key (config,
                          "AI39si4EfscPllSfUy1IwexMf__kntTL_G5dfSr2iUEVN45RHG"
                          "q92Aq0lX25OlnOkG6KTN-4soVAkAf67fWYXuHfVADZYr7S1A");
  registry = grl_registry_get_default ();
  grl_registry_add_config (registry, config, NULL);

  /* When the plugin is loaded, the framework will provide
     this configuration for it */
}

gint
main (int argc, gchar *argv[])
{
  GMainLoop *loop;

  grl_init (&argc, &argv);
  GRL_LOG_DOMAIN_INIT (example_log_domain, "example");
  configure_plugins ();       /* Configure plugins */
  load_plugins ();            /* Load Grilo plugins */

  /* Run the main loop */
  loop = g_main_loop_new (NULL, FALSE);
  g_main_loop_run (loop);

  grl_deinit ();

  return 0;
}

For more information on how to configure plugins please check the GrlConfig API reference.

Browsing media content

Here is a small program illustrating how you can browse content from a particular media source (a similar approach can be used for searching content instead of browsing):

/*
 * Browsing in Grilo.
 * Shows the first 5 elements of each browsable source
 */

#include <grilo.h>

#define GRL_LOG_DOMAIN_DEFAULT  example_log_domain
GRL_LOG_DOMAIN_STATIC(example_log_domain);

/* This callback is invoked for each result that matches our
   browse operation. The arguments are:
   1) The source we obtained the content from.
   2) The operation identifier this result relates to.
   3) A media object representing content that matched the browse operation.
   4) Estimation of the number of remaining media objects that will be sent
   after this one as part of the same resultset (0 means that the browse
   operation is finished).
   5) User data passed to the grl_media_source_browse method.
   6) A GError if an error happened, NULL otherwise */
static void
browse_cb (GrlSource *source,
       guint browse_id,
       GrlMedia *media,
       guint remaining,
       gpointer user_data,
       const GError *error)
{
  /* First we check if the operation failed for some reason */
  if (error) {
    g_error ("Browse operation failed. Reason: %s", error->message);
  }

  /* Check if we received 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) {
    /* Get the metadata we are interested in */
    const gchar *title = grl_media_get_title (media);

    /* If the media is a container that means we could
       browse it again (that is, we could use it as the second parameter
       of the grl_media_source_browse method) */
    if (grl_media_is_container (media)) {
      guint childcount = grl_media_get_childcount (media);
      g_debug ("\t Got '%s' (container with %d elements)", title, childcount);
    } else {
      guint seconds = grl_media_get_duration (media);
      const gchar *url = grl_media_get_url (media);
      g_debug ("\t Got '%s' (media - length: %d seconds)", title, seconds);
      g_debug ("\t\t URL: %s", url);
    }
    g_object_unref (media);
  }

  /* Check if this was the last result */
  if (remaining == 0) {
    g_debug ("Browse operation finished!");
  }
}

static void
source_added_cb (GrlRegistry *registry, GrlSource *source, gpointer user_data)
{
  static gboolean first = TRUE;
  GList * keys = grl_metadata_key_list_new (GRL_METADATA_KEY_TITLE,
                        GRL_METADATA_KEY_DURATION,
                        GRL_METADATA_KEY_URL,
                        GRL_METADATA_KEY_CHILDCOUNT,
                        GRL_METADATA_KEY_INVALID);
  g_debug ("Detected new source available: '%s'",
           grl_source_get_name (source));

  /* We will just issue a browse operation on the first browseble
     source we find */
  if (first &&
      grl_source_supported_operations (source) & GRL_OP_BROWSE) {
    GrlOperationOptions *options;
    GrlCaps *caps;
    first = FALSE;
    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, 5);
    grl_operation_options_set_resolution_flags (options, GRL_RESOLVE_IDLE_RELAY);

    grl_source_browse (source,
                       NULL,
                       keys,
                       options,
                       browse_cb,
                       NULL);
    g_object_unref (caps);
    g_object_unref (options);
  }

  g_list_free (keys);
}

static void
load_plugins (void)
{
  GrlRegistry *registry;
  GError *error = NULL;

  registry = grl_registry_get_default ();
  g_signal_connect (registry, "source-added",
            G_CALLBACK (source_added_cb), NULL);
  if (!grl_registry_load_all_plugins (registry, TRUE, &error)) {
    g_error ("Failed to load plugins: %s", error->message);
  }
}

gint
main (int argc, gchar *argv[])
{
  GMainLoop *loop;
  grl_init (&argc, &argv);
  GRL_LOG_DOMAIN_INIT (example_log_domain, "example");
  load_plugins ();
  loop = g_main_loop_new (NULL, FALSE);
  g_main_loop_run (loop);
  grl_deinit ();
  return 0;
}

Please notice:

  • Finalization of the Browse operation is always indicated by setting the ‘remaining’ parameter to 0.
  • The user callback used to receive the results is guaranteed to be invoked at least once (with remaining set to 0), even if the operation fails or is cancelled (in these cases the error parameter will be set to a non-NULL value).
  • Received GrlMedia objects are owned by the application and must be freed when no longer needed.
  • If an error occurs, the GError parameter is set in the callback, but this error parameter is owned by Grilo and will be freed immediately after the callback. If you want to keep the error beyond the scope of the callback you must add a reference to it in the callback code.
  • When the media source cannot estimate the number of remaining items, remaining is set to GRL_SOURCE_REMAINING_UNKNOWN.

For more information on how to operate media sources please check the GrlSource API reference.

Searching media content

Here is a small program illustrating how you can search content by text from a particular media source (Jamendo in this example):

/*
 * Searching in Grilo.
 * Search all media in Jamendo with the word 'rock'.
 */

#include <grilo.h>
#include <string.h>

#define GRL_LOG_DOMAIN_DEFAULT  example_log_domain
GRL_LOG_DOMAIN_STATIC(example_log_domain);

static void
search_cb (GrlSource *source,
       guint browse_id,
       GrlMedia *media,
       guint remaining,
       gpointer user_data,
       const GError *error)
{
  if (error) {
    g_error ("Search operation failed. Reason: %s", error->message);
  }

  if (media) {
    const gchar *title = grl_media_get_title (media);
    if (grl_media_is_container (media)) {
      guint childcount = grl_media_get_childcount (media);
      g_debug ("\t Got '%s' (container with %d elements)", title, childcount);
    } else {
      guint seconds = grl_media_get_duration (media);
      const gchar *url = grl_media_get_url (media);
      g_debug ("\t Got '%s' (media - length: %d seconds)", title, seconds);
      g_debug ("\t\t URL: %s", url);
    }
  }

  if (remaining == 0) {
    g_debug ("Search operation finished!");
  } else {
    g_debug ("\t%d results remaining!", remaining);
  }

  g_object_unref (media);
}

static void
source_added_cb (GrlRegistry *registry, GrlSource *source, gpointer user_data)
{
  const gchar *id;
  GrlCaps *caps;
  GrlOperationOptions *options;
  GList * keys = grl_metadata_key_list_new (GRL_METADATA_KEY_TITLE,
                        GRL_METADATA_KEY_DURATION,
                        GRL_METADATA_KEY_CHILDCOUNT,
                        GRL_METADATA_KEY_INVALID);

  /* Not interested if not searchable */
  if (!(grl_source_supported_operations (source) & GRL_OP_SEARCH))
    return;

  g_debug ("Detected new searchable source available: '%s'",
       grl_source_get_name (source));

  /* Only interested in Jamendo */
  id = grl_source_get_id (source);
  if (strcmp (id, "grl-jamendo"))
    return;

  caps = grl_source_get_caps (source, GRL_OP_SEARCH);
  options = grl_operation_options_new (caps);
  grl_operation_options_set_count (options, 5);
  grl_operation_options_set_resolution_flags (options, GRL_RESOLVE_IDLE_RELAY);

  g_debug ("Searching \"rock\" in Jamendo");
  grl_source_search (source,
               "rock",
               keys,
               options,
               search_cb,
               NULL);

  g_list_free (keys);
}

static void
load_plugins (void)
{
  GrlRegistry *registry;
  GError *error = NULL;

  registry = grl_registry_get_default ();
  g_signal_connect (registry, "source-added",
            G_CALLBACK (source_added_cb), NULL);
  if (!grl_registry_load_all_plugins (registry, TRUE, &error)) {
    g_error ("Failed to load plugins: %s", error->message);
  }
}

gint
main (int argc, gchar *argv[])
{
  GMainLoop *loop;
  grl_init (&argc, &argv);
  GRL_LOG_DOMAIN_INIT (example_log_domain, "example");
  load_plugins ();
  loop = g_main_loop_new (NULL, FALSE);
  g_main_loop_run (loop);
  grl_deinit ();
  return 0;
}

Please notice that all the considerations remarked for the Browse operations above apply also to Search operations.

For more information on how to operate media sources please check the GrlSource API reference.

Handling multi-valued metadata

When working with multimedia content, it can happen that certain attributes of a particular media resource are multi-valued. For example, a particular video resource may have multiple URIs associated, considering different resolutions, streaming protocols and/or formats.

Grilo provides plugin and application developers with means to handle multi-valued properties.

One issue concerning multi-valued properties are their relations with other properties. Following the example of the video resource with multiple URIs, each of these URIs should have its own mime-type associated, as well as its own width and height values. When dealing with multi-valued properties it is necessary to make this relations among keys more explicit.

Grilo provides application and plugin developers with a high-level APIs to handle certain relations among keys consistently. Continuing with the example of the video resource with multiple URIs, there is grl_media_add_url_data() and grl_media_get_url_data_nth() to add and retrieve all the metadata associated with a particular instance of the video resource (URI, mime-type, framerate, width and height, etc) in one go.

Grilo allows plugin developers to define their own metadata keys. In this case, the plugin developer must also provide information on the relations that these keys hold with others. In this scenario plugin developers must use GrlRelatedKeys objects to group related keys together.

Here is a small program illustrating how get all available URLs from a video resource, as well the corresponding MIME value for each one. We use GrlRelatedKeys instead of the high-level API from GrlMedia to illustrate how to use it:

/*
 * Handling multivalued elements in Grilo.
 * Search all 'rock' content in Youtube, and for each one prints the available
 * URLs.
 */

#include <grilo.h>
#include <string.h>

#define GRL_LOG_DOMAIN_DEFAULT  example_log_domain
GRL_LOG_DOMAIN_STATIC(example_log_domain);

static void
search_cb (GrlSource *source,
       guint browse_id,
       GrlMedia *media,
       guint remaining,
       gpointer user_data,
       const GError *error)
{
  guint i;
  GrlRelatedKeys *url_info;

  if (error) {
    g_error ("Search operation failed. Reason: %s", error->message);
  }

  if (media) {
    /* Look through all available URLs for this video resource */
    for (i = 0; i < grl_data_length (GRL_DATA (media), GRL_METADATA_KEY_URL); i++) {
      /* Here we use the low-level GrlRelatedKeys API for demonstration purposes only,
         but we could have just used the more convenient
         grl_media_video_get_url_data_nth() API instead in this case */
      url_info = grl_data_get_related_keys (GRL_DATA (media), GRL_METADATA_KEY_URL, i);
      g_debug ("\t [%s] Got url '%s' and mime-type '%s'",
               grl_media_get_id (media),
               grl_related_keys_get_string (url_info, GRL_METADATA_KEY_URL),
               grl_related_keys_get_string (url_info, GRL_METADATA_KEY_MIME));
    }
  }

  if (remaining == 0) {
    g_debug ("Search operation finished!");
  }

  g_object_unref (media);
}

static void
source_added_cb (GrlRegistry *registry, GrlSource *source, gpointer user_data)
{
  const gchar *id;
  GrlCaps *caps;
  GrlOperationOptions *options;
  GList * keys = grl_metadata_key_list_new (GRL_METADATA_KEY_TITLE,
                        GRL_METADATA_KEY_URL,
                                            GRL_METADATA_KEY_MIME,
                        GRL_METADATA_KEY_INVALID);

  /* Not interested if not searchable */
  if (!(grl_source_supported_operations (source) & GRL_OP_SEARCH))
    return;

  g_debug ("Detected new searchable source available: '%s'",
       grl_source_get_name (source));

  /* Only interested in Youtube */
  id = grl_source_get_id (source);
  if (strcmp (id, "grl-youtube"))
    return;

  caps = grl_source_get_caps (source, GRL_OP_SEARCH);
  options = grl_operation_options_new (caps);
  grl_operation_options_set_skip (options, 0);
  grl_operation_options_set_count (options, 5);
  grl_operation_options_set_resolution_flags (options, GRL_RESOLVE_IDLE_RELAY);

  g_debug ("Searching \"rock\" in Youtube");
  grl_source_search (source,
               "rock",
               keys,
               options,
               search_cb,
               NULL);

  g_object_unref (options);
  g_object_unref (caps);

  g_list_free (keys);
}

static void
load_plugins (void)
{
  GrlRegistry *registry;
  GError *error = NULL;

  registry = grl_registry_get_default ();
  g_signal_connect (registry, "source-added",
            G_CALLBACK (source_added_cb), NULL);
  if (!grl_registry_load_all_plugins (registry, TRUE, &error)) {
    g_error ("Failed to load plugins: %s", error->message);
  }
}

static void
configure_plugins (void)
{
  GrlConfig *config;
  GrlRegistry *registry;

  config = grl_config_new ("grl-youtube", NULL);
  grl_config_set_api_key (config,
                          "AI39si4EfscPllSfUy1IwexMf__kntTL_G5dfSr2iUEVN45RHG"
                          "q92Aq0lX25OlnOkG6KTN-4soVAkAf67fWYXuHfVADZYr7S1A");
  registry = grl_registry_get_default ();
  grl_registry_add_config (registry, config, NULL);
}

gint
main (int argc, gchar *argv[])
{
  GMainLoop *loop;
  grl_init (&argc, &argv);
  GRL_LOG_DOMAIN_INIT (example_log_domain, "example");
  configure_plugins ();
  load_plugins ();
  loop = g_main_loop_new (NULL, FALSE);
  g_main_loop_run (loop);
  grl_deinit ();
  return 0;
}

For more information on how to operate with media objects please check the GrlData hierarchy API reference.

Efficient metadata resolution

When executing operations that return lists of media items (like Browse or Search) it is convenient to ensure that we do not request metadata that could slow down the operation unless it is really necessary.

A clear example of this situation is the way Youtube video URLs are resolved: a browse operation on Youtube can usually be resolved in a single HTTP query, however, if one wants to obtain the URLs of the videos then for each video in the result set another HTTP request is needed. This would slow down the browse operation remarkably and would spam Youtube with requests to obtain URLs that may not be ever needed. Indeed, a normal player would browse a list of videos and show information useful for the user to select the one he/she is interested in (title, duration, artist, etc), the URL is not interesting at this stage, it is only interesting when the user selected a video to play, and we would be interested only in that single URL and not in all the URLs of the videos we are displaying.

Grilo provides methods to application developers to query the keys (if any) that may have an impact on the performance for a particular source (like the URL in the case of Youtube), but it is usually easier to just use the GRL_RESOLVE_FAST_ONLY flag when issuing Search, Browse or Query operations.

By using this flag, Grilo will resolve only the keys that do not have an impact on performance. If you browse Youtube with this flag Grilo won’t request the URL key to the Youtube source. However, if the source could resolve the URL without performance penalties, it would do so normally.

Usually, for operations like Browse or Search that operate with large result sets it is recommended to use GRL_RESOLVE_FAST_ONLY. If you really need to get the metadata you requested for a specific item (for example if you want to play a video from Youtube you really need the URL), then you can safely use the Metadata operation without the GRL_RESOLVE_FAST_ONLY flag, which would only operate on this particular item, reducing the performance penalty and providing a more efficient solution.

The program below demonstrates how this works, it accepts as argument the id of the source we want to operate with, when the source is registered it will issue a search operation requesting only fast keys. When the search callback is invoked we will print both the title information and the URL of the first media that matched the search text.

If we run the program using grl-jamendo as target source, we will see that it retrieves both the title and the URL of the media immediately, however, if we use grl-youtube, it won’t and in order to obtain the URL we issue a new Metadata operation, this time without the GRL_RESOLVE_FAST_ONLY flag.

Of course this is a very simple example, in a real application the way this would work is that we would request the URL in a Browse/Search that could return hundreds of results and we may or may not get the URLs depending on the source we are operating with, but in any case we will ensure the operation will run as fast as possible: the user will see the results of the search fast. Then, when the user selects a media item to be played from that result set we would check if we have the URL already (and we will have the URL ready if the source could resolve it fast) in which case we can play the media right away (no time penalty at all from the user point of view). If the URL could not be resolved because it was slow for the source (like in the case of Youtube) then we just have to issue a Metadata operation requesting the URL, but that won’t be too bad because we are requesting it only for the item that the user selected, so from the user’s perspective the playback will take slightly more to start but would still be an acceptable delay.

/*
 * Shows how to get content in an efficient way.
 * Search 'rock' content in all searchable sources.
 */

#include <grilo.h>
#include <string.h>
#include <stdlib.h>

#define GRL_LOG_DOMAIN_DEFAULT  example_log_domain
GRL_LOG_DOMAIN_STATIC(example_log_domain);

const gchar *target_source_id = NULL;

static void
resolve_cb (GrlSource *source,
            guint operation_id,
            GrlMedia *media,
            gpointer user_data,
            const GError *error)
{
  if (error)
    g_error ("Resolve operation failed. Reason: %s", error->message);

  const gchar *url = grl_media_get_url (media);
  g_debug ("\tURL: %s", url);
  g_object_unref (media);
  exit (0);
}

static void
search_cb (GrlSource *source,
       guint browse_id,
       GrlMedia *media,
       guint remaining,
       gpointer user_data,
       const GError *error)
{
  if (error)
    g_error ("Search operation failed. Reason: %s", error->message);

  if (!media) {
    g_error ("No media items found matching the text \"rock\"!");
    return;
  }

  g_debug ("Got matching media from %s. Details:", target_source_id);
  const gchar *title = grl_media_get_title (media);
  g_debug ("\tTitle: %s", title);
  const gchar *url = grl_media_get_url (media);
  if (url) {
    g_debug ("\tURL: %s:", url);
    g_object_unref (media);
    exit (0);
  } else {
    g_debug ("URL no available, trying with slow keys now");
    GrlOperationOptions *options;
    GrlCaps *caps;
    GList *keys = grl_metadata_key_list_new (GRL_METADATA_KEY_URL, GRL_METADATA_KEY_INVALID);

    caps = grl_source_get_caps (source, GRL_OP_RESOLVE);
    options = grl_operation_options_new (caps);
    grl_operation_options_set_resolution_flags (options, GRL_RESOLVE_IDLE_RELAY);
    grl_source_resolve (source,
                   media,
                   keys,
                   options,
                   resolve_cb,
                   NULL);
    g_object_unref (caps);
    g_object_unref (options);
    g_list_free (keys);
  }
}

static void
source_added_cb (GrlRegistry *registry, GrlSource *source, gpointer user_data)
{
  const gchar *source_id = grl_source_get_id (source);
  GrlCaps *caps;
  GrlOperationOptions *options;

  /* We are looking for one source in particular */
  if (strcmp (source_id, target_source_id))
    return;

  GList *keys = grl_metadata_key_list_new (GRL_METADATA_KEY_TITLE,
                       GRL_METADATA_KEY_URL,
                       GRL_METADATA_KEY_INVALID);

  /* The source must be searchable */
  if (!(grl_source_supported_operations (source) & GRL_OP_SEARCH))
    g_error ("Source %s is not searchable!", source_id);

  caps = grl_source_get_caps (source, GRL_OP_SEARCH);
  options = grl_operation_options_new (caps);
  grl_operation_options_set_count (options, 5);
  grl_operation_options_set_resolution_flags (options,
                                   GRL_RESOLVE_IDLE_RELAY
                                   | GRL_RESOLVE_FAST_ONLY);

  /* Retrieve the first media from the source matching the text "rock" */
  g_debug ("Searching \"rock\" in \"%s\"", source_id);
  grl_source_search (source,
               "rock",
               keys,
               options,
               search_cb,
               NULL);
  g_object_unref (caps);
  g_object_unref (options);
  g_list_free (keys);
}

static void
configure_plugins (void)
{
  GrlConfig *config;
  GrlRegistry *registry;

  /* Let's configure only the Youtube plugin (only requires an API key) */
  config = grl_config_new ("grl-youtube", NULL);
  grl_config_set_api_key (config,
                          "AI39si4EfscPllSfUy1IwexMf__kntTL_G5dfSr2iUEVN45RHG"
                          "q92Aq0lX25OlnOkG6KTN-4soVAkAf67fWYXuHfVADZYr7S1A");
  registry = grl_registry_get_default ();
  grl_registry_add_config (registry, config, NULL);
}

static void
load_plugins (void)
{
  GrlRegistry *registry;
  GError *error = NULL;

  registry = grl_registry_get_default ();
  g_signal_connect (registry, "source-added",
            G_CALLBACK (source_added_cb), NULL);
  if (!grl_registry_load_all_plugins (registry, TRUE, &error)) {
    g_error ("Failed to load plugins: %s", error->message);
  }
}

gint
main (int argc, gchar *argv[])
{
  GMainLoop *loop;
  grl_init (&argc, &argv);

  if (argc != 2) {
    g_print ("Please specify id of the source to search " \
         "(example: grl-youtube)\n");
    exit (1);
  } else {
    target_source_id = argv[1];
  }

  GRL_LOG_DOMAIN_INIT (example_log_domain, "example");
  configure_plugins ();
  load_plugins ();
  loop = g_main_loop_new (NULL, FALSE);
  g_main_loop_run (loop);
  grl_deinit ();

  return 0;
}