Backport changes from 89675dfb
This commit is contained in:
parent
108fea515d
commit
506f74a9e2
|
@ -1,15 +1,15 @@
|
||||||
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||||
<addon id="plugin.video.peertube" name="PeerTube" version="1.3.0-dev" provider-name="Cyrille B. + Thomas B.">
|
<addon id="plugin.video.peertube" name="PeerTube" version="1.3.0-alpha" provider-name="Cyrille B. + Thomas B.">
|
||||||
<requires>
|
<requires>
|
||||||
<import addon="xbmc.python" version="3.0.0"/>
|
<import addon="xbmc.python" version="3.0.0"/>
|
||||||
<!--<import addon="script.module.addon.signals" version="0.0.6"/>-->
|
<import addon="script.module.addon.signals" version="0.0.6"/>
|
||||||
<import addon="script.module.requests" version="2.25.1"/>
|
<import addon="script.module.requests" version="2.25.1"/>
|
||||||
<import addon="vfs.libtorrent"/>
|
<import addon="vfs.libtorrent"/>
|
||||||
</requires>
|
</requires>
|
||||||
<extension point="xbmc.python.pluginsource" library="main.py">
|
<extension point="xbmc.python.pluginsource" library="main.py">
|
||||||
<provides>video</provides>
|
<provides>video</provides>
|
||||||
</extension>
|
</extension>
|
||||||
<!--<extension point="xbmc.service" library="service.py"/>-->
|
<extension point="xbmc.service" library="service.py"/>
|
||||||
<extension point="xbmc.addon.metadata">
|
<extension point="xbmc.addon.metadata">
|
||||||
<summary lang="de_DE">Add-on für PeerTube</summary>
|
<summary lang="de_DE">Add-on für PeerTube</summary>
|
||||||
<description lang="de_DE">PeerTube ist eine kostenlose, dezentralisierte und föderierte Alternative zu anderen Videoplattformen (wie YouTube).</description>
|
<description lang="de_DE">PeerTube ist eine kostenlose, dezentralisierte und föderierte Alternative zu anderen Videoplattformen (wie YouTube).</description>
|
||||||
|
|
|
@ -107,6 +107,16 @@ msgid "If the preferred resolution is not available for a given video,"
|
||||||
" preference will be used."
|
" preference will be used."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
msgctxt "#30020"
|
||||||
|
msgid "Size of the initial chunk to download (seconds)"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgctxt "#30021"
|
||||||
|
msgid "Number of seconds in the video to download before starting playback."
|
||||||
|
" You should adapt this value depending on the power of your machine and the"
|
||||||
|
" speed of your internet connection."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
# -----------------------------------
|
# -----------------------------------
|
||||||
# Other strings (from 30400 to 30999)
|
# Other strings (from 30400 to 30999)
|
||||||
# -----------------------------------
|
# -----------------------------------
|
||||||
|
@ -116,8 +126,8 @@ msgid "PeerTube service started"
|
||||||
msgstr "PeerTube-Dienst gestartet"
|
msgstr "PeerTube-Dienst gestartet"
|
||||||
|
|
||||||
msgctxt "#30401"
|
msgctxt "#30401"
|
||||||
msgid "Torrents can now be downloaded."
|
msgid "You may now start playing videos."
|
||||||
msgstr "Torrents können jetzt heruntergeladen werden."
|
msgstr ""
|
||||||
|
|
||||||
msgctxt "#30402"
|
msgctxt "#30402"
|
||||||
msgid "Request error"
|
msgid "Request error"
|
||||||
|
@ -159,13 +169,9 @@ msgctxt "#30411"
|
||||||
msgid "No videos found matching the keywords '{}'"
|
msgid "No videos found matching the keywords '{}'"
|
||||||
msgstr "Keine Videos zu den Stichworten gefunden '{}'"
|
msgstr "Keine Videos zu den Stichworten gefunden '{}'"
|
||||||
|
|
||||||
msgctxt "#30412"
|
# 30412 is not used anymore
|
||||||
msgid "Error: libtorrent could not be imported"
|
|
||||||
msgstr "Fehler: libtorrent konnte nicht importiert werden"
|
|
||||||
|
|
||||||
msgctxt "#30413"
|
# 30413 is not used anymore
|
||||||
msgid "PeerTube cannot play videos without libtorrent\nPlease follow the instructions at {}"
|
|
||||||
msgstr "PeerTube kann keine Videos ohne libtorrent abspielen\nBitte folgen Sie den Anweisungen unter {}"
|
|
||||||
|
|
||||||
# 30414 is not used anymore
|
# 30414 is not used anymore
|
||||||
|
|
||||||
|
@ -187,9 +193,7 @@ msgctxt "#30419"
|
||||||
msgid "{} is now the selected instance."
|
msgid "{} is now the selected instance."
|
||||||
msgstr "{} ist nun die ausgewählte Instanz."
|
msgstr "{} ist nun die ausgewählte Instanz."
|
||||||
|
|
||||||
msgctxt "#30420"
|
# 30420 is not used anymore
|
||||||
msgid "You can still browse and search videos but you will not be able to play them (except live videos).\nPlease follow the instructions at {}"
|
|
||||||
msgstr "Sie können weiterhin Videos durchsuchen und suchen, aber Sie können sie nicht abspielen (außer Live-Videos).\nBefolgen Sie bitte die Anweisungen unter {}"
|
|
||||||
|
|
||||||
msgctxt "#30421"
|
msgctxt "#30421"
|
||||||
msgid "Download error"
|
msgid "Download error"
|
||||||
|
@ -198,3 +202,11 @@ msgstr ""
|
||||||
msgctxt "#30422"
|
msgctxt "#30422"
|
||||||
msgid "Error when trying to download the video. Check the log for more information."
|
msgid "Error when trying to download the video. Check the log for more information."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
msgctxt "#30423"
|
||||||
|
msgid "Playback error"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgctxt "#30424"
|
||||||
|
msgid "When the playback started, the portion of the video that was downloaded was probably not big enough. Do you want to try again to play the video?\n(If this error occurs often you should increase the initial wait time in the settings.)"
|
||||||
|
msgstr ""
|
||||||
|
|
|
@ -40,6 +40,7 @@ msgstr ""
|
||||||
#msgctxt "#30004"
|
#msgctxt "#30004"
|
||||||
#msgid "Delete downloaded videos when Kodi exits"
|
#msgid "Delete downloaded videos when Kodi exits"
|
||||||
#msgstr ""
|
#msgstr ""
|
||||||
|
#TODO: remove this string?
|
||||||
|
|
||||||
msgctxt "#30005"
|
msgctxt "#30005"
|
||||||
msgid "List only videos with scope"
|
msgid "List only videos with scope"
|
||||||
|
@ -107,6 +108,16 @@ msgid "If the preferred resolution is not available for a given video,"
|
||||||
" preference will be used."
|
" preference will be used."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
msgctxt "#30020"
|
||||||
|
msgid "Size of the initial chunk to download (seconds)"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgctxt "#30021"
|
||||||
|
msgid "Number of seconds in the video to download before starting playback."
|
||||||
|
" You should adapt this value depending on the power of your machine and the"
|
||||||
|
" speed of your internet connection."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
# -----------------------------------
|
# -----------------------------------
|
||||||
# Other strings (from 30400 to 30999)
|
# Other strings (from 30400 to 30999)
|
||||||
# -----------------------------------
|
# -----------------------------------
|
||||||
|
@ -116,7 +127,7 @@ msgid "PeerTube service started"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
msgctxt "#30401"
|
msgctxt "#30401"
|
||||||
msgid "Torrents can now be downloaded."
|
msgid "You may now start playing videos."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
msgctxt "#30402"
|
msgctxt "#30402"
|
||||||
|
@ -159,13 +170,9 @@ msgctxt "#30411"
|
||||||
msgid "No videos found matching the keywords '{}'"
|
msgid "No videos found matching the keywords '{}'"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
msgctxt "#30412"
|
# 30412 is not used anymore
|
||||||
msgid "Error: libtorrent could not be imported"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgctxt "#30413"
|
# 30413 is not used anymore
|
||||||
msgid "PeerTube cannot play videos without libtorrent\nPlease follow the instructions at {}"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
# 30414 is not used anymore
|
# 30414 is not used anymore
|
||||||
|
|
||||||
|
@ -187,9 +194,7 @@ msgctxt "#30419"
|
||||||
msgid "{} is now the selected instance."
|
msgid "{} is now the selected instance."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
msgctxt "#30420"
|
# 30420 is not used anymore
|
||||||
msgid "You can still browse and search videos but you will not be able to play them (except live videos).\nPlease follow the instructions at {}"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgctxt "#30421"
|
msgctxt "#30421"
|
||||||
msgid "Download error"
|
msgid "Download error"
|
||||||
|
@ -198,3 +203,11 @@ msgstr ""
|
||||||
msgctxt "#30422"
|
msgctxt "#30422"
|
||||||
msgid "Error when trying to download the video. Check the log for more information."
|
msgid "Error when trying to download the video. Check the log for more information."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
msgctxt "#30423"
|
||||||
|
msgid "Playback error"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgctxt "#30424"
|
||||||
|
msgid "When the playback started, the portion of the video that was downloaded was probably not big enough. Do you want to try again to play the video?\n(If this error occurs often you should increase the initial wait time in the settings.)"
|
||||||
|
msgstr ""
|
|
@ -113,6 +113,18 @@ msgstr "Si la résolution préférée n'est pas disponible pour une vidéo donn
|
||||||
"Si elle n'est pas disponible, la résolution plus haute et la plus proche de"
|
"Si elle n'est pas disponible, la résolution plus haute et la plus proche de"
|
||||||
" votre préférence sera utilisée."
|
" votre préférence sera utilisée."
|
||||||
|
|
||||||
|
msgctxt "#30020"
|
||||||
|
msgid "Size of the initial chunk to download (seconds)"
|
||||||
|
msgstr "Taille du premier morceau à télécharger (secondes)"
|
||||||
|
|
||||||
|
msgctxt "#30021"
|
||||||
|
msgid "Number of seconds in the video to download before starting playback."
|
||||||
|
" You should adapt this value depending on the power of your machine and the"
|
||||||
|
" speed of your internet connection."
|
||||||
|
msgstr "Nombre de secondes dans la vidéo à télécharger avant de lancer la"
|
||||||
|
"lecture. Vous devriez ajuster cette valeur en fonction de la puissance de"
|
||||||
|
" votre machine et de la vitesse de votre connexion internet."
|
||||||
|
|
||||||
# -----------------------------------
|
# -----------------------------------
|
||||||
# Other strings (from 30400 to 30999)
|
# Other strings (from 30400 to 30999)
|
||||||
# -----------------------------------
|
# -----------------------------------
|
||||||
|
@ -122,8 +134,8 @@ msgid "PeerTube service started"
|
||||||
msgstr "Le service PeerTube a démarré"
|
msgstr "Le service PeerTube a démarré"
|
||||||
|
|
||||||
msgctxt "#30401"
|
msgctxt "#30401"
|
||||||
msgid "Torrents can now be downloaded."
|
msgid "You may now start playing videos."
|
||||||
msgstr "Les torrents peuvent maintenant être téléchargés."
|
msgstr "Vous pouvez maintenant lire des vidéos."
|
||||||
|
|
||||||
msgctxt "#30402"
|
msgctxt "#30402"
|
||||||
msgid "Request error"
|
msgid "Request error"
|
||||||
|
@ -165,13 +177,9 @@ msgctxt "#30411"
|
||||||
msgid "No videos found matching the keywords '{}'"
|
msgid "No videos found matching the keywords '{}'"
|
||||||
msgstr "Aucune vidéo ne correspond aux mots-clés '{}'"
|
msgstr "Aucune vidéo ne correspond aux mots-clés '{}'"
|
||||||
|
|
||||||
msgctxt "#30412"
|
# 30412 is not used anymore
|
||||||
msgid "Error: libtorrent could not be imported"
|
|
||||||
msgstr "Erreur: libtorrent n'a pas pu être importé"
|
|
||||||
|
|
||||||
msgctxt "#30413"
|
# 30413 is not used anymore
|
||||||
msgid "PeerTube cannot play videos without libtorrent\nPlease follow the instructions at {}"
|
|
||||||
msgstr "PeerTube ne peut pas lire de vidéos sans libtorrent.\nMerci de suivre les instructions depuis {}"
|
|
||||||
|
|
||||||
# 30414 is not used anymore
|
# 30414 is not used anymore
|
||||||
|
|
||||||
|
@ -193,9 +201,7 @@ msgctxt "#30419"
|
||||||
msgid "{} is now the selected instance."
|
msgid "{} is now the selected instance."
|
||||||
msgstr "{} est maintenant l'instance sélectionnée."
|
msgstr "{} est maintenant l'instance sélectionnée."
|
||||||
|
|
||||||
msgctxt "#30420"
|
# 30420 is not used anymore
|
||||||
msgid "You can still browse and search videos but you will not be able to play them (except live videos).\nPlease follow the instructions at {}"
|
|
||||||
msgstr "Vous pouvez parcourir ou chercher des vidéos mais vous ne pourrez pas les lire (sauf les live).\nMerci de suivre les instructions depuis {}"
|
|
||||||
|
|
||||||
msgctxt "#30421"
|
msgctxt "#30421"
|
||||||
msgid "Download error"
|
msgid "Download error"
|
||||||
|
@ -204,3 +210,11 @@ msgstr "Erreur de téléchargement"
|
||||||
msgctxt "#30422"
|
msgctxt "#30422"
|
||||||
msgid "Error when trying to download the video. Check the log for more information."
|
msgid "Error when trying to download the video. Check the log for more information."
|
||||||
msgstr "Une erreur est survenue pendant le téléchargement de la vidéo. Voir le journal pour plus d'informations."
|
msgstr "Une erreur est survenue pendant le téléchargement de la vidéo. Voir le journal pour plus d'informations."
|
||||||
|
|
||||||
|
msgctxt "#30423"
|
||||||
|
msgid "Playback error"
|
||||||
|
msgstr "Erreur de lecture"
|
||||||
|
|
||||||
|
msgctxt "#30424"
|
||||||
|
msgid "When the playback started, the portion of the video that was downloaded was probably not big enough. Do you want to try again to play the video?\n(If this error occurs often you should increase the initial wait time in the settings.)"
|
||||||
|
msgstr "Quand la lecture a démarré, la portion de la vidéo qui avait été téléchargée était certainement trop petite. Voulez-vous essayer de lire la vidéo à nouveau ?\n(Si cette erreur survient souvent vous devriez augmenter le délai d'attente initial dans les paramètres.)"
|
||||||
|
|
|
@ -15,6 +15,7 @@ from urllib.parse import quote_plus
|
||||||
from resources.lib.kodi_utils import kodi
|
from resources.lib.kodi_utils import kodi
|
||||||
from resources.lib.peertube import PeerTube, list_instances
|
from resources.lib.peertube import PeerTube, list_instances
|
||||||
|
|
||||||
|
import AddonSignals
|
||||||
import xbmcvfs
|
import xbmcvfs
|
||||||
|
|
||||||
class PeerTubeAddon():
|
class PeerTubeAddon():
|
||||||
|
@ -22,9 +23,6 @@ class PeerTubeAddon():
|
||||||
Main class used by the add-on
|
Main class used by the add-on
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# URL of the page which explains how to install libtorrent
|
|
||||||
HELP_URL = "https://link.infini.fr/libtorrent-peertube-kodi"
|
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
"""Initialize parameters and create a PeerTube instance"""
|
"""Initialize parameters and create a PeerTube instance"""
|
||||||
|
|
||||||
|
@ -35,19 +33,6 @@ class PeerTubeAddon():
|
||||||
self.preferred_resolution = \
|
self.preferred_resolution = \
|
||||||
int(kodi.get_setting("preferred_resolution"))
|
int(kodi.get_setting("preferred_resolution"))
|
||||||
|
|
||||||
# Nothing to play at initialisation
|
|
||||||
self.play = False
|
|
||||||
self.torrent_name = ""
|
|
||||||
self.torrent_file = ""
|
|
||||||
|
|
||||||
# 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 = \
|
|
||||||
kodi.get_property("libtorrent_imported") == "True"
|
|
||||||
|
|
||||||
# Create a PeerTube object to send requests: settings which are used
|
# Create a PeerTube object to send requests: settings which are used
|
||||||
# only by this object are directly retrieved from the settings
|
# only by this object are directly retrieved from the settings
|
||||||
self.peertube = PeerTube(
|
self.peertube = PeerTube(
|
||||||
|
@ -210,46 +195,35 @@ class PeerTubeAddon():
|
||||||
|
|
||||||
return next_page_item
|
return next_page_item
|
||||||
|
|
||||||
def _get_video_url(self, video_id, instance=None):
|
def _get_url_with_resolution(self, list_of_url_and_resolutions):
|
||||||
"""Return the URL of a video and its type (live or not)
|
"""
|
||||||
|
Build the URL of the video
|
||||||
Find the URL of the video with the best possible quality matching
|
|
||||||
user's preferences.
|
PeerTube creates 1 URL for each resolution so we browse all the
|
||||||
The information whether the video is live or not will also be returned.
|
available resolutions and select the best possible quality matching
|
||||||
|
user's preferences.
|
||||||
:param str video_id: ID of the torrent linked with the video
|
If the preferred resolution cannot be found, the one just below will
|
||||||
:param str instance: PeerTube instance hosting the video (optional)
|
be used. If it is not possible the one just above we will be used.
|
||||||
:return: a boolean indicating if the video is a live stream and the URL
|
|
||||||
of the video (containing the resolution for non-live videos) as a
|
:param list list_of_url_and_resolutions: list of dict containing 2 keys:
|
||||||
string
|
the resolution and the associated URL.
|
||||||
:rtype: tuple
|
:return: the URL matching the selected resolution
|
||||||
|
:rtype: str
|
||||||
"""
|
"""
|
||||||
# Retrieve the information about the video including the different
|
|
||||||
# resolutions available
|
|
||||||
video_files = self.peertube.get_video_urls(video_id, instance=instance)
|
|
||||||
|
|
||||||
# Find the best resolution matching user's preferences
|
# Find the best resolution matching user's preferences
|
||||||
current_resolution = 0
|
current_resolution = 0
|
||||||
higher_resolution = -1
|
higher_resolution = -1
|
||||||
url = ""
|
url = None
|
||||||
is_live = False
|
for video in list_of_url_and_resolutions:
|
||||||
for video in video_files:
|
|
||||||
# Get the resolution
|
# Get the resolution
|
||||||
resolution = video.get("resolution")
|
resolution = video.get("resolution")
|
||||||
if resolution is None:
|
|
||||||
# If there is no resolution in the dict, then the video is a
|
|
||||||
# live stream: no need to find the best resolution as there is
|
|
||||||
# only 1 URL in this case
|
|
||||||
url = video["url"]
|
|
||||||
is_live = True
|
|
||||||
return (is_live, url)
|
|
||||||
if resolution == self.preferred_resolution:
|
if resolution == self.preferred_resolution:
|
||||||
# Stop directly when we find the exact same resolution as the
|
# Stop directly when we find the exact same resolution as the
|
||||||
# user's preferred one
|
# user's preferred one
|
||||||
kodi.debug("Found video with preferred resolution ({})"
|
kodi.debug("Found video with preferred resolution ({})"
|
||||||
.format(self.preferred_resolution))
|
.format(self.preferred_resolution))
|
||||||
url = video["url"]
|
return video["url"]
|
||||||
return (is_live, url)
|
|
||||||
elif (resolution < self.preferred_resolution
|
elif (resolution < self.preferred_resolution
|
||||||
and resolution > current_resolution):
|
and resolution > current_resolution):
|
||||||
# Otherwise, try to find the best one just below the user's
|
# Otherwise, try to find the best one just below the user's
|
||||||
|
@ -272,12 +246,12 @@ class PeerTubeAddon():
|
||||||
|
|
||||||
# When we didn't find a resolution equal or lower than the user's
|
# When we didn't find a resolution equal or lower than the user's
|
||||||
# preferred one, use the resolution just above the preferred one
|
# preferred one, use the resolution just above the preferred one
|
||||||
if not url:
|
if url is None:
|
||||||
kodi.debug("Using video with higher resolution as alternative ({})"
|
kodi.debug("Using video with higher resolution as alternative ({})"
|
||||||
.format(higher_resolution))
|
.format(higher_resolution))
|
||||||
url = backup_url
|
url = backup_url
|
||||||
|
|
||||||
return (is_live, url)
|
return url
|
||||||
|
|
||||||
def _home_page(self):
|
def _home_page(self):
|
||||||
"""Display the items of the home page of the add-on"""
|
"""Display the items of the home page of the add-on"""
|
||||||
|
@ -335,37 +309,85 @@ class PeerTubeAddon():
|
||||||
# Create the associated items in Kodi
|
# Create the associated items in Kodi
|
||||||
kodi.create_items_in_ui(list_of_videos)
|
kodi.create_items_in_ui(list_of_videos)
|
||||||
|
|
||||||
def _play_video(self, torrent_url):
|
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
|
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 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))
|
kodi.debug("Starting torrent download ({})".format(torrent_url))
|
||||||
|
|
||||||
# Download the torrent using vfs.libtorrent: the torrent URL must be
|
# Download the torrent using vfs.libtorrent: the torrent URL must be
|
||||||
# URL encoded to be correctly read by vfs.libtorrent
|
# URL-encoded to be correctly read by vfs.libtorrent
|
||||||
vfs_url = "torrent://{}".format(quote_plus(torrent_url))
|
vfs_url = "torrent://{}".format(quote_plus(torrent_url))
|
||||||
torrent = xbmcvfs.File(vfs_url)
|
torrent = xbmcvfs.File(vfs_url)
|
||||||
|
|
||||||
# Download the file
|
# Get information about the torrent
|
||||||
if(torrent.write("download")):
|
torrent_info = json.loads(torrent.read())
|
||||||
|
|
||||||
# Get information about the torrent
|
if torrent_info["nb_files"] > 1:
|
||||||
torrent_info = json.loads(torrent.read())
|
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
|
||||||
|
|
||||||
|
# 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
|
# Build the path of the downloaded file
|
||||||
self.torrent_file = os.path.join(torrent_info["save_path"],
|
torrent_file = os.path.join(torrent_info["save_path"],
|
||||||
torrent_info["files"][0]["path"])
|
torrent_info["files"][0]["path"])
|
||||||
|
|
||||||
if torrent_info["nb_files"] > 1:
|
# Send information about the torrent to the service so that it can
|
||||||
kodi.warning("There are more than 1 file in {} but only the"
|
# control the torrent later(e.g. pause the download when the
|
||||||
" first one will be played.".format(torrent_url))
|
# playback stops)
|
||||||
|
AddonSignals.sendSignal("torrent_information",
|
||||||
|
{
|
||||||
|
"run_url": kodi.build_kodi_url(kodi.get_run_parameters()),
|
||||||
|
"torrent_url": vfs_url
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
# Play the file
|
# Play the file
|
||||||
kodi.debug("Starting video playback of {}".format(self.torrent_file))
|
kodi.debug("Starting video playback of {}".format(torrent_file))
|
||||||
kodi.play(self.torrent_file)
|
kodi.play(torrent_file)
|
||||||
|
|
||||||
else:
|
else:
|
||||||
kodi.notif_error(title=kodi.get_string(30421),
|
kodi.notif_error(title=kodi.get_string(30421),
|
||||||
message=kodi.get_string(30422))
|
message=kodi.get_string(30422))
|
||||||
|
@ -414,24 +436,12 @@ class PeerTubeAddon():
|
||||||
# Browse PeerTube instances
|
# Browse PeerTube instances
|
||||||
self._browse_instances(int(params["start"]))
|
self._browse_instances(int(params["start"]))
|
||||||
elif action == "play_video":
|
elif action == "play_video":
|
||||||
# This action comes with the id of the video to play as
|
self._play_video(instance=params.get("instance"),
|
||||||
# parameter. The instance may also be in the parameters. Use
|
video_id=params.get("id"))
|
||||||
# these parameters to retrieve the complete URL of the video
|
|
||||||
# (containing the resolution) and the type of the video (live
|
|
||||||
# or not).
|
|
||||||
is_live, url = self._get_video_url(
|
|
||||||
instance=params.get("instance"),video_id=params.get("id"))
|
|
||||||
|
|
||||||
# Play the video (Kodi can play live videos (.m3u8) out of the
|
|
||||||
# box whereas torrents must first be downloaded)
|
|
||||||
if is_live:
|
|
||||||
kodi.play(url)
|
|
||||||
else:
|
|
||||||
self._play_video(url)
|
|
||||||
elif action == "select_instance":
|
elif action == "select_instance":
|
||||||
# Set the selected instance as the preferred instance
|
# Set the selected instance as the preferred instance
|
||||||
self._select_instance(params["url"])
|
self._select_instance(params["url"])
|
||||||
else:
|
else:
|
||||||
# Display the addon's main menu when the plugin is called from
|
# Display the addon's main menu when the plugin is called from
|
||||||
# Kodi UI without any parameters
|
# Kodi UI without any parameters
|
||||||
self._home_page()
|
self._home_page()
|
|
@ -206,6 +206,13 @@ class KodiUtils:
|
||||||
:param str message: Message in the box
|
:param str message: Message in the box
|
||||||
"""
|
"""
|
||||||
xbmcgui.Dialog().ok(heading=title, message=message)
|
xbmcgui.Dialog().ok(heading=title, message=message)
|
||||||
|
#TODO: this function is not used anymore: keep it?
|
||||||
|
|
||||||
|
def open_dialog_progress(self):
|
||||||
|
"""Open a dialog box with a progress bar
|
||||||
|
|
||||||
|
"""
|
||||||
|
return xbmcgui.DialogProgress()
|
||||||
|
|
||||||
def open_input_box(self, title):
|
def open_input_box(self, title):
|
||||||
"""Open a box for the user to input alphanumeric data
|
"""Open a box for the user to input alphanumeric data
|
||||||
|
@ -224,6 +231,16 @@ class KodiUtils:
|
||||||
return entered_string.decode("utf-8")
|
return entered_string.decode("utf-8")
|
||||||
else:
|
else:
|
||||||
return entered_string
|
return entered_string
|
||||||
|
#TODO: keep this code for Matrix?
|
||||||
|
|
||||||
|
|
||||||
|
def open_yes_no_dialog(self, title, message):
|
||||||
|
"""Open a dialog box with "Yes" "No" buttons
|
||||||
|
|
||||||
|
:param str title: Title of the box
|
||||||
|
:param str message: Message in the box
|
||||||
|
"""
|
||||||
|
return xbmcgui.Dialog().yesno(heading=title, message=message)
|
||||||
|
|
||||||
def play(self, url):
|
def play(self, url):
|
||||||
"""Play the media behind the URL
|
"""Play the media behind the URL
|
||||||
|
@ -290,4 +307,4 @@ class KodiUtils:
|
||||||
|
|
||||||
xbmc.log("[{}] {}".format(prefix, message), xbmc.LOGWARNING)
|
xbmc.log("[{}] {}".format(prefix, message), xbmc.LOGWARNING)
|
||||||
|
|
||||||
kodi = KodiUtils()
|
kodi = KodiUtils()
|
|
@ -142,31 +142,44 @@ class PeerTube:
|
||||||
sort_methods = ["likes", "views"]
|
sort_methods = ["likes", "views"]
|
||||||
return sort_methods[int(kodi.get_setting("video_sort_method"))]
|
return sort_methods[int(kodi.get_setting("video_sort_method"))]
|
||||||
|
|
||||||
def get_video_urls(self, video_id, instance=None):
|
def get_video_info(self, video_id, instance=None):
|
||||||
"""Return the URLs of a video
|
"""Return the info of a video in a simple form
|
||||||
|
|
||||||
PeerTube creates 1 URL for each resolution of a video so this method
|
Get the information of the video from the PeerTube instance and
|
||||||
returns a list of URL/resolution pairs. In the case of a live video,
|
preprocess some of it so that it can be easily used outside of this
|
||||||
only an URL will be returned (no resolution).
|
class. The returned information are:
|
||||||
|
- the type of the video (live or not)
|
||||||
|
- a list of URL/resolution pairs (PeerTube creates 1 URL for each
|
||||||
|
resolution of a video). In the case of a live video, only 1 URL will
|
||||||
|
be returned (as there is no resolution).
|
||||||
|
- the duration (in seconds) of the video (only if it is not a live)
|
||||||
|
|
||||||
:param str video_id: ID or UUID of the video
|
:param str video_id: ID or UUID of the video
|
||||||
:param str instance: URL of the instance hosting the video. The
|
:param str instance: URL of the instance hosting the video. The
|
||||||
configured instance will be used if empty.
|
configured instance will be used if empty.
|
||||||
:return: pair(s) of URL/resolution
|
:return: information of the video
|
||||||
:rtype: generator
|
:rtype: dict
|
||||||
"""
|
"""
|
||||||
# Get the information about the video
|
# Get the information about the video
|
||||||
metadata = self._request(method="GET",
|
metadata = self._request(method="GET",
|
||||||
url="videos/{}".format(video_id),
|
url="videos/{}".format(video_id),
|
||||||
instance=instance)
|
instance=instance)
|
||||||
|
|
||||||
|
video_info = {}
|
||||||
|
|
||||||
if metadata["isLive"]:
|
if metadata["isLive"]:
|
||||||
# When the video is a live, yield the unique playlist URL (there is
|
video_info["is_live"] = True
|
||||||
# no resolution in this case)
|
# When the video is a live, return the unique playlist URL (there is
|
||||||
yield {
|
# no resolution in this case). Even in this case the format of the
|
||||||
"url": metadata['streamingPlaylists'][0]['playlistUrl'],
|
# structure is preserved: we use a list of dict with the key "url"
|
||||||
}
|
video_info["files"] = [
|
||||||
|
{"url": metadata['streamingPlaylists'][0]['playlistUrl']}
|
||||||
|
]
|
||||||
else:
|
else:
|
||||||
|
video_info["is_live"] = False
|
||||||
|
# Add the duration in the returned info
|
||||||
|
video_info["duration"] = metadata["duration"]
|
||||||
|
|
||||||
# For non live videos, the files corresponding to different
|
# For non live videos, the files corresponding to different
|
||||||
# resolutions available for a video may be stored in "files" or
|
# resolutions available for a video may be stored in "files" or
|
||||||
# "streamingPlaylists[].files" depending if WebTorrent is enabled
|
# "streamingPlaylists[].files" depending if WebTorrent is enabled
|
||||||
|
@ -177,11 +190,17 @@ class PeerTube:
|
||||||
else:
|
else:
|
||||||
files = metadata["streamingPlaylists"][0]["files"]
|
files = metadata["streamingPlaylists"][0]["files"]
|
||||||
|
|
||||||
|
video_urls = []
|
||||||
for file in files:
|
for file in files:
|
||||||
yield {
|
video_urls.append(
|
||||||
"resolution": int(file["resolution"]["id"]),
|
{
|
||||||
"url": file["torrentUrl"],
|
"resolution": int(file["resolution"]["id"]),
|
||||||
}
|
"url": file["torrentUrl"],
|
||||||
|
}
|
||||||
|
)
|
||||||
|
video_info["files"] = video_urls
|
||||||
|
|
||||||
|
return video_info
|
||||||
|
|
||||||
def list_videos(self, start):
|
def list_videos(self, start):
|
||||||
"""List the videos in the instance
|
"""List the videos in the instance
|
||||||
|
@ -282,4 +301,4 @@ def list_instances(start):
|
||||||
kodi.notif_error(title=kodi.get_string(30402), message=message)
|
kodi.notif_error(title=kodi.get_string(30402), message=message)
|
||||||
raise exception
|
raise exception
|
||||||
|
|
||||||
return json
|
return json
|
|
@ -76,6 +76,20 @@
|
||||||
<heading>30003</heading>
|
<heading>30003</heading>
|
||||||
</control>
|
</control>
|
||||||
</setting>
|
</setting>
|
||||||
|
<setting help="30021" id="initial_wait_time" label="30020" type="integer">
|
||||||
|
<level>0</level>
|
||||||
|
<default>10</default>
|
||||||
|
<constraints>
|
||||||
|
<options>
|
||||||
|
<minimum>0</minimum>
|
||||||
|
<step>1</step>
|
||||||
|
<maximum>30</maximum>
|
||||||
|
</options>
|
||||||
|
</constraints>
|
||||||
|
<control type="slider" format="integer">
|
||||||
|
<popup>false</popup>
|
||||||
|
</control>
|
||||||
|
</setting>
|
||||||
</group>
|
</group>
|
||||||
</category>
|
</category>
|
||||||
</section>
|
</section>
|
||||||
|
|
188
service.py
188
service.py
|
@ -1,6 +1,6 @@
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
"""
|
"""
|
||||||
PeerTube service to download torrents in the background
|
PeerTube service to perform action on torrents in background
|
||||||
|
|
||||||
Copyright (C) 2018 Cyrille Bollu
|
Copyright (C) 2018 Cyrille Bollu
|
||||||
Copyright (C) 2021 Thomas Bétous
|
Copyright (C) 2021 Thomas Bétous
|
||||||
|
@ -8,73 +8,101 @@
|
||||||
SPDX-License-Identifier: GPL-3.0-only
|
SPDX-License-Identifier: GPL-3.0-only
|
||||||
See LICENSE.txt for more information.
|
See LICENSE.txt for more information.
|
||||||
"""
|
"""
|
||||||
|
import AddonSignals
|
||||||
import AddonSignals # Module exists only in Kodi - pylint: disable=import-error
|
|
||||||
from threading import Thread
|
|
||||||
import xbmc
|
import xbmc
|
||||||
import xbmcvfs
|
import xbmcvfs
|
||||||
|
|
||||||
from resources.lib.kodi_utils import kodi
|
from resources.lib.kodi_utils import kodi
|
||||||
|
|
||||||
class PeertubeDownloader(Thread):
|
class PeertubePlayer(xbmc.Player):
|
||||||
"""
|
# Initialize the attributes and call the parent class constructor
|
||||||
A class to download PeerTube torrents in the background
|
def __init__(self):
|
||||||
"""
|
self.torrent_url = None
|
||||||
|
self.run_url = None
|
||||||
def __init__(self, url, temp_dir):
|
self.playback_started = False
|
||||||
"""
|
super(xbmc.Player, self).__init__()
|
||||||
Initialise a PeertubeDownloader instance for downloading the torrent
|
# TODO: Use the python3 format on Matrix
|
||||||
specified by url
|
# super().__init__()
|
||||||
|
|
||||||
:param url, temp_dir: str
|
|
||||||
:return: None
|
|
||||||
"""
|
|
||||||
Thread.__init__(self)
|
|
||||||
self.torrent = url
|
|
||||||
self.temp_dir = temp_dir
|
|
||||||
|
|
||||||
def debug(self, message):
|
def debug(self, message):
|
||||||
"""Log a debug message
|
"""Log a debug message with the name of the class as prefix
|
||||||
|
|
||||||
:param str message: Message to log (will be prefixed with the name of
|
:param str message: message to log
|
||||||
the class)
|
|
||||||
"""
|
"""
|
||||||
kodi.debug(message=message, prefix="PeertubeDownloader")
|
kodi.debug(message=message, prefix="PeertubePlayer")
|
||||||
|
|
||||||
def run(self):
|
def onAVStarted(self):
|
||||||
|
"""Callback called when Kodi has a video stream.
|
||||||
|
|
||||||
|
When it is called we consider that the video is actually being played
|
||||||
|
and we set the according flag.
|
||||||
"""
|
"""
|
||||||
Download the torrent specified by self.torrent
|
# Check if the file that is being played belongs to this add-on to not
|
||||||
:param: None
|
# conflict with other add-ons or players.
|
||||||
:return: None
|
if self.torrent_url is not None:
|
||||||
|
self.playback_started = True
|
||||||
|
self.debug(message="Playback started for {}".format(self.file_name))
|
||||||
|
|
||||||
|
def onPlayBackStopped(self):
|
||||||
|
"""Callback called when the playback stops
|
||||||
|
|
||||||
|
If the file was actually being played (i.e. onAVStarted() was called for
|
||||||
|
this file before), then we consider that the playback didn't encounter
|
||||||
|
any error and it was stopped willingly by the user. In this case we
|
||||||
|
pause the download of the torrent to avoid downloading in background a
|
||||||
|
video which may never be played again.
|
||||||
|
But if no file was being played, we ask the user if we should try again
|
||||||
|
to play the file: it supports the use case when the playback started
|
||||||
|
whereas the portion of the file that was downloaded was not big enough.
|
||||||
"""
|
"""
|
||||||
|
# First check if the file that was being played belongs to this add-on
|
||||||
|
# to not conflict with other add-ons or players.
|
||||||
|
if self.torrent_url is not None:
|
||||||
|
# Then check if the playback actually started: if the playback
|
||||||
|
# didn't start (probably because there was a too small portion of
|
||||||
|
# the file that was downloaded), do not pause the torrent (Kodi
|
||||||
|
# do not call onPlayBackError() in this case for some reason...)
|
||||||
|
# and ask the user what should be done.
|
||||||
|
# Otherwise pause the torrent because we consider the user decided
|
||||||
|
# to stop the playback.
|
||||||
|
if self.playback_started:
|
||||||
|
self.debug(message="Playback stopped: pausing download...")
|
||||||
|
self.pause_torrent()
|
||||||
|
self.torrent_url = None
|
||||||
|
self.playback_started = False
|
||||||
|
else:
|
||||||
|
self.debug(message="Playback stopped but an error was"
|
||||||
|
" detected: asking the user what should be"
|
||||||
|
" done.")
|
||||||
|
if kodi.open_yes_no_dialog(title=kodi.get_string(30423),
|
||||||
|
message=kodi.get_string(30424)):
|
||||||
|
self.debug(message="Trying to play the video again...")
|
||||||
|
self.play(item=self.run_url)
|
||||||
|
else:
|
||||||
|
self.debug(message="Pausing the download...")
|
||||||
|
self.pause_torrent()
|
||||||
|
self.torrent_url = None
|
||||||
|
self.playback_started = False
|
||||||
|
|
||||||
self.debug("Opening BitTorent session")
|
def pause_torrent(self):
|
||||||
# Open BitTorrent session
|
"""Pause download of the torrent self.torrent_url"""
|
||||||
ses = libtorrent.session()
|
# Get the torrent handle
|
||||||
ses.listen_on(6881, 6891)
|
torrent = xbmcvfs.File(self.torrent_url)
|
||||||
|
# Call seek() with -1 to pause the torrent. 0 is used as second argument
|
||||||
|
# so that GetLength() is not called.
|
||||||
|
# TODO: ommit the second argument on Matrix
|
||||||
|
torrent.seek(-1, 0)
|
||||||
|
|
||||||
# Add torrent
|
def update_torrent_info(self, data):
|
||||||
self.debug("Adding torrent {}".format(self.torrent))
|
"""Save the information about the torrent being played currently
|
||||||
h = ses.add_torrent({"url": self.torrent, "save_path": self.temp_dir})
|
|
||||||
|
|
||||||
# Set sequential mode to allow watching while downloading
|
This function is called through AddonSignals when a video is played.
|
||||||
h.set_sequential_download(True)
|
"""
|
||||||
|
self.torrent_url = data["torrent_url"]
|
||||||
|
self.run_url = data["run_url"]
|
||||||
|
self.debug(message="Received information:\nURL={}\nrun_url={}"
|
||||||
|
.format(self.torrent_url, self.run_url))
|
||||||
|
|
||||||
# Download torrent
|
|
||||||
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
|
|
||||||
if s.state >=3 and signal_sent == 0:
|
|
||||||
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
|
|
||||||
|
|
||||||
class PeertubeService():
|
class PeertubeService():
|
||||||
"""
|
"""
|
||||||
|
@ -83,13 +111,10 @@ class PeertubeService():
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
"""
|
"""
|
||||||
PeertubeService initialisation function
|
Create an instance of PeertubePlayer that will always be active
|
||||||
|
(required to monitor the events and call the callbacks)
|
||||||
"""
|
"""
|
||||||
# Create our temporary directory
|
self.player = PeertubePlayer()
|
||||||
self.temp = "{}{}".format(xbmc.translatePath("special://temp"),
|
|
||||||
"plugin.video.peertube/")
|
|
||||||
if not xbmcvfs.exists(self.temp):
|
|
||||||
xbmcvfs.mkdir(self.temp)
|
|
||||||
|
|
||||||
def debug(self, message):
|
def debug(self, message):
|
||||||
"""Log a debug message
|
"""Log a debug message
|
||||||
|
@ -99,43 +124,29 @@ class PeertubeService():
|
||||||
"""
|
"""
|
||||||
kodi.debug(message=message, prefix="PeertubeService")
|
kodi.debug(message=message, prefix="PeertubeService")
|
||||||
|
|
||||||
def download_torrent(self, data):
|
|
||||||
"""
|
|
||||||
Start a downloader thread to download torrent specified by data["url"]
|
|
||||||
:param data: dict
|
|
||||||
:return: None
|
|
||||||
"""
|
|
||||||
|
|
||||||
self.debug("Received a start_download signal")
|
|
||||||
downloader = PeertubeDownloader(data["url"], self.temp)
|
|
||||||
downloader.start()
|
|
||||||
|
|
||||||
def run(self):
|
def run(self):
|
||||||
"""
|
"""
|
||||||
Main loop of the PeertubeService class
|
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.
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
self.debug("Starting")
|
# Signal that is sent by the main script of the add-on
|
||||||
|
|
||||||
# Launch the download_torrent callback function when the
|
|
||||||
# "start_download" signal is received
|
|
||||||
AddonSignals.registerSlot(kodi.addon_id,
|
AddonSignals.registerSlot(kodi.addon_id,
|
||||||
"start_download",
|
"torrent_information",
|
||||||
self.download_torrent)
|
self.player.update_torrent_info)
|
||||||
|
|
||||||
# Monitor Kodi's shutdown signal
|
|
||||||
self.debug("Service started, waiting for signals")
|
# Display a notification now that the service started.
|
||||||
|
self.debug("Service started")
|
||||||
if kodi.get_setting("service_start_notif") == "true":
|
if kodi.get_setting("service_start_notif") == "true":
|
||||||
kodi.notif_info(title=kodi.get_string(30400),
|
kodi.notif_info(title=kodi.get_string(30400),
|
||||||
message=kodi.get_string(30401))
|
message=kodi.get_string(30401))
|
||||||
|
|
||||||
|
# Run the service until Kodi exits
|
||||||
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
|
|
||||||
self.debug("Exiting")
|
self.debug("Exiting")
|
||||||
break
|
break
|
||||||
|
|
||||||
|
@ -143,18 +154,5 @@ if __name__ == "__main__":
|
||||||
# Create a PeertubeService instance
|
# Create a PeertubeService instance
|
||||||
service = PeertubeService()
|
service = PeertubeService()
|
||||||
|
|
||||||
# Import libtorrent here to manage when the library is not installed
|
|
||||||
try:
|
|
||||||
from python_libtorrent 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
|
|
||||||
kodi.set_property("libtorrent_imported", str(LIBTORRENT_IMPORTED))
|
|
||||||
|
|
||||||
# Start the service
|
# Start the service
|
||||||
service.run()
|
service.run()
|
Loading…
Reference in New Issue