Johan Zietsman

Johan Zietsman


I'm Johan Zietsman, and welcome to my software blog, where I'm committed to sharing valuable insights, best practices, and in-depth tutorials.

Curious software developer, motorcycle enthusiast, rugby fanatic and an avid gamer. My code always works sometimes.

Share


Tags


Johan Zietsman

Kodi Add-on Development

Lessons learned from developing my first Kodi add-on.

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:

A Kodi repository is also packaged and installed as an add-on.

repo-structure

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/.

addon-structure

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.

git-structure

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>
view raw addon.xml hosted with ❤ by GitHub

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.

<?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>
view raw addon.xml hosted with ❤ by GitHub

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.

screen_shot_1

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)
view raw addon.py hosted with ❤ by GitHub

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)
view raw addon.py hosted with ❤ by GitHub

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
view raw addon.py hosted with ❤ by GitHub

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/.

References

http://brianhornsby.com/kodi_addons/openvpn

Curious software developer, motorcycle enthusiast, rugby fanatic and an avid gamer. My code always works sometimes.

View Comments