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/)
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__':

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