kodi.plugin.video.peertube/resources/lib/addon.py

442 lines
17 KiB
Python

# -*- coding: utf-8 -*-
"""
Main class used by the add-on
Copyright (C) 2018 Cyrille Bollu
Copyright (C) 2021 Thomas Bétous
SPDX-License-Identifier: GPL-3.0-only
See LICENSE.txt for more information.
"""
import AddonSignals # Module exists only in Kodi - pylint: disable=import-error
from resources.lib.kodi_utils import kodi
from resources.lib.peertube import PeerTube, list_instances
class PeerTubeAddon():
"""
Main class used by the add-on
"""
# URL of the page which explains how to install libtorrent
HELP_URL = "https://link.infini.fr/libtorrent-peertube-kodi"
def __init__(self):
"""Initialize parameters and create a PeerTube instance"""
# Get the number of items to show per page
self.items_per_page = int(kodi.get_setting("items_per_page"))
# Get the preferred resolution for video
self.preferred_resolution = \
int(kodi.get_setting("preferred_resolution"))
# Nothing to play at initialisation
self.play = False
self.torrent_name = ""
self.torrent_file = ""
# Check whether libtorrent could be imported by the service. The value
# of the associated property is retrieved only once and stored in an
# attribute because libtorrent is imported only once at the beginning
# of the service (we assume it is not possible to start the add-on
# before the service)
self.libtorrent_imported = \
kodi.get_property("libtorrent_imported") == "True"
# Create a PeerTube object to send requests: settings which are used
# only by this object are directly retrieved from the settings
self.peertube = PeerTube(
instance=kodi.get_setting("preferred_instance"),
count=self.items_per_page,
sort=kodi.get_setting("video_sort_method"),
video_filter=kodi.get_setting("video_filter"))
def _browse_videos(self, start):
"""Display the list of all the videos published on a PeerTube instance
:param int start: index of the first video to display (pagination)
"""
# Use the API to get the list of the videos
results = self.peertube.list_videos(start)
# Extract the information of each video from the API response
list_of_videos = self._create_list_of_videos(results, start)
# Create the associated items in Kodi
kodi.create_items_in_ui(list_of_videos)
def _browse_instances(self, start):
"""
Function to navigate through all the PeerTube instances
:param int start: index of the first instance to display (pagination)
"""
# Use the API to get the list of the instances
results = list_instances(start)
# Extract the information of each instance from the API response
list_of_instances = self._create_list_of_instances(results, start)
# Create the associated items in Kodi
kodi.create_items_in_ui(list_of_instances)
def _create_list_of_instances(self, response, start):
"""Generator of instance items to be added in Kodi UI
:param dict response: data returned by joinpeertube
:param int start: index of the first item to display (pagination)
:return: yield the information of each item
:rtype: dict
"""
for data in response["data"]:
# The description of each instance in Kodi will be composed of:
# * the description of the instance (from joinpeertube.org)
# * the number of local videos hosted on this instance
# * the number of users on this instance
description = "{}\n\n----------\nNumber of local videos: {}\n"\
"Number of users: {}".format(
data["shortDescription"].encode("utf-8"),
data["totalLocalVideos"],
data["totalUsers"])
# The value of "totalLocalVideos" and "totalUsers" are int so they
# don't need to be encoded.
instance_info = kodi.generate_item_info(
name=data["name"],
url=kodi.build_kodi_url({
"action": "select_instance",
"url": data["host"]
}
),
is_folder=True,
plot=description
)
yield instance_info
else:
# Add a "Next page" button when there are more items to show
next_page_item = self._create_next_page_item(
total=int(response["total"]),
current_index=start,
url=kodi.build_kodi_url(
{
"action": "browse_instances",
"start": start + self.items_per_page
}
)
)
if next_page_item:
yield next_page_item
def _create_list_of_videos(self, response, start):
"""Generator of video items to be added in Kodi UI
:param dict response: data returned by PeerTube
:param int start: index of the first item to display (pagination)
:return: yield the information of each item
:rtype: dict
"""
for data in response["data"]:
video_info = kodi.generate_item_info(
name=data["name"],
url=kodi.build_kodi_url(
{
"action": "play_video",
"id": data["uuid"]
}
),
is_folder=False,
plot=data["description"],
duration=data["duration"],
thumbnail="{0}/{1}".format(self.peertube.instance,
data["thumbnailPath"]),
aired=data["publishedAt"]
)
yield video_info
else:
# Add a "Next page" button when there are more items to show
next_page_item = self._create_next_page_item(
total=int(response["total"]),
current_index=start,
url=kodi.build_kodi_url(
{
"action": "browse_videos",
"start": start + self.items_per_page
}
)
)
if next_page_item:
yield next_page_item
def _create_next_page_item(self, total, current_index, url):
"""Return the info required to create an item to go to the next page
:param int total: total number of elements
:param int current_index: index of the first element currently used
:param str url: URL to reach when the "Next page" item is run
:return: yield the info to create a "Next page" item in Kodi UI if
there are more items to show
:rtype: dict
"""
next_index = current_index + self.items_per_page
if total > next_index:
next_page = (next_index / self.items_per_page) + 1
total_pages = (total / self.items_per_page) + 1
next_page_item = kodi.generate_item_info(
name="Next page ({}/{})".format(next_page, total_pages),
url=url
)
return next_page_item
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 str video_id: ID of the torrent linked with the video
:param str instance: PeerTube instance hosting the video (optional)
:return: URL of the video containing the resolution
:rtype: str
"""
# Retrieve the information about the video including the different
# resolutions available
video_files = self.peertube.get_video_urls(video_id, instance=instance)
# Find the best resolution matching user's preferences
current_resolution = 0
higher_resolution = -1
url = ""
for video in video_files:
# Get the resolution
resolution = video["resolution"]
if resolution == self.preferred_resolution:
# Stop directly when we find the exact same resolution as the
# user's preferred one
kodi.debug("Found video with preferred resolution ({})"
.format(self.preferred_resolution))
url = video["url"]
break
elif (resolution < self.preferred_resolution
and resolution > current_resolution):
# Otherwise, try to find the best one just below the user's
# preferred one
kodi.debug("Found video with good lower resolution ({})"
.format(resolution))
url = video["url"]
current_resolution = resolution
elif (resolution > self.preferred_resolution and
(resolution < higher_resolution or
higher_resolution == -1)):
# In the worst case, we'll take the one just above the user's
# preferred one
kodi.debug("Saving video with higher resolution ({}) as a"
" possible alternative".format(resolution))
backup_url = video["url"]
higher_resolution = resolution
else:
kodi.debug("Ignoring the resolution '{}'".format(resolution))
# 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 url:
kodi.debug("Using video with higher resolution as alternative ({})"
.format(higher_resolution))
url = backup_url
return url
def _home_page(self):
"""Display the items of the home page of the add-on"""
home_page_items = [
kodi.generate_item_info(
name="Browse videos on the selected instance",
url=kodi.build_kodi_url({"action": "browse_videos","start": 0})
),
kodi.generate_item_info(
name="Search videos on the selected instance",
url=kodi.build_kodi_url({"action": "search_videos","start": 0})
),
kodi.generate_item_info(
name="Browse videos on the selected instance",
url=kodi.build_kodi_url({
"action": "browse_instances",
"start": 0
}
)
)
]
kodi.create_items_in_ui(home_page_items)
def _search_videos(self, start):
"""
Function to search for videos on a PeerTube instance
:param str start: index of the first video to display (pagination)
"""
# Ask the user which keywords must be searched for
keywords = kodi.open_input_box(
title="Search videos on {}".format(self.peertube.instance))
# Go back to the home page when the user cancels or didn't enter any
# string
if not keywords:
return
# Use the API to search for videos
results = self.peertube.search_videos(keywords, start)
# Exit directly when no result is found
if not results:
kodi.notif_warning(
title="No videos found",
message="No videos found matching the keywords.")
return
# Extract the information of each video from the API response
list_of_videos = self._create_list_of_videos(results, start)
# Create the associated items in Kodi
kodi.create_items_in_ui(list_of_videos)
def _play_video(self, torrent_url):
"""
Start the torrent's download and play it while being downloaded
:param str torrent_url: URL of the torrent file to download and play
"""
# If libtorrent could not be imported, display a message and do not try
# download nor play the video as it will fail.
if not self.libtorrent_imported:
kodi.open_dialog(
title="Error: libtorrent could not be imported",
message="PeerTube cannot play videos without libtorrent\n"
"Please follow the instructions at {}"
.format(self.HELP_URL))
return
kodi.debug("Starting torrent download ({})".format(torrent_url))
kodi.notif_info(title="Download started",
message="The video will be played soon.")
# Start a downloader thread
AddonSignals.sendSignal("start_download", {"url": torrent_url})
# Wait until the PeerTubeDownloader has downloaded all the torrent's
# metadata
AddonSignals.registerSlot(kodi.addon_id,
"metadata_downloaded",
self._play_video_continue)
timeout = 0
while not self.play and timeout < 10:
kodi.sleep(1000)
timeout += 1
# Abort in case of timeout
if timeout == 10:
kodi.notif_error(
title="Download timeout",
message="Timeout fetching {}".format(torrent_url))
return
else:
# Wait a little before starting playing the torrent
kodi.sleep(3000)
# Pass the item to the Kodi player for actual playback.
kodi.debug("Starting video playback ({})".format(self.torrent_file))
kodi.play(self.torrent_file)
def _play_video_continue(self, data):
"""
Callback function to let the _play_video method resume when the
PeertubeDownloader has downloaded all the torrent's metadata
:param data: dict of information sent from PeertubeDownloader
"""
kodi.debug(
"Received metadata_downloaded signal, will start playing media")
self.play = True
self.torrent_file = data["file"]
def _select_instance(self, instance):
"""
Change currently selected instance to "instance" parameter
:param str instance: URL of the new instance
"""
# Update the PeerTube object attribute even though it is not used
# currently (because the value will be retrieved from the settings on
# the next run of the add-on but it may be useful in case
# reuselanguageinvoker is enabled)
self.peertube.set_instance(instance)
# Update the preferred instance in the settings so that this choice is
# reused on the next runs and the next calls of the add-on
kodi.set_setting("preferred_instance", instance)
# Notify the user and log the event
message = \
"{} is now the selected instance".format(self.peertube.instance)
kodi.notif_info(title="Current instance changed", message=message)
kodi.debug(message)
def router(self, params):
"""Route the add-on to the requested actions
:param dict params: Parameters the add-on was called with
"""
# Check the parameters passed to the plugin
if params:
action = params["action"]
if action == "browse_videos":
# Browse videos on the selected instance
self._browse_videos(int(params["start"]))
elif action == "search_videos":
# Search for videos on the selected instance
self._search_videos(int(params["start"]))
elif action == "browse_instances":
# Browse PeerTube instances
self._browse_instances(int(params["start"]))
elif action == "play_video":
# 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":
# Set the selected instance as the preferred instance
self._select_instance(params["url"])
else:
# Display the addon's main menu when the plugin is called from
# Kodi UI without any parameters
self._home_page()
# Display a warning if libtorrent could not be imported
if not self.libtorrent_imported:
kodi.open_dialog(
title="Error: libtorrent could not be imported",
message="You can still browse and search videos but you"
" will not be able to play them.\n"
"Please follow the instructions at {}"
.format(self.HELP_URL))