Thursday, December 11th, 2014

Developing an XBMC add-on in Python

XBMC is one awesome piece of media center software that runs on all popular platforms. What's more, is that it's entirely programmable via Python. I have released a couple XBMC related Python tools and videos earlier this year, and now I wanted to dive into add-on development. The add-on I will show you today takes advantage of my gopherlib.py module. This add-on will soon be submitted to the Official XBMC Addon repository in the coming weeks for all to enjoy. Here is how the add-on looks when it's running in XBMC:

As I mentioned in my last post about Gopher, XBMC follows the Gopher protocol pretty closely, so it was pretty easy to create an XBMC Gopher client. Here is the code which I will explain afterwords:

import sys, xbmc, xbmcgui, xbmcplugin, urlparse, os
from gopherlib import Gopher, GopherMenu, TextFile

BASE_URL = sys.argv[0]
addon_handle = int(sys.argv[1])

xbmc.log('HANDLE: %s' % addon_handle, xbmc.LOGNOTICE)

def add_directory(title, url):
    li = xbmcgui.ListItem(title, iconImage='DefaultFolder.png')
    xbmcplugin.addDirectoryItem(handle=addon_handle, url=url,
                                listitem=li, isFolder=True)

def add_text_file(title, url):
    li = xbmcgui.ListItem(title, iconImage='DefaultVideo.png')
    xbmcplugin.addDirectoryItem(handle=addon_handle, url=url,
                                listitem=li)

def add_image_file(title, url):
    li = xbmcgui.ListItem(title, iconImage='DefaultPicture.png', path=url)
    #li.setProperty('isPlayable', 'true')
    xbmcplugin.addDirectoryItem(handle=addon_handle, url=url,
                                listitem=li)

def add_info(info):
    li = xbmcgui.ListItem(info, iconImage='DefaultVideo.png')
    xbmcplugin.addDirectoryItem(handle=addon_handle, url='/', listitem=li)

def text_viewer(title, text):
    WINDOW = 10147 # Text Viewer window in XBMC
    TITLEBAR = 1
    CONTENTS = 5
    xbmc.executebuiltin("ActivateWindow(%d)" % WINDOW)
    w = xbmcgui.Window(WINDOW)
    xbmc.sleep(500)
    w.getControl(TITLEBAR).setLabel(title)
    w.getControl(CONTENTS).setText(text)

def image_viewer(img):
    w = xbmcgui.Window()
    w.addControl(xbmcgui.ControlImage(0,0,w.getWidth(), w.getHeight(), img, 2))
    w.doModal()
    del w

xbmc.log('BASE URL: %s' % BASE_URL, xbmc.LOGNOTICE)
xbmc.log('Arguments: %s' % sys.argv[2], xbmc.LOGNOTICE)

scheme, remainder = BASE_URL.split(':', 1)
parts = urlparse.urlparse(remainder)
netloc, path, query = (parts[1], parts[2], parts[4])
args = urlparse.parse_qs(query)

query = args.get('query', None)

xbmc.log('Path: %s' % path, xbmc.LOGNOTICE)

if path == '/':
    path = '/gopher.floodgap.com:70/1'

hostname, selector = path[1:].split('/', 1)
host, port = hostname.split(':')

g = Gopher(host, int(port))

if selector[0] == '7' and query is None:
    keyboard = xbmc.Keyboard('', 'Input Required', False)
    keyboard.doModal()
    if keyboard.isConfirmed():
        query = keyboard.getText()

data = g.get_selector(selector+sys.argv[2], query)

if isinstance(data, GopherMenu):
    for item in data.get_data():
        url = 'plugin://plugin.picture.gopher/%s:%s/%s' % (item['host'], item['port'], item['selector'])
        if item['type'] == '0':
            add_text_file(item['name'], url)
        elif item['type'] == '1':
            add_directory(item['name'], url)
        elif item['type'] == 'i':
            add_info(item['name'])
        elif item['type'] == '7':
            add_directory(item['name'], url)
        elif item['type'] == 'I':
            add_image_file(item['name'], url)
    xbmcplugin.endOfDirectory(addon_handle)
elif isinstance(data, TextFile):
    text_viewer(selector, '%s' % data)
elif selector[0] == 'I':
    filename = selector.split('/')[-1]
    temp = xbmc.translatePath('special://temp/')
    open(temp+filename,'w').write(data)
    image_viewer(temp+filename)
    os.unlink(temp+filename)

This code should be fairly easy to follow, as it basically goes from top to bottom, with a function call here and there. The first 4 functions are for generating the correct directory entries within the XBMC GUI. After, you will see 2 specialized functions used to display textual data, and another to display an image. I was unable to find a more elegant way to display resolved image URLs in XBMC. I tried using many different XBMC functions, but xbmcplugin.setResolvedUrl just wouldn't work with image files... When I implement video and audio support in the future, I will try it again. I may need to see if I can create a new VFS module for XBMC, so that the gopher:// protocol is natively supported for streaming.

The next section here takes the arguments from XBMC and turns them into a valid Gopher URL which we can then send to my gopherlib module. Once that is out of the way, we can instantiate our Gopher class. It is here that we check if the selector is a search selector and bring up the on-screen keyboard for the user if needed. Finally we get our selector and the resulting data object.

The final if structure here is used to either display an XBMC directory, a text file, or an image to the user.

You can view many more screenshots and even download the prepackaged add-on in ZIP format from my Gopher server or use the Gopher proxy.

On a side note, I also updated the Android Gopher client, since it's licensed under the BSD license, the source was available. The update now brings a more modern look and feel on newer phones and tablets, and now included a working address bar. You can grab the APK from my Gopher or Gopher proxy and side load today!

One added benefit coming soon to Gopher users, will be the ability to browse and search all of the content on Python Diary from the comfort of Gopher. I will be developing a basic Gopher application which will be-able to access the current Python Diary database and query it. So, Python Diary will soon be available on XBMC I guess. So keep a look out for that update when it arrives.

About Me

My Photo
Names Kevin, hugely into UNIX technologies, not just Linux. I've dabbled with the demons, played with the Sun, and now with the Penguins.




Kevin Veroneau Consulting Services
Do you require the services of a Django contractor? Do you need both a website and hosting services? Perhaps I can help.

This Month

If you like what you read, please consider donating to help with hosting costs, and to fund future books to review.

Python Powered | © 2012-2014 Kevin Veroneau