Amarok/Development/Scripted Services Tutorial 2.0

From KDE Community Wiki
Revision as of 09:51, 19 November 2015 by Mamarok (talk | contribs) (→‎The template service.: fix some more obsolete stuff)

Scripted Service Tutorial

First of all, note that this tutorial is very much a work in progress!!

For some general scripting info that will also be useful for writing services, see Scripting HowTo 2.0


So you want to write a scripted service for Amarok 2 but don't know how to get started? Well, in that case, you have come to the right place.

What is a scripted service and what can it do?

A scripted service is a special Amarok 2 service that uses an external script to populate its content. Services such as the Magnatune.com or Jamendo services are plugins written in C++/Qt that features a large amount of custom code. While this approach is very powerful and allows the service to implement custom user interface elements and advanced functionality, it takes a great deal of work to write such a service. The scripted services on the other hand are very basic in comparison, offering very little room for customization, but they provide a very convenient way of quickly integrating content into Amarok 2.

what you will need.

To write a scripted service, all you need is a text editor and a recent version of Amarok 2, at the time of writing, Amarok 2.0.0 Beta3 or later is recommended.

General concepts.

Before we dive into the code, we should look at the basic concepts that scripted services rely on.

Levels

Each scripted service operates with a number of levels. This value is set when the service is first created and cannot be changed. The number of levels represents how many nested layers there will be in the service. Level 0 is always the level of playable content. A scripted service can have a maximum of 4 levels, and a minimum of 1. For instance a script that organizes its content into genres, artists, albums and tracks would have 4 levels, as shown below:

    +Genre                                 +Level 3
      +Artist                =               +Level 2
        +Album                                 +Level 1
            Track                                   +Level 0

Of course, level 2 does not have to actually represent artists, this is just to illustrate the nesting. NOTE: currently, it is not possible to actually name the levels, but that will be added sometimes after 2.0.0 i released.

A script that organizes content into two levels would look something like this:

    +Category                             +Level 1
      Track                =               +Level 0

Dynamic fetching of content

The scripted service interface is designed in a way that the script dynamically provides the content that is needed at any given time. This means that the script only have to provide limited sets of data at any time, and does not have to fetch / download / parse everything at once. This also makes it very convenient when using query based interfaces for fetching content. For instance, in a script with 4 levels like the one above, when the service is opened, Amarok will ask the script for all level 3 items ( genres ). When the user expands one of these genres ( or a few other cases where Amarok needs to get the contents of the genre ), Amarok will call the script to provide a list of artists in the genre that was expanded.

Items

Anything that is added to the service, no matter what level, is treated as an item in the script. This means that an item can represent an album, a genre, a book, an episode, a track or whatever types the script deals with. An item has a level value that describes what kind of item it is. On important rule is that items with level = 0 must always have a playable url, while items with a level greater than 0 should not ( technically they can, but it will be ignored ). Items with a level greater than 0 should also have a callback string set ( see below ). Items can also provide an html formatted info string that will be shown in the service browser when this item is selected in the service.

Callback string

As mentioned, all items that are not on level 0 ( meaning that all items that do not represent a playable item ) must have a callback string set. A callback string is basically a "note to self" for the script. It is a string that will be passed back to the script whenever Amarok needs the script to fetch the children of that item. What this callback string actually consists off is completely up to the script. I can be a url where information about child items can be fetched, a list index if the script keeps its own list of content, the name of the time, or anything else. Basically it just needs to be whatever the scripts needs to know how to get the child items of that particular item.

A very big benefit of this approach is that it makes most scripts completely stateless. The script itself does not need to mirror the tree structure it is creating within Amarok, as Amarok will always tell the script exactly what it needs to know to fetch the children of reach expanded item

How about an example. Take the diagram above of the 4 level script:

    +Genre                                 +Level 3
      +Artist                =               +Level 2
        +Album                                 +Level 1
            Track                                   +Level 0
    +Genre2                                 +Level 3
      +Artist2                =               +Level 2
        +Album2                                 +Level 1
            Track2                                   +Level 0

