====== 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: ListItem.IsFolder + Container.Content(images) 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 Slideshow Presentazione 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 **%%