# -*- 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 json import os.path from urllib import quote_plus from resources.lib.kodi_utils import kodi from resources.lib.peertube import PeerTube, list_instances import AddonSignals import xbmcvfs class PeerTubeAddon(): """ Main class used by the add-on """ 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")) # 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) 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 = kodi.get_string(30404).format( data["shortDescription"], data["totalLocalVideos"], data["totalUsers"] ) 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"] ) # Note: the type of video (live or not) is available in "response" # but this information is ignored here so that the "play_video" # action is the same whatever the type of the video. The goal is to # allow external users of the API to play a video only with its ID # without knowing its type. # The information about the type of the video will anyway be # available in the response used to get the URL of a video so this # solution does not impact the performance. 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=u"{} ({}/{})".format(kodi.get_string(30405), next_page, total_pages), url=url ) return next_page_item def _get_url_with_resolution(self, list_of_url_and_resolutions): """ Build the URL of the video PeerTube creates 1 URL for each resolution so we browse all the available resolutions and select the best possible quality matching user's preferences. If the preferred resolution cannot be found, the one just below will be used. If it is not possible the one just above we will be used. :param list list_of_url_and_resolutions: list of dict containing 2 keys: the resolution and the associated URL. :return: the URL matching the selected resolution :rtype: str """ # Find the best resolution matching user's preferences current_resolution = 0 higher_resolution = -1 url = None for video in list_of_url_and_resolutions: # Get the resolution resolution = video.get("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)) return video["url"] 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 url is None: 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=kodi.get_string(30406), url=kodi.build_kodi_url({"action": "browse_videos","start": 0}) ), kodi.generate_item_info( name=kodi.get_string(30407), url=kodi.build_kodi_url({"action": "search_videos","start": 0}) ), kodi.generate_item_info( name=kodi.get_string(30408), 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=kodi.get_string(30409).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=kodi.get_string(30410), message=kodi.get_string(30411).format(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, video_id, instance): """ Get the required information and play the video :param str video_id: ID of the torrent linked with the video :param str instance: PeerTube instance hosting the video """ # Get the information of the video including the different resolutions # available video_info = self.peertube.get_video_info(video_id, instance) # Check if the video is a live (Kodi can play live videos (.m3u8) out of # the box whereas torrents must first be downloaded) if video_info["is_live"]: kodi.play(video_info["files"][0]["url"]) else: # Get the URL of the file which resolution is the closest to the # user's preferences url = self._get_url_with_resolution(video_info["files"]) self._download_and_play(url, int(video_info["duration"])) def _download_and_play(self, torrent_url, duration): """ Start the torrent's download and play it while being downloaded The user configures in the settings the number of seconds of the file that must be downloaded before the playback starts. :param str torrent_url: URL of the torrent file to download and play :param int duration: duration of the video behind the URL in seconds """ kodi.debug("Starting torrent download ({})".format(torrent_url)) # Download the torrent using vfs.libtorrent: the torrent URL must be # URL-encoded to be correctly read by vfs.libtorrent vfs_url = "torrent://{}".format(quote_plus(torrent_url)) torrent = xbmcvfs.File(vfs_url) # Get information about the torrent torrent_info = json.loads(torrent.read()) if torrent_info["nb_files"] > 1: kodi.warning("There are more than 1 file in {} but only the" " first one will be played.".format(torrent_url)) # Compute the amount of the file that we want to wait to be downloaded # before playing the video. It is based on the number of seconds # configured by the user and the total duration of the video. initial_chunk_proportion = (int(kodi.get_setting("initial_wait_time")) * 100. / duration) # TODO: Remove the dot in 100. in python 3? Or keep it to suport both # python2 and python3 kodi.debug("initial_chunk_proportion = {}".format(initial_chunk_proportion)) # Download the file, waiting for "initial_chunk_proportion" % of the # file to be downloaded (seek() takes only integers so the proportion # is multiplied to have more granularity.) if(torrent.seek(initial_chunk_proportion*100, 0) != -1): # Build the path of the downloaded file torrent_file = os.path.join(torrent_info["save_path"], torrent_info["files"][0]["path"]) # Send information about the torrent to the service so that it can # control the torrent later(e.g. pause the download when the # playback stops) AddonSignals.sendSignal("torrent_information", { "run_url": kodi.build_kodi_url(kodi.get_run_parameters()), "torrent_url": vfs_url } ) # Play the file kodi.debug("Starting video playback of {}".format(torrent_file)) kodi.play(torrent_file) else: kodi.notif_error(title=kodi.get_string(30421), message=kodi.get_string(30422)) 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 kodi.notif_info( title=kodi.get_string(30418), message=kodi.get_string(30419).format(self.peertube.instance)) kodi.debug("{} is now the selected instance" .format(self.peertube.instance)) 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": self._play_video(instance=params.get("instance"), video_id=params.get("id")) 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()