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:
parent
7a21bd92ac
commit
4346178db9
|
@ -0,0 +1,3 @@
|
|||
.DS_Store
|
||||
*.pyo
|
||||
*.pyc
|
111
peertube.py
111
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__':
|
||||
|
||||
|
|
|
@ -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.
|
||||
"""
|
|
@ -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.
|
||||
"""
|
|
@ -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)
|
110
service.py
110
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)
|
||||
|
|
Loading…
Reference in New Issue