Plex Media Center

  • Downloads
  • Mobile
    • iOS
    • Android
  • Support
    • Forums
    • Wiki
  • Dev Center
  • Contact Us
  • News
    • Blog
    • Press Releases
  • Labs

A Beginners Guide to v2.1

November 16th, 2011 | Category: Beginner | Author: Ian

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))
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 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

5 Comments so far

  1. till213 December 5th, 2011 1:00 pm

    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

  2. Ian December 5th, 2011 6:58 pm

    @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!

  3. till213 December 6th, 2011 7:40 pm

    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

  4. till213 December 8th, 2011 10:59 am

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

  5. till213 December 8th, 2011 1:04 pm

    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…

Leave a reply

Support Plex

Mmmmm...Beer!

Archives

  • November 2011

Categories

  • Beginner

Meta

  • RSS
  • Log in
  • Valid XHTML
  • XFN
  • WordPress
PLEX
Download
Wiki
Forums
Blog
Credits