From 4346178db9232359f510aa301bde5257651a4834 Mon Sep 17 00:00:00 2001 From: Thomas Date: Fri, 2 Apr 2021 22:07:35 +0000 Subject: [PATCH] Guide the user when libtorrent cannot be imported Libtorrent is required to play videos but its installation is still manual so now a message is displayed when libtorrent could not be imported instead of having a "service could not start" error at Kodi startup. The message contains a link to a page which explains how to install libtorrent. It will be displayed when: * the add-on starts * the user selects a video to play (including when called externally) Other additions: * Create a kodi_utils module to centralize some calls to the Kodi API * Add license information in the header of the files * Ignore some files in Git (python cache and Mac OS system file) --- .gitignore | 3 + peertube.py | 111 +++++++++++++++++------------------- resources/__init__.py | 7 +++ resources/lib/__init__.py | 7 +++ resources/lib/kodi_utils.py | 76 ++++++++++++++++++++++++ service.py | 110 ++++++++++++++++++++++++----------- 6 files changed, 222 insertions(+), 92 deletions(-) create mode 100644 .gitignore create mode 100644 resources/__init__.py create mode 100644 resources/lib/__init__.py create mode 100644 resources/lib/kodi_utils.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..aec41d8 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +.DS_Store +*.pyo +*.pyc diff --git a/peertube.py b/peertube.py index 5795228..44ef4e1 100644 --- a/peertube.py +++ b/peertube.py @@ -1,13 +1,13 @@ -""" A Kodi Addon to play video hosted on the PeerTube service +# -*- coding: utf-8 -*- +""" + A Kodi add-on 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 + 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 sys @@ -26,17 +26,22 @@ import xbmcaddon import xbmcgui import xbmcplugin +from resources.lib.kodi_utils import debug, get_property, notif_error, \ + notif_info, notif_warning, open_dialog + class PeertubeAddon(): """ Main class of the addon """ + # URL of the page which explains how to install libtorrent + HELP_URL = 'https://link.infini.fr/peertube-kodi-libtorrent' + def __init__(self, plugin, plugin_id): """ Initialisation of the PeertubeAddon class :param plugin, plugin_id: str, int - :return: None """ # These 2 steps must be done first since the logging function requires @@ -46,8 +51,6 @@ class PeertubeAddon(): # 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 @@ -77,15 +80,21 @@ class PeertubeAddon(): else: self.video_filter = 'local' - return None + # 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 = \ + get_property('libtorrent_imported') == 'True' def debug(self, message): - """Log a message in Kodi's log with the level xbmc.LOGDEBUG + """Log a debug message - :param message: Message to log - :type message: str + :param str message: Message to log (will be prefixed with the add-on + name) """ - xbmc.log('{0}: {1}'.format(self.addon_name, message), xbmc.LOGDEBUG) + debug('{0}: {1}'.format(self.addon_name, message)) def query_peertube(self, req): """ @@ -104,10 +113,8 @@ class PeertubeAddon(): try: response.raise_for_status() except requests.HTTPError as e: - xbmcgui.Dialog().notification('Communication error', - 'Error when sending request {0}' - .format(req), - xbmcgui.NOTIFICATION_ERROR) + notif_error(title='Communication error', + message='Error when sending request {}'.format(req)) # If the JSON contains an 'error' key, print it error_details = data.get('error') if error_details is not None: @@ -336,7 +343,6 @@ class PeertubeAddon(): in the results :param start: string - :result: None """ # Show a 'Search videos' dialog @@ -346,7 +352,7 @@ class PeertubeAddon(): # Go back to main menu when user cancels if not search: - return None + return # Create the PeerTube REST API request for searching videos req = self.build_video_rest_api_request(search, start) @@ -356,10 +362,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) - return None + notif_warning(title='No videos found', + message='No videos found matching the query.') + return # Create array of xmbcgui.ListItem's listing = self.create_list(results, 'videos', start) @@ -368,15 +373,12 @@ class PeertubeAddon(): xbmcplugin.addDirectoryItems(self.plugin_id, listing, len(listing)) xbmcplugin.endOfDirectory(self.plugin_id) - return None - def browse_videos(self, start): """ Function to navigate through all the video published by a PeerTube instance :param start: string - :return: None """ # Create the PeerTube REST API request for listing videos @@ -392,13 +394,10 @@ class PeertubeAddon(): xbmcplugin.addDirectoryItems(self.plugin_id, listing, len(listing)) xbmcplugin.endOfDirectory(self.plugin_id) - 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 @@ -414,15 +413,12 @@ class PeertubeAddon(): xbmcplugin.addDirectoryItems(self.plugin_id, listing, len(listing)) xbmcplugin.endOfDirectory(self.plugin_id) - return None - 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 :param data: dict - :return: None """ self.debug( @@ -430,14 +426,19 @@ class PeertubeAddon(): self.play = 1 self.torrent_f = data['file'] - return None - def play_video(self, torrent_url): """ Start the torrent's download and play it while being downloaded :param torrent_url: str - :return: None """ + # 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: + open_dialog(title='Error: libtorrent could not be imported', + message='PeerTube cannot play videos without' + ' libtorrent.\nPlease follow the instructions' + ' at {}'.format(self.HELP_URL)) + return self.debug('Starting torrent download ({0})'.format(torrent_url)) @@ -456,11 +457,9 @@ class PeertubeAddon(): # Abort in case of timeout if timeout == 10: - xbmcgui.Dialog().notification('Download timeout', - 'Timeout fetching {}' - .format(torrent_url), - xbmcgui.NOTIFICATION_ERROR) - return None + notif_error(title='Download timeout', + message='Timeout fetching {}'.format(torrent_url)) + return else: # Wait a little before starting playing the torrent xbmc.sleep(3000) @@ -470,25 +469,19 @@ class PeertubeAddon(): 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://{}'.format(instance) - xbmcgui.Dialog().notification('Current instance changed', - 'Changed current instance to {0}' - .format(self.selected_inst), - xbmcgui.NOTIFICATION_INFO) + notif_info(title='Current instance changed', + message='Changed current instance to {0}' + .format(self.selected_inst)) self.debug('Changing currently selected instance to {0}' .format(self.selected_inst)) - return None - def build_kodi_url(self, parameters): """Build a Kodi URL based on the parameters. @@ -501,8 +494,6 @@ class PeertubeAddon(): def main_menu(self): """ Addon's main menu - :param: None - :return: None """ # Create a list for our items. @@ -529,14 +520,11 @@ class PeertubeAddon(): # Finish creating a virtual folder. xbmcplugin.endOfDirectory(self.plugin_id) - return None - def router(self, paramstring): """ Router function that calls other functions depending on the provided paramstring :param paramstring: dict - :return: None """ # Parse a URL-encoded paramstring to the dictionary of @@ -571,8 +559,13 @@ class PeertubeAddon(): # Kodi UI without any parameters self.main_menu() - return None - + # Display a warning if libtorrent could not be imported + if not self.libtorrent_imported: + 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)) if __name__ == '__main__': diff --git a/resources/__init__.py b/resources/__init__.py new file mode 100644 index 0000000..b37d783 --- /dev/null +++ b/resources/__init__.py @@ -0,0 +1,7 @@ +# -*- coding: utf-8 -*- +""" + Copyright (C) 2021 Thomas Bétous + + SPDX-License-Identifier: GPL-3.0-only + See LICENSE.txt for more information. +""" diff --git a/resources/lib/__init__.py b/resources/lib/__init__.py new file mode 100644 index 0000000..b37d783 --- /dev/null +++ b/resources/lib/__init__.py @@ -0,0 +1,7 @@ +# -*- coding: utf-8 -*- +""" + Copyright (C) 2021 Thomas Bétous + + SPDX-License-Identifier: GPL-3.0-only + See LICENSE.txt for more information. +""" diff --git a/resources/lib/kodi_utils.py b/resources/lib/kodi_utils.py new file mode 100644 index 0000000..7414f31 --- /dev/null +++ b/resources/lib/kodi_utils.py @@ -0,0 +1,76 @@ +# -*- coding: utf-8 -*- +""" + Utility functions to interact easily with Kodi + + Copyright (C) 2021 Thomas Bétous + + SPDX-License-Identifier: GPL-3.0-only + See LICENSE.txt for more information. +""" +import xbmc +import xbmcgui + + +def debug(message): + """Log a message in Kodi's log with the level xbmc.LOGDEBUG + + :param str message: Message to log + """ + xbmc.log(message, xbmc.LOGDEBUG) + +def get_property(name): + """Retrieve the value of a window property related to the add-on + + :param str name: name of the property which value will be retrieved (the + actual name of the property is prefixed with "peertube_") + :return: the value of the window property + :rtype: str + """ + return xbmcgui.Window(10000).getProperty('peertube_{}'.format(name)) + +def notif_error(title, message): + """Display a notification with the error icon + + :param str title: Title of the notification + :param str message: Message of the notification + """ + xbmcgui.Dialog().notification(heading=title, + message=message, + icon=xbmcgui.NOTIFICATION_ERROR) + +def notif_info(title, message): + """Display a notification with the info icon + + :param str title: Title of the notification + :param str message: Message of the notification + """ + xbmcgui.Dialog().notification(heading=title, + message=message, + icon=xbmcgui.NOTIFICATION_INFO) + +def notif_warning(title, message): + """Display a notification with the warning icon + + :param str title: Title of the notification + :param str message: Message of the notification + """ + xbmcgui.Dialog().notification(heading=title, + message=message, + icon=xbmcgui.NOTIFICATION_WARNING) + +def open_dialog(title, message): + """Open a dialog box with an "OK" button + + :param str title: Title of the box + :param str message: Message in the box + """ + xbmcgui.Dialog().ok(heading=title, line1=message) + +def set_property(name, value): + """Modify the value of a window property related to the add-on + + :param str name: Name of the property which value will be modified (the + actual name of the property is prefixed with "peertube_") + :param str value: New value of the property + """ + xbmcgui.Window(10000).setProperty('peertube_{}'.format(name), value) diff --git a/service.py b/service.py index b198a0d..e3053a0 100644 --- a/service.py +++ b/service.py @@ -1,24 +1,46 @@ -import libtorrent -import time, sys -import xbmc, xbmcvfs +# -*- coding: utf-8 -*- +""" + PeerTube service to download torrents in the background + + 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 from threading import Thread +import xbmc +import xbmcvfs + +from resources.lib.kodi_utils import debug, set_property class PeertubeDownloader(Thread): """ - A class to download peertube torrents in the background + A class to download PeerTube torrents in the background """ def __init__(self, url, temp_dir): """ - Initialise a PeertubeDownloader instance for downloading the torrent specified by url + Initialise a PeertubeDownloader instance for downloading the torrent + specified by url + :param url, temp_dir: str :return: None """ - Thread.__init__(self) + super(PeertubeDownloader, self).__init__(self) self.torrent = url self.temp_dir = temp_dir + def debug(self, message): + """Log a debug message + + :param str message: Message to log (will be prefixed with the name of + the class) + """ + debug('PeertubeDownloader: {}'.format(message)) + def run(self): """ Download the torrent specified by self.torrent @@ -26,51 +48,56 @@ class PeertubeDownloader(Thread): :return: None """ - xbmc.log('PeertubeDownloader: Opening bitTorent session', xbmc.LOGDEBUG) - # Open bitTorrent session + self.debug('Opening BitTorent session') + # Open BitTorrent session ses = libtorrent.session() ses.listen_on(6881, 6891) # Add torrent - xbmc.log('PeertubeDownloader: Adding torrent ' + self.torrent, xbmc.LOGDEBUG) + self.debug('Adding torrent {}'.format(self.torrent)) h = ses.add_torrent({'url': self.torrent, 'save_path': self.temp_dir}) # Set sequential mode to allow watching while downloading h.set_sequential_download(True) # Download torrent - xbmc.log('PeertubeDownloader: Downloading torrent ' + self.torrent, xbmc.LOGDEBUG) + self.debug('Downloading torrent {}'.format(self.torrent)) signal_sent = 0 while not h.is_seed(): xbmc.sleep(1000) s = h.status() - # Inform addon that all the metadata has been downloaded and that it may start playing the torrent + # Inform addon that all the metadata has been downloaded and that + # it may start playing the torrent if s.state >=3 and signal_sent == 0: - xbmc.log('PeertubeDownloader: Received all torrent metadata, notifying PeertubeAddon', xbmc.LOGDEBUG) + self.debug('Received all torrent metadata, notifying' + ' PeertubeAddon') i = h.torrent_file() f = self.temp_dir + i.name() AddonSignals.sendSignal('metadata_downloaded', {'file': f}) signal_sent = 1 - # Everything is done - return - class PeertubeService(): """ + Class used to run a service when Kodi starts """ def __init__(self): """ PeertubeService initialisation function """ - - xbmc.log('PeertubeService: Initialising', xbmc.LOGDEBUG) # Create our temporary directory - self.temp = xbmc.translatePath('special://temp') + '/plugin.video.peertube/' + self.temp = '{}{}'.format(xbmc.translatePath('special://temp'), + 'plugin.video.peertube/') if not xbmcvfs.exists(self.temp): xbmcvfs.mkdir(self.temp) - return + def debug(self, message): + """Log a debug message + + :param str message: Message to log (will be prefixed with the name of + the class) + """ + debug('PeertubeService: {}'.format(message)) def download_torrent(self, data): """ @@ -79,35 +106,52 @@ class PeertubeService(): :return: None """ - xbmc.log('PeertubeService: Received a start_download signal', xbmc.LOGDEBUG) - downloader = PeertubeDownloader(data['url'], self.temp) + self.debug('Received a start_download signal') + downloader = PeertubeDownloader(data['url'], self.temp) downloader.start() - - return def run(self): """ - Main loop of the PeertubeService class, registring the start_download signal to start a - peertubeDownloader thread when needed, and exit when Kodi is shutting down + Main loop of the PeertubeService class + + It registers the start_download signal to start a PeertubeDownloader + thread when needed, and exit when Kodi is shutting down. """ - # Launch the download_torrent callback function when the 'start_download' signal is received - AddonSignals.registerSlot('plugin.video.peertube', 'start_download', self.download_torrent) + self.debug('Starting') + + # Launch the download_torrent callback function when the + # 'start_download' signal is received + AddonSignals.registerSlot('plugin.video.peertube', + 'start_download', + self.download_torrent) # Monitor Kodi's shutdown signal - xbmc.log('PeertubeService: service started, Waiting for signals', xbmc.LOGDEBUG) + self.debug('Service started, waiting for signals') monitor = xbmc.Monitor() while not monitor.abortRequested(): if monitor.waitForAbort(1): # Abort was requested while waiting. We must exit # TODO: Clean temporary directory + self.debug('Exiting') break - - return if __name__ == '__main__': - # Start a peertubeService instance - xbmc.log('PeertubeService: Starting', xbmc.LOGDEBUG) + # Create a PeertubeService instance service = PeertubeService() + + # Import libtorrent here to manage when the library is not installed + try: + import libtorrent + LIBTORRENT_IMPORTED = True + except ImportError as exception: + LIBTORRENT_IMPORTED = False + service.debug('The libtorrent library could not be imported because of' + ' the following error:\n{}'.format(exception)) + + # Save whether libtorrent could be imported as a window property so that + # this information can be retrieved by the add-on + set_property('libtorrent_imported', str(LIBTORRENT_IMPORTED)) + + # Start the service service.run() - xbmc.log('PeertubeService: Exiting', xbmc.LOGDEBUG)