A Beginner's Guide to v2.1

Introduction

For fun, I thought it might be quite useful to run through a very simple example of a new v2.1 plug-in. For those of you who are familiar with plug-ins, this will probably be fairly basic but for those keen to learn and hopefully start writing your own plug-ins, it should be a nice little walkthough and explanation.

The simplest type of plug-in is one which is constructed based upon an RSS feed. RSS is a family of web feed formats used to publish frequently updated works–such as blog entries, news headlines, audio, and video–in a standardized format. An RSS document (aka feed) includes full or summarized text, plus metadata such as publishing dates and authorship. Plug-ins which are created using these RSS feeds generally present a list of the feed entries to the user, allowing them to select the one of interest and therefore play the associated media. There are a variety of examples available on GitHub but for this example, we will consider Ask A Ninja.

Ask A Ninja is a somewhat "strange" site but provides access to a number of short clips where a Ninja attempts to answer some of the world's questions. The videos are actually hosted by a popular video site called blip.tv. This makes the process of creating a plug-in even simpler for us, since a URL Service for blip.tv already exists. If you are not familiar with a URL Service, it is the code responsible for translating a webpage's url into the actual video url and associated metadata. We're going to come back to this in a future blog.

Related Page: Channels > The Power of the URL Service

The Basics

Let's make a start with the very basics. A plug-in is essentially a folder which contains a number of source files and resources. The folder name is commonly the name of the desired plug-in with an extension of ".bundle". If you're using OS X, and you want to take a look in a bundle, simply right click on the file and select "Show Package Contents". An example folder contents is as follows:

We're going to skip over most of the plug-in configuration and resource files, but great documentation can be found here. For now, lets consider the actual source code, which is contained within the "__init__.py" file.

Note:Here are the code files from the examples below Code Examples

Starting with the Code

To begin with, a Plug-in must provide an entry point which gives the Plex client information about the plug-in. Lets jump in and start looking at some code:

TITLE = 'Ask A Ninja'
RSS_FEED = 'http://askaninja.blip.tv/rss'
NS = {'blip':'http://blip.tv/dtd/blip/1.0', 'media':'http://search.yahoo.com/mrss/'}
ART = 'art-default.jpg'
ICON = 'icon-default.png'
ICON_SEARCH = 'icon-search.png'

#####################################################################
# This (optional) function is initially called by the PMS framework to
# initialize the plug-in. This includes setting up the Plug-in static
# instance along with the displayed artwork.

def Start(): # Initialize the plug-in

Plugin.AddViewGroup("Details", viewMode="InfoList", mediaType="items")
Plugin.AddViewGroup("List", viewMode="List", mediaType="items")

# Setup the default attributes for the ObjectContainer
ObjectContainer.title1 = TITLE
ObjectContainer.view_group = 'List'
ObjectContainer.art = R(ART)

# Setup the default attributes for the other objects
DirectoryObject.thumb = R(ICON)
DirectoryObject.art = R(ART)
VideoClipObject.thumb = R(ICON)
VideoClipObject.art = R(ART)

#####################################################################
@handler('/video/askaninja', TITLE)
def MainMenu():

oc = ObjectContainer()
return oc

As the above code suggests, plug-ins can optionally define a "Start" method. This can be used for initializing the plug-in and telling the framework information about itself. For example, its default icon and artwork for constructed ObjectContainers, etc.

Let's break this down a little further:

ViewTypes

# Initialize the plug-in
Plugin.AddViewGroup("Details", viewMode="InfoList", mediaType="items")
Plugin.AddViewGroup("List", viewMode="List", mediaType="items")

These lines are configuring the type of ViewTypes that the plug-in might want to use. I'm sure you've all noticed that within the OS X client of Plex it's possible to change the type of view being displayed at any time. However, for the iOS client this is not configurable. Therefore, the developer needs to decide which view types make sense for each Container object returned by the Plug-in. This plug-in defines a single group, called "List" which actually maps to the Plex View Type of "List". You should note that you can add multiple of these for different types.

Default Values

The next section is configuring the default values for both the ObjectContainer, DirectoryObject and VideoClipObjects. You'll note that when assigning the attributes associated with the icon and art work, the string is wrapped with an "R(X)". This tells Plex that it's actually a name of a file contained within the Plug-in's Resource directory. It would also work if this was a URL to an online resource.

