By using the Lua-Factory plugin it is possible to write Grilo sources in Lua. This has several advantages over the standard C source such as abstracting the GObjectification of GrlSource into a simple Lua script.
There are a few disavantages as well. Not all Grilo operations are implemented in Lua-Factory, only the most used ones: Search, Browse, Query and Resolve; Also, extending the Lua source with other Lua libraries is possible but has its own set of problems related to compatibility and security thus is not encouraged (Lua-Factory provides its own library which helps general purpose plugins).
In all, you can write faster Grilo sources with less complexity if your source match Lua-Factory limitations. In the following sections you can learn how to write Lua sources.
Overall, the high level steps of a Grilo source written in Lua is the same as one written in C: Initialization and Source operations. The finalization of the source is taken care by Lua-Factory and Lua's Garbage Collector.
The initialization is done through a global table called "source" where you should define information like the id and name of the source. There several other options which are described at Registering your lua source.
Besides the global table, Lua-Factory will try to call "grl_source_init" function with the configuration keys provided to your source. This can be used to verify and store localy the configuration keys.
Now, let's take a look at all Grilo operations that you can implement in Lua sources;
The initialization of the source happens when a client application requests the source from grilo. This usually happens during the client application start. Because of this, this is a good place to do all the synchronous calls to the grilo Lua API and do all the preparatory requests to the remote servers.
In case of a GOA-integrated plugin, it is also a good place to request all the needed information from GOA.
The single function that is responsible for the source initialization is grl_source_init.
Parameters: Table where each key is the config-key's name and the value is the one provided by Application;
Return value: Return true if the source can be initialize, false otherwise.
Description: This function is used to initialize the plugin. Check and store all the configuration parameters which are required, request GOA for credentials.
The example below shows the initialization, that first tries to get the needed credentials from the config, and if there is none, from the GOA.
-- Global config table which requires username and password source = { ... config_keys = { required = { "username", "password" }, }, ... goa_account_provider = "my_goa_type", goa_account_feature = "music", ... } -- Global table to store config data ldata = {} function grl_source_init(config) if (config) then if (config.username) and (config.password) then ldata.username = configs.username ldata.password = configs.password ldata.from_goa = false return true end end ldata.oauth2_token = grl.goa_access_token () ldata.from_goa = true if (not oauth2_token) then return false end return true end
There are four grilo operations that could be implemented in Lua plugin: grl_source_search, grl_source_browse, grl_source_query and grl_source_resolve.
The calls to all of this functions is somewhat similar: all of them have three parameters, with the first parameter being either a media or a string with which the plugin is expected to work, the second parameter being the key-value table of the operation options, and the third parameter being the callback function. And while the first parameter is trivial (either a string or a key-value table of GrlMedia), the other two need some explanation.
The options is a key-value table that represents the compilation of the various operation options. It more or less follows the structure of the GrlOperationOptions class, with type filter, filters and range filters being all placed in the "filters" table. The type filter can be found there by the key "type", while all the other filters can be found by their names. The table also includes the requested keys table. The whole options table can be shown as this:
options = { count = <int>, skip = <int>, operation_id = <int>, requested_keys = {"<key1>", "<key2>", ...} filters = { type = { audio = <boolean>, video = <boolean>, image = <boolean> } <filter name> = { value = <filter value> } <range filter name> = { min = <range filter value> max = <range filter value> } } }
The value inside the filters can be a number, a string, or a datetime, in which case it's an integer representing unixtime.
The callback is a function that is a wrapper for it's C counterpart. It doesn't require an explicit operation_id as a parameter, but is valid only for the operation to which it was provided. It is both pointless and harmful to store the callback from one operation, and use it for another.
The function accepts two parameters, both of which are optional: media which can be a table or a nil, and remaining which is integer. Just like in C version, the media is returned as a result of the current operation, and remaining should be either a non-negative integer or -1 if the remaining number is unknown. If the remaining parameter is omitted, it is considered to be zero. If the second parameter is zero, the callback is considered to be the finishing callback. Calling callback again after the finishing callback is done is considered an error. Calling callback with no parameters at all is also a valid way to make a finishing callback.
The media returned with the callback should be a key-value table. Lua-factory will attempt to reconstruct the media object with the provided data, but if the key would be unknown or the type of the value would be incompatible, the key-value pair would be ignored, and the warning would be issued. It is advisable to add a "type" key to the table, with the value being "audio", "video", "image" or "container" depending on the type of media. Without this parameter the reconstructed media will have a generic type, which may be undesirable in some cases (e.g. the media will be unbrowseable without the "container" type).
Every operation should end with a finishing callback. Leaving operation unfinished is considered an error. And even though lua-factory will handle such a situation by making the finishing callback itself, one should not rely on this behavior on a regular basis.
Parameters: String: text provided by application. Table: a key-value table with the search options. Function: a callback function with which the results should be returned.
Return value: None.
Description: Similar to its C version.
The following example shows what the port of the C example function would look like.
-- In this example we assume a media provider that can be -- queried over http, and that provides its results in xml format function foo_search_async_cb (results, callback) count = count_results (results); if (count == 0) then -- Signal "no results" callback (); else -- parse_next parses the next media item in the XML -- and creates a key-value table with the data extracted media = parse_next (results) while (media ~= nil) do count -= 1 callback (media, count) media = parse_next (results) end end end function grl_source_search (query, options, callback) foo_http_search = string.format ("http://media.foo.com?text=%s&offset=%d&count=%d", ss->text, options.count), options.skip) -- asynchronously execute an http query with callback and userdata -- userdata in our case is grilo callback grl.fetch (foo_http_search, foo_search_async_cb, callback) end
Parameters: Table: a key-value table of the container to browse. Table: a key-value table with the search options. Function: a callback function with which the results should be returned.
Return value: None.
Description: Similar to its C version.
The following example shows what the port of the C example function would look like.
-- 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. function foo_categories_async_cb (results, callback) count = count_results (xml); if (count == 0) then -- Signal "no results" callback () else -- parse_next parses the next category item in the XML -- and creates a key-value table with the data extracted, -- media.type = "container" media = parse_next_cat (results) while (media ~= nil) do count -= 1 callback (media, count) media = parse_next_cat (results) end end end function foo_media_async_cb (gchar *xml, GrlSourceBrowseSpec *os) count = count_results (xml); if (count == 0) then -- Signal "no results" callback () else -- parse_next parses the next category item in the XML -- and creates a key-value table with the data extracted, media = parse_next_media (results) while (media ~= nil) do count -= 1 callback (media, count) media = parse_next_media (results) end end end function grl_source_browse (media, options, callback) if (not media) or (not media.id) then -- Browsing the root container, the result must be the list of -- categories provided by the service foo_http_browse = string.format("http://media.foo.com/category_list?offset=%d&count=%d", options.skip, options.count); -- asynchronously execute an http query with callback and userdata -- userdata in our case is grilo callback grl.fetch (foo_http_browse, foo_categories_async_cb, callback); else -- Browsing a specific category -- We use the names of the categories as their media identifiers foo_http_browse = string.format("http://media.foo.com/content/%s?offset=%d&count=%d", media.id, options.skip, options.count) -- asynchronously execute an http query with callback and userdata -- userdata in our case is grilo callback grl.fetch (foo_http_browse, media_async_cb, callback); end end
Parameters: String: text provided by application. Table: a key-value table with the search options. Function: a callback function with which the results should be returned.
Return value: None.
Description: Similar to its C version.
The following example shows what the port of the C example function would look like.
function grl_source_query (query, options, callback) { -- In this example, we are assuming query is expected to contain a -- suitable SQL filter sql_query = prepare_sql_with_custom_filter (query, options.skip, options.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 = count_results (results); if (count == 0) then -- Signal "no results" callback (); else -- parse_next parses the next media item in the XML -- and creates a key-value table with the data extracted media = parse_next (results) while (media ~= nil) do count -= 1 callback (media, count) media = parse_next (results) end end end
Parameters: Table: a key-value table of the container to browse. Table: a key-value table with the search options. Function: a callback function with which the results should be returned.
Return value: None.
Description: Similar to its C version.
The following example shows what the port of the C example function would look like. Note that may_resolve function is handled automatically by Lua, so only the resolve function itself needs implementation.
-- In this example we assume a plugin that can resolve thumbnail -- information for audio items given that we have artist and album -- information available function grl_source_resolve (media, options, callback) for key, value in pairs(options.requested_keys) do if (value == "thumbnail") then thumbnail_needed = true end end if (not thumbnail_needed) or (not media) or (not media.artist) or (not media.album) or (#media.artist == 0) or (#media.album == 0) then callback() return end media.thumbnail = {} media.thumbnail[1] = resolve_thumb_uri (artist, album) callback (media) end
There are a few differences between how the configuration is done on C sources and how it is done in Lua sources in order to keep writing Lua sources simpler. All configuration necessary is kept in a global table named "source" and this table is used to:
The example bellow is the source table from Metrolyrics. We will pass through the required and optional fields and what are the expected values from each.
source = { id = "grl-metrolyrics", name = "Metrolyrics", description = "a source for lyrics", supported_keys = { "lyrics" }, supported_media = { 'audio', 'video' }, resolve_keys = { ["type"] = "audio", required = { "artist", "title" }, }, tags = { 'music', 'net:internet' }, }
Description: This table is used when source is being registered in order to use config keys from application. There are two possible values to set in this table:
Expects an array of strings with the name of GrlConfig keys that are mandatory for this source to be available.
Expects an array of strings with the name of GrlConfig keys that are optional for this source.
You can check at GrlConfig for a list of available config-keys.
Expecting: Table
Required: No
Description: For each value of the array, we expect the name of a metadata-key that the source should support as described at supported-keys. Check here for a list of all core metadata-keys
Expecting: Array of Strings
Required: No
Description: For each value of the array, we expect the name of a metadata-key that the source should support as described at slow-keys. Check here for a list of all core metadata-keys
Expecting: Array of Strings
Required: No
Description: This table is used for resolve operation. There are two possible values to set in this table:
The media type that the source can resolve. Expects one of the following strings which correspond to its Grilo media type: audio, video, image, all.
which expects an array with the name of metadata-keys necessary to the resolve operation. Check here for a list of all core metadata-keys.
Expecting: Table
Required: No
Lua-Factory provides a Lua library to communicate with Grilo and also to provide utility functions to the Lua source. This functions are accessible through the grl global table.
Specific functions design for Lua only can be find inside grl.lua table.
It is also worth mention that it is possible to use other libraries by including them at the global source table under "dependencies" field. Lua-Factory will check if the library exists on source-initialization.