Documentation on plugin development for Plex Media Server. Documentation source: https://web.archive.org/web/20150327015813/http://dev.plexapp.com/docs/bundles/index.html.
Each plug-in’s code and support files are stored in a self-contained bundle. A bundle is a specially organized directory with a .bundle extension. Mac OS X treats bundles as if they were files, allowing you to manipulate them as such in Finder. To view the contents of a bundle, right-click on it & select “Show Package Contents”. On Windows or Linux, bundles appear as regular folders.
The bundle directory itself contains only one item: the Contents
directory. This directory contains all files and other directories belonging to the plug-in. The Contents
directory should include the following items
The Info.plist
file contains information in the Apple property list format about the bundle’s contents. This information is used by the framework when loading the plug-in.
Key | Description |
---|---|
CFBundleIdentifier | A string property containing a unique identifier for the plug-in. This should be in the reverse DNS format (e.g. com.mysite.myplugin). |
PlexFrameworkVersion | A string property specifying the bundle’s target framework version. This should be set to 2 to target the current framework version. |
PlexPluginClass | A string plug-in identifying the class of the bundle. This should be set to either Content or Agent. |
PlexClientPlatforms | A string specifying a comma-separated list of client platforms supported by the bundle’s contents. The list should contain constants from ClientPlatform. |
PlexURLServices | |
PlexSearchServices | |
PlexRelatedContentServices | These properties describe the services available within the bundle. Note: These properties are optional. |
PlexMediaContainer | An array of strings identifying the containers returned by the plug-in. The array should contain constants from Container. Note: This property is optional. |
PlexAudioCodec | An array of strings identifying the containers returned by the plug-in. The array should contain constants from AudioCodec. Note: This property is optional. |
PlexVideoCodec | An array of strings identifying the containers returned by the plug-in. The array should contain constants from VideoCodec. Note: This property is optional. |
DefaultPrefs.json
is a JSON-formatted file containing a set of preferences used by the plug-in, and the default values to use if the user hasn’t specified their own.
The remaining files inside the bundle are separated into specific directories, depending on their purpose.
The Code
directory contains all of the plug-in’s source code files. Third-party code should not be included here, only files created by the developer.
The __init__.py
file is the main source code file of the plug-in. It is responsible for any initialization required, and loading any extra source code files.
See also:
The Start()
function
The predefined framework function which can be used to initialize your plugin.
The Services
directory contains source code files for the plug-in’s services.
More information about services can be found here.
The Resources
directory contains any resource files required by the plug-in, like icons or background images.
Information about loading these resource files in plug-in code is provided in the Resource API reference.
The Strings
directory contains JSON-formatted text files for each language the plug-in supports. String files should have a .json
extension and be named according to the ISO specification for language codes (e.g. en.json
). The name may also include an optional country code (e.g. en-us.json
).
More information about string files can be found in the Locale API reference.
Channels are plug-ins that provide playable content to clients via the media server. They can extend the server’s media tree to provide access to almost any type of content available online, and present a rich, browsable hierarchy to the user.
Before starting to write code, the developer should understand the concept of contexts within the runtime environment. A context is an encapsulation of certain state information. When your plug-in first starts, a single context is created - this is referred to as the global context. As a plug-in receives requests, the framework creates a new context for each one - these are referred to as request contexts.
Many of the framework’s APIs are context aware, meaning that they are able to act on information stored in the current context. What this means for the developer is that they need to do much less work to effectively support multiple parallel requests. The framework will ensure that API calls from plug-in code return values based on the correct context, affecting all manner of things from string localization, to user preferences, to supported object types.
There is only one caveat to this method - global objects cannot be modified from request contexts. Since plug-ins can potentially serve thousands of simultaneous requests, a global object could be modified by one request while it was still in use by another. Rather than use global objects, the developer should attempt to maintain state by passing arguments between functions, or using one of the data storage APIs provided by the framework.
To indicate that the bundle contains a channel plug-in, the PlexPluginClass key should be set to Content
in the Info.plist
file.
Channel plug-ins add themselves to the user interface by first defining a prefix. Once a prefix has been registered, the plug-in effectively ‘owns’ all requests received by the media server beginning with that prefix.
To register a prefix, the developer must define a function to be the main prefix handler using the @handler decorator:
@handler('/video/example', 'Example')
def Main():
pass
Any incoming requests with the /video/example
prefix will now be routed to this plug-in, and requests matching the path exactly will be directed to this function, and the channel will appear as a new icon in each supported client’s Channels menu.
To create the beginning of a browsable tree of metadata items, the developer needs to create an ObjectContainer, populate it with objects and return it to the client. The objects in this container can point to other functions within the plug-in, allowing the developer to define a rich navigation hierarchy.
The simplest and most common use of function callbacks is to connect a DirectoryObject to a function that will generate another ObjectContainer (providing a new level of navigation) using the Callback() function:
def Main():
oc = ObjectContainer(
objects = [
DirectoryObject(
key = Callback(SecondMenu),
title = "Example Directory"
)
]
)
return oc
def SecondMenu():
oc = ObjectContainer(
...
)
return oc
The Callback() function can also be used to return image data for thumbnails or background art, or provide data for a media object (usually by redirecting to the real source).
The framework reserves a few predefined function names. The developer can implement these functions if they wish to. They will be called when specific events occur.
This function is called when the plug-in first starts. It can be used to perform extra initialisation tasks such as configuring the environment and setting default attributes.
This function is called when the user modifies their preferences. The developer can check the newly provided values to ensure they are correct (e.g. attempting a login to validate a username and password), and optionally return a MessageContainer
to display any error information to the user.
This function is called when the user sets the rating of a metadata item returned by the plug-in. The key argument will be equal to the value of the item’s rating_key
attribute.
The framework provides a basic system for defining and retrieving user preferences. Allowing the user to modify and save preferences is handled automatically by the clients and the framework - the developer only needs to read them at the required points in their code.
The Prefs API provides a simple method for retrieving a given preference.
x = Prefs[id]
Retrieves the value of the user preference with the given id.
Preference files are used to define a set of preferences available for a plug-in. The files should contain a JSON-formatted array, composed of a dictionary for each preference. The following code demonstrates the available preference types:
[
{
"id": "pref1",
"label": "Text preference",
"type": "text",
"default": "Default Value",
},
{
"id": "pref2",
"label": "Hidden text preference (passwords, etc.)",
"type": "text",
"option": "hidden",
"default": "",
"secure": "true", # Indicates that the value should not be transmitted over insecure connections.
},
{
"id": "pref3",
"label": "Boolean preference",
"type": "bool",
"default": "true",
},
{
"id": "pref4",
"label": "Enum preference",
"type": "enum",
"values": ["value1", "value2", "value3"],
"default": "value3",
},
]
The Plex development platform provides a great deal of power and flexibility for developing self-contained channel plug-ins. However, these plug-ins can (and often do) present very different media hierarchies and handle media from a wide range of sources. Because of this inconsistency, it is difficult to treat the content as anything other than a navigable tree of media - there is no standard way to obtain meaningful information about a specific piece of media, find a piece of media in a predictable manner, or establish relationships between multiple pieces of media, restricting what channel plug-ins are capable of.
In order to solve this limitation, the plug-in framework includes the concept of “services”. A service consists of a set of functions that work together to perform a specific task in a well-defined manner. Because the behaviour of these services is always consistent between plug-ins, a lot of new and interesting functionality can be leveraged, allowing much deeper integration into the Plex ecosystem - media easily be searched for or linked with related content, and additional features will be enabled in future. The code is often simpler and easier to reuse too!
Three types of service are currently available:
URL services are responsible for taking a website’s URL and returning standardized Plex metadata objects. For example, given the URL http://youtube.com/watch?v=vzKbhxY1eQU, the URL service would return a video clip object with appropriate title, summary, and other relevant details.
The magic of a URL service is its ability to convert arbitrarily formatted web content into a normalized schema for a piece of media, with all the richness of the Plex library metadata (e.g. tags, cast, media format/codec information, etc.).
To add a URL service to a plug-in, the developer just to make a small addition to the plug-in’s Info.plist file. The following example illustrates a definition for a URL service handling content from YouTube:
<key>PlexURLServices</key>
<dict>
<key>YouTube</key>
<dict>
<key>Identifier</key>
<string>com.plexapp.plugins.youtube</string>
<key>URLPattern</key>
<string>http://.*youtube\.com/(watch)?\?v=</string>
<key>TestURLs</key>
<array>
<string>http://youtube.com/watch?v=vzKbhxY1eQU</string>
</array>
</dict>
</dict>
PlexURLServices
is a dictionary that can define multiple URL services. Services must be uniquely named.
Each service must have a URLPattern
defined. The value should be a regular expression that matches URLs that can be handled by the service. Plex uses this expression to determine which service should be used to handle a given URL.
The Identifier
of the service must be a globally unique string, and cannot be shared with any other URL service, even ones in different bundles. While optional, this string must be provided in order to link a URL service with a related content service.
Additionally, each service can optionally define an array of TestURLs
. This array should contain a list of URLs that the service is capable of handling. Plex’s automated testing system will use these URLs to verify that the service is functioning correctly and generate an alert if any problems occur. Developers are strongly encouraged to add a list of testable URLs to allow errors to be caught quickly.
Once a service has been defined in the Info.plist file, the developer needs to add a file containing the service’s source code. The file should be named ServiceCode.pys
(the .pys extension indicates that the file contains service code) and added to a subdirectory with the same name as the key used in the service definition. Continuing the YouTube example above, the path of the file would be as follows:
Contents/URL Services/YouTube/ServiceCode.pys
URL services should define the following functions:
This function should create and return a metadata object (for example, a VideoClipObject) and populate it with metadata from the given URL. Only the metadata should be added here - the object’s key and rating_key properties will be synthesised based on the URL.
Note: Although the object’s media items can be created here, this isn’t necessary. If no items are provided, they will be populated using the function defined below.
Key | Value |
---|---|
Parameters: | url (str) - The URL of the web page to use as a source for generating the metadata object. |
Returns: | A metadata object representing the media available at the given URL. |
This function should create and return a list of media objects and part objects representing the media available at the given URL. Callbacks may be used if obtaining the final media location requires additional computation.
Note: This function is expected to execute and return very quickly, as it could be called several times for different URLs when building a container. As such, developers should avoid making any API calls that could delay execution (e.g. HTTP requests).
Key | Value |
---|---|
Parameters: | url (str) - The URL of the web page to use as a source for generating the metadata object. |
Returns: | (list) - Media objects representing the media available at the given URL. |
This function should return a “normalised” version of the given URL. Plex uses the URL as a unique identifier for a given piece of content, but the same media will often appear at different URLs, even simply due to additional query string arguments. For example, the following URLs all point to the same video:
http://www.youtube.com/watch?v=dQw4w9WgXcQ http://www.youtube.com/?v=dQw4w9WgXcQ http://youtube.com/watch?v=dQw4w9WgXcQ http://www.youtube.com/watch?v=dQw4w9WgXcQ&feature=list_related&playnext=1&list=MLGxdCwVVULXfkWAI6t0EMb6ZsR02iwzBS
In this case, the normalised version of this URL would be:
This function is called automatically before passing the URL to the above two functions. They will always receive normalised URLs.
Note: This function is expected to execute and return very quickly, as it could be called several times for different URLs when building a container. As such, developers should avoid making any API calls that could delay execution (e.g. HTTP requests).
Key | Value |
---|---|
Parameters: | url (str) - The URL to be normalised. |
Returns: | (str) - A normalised version of the URL. |
The simplest way of testing a URL service is to use the built-in lookup function. Using the example URL given above, the service could be tested by making the following HTTP request:
$> curl "http://localhost:32400/system/services/url/lookup?url=http%3A%2F%2Fwww.youtube.com%2Fwatch%3Fv%3DdQw4w9WgXcQ"
<?xml version='1.0' encoding='utf-8'?>
<MediaContainer size="1" identifier="com.plexapp.system" mediaTagPrefix="/system/bundle/media/flags/" mediaTagVersion="1300301082">
<Video url="http://youtube.com/?v=dQw4w9WgXcQ" key="/system/services/url/lookup?url=http%3A//youtube.com/%3Fv%3DdQw4w9WgXcQ" type="clip" rating="9.3569088" duration="213000" title="Rick Astley - Never Gonna Give You Up" originallyAvailableAt="2009-10-25" summary="Music video by Rick Astley performing Never Gonna Give You Up. YouTube view counts pre-VEVO: 2,573,462 (C) 1987 PWL" ratingKey="http://youtube.com/?v=dQw4w9WgXcQ">
<Media indirect="1">
<Part file="" key="/:/plugins/com.plexapp.system/urlservice_function/Y2VyZWFsMQowCnMzMwpodHRwOi8veW91dHViZS5jb20vP3Y9ZFF3NHc5V2dYY1E_/PlayVideo?args=Y2VyZWFsMQoxCnR1cGxlCjAKcjAK&kwargs=Y2VyZWFsMQoxCmRpY3QKMQpzMzMKaHR0cDovL3lvdXR1YmUuY29tLz92PWRRdzR3OVdnWGNRczMKdXJscjAK&indirect=1"/>
</Media>
<Media indirect="1">
<Part file="" key="/:/plugins/com.plexapp.system/urlservice_function/Y2VyZWFsMQowCnMzMwpodHRwOi8veW91dHViZS5jb20vP3Y9ZFF3NHc5V2dYY1E_/PlayVideo?args=Y2VyZWFsMQoxCnR1cGxlCjAKcjAK&kwargs=Y2VyZWFsMQoxCmRpY3QKMgpzMzMKaHR0cDovL3lvdXR1YmUuY29tLz92PWRRdzR3OVdnWGNRczMKdXJsczQKNzIwcHMxMQpkZWZhdWx0X2ZtdHIwCg__&indirect=1"/>
</Media>
<Media indirect="1">
<Part file="" key="/:/plugins/com.plexapp.system/urlservice_function/Y2VyZWFsMQowCnMzMwpodHRwOi8veW91dHViZS5jb20vP3Y9ZFF3NHc5V2dYY1E_/PlayVideo?args=Y2VyZWFsMQoxCnR1cGxlCjAKcjAK&kwargs=Y2VyZWFsMQoxCmRpY3QKMgpzMzMKaHR0cDovL3lvdXR1YmUuY29tLz92PWRRdzR3OVdnWGNRczMKdXJsczQKSGlnaHMxMQpkZWZhdWx0X2ZtdHIwCg__&indirect=1"/>
</Media>
<Genre tag="Music"/>
<Tag tag="Rick"/>
<Tag tag="Astley"/>
<Tag tag="Sony"/>
<Tag tag="BMG"/>
<Tag tag="Music"/>
<Tag tag="UK"/>
<Tag tag="Pop"/>
</Video>
</MediaContainer>
The URL service can be invoked using the Service API. However, this is rarely the best course of action. Using the URL service to generate a full metadata object is often slow, and since plug-ins are usually providing a large container of items to be returned to a client, calling the service for each item in the container would cause an unacceptable delay for the user.
The most beneficial use of URL services within plug-ins is to use them to generate the media objects, but not the metadata. Since creating the media objects should always be fast, and the plug-in will usually have access to the metadata from the web page used to generate the container, this hybrid approach offers the best of both worlds - fast execution and use of the extra features enabled by using the service.
In order to simplify implementation of this common case, the framework will automatically call the URL service to populate the list of media items and set the metadata object’s key
and rating_key
properties if the developer sets the metadata object’s url
property:
video = VideoClipObject(
title = video_title,
summary = video_summary,
originally_available_at = video_date,
rating = video_rating,
url = 'http://www.youtube.com/watch?v=dQw4w9WgXcQ'
)
Search services are responsible for accepting a search query and returning a container of metadata objects that match the query. They are far simpler than URL services, and usually benefit from being able to reuse some of the service code. All installed search services are able to contribute to results from Plex’s universal search feature.
Like URL services, search services are defined in the plug-in’s Info.plist file. The following example illustrates the definition of the VideoSurf search service:
<key>PlexSearchServices</key>
<dict>
<key>VideoSurf</key>
<dict>
<key>Identifier</key>
<string>com.plexapp.search.videosurf</value>
</dict>
</dict>
As with URL services, PlexSearchServices
is a dictionary that can contain multiple search services. The key of each item should be a name for the service unique to the bundle. The value of each item should be a dictionary containing information about the service.
Currently the only item stored in this dictionary is Identifier
. This should be a globally unique identifier for the search service - no two search services (even in separate bundles) may share an identifier. Search services may share an identifier with an URL service or related content service.
Similarly, the developer needs to add a file containing the service’s source code after defining the service in the Info.plist file. The file should be named ServiceCode.pys
(the .pys extension indicates that the file contains service code) and added to a subdirectory with the same name as the key used in the service definition. Continuing the VideoSurf example above, the path of the file would be as follows:
Contents/Search Services/VideoSurf/ServiceCode.pys
Search services should define the following function:
This function should accept a user-entered query and return a container of relevant results. Mixed object types can be added to the container if necessary - the client is responsible for separating them into relevant groups.
In addition to the standard metadata properties, the source_title property can be set on the returned objects if required. This will be displayed by the client to indicate the result’s source, and is useful for services that return results from multiple sources (like VideoSurf). If no source title is given, the service’s name is used.
If the items to be returned can be handled by a URL service, it can be invoked by setting the url property of the objects.
Key | Value |
---|---|
Parameters: | query (str) - The search query entered by the user. |
Returns: | (ObjectContianer) - A container of items matching the given query. |
The simplest way of testing a search service is to use the built-in search function. Using the example service above and the query “dog”, the service could be tested by making the following HTTP request:
$> curl "http://localhost:32400/system/services/search?identifier=com.plexapp.search.videosurf&query=dog"
<?xml version='1.0' encoding='utf-8'?>
<MediaContainer size="20" identifier="com.plexapp.system" mediaTagPrefix="/system/bundle/media/flags/" mediaTagVersion="1300301082">
<Video url="http://www.youtube.com/watch?v=BqeJlOWdyLs" key="/system/services/url/lookup?url=http%3A//youtube.com/%3Fv%3DBqeJlOWdyLs" type="clip" duration="119000" title="Mickey Mouse hot dog en español" originallyAvailableAt="2008-01-20" summary="club disney Mickey Mouse hot dog en español" sourceTitle="YouTube" thumb="http://rexee-14.vo.llnwd.net/d1/video_image_1/4508/61218054_1729.jpg" ratingKey="http://www.youtube.com/watch?v=BqeJlOWdyLs">
<Media indirect="1">
<Part file="" key="/:/plugins/com.plexapp.system/urlservice_function/Y2VyZWFsMQowCnMzMwpodHRwOi8veW91dHViZS5jb20vP3Y9QnFlSmxPV2R5THM_/PlayVideo?args=Y2VyZWFsMQoxCnR1cGxlCjAKcjAK&kwargs=Y2VyZWFsMQoxCmRpY3QKMQpzMzMKaHR0cDovL3lvdXR1YmUuY29tLz92PUJxZUpsT1dkeUxzczMKdXJscjAK&indirect=1"/>
</Media>
<Media indirect="1">
<Part file="" key="/:/plugins/com.plexapp.system/urlservice_function/Y2VyZWFsMQowCnMzMwpodHRwOi8veW91dHViZS5jb20vP3Y9QnFlSmxPV2R5THM_/PlayVideo?args=Y2VyZWFsMQoxCnR1cGxlCjAKcjAK&kwargs=Y2VyZWFsMQoxCmRpY3QKMgpzMzMKaHR0cDovL3lvdXR1YmUuY29tLz92PUJxZUpsT1dkeUxzczMKdXJsczQKNzIwcHMxMQpkZWZhdWx0X2ZtdHIwCg__&indirect=1"/>
</Media>
<Media indirect="1">
<Part file="" key="/:/plugins/com.plexapp.system/urlservice_function/Y2VyZWFsMQowCnMzMwpodHRwOi8veW91dHViZS5jb20vP3Y9QnFlSmxPV2R5THM_/PlayVideo?args=Y2VyZWFsMQoxCnR1cGxlCjAKcjAK&kwargs=Y2VyZWFsMQoxCmRpY3QKMgpzMzMKaHR0cDovL3lvdXR1YmUuY29tLz92PUJxZUpsT1dkeUxzczMKdXJsczQKSGlnaHMxMQpkZWZhdWx0X2ZtdHIwCg__&indirect=1"/>
</Media>
</Video>
...
</MediaContainer>
Related content services are similar to search services - they are responsible for accepting a metadata object and returning a new container of metadata objects that are related to the one provided. Like search services, they too can usually benefit from being able to reuse some of the URL service code.
Like other services, related content services are defined in the plug-in’s Info.plist file. The following example illustrates the definition of a related content service for YouTube:
<key>PlexRelatedContentServices</key>
<dict>
<key>YouTube</key>
<dict>
<key>Identifier</key>
<string>com.plexapp.search.videosurf</value>
</dict>
</dict>
As with other services, PlexRelatedContentServices
is a dictionary that can contain multiple related content services. The key of each item should be a name for the service unique to the bundle. The value of each item should be a dictionary containing information about the service.
Currently the only item stored in this dictionary is Identifier
. This should be a globally unique identifier for the related content service - no two related content services (even in separate bundles) may share an identifier. Each related content services must be associated with a URL service by assigning a common identifier to both. This allows Plex to select related content for an item based on its URL.
The developer needs to add a file containing the service’s source code after defining the service in the Info.plist file. The file should be named ServiceCode.pys
(the .pys extension indicates that the file contains service code) and added to a subdirectory with the same name as the key used in the service definition. Continuing the YouTube example above, the path of the file would be as follows:
Contents/Related Content Services/YouTube/ServiceCode.pys
Search services should define the following function:
This function should accept a metadata object and return a container of related items. Mixed object types can be added to the container if necessary - the client is responsible for separating them into relevant groups.
In addition to the standard metadata properties, the source_title
property can be set on the returned objects if required. This will be displayed by the client to indicate the result’s source, and is useful for services that return results from multiple sources (like VideoSurf). If no source title is given, the service’s name is used.
If the items to be returned can be handled by a URL service, it can be invoked by setting the url
property of the objects.
Key | Value |
---|---|
Parameters: | metadata (str) - The metadata object to present related content for. |
Returns: | (ObjectContainer) - A container of items related to the given metadata object. |
The simplest way of testing a related content service is to use the built-in lookup function. Using the example service above and the sample URL http://www.youtube.com/watch?v=dQw4w9WgXcQ, the service could be tested by making the following HTTP request:
$> curl "http://localhost:32400/system/services/relatedcontent/lookup?url=http%3A%2F%2Fwww.youtube.com%2Fwatch%3Fv%3DdQw4w9WgXcQ"
The Resource API provides methods that can be used to load files contained in the bundle’s Resources directory.
Generates an externally accessible path for the named resource. This method is usually used to assign bundled images to object attributes.
Generates an externally accessible path for the named shared resource. A list of valid shared resource names is provided below.
Loads the named resource file and returns the data as a string.
Loads the named shared resource file and returns the data as a string.
Attempts to guess the MIME type of a given path based on its extension.
Assigns the specified MIME type to the given extension.
Retrieves the localized version of a string with the given key. Strings from the user’s current locale are given highest priority, followed by strings from the default locale.
See String files for more information on proividing localized versions of strings.
Locale.Language provides constants representing every language known by the framework. These constants represent two-character ISO 639-1 language codes.
Attempt to match a given string to a language. Returns the unknown code (xx) if no match could be found.