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)
This commit is contained in:
Thomas 2021-04-02 22:07:35 +00:00 committed by Thomas Bétous
parent 7a21bd92ac
commit 4346178db9
6 changed files with 222 additions and 92 deletions

3
.gitignore vendored Normal file
View File

@ -0,0 +1,3 @@
.DS_Store
*.pyo
*.pyc

View File

@ -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/) (http://joinpeertube.org/)
TODO: Copyright (C) 2018 Cyrille Bollu
- Delete downloaded files by default Copyright (C) 2021 Thomas Bétous
- Allow people to choose if they want to keep their download after watching?
- Do sanity checks on received data SPDX-License-Identifier: GPL-3.0-only
- Handle languages better (with .po files) See LICENSE.txt for more information.
- Get the best quality torrent given settings and/or available bandwidth
See how they do that in the peerTube client's code
""" """
import sys import sys
@ -26,17 +26,22 @@ import xbmcaddon
import xbmcgui import xbmcgui
import xbmcplugin import xbmcplugin
from resources.lib.kodi_utils import debug, get_property, notif_error, \
notif_info, notif_warning, open_dialog
class PeertubeAddon(): class PeertubeAddon():
""" """
Main class of the addon 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): def __init__(self, plugin, plugin_id):
""" """
Initialisation of the PeertubeAddon class Initialisation of the PeertubeAddon class
:param plugin, plugin_id: str, int :param plugin, plugin_id: str, int
:return: None
""" """
# These 2 steps must be done first since the logging function requires # These 2 steps must be done first since the logging function requires
@ -46,8 +51,6 @@ class PeertubeAddon():
# Get the add-on name # Get the add-on name
self.addon_name = addon.getAddonInfo('name') self.addon_name = addon.getAddonInfo('name')
self.debug('Initialising')
# Save addon URL and ID # Save addon URL and ID
self.plugin_url = plugin self.plugin_url = plugin
self.plugin_id = plugin_id self.plugin_id = plugin_id
@ -77,15 +80,21 @@ class PeertubeAddon():
else: else:
self.video_filter = 'local' 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): 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 :param str message: Message to log (will be prefixed with the add-on
:type message: str 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): def query_peertube(self, req):
""" """
@ -104,10 +113,8 @@ class PeertubeAddon():
try: try:
response.raise_for_status() response.raise_for_status()
except requests.HTTPError as e: except requests.HTTPError as e:
xbmcgui.Dialog().notification('Communication error', notif_error(title='Communication error',
'Error when sending request {0}' message='Error when sending request {}'.format(req))
.format(req),
xbmcgui.NOTIFICATION_ERROR)
# If the JSON contains an 'error' key, print it # If the JSON contains an 'error' key, print it
error_details = data.get('error') error_details = data.get('error')
if error_details is not None: if error_details is not None:
@ -336,7 +343,6 @@ class PeertubeAddon():
in the results in the results
:param start: string :param start: string
:result: None
""" """
# Show a 'Search videos' dialog # Show a 'Search videos' dialog
@ -346,7 +352,7 @@ class PeertubeAddon():
# Go back to main menu when user cancels # Go back to main menu when user cancels
if not search: if not search:
return None return
# Create the PeerTube REST API request for searching videos # Create the PeerTube REST API request for searching videos
req = self.build_video_rest_api_request(search, start) req = self.build_video_rest_api_request(search, start)
@ -356,10 +362,9 @@ class PeertubeAddon():
# Exit directly when no result is found # Exit directly when no result is found
if not results: if not results:
xbmcgui.Dialog().notification('No videos found', notif_warning(title='No videos found',
'No videos found matching query', message='No videos found matching the query.')
xbmcgui.NOTIFICATION_WARNING) return
return None
# Create array of xmbcgui.ListItem's # Create array of xmbcgui.ListItem's
listing = self.create_list(results, 'videos', start) listing = self.create_list(results, 'videos', start)
@ -368,15 +373,12 @@ class PeertubeAddon():
xbmcplugin.addDirectoryItems(self.plugin_id, listing, len(listing)) xbmcplugin.addDirectoryItems(self.plugin_id, listing, len(listing))
xbmcplugin.endOfDirectory(self.plugin_id) xbmcplugin.endOfDirectory(self.plugin_id)
return None
def browse_videos(self, start): def browse_videos(self, start):
""" """
Function to navigate through all the video published by a PeerTube Function to navigate through all the video published by a PeerTube
instance instance
:param start: string :param start: string
:return: None
""" """
# Create the PeerTube REST API request for listing videos # Create the PeerTube REST API request for listing videos
@ -392,13 +394,10 @@ class PeertubeAddon():
xbmcplugin.addDirectoryItems(self.plugin_id, listing, len(listing)) xbmcplugin.addDirectoryItems(self.plugin_id, listing, len(listing))
xbmcplugin.endOfDirectory(self.plugin_id) xbmcplugin.endOfDirectory(self.plugin_id)
return None
def browse_instances(self, start): def browse_instances(self, start):
""" """
Function to navigate through all PeerTube instances Function to navigate through all PeerTube instances
:param start: str :param start: str
:return: None
""" """
# Create the PeerTube REST API request for browsing PeerTube instances # 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.addDirectoryItems(self.plugin_id, listing, len(listing))
xbmcplugin.endOfDirectory(self.plugin_id) xbmcplugin.endOfDirectory(self.plugin_id)
return None
def play_video_continue(self, data): def play_video_continue(self, data):
""" """
Callback function to let the play_video function resume when the Callback function to let the play_video function resume when the
PeertubeDownloader has downloaded all the torrent's metadata PeertubeDownloader has downloaded all the torrent's metadata
:param data: dict :param data: dict
:return: None
""" """
self.debug( self.debug(
@ -430,14 +426,19 @@ class PeertubeAddon():
self.play = 1 self.play = 1
self.torrent_f = data['file'] self.torrent_f = data['file']
return None
def play_video(self, torrent_url): def play_video(self, torrent_url):
""" """
Start the torrent's download and play it while being downloaded Start the torrent's download and play it while being downloaded
:param torrent_url: str :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)) self.debug('Starting torrent download ({0})'.format(torrent_url))
@ -456,11 +457,9 @@ class PeertubeAddon():
# Abort in case of timeout # Abort in case of timeout
if timeout == 10: if timeout == 10:
xbmcgui.Dialog().notification('Download timeout', notif_error(title='Download timeout',
'Timeout fetching {}' message='Timeout fetching {}'.format(torrent_url))
.format(torrent_url), return
xbmcgui.NOTIFICATION_ERROR)
return None
else: else:
# Wait a little before starting playing the torrent # Wait a little before starting playing the torrent
xbmc.sleep(3000) xbmc.sleep(3000)
@ -470,25 +469,19 @@ class PeertubeAddon():
play_item = xbmcgui.ListItem(path=self.torrent_f) play_item = xbmcgui.ListItem(path=self.torrent_f)
xbmcplugin.setResolvedUrl(self.plugin_id, True, listitem=play_item) xbmcplugin.setResolvedUrl(self.plugin_id, True, listitem=play_item)
return None
def select_instance(self, instance): def select_instance(self, instance):
""" """
Change currently selected instance to 'instance' parameter Change currently selected instance to 'instance' parameter
:param instance: str :param instance: str
:return: None
""" """
self.selected_inst = 'https://{}'.format(instance) self.selected_inst = 'https://{}'.format(instance)
xbmcgui.Dialog().notification('Current instance changed', notif_info(title='Current instance changed',
'Changed current instance to {0}' message='Changed current instance to {0}'
.format(self.selected_inst), .format(self.selected_inst))
xbmcgui.NOTIFICATION_INFO)
self.debug('Changing currently selected instance to {0}' self.debug('Changing currently selected instance to {0}'
.format(self.selected_inst)) .format(self.selected_inst))
return None
def build_kodi_url(self, parameters): def build_kodi_url(self, parameters):
"""Build a Kodi URL based on the parameters. """Build a Kodi URL based on the parameters.
@ -501,8 +494,6 @@ class PeertubeAddon():
def main_menu(self): def main_menu(self):
""" """
Addon's main menu Addon's main menu
:param: None
:return: None
""" """
# Create a list for our items. # Create a list for our items.
@ -529,14 +520,11 @@ class PeertubeAddon():
# Finish creating a virtual folder. # Finish creating a virtual folder.
xbmcplugin.endOfDirectory(self.plugin_id) xbmcplugin.endOfDirectory(self.plugin_id)
return None
def router(self, paramstring): def router(self, paramstring):
""" """
Router function that calls other functions Router function that calls other functions
depending on the provided paramstring depending on the provided paramstring
:param paramstring: dict :param paramstring: dict
:return: None
""" """
# Parse a URL-encoded paramstring to the dictionary of # Parse a URL-encoded paramstring to the dictionary of
@ -571,8 +559,13 @@ class PeertubeAddon():
# Kodi UI without any parameters # Kodi UI without any parameters
self.main_menu() 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__': if __name__ == '__main__':

7
resources/__init__.py Normal file
View File

@ -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.
"""

View File

@ -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.
"""

View File

@ -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)

View File

@ -1,24 +1,46 @@
import libtorrent # -*- coding: utf-8 -*-
import time, sys """
import xbmc, xbmcvfs 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 import AddonSignals
from threading import Thread from threading import Thread
import xbmc
import xbmcvfs
from resources.lib.kodi_utils import debug, set_property
class PeertubeDownloader(Thread): 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): 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 :param url, temp_dir: str
:return: None :return: None
""" """
Thread.__init__(self) super(PeertubeDownloader, self).__init__(self)
self.torrent = url self.torrent = url
self.temp_dir = temp_dir 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): def run(self):
""" """
Download the torrent specified by self.torrent Download the torrent specified by self.torrent
@ -26,51 +48,56 @@ class PeertubeDownloader(Thread):
:return: None :return: None
""" """
xbmc.log('PeertubeDownloader: Opening bitTorent session', xbmc.LOGDEBUG) self.debug('Opening BitTorent session')
# Open bitTorrent session # Open BitTorrent session
ses = libtorrent.session() ses = libtorrent.session()
ses.listen_on(6881, 6891) ses.listen_on(6881, 6891)
# Add torrent # 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}) h = ses.add_torrent({'url': self.torrent, 'save_path': self.temp_dir})
# Set sequential mode to allow watching while downloading # Set sequential mode to allow watching while downloading
h.set_sequential_download(True) h.set_sequential_download(True)
# Download torrent # Download torrent
xbmc.log('PeertubeDownloader: Downloading torrent ' + self.torrent, xbmc.LOGDEBUG) self.debug('Downloading torrent {}'.format(self.torrent))
signal_sent = 0 signal_sent = 0
while not h.is_seed(): while not h.is_seed():
xbmc.sleep(1000) xbmc.sleep(1000)
s = h.status() 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: 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() i = h.torrent_file()
f = self.temp_dir + i.name() f = self.temp_dir + i.name()
AddonSignals.sendSignal('metadata_downloaded', {'file': f}) AddonSignals.sendSignal('metadata_downloaded', {'file': f})
signal_sent = 1 signal_sent = 1
# Everything is done
return
class PeertubeService(): class PeertubeService():
""" """
Class used to run a service when Kodi starts
""" """
def __init__(self): def __init__(self):
""" """
PeertubeService initialisation function PeertubeService initialisation function
""" """
xbmc.log('PeertubeService: Initialising', xbmc.LOGDEBUG)
# Create our temporary directory # 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): if not xbmcvfs.exists(self.temp):
xbmcvfs.mkdir(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): def download_torrent(self, data):
""" """
@ -79,35 +106,52 @@ class PeertubeService():
:return: None :return: None
""" """
xbmc.log('PeertubeService: Received a start_download signal', xbmc.LOGDEBUG) self.debug('Received a start_download signal')
downloader = PeertubeDownloader(data['url'], self.temp) downloader = PeertubeDownloader(data['url'], self.temp)
downloader.start() downloader.start()
return
def run(self): def run(self):
""" """
Main loop of the PeertubeService class, registring the start_download signal to start a Main loop of the PeertubeService class
peertubeDownloader thread when needed, and exit when Kodi is shutting down
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 self.debug('Starting')
AddonSignals.registerSlot('plugin.video.peertube', 'start_download', self.download_torrent)
# 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 # 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() monitor = xbmc.Monitor()
while not monitor.abortRequested(): while not monitor.abortRequested():
if monitor.waitForAbort(1): if monitor.waitForAbort(1):
# Abort was requested while waiting. We must exit # Abort was requested while waiting. We must exit
# TODO: Clean temporary directory # TODO: Clean temporary directory
self.debug('Exiting')
break break
return
if __name__ == '__main__': if __name__ == '__main__':
# Start a peertubeService instance # Create a PeertubeService instance
xbmc.log('PeertubeService: Starting', xbmc.LOGDEBUG)
service = PeertubeService() 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() service.run()
xbmc.log('PeertubeService: Exiting', xbmc.LOGDEBUG)