A Beginners Guide to v2.1
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.
Right, lets make a start, starting 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 OSX, 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-ins 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.
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 it’s default icon and artwork for constructed ObjectContainers, etc.
Let’s break this down a little further:
# 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 OSX client of Plex it’s possible to change the type of view being displayed at any time. However, for the iOS client (and probably the Andriod one too), 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 mulitple ones of these for different types.
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)
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 it’s 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 it’s 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))
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 actual web pages 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, but i’m going to re-visit shortly in another blog… 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! For those of you who are observant and have looked at the latest code in GitHub, you might be wondering why I also left out references to the Search Service. Well, we’ll be back to go into this in a lot more detail…
5 Comments so far
Leave a reply

Hi,
being a beginner when it comes to Plex Plugin development (and Python) I was totally confused yesterday what the current API would be. For instance do I use MediaContainer/DirectoryItem (as generated by http://dev.plexapp.com/tools/plugin_kickstart/) or ObjectContainer/DirectoryObject? Function vs Callback? Etc.
Now according to your article it seems that the API documenation (http://dev.plexapp.com/docs/) is really more up to date than the code I found in most plugins.
However in your example above you still use the global “Function”:
thumb = Function(Thumb, url=thumb)
Shoulnd’t that read
thumb = Callback(Thumb, url=thumb)
now?
Also: what is the new equivalent of “PhotoItem”? In your code above you seem to use the (generic?) DataObject(data, ‘image/jpeg’). But in the documenation I could not find anything about DataObject, not to mention anything related to “Photo”.
Is DataObject the way to go for “Photos”?
Thanks, Oliver
@till213 – You’re completely correct, this blog is describing the latest and greatest version of the framework, however there is still a lot of existing plugins which are using previous iterations. I’ve updated the article to use the Callback instead of Function which was actually a v2.0 mechanism, but in this example, they are entirely interchangeable. The official documentation is definitely the first place to go for up to date information. If you don’t find what you’re looking for, there is normally a few of us reading through the forums and answering questions.
In regards to Photo classes, if you’re looking for a specific v2.1 example you should checkout the new National Geographic plug-in (https://github.com/plexinc-plugins/NationalGeographic.bundle/blob/master/Contents/Code/__init__.py). This demonstrates the use of PhotoObjects nicely. Also, I believe the newest version of MPORA also makes use of this. Both plug-ins are using Photo URL Services which can be found in the Services bundle. However, i’m just putting together the last parts of a new blog to introduce URL Services… Hopefully this explanation helps!
Hello Ian,
yes, that does help a lot, thanks for your answer. And I am aware of the forum and got my first (quick!) responses already
For now I have plenty of “pointers” to follow, but also already some concrete questions, but for which I want first to come up with some concrete example code and build up some experience before flooding the forum (also I want to make sure that they are not solely based on my noob level in Python, but really have to do with the Plex Plugin API
).
Thanks,
Oliver
I’m still struggling with these “URL Services”: when I add my PhotoObject(url = myUrl, …, thumb = myThumbUrl) I see that Plex is looking for these URL Services.
Following the API docs I extended my Info.plist with an URL RegExp to catch these photo URLs and added a *.pys code accordingly:
Contents/URL Services/[My Service URL Keyword]/ServiceCode.pys
But what I don’t quite understand is the following here: http://dev.plexapp.com/docs/channels/services/url.html#writing-the-code
“[MediaObjectsForURL(url)] As such, developers should avoid making any API calls that could delay execution (e.g. HTTP requests).”
Uhm… I get an URL (which in my case even points straight to the actual JPEG, for both the actual photo and the thumb) and am not supposed to use HTTP to actually fetch the image data? So when this function is supposed to return “Media objects representing the media available at the given URL”, how else would I get the JPEG data?
So currently I have in my MediaObjectsForURL function:
data = HTTP.Request(url, cacheTime = CACHE_1MONTH).content
return DataObject(data, ‘image/jpeg’)
But for whatever reason my code is not called yet (even though my RegExp does match, I’ve tested that), but did not have much time to investigate yesterday.
In any case, I somehow think I’m re-inventing the wheel here: for both the actual photo and the thumb I get the direct URLs (using the Instagram API – no need to parse any website for the actual photo URL).
And I don’t see any reference in the National Geographic plugin to “URL Services” either: https://github.com/plexinc-plugins/NationalGeographic.bundle/blob/master/Contents/Info.plist
(I haven’t tested that latest Plugin taken from Git yet, so I don’t know whether it would actually work).
Also you’ve mentioned a “Services bundle”: what and where is that? Somehow Plex should be able to fetch image data itself, given the direct URL, without me implementing an “URL Service” – just like it worked with the old PhotoItem, right?
In short: I’m looking forward to your upcoming article about “URL Services”, and hope you also include an example with plain and simple “photo URLs”
Btw I’ve just noticed that you did replace the “Function” (not documented anymore, so I guess that’s the pre-V2 API way to do it) with the new “Callback” in the main code, but not yet in the code excerpt (the very last one).
As to answer my own question, I found the location of this “Service bundle”:
https://github.com/plexinc-plugins/Services.bundle/
Seems to be a “system plugin” which bundles and provides “URL Services” for “supported (or common) video/image” plugins.
The following “URL Service” for the above mentioned National Geographic plugin shows how to deal with photos:
https://github.com/plexinc-plugins/Services.bundle/blob/master/Contents/Service%20Sets/com.plexapp.plugins.nationalgeographic/URL/National%20Geographic%20Photography/ServiceCode.pys
That code really parses the given url in the function GetPhoto:
photo_url = page.xpath(“//div[@class = 'primary_photo']//img”)[0].get(‘src’)
return Redirect(photo_url)
and then merely redirects to the actual and extracted “photo_url”.
But in my case I already get the actual URLs (for both the actual image and its thumb), so I think I don’t need a “PhotoObject” (which then triggers the “URL Services, as to resolve the URLs”) in the first place, but I can create with my URLs a “MediaObject and PartObject”, so no need for “URL Services” in my case – right?
Going to try that next time I get to code for my plugin…