# Setup the default attributes for the ObjectContainer
ObjectContainer.title1 = TITLE
ObjectContainer.view_group = 'List'
ObjectContainer.art = R(ART)

# Setup the default attributes for the other objects
DirectoryObject.thumb = R(ICON)
DirectoryObject.art = R(ART)
VideoClipObject.thumb = R(ICON)
VideoClipObject.art = R(ART)

Prefix and Method

Some of you familiar with older versions of the framework might not have come across the following syntax yet, but this is something brand new in v2.1:

@handler('/video/askaninja', TITLE)
def MainMenu():

oc = ObjectContainer()
return oc

This declaration is actually performing a number of tasks. It is firstly defining a "Prefix". The "Prefix" is basically a unique identifier for the plug-in which not only defines its identifier, but also its type. It's important to note that this is of the format "/video/*" meaning that it is a Video plug-in. For Music plug-ins, this would be "/music/*" and for Photo plug-ins, this would be "/photos/*". The general convention would be to just use the name of the actual plug-in, i.e. skygo or spotify, etc.

This attribute accepts a number of optional parameters (art=…, thumb=…) for specifying the resources to be used in the Channel selection menu. By default, these will be "art-default.png" and "icon-default.png" but anything can be specified manually.

The last important aspect of this code is that it is marking a method to be called when the user selects this particular plugin (which is displayed using the defined TITLE). If the user starts this plug-in, this MainMenu function will be called and is therefore responsible for returning an ObjectContainer which contains a number of Objects. As I mentioned earlier, this plug-in is going to iterate over the items contained within the associated RSS Feed and display these as potential videos to play. Let's just jump straight in again.

@handler('/video/askaninja', TITLE)
def MainMenu():

oc = ObjectContainer()

for video in XML.ElementFromURL(RSS_FEED).xpath('//item'):

url = video.xpath('./link')[0].text
title = video.xpath('./title')[0].text
date = video.xpath('./pubDate')[0].text
date = Datetime.ParseDate(date)
summary = video.xpath('./blip:puredescription', namespaces=NS)[0].text
thumb = video.xpath('./media:thumbnail', namespaces=NS)[0].get('url')

if thumb[0:4] != 'http':
thumb = 'http://a.images.blip.tv' + thumb

duration_text = video.xpath('./blip:runtime', namespaces=NS)[0].text
duration = int(duration_text) * 1000

oc.add(VideoClipObject(
url = url,
title = title,
summary = summary,
thumb = Callback(Thumb, url=thumb),
duration = duration,
originally_available_at = date
))

return oc

#####################################################################
def Thumb(url):

try:
data = HTTP.Request(url, cacheTime = CACHE_1MONTH).content
return DataObject(data, 'image/jpeg')
except:
return Redirect(R(ICON))

Okay, so what's happening here then? We're going to have to process each item located in the RSS feed, which is being performed by the following for loop:

for video in XML.ElementFromURL(RSS_FEED).xpath('//item'):
# Do Something

This is making use of the XML helper functions provided by Plex. This will make a HTTP request to the given URL and construct an object representation of the XML document in memory. It is then using XPATH to query the document to obtain all instances where we find a node with the name "item". If you're a little unclear with this, it's best to navigate to the RSS URL and have a look at its source. You should be able to see the different items that we are iterating over.

def Thumb(url):

try:
data = HTTP.Request(url, cacheTime = CACHE_1MONTH).content
return DataObject(data, 'image/jpeg')
except:
return Redirect(R(ICON))

So, it turns out that sometimes the RSS Feed may reference the URL of an image which doesn't actually exist! This is not ideal, as if it is not suitably handled by the plug-in, it would simply cause a blank image to be used by the clients. Therefore, this Function will actually test for this case and if the URL does not exist (thus throwing an exception), it will Redirect back to the default Icon.

The second bit of magic that some of you might be wondering is how is Plex able to extract the actual video from just the web page's URL. This is done by utilizing another important addition to the v2.1 framework, called a URL Service. The formal documentation for this can be found here.

Related Page: The Power of the URL Service

If you note, the url attribute of the VideoClipObject has been assigned with the URL of the webpage:

oc.add(VideoClipObject(
url = url,
title = title,
summary = summary,
thumb = Function(Thumb, url=thumb),
duration = duration,
originally_available_at = date
))

By doing this, Plex will automatically locate and call the URL Service's MediaObjectForURL function. This includes the logic for extracting this video and redirecting the client to the appropriate video file. Now, isn't that Magic?