For each level with a + (Genre, Artist, and Album), when you create it, you associate a callback string with it. Suppose for this example, you made the callback string the same as the name. Now, when the user clicks on Genre for the first time, the service tells you that an item at Level 3 with callback string "Genre" was just expanded, so you need to return the children of "Genre." If the user expanded Genre2, then the callback string would indicate this, and you would fetch the content for Genre2. More later on how exactly the service notifies you about expansion.

Filter

On each request the filter receives to populate an item ( or populate the root level ), a filter string is supplied as well. This string simply represents the value present in the "filter" text edit in the service ( the service can, on creation choose whether this box should be shown or not, more on that later ). How the service chooses to deal with this string is entirely up to the service and greatly depends on context. Example: it can be used as a way to search the service for content.


The template service.

With the basic concepts out of the way, let us have a look at the template service script, as that will give us an idea of the elements required. This service lives in the Amarok source at "src/scripting/scripts/templates/" or it can be downloaded from git at "https://projects.kde.org/projects/extragear/multimedia/amarok/repository/revisions/master/show/src/scripting/scripts/templates/". If you fetch it from git, make sure to always get the master version.

A service script consists of 2 main files, main.js ( this file is called template.js in the template and should be copied to main.js ) and script.spec. In the folder there is also a CmakeLists.txt file. This is only needed if you are developing your script in the Amarok playground directory and installing it using cmake.

The template.js file contains the following:

    function Service()
    {
        ScriptableServiceScript.call( this, "Template Name", 1, "Description", "Introduction", false );
    }

    function onConfigure()
    {
        Amarok.alert( "This script does not require any configuration." );
    }

    function onPopulating( level, callbackData, filter )
    {
       Amarok.debug( "populating level " + level );

       var numberOfItems = 10;
          
       for ( i = 0; i < numberOfItems; i++ )
       {
            item = Amarok.StreamItem;
            item.level = ;
            item.callbackData = ;
            item.itemName = ;
            item.playableUrl = ;
            item.infoHtml = ;
            script.insertItem( item );
       }
       script.donePopulating();
   }

   Amarok.configured.connect( onConfigure );
   script = new Service();
   script.populate.connect( onPopulating );

On line one we have the function that creates the service itself.


Line 3 is the interesting one "ScriptableServiceScript.call( this, "Template Name", 1, "Title", "Introduction", false );"

The first argument "this" is simply a reference back to this script and must always be included.

The second argument is the name of the service we are creating. This is the name that will be shown in the service browser and at the top of the service itself.

The third argument is the number of levels the service has. In this template we only have 1 level, meaning that we only have playable items with no parents ( they are all on level 0 ).

The fourth argument is the short text that will be shown beneath the service name in the service browser. It should be short and briefly describe what the service does.

The fifth argument is the html formatted info that will be shown in the service info apple tin the context view when the service is first activated ( and before any items are selected )

The last argument is a boolean value that controls whether the filter bar should be shown in the service ( should it be possible for the user to filter or search in the service. Remember that the actual implementation of this has to be done by the script itself )


Line 6 starts the function that can be used of the script somehow needs to configure itself. Actually calling this function is no implemented in Amarok at the time of writing, but basically the script can pop up any dialogs and save any values it needs in this function.

The main function starts on line 11. This is the function that Amarok calls whenever it needs the script to provide more content. It has 3 arguments:

level: this is the level of the items that we need to supply. For instance, when activating the scripted service for the first time, the level will be that of the topmost level, and when expanding an item, the level will be the level of the parent -1( for instance, in the example from the earlier where we have 4 levels, when expanding an artist ( level 2 ) the level argument will be 1 as we need to supply the albums that belong to this artist )

callbackData: This is whatever callback string we supplied to the parent item currently being expanded. For top level items that do not have a parent item, this string will be empty.

filter: This is the string ( if any ) entered by the user in the filter edit in the service.

Most scripts will have different way to handle each level, but in the template script, since we only use 1, this is not applicable.

Now comes the part where you need to add your own magic. As you will notice, the rest of the onPopulating function simply creates a number of items, inserts them into the service with the "script.insertItem( item );" and calls "script.insertItem( item );" when it is all done. For a very simple example of how to add static content, look at the "cool streams" script ( http://websvn.kde.org/*checkout*/trunk/extragear/multimedia/amarok/src/scripts/radio_station_service/main.js ) and for a more advances example that fetches content from a remote site and uses xml parsing and html scraping, look at the Librivox script ( http://websvn.kde.org/*checkout*/trunk/extragear/multimedia/amarok/src/scripts/librivox_service/main.js )


At the end of the file on line 31-32 are the 3 commands needed to actually kick-start the script and connect the callback functions to Amarok.

To run the script, Amarok also needs a script.spec file. This tells Amarok how to run the script. In this file, all you need to change are the occurrences of the script name, which should match whatever name you use for your script in the main.js file .

Those are the very basics of writing a custom scripted service, but hold on, we are not quite done yet! :-)

Testing and debugging

Currently, the easiest way to test scripts is to actually develop then in the Amarok source code, in the playground directory. If you add your script directory to the CMakeLists.txt file in playground/src/scripts and create a CMakeLists.txt file with the following contents in your script directory:

install( FILES
        script.spec
        main.js
        DESTINATION ${DATA_INSTALL_DIR}/amarok/scripts/my_script_folder
)

replacing "my_script_folder" with the name of the folder your script is in.

After doing that, go to the root of the playground directory, make a directory called playground_build ( mkdir playground_build ), cd into this folder and run the same command that you would use to build Amarok. This will install your script and you will be able to load it from the script manager. If developing this way, a big advantage is that you do not need to restart Amarok when making changes to the script, but can simply unload and then reload it from the script manager.

If you would rather not have to check out all the Amarok source just to play with a script, you will have to package your script and install it from the script manager each time you change it, see next chapter for instructions.

When debugging your script, you will most likely encounter errors initially. These most often shows in one of 2 ways. Either you get an error message when trying to load the script, or nothing happens when you try to activate it or expand an item. To get debug info from your script, wrap the parts of your code you want to debug in a try-catch blog like this:

   try {
      lots_of_code_goes_here...
   }
   catch( err )
   {
       Amarok.debug( err );
   }

This will make any errors in this part of the code show up in the Amarok debug output. Alternatively, you can pop up a dialog showing the error using "Amarok.alert( err );"

Though this trick doesn't save you if you have a syntax error in Amarok 2.0.1 or earlier. The safest way to test scripts is putting this:

//this is a wrapper that saves amarok even if one of the files has syntax errors

try
{
  Importer.include("Real_Main_Script.js");
}
catch(ex)
{
  err = "Error: " + ex.toString() + "\n"
  if (ex.name)
    err += "Error Name: " + ex.name + "\n";
  if (ex.message)
    err += "Messag: " + ex.message + "\n";
  if (ex.fileName)
    err +="File: " + ex.fileName + "\n";
  if (ex.lineNumber)
    err += "Line: " + ex.lineNumber + "\n";
  if (ex.stack)
    err += "stack:\n" + ex.stack + "\n";
  
 Amarok.alert(err);
}

This way you will be notified on any errors that may occur even syntax ones without crashing Amarok. This is only needed for Amarok 2.0.1 or earlier.

Packaging and distribution.

All Amarok 2 scripts have one of these formats, foo.amarokscript.tar, foo.amarokscript.tar.gz or foo.amarokscript.tar .bz2. This file should contains the script.spec file, the main.js file, and any additional files that the script needs.

The simplest way to package a script is to move up one directory, and simply doing "tar -cjf my_service_0.1.amarokscript.tar.bz2 my_script_folder/". This will create a script archive ready for installing via the script manager or uploading to kde-apps.org. Do remember to remove any un-needed files before packaging a script for release!