Channel Development Templates

I know I ask alot of questions here on this forum about all the little details and I greatly appreciate all the help you guys are giving me. 

 

One of the reason I ask so many questions is that I am making some basic templates for all the different files needed for channel development with explanations, syntax and other notes. I am doing this for my own benefit, because my memory is not what it used to be, so it will keep me from constantly having to refer back to multiple post located on lots of different topics. And it is easier for me to remember to make note of these details as I go through the process.

But I thought, once I feel like I undertsand the basics (or more accurately I have beaten each subject area like a dead horse :D ), I could share my templates and maybe they would help some other newbies in the future. Kind of a way to pay it forward. And I know that I am still just learning the basics and am in no way qualified to teach others anything.  But sometimes when you are just starting out, I think you question some of the basic general concepts that other newbies would need to know that someone who has been programming for awhile takes for granted or just assumes is understood.

I can post these templates at the end of each topic, see any additional info or tweaks I might get from you more seasoned programmers on the forum, and then go back and edit that template based on the feedback I get. And then I will add the finished templates here.

 

Please let me know if there is something I have missed or gotten wrong in these templates or anything else that can be added.
 

Info.plist Template

Here is the first template I have completed, mainly because it was very basic and simple it did not require alot of added information.  (For some reason my comments are tabbing out a little farther than they should when I add them to this post)

<?xml version="1.0" encoding="UTF-8"?>



            
            
        CFBundleIdentifier
            
            com.plexapp.plugins.yourchannelname
        PlexFrameworkVersion
                    
            2
            
            
            
        
        PlexClientPlatforms
                
            *

        
                
        PlexClientPlatformExclusions
                
            
        PlexPluginRegions
            
                
                
                
            
        
                
                
            
        PlexServerPlatforms
            
        PlexServerPlatformsExclusions
                
            
        PlexMinimumServerVersion
                
               
        PlexFrameworkFlags
            
                
                UseRealRTMP
            
        
        PlexPluginDebug
        
                
                
                
        
        PlexPluginTesterFlags
            
                    
                    
                NoURLServices
            
        PlexPluginCodePolicy
                
            Elevated
        PlexPluginMode
                
            AlwaysOn
            
            
        PlexPluginCodePolicy
            Elevated
        PlexAgentAttributionText
            
                Text about the agent here
            
        PlexPluginClass
                
                
            

        PlexPluginConsoleLogging
            
            0
            
        

Hope it helps.

Since I posted my url service templates over in the URL service thread and did not get any input, I am going to go ahead and post them here, so they are all in one place.  I did update the template for the ServiceInfo.pys on advice from Mikedm139 to try not to include Python modules and use regex when possible for searching within data.

If anyone sees any part that needs to be updated or change, just let me know.

I haven't created a template for the __inti__.py yet, but so many parts of that vary so much and depend on your coding.  I am making notes and have some more questions to ask, but I will eventually make a basic template for that as well.

ServiceInfo.plist Template:

<?xml version="1.0" encoding="UTF-8"?>



    
    
	
	
	Search
        
            
            
            Your Unique Name
                
        
    URL
        
            
            
            Your Unique Name
                
                    
                    URLPatterns
                        
                        
                        
                        
                            
                            
                            http://([^.]+.)?domainname/.+ 		
                        
        <!-- THE REST OF THE KEYS BELOW ARE OPTIONAL FOR THE URL SERVICE -->
                <!-- If you do not add a test function to your URL service to find example urls-->
                <!-- Then you will need to add an example url here to be used for testing-->
                <key>TestURLs</key>
                    <array>
                        <!-- The URL you use here, must be constant and not change on the website over time-->
                        <!-- If you think this address will be altered or be deleted later -->
                        <!-- it is better to create a test function in your ServiceCode.pys instead -->
                        <string>http://www.anywebsite.com/anyfolder/anyparticularpage.html</string>
                    </array>
                <!-- If you do not add a test function to your URL service to find example urls-->
                <key>TestURLs</key>
                    <array>
                        <!-- The URL you use here, must be constant and not change on the website over time-->
                        <!-- If you think this address will be altered or be deleted later -->
                        <!-- it is better to create a test function in your ServiceCode.pys instead -->
                        <string>http://www.anywebsite.com/anyfolder/anyparticularpage.html</string>
                    </array>
        </dict>
    </dict>
<!-- OPTIONAL PlexFrameworkFlags KEY -->
<key>PlexFrameworkFlags</key>
    <array>
        <!-- possible values are UseRealRTMP -->	
        <string>UseRealRTMP</string>
    </array>

ServiceCode.pys Template:

