====== How to write a Kodi Addon ======
Here are some notes I wrote while writing the **[[https://kodi.tv/|Kodi]]** plugin **[[https://github.com/RigacciOrg/script.picture.photo-frame|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 **[[https://forum.kodi.tv/showthread.php?tid=343296|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 **[[https://kodi.wiki/view/HOW-TO:HelloWorld_addon|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 [[https://kodi.wiki/view/About_Add-ons|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 **[[https://kodi.wiki/view/Addon.xml|addon.xml]]**.
Let's see some parts of the **addon.xml** file:
executable
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 **%%%%** section into the **addon.xml** file:
The **%%%%** 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 **[[https://kodi.wiki/view/Conditional_visibility|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 **[[https://kodi.wiki/view/Add-on_settings|Add-on settings]]** page. Once deployed, you can access the settings page from **Main menu** => //Add-ons// => //Add-on Context Menu// => **Settings**. 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:
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:
Photo Frame SlideshowPresentazione Photo Frame
Besides the **%%%%** tag, you can localize also **%%%%**, etc. The supported **language codes** are the **//alpha-2// ISO-639 codes** (two letters), and they are listed into the [[https://kodi.wiki/view/List_of_language_codes_(ISO-639:1988)|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:
* **resources/language/resource.language.en_gb/strings.po**
* **resources/language/resource.language.it_it/strings.po**
**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 \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:
* If you **update a .po file** you have to **restart Kodi** to re-read it, restarting the add-on is not sufficient.
* For **scripts add-ons**, according to the page **[[https://kodi.wiki/view/Language_support#String_ID_range|Language support]]**, we should use the range **32000** thru **32999** for string IDs.
* If a string contains a **double quote**, it must be escaped with a **backslash**. Notice that //C syntax// is applied, so backslash is used as the escape character.
* For better reading a **string can be split over several lines**. Just close the string with a double quote and start a new line with a double quote too. No newline characters will be added automatically, you have to include the sequence **%%\n%%** explicitly.
* For more info see **[[https://www.gnu.org/software/gettext/manual/html_node/PO-Files.html|PO-Files]]**.
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 **%%