Scripting Xibo Content Management - A brief tour of the API

One common use case people put forward is that they want to update a particular piece of content in a layout automatically on a schedule.

That could take the form of a video being updated on a daily basis, or an image.

Since Xibo 1.5 series, there has been a “work in progress” OAuth authenticated API for scripting actions in the Xibo CMS. As of Xibo 1.6.0-rc1, there are enough API methods implemented to do some basic layout and media management to script tasks such as these.

This article is a basic walk through showing you how to connect to the API, authorise an application to use the Xibo CMS on your behalf, a quick look at some basic API methods, and finally complete code to automatically replace a media item in a layout.

Firstly you must be running Xibo CMS 1.6.0-rc1 or later. Earlier versions may not have the API methods required for this guide.

This guide uses Python as the scripting language. Python is chosen because it’s cross platform, widely available, and it’s what the Xibo project uses to test the API with, so there are existing libraries (XiboAPI) that we can leverage. It’s perfectly possible to connect to the API from other languages. Indeed some example PHP code is provided in the main Xibo repository. The use of the API from other languages however falls outside this scope of this guide.

Most modern Linux installations will come with Python ready-installed. Windows users can download and install Python here: http://www.python.org/getit This guide assumes you have Python 2.7 installed.

  • You also need the Python OAuth2 library installed. - On Linux, you’ll need to install the “python-oauth2” package (at least on Debian based systems such as Ubuntu).
  • On Windows, the process is slightly less straight forward - Download OAuth2 library from github: https://github.com/simplegeo/python-oauth2/archive/master.zip
  • Extract the downloaded file somewhere on your PC (remember where this is!)
  • Open a command promt (Start -> Run -> cmd)
  • Change directory to where you extracted OAuth2 to (eg cd c:\tmp\python-oauth2-master)
  • Then run: python setup.py install
  • The OAuth2 library will be installed to your Python installation
  • Make an empty folder somewhere to store your new script in.
  • Download the XiboAPI.py library and config file, and save a copy of both in your new folder.
  • Python is a white-space delimited language, which means that the amount each line of code is indented defines which code block a line belongs to. It’s therefore very important that you setup your text editor to follow the standard Python convention. Tab characters should not be used, but instead four space characters. On Linux, gedit is a suitable choice, and on Windows I’d recommend Notepad++.
  • This guide will give you the overview of how to communicate with the Xibo CMS via the API. It is not intended to be a guide on programming in the Python scripting language. There’s a plethora of resources out there aimed at that. I would recommend Dive into Python as a good primer before embarking on this is Python is your chosen language.
  • So lets make a start! We’ll create a new file called ReplaceVideo.py with the following contents:

!/usr/bin/python # -- coding: utf-8 -- # # Script to upload a Video to the Xibo CMS and replace # an old copy of it in a region # Imports import os from xml.dom import minidom import XiboAPI api = XiboAPI.XiboAPI()

  • So there we’ve defined that the script is Python and that it’s UTF-8 encoded (so we don’t have issues with unicode characters down the line). We then import the os, minidom and XiboAPI libraries. Finally we create an instance of the XiboAPI to communicate with the CMS.
  • Next we need to register your application with the Xibo CMS. You’ll need an administrative login to your Xibo CMS. - Once logged in as an administrator (eg xibo_admin), go to Administration -> Applications
  • Click Add Application
  • Enter your name and email address. You can leave the third and fourth boxes (website and callback URL empty)
  • Click Register
  • Note the Key and Secret that have been generated. We’ll need those in the next step.
  • Next open defaults.cfg in your text editor. We need to point the XiboAPI library at your Xibo CMS.
  • Edit the file as follows, replacing the consumer_key and consumer_secrets with the values you got from your CMS, and the URL for your Xibo CMS:

[Main] secret = NULL token = NULL url = http://yourXiboServer.com/path/xibo consumer_key = 1b9e2c84fc747257375ae3eaaa01d49705303dac7 consumer_secret = 948989ef9eb9c17c26369a2265c35cea

  • Save defaults.cfg and close it
  • Save your ReplaceVideo,py script
  • Now lets run what we have:

alex@alex-laptop:~/scratch/xibo-release/ReplaceVideo$ python ReplaceVideo.py Request Token: - oauth_token = 7dda5851bf99e09bdad1b27aebaa6fd005303dc3e - oauth_token_secret = a289a41cab324475c849283d553b113b Go to the following link in your browser: http://localhost/html/xibo-16/server/index.php?p=oauth&q=authorize&oauth_token=7dda5851bf99e09bdad1b27aebaa6fd005303dc3e Have you authorized me? (y/n)

  • That’s the output from my script so far. So hopefully your script will attempt to connect to the Xibo CMS at the URL you configured, and will ask you to authorise it with the CMS. Now before you click the link, it’s important that you log on to the Xibo CMS as the user you want the script to impersonate. So if the script needs to appear to be the user “alex”, log off the Xibo CMS and log in as “alex”. Then follow the link.
  • You’ll be taken to a page “Xibo API – Authorization Requested”. Click the Allow button. Since we didn’t define a callback URL, the page will refresh lots now. That’s normal and can be ignored. Keep clicking the Allow button until you see a message ‘Request authorized. Please return to your application.’
  • Go back to your running script and enter “y” to indicate the API request has been authorised.
  • You will see that access has been authorised:

Access Token: - oauth_token = 93399a91be50497afd0e3f66521ddc4b05303de08 - oauth_token_secret = 104e4911f5f0e1dc9a45978d2428cbfa You may now access protected resources using the access tokens above.

  • Excellent, so now we have access to the XiboAPI as your intended user. I appreciate that’s alot of setup work but most of this only ever needs to be done once. You can reuse the same XiboAPI object in more than one project.
  • Your access tokens will automatically be written to disk in a file called site.cfg. Keep that file safe as the contents allow API access to your Xibo CMS.
  • Now let’s do something useful with the API! Lets get a list of layouts in the system. Add the following to your ReplaceVideo.py script:

Call the LayoutList Method response, status, code, message, content = api.callMethod('LayoutList') # Parse the response XML doc = minidom.parseString(content) #List the layouts nodes = doc.getElementsByTagName('layout') for layout in nodes: layoutId = int(layout.attributes['layoutid'].value) layoutName = str(layout.attributes['layout'].value) layoutDescription = str(layout.attributes['description'].value) layoutTags = str(layout.attributes['tags'].value) print "Layout ID %d" % layoutId print " Name: %s" % layoutName print " Description: %s" % layoutDescription print " Tags: %s\n" % layoutTags print "Layout List Complete\n\n"

  • Save the file and run it again. Note this time since we already have API credentials, the access to the API is seemless and no user interaction is required. A list of layouts the user has access to view is shown:

alex@alex-laptop:~/scratch/xibo-release/ReplaceVideo$ python ReplaceVideo.py Layout ID 4 Name: Default Layout Description: Tags: Layout ID 5 Name: My Video Layout Description: Daily Updated Video Tags: video Layout List Complete

  • So here the Layout we’re interested in is Layout ID 5. Lets add some code to see what regions are in Layout ID 5. Add the following to ReplaceVideo.py:

We're interested in LayoutID 5 params = [('layoutid', '5')] # Call the RegionList Method response, status, code, message, content = api.callMethod('LayoutRegionList', params) # Parse the response XML doc = minidom.parseString(content) # List the Regions nodes = doc.getElementsByTagName('region') for node in nodes: regionId = str(node.attributes['regionid'].value) regionWidth = int(node.attributes['width'].value) regionHeight = int(node.attributes['height'].value) regionTop = int(node.attributes['top'].value) regionLeft = int(node.attributes['left'].value) print "Region ID %s" % regionId print " Width: %s" % regionWidth print " Height: %s" % regionHeight print " Top: %s" % regionTop print " Left: %s\n" % regionLeft print "Region List Complete\n\n"

  • Again save the file and run it again:

Region ID 47ff29524ce1b Width: 409 Height: 450 Top: 0 Left: 0 Region ID 5303e4c0c0973 Width: 227 Height: 212 Top: 40 Left: 502 Region List Complete

  • So my example layout (id 5) has two regions on it. The right-most region is the one which will host our video and that’s the second one in the list (we know it’s that one as the Left value is highest putting it further away from the left hand edge of the layout). So we’ll make a note of the region ID (5303e4c0c0973)
  • Now let’s see what’s in that region already:

We're interested in LayoutID 5 # RegionID 5303e4c0c0973 params = [('layoutid', '5'), ('regionid', '5303e4c0c0973')] # Call the RegionList Method response, status, code, message, content = api.callMethod('LayoutRegionTimelineList', params) # Parse the response XML doc = minidom.parseString(content) # List the Regions nodes = doc.getElementsByTagName('media') for node in nodes: mediaId = str(node.attributes['mediaid'].value) mediaType = str(node.attributes['mediatype'].value) mediaDuration = str(node.attributes['mediaduration'].value) print "Media ID %s" % mediaId print " Type: %s" % mediaType print " Duration: %s\n" % mediaDuration print "Media List Complete\n\n"

Media ID 11 Type: video Duration: 0 Media List Complete

  • See the video that’s already in the region listed.
  • Hopefully that’s given a basic overview of how to navigate the API, how to pass in parameters etc. Now we’ll remove most of the code we’ve written and get on with the job in hand of scripting the file upload and replacing that video in the region. We need to remember it’s Layout ID 5, region 5303e4c0c0973 (or the correct values from your CMS). That’s how we’ll identify where we want our video to end up being added, and the old video to delete.
  • So now your ReplaceVideo.py should look like this:

!/usr/bin/python # -- coding: utf-8 -- # # Script to upload a Video to the Xibo CMS and replace # an old copy of it in a region # Imports import os from xml.dom import minidom import XiboAPI api = XiboAPI.XiboAPI() layoutId = 5 regionId = '5303e4c0c0973' fileToUpload = 'myVideo.mp4' mediaName = 'Daily Video'

  • Clearly set layoutId and regionId to the correct values we obtained earlier. Change fileToUpload to be the filename of the video you want to upload. It could be in the same directory as your script (in which case just the name is sufficient), or it could be a full path to the file. It’s up to you.
  • I’ve included some library functions to do the heavy lifting for you. Add the following to the file and save:

################################# # START LIBRARY FUNCTIONS ################################# def uploadFile(api,path,chunksize=256000,fileid='0',offset=0,size=0): # Uploads file at path (full path) # Returns a file upload ID which can be used to add the media # to the library # # eg uploadFile(api,'/home/user/1.jpg') # # Optional Parameters: # chunksize - Int: How many bytes to upload at each pass # fileid - String: The fileUploadID to use. Useful if you're resuming an interupted upload. # offset - Long: How many bytes to skip over before beginning upload. Useful for resuming an interupted upload. # size - Long: How many bytes to upload. 0 = the whole file. Useful to break an upload part way through for testing. # Must be smaller than the size of the file in bytes. # First check the test file exists if not os.path.isfile(path): print "Error: File does not exist %s " % path exit(1) checksum = '' data = '' if size == 0: size = os.path.getsize(path) chunkOK = False # If the file is smaller than the chunksize, send one chunk # of the correct size. if chunksize > size: chunksize = size # Open the file for binary read fh = open(path,'rb') while offset size: chunksize = size - offset # All chunks uploaded # Close the file handle fh.close() return fileid def layoutRegionMediaDelete(api,layoutid,regionid,mediaid,lkid): params = [('mediaId',mediaid), ('regionId',regionid), ('layoutId',int(layoutid)), ('lkId',int(lkid)) ] response, status, code, message, content = api.callMethod('LayoutRegionMediaDelete', params) def libraryMediaDelete(api,mediaid): params = [('mediaId',mediaid)] response, status, code, message, content = api.callMethod('LibraryMediaDelete', params) ############################################# # END LIBRARY FUNCTIONS #############################################

  • So first up we upload the new file to the CMS. Note here that by upload we mean copy it to the server ready to be added to the library. We add it to the library in a second stage later on. Add the following to upload:

Upload our file to the CMS so it's ready to add # Note this won't make it appear in the library # We do that further down the script uploadId = uploadFile(api,fileToUpload)

  • Next we need to find the ID of the old video file, remove it from the Region Timeline, and then delete it from the library. Add the following:

Find out what the mediaID of the old video is, and remove it from the layout oldMediaId = None params = [('layoutid', layoutId), ('regionid', regionId)] # Call the RegionList Method response, status, code, message, content = api.callMethod('LayoutRegionTimelineList', params) # Parse the response XML doc = minidom.parseString(content) # List the Regions nodes = doc.getElementsByTagName('media') for node in nodes: if str(node.attributes['mediatype'].value) == 'video': oldMediaId = str(node.attributes['mediaid'].value) lkid = str(node.attributes['lkid'].value) if not oldMediaId is None: # We found an old video in the region # Lets delete it print "Removing old media with ID %s" % oldMediaId layoutRegionMediaDelete(api, layoutId, regionId, oldMediaId, lkid) libraryMediaDelete(api, oldMediaId)

  • Now the old video is gone from the layout timeline, and from the CMS Library, we can add our new video to the library and add it to the region. Add the following:

Now the file is uploaded, and the old file is deleted, # we need to add the newly uploaded file to the CMS Library params = [('fileId', uploadId), ('type', 'video'), ('name', mediaName), ('duration', '0'), ('permissionId','1'), ('fileName',os.path.basename(fileToUpload)) ] response, status, code, message, content = api.callMethod('LibraryMediaAdd', params) # Get the ID of the file we just added mediaId = api.parseID(content,'media') # Now mediaID contains the library media ID of our freshly uploaded video print "Adding new media with ID %s" % mediaId # Now add our new media item to the region params = [('layoutId',layoutId), ('regionId',regionId), ('mediaList',mediaId), ] response, status, code, message, content = api.callMethod('LayoutRegionLibraryAdd', params)

  • Finally check that the video was added correctly by listing the media items in the region. Add the following:

Finally Check the layout now contains the new video # by listing the contents of the region and checking for our new video params = [('layoutId', layoutId), ('regionId', regionId)] # Call the RegionList Method response, status, code, message, content = api.callMethod('LayoutRegionTimelineList', params) # Parse the response XML doc = minidom.parseString(content) # List the Regions nodes = doc.getElementsByTagName('media') for node in nodes: if str(node.attributes['mediatype'].value) == 'video': if str(node.attributes['mediaid'].value) == str(mediaId): print "Video Uploaded and Layout Modified Successfully" exit(0) print "Error Uploading Video"

  • Now save and run your file. You should see output as follows:

alex@alex-laptop:~/scratch/xibo-release/ReplaceVideo$ python ReplaceVideo.py Removing old media with ID 15 Adding new media with ID 16 Video Uploaded and Layout Modified Successfully

You can download the completed ReplaceVideo.py file here.