This is the result of trying to make a small change that completely removed this template. 
# IMPORTANT NOTE: AS WITH ALL PYTHON CODING, PROPER INDENTATION IS NECESSARY TO PREVENT ERRORS.
# PROGRAMS LIKE NOTEPAD++ DO NOT ALWAYS LINE UP PROPERLY OR CATCH THESE INDENTION ERRORS. TO
# PREVENT THESE AND OTHER PYTHON ERRORS, BEFORE RUNNING THE PROGRAM IN PLEX OR AFTER MAKING CHANGES
# TO THE FILE, I OPEN IT FIRST IN A LOCAL VERSION OF PYTHON I HAVE LOADED AND CHOOSE
# RUN > CHECK MODULE TO FIND ANY POSSIBLE PROBLEMS.
# These are python variables you set up for use later in this file   
# The naming and values are based on the where and how you choose to use them in your code
# For the most part, you are defining variables you will use in the PlayVideo function
BASE_URL = 'base_http_address_here'
# It is best practice to use regex when possible and avoid importing any Python modules, so below is a regex statement I use
# later in my PlayVideo function to find the video info xml address within the page that allows me to easily pull the 
# web pages corresponding video file location
RE_XML_URL = Regex("/xml/video(.+?)',")
# The variable below is basic regex to pull a video from an html page.  I show its use in a optional version of the PlayVideo function
# RE_VIDEO_URL = Regex('videofile:"(?P[^"]+)"')
# IMPORTANT NOTE: THE VALUE OF 'URL' THAT IS PASSED TO THE DIFFERENT FUNCTIONS IN THIS PROGRAM IS DETERMINED EITHER 
# WITHIN THE PROGRAMMING OF THE INDIVIDUAL CHANNEL PLUGIN THAT USES THIS URL SERVICE OR BY THE END USER CHOOSING THE PLEXIT BUTTON
########################################################################################################
# BELOW IS AN OPTIONAL CODE FOR CONVERTING HTML VIDEO PAGES TO THEIR CORRESPONDING VIDEO INFO XML PAGES.
# IF YOU CAN FIND REFERENCE TO THESE XML VIDEO INFO FILES ON THE VIDEO WEBPAGES, THESE XML PAGES CAN BE 
# USED FOR EASY RETRIEVAL OF METADATA AND VIDEO FILES USING XML.ElementFromURL. 
# We use string.replace() to create a new url that adds /xml/video-folder/ to middle and changes the extension to .xml.  
########################################################################################################
XML_URL = url.replace('http://www.anywebsite.com/','http://www.anywebsite.com/xml/videos-page/').replace('.html','.xml')
####################################################################################################
# This pulls the metadata that is used for Plexit, suggested videos (to friends), the pre-play screen on iOS, Roku, Android and Plex/Web, 
# as well as if you get the "info" or "details" within the OSX/Windows/Linux desktop client.
# Afer you pull the data you should save it to preset variables.  These basic variables are title, summary, and thumb. 
# but some also include duration, and originally_available_at, content_rating, show, tags and index 

def MetadataObjectForURL(url):

Here you are using the ElementFromURL() API to parse or pull up all the data from the webpage.

See the Framework documentation API reference for all the choices for parsing data

page = HTML.ElementFromURL(url)

# Extract the data available on the page using xpath commands. 
# Think about what will access the metadata from this URL service to determine what info you want to extract here
# Below is a basic example that pulls the the title, description and thumb from the head of a html document that makes a request of this URL Service
title = page.xpath("//head//meta[@property='og:title']")[0].get('content')
description = page.xpath("//head//meta[@property='og:description']")[0].get('content')
thumb = page.xpath("//head//meta[@property='og:image']")[0].get('url')	
# This command returns the metadata values you extracted as a video clip object.  
# See the Framework documentation API reference for all the choices of objects to return metadata
return VideoClipObject(
	title = title,
	summary = description,
	thumb = thumb,
)

####################################################################################################

Here you will define the different types of videos available on this website and the values for their quality

You will need to search the source of the web pages that play a video to determine the type and location of videos offered thru the site

You can see if the name and location is available through an embed link, but you may have to look into the subpages for a web page

like javascript or style sheets to find this information. You will also need this information later when

writing the code for the PlayVideo function that pulls these specific video files from the webpage

