Add-on Repository Sturcture
To distribute an add-on repository, a specific folder structure needs to be exposed over HTTP. At the root of the structure is addons.xml
that indexes the add-ons contained in the repository. Every add-on is placed in a subfolder that matches its id
and contains 4 files:
${id}-${version}.zip
- The add-on code.${id}-${version}.zip.md5
- MD5 file of the zipped add-on.icon.png
- The add-on icon.fanart.jpg
- The background image when the add-on is selected in Kodi.
A Kodi repository is also packaged and installed as an add-on.
Add-On Structure
A basic script add-on consists of an addon.xml
file and a python script that acts as an entry point. Custom window xml files are placed in resources/skins/default/720p/
.
Building the Distribution
The kodi-monkey-repo is structured to contain the source for every add-on in a subfolder. Executing the addons_xml_generator.py
python script, will produce the Kodi add-on repository structure in the dist
folder. Pushing the dist
folder to Github simplifies sharing the repository with the Kodi community.
Development Environment
Kodi dev is a Vagrant project that builds an Arch Linux Virtual Machine (VM) with a clean Kodi installation. This provides the perfect environment for developing an add-on.
Vagrant will automatically mount the working directory on the host to /vagrant
on the VM. Placing the git repo here makes the source visible to the VM.
$ git clone https://github.com/monkey-codes/kodi-dev
$ cd kodi-dev
$ mkdir -p ./git/kodi-monkey-repo
$ vagrant plugin install vagrant-reload
$ vagrant up
Ideally you will want to make changes to the add-on and just re-run the script in Kodi, instead of re-installing the add-on repeatedly. To do this, first produce a distribution of the add-on and push it to Github. Then install the repository in Kodi from the zip file in the dist folder. The repository add-on itself will just contain an addon.xml file with URL's pointing to the dist
folder on Github.
<?xml version="1.0" encoding="UTF-8" standalone="yes"?> | |
<addon id="repo.monkey" name="Code Monkeys's Kodi Addons" version="1.0.0" provider-name="monkey codes"> | |
<requires> | |
<import addon="xbmc.addon" version="12.0.0"/> | |
</requires> | |
<extension point="xbmc.addon.repository" name="Code Monkey Addon Repository"> | |
<info compressed="false">https://raw.githubusercontent.com/monkey-codes/kodi-monkey-repo/master/dist/addons.xml</info> | |
<checksum>https://raw.githubusercontent.com/monkey-codes/kodi-monkey-repo/master/dist/addons.xml.md5</checksum> | |
<datadir zip="true">https://raw.githubusercontent.com/monkey-codes/kodi-monkey-repo/master/dist/</datadir> | |
</extension> | |
<extension point="xbmc.addon.metadata"> | |
<summary>Code Monkey Addon Repository</summary> | |
<description>Home of the OpenVPN Script</description> | |
<disclaimer></disclaimer> | |
<platform>all</platform> | |
</extension> | |
</addon> |
Once the repository and the add-on is installed, you can symlink the installed source back to the git folder mounted under /vagrant
on the VM.
$ vagrant ssh
$ sudo su - kodi -s /bin/bash
$ cd ~/.kodi/addons/script.monkey.openvpn/
$ rm -rf ./*
$ ln -sf /vagrant/git/kodi-monkey-repo/script.monkey.openvpn/* ./
$ exit
$ sudo systemctl restart kodi
From this point onwards, changing the source on the host machine will reflect in Kodi, when you re-run the add-on.
Add-on Basics
This section covers a few examples of how to do the most common things in a script.
I started with the Hello World Add-on example and then progressively modified it to develop the OpenVPN Add-on.
addon.xml
This file describes the add-on to Kodi, including how to execute it.
<requires>
defines the add-on dependencies.xbmc.python.script
extension point tells Kodi that this is a script add-on and which python file to execute when the add-on is invoked.
<?xml version="1.0" encoding="UTF-8" standalone="yes"?> | |
<addon id="script.monkey.openvpn" name="OpenVPN Script" version="1.0.0" provider-name="monkey codes"> | |
<requires> | |
<import addon="xbmc.python" version="2.14.0"/> | |
<import addon="script.module.beautifulsoup" version="3.0.8"/> | |
</requires> | |
<extension point="xbmc.python.script" library="addon.py"> | |
<provides>executable</provides> | |
</extension> | |
<extension point="xbmc.addon.metadata"> | |
<platform>all</platform> | |
<summary lang="en">Control OpenVPN Connections through systemd</summary> | |
<description lang="en">Control OpenVPN Connections from inside Kodi interface.</description> | |
<license>GNU General Public License, v2</license> | |
<language></language> | |
<source>https://github.com/monkey-codes/kodi-monkey-repo/tree/master/script.monkey.openvpn</source> | |
<website>https://github.com/monkey-codes/kodi-monkey-repo</website> | |
<email>null@localhost</email> | |
<news>Updated the addon to use new addon.xml metadata</news> | |
</extension> | |
</addon> |
Displaying a List of Options
The easiest way to display a list of options or a menu is to use the select dialog. It takes a list of options and blocks the script until an option is selected, returning the index of the selected option.
Since selecting options from a list is a common operation, I found it useful to define a generic select
function that accepts a specific data structure for options. An option consists of a label
, a function (func
) to be executed when the option is selected, and a complete
function to control flow of the script after an option has been selected.
def select(heading, options, default=lambda: select_main): | |
labels = [option['label'] for option in options] | |
result = xbmcgui.Dialog().select(heading, labels) | |
selected = options[result] | |
if result == -1: | |
log_debug("using default action") | |
default() | |
return | |
selected['func'](selected) | |
selected['complete']() | |
def select_main(): | |
menu = [ | |
{'label': 'Display IP location', 'func': cmd_busy(cmd_display_current_location), 'complete': select_main}, | |
{'label': 'Select VPN', 'func': cmd_busy(cmd_select_vpn), 'complete': select_main} | |
] | |
select('OpenVPN', menu, default=select_noop) | |
The example above illustrates the code for the main menu in the add-on. Hopefully it clarifies the use of the complete
function, in this case the script will render the main menu again once one of the selected options complete.
It is worth noting that selecting options can be nested. The cmd_select_vpn
will in fact display another dialog using the same mechanism.
Displaying a Busy Dialog
Some options may take a while to execute, a good user experience will let the user know that something is happening. Kodi provides a built-in busy dialog. To display it whenever an option is selected, the func
of that option is curried with the cmd_busy
function.
def cmd_busy(command): | |
def busy_command(option): | |
xbmc.executebuiltin( "ActivateWindow(busydialog)" ) | |
retVal = command(option) | |
xbmc.executebuiltin( "Dialog.Close(busydialog)" ) | |
return retVal | |
return busy_command | |
def select_main(): | |
menu = [ | |
{'label': 'Display IP location', 'func': cmd_busy(cmd_display_current_location), 'complete': select_main}, | |
{'label': 'Select VPN', 'func': cmd_busy(cmd_select_vpn), 'complete': select_main} | |
] | |
select('OpenVPN', menu, default=select_noop) | |
Custom Windows using XML
When none of the basic GUI controls are sufficient, you can compose a new window using XML. These files live in resources/skins/default/720p
. A good place to start is to copy one of the bundled files in the default skin under /usr/share/kodi/addons/skin.confluence/720p/
.
After plenty of googling, I found a way to pass variables to the custom window. Basically lookup one of the predefined windows and set a property on it. The variable can then be read in the XML using $INFO[Window(10001).Property(PropertyName)]
.
def cmd_display_current_location(*args): | |
window = xbmcgui.WindowXMLDialog('custom-DialogVPNInfo.xml',xbmcaddon.Addon().getAddonInfo('path').decode('utf-8'), 'default', '1080p') | |
win =xbmcgui.Window(10001) | |
win.setProperty( 'VPN.ExitNode' , 'Brisbane, Australia') | |
#Property can be accessed in the XML using: | |
#<label fallback="416">$INFO[Window(10001).Property(VPN.ExitNode)]</label> | |
window.doModal() | |
del window | |
Troubleshooting Installation Issues
After many infuriating attempts to re-install the add-on after having pushed changes to Github, I realized that Kodi caches the packages locally after the first download. Simply uninstalling an add-on does not remove the cached package. To ensure that Kodi downloads a the latest distribution, delete the cached packages in /var/lib/kodi/.kodi/addons/packages/
.