Added readme; other changes
This commit is contained in:
parent
91a7d572cf
commit
978baa4cad
|
|
@ -0,0 +1,8 @@
|
||||||
|
Simple example plugin for Kodi mediacenter
|
||||||
|
===
|
||||||
|
|
||||||
|
This is a simple yet fully functional example of a video plugin for [Kodi](www.kodi.tv) mediacenter.
|
||||||
|
|
||||||
|
**tests/** folder includes a simple example for partial unit testing of the plugin code.
|
||||||
|
|
||||||
|
The plugin uses free sample videos are provided by [www.vidsplay.com](www.vidsplay.com).
|
||||||
|
|
@ -12,5 +12,6 @@ provider-name="Roman_V_M">
|
||||||
<extension point="xbmc.addon.metadata">
|
<extension point="xbmc.addon.metadata">
|
||||||
<summary lang="en">Example Kodi Video Plugin</summary>
|
<summary lang="en">Example Kodi Video Plugin</summary>
|
||||||
<description lang="en">An example video plugin for Kodi mediacenter.</description>
|
<description lang="en">An example video plugin for Kodi mediacenter.</description>
|
||||||
|
<disclaimer lang="en">Free sample videos are provided by www.vidsplay.com.</disclaimer>
|
||||||
</extension>
|
</extension>
|
||||||
</addon>
|
</addon>
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@
|
||||||
# License: GPL v.3 https://www.gnu.org/copyleft/gpl.html
|
# License: GPL v.3 https://www.gnu.org/copyleft/gpl.html
|
||||||
|
|
||||||
import sys
|
import sys
|
||||||
import urlparse
|
from urlparse import parse_qs
|
||||||
import xbmcgui
|
import xbmcgui
|
||||||
import xbmcplugin
|
import xbmcplugin
|
||||||
|
|
||||||
|
|
@ -18,40 +18,36 @@ __handle__ = int(sys.argv[1])
|
||||||
# Here we use a fixed set of properties simply for demonstrating purposes
|
# Here we use a fixed set of properties simply for demonstrating purposes
|
||||||
# In a "real life" plugin you will need to get info and links to video files/streams
|
# In a "real life" plugin you will need to get info and links to video files/streams
|
||||||
# from some web-site or online service.
|
# from some web-site or online service.
|
||||||
VIDEOS = {'Animals': [('Crab', 'http://www.vidsplay.com/vids/crab.jpg',
|
VIDEOS = {'Animals': [{'name': 'Crab',
|
||||||
'http://www.vidsplay.com/vids/crab.mp4'),
|
'thumb': 'http://www.vidsplay.com/vids/crab.jpg',
|
||||||
('Alligator', 'http://www.vidsplay.com/vids/alligator.jpg',
|
'video': 'http://www.vidsplay.com/vids/crab.mp4'},
|
||||||
'http://www.vidsplay.com/vids/alligator.mp4'),
|
{'name': 'Alligator',
|
||||||
('Turtle', 'http://www.vidsplay.com/vids/turtle.jpg',
|
'thumb': 'http://www.vidsplay.com/vids/alligator.jpg',
|
||||||
'http://www.vidsplay.com/vids/turtle.mp4')],
|
'video': 'http://www.vidsplay.com/vids/alligator.mp4'},
|
||||||
'Cars': [('Postal Truck', 'http://www.vidsplay.com/vids/us_postal.jpg',
|
{'name': 'Turtle',
|
||||||
'http://www.vidsplay.com/vids/us_postal.mp4'),
|
'thumb': 'http://www.vidsplay.com/vids/turtle.jpg',
|
||||||
('Traffic', 'http://www.vidsplay.com/vids/traffic1.jpg',
|
'video': 'http://www.vidsplay.com/vids/turtle.mp4'}
|
||||||
'http://www.vidsplay.com/vids/traffic1.avi'),
|
],
|
||||||
('Traffic Arrows', 'http://www.vidsplay.com/vids/traffic_arrows.jpg',
|
'Cars': [{'name': 'Postal Truck',
|
||||||
'http://www.vidsplay.com/vids/traffic_arrows.mp4')],
|
'thumb': 'http://www.vidsplay.com/vids/us_postal.jpg',
|
||||||
'Food': [('Chicken', 'http://www.vidsplay.com/vids/chicken.jpg',
|
'video': 'http://www.vidsplay.com/vids/us_postal.mp4'},
|
||||||
'http://www.vidsplay.com/vids/bbqchicken.mp4'),
|
{'name': 'Traffic',
|
||||||
('Hamburger', 'http://www.vidsplay.com/vids/hamburger.jpg',
|
'thumb': 'http://www.vidsplay.com/vids/traffic1.jpg',
|
||||||
'http://www.vidsplay.com/vids/hamburger.mp4'),
|
'video': 'http://www.vidsplay.com/vids/traffic1.avi'},
|
||||||
('Pizza', 'http://www.vidsplay.com/vids/pizza.jpg',
|
{'name': 'Traffic Arrows',
|
||||||
'http://www.vidsplay.com/vids/pizza.mp4')]}
|
'thumb': 'http://www.vidsplay.com/vids/traffic_arrows.jpg',
|
||||||
|
'video': 'http://www.vidsplay.com/vids/traffic_arrows.mp4'}
|
||||||
|
],
|
||||||
def get_params():
|
'Food': [{'name': 'Chicken',
|
||||||
"""
|
'thumb': 'http://www.vidsplay.com/vids/chicken.jpg',
|
||||||
Parse parameters string received as sys.argv[2] list item.
|
'video': 'http://www.vidsplay.com/vids/bbqchicken.mp4'},
|
||||||
:return: list
|
{'name': 'Hamburger',
|
||||||
"""
|
'thumb': 'http://www.vidsplay.com/vids/hamburger.jpg',
|
||||||
# Remove the starting '?' character from the paramstring.
|
'video': 'http://www.vidsplay.com/vids/hamburger.mp4'},
|
||||||
paramstring = sys.argv[2].replace('?', '')
|
{'name': 'Pizza',
|
||||||
if paramstring:
|
'thumb': 'http://www.vidsplay.com/vids/pizza.jpg',
|
||||||
# if a paramstring present, parse it to the list of tuples (parameter, value)
|
'video': 'http://www.vidsplay.com/vids/pizza.mp4'}
|
||||||
params = urlparse.parse_qsl(paramstring)
|
]}
|
||||||
else:
|
|
||||||
# Return an empty stub list if there's no paramstring passed to the plugin.
|
|
||||||
params = [('',)]
|
|
||||||
return params
|
|
||||||
|
|
||||||
|
|
||||||
def get_categories():
|
def get_categories():
|
||||||
|
|
@ -86,13 +82,13 @@ def list_categories():
|
||||||
# Iterate through categories
|
# Iterate through categories
|
||||||
for category in categories:
|
for category in categories:
|
||||||
# Create a list item with a text label and a thumbnail image.
|
# Create a list item with a text label and a thumbnail image.
|
||||||
list_item = xbmcgui.ListItem(label=category, thumbnailImage=VIDEOS[category][0][1])
|
list_item = xbmcgui.ListItem(label=category, thumbnailImage=VIDEOS[category][0]['thumb'])
|
||||||
# Set a fanart image for the list item.
|
# Set a fanart image for the list item.
|
||||||
# Here we use the same image as the thumbnail for simplicity's sake.
|
# Here we use the same image as the thumbnail for simplicity's sake.
|
||||||
list_item.setProperty('fanart_image', VIDEOS[category][0][1])
|
list_item.setProperty('fanart_image', VIDEOS[category][0]['thumb'])
|
||||||
# Create a URL for the plugin recursive callback.
|
# Create a URL for the plugin recursive callback.
|
||||||
# Example: plugin://plugin.video.example/?category=Animals
|
# Example: plugin://plugin.video.example/?action=listing&category=Animals
|
||||||
url = '{0}?category={1}'.format(__url__, category)
|
url = '{0}?action=listing&category={1}'.format(__url__, category)
|
||||||
# Add the list item to a virtual Kodi folder.
|
# Add the list item to a virtual Kodi folder.
|
||||||
# isFolder=True means that this item opens a sub-list of lower level items.
|
# isFolder=True means that this item opens a sub-list of lower level items.
|
||||||
xbmcplugin.addDirectoryItem(__handle__, url, list_item, isFolder=True)
|
xbmcplugin.addDirectoryItem(__handle__, url, list_item, isFolder=True)
|
||||||
|
|
@ -109,20 +105,20 @@ def list_videos(category):
|
||||||
:return: None
|
:return: None
|
||||||
"""
|
"""
|
||||||
# Get the list of videos in the category.
|
# Get the list of videos in the category.
|
||||||
videos = VIDEOS[category]
|
videos = get_videos(category)
|
||||||
# Iterate through videos.
|
# Iterate through videos.
|
||||||
for video in videos:
|
for video in videos:
|
||||||
# Create a list item with a text label and a thumbnail image.
|
# Create a list item with a text label and a thumbnail image.
|
||||||
list_item = xbmcgui.ListItem(label=video[0], thumbnailImage=video[1])
|
list_item = xbmcgui.ListItem(label=video['name'], thumbnailImage=video['thumb'])
|
||||||
# Set a fanart image for the list item.
|
# Set a fanart image for the list item.
|
||||||
# Here we use the same image as the thumbnail for simplicity's sake.
|
# Here we use the same image as the thumbnail for simplicity's sake.
|
||||||
list_item.setProperty('fanart_image', video[1])
|
list_item.setProperty('fanart_image', video['thumb'])
|
||||||
# Set 'IsPlayable' property to 'true'.
|
# Set 'IsPlayable' property to 'true'.
|
||||||
# This is mandatory for playable items!
|
# This is mandatory for playable items!
|
||||||
list_item.setProperty('IsPlayable', 'true')
|
list_item.setProperty('IsPlayable', 'true')
|
||||||
# Create a URL for the plugin recursive callback.
|
# Create a URL for the plugin recursive callback.
|
||||||
# Example: plugin://plugin.video.example/?play=http://www.vidsplay.com/vids/crab.mp4
|
# Example: plugin://plugin.video.example/?action=play&video=http://www.vidsplay.com/vids/crab.mp4
|
||||||
url = '{0}?play={1}'.format(__url__, video[2])
|
url = '{0}?action=play&video={1}'.format(__url__, video['video'])
|
||||||
# Add the list item to a virtual Kodi folder.
|
# Add the list item to a virtual Kodi folder.
|
||||||
# isFolder=False means that this item won't open any sub-list.
|
# isFolder=False means that this item won't open any sub-list.
|
||||||
xbmcplugin.addDirectoryItem(__handle__, url, list_item, isFolder=False)
|
xbmcplugin.addDirectoryItem(__handle__, url, list_item, isFolder=False)
|
||||||
|
|
@ -142,17 +138,31 @@ def play_video(path):
|
||||||
xbmcplugin.setResolvedUrl(__handle__, True, listitem=play_item)
|
xbmcplugin.setResolvedUrl(__handle__, True, listitem=play_item)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
def router(paramstring):
|
||||||
# Get parameters
|
"""
|
||||||
params = get_params()
|
Router function that calls other functions
|
||||||
|
depending on the provided paramstring
|
||||||
|
:param paramstring:
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
|
# Parse a URL-encoded paramstring to the dictionary of
|
||||||
|
# {<parameter>: [<list of values>]} elements
|
||||||
|
params = parse_qs(paramstring)
|
||||||
# Check the parameters passed to the plugin
|
# Check the parameters passed to the plugin
|
||||||
if params[0][0] == 'category':
|
if params:
|
||||||
# Display the list of videos in a given category.
|
if params['action'][0] == 'listing':
|
||||||
list_videos(params[0][1])
|
# Display the list of videos in a provided category.
|
||||||
elif params[0][0] == 'play':
|
list_videos(params['category'][0])
|
||||||
# Play a video from a given URL.
|
elif params['action'][0] == 'play':
|
||||||
play_video(params[0][1])
|
# Play a video from a provided URL.
|
||||||
|
play_video(params['video'][0])
|
||||||
else:
|
else:
|
||||||
# Display the list of video categories
|
# If the plugin is called from Kodi UI without any parameters,
|
||||||
# if the plugin is called from Kodi UI without parameters.
|
# display the list of video categories
|
||||||
list_categories()
|
list_categories()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
# Call the router function and pass the plugin call parameters to it.
|
||||||
|
# We use string slicing to remove starting "?" from the paramstring
|
||||||
|
router(sys.argv[2][1:])
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,63 @@
|
||||||
|
# License: GPL v3: http://www.gnu.org/copyleft/gpl.html
|
||||||
|
# This is a very minimalistic example of unit testing for Kodi addons.
|
||||||
|
# Perquisites:
|
||||||
|
# xbmcstubs: https://github.com/romanvm/xbmcstubs
|
||||||
|
# mock: https://pypi.python.org/pypi/mock
|
||||||
|
__author__ = 'Roman_V_M'
|
||||||
|
|
||||||
|
import unittest
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import mock
|
||||||
|
# Add our plugin to sys.path
|
||||||
|
sys.path.append(os.path.join(os.path.dirname(os.path.dirname(__file__)), 'plugin.video.expample'))
|
||||||
|
# Mock plugin call parameters so that our plugin can be imported correctly
|
||||||
|
with mock.patch('sys.argv', ['plugin://plugin.video.example', '5', '']):
|
||||||
|
# Import our plugin for testing
|
||||||
|
import default
|
||||||
|
|
||||||
|
|
||||||
|
class RouterTestCase(unittest.TestCase):
|
||||||
|
"""
|
||||||
|
Test router function
|
||||||
|
|
||||||
|
router() function includes custom logic for calling other
|
||||||
|
functions depending on a provided paramstring.
|
||||||
|
So it's a good candidate for unit testing.
|
||||||
|
We don't test other functions here
|
||||||
|
so they are replaced with mocks.
|
||||||
|
"""
|
||||||
|
@mock.patch('default.list_categories')
|
||||||
|
def test_router_with_no_params(self, mock_list_categories):
|
||||||
|
"""
|
||||||
|
Test router with an empty paramstring
|
||||||
|
"""
|
||||||
|
default.router('')
|
||||||
|
mock_list_categories.assert_called_with()
|
||||||
|
|
||||||
|
@mock.patch('default.list_videos')
|
||||||
|
def test_router_list_videos(self, mock_list_videos):
|
||||||
|
"""
|
||||||
|
Test router for open a category request
|
||||||
|
"""
|
||||||
|
default.router('action=listing&category=Animals')
|
||||||
|
mock_list_videos.assert_called_with('Animals')
|
||||||
|
|
||||||
|
@mock.patch('default.play_video')
|
||||||
|
def test_router_play_video(self, mock_play_video):
|
||||||
|
"""
|
||||||
|
Test router for a play video request
|
||||||
|
"""
|
||||||
|
default.router('action=play&video=http://test')
|
||||||
|
mock_play_video.assert_called_with('http://test')
|
||||||
|
|
||||||
|
# list_categories(), list_videos() and play_video() functions include
|
||||||
|
# mostly calls to Kody Python API with little or no custom logic.
|
||||||
|
# So they are bad candidates for unit testing outside Kodi.
|
||||||
|
# They are better tested on a running Kodi instance.
|
||||||
|
# It is always a good idea to separate Kodi and non-Kodi logic
|
||||||
|
# into separate units of code (functions, classes, modules).
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
# Run our tests
|
||||||
|
unittest.main()
|
||||||
Loading…
Reference in New Issue