def MediaObjectsForURL(url):
return [

First you are calling a MediaObject() for each type of video the website offers using the MediaObject API command

Most separate these types of videos by the resolution, for example a site may offer a high and low quality option for each video on its site

or they may offer an .flv format and an .mp4 format version of the video file. You can choose to only offer one type of video file

or give the user the option of choosing the type of video file they want to use if there are several different types and qualities.

	MediaObject(

Then within each MediaObject you define the values for that particular type of video

The options most used are video_codec, audio_codec, parts, container, video_resolution, audio_channels, and bitrate

See the Framework documentation API reference for a lists all possible attributes for MediaObject()

Audio Codecs are AAC and .MP3; Video Codecs are .H264; Container File Types are .MP4, .MKV, .MOV, and .AVI¶

And for audio channels stereo equals 2

I have found the best way to determine these attributes is to use VLC player and open the network stream URL of a few of the

videos available on the site. And then use the tools to view the media information esp. codec info

		video_codec = VideoCodec.H264,
		audio_codec = AudioCodec.MP3,
		video_resolution = '720',
		audio_channels = 2,
		container = 'f4v',

This section of the code allows you to peice together videos that come in 2 or more parts it also uses a callback function

that calls the PlayVideo function below. This done to separate the playing of the video from the call for the media object

that way it will only play the video if the user selects that particular video.

The code below is the basic format that all URL Serivces use and then they define PlayVideo if the video only has one part, it only needs one PartObject

Note that we are also sending a fmt variable to tell the PlayVideo function whether this is a high or low quality video. This is only

necessary when you have more than one choice for the video files available.

		parts = [PartObject(key=Callback(PlayVideo, url = url, fmt='hi')]
	)

Below we are doing the same as above, and defining a second MediaObject that is a low quality version of the video file that is available.

The fmt variable is sent to the PlayVideo function to help you determine which of the available video types to send back to the Media Object.

	MediaObject(
		video_codec = VideoCodec.H264,
		audio_codec = AudioCodec.MP3,
		video_resolution = '420',
		audio_channels = 2,
		container = 'f4v',
		parts = [PartObject(key=Callback(PlayVideo, url = url, fmt='lo')]


	)
]

####################################################################################################

Here we are defining the PlayVideo function we called in the callback function above. This function defines the pattern for

the location and naming scheme of the video so we can play the video file directly. You use HTML request, regular expressions,

and predefined variables to create the path or http address of the video associated with the html or xml page that was sent

to this service through the “URL” value. The programming here will vary greatly based on the location of the

video file related to your video pages. This is where you will be doing the majority to the programming.

It is best to refer to other services already created for examples of how to pull the video file.

First we define the function taking the the variables for the url entered into the service and the

fmt variable we established above in MediaObjects

@indirect
def PlayVideo(url, fmt):

Below I have included a basic example of how to program the PlayVideo function for pulling the video location on the web page

whose URL was sent to the service and only has ONE video on the page. The code pulls the raw data from the web page

using the URL and then uses a simple page search that returns an f4v video file url that is located in each html video page.

It uses both of the variables we established at the top of the service.

EXAMPLE:

page = HTTP.Request(url).content

video = RE_VIDEO_URL.search(page).group(‘video_url’) + “.f4v”

video = BASE_URL + video

The example I chose to use for this function uses the optional xml video information file to pull the high and low

quality version of a video from the url sent to this URL service.

It basically pulls the content from the URL, looks through the page to find a mention of the regex value I defined as a

global variable at the beginning of this document. Because the line of data I was using contained a parenthesis right before

the address, I had to get a little creative in pulling it and then replacing the ending single quote to properly pull

the xml address out of the page. The function then opens that xml video info page and uses xpath commands to extract the

video URL and returns those values in the form of a high and low quality video. Then it uses the fmt variable to determine

which of these videoss should be the value of video.

content = HTTP.Request(url).content
xmlurl = RE_XML_URL.search(content).group(0)
xmlurl = xmlurl.replace("'", '')
xmlurl = base_url + xmlurl
       
xml = XML.ElementFromURL(xmlurl)
if xml:
# Here we are extracting the video file addresses from the xml video file metadata
    videoHi = xml.xpath('//video/videoHi//text()')[0]
    videoLo = xml.xpath('//video/videoLo')[0].text
							
    videoHi = videoHi.replace('.f4v', '.mp4')
    videoLo = videoLo.replace('.f4v', '.mp4')

if fmt == 'hi':
    video = videoHi
else:
    video = videoLo

We then return the address or value of the video. This video URL is then sent back to the MediObject above with return Redirect(video)

The IndirectResponse() is a Plex Framework function that tells the client to only execute this function when the user hits play so there is no delays.

return IndirectResponse(VideoClipObject, key=video)
####################################################################################################
def TestURLs():

This is for testing the URL, so you are making sure the URL service is working.

YOU DO NOT NEED THIS IF YOU PUT A TEST URL IN YOUR SERVICEINFO.PLIST FILE. THIS IS AN ALTERNATIVE WAY IF YOU THINK THE VIDEO PAGES WILL CHANGE

ALOT AND YOU DO NOT THINK YOU CAN DETERMINE A SPECIFIC URL THAT WILL NOT CHANGE

This coding below is a basic example that allows you to make the test url more dynamic so the service will not fail just because the

test url no longer exists. Enter the main domain for urls that will be sent to this service in place of anydomain.com

test_urls = []
page = HTML.ElementFromURL('http://www.anydomain.com/')

# here in this link command you need to edit this xpath to reflect the id or class is used to represent 
# a video player container on pages for this url service and enter the correct domain name
for link in page.xpath("//a/span[@class='vid']/.."):
	if len(test_urls) < 3:
	url = link.get('href')
	url = "http://www.anydomain.com" + url

if url not in test_urls:
	test_urls.append(url)
else:
	break

return test_urls

See last post for alternative location

Here is the last template for the __init__.py file:

# MOST IMPORTANT NOTE: BEFORE WRITING A CHANNEL, THERE MUST ALREADY BE A URL SERVICE FOR THE VIDEOS ON THE WEBSITE
# YOU WANT TO CREATE A CHANNEL FOR OR YOU WILL HAVE TO WRITE A URL SERVICE BEFORE YOU CAN WRITE THE CHANNEL. TO
# SEE IF A URL SERVICE ALREADY EXISTS, CHECK THE SERVICES BUNDLE IN THE PLEX PLUGIN FOLDER

# IMPORTANT NOTE: PYTHON IS VERY SENSITIVE TO PROPER INDENTIONS.  IF YOUR CHANNEL HAS IMPROPER INDENTIONS IT WILL
# NOT BE RECOGNIZED BY PLEX. I RUN THE PROGRAM THROUGH A CHECK MODULE ON A LOCAL VERSION OF PYTHON I HAVE LOADED
# PRIOR TO ACCESSING IT THROUGH PLEX TO MAKE SURE THERE ARE NO INDENTION ERRORS.

# You will need to decide how you want to set up your channel. If you want to have just one page that list all 
# the videos or if you want to break these videos down into subsections for different types of videos, individual shows, season, etc
# It is easiest to determine this system based on the structure of the website you are accessing. 

# You can hard code these choice in or pull the data from a web page or JSON data file and put it in a for loop to 
# automate the process. I created a basic example in the form of functions below to show the most common methods of 
# parsing data from different types of websites. When you want to produce results to the screen and have subpage come up # when they click on those results, you usually will use a
# DirectoryObject and include the name of the next function that will create that subpage called in the key.
# The key callback section sends your data to the next function that you will use to produce your next subpage.  Usually
# you will pass the value of the url onto your next function, but there are many attributes that can be sent.  It is good 
# to pass the title as well so it shows up at the top of the screen. Refer to the Framework Documentation to see the full
# list

# You will need a good working knowledge of xpath the parse the data properly.  Good sources for information related to 
# xpath are:
# 'http://devblog.plexapp.com/2012/11/14/xpath-for-channels-the-good-the-bad-and-the-fugly/'
# 'http://forums.plexapp.com/index.php/topic/49086-xpath-coding/'

# Here is a good article about working with Chrome Development Tools: 
# 'http://devblog.plexapp.com/2012/09/27/using-chromes-built-in-debugger-for-channel-development/'

# And here are a few pages that give you some pointers ON figuring out the basics of creating a channel
# 'http://devblog.plexapp.com/2011/11/16/a-beginners-guide-to-v2-1/'
# 'http://forums.plexapp.com/index.php/topic/28084-plex-plugin-development-walkthrough/'

# The title of your channel should be unique and as explanatory as possible.  The preifx is used for the channel
# store and shows you where the channel is executed in the log files

TITLE    = 'Channel Title'
PREFIX   = '/video/channelname'

# The images below are the default graphic for your channel and should be saved or located in you Resources folder
# The art and icon should be a certain size for channel submission. The graphics should be good quality and not be blurry
# or pixelated. Icons must be 512x512 PNG files and be named, icon-default.png. The art must be 1280x720 JPG files and be
# named, art-default.jpg. The art shows up in the background of the PMC Client, so you want to make sure image you choose 
# is not too busy or distracting.  I tested out a few in PMC to figure out which one looked best.

ART      = 'art-default.jpg'
ICON     = 'icon-default.png'

# Below you would set any other global variables you may want to use in your programming. I tend to automatically create 
# a base url for the website I am accessing to add to any urls that are returned with just the folder path and not the 
# whole url and an http for urls that are returned to the channel without these at the beginning

WebsiteURL = 'http://www.anysite.com'
http = 'http:'
# This variable it to make an id below work as a url link
WebsiteEpURL = 'http://www.anysite.com/watch/'

# These are regex variables that will be used later to search a document for ids
# it seems to work well to just put "(.?)" between the any text that appears before and after the data you 
# want to return with your regex, where "(.?)" represents the data you are trying to pull
RE_LIST_ID = Regex('listId: "(.+?)", pagesConfig: ')
RE_CONTENT_ID = Regex('CONTENT_ID = "(.+?)";')


###################################################################################################
# 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. These setting below are pretty standard
# You first set up the containers and default for all possible objects.  You will probably mostly use Directory Objects
# and Videos Objects. But many of the other objects give you added entry fields you may want to use and set default thumb
# and art for. For a full list of objects and their parameters, refer to the Framework Documentation.
  
def Start():

# There are  few commands you may see appear in this section that are no longer needed.  Below is an explanation of them
# provided from a very helpful channel designer who was nice enough to explain their purpose to me:
#    Plugin.AddViewGroup("List", viewMode="List", mediaType="items")
# This a left-over from when plugins had more control over how the contents were displayed. ViewGroups were defined to 
# tell the client how to display the contents of the directory. Now, most (if not all) clients have a fairly rigid model
# for what types of content get display in which way. Generally, it is best to remove it from a plugins when since it
# gets ignored anyways. 
#
#    HTTP.CacheTime = CACHE_1HOUR
# This setting a global cache time for all HTTP requests made by the plugin. This over-rides the framework's default 
# cache period which,
# I don't remember off the top of my head. It is entirely optional, but if you're going to use it, the idea is to pick a 
# cache-time that is reasonable. Ie. store data for a long enough time that you can realistically reduce the load on
# external servers as well as speed up the load-time for HTTP-requests, but not so long that changes/additions are not
# caught in a reasonable time frame. IMO, unless you specifically want/need a specific cache-length, I would remove that
# line and allow the framework to manage the cache with its default settings.
#
#   HTTP.Headers['User-Agent'] = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10.8; rv:18.0) Gecko/20100101 Firefox/18.0'
# This assigning a specific User-Agent header for all HTTP requests made by the plugin. Generally speaking, each time a
# plugin is started, a user-agent is randomly selected from a list to be used for all HTTP requests. In some cases, a
# plugin will perform better using a specific user-agent instead of a randomly assigned one. For example, some websites
# return different data for Safari on an iPad, then what they return for Chrome or Firefox on a PC. Again, if you don't
# have a specific need to set a specific user-agent, I would remove that code from your channel.

# You set up the default attributes for all you object containers and objects.  You will probably mostly use Directory
# Objects and Videos Objects but many of the other objects give you added entry fields you may want to use.  For a full 
# list of objects and their parameters, refer to the Framework Documentation.

# Important Note: (R stands for Resources folder) to tell the channel where these images are located.

  ObjectContainer.title1 = TITLE
  ObjectContainer.art = R(ART)

  DirectoryObject.thumb = R(ICON)
  DirectoryObject.art = R(ART)
  EpisodeObject.thumb = R(ICON)
  EpisodeObject.art = R(ART)
  VideoClipObject.thumb = R(ICON)
  VideoClipObject.art = R(ART)

###################################################################################################
# This tells Plex how to list you in the available channels and what type of channels this is 
@handler(PREFIX, TITLE, art=ART, thumb=ICON)

# This function is the first and main menu for you channel.

 
def MainMenu():

# You have to open an object container to produce the icons you want to appear on this page. 
  oc = ObjectContainer()

# Below is a basic example of a list of three object containers that returns an icon to the screen with a title.
# In this version we just hardcoded in the sections we would like to break the videos into based on the types of functions
# that will be described below. I am using a function from below that pulls the thumb from the head of the page
  oc.add(DirectoryObject(key=Callback(ShowRSS, title="RSS Shows", url='http://www.webpage1.com'), title="RSS Shows", thumb=GetThumb(url=''http://www.webpage1.com)))
  oc.add(DirectoryObject(key=Callback(ShowHTML, title="HTML Page Source Shows", url='http://www.webpage1.com'), title="HTML Page Source Shows", thumb=GetThumb(url=''http://www.webpage2.com)))
  oc.add(DirectoryObject(key=Callback(ShowJSON, title="JSON Source Shows", url='http://www.webpage1.com'), title="JSON Source Shows", thumb=GetThumb(url=''http://www.webpage3.com)))

  return oc

#########################################################################################################################
# the command below is helpful when looking at logs to determine which function is being executed
# ALSO SAYS IT HELPS THE DESIGNER CREATE A "REST-like API" BUT I DO NOT KNOW WHAT EXACTLY THAT MEANS. I AM THINKING IT
# ALLOWS ACCESS DIRECTLY TO THE FUNCTION FROM OUTSIDE OF THE CODE
@route(PREFIX + '/showrss')

# This function shows a basic loop to go through an xml rss feed and pull the data for all the videos or shows listed
# there
def VideoRSS(title):

# define an object container and pass the title in from the function above
  oc = ObjectContainer(title2=title)
  
# This is the data parsing API to pull elements from an RSS feed. 
  xml = RSS.FeedFromURL(url)
# enter a for loop to return an object for every entry in the feed
  for item in xml.entries:
# Pull the data that is available in the form of link, title, date and description
    url = item.link
    title = item.title
	# The date is not always in the correct format so it is always best to return use Datetime.ParseDate to make sure it is correct 
    date = Datetime.ParseDate(item.date)
    desc = item.description
	# Return an object for each item you loop through.  This produces an icon or video name for each entry to the screen.
	# It is important to ensure you are reading these attributes in to the correct name.  See the Framework Documentation 
	# for a complete list of objects and the attributes that can be returned with each.
    oc.add(VideoClipObject(
      url = url, 
      title = title, 
      summary = description, 
	  # Resource.ContentsOfURLWithFallback test the icon to make sure it is valid or returns the fallback if not
      thumb = Resource.ContentsOfURLWithFallback(thumb, fallback=R(ICON)), 
      originally_available_at = date
      ))
	  
# This code below is helpful to show when a source is empty
  if len(oc) < 1:
    Log ('still no value for objects')
    return ObjectContainer(header="Empty", message="Unable to display videos for this show right now.")      

  return oc

#################################################################################################################
# This function is an example of parsing data from a html page
@route(PREFIX + '/showhtml')
def ShowHTML(title):


# The parsing API is followed by .xpath(‘//PARENTELEMENT’): where PARENTELEMENT is the parent element within the XML document for  
# which you want to return data values to the variables for the videos. For example each video in your RSS feed may be contained 
# within an Item element tag that has element tags for title, thumbnail, description, etc.

# Open the object container
  oc = ObjectContainer(title2=title)

# The HTML.ElementFromURL create a tree structure of all the elements, attributes and data in your html documents called a DOM Tree. 
# This tree structure is necessary to pull data with xpath commands
  html = HTML.ElementFromURL(url)

# We start a for loop for all the items we want to pull off of the page.  You will need to search the source document and play around with
# and xpath checker to find the right structure to get you to a point of returning the full list of items you want to pull the data from.
  for video in html.xpath('//div/div/div/ul/li/ul/li'):
  # need to check if urls need additions and if image needs any additions to address
    url = video.xpath('./div/a//@href')[0]
	# here we are adding the sites domain name with a global variable we set at the start of the channel
    url = WebsiteURL + url
    thumb = video.xpath('./div/a/img//@style')[0]
	# A value that is returned may have extra code around it that needs to be removed. Here we use the replace string method to fix the thumb address
    thumb = thumb.replace("background-image:url('", '').replace("');", '')
    title = video.xpath('./div/div/p/a//text()')[0]
				
# SEE THE LINKS AT THE TOP FOR MORE DETAILED INFO ON XPATH 
# We are using xpath to get of all the values for each element with the parent element returned by the variable video 
# the syntax is: video.xpath (‘./CHILDELEMENT’)[FIRSTVALUE].FORMAT or video.xpath (‘./CHILDELEMENT/FORMAT’)[FIRSTVALUE]
# where CHILDELEMENT is the xpath commands that gets us to the location of the data in the child element ( ex. title, url, date),
# FIRSTVALUE is the first occurrence of the child element. Usually you want to get all occurences of the child element for 
# each parent element so we give this a value of [0] (Python starts the count at zero), and FORMAT defines whether we want 
# to return the data contained in the element as text or as an attribute.  The syntax is are .text or .get(‘ATTRIBUTE’) 
# or //text() and //@ATTRIBUTE when attached to the child element xpath where ATTRIBUTE is the name of the attribute 
# you want to return ex. //@href or .get(href) to return the anchor attribute href="www.domainname.com/file.htm"

# this is where the values for each child element we specified are added to the channel as video clip objects the naming scheme for
# the values passed here are listed in the Framework Documentation for attributes of VideoClipObjects

    oc.add(VideoClipObject(
      url = url, 
      title = title, 
      thumb = Resource.ContentsOfURLWithFallback(thumb, fallback=R(ICON))))
      
# This code below is helpful to show when a source is empty
  if len(oc) < 1:
    Log ('still no value for objects')
    return ObjectContainer(header="Empty", message="Unable to display videos for this show right now.")      
	  
# it will loop through and return the values for all items in the page 
  return oc



#######################################################################################################################
@route(PREFIX + '/showjson')
def ShowJSON(title):

# This function shows you how to pull data from a json data file.  First you need to determine if a json file is available for 
# the page you are trying to access.  The best way is to open the show with Chrome and use the Developers Tools.  From there you
# can see all the files that are being opened by the page you are accessing.  (Sometimes you have to hit refresh button for those
# sources to show up). I usually list them by type so the applications are listed first. It takes a little time and research to
# figure out what you are looking at and when you find a json file.  You may also have to play with the parameters like ids and
# number of results to get the list you want.

# Then you have to figure out how to ensure that you are going to use the proper url address for the json file everytime

# Below is a basic example of a json data pull

  oc = ObjectContainer(title2 = title)
  # Here we call the function we created below to find an id
  show_id = JSONID(url)
  # Below is a code I found within the hulu website to pull JSON data for their shows. So I broke the data up to add the correct show id
  # you could also make global variables for the beginning and end of the JSON address and use those to create the full JSON URl
  json_url = 'http://www.hulu.com/mozart/v1.h2o/shows/' + show_id +'/episodes?free_only=0&show_id=' + show_id + '&sort=seasons_and_release&video_type=episode&items_per_page=32&access_token=Jk0-QLbtXxtSMOV4TUnwSXqhhDM%3DaoY_yiNlac0ed1d501bee91624b25159a0c57b05d5f34fa3dc981da5c5e9169daf661ffb043b2805717849b7bdeb8e01baccc43f'

  # This is the API used to parse data from JSON
  videos = JSON.ObjectFromURL(json_url)
  # Enter a for loop to run through all data sets of the type data
  for video in videos['data']:
  # Below this is the format for pulling data from the JSON data from the structure of the Hulu JSON file which requires 
  # going a little deeper into the structure of the file to pull the data.  Often you will see JSON parses that just go in one level
  # Ex. url = video['url']. You may have to play with the code to find the right combination for your JSON data
    url = video['video']['id']
	# this particular site does not provide a proper url link, so we are using a variable to make the id work
    url = WebsiteEpURL + str(url)
    title = video['video']['title']
    thumb = video['video']['thumbnail_url']
    duration = video['video']['duration']
	# the duration must be in milliseconds so at the least you will need to usually multiply it by 1000
	# duration = int(duration) * 1000
	# The code below will change it from a MM:SS format to milliseconds 
	# duration = Datetime.MillisecondsFromString(duration)
    date = video['video']['available_at']
    date = Datetime.ParseDate(date)
    summary = video['video']['description']

    oc.add(EpisodeObject(
      url = url, 
      title = title,
      thumb = Resource.ContentsOfURLWithFallback(thumb, fallback=R(ICON)),
      summary = summary,
	  # (this code is not accurate for Hulu and just added to show as an example)
      # duration = duration,
      originally_available_at = date))

  if len(oc) < 1:
    Log ('still no value for objects')
    return ObjectContainer(header="Empty", message="Unable to display videos for this show right now.")      

  return oc

###############################################################################################################
# This function pulls the ID from each show page for it to be entered into the JSON data url
# The example below pulls the id from one of two places in the source of the web page 
# it first looks for a list id and then looks for a content id based on global regex variables set at the top
# of this program
@route(PREFIX + '/jsonid')
def JSONID(url):

  ID = ''
  content = HTTP.Request(url).content
  try:
    ID = RE_LIST_ID.search(content).group(1)
  except:
    ID = RE_CONTENT_ID.search(content).group(1)
  return ID

#############################################################################################################################
# This is a function to pull the thumb from a the head of a page
@route(PREFIX + '/getthumb')
def GetThumb(url):
  page = HTML.ElementFromURL(url)
  try:
    thumb = page.xpath("//head//meta[@property='og:image']//@content")[0]
    if not thumb.startswith('http://'):
      thumb = http + thumb
  except:
    thumb = R(ICON)
  return thumb

###############################################################################################################
# OTHER THINGS TO LOOK AT WHEN DESIGNING YOUR CHANNEL
###############################################################################################################
#
# DEBUG LOG MESSAGES
# ANYWHERE IN YOUR CODE THAT YOU WANT TO PUT A DEBUG CODE THAT RETURNS A LINE OF TEXT OR A VARIABLE
# YOU WOULD USE THE LOG COMMAND.  THE PROPER FORMAT IS BELOW:
# To just include a statement, add Log('I am here') To return the value of a variable VAR in the log statement, 
# you would add Log('the value of VAR is %s' %VAR)
#
# PYTHON STRING METHODS
# CHECK OUT THE PYTHON STRING METHODS. THESE GIVE YOU SEVERAL WAYS TO MANIPULATE STRINGS THAT CAN BE HELPFUL IN YOUR CHANNEL CODE
# THIS IS A GOOD PAGE WITH BASIC TUTORIALS AND EXPLANATIONS FOR STRING METHODS: 'http://www.tutorialspoint.com/python/python_strings.htm'
#
# XML XPATH CHECKER
# TRADITIONAL XPATH CHECKERS DO NOT WORK ON XML PAGES. HERE IS A LINK TO AN XML XPATH CHECKING PROGRAM THAT IS VERY HELPFUL
# 'http://chris.photobooks.com/xml/default.htm'
# 
# TRY/EXCEPT 
# TRY IS GOOD FOR SITUATIONS WHERE YOUR XPATH COMMANDS MAY OR MAY NOT WORK. IF YOUR XPATH IS OUT OF RANGE YOU WILL GET ERRORS IN YOUR
# CODE.  USING TRY ALLOWS YOU TO TRY THE XPATH AND IF IT DOESN'T WORK, PUT ALTERNATIVE CODE UNDER EXCEPT AND YOU WILL NOT GET ERRORS
# IN YOUR CODE
#
# DICT[] 
# DICT[] IS PART OF THE PLEX FRAMEWORK THAT ALLOWS YOU TO SAVE DATA IN A GLOBAL VARIABLE THAT IS RETAINED WHEN YOU EXIT THE PLUGIN
# SO YOU CAN PULL IT UP IN MULTIPLE FUNCTIONS WITHOUT PASSING THE VARIABLES FROM FUNCTION TO FUNCTION. AND IT CAN BE ACCESSED AND USED
# OVER MULTIPLE SESSIONS

Just wanted to say thanks for these.  Helped immensely!  :D

I did want to add this method for writing the MediaObjectsforURL() function in the URL service that was shown to me. It makes it much easier and more compact when you have multiple parts and resolutions for your videos. I was going to add it to the template above, but figured with all the explanation in the template, it was better to keep that example simple and add this as a separate post.

Here is the alternative method for multiple parts and resolutions/bitrates:

####################################################################################################
def MediaObjectsForURL(url):

    if 'playlist' in url or 'full-episode' in url:
        num_parts = 6
    else:
        num_parts = 1

    return [
        MediaObject(
            parts = [
                PartObject(
                    key=Callback(PlayVideo, url=url, bitrate=str(bitrate), index=i)
                    ) for i in range(num_parts)
            ],
            bitrate = bitrate,
            container = Container.MP4,
            video_resolution = resolution,
            video_codec = VideoCodec.H264,
            audio_codec = AudioCodec.AAC,
            audio_channels = 2,
            optimized_for_streaming = True
        ) for bitrate,resolution in [(3500, '720'), (2200, '540'), (1700, '432'), (1200, '360')]
    ]

Just wanted to say thanks for these.  Helped immensely!  :D

Glad it was helpful.

I had seen in another post that someone had asked if there was any documentation that showed all the possible keys and values you could use for the Info.plist file. Since I already had a basic version of that here in the Info.plist template, I decided I would go through some of the existing channels and try to compile a full list.

After the talking to the Dev guys who were able to fill in the blanks with extra explanations, and values, as well as which keys were no longer used, I was able to make the template a comprehensive list of all the possible keys and values for a Plex channel Info.plist.

I have updated the Info.plist template above to include all of these possible keys, values and explanations. Hope it helps.

I tried to do an small edit to the ServiceCode.pys, and It messed up the code. I had to alter the code slightly for it to accept it again, but it is back. 

In order to make it work, I removed the section about testing URLs. I will add that info below as a separate post.

If you are interested, the original version of the template is available at Pastebin:

ServiceCode.pys Template:

 http://pastebin.com/0FQt3LV5.

TESTING YOUR URL SERVICE

To test your url service, the best way is to use 

http://localhost:32400/system/services/url/lookup?url=

This should be followed by an http address for a page within the website the URL service is used for that contains a video. Also, you must first use a URL encoder like http://meyerweb.com/eric/tools/dencoder/ to put that video page URL in the proper format. 

UPDATE: When using the latest version of PMS that includes Plex Home, you must add &X-Plex-Token= to the end of the encoded URL. To find your token, see these instructions https://support.plex.tv/hc/en-us/articles/204059436-Finding-your-account-token-X-Plex-Token. So it would be:

http://localhost:32400/system/services/url/lookup?url=&X-Plex-Token=

Once you paste the Plex test address into the address bar of your browser, an XML page will show you the metadata for this video along with the available media with keys for the video parts. If this info does not come up, check the system log (come.plexapp.system.log) for errors.

To see the actual video file URL and any errors that may result from playing this video listed in a key, you will then cut the key data from that XML page you pulled up above and paste it in the address bar of your browser following the base Plex address of 'http://localhost:32400'.

The resulting page should take you either directly to a page that plays the video file if you used Redirect in your PlayVideo function or if you use @indirect in your PlayVideo function, it will take you to an XML document that provides the exact details of that video file based on how your media is returned in your ServiceCode.pys. If it does not open the video or the proper XML file with the video information, the resulting XML page should show you any errors within the code including which lines of your code caused the error.

Example:

To test the URL service for the L Studios URL service, I will use the video available at  http://www.lstudio.com/web-therapy/season-4-morals-to-the-max.html.  The full format of the Plex test URL for the video page above is http://localhost:32400/system/services/url/lookup?url=http%3A%2F%2Fwww.lstudio.com%2Fweb-therapy%2Fseason-4-morals-to-the-max.html.

We will use the first key for the first media part available through the Plex test URL above that is the 720p version of the video. The value of that key is:

/:/plugins/com.plexapp.system/serviceFunction/url/com.plexapp.plugins.lstudio/L%20Studio/PlayVideo?args=Y2VyZWFsMQoxCnR1cGxlCjAKcjAK&kwargs=Y2VyZWFsMQoxCmRpY3QKMgpzNjYKaHR0cDovL3d3dy5sc3R1ZGlvLmNvbS93ZWItdGhlcmFweS9zZWFzb24tNC1tb3JhbHMtdG8tdGhlLW1heC5odG1sczMKdXJsczIKaGlzMwpmbXRyMAo_&indirect=1&mediaInfo=%7B%22audio_channels%22%3A%202%2C%20%22protocol%22%3A%20null%2C%20%22optimized_for_streaming%22%3A%20null%2C%20%22video_frame_rate%22%3A%20null%2C%20%22duration%22%3A%20null%2C%20%22height%22%3A%20720%2C%20%22width%22%3A%201280%2C%20%22container%22%3A%20%22mp4%22%2C%20%22audio_codec%22%3A%20%22aac%22%2C%20%22aspect_ratio%22%3A%20null%2C%20%22video_codec%22%3A%20%22h264%22%2C%20%22video_resolution%22%3A%20%22720%22%2C%20%22bitrate%22%3A%20null%7D

If we add that key value to the local host address, the resulting URL would be http://localhost:32400/:/plugins/com.plexapp.system/serviceFunction/url/com.plexapp.plugins.lstudio/L%20Studio/PlayVideo?args=Y2VyZWFsMQoxCnR1cGxlCjAKcjAK&kwargs=Y2VyZWFsMQoxCmRpY3QKMgpzNjYKaHR0cDovL3d3dy5sc3R1ZGlvLmNvbS93ZWItdGhlcmFweS9zZWFzb24tNC1tb3JhbHMtdG8tdGhlLW1heC5odG1sczMKdXJsczIKaGlzMwpmbXRyMAo_&indirect=1&mediaInfo=%7B%22audio_channels%22%3A%202%2C%20%22protocol%22%3A%20null%2C%20%22optimized_for_streaming%22%3A%20null%2C%20%22video_frame_rate%22%3A%20null%2C%20%22duration%22%3A%20null%2C%20%22height%22%3A%20720%2C%20%22width%22%3A%201280%2C%20%22container%22%3A%20%22mp4%22%2C%20%22audio_codec%22%3A%20%22aac%22%2C%20%22aspect_ratio%22%3A%20null%2C%20%22video_codec%22%3A%20%22h264%22%2C%20%22video_resolution%22%3A%20%22720%22%2C%20%22bitrate%22%3A%20null%7D.

Since @indirect is used in the PlayVideo function of this example URL service, when you put that URL into the address bar of your web browser, it takes you to an XML page that has the final and direct info about the video associated with that page that is of type mp4 and a quality of 720p, since that was the version of the video we chose from the keys.

IMPORTANT NOTE:

I have recently been informed that you should always use the @indirect method in your code. Redirect can cause errors with some clients if the video file has to be transcoded, so it is always the best practice to use @indirect.

The information on this page includes the direct video file URL for the media (http://videos.lstudio.com/high/WT_S4_Ep1_HI.mp4) that is associated with the video page we chose in the beginning of this example.

Here is one more tip that may be useful for beginners writing a URL service. It is a very simple tip, but it was one of those that took me a while to stumble across.

I steal other peoples code all the time. If others, who are much smarter than me, have already found a better way to do thing, I am going to take advantage of that knowledge. Also, you get exposed to new methods and learn some great new tricks along the way, so hopefully they make you a little smarter.

One thing I have found that is helpful in finding URL services that may be similar to your project is to search the Plex-Inc Services.bundle on Github for similar code.

If you pull up that repository (https://github.com/plexinc-plugins/Services.bundle), you can enter search terms at the top to look through all the code in that repository.  When I am looking at a new project, I try to look for something in the webpage for those videos that may be a unique term, I can use to compare to the existing service set in Services.bundle to see if any are similar. The Github search will return all the results for where that term may show up in an existing URL service in the Plex-Inc Services.bundle.

So it is an easy way to figure out which URL services may be helpful examples for you.

One other quick note that Mike recently pointed out on another post.

It is always best to use @indirect in your URL service PlayVideo function. Redirect can cause issues with certain clients if the file type is not native to that device, so using @indirect with IndirectResponse is the preferred and safe method.

UPDATE:

It is best to use @indirect unless the video has parts. Video in parts give errors when you use indirect, so if your videos have multiple parts, you will need to use Redirect instead.

Great work @shopgirl284!

I did want to add this method for writing the MediaObjectsforURL() function in the URL service that was shown to me. It makes it much easier and more compact when you have multiple parts and resolutions for your videos. I was going to add it to the template above, but figured with all the explanation in the template, it was better to keep that example simple and add this as a separate post.

Here is the alternative method for multiple parts and resolutions/bitrates:

####################################################################################################
def MediaObjectsForURL(url):
if 'playlist' in url or 'full-episode' in url:
    num_parts = 6
else:
    num_parts = 1

return [
    MediaObject(
        parts = [
            PartObject(
                key=Callback(PlayVideo, url=url, bitrate=str(bitrate), index=i)
                ) for i in range(num_parts)
        ],
        bitrate = bitrate,
        container = Container.MP4,
        video_resolution = resolution,
        video_codec = VideoCodec.H264,
        audio_codec = AudioCodec.AAC,
        audio_channels = 2,
        optimized_for_streaming = True
    ) for bitrate,resolution in [(3500, '720'), (2200, '540'), (1700, '432'), (1200, '360')]
]

If I understand correctly, this is for when all the parts are on 1 single page, right? What to do if it's on the following "part 2" (linked on that same page) page?

If I understand correctly, this is for when all the parts are on 1 single page, right? What to do if it's on the following "part 2" (linked on that same page) page?

Yes. A URL service only handles one URL at a time and is set up to only construct a media player and find the media file or files that play on that unique URL that is sent to the service.

In the situation you are describing, you would access each URL individually as Part 1, Part 2, etc.

I am not aware of any situation where someone has used a URL service to process multiple URLs or related URLs as parts of one MediaObject and I think that would break the rules of how a URL service is supposed to work, but you would need to ask this as a separate post in the Channel Development forum to know for sure. It may be that in your situation you would not want to create a separate URL service to handle these video parts, but would process those videos within your channel code.

Thanks for your answer. I wanted to delete my post and repost it in the developer channel about 2 minutes after I had posted. Unfortunately there is no delete button.

Many, many, many thanks shopgirl284! Yet another reason plex kicks much ars…the great plex community.

thank you very much