diff --git a/addon.xml b/addon.xml
index a9862f4..706b3a1 100644
--- a/addon.xml
+++ b/addon.xml
@@ -3,6 +3,7 @@
+ video
@@ -21,7 +22,7 @@
0.3.1.1
-Add local videos only browsing
+Add local videos only browsing
0.3.1
Fixed some bugs
The 'change current instance" functionality currently doesn't work because
@@ -32,13 +33,13 @@ Implemented the 'Browse instances' functionality
0.2.2
Implemented the 'video sort method' functionality
0.2.1
-Fixed some bugs
+Fixed some bugs
Added a 'video sort method' setting (functionality not implemented yet though)
0.2.0
-Implemented 'browse selected instance' functionality
+Implemented 'browse selected instance' functionality
Implemented 'search videos on selected instance' functionality
0.1.1
-4th PoC.
+4th PoC.
First functional PoC with background download
0.1.0
Third PoC (download in background should work. Needs testing though)
diff --git a/peertube.py b/peertube.py
index 4116977..0aa1e85 100644
--- a/peertube.py
+++ b/peertube.py
@@ -5,13 +5,25 @@
# - Do sanity checks on received data
# - Handle languages better (with .po files)
# - Get the best quality torrent given settings and/or available bandwidth
-# See how they do that in the peerTube client's code
+# See how they do that in the peerTube client's code
+import sys
+
+try:
+ # Python 3.x
+ from urllib.parse import parse_qsl
+except ImportError:
+ # Python 2.x
+ from urlparse import parse_qsl
-import time, sys
-import urllib2, json
-from urlparse import parse_qsl
-import xbmc, xbmcgui, xbmcaddon, xbmcplugin, xbmcvfs
import AddonSignals
+import requests
+from requests.compat import urljoin, urlencode
+import xbmc
+import xbmcaddon
+import xbmcgui
+import xbmcplugin
+import xbmcvfs
+
class PeertubeAddon():
"""
@@ -25,19 +37,23 @@ class PeertubeAddon():
:return: None
"""
- xbmc.log('PeertubeAddon: Initialising', xbmc.LOGDEBUG)
-
+ # These 2 steps must be done first since the logging function requires
+ # the add-on name
+ # Get an Addon instance
+ addon = xbmcaddon.Addon()
+ # Get the add-on name
+ self.addon_name = addon.getAddonInfo('name')
+
+ self.debug('Initialising')
+
# Save addon URL and ID
self.plugin_url = plugin
self.plugin_id = plugin_id
- # Get an Addon instance
- addon = xbmcaddon.Addon()
-
# Select preferred instance by default
- self.selected_inst = addon.getSetting('preferred_instance')
+ self.selected_inst = addon.getSetting('preferred_instance')
- # Get the number of videos to show per page
+ # Get the number of videos to show per page
self.items_per_page = int(addon.getSetting('items_per_page'))
# Get the video sort method
@@ -50,12 +66,24 @@ class PeertubeAddon():
self.play = 0
self.torrent_name = ''
- # filter= 'local' or 'all-local' in verb search rest api
- # applied for browsing only
- self.video_filter = addon.getSetting('video_filter')
-
+ # Get the video filter from the settings that will be used when
+ # browsing the videos. The value from the settings is converted into
+ # one of the expected values by the REST APIs ("local" or "all-local")
+ if 'all-local' in addon.getSetting('video_filter'):
+ self.video_filter = 'all-local'
+ else:
+ self.video_filter = 'local'
+
return None
+ def debug(self, message):
+ """Log a message in Kodi's log with the level xbmc.LOGDEBUG
+
+ :param message: Message to log
+ :type message: str
+ """
+ xbmc.log('{0}: {1}'.format(self.addon_name, message), xbmc.LOGDEBUG)
+
def query_peertube(self, req):
"""
Issue a PeerTube API request and return the results
@@ -64,20 +92,30 @@ class PeertubeAddon():
"""
# Send the PeerTube REST API request
+ self.debug('Issuing request {0}'.format(req))
+ response = requests.get(url=req)
+ data = response.json()
+
+ # Use Request.raise_for_status() to raise an exception if the HTTP
+ # request returned an unsuccessful status code.
try:
- xbmc.log('PeertubeAddon: Issuing request {0}'.format(req), xbmc.LOGDEBUG)
- resp = urllib2.urlopen(req)
- data = json.load(resp)
- except:
- xbmcgui.Dialog().notification('Communication error', 'Error during my request on {0}'.format(self.selected_inst), xbmcgui.NOTIFICATION_ERROR)
- return None
+ response.raise_for_status()
+ except requests.HTTPError as e:
+ xbmcgui.Dialog().notification('Communication error',
+ 'Error during request on {0}'
+ .format(self.selected_inst),
+ xbmcgui.NOTIFICATION_ERROR)
+ # Print the JSON as it may contain an 'error' key with the details
+ # of the error
+ self.debug('Error => "{}"'.format(data['error']))
+ raise e
# Return when no results are found
if data['total'] == 0:
- xbmc.log('PeertubeAddon: No result found', xbmc.LOGDEBUG)
+ self.debug('No result found')
return None
else:
- xbmc.log('PeertubeAddon: Found {0} results'.format(data['total']), xbmc.LOGDEBUG)
+ self.debug('Found {0} results'.format(data['total']))
return data
@@ -93,16 +131,16 @@ class PeertubeAddon():
# Create a list item with a text label
list_item = xbmcgui.ListItem(label=data['name'])
-
+
if data_type == 'videos':
# Add thumbnail
list_item.setArt({'thumb': self.selected_inst + '/' + data['thumbnailPath']})
# Set a fanart image for the list item.
- #list_item.setProperty('fanart_image', data['thumb'])
+ # list_item.setProperty('fanart_image', data['thumb'])
# Compute media info from item's metadata
- info = {'title':data['name'],
+ info = {'title': data['name'],
'playcount': data['views'],
'plotoutline': data['description'],
'duration': data['duration']
@@ -113,54 +151,64 @@ class PeertubeAddon():
info['rating'] = data['likes']/(data['likes'] + data['dislikes'])
# Set additional info for the list item.
- list_item.setInfo('video', info)
+ list_item.setInfo('video', info)
# Videos are playable
list_item.setProperty('IsPlayable', 'true')
- # Find the URL of the best possible video matching user's preferrence
+ # Find the URL of the best possible video matching user's preferences
# TODO: Error handling
current_res = 0
higher_res = -1
torrent_url = ''
- resp = urllib2.urlopen(self.selected_inst + '/api/v1/videos/' + data['uuid'])
- metadata = json.load(resp)
- xbmc.log('PeertubeAddon: Looking for the best possible video matching user preferrences', xbmc.LOGDEBUG)
+ response = requests.get(self.selected_inst + '/api/v1/videos/'
+ + data['uuid'])
+ metadata = response.json()
+ self.debug('Looking for the best possible video quality matching user preferences')
for f in metadata['files']:
- # Get file resolution
- res = f['resolution']['id']
- if res == self.preferred_resolution:
- # Stop directly, when we find the exact same resolution as the user's preferred one
- xbmc.log('PeertubeAddon: Found video with preferred resolution', xbmc.LOGDEBUG)
- torrent_url = f['torrentUrl']
- break
- elif res < self.preferred_resolution and res > current_res:
- # Else, try to find the best one just below the user's preferred one
- xbmc.log('PeertubeAddon: Found video with good lower resolution ({0})'.format(f['resolution']['label']), xbmc.LOGDEBUG)
- torrent_url = f['torrentUrl']
- current_res = res
- elif res > self.preferred_resolution and ( res < higher_res or higher_res == -1 ):
- # In the worth case, we'll take the one just above the user's preferred one
- xbmc.log('PeertubeAddon: Saving video with higher resolution ({0}) as a possible alternative'.format(f['resolution']['label']), xbmc.LOGDEBUG)
- backup_url = f['torrentUrl']
- higher_res = res
+ # Get file resolution
+ res = f['resolution']['id']
+ if res == self.preferred_resolution:
+ # Stop directly, when we find the exact same resolution as the user's preferred one
+ self.debug('Found video with preferred resolution')
+ torrent_url = f['torrentUrl']
+ break
+ elif res < self.preferred_resolution and res > current_res:
+ # Else, try to find the best one just below the user's preferred one
+ self.debug('Found video with good lower resolution'
+ '({0})'.format(f['resolution']['label']))
+ torrent_url = f['torrentUrl']
+ current_res = res
+ elif res > self.preferred_resolution and (res < higher_res or higher_res == -1):
+ # In the worst case, we'll take the one just above the user's preferred one
+ self.debug('Saving video with higher resolution ({0})'
+ 'as a possible alternative'
+ .format(f['resolution']['label']))
+ backup_url = f['torrentUrl']
+ higher_res = res
# Use smallest file with an higher resolution, when we didn't find a resolution equal or
- # slower than the user's preferred one
+ # lower than the user's preferred one
if not torrent_url:
- xbmc.log('PeertubeAddon: Using video with higher resolution as alternative', xbmc.LOGDEBUG)
- torrent_url = backup_url
+ self.debug('Using video with higher resolution as alternative')
+ torrent_url = backup_url
# Compose the correct URL for Kodi
- url = self.build_kodi_url_action_url('play_video', torrent_url)
-
+ url = self.build_kodi_url({
+ 'action': 'play_video',
+ 'url': torrent_url
+ })
+
elif data_type == 'instances':
# TODO: Add a context menu to select instance as preferred instance
# Instances are not playable
list_item.setProperty('IsPlayable', 'false')
# Set URL to select this instance
- url = self.build_kodi_url_action_url('select_instance',data['host'])
+ url = self.build_kodi_url({
+ 'action': 'select_instance',
+ 'url': data['host']
+ })
# Add our item to the listing as a 3-element tuple.
listing.append((url, list_item, False))
@@ -168,21 +216,77 @@ class PeertubeAddon():
# Add a 'Next page' button when there are more data to show
start = int(start) + self.items_per_page
if lst['total'] > start:
- list_item = xbmcgui.ListItem( label='Next page ({0})'.format(start/self.items_per_page) )
- url = '{0}?action=browse_{1}&start={2}'.format(self.plugin_url, data_type, start)
+ list_item = xbmcgui.ListItem(label='Next page ({0})'
+ .format(start/self.items_per_page))
+ url = self.build_kodi_url({
+ 'action': 'browse_{0}'.format(data_type),
+ 'start': start})
listing.append((url, list_item, True))
return listing
- def build_peertube_rest_api_search_request(self,search,start):
- # Didn't yet find a correct way to do a search with a filter set to local.
- # then if a search value is given it won't filter on local
+ def build_video_rest_api_request(self, search, start):
+ """Build the URL of an HTTP request using the PeerTube videos REST API.
+
+ The same function is used for browsing and searching videos.
+
+ :param search: keywords to search
+ :type search: string
+ :param start: offset
+ :type start: int
+ :return: the URL of the request
+ :rtype: str
+
+ Didn't yet find a correct way to do a search with a filter set to
+ local. Then if a search value is given it won't filter on local
+ """
+
+ # Common parameters of the request
+ params = {
+ 'count': self.items_per_page,
+ 'start': start,
+ 'sort': self.sort_method
+ }
+
+ # Depending on the type of request (search or list videos), add
+ # specific parameters and define the API to use
if search is None:
- # video api does not provide search=
- req = '{0}/api/v1/videos?count={1}&start={2}&sort={3}&filter={4}'.format(self.selected_inst, self.items_per_page, start, self.sort_method,self.video_filter)
+ # Video API does not provide "search" but provides "filter" so add
+ # it to the parameters
+ params.update({'filter': self.video_filter})
+ api_url = '/api/v1/videos'
else:
- # search api does not provide filter=
- req = '{0}/api/v1/search/videos?count={1}&start={2}&sort={3}&search={4}'.format(self.selected_inst, self.items_per_page, start, self.sort_method,search)
+ # Search API does not provide "filter" but provides "search" so add
+ # it to the parameters
+ params.update({'search': search})
+ api_url = '/api/v1/search/videos'
+
+ # Build the full URL of the request (instance + API + parameters)
+ req = '{0}?{1}'.format(urljoin(self.selected_inst, api_url),
+ urlencode(params))
+
+ return req
+
+ def build_browse_instances_rest_api_request(self, start):
+ """Build the URL of an HTTP request using the PeerTube REST API to
+ browse the PeerTube instances.
+
+ :param start: offset
+ :type start: int
+ :return: the URL of the request
+ :rtype: str
+ """
+
+ # Create the parameters of the request
+ params = {
+ 'count': self.items_per_page,
+ 'start': start
+ }
+
+ # Join the base URL with the REST API and the parameters
+ req = '{0}?{1}'.format('https://instances.joinpeertube.org/api/v1/instances',
+ urlencode(params))
+
return req
def search_videos(self, start):
@@ -198,9 +302,9 @@ class PeertubeAddon():
# Go back to main menu when user cancels
if not search:
return None
-
+
# Create the PeerTube REST API request for searching videos
- req = self.build_peertube_rest_api_search_request(search,start)
+ req = self.build_video_rest_api_request(search, start)
# Send the query
results = self.query_peertube(req)
@@ -218,7 +322,7 @@ class PeertubeAddon():
xbmcplugin.endOfDirectory(self.plugin_id)
return None
-
+
def browse_videos(self, start):
"""
Function to navigate through all the video published by a PeerTube instance
@@ -227,7 +331,7 @@ class PeertubeAddon():
"""
# Create the PeerTube REST API request for listing videos
- req = self.build_peertube_rest_api_search_request(None,start)
+ req = self.build_video_rest_api_request(None, start)
# Send the query
results = self.query_peertube(req)
@@ -242,14 +346,14 @@ class PeertubeAddon():
return None
def browse_instances(self, start):
- """
+ """
Function to navigate through all PeerTube instances
:param start: str
:return: None
"""
# Create the PeerTube REST API request for browsing PeerTube instances
- req = '{0}/api/v1/instances?count={1}&start={2}'.format('https://instances.joinpeertube.org', self.items_per_page, start)
+ req = self.build_browse_instances_rest_api_request(start)
# Send the query
results = self.query_peertube(req)
@@ -260,7 +364,7 @@ class PeertubeAddon():
# Add our listing to Kodi.
xbmcplugin.addDirectoryItems(self.plugin_id, listing, len(listing))
xbmcplugin.endOfDirectory(self.plugin_id)
-
+
return None
def play_video_continue(self, data):
@@ -271,8 +375,8 @@ class PeertubeAddon():
:return: None
"""
- xbmc.log('PeertubeAddon: Received metadata_downloaded signal, will start playing media', xbmc.LOGDEBUG)
- self.play = 1
+ self.debug('Received metadata_downloaded signal, will start playing media')
+ self.play = 1
self.torrent_f = data['file']
return None
@@ -284,12 +388,12 @@ class PeertubeAddon():
:return: None
"""
- xbmc.log('PeertubeAddon: Starting torrent download ({0})'.format(torrent_url), xbmc.LOGDEBUG)
+ self.debug('Starting torrent download ({0})'.format(torrent_url))
# Start a downloader thread
AddonSignals.sendSignal('start_download', {'url': torrent_url})
- # Wait until the PeerTubeDownloader has downloaded all the torrent's metadata
+ # Wait until the PeerTubeDownloader has downloaded all the torrent's metadata
AddonSignals.registerSlot('plugin.video.peertube', 'metadata_downloaded', self.play_video_continue)
timeout = 0
while self.play == 0 and timeout < 10:
@@ -305,32 +409,34 @@ class PeertubeAddon():
xbmc.sleep(3000)
# Pass the item to the Kodi player for actual playback.
- xbmc.log('PeertubeAddon: Starting video playback ({0})'.format(torrent_url), xbmc.LOGDEBUG)
+ self.debug('Starting video playback ({0})'.format(torrent_url))
play_item = xbmcgui.ListItem(path=self.torrent_f)
xbmcplugin.setResolvedUrl(self.plugin_id, True, listitem=play_item)
return None
-
+
def select_instance(self, instance):
"""
Change currently selected instance to 'instance' parameter
:param instance: str
:return: None
"""
-
+
self.selected_inst = 'https://' + instance
xbmcgui.Dialog().notification('Current instance changed', 'Changed current instance to {0}'.format(self.selected_inst), xbmcgui.NOTIFICATION_INFO)
- xbmc.log('PeertubeAddon: Changing currently selected instance to {0}'.format(self.selected_inst), xbmc.LOGDEBUG)
+ self.debug('Changing currently selected instance to {0}'
+ .format(self.selected_inst))
return None
- def build_kodi_url_action_url(self,action,url):
- url = '{0}?action={1}&url={2}'.format(self.plugin_url,action,url)
- return url
+ def build_kodi_url(self, parameters):
+ """Build a Kodi URL based on the parameters.
- def build_kodi_url_action(self,action):
- url = '{0}?action={1}&start=0'.format(self.plugin_url,action)
- return url
+ :param parameters: dict containing all the parameters that will be
+ encoded in the URL
+ """
+
+ return '{0}?{1}'.format(self.plugin_url, urlencode(parameters))
def main_menu(self):
"""
@@ -344,17 +450,17 @@ class PeertubeAddon():
# 1st menu entry
list_item = xbmcgui.ListItem(label='Browse selected instance')
- url = self.build_kodi_url_action('browse_videos')
+ url = self.build_kodi_url({'action': 'browse_videos', 'start': 0})
listing.append((url, list_item, True))
# 2nd menu entry
list_item = xbmcgui.ListItem(label='Search on selected instance')
- url = self.build_kodi_url_action('search_videos')
+ url = self.build_kodi_url({'action': 'search_videos', 'start': 0})
listing.append((url, list_item, True))
# 3rd menu entry
list_item = xbmcgui.ListItem(label='Select other instance')
- url = self.build_kodi_url_action('browse_instances')
+ url = self.build_kodi_url({'action': 'browse_instances', 'start': 0})
listing.append((url, list_item, True))
# Add our listing to Kodi.
@@ -362,7 +468,7 @@ class PeertubeAddon():
# Finish creating a virtual folder.
xbmcplugin.endOfDirectory(self.plugin_id)
-
+
return None
def router(self, paramstring):
@@ -400,6 +506,7 @@ class PeertubeAddon():
return None
+
if __name__ == '__main__':
# Initialise addon
diff --git a/resources/language/resource.language.en_gb/strings.po b/resources/language/resource.language.en_gb/strings.po
index 8b6885c..14bf8bb 100644
--- a/resources/language/resource.language.en_gb/strings.po
+++ b/resources/language/resource.language.en_gb/strings.po
@@ -39,5 +39,5 @@ msgid "Delete downloaded videos when Kodi exits"
msgstr ""
msgctxt "#30005"
-msgid "Filter local or all videos ( for browsing only )"
+msgid "Filter local or all videos (for browsing only)"
msgstr ""
diff --git a/resources/settings.xml b/resources/settings.xml
index 91e8bc0..839d6fd 100644
--- a/resources/settings.xml
+++ b/resources/settings.xml
@@ -7,5 +7,5 @@
-
+