Table of Contents

How to write a Kodi Addon

Here are some notes I wrote while writing the Kodi plugin script.picture.photo-frame.

What I needed was a slideshow plugin witout fancy transition effects (no transition effects at all!), but with the ability to select only some pictures from a directory and the ability to crop a selected portion of the images, without the need to actually modify the original ones. The need arose because I shot my photos using a 4:3 digital camera, but I wish to show them on a 16:9 TV screen. Black borders are unacceptable to me. Automatic zoom/crop is not an option because I want the control on it; also keeping a copy of each original photo is not an acceptable option.

I also opened this forum thread about adding this functionality to Kodi.

The idea was to start from a playlist file, which contains the list of images to show, along with geometry data to crop each image. Something like this:

IMG_6602.JPG|4000x2250+0+332
IMG_6605.JPG|2971x1671+796+628
IMG_6606.JPG|4000x2250+0+442
IMG_6610.JPG|3810x2143+90+387

My starting point was the Hello World Addon, but I had to solve several problems, so here are my notes.

Add-on types

First of all we need to understand the differences between a script add-on and a plugin add-on. As explained in the About Add-ons page:

Unlike Scripts (which can basically perform any action), Plugins do not really provide new functionality to Kodi, instead what they do is provide a directory listing (for instance, like your movie library) to Kodi. Another difference is that scripts can create their own gui (skin) while plugins can't. Plugin listings are presented in the current skin.

So it is clear that I want a script add-on, because I want full control on how each picture is displayed, I don't want to rely on the standard Kodi's picture viewer.

Kodi add-ons are mainly developed in Python language. Kodi provides its own version of Python, so it does not need to rely on the underlaying operating system. Kodi 17 comes with Python 2.7, Kodi version 19 will include Python 3.

addon.xml and addon.py

The add-on is composed mainly by two files: addon.xml and addon.py. The first one defines how the add-on is integrated in Kodi, so it is read on Kodi startup. The second file is actually the Python program run on add-on activation. You can modifiy it while Kodi is running and start it again to see the effects.

There is a dedicated page on the wiki about addon.xml.

Let's see some parts of the addon.xml file:

<addon>
    <extension point="xbmc.python.script" library="addon.py">
        <provides>executable</provides>
    </extension>
</addon>

To be considered a script add-on, we have declared it at the extension point xbmc.python.script. The library attribute defines the name of the Python script to be executed, addon.py in our case.

The addon.py in this study case is used also to create the user interface. In fact we use the Python classes xbmcgui.Window() and xbmcgui.ControlImage() to creat all we need. Another approach is to use the xbmcgui.WindowXML() class, where you provide an XML file where the user interface is defined.

Install add-on from a local zip file

The target directory for add-on installation is /home/kodi/.kodi/addons/, where each add-on have its directory. But it is not sufficient to unzip the archive there, you have to use the add-on installation procedure of Kodi. The add-on must be packed into a zip file, containing the add-on directory at the top level. Then use the following:

Main Menu ⇒ Add-ons ⇒ Search (Add-on browser) ⇒ Cancel ⇒ Install from zip file

Disable the Screen Saver

Our add-on script will perform a slideshow, so we have to disable the Kodi screensaver before starting it. We also need to re-enable the same screensaver before exiting the add-on code. We will use Kodi RPC to get the screensaver status and to set it. Passing an empty string means disabling the screensaver.

Here it is an example using the xbmc.executeJSONRPC() function and json:

import json
import xbmcaddon
import xbmcgui
 
def getScreensaver():
    command = {
        'jsonrpc': '2.0', 'id': 0, 'method': 'Settings.getSettingValue',
        'params': { 'setting': 'screensaver.mode' }
    }
    json_rpc = json.loads(xbmc.executeJSONRPC(json.dumps(command)))
    try:
        result = json_rpc['result']['value']
    except:
        result = None
    return result
 
def setScreensaver(mode):
    command = {
        'jsonrpc': '2.0', 'id': 0, 'method': 'Settings.setSettingValue',
        'params': { 'setting': 'screensaver.mode', 'value': mode}
    }
    json_rpc = json.loads(xbmc.executeJSONRPC(json.dumps(command)))
    try:
        result = json_rpc['result']
    except:
        result = False
    return result
 
 
saved_screensaver = getScreensaver()
setScreensaver('')
# ...
setScreensaver(saved_screensaver)

Run the Script using the Context Menu

The Context Menu is activated in Kodi by pressing the C key or the right mouse button. It is possibile to add an item to the Context Menu to launch our add-on. Just add an <extension> section into the addon.xml file:

<addon>
    <extension point="kodi.context.item">
        <menu id="kodi.core.main">
            <item library="addon.py">
                <label>View in Photo Frame</label>
                <visible>ListItem.IsFolder + Container.Content(images)</visible>
            </item>
        </menu>
    </extension>
</addon>

The <visible> tag allows to specify when the menu item will be visible. Here we specified two conditions, so that the menu will be available on folders and over containers containing images, which specifically means an m3u playlists. The two conditions should be both true (the plus sign means a logical AND), so it seems that a playlist file is considered as with IsFolder = True. See the wiki about Conditional visibility.

Inside the add-on Python code you can retrieve the item that was active when the Context Menu was selected:

contextmenu_item = xbmc.getInfoLabel('ListItem.FilenameAndPath')

Using settings.xml

Adding a settings page to an add-on is as simple as writing an XML file. Create the file resources/settings.xml and follow the syntax described into the Add-on settings page. Once deployed, you can access the settings page from Main menuAdd-onsAdd-on Context MenuSettings. Kodi take cares of displaying the settings page and storing the user preferences.

Here it is an example of settings.xml which allow to set two values, identified by the id attribute:

<settings>
    <category label="Window Size">
        <setting label="Width"  type="labelenum" id="WindowWidth"  values="720|1280|1920"    option="int" default="1280"/>
        <setting label="Height" type="labelenum" id="WindowHeight" values="480|576|720|1080" option="int" default="720"/>
    </category>
</settings>

From the Python add-on code, it will be possible to access the setting values using the getSetting() function. E.g.:

img_w = int(ADDON.getSetting('WindowWidth'))

Localization

Localization of a Kodi add-on takes place in several files. First of all we can localize the Information item of the Context Menu, by just adding the relevant part into the addon.xml file:

<addon>
    <extension point="xbmc.addon.metadata">
        <summary lang="en">Photo Frame Slideshow</summary>
        <summary lang="it">Presentazione Photo Frame</summary>
    </extension>
</addon>

Besides the <summary> tag, you can localize also <description>, etc. The supported language codes are the alpha-2 ISO-639 codes (two letters), and they are listed into the ISO-639:1988 Kodi Wiki page.

Then you will need to localize text strings used into Python code and into XML files (e.g. addon.xml, settings.xml, etc.). For each language you intend to support, you have to prepare a file:

NOTICE: Kodi documentation says to use here the alpha-4 ISO-639 codes (four letters) as directory name suffix.

Every file begins with a preamble:

# Kodi Media Center language file
# Addon Name: Photo Frame
# Addon id: script.picture.photo-frame
# Addon Provider: niccolo@rigacci.org
msgid ""
msgstr ""
"Project-Id-Version: XBMC-Addons\n"
"Report-Msgid-Bugs-To: niccolo@rigacci.org\n"
"POT-Creation-Date: 2019-09-11 15:40+0200\n"
"PO-Revision-Date: 2019-09-12 11:56+0200\n"
"Last-Translator: Niccolo Rigacci <niccolo@rigacci.org>\n"
"Language-Team: LANGUAGE\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Language: it_IT\n"
"Plural-Forms: nplurals=2; plural=(n != 1)\n"

then the file contains every string to be localized along with its translation. Here it is an excerpt of the Italian file:

msgctxt "#32001"
msgid "View in Photo Frame"
msgstr "Presentazione Photo Frame"

msgctxt "#32007"
msgid "Auto Play"
msgstr "Avanzamento automatico"

msgctxt "#32013"
msgid "Bad geometry for image \"%s\""
msgstr "Geometria errata per l'immagine \"%s\""

Notice that msgid is the original, untranslated English text. The msgstr is the localized version and msgctxt is an unique ID for each string.

If a localization is missing, the british english version will be used by default, so the file for the en_gb regional must exists. That file contains only the original text, not the translation:

msgctxt "#32001"
msgid "View in Photo Frame"
msgstr ""

msgctxt "#32007"
msgid "Auto Play"
msgstr ""

msgctxt "#32013"
msgid "Bad geometry for image \"%s\""
msgstr ""

Beware of this:

Finally we have to change the Python code, substituting the strings with a function call and the string ID:

ADDON = xbmcaddon.Addon()
__localize__ = ADDON.getLocalizedString
 
heading = __localize__(32004)
message = __localize__(32005)
xbmcgui.Dialog().notification(heading, message, xbmcgui.NOTIFICATION_WARNING)

Where __localize__ is simply an handy name that we will use to call the xbmcaddon.Addon().getLocalizedString() function.

We can use the string IDs also in <label> tags, label attributes, etc. in XML files. Here an example from a settings.xml:

<settings>
    <category label="32016">
        <setting label="32017" type="labelenum" id="WindowWidth"  ... />
        <setting label="32018" type="labelenum" id="WindowHeight" ... />
    </category>
</settings>

Here an example from addon.xml:

<addon>
    <extension point="kodi.context.item">
        <menu id="kodi.core.main">
            <item library="addon.py">
                <label>32001</label>
            </item>
        </menu>
    </extension>
</addon>

Change Regional Settings

Regional default for Kodi is English, which means British English (en_gb). Other English variants are labeled e.g. English (US), which requires the resource.language.en_us add-on.

When you change regional settings in Kodi (Settings ⇒ …) - if the choosen set is not already installed - the system tries to download the appropriate archive. In our test case (Kodi 17.6 on the Raspberry Pi, Debian Stretch 9.3) the download URL is something like this:

http://mirrors.kodi.tv/addons/krypton/resource.language.en_us/resource.language.fr_fr-3.0.6.zip.md5

Unfortunately the above md5 file does not exists, so the installation of the requested resource fails. You have to download the zip file manually and follow the procedure to install an add-on from a zip file. The installation will create the directory /home/kodi/.kodi/addons/resource.language.fr_fr/.

Cannot execute a graphical external program

After many hours of trying and failing, it turned out that it is not possibile to start an external program from Kodi and use the graphic display; at least it is not possible if Kodi is running without a full windowing system (e.g. X11).

When Kodi is installed on the Raspberry Pi using the RaspiOS operating system (based on Debian GNU/Linux 11 Bullseye), the program is started by the binary program kodi.bin using the argument --standalone.

kodi-standalone on the Raspberry Pi uses the GBM (Generic Buffer Management) windowing system to draw its graphics via the Mesa (an open source implementation of the OpenGL API). Other platforms supported by kodi-standalone are X11 and Wayland. You can see the following line into the kodi.log:

INFO <general>: CApplication::CreateGUI - using the gbm windowing system

GBM/KMS only supports one “DRM master”. Other applications can only use the screen when going through this master. Kodi is launched through a wrapper that uses chvt to switch X away from being the DRM master and allow kodi to become it. Without kodi supporting relinquishing being the master when launching the app and reacquiring it afterwards then launching another gui app isn't possible.

You can verify if it is possibile to lanuch a simple graphical program like kmscube (from the homonymous Debian package). Just execute the command:

# /dev/dri/card0 is /dev/dri/by-path/platform-soc:gpu-card
kmscube -D /dev/dri/card0

If Kodi is not running the kmscube runs nicely, but if Kodi is running (you can launch the program using xbmc.executescript() as explained below), you will get the following error:

failed to set mode: Permission denied

If you use strace to debug the error, you can see that the problem is with the /dev/dri/by-path/platform-soc:gpu-card device:

ioctl(3, DRM_IOCTL_MODE_SETCRTC, 0x7e9e17f8) = -1 EACCES (Permission denied)

How to start an external program from Kodi

I tried (but failed) to start a Python graphical program from Kodi. Because there is not an X11 windowing system, I tried Pygame with the drivers directfb, fbcon or svgalib. No problem if Kodi is stopped, Permission denied on the display if Kodi is running. Using the Python3 OpenGL module I was able to create a graphical program only under the X11 environment (using the DISPLAY environment variable).

To execute an external program from my add-on video plugin, I used the following two methods. The first one just starts an external Python script when the code is executed:

# This works only for a Python script.
# Notice "special://home" is "/home/kodi/.kodi" on the Raspberry Pi.
xbmc.executescript('special://home/bin/run-script.py')

This second method will add a context menu item to the add-on ListItem, which will launch the external program when selected:

# Add a context menu item to the add-on xbmcgui.ListItem object.
# This works only for a Python script.
li.addContextMenuItems([('Photo Reframe', 'RunScript(special://home/bin/run-script.py,arg1)')])

A third method should be the following, but I did not tried it:

xbmc.executebuiltin('XBMC.RunScript(special://home/bin/run-script.py)')

Other non-tested code to execute plugins or scripts:

xbmc.executebuiltin('RunPlugin(plugin://plugin.video.youtube/?addon_id=plugin.video.example)')
xbmc.executebuiltin('RunPlugin("plugin.video.something")')
xbmc.executebuiltin('RunAddon("script.something")')

Web References