Allow playing videos only with the video ID

Now the "play_video" action can be called with the ID of the video (and
optionally the URL of the instance hosting the video) as parameter
instead of the full URL: it will allow other add-ons to call the add-on
to play videos since the full URL contains the resolution which is not
known.

It led to some refactoring and changes in the code:
* Only the instance and the id of a video is retrieved when browsing and
  listing videos which improves the performance a lot (the video URL and
  the resolution are defined only when the video is played)
* the "https://" prefix is now automatically added to the instances URL
  because the instance-related REST APIs use URLs without this prefix.
  It also simplifies the external API because the user does not have to
  provide this prefix.
  Consequently the prefix was removed from the default value of the
  selected instance in the settings: it simplifies the code but it
  generates a non-backward compatible change. The impact is limited
  because it can be easily fixed by resetting the settings to the
  default value and there are very few users currently.

Other changes:
 - manage errors when retrieving the information of a video
 - fix some PEP 8 errors
This commit is contained in:
Thomas 2021-03-31 21:58:10 +00:00 committed by Thomas Bétous
parent d14bf5b094
commit 7a21bd92ac
3 changed files with 179 additions and 88 deletions

View File

@ -1,15 +1,19 @@
A Kodi add-on for watching content hosted on [Peertube](http://joinpeertube.org/).
This code is still proof-of-concept but it works, and you're welcome to improve it.
This code is still proof-of-concept but it works, and you're welcome to improve
it.
# Features
* Browse all videos on a PeerTube instance
* Search for videos on a PeerTube instance
* Select Peertube instance to use (Doesn't work yet)
* Select the preferred video resolution: the plugin will try to play the select video resolution.
If it's not available, it will play the lower resolution that is the closest from your preference.
If not available, it will play the higher resolution that is the closest from your preference.
* Select the preferred video resolution: the plugin will try to play the select
video resolution.
If it's not available, it will play the lower resolution that is the closest
from your preference.
If not available, it will play the higher resolution that is the closest from
your preference.
# User settings
@ -23,11 +27,31 @@ If not available, it will play the higher resolution that is the closest from yo
* all-local will only display the videos which are local to the selected
instance plus the private and unlisted videos **(requires admin privileges)**
# API
This add-on can be called from other add-ons in Kodi to play videos thanks to
the following API:
`plugin://plugin.video.peertube/?action=play_video&instance=<instance>&id=<id>`
where:
* `<instance>` is the base URL of the instance hosting the video
* `<id>` is the ID or the UUID of the video on the instance server
For instance to play the video behind the URL
`https://framatube.org/videos/watch/9c9de5e8-0a1e-484a-b099-e80766180a6d` call
the add-on with:
`plugin://plugin.video.peertube/?action=play_video&instance=framatube.org&id=9c9de5e8-0a1e-484a-b099-e80766180a6d`
# Limitations
* This add-on doesn't support Webtorrent yet. So, it cannot download/share from/to regular PeerTube clients.
The reason is that it uses the libtorrent python library which doesn't support it yet (see https://github.com/arvidn/libtorrent/issues/223)
* The add)on doesn't delete the downloaded files at the moment. So, it may fills up your disk.
* This add-on doesn't support Webtorrent yet. So, it cannot download/share
from/to regular PeerTube clients. The reason is that it uses the libtorrent
python library which doesn't support it yet (see
https://github.com/arvidn/libtorrent/issues/223)
* The add-on doesn't delete the downloaded files at the moment. So, it may fill
up your disk.
# Requirements

View File

@ -1,11 +1,14 @@
# A Kodi Addon to play video hosted on the peertube service (http://joinpeertube.org/)
#
# TODO: - Delete downloaded files by default
# - Allow people to choose if they want to keep their download after watching?
# - 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
""" A Kodi Addon to play video hosted on the PeerTube service
(http://joinpeertube.org/)
TODO:
- Delete downloaded files by default
- Allow people to choose if they want to keep their download after watching?
- 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
"""
import sys
try:
@ -22,7 +25,6 @@ import xbmc
import xbmcaddon
import xbmcgui
import xbmcplugin
import xbmcvfs
class PeertubeAddon():
@ -51,7 +53,8 @@ class PeertubeAddon():
self.plugin_id = plugin_id
# Select preferred instance by default
self.selected_inst = addon.getSetting('preferred_instance')
self.selected_inst = 'https://{}'\
.format(addon.getSetting('preferred_instance'))
# Get the number of videos to show per page
self.items_per_page = int(addon.getSetting('items_per_page'))
@ -102,20 +105,25 @@ class PeertubeAddon():
response.raise_for_status()
except requests.HTTPError as e:
xbmcgui.Dialog().notification('Communication error',
'Error during request on {0}'
.format(self.selected_inst),
'Error when sending request {0}'
.format(req),
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']))
# If the JSON contains an 'error' key, print it
error_details = data.get('error')
if error_details is not None:
self.debug('Error => "{}"'.format(data['error']))
raise e
# Return when no results are found
if data['total'] == 0:
self.debug('No result found')
return None
else:
self.debug('Found {0} results'.format(data['total']))
# Try to get the number of elements in the response
results_found = data.get('total', None)
# If the information is available in the response, use it
if results_found is not None:
# Return when no results are found
if results_found == 0:
self.debug('No result found')
return None
else:
self.debug('Found {0} results'.format(results_found))
return data
@ -134,7 +142,9 @@ class PeertubeAddon():
if data_type == 'videos':
# Add thumbnail
list_item.setArt({'thumb': self.selected_inst + '/' + data['thumbnailPath']})
list_item.setArt({
'thumb': '{0}/{1}'.format(self.selected_inst,
data['thumbnailPath'])})
# Set a fanart image for the list item.
# list_item.setProperty('fanart_image', data['thumb'])
@ -148,7 +158,8 @@ class PeertubeAddon():
# For videos, add a rating based on likes and dislikes
if data['likes'] > 0 or data['dislikes'] > 0:
info['rating'] = data['likes']/(data['likes'] + data['dislikes'])
info['rating'] = data['likes'] / (
data['likes'] + data['dislikes'])
# Set additional info for the list item.
list_item.setInfo('video', info)
@ -156,51 +167,16 @@ class PeertubeAddon():
# Videos are playable
list_item.setProperty('IsPlayable', 'true')
# Find the URL of the best possible video matching user's preferences
# TODO: Error handling
current_res = 0
higher_res = -1
torrent_url = ''
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
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
# lower than the user's preferred one
if not torrent_url:
self.debug('Using video with higher resolution as alternative')
torrent_url = backup_url
# Compose the correct URL for Kodi
# Build the Kodi URL to play the associated video only with the
# id of the video. The instance is omitted because the
# currently selected instance will be used automatically.
url = self.build_kodi_url({
'action': 'play_video',
'url': torrent_url
'id': data['uuid']
})
elif data_type == 'instances':
# TODO: Add a context menu to select instance as preferred instance
# TODO: Add a context menu to select instance as preferred
# Instances are not playable
list_item.setProperty('IsPlayable', 'false')
@ -225,6 +201,71 @@ class PeertubeAddon():
return listing
def get_video_url(self, video_id, instance=None):
"""Find the URL of the video with the best possible quality matching
user's preferences.
:param video_id: ID of the torrent linked to the video
:type video_id: str
:param instance: PeerTube instance hosting the video (optional)
:type instance: str
"""
# If no instance was provided, use the selected one (internal call)
if instance is None:
instance = self.selected_inst
else:
# If an instance was provided (external call), ensure the URL is
# prefixed with HTTPS
if not instance.startswith('https://'):
instance = 'https://{}'.format(instance)
current_res = 0
higher_res = -1
torrent_url = ''
# Retrieve the information about the video
metadata = self.query_peertube(urljoin(instance,
'/api/v1/videos/{}'
.format(video_id)))
self.debug(
'Looking for the best resolution matching the user preferences')
for f in metadata['files']:
# Get the 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:
# Otherwise, 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
# When we didn't find a resolution equal or lower than the user's
# preferred one, use the resolution just above the preferred one
if not torrent_url:
self.debug('Using video with higher resolution as alternative')
torrent_url = backup_url
return torrent_url
def build_video_rest_api_request(self, search, start):
"""Build the URL of an HTTP request using the PeerTube videos REST API.
@ -284,20 +325,24 @@ class PeertubeAddon():
}
# Join the base URL with the REST API and the parameters
req = '{0}?{1}'.format('https://instances.joinpeertube.org/api/v1/instances',
urlencode(params))
req = 'https://instances.joinpeertube.org/api/v1/instances?{0}'\
.format(urlencode(params))
return req
def search_videos(self, start):
"""
Function to search for videos on a PeerTube instance and navigate in the results
Function to search for videos on a PeerTube instance and navigate
in the results
:param start: string
:result: None
"""
# Show a 'Search videos' dialog
search = xbmcgui.Dialog().input(heading='Search videos on ' + self.selected_inst, type=xbmcgui.INPUT_ALPHANUM)
search = xbmcgui.Dialog().input(
heading='Search videos on {}'.format(self.selected_inst),
type=xbmcgui.INPUT_ALPHANUM)
# Go back to main menu when user cancels
if not search:
@ -311,7 +356,9 @@ class PeertubeAddon():
# Exit directly when no result is found
if not results:
xbmcgui.Dialog().notification('No videos found', 'No videos found matching query', xbmcgui.NOTIFICATION_WARNING)
xbmcgui.Dialog().notification('No videos found',
'No videos found matching query',
xbmcgui.NOTIFICATION_WARNING)
return None
# Create array of xmbcgui.ListItem's
@ -325,7 +372,9 @@ class PeertubeAddon():
def browse_videos(self, start):
"""
Function to navigate through all the video published by a PeerTube instance
Function to navigate through all the video published by a PeerTube
instance
:param start: string
:return: None
"""
@ -369,13 +418,15 @@ class PeertubeAddon():
def play_video_continue(self, data):
"""
Callback function to let the play_video function resume when the PeertubeDownloader
has downloaded all the torrent's metadata
Callback function to let the play_video function resume when the
PeertubeDownloader has downloaded all the torrent's metadata
:param data: dict
:return: None
"""
self.debug('Received metadata_downloaded signal, will start playing media')
self.debug(
'Received metadata_downloaded signal, will start playing media')
self.play = 1
self.torrent_f = data['file']
@ -393,8 +444,11 @@ class PeertubeAddon():
# Start a downloader thread
AddonSignals.sendSignal('start_download', {'url': torrent_url})
# Wait until the PeerTubeDownloader has downloaded all the torrent's metadata
AddonSignals.registerSlot('plugin.video.peertube', 'metadata_downloaded', self.play_video_continue)
# 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:
xbmc.sleep(1000)
@ -402,7 +456,10 @@ class PeertubeAddon():
# Abort in case of timeout
if timeout == 10:
xbmcgui.Dialog().notification('Download timeout', 'Timeout fetching ' + torrent_url, xbmcgui.NOTIFICATION_ERROR)
xbmcgui.Dialog().notification('Download timeout',
'Timeout fetching {}'
.format(torrent_url),
xbmcgui.NOTIFICATION_ERROR)
return None
else:
# Wait a little before starting playing the torrent
@ -422,8 +479,11 @@ class PeertubeAddon():
: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)
self.selected_inst = 'https://{}'.format(instance)
xbmcgui.Dialog().notification('Current instance changed',
'Changed current instance to {0}'
.format(self.selected_inst),
xbmcgui.NOTIFICATION_INFO)
self.debug('Changing currently selected instance to {0}'
.format(self.selected_inst))
@ -496,12 +556,19 @@ class PeertubeAddon():
# Browse peerTube instances
self.browse_instances(params['start'])
elif action == 'play_video':
# Play video from provided URL.
self.play_video(params['url'])
# This action comes with the id of the video to play as
# parameter. The instance may also be in the parameters. Use
# these parameters to retrieve the complete URL (containing the
# resolution).
url = self.get_video_url(instance=params.get('instance'),
video_id=params.get('id'))
# Play the video using the URL
self.play_video(url)
elif action == 'select_instance':
self.select_instance(params['url'])
else:
# Display the addon's main menu when the plugin is called from Kodi UI without any parameters
# Display the addon's main menu when the plugin is called from
# Kodi UI without any parameters
self.main_menu()
return None

View File

@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<settings>
<setting id="preferred_instance" type="text" default="https://framatube.org" label="30000"/>
<setting id="preferred_instance" type="text" default="framatube.org" label="30000"/>
<setting type="sep"/>
<setting id="items_per_page" type="select" values="10|20|50|100" default="20" label="30001"/>
<setting id="video_sort_method" type="select" values="likes|views" default='views' label="30002"/>