Compare commits
31 Commits
Author | SHA1 | Date |
---|---|---|
Thomas | 11b92d5896 | |
Thomas | 8dec0f316e | |
Thomas Bétous | b629ee43a6 | |
Thomas | 6f3759a928 | |
Thomas Bétous | 2dc6e8a29f | |
Thomas | 9e79850eee | |
Thomas Bétous | b1b8106275 | |
Thomas | 7297f8cef0 | |
Thomas | 134d2ea974 | |
Thomas Bétous | 939f1f0ea5 | |
Thomas | afd8756b38 | |
Thomas | cb1825c1f9 | |
Thomas Bétous | d87f3038d9 | |
Thomas | 17f602da1a | |
Thomas | 13186dc697 | |
Thomas Bétous | 50a886d8cd | |
Thomas Bétous | cc09006bd3 | |
Thomas | 4f8af35035 | |
DavidHenryThoreau | ed39c453e9 | |
Thomas | 7a1a4e8485 | |
Thomas | 6f94e05398 | |
Thomas | 074be7aa12 | |
Thomas Bétous | 142df05350 | |
Thomas | 0efeb9ffdf | |
Thomas | f138f2595a | |
Thomas | c723ced0c6 | |
Thomas Bétous | a88a60376f | |
Thomas | 77ce68a637 | |
Thomas Bétous | a86f2b8f09 | |
Thomas | 4346178db9 | |
Thomas | 7a21bd92ac |
|
@ -0,0 +1,7 @@
|
|||
# Visual Studio Code files
|
||||
*.code-workspace
|
||||
.vscode/*
|
||||
|
||||
.DS_Store
|
||||
*.pyo
|
||||
*.pyc
|
|
@ -0,0 +1,102 @@
|
|||
default:
|
||||
# Cancel any pipeline when a newer instance is started (does not seem to work
|
||||
# currently for the detached pipelines created from merge requests...)
|
||||
interruptible: true
|
||||
|
||||
stages:
|
||||
- validation
|
||||
- release
|
||||
|
||||
# Create YAML anchors to avoid code duplicate (note: an anchor cannot call
|
||||
# another anchor as it will result in an array of array)
|
||||
.apt_get_update: &apt_get_update
|
||||
- apt-get update > /dev/null
|
||||
|
||||
.python_prep: &python_prep
|
||||
- apt-get install --yes python3-dev python3-pip > /dev/null
|
||||
- python3 -m pip --quiet install -r misc/python_requirements.txt
|
||||
|
||||
.release_script_prep: &release_script_prep
|
||||
- apt-get install --yes python3-dev python3-pip git > /dev/null
|
||||
- git clone https://framagit.org/thombet/scripts-for-kodi-add-ons.git
|
||||
- python3 -m pip --quiet install -r scripts-for-kodi-add-ons/create-new-add-on-release/requirements.txt
|
||||
|
||||
# Quality job: check no pylint violations are reported.
|
||||
quality:
|
||||
stage: validation
|
||||
# Do not get any artifacts from previous jobs
|
||||
dependencies: []
|
||||
rules:
|
||||
# Run this job only on merge requests and only as "manual" (it would have
|
||||
# been better to configure this at pipeline-level with "workflow" but it
|
||||
# does not support "when: manual"...).
|
||||
- if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
|
||||
when: manual
|
||||
before_script:
|
||||
- *apt_get_update
|
||||
- *python_prep
|
||||
script:
|
||||
- find . -iname '*.py' | xargs -t python3 -m pylint --rcfile=misc/pylint-rcfile.txt | tee pylint.log
|
||||
artifacts:
|
||||
name: "quality-logs-$CI_JOB_ID"
|
||||
expose_as: "Quality Logs"
|
||||
paths:
|
||||
- pylint.log
|
||||
expire_in: 1 week
|
||||
when: always
|
||||
|
||||
# Translation job: check that all the string.po files use the same strings as
|
||||
# the reference file
|
||||
translation:
|
||||
stage: validation
|
||||
# Do not get any artifacts from previous jobs
|
||||
dependencies: []
|
||||
rules:
|
||||
# Run this job only on merge requests containing changes in strings.po
|
||||
# files and only as "manual"
|
||||
- if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
|
||||
changes:
|
||||
- resources/language/*/strings.po
|
||||
when: manual
|
||||
before_script:
|
||||
- *apt_get_update
|
||||
- apt-get install --yes gettext > /dev/null
|
||||
# We cannot use a simple "find ... -exec msgcmp {} strings.po" because it
|
||||
# would always return 0 as exit code
|
||||
script:
|
||||
- files=$(find . -name strings.po -not -path './resources/language/resource.language.en_gb/*') && for file in $files; do echo -e "\n\033[94mChecking translation file $file\033[0m"; msgcmp $file ./resources/language/resource.language.en_gb/strings.po || continue; done
|
||||
|
||||
# Pre-release job: will be available in all the merge requests with release
|
||||
# branches in order to verify the release can be actually created. The
|
||||
# verification is done by running the release script in dry run mode.
|
||||
pre-release:
|
||||
stage: validation
|
||||
# Do not get any artifacts from previous jobs
|
||||
dependencies: []
|
||||
rules:
|
||||
# Run this job only on merge requests for release branches
|
||||
- if: '$CI_MERGE_REQUEST_SOURCE_BRANCH_NAME =~ /^release\//'
|
||||
when: manual
|
||||
before_script:
|
||||
- *apt_get_update
|
||||
- *release_script_prep
|
||||
script:
|
||||
- python3 scripts-for-kodi-add-ons/create-new-add-on-release/create-new-add-on-release.py --dry-run
|
||||
|
||||
# Release job: will create a new GitLab release with the latest commit on the
|
||||
# main branch.
|
||||
create-release:
|
||||
stage: release
|
||||
# Do not get any artifacts from previous jobs
|
||||
dependencies: []
|
||||
# Run this job only for new commits on the branch "main" and only as "manual"
|
||||
# because it is not mandatory to release all the commits on mainline.
|
||||
rules:
|
||||
- if: '$CI_COMMIT_BRANCH == "main"'
|
||||
when: manual
|
||||
- when: never
|
||||
before_script:
|
||||
- *apt_get_update
|
||||
- *release_script_prep
|
||||
script:
|
||||
- python3 scripts-for-kodi-add-ons/create-new-add-on-release/create-new-add-on-release.py
|
|
@ -0,0 +1,26 @@
|
|||
## Summary
|
||||
|
||||
(Summarize the bug encountered concisely)
|
||||
|
||||
## Steps to reproduce
|
||||
|
||||
(Steps by steps instructions to reproduce the bug)
|
||||
|
||||
## Debug logs
|
||||
|
||||
(Add here at least the Kodi [debug log](https://kodi.wiki/view/Log_file) which
|
||||
contains the error reported while reproducing the bug. You may also attach any relevant screenshot.)
|
||||
|
||||
|
||||
## System Information
|
||||
|
||||
* **Kodi version**: (from Kodi "Settings" → "System Information" →
|
||||
"Version info" at the bottom-right of the screen with Estuary skin)
|
||||
* **Operating System name and version**: (from Kodi "Settings" → "System
|
||||
Information" → "Summary" → "Operating System")
|
||||
* **Add-on version**: (from Kodi "Settings" → "Add-ons" → "My add-ons"
|
||||
→ "Video add-ons" → "PeerTube" → the version is at the top of
|
||||
the screen below the title with Estuary skin)
|
||||
|
||||
|
||||
/label ~bug
|
96
README.md
96
README.md
|
@ -1,36 +1,84 @@
|
|||
A Kodi add-on for watching content hosted on [Peertube](http://joinpeertube.org/).
|
||||
A Kodi add-on for watching content hosted on [PeerTube](http://joinpeertube.org/).
|
||||
|
||||
This code is still proof-of-concept but it works, and you're welcome to improve it.
|
||||
This add-on is under development so only basic features work, and you're
|
||||
welcome to improve it.
|
||||
If you want to contribute, please start with the
|
||||
[contribution guidelines](contributing.md) and the
|
||||
[pending issues](https://framagit.org/StCyr/plugin.video.peertube/-/issues).
|
||||
|
||||
---
|
||||
|
||||
[[_TOC_]]
|
||||
|
||||
# Installation and prerequisites
|
||||
|
||||
Please read the
|
||||
[wiki](https://framagit.org/StCyr/plugin.video.peertube/-/wikis/home)
|
||||
for more information.
|
||||
|
||||
# Features
|
||||
|
||||
* Browse all videos on a PeerTube instance
|
||||
* Play videos (including live videos)
|
||||
* Browse the videos on a PeerTube instance
|
||||
* Search for videos on a PeerTube instance
|
||||
* Select Peertube instance to use (Doesn't work yet)
|
||||
* Select the preferred video resolution: the plugin will try to play the select video resolution.
|
||||
If it's not available, it will play the lower resolution that is the closest from your preference.
|
||||
If not available, it will play the higher resolution that is the closest from your preference.
|
||||
* Select the PeerTube instance to use
|
||||
* Select the preferred video resolution: the plugin will try to play the
|
||||
preferred video resolution.
|
||||
If it's not available, it will play the lower resolution that is the closest
|
||||
to your preference.
|
||||
If not available, it will play the higher resolution that is the closest from
|
||||
your preference.
|
||||
|
||||
# User settings
|
||||
The following languages are available:
|
||||
* English
|
||||
* French
|
||||
* German
|
||||
|
||||
* Preferred PeerTube instance
|
||||
* Preferred video resolution
|
||||
* Number of videos to display per page
|
||||
* Sort method to be used when listing videos (Currently, only 'views' and
|
||||
'likes')
|
||||
* Select the filter to use when browsing the videos on an instance:
|
||||
* local will only display the videos which are local to the selected instance
|
||||
* all-local will only display the videos which are local to the selected
|
||||
instance plus the private and unlisted videos **(requires admin privileges)**
|
||||
If you want to help translating the add-on in your language, check
|
||||
[here](contributing.md#translation).
|
||||
|
||||
# Limitations
|
||||
|
||||
* This add-on doesn't support Webtorrent yet. So, it cannot download/share from/to regular PeerTube clients.
|
||||
The reason is that it uses the libtorrent python library which doesn't support it yet (see https://github.com/arvidn/libtorrent/issues/223)
|
||||
* The add)on doesn't delete the downloaded files at the moment. So, it may fills up your disk.
|
||||
* This add-on doesn't support Webtorrent yet. So, it cannot download/share
|
||||
from/to regular PeerTube clients.
|
||||
* The add-on doesn't delete the downloaded files at the moment so it may fill
|
||||
up your disk. You may delete manually the downloaded files in the folder
|
||||
`<kodi_home>/temp/plugin.video.peertube/` (more information about
|
||||
`<kodi_home>` [here](https://kodi.wiki/view/Kodi_data_folder#Location)).
|
||||
|
||||
# Requirements
|
||||
# User settings
|
||||
|
||||
* Kodi 17 (Krypton) or above
|
||||
* [libtorrent](https://libtorrent.org/) python bindings must be installed on
|
||||
your machine (on Debian type `apt install python-libtorrent` as root).
|
||||
* Preferred PeerTube instance
|
||||
* Display (or not) a notification when the service starts: the notification
|
||||
lets the user know that the videos can be played which may be useful on slow
|
||||
devices (when the service takes some time to start)
|
||||
* Browsing/Searching:
|
||||
* Number of items to show per page (max 100)
|
||||
* Field used to sort items when listing/searching videos:
|
||||
* `views`: sort by number of views (ascending only)
|
||||
* `likes`: sort by number of likes (ascending only)
|
||||
* Select the filter to use when browsing and searching the videos on an instance:
|
||||
* `local` will only display the videos which are local to the selected
|
||||
instance
|
||||
* `all-local` will only display the videos which are local to the selected
|
||||
instance **plus** the private and unlisted videos
|
||||
**(requires admin privileges)**
|
||||
* Video playback:
|
||||
* Preferred video resolution
|
||||
|
||||
# API
|
||||
|
||||
This add-on can be called from other add-ons in Kodi to play videos thanks to
|
||||
the following API:
|
||||
|
||||
`plugin://plugin.video.peertube/?action=play_video&instance=<instance>&id=<id>`
|
||||
|
||||
where:
|
||||
* `<instance>` is the base URL of the instance hosting the video
|
||||
* `<id>` is the ID or the UUID of the video on the instance server
|
||||
|
||||
For instance to play the video behind the URL
|
||||
`https://framatube.org/videos/watch/9c9de5e8-0a1e-484a-b099-e80766180a6d` call
|
||||
the add-on with:
|
||||
|
||||
`plugin://plugin.video.peertube/?action=play_video&instance=framatube.org&id=9c9de5e8-0a1e-484a-b099-e80766180a6d`
|
||||
|
|
11
TESTME.md
11
TESTME.md
|
@ -1,11 +0,0 @@
|
|||
|
||||
Under debian install kodi 17 or above and python-libtorrent library
|
||||
|
||||
apt install kodi
|
||||
apt install python-libtorrent
|
||||
|
||||
then create a zip from this content using :
|
||||
|
||||
./createaddon.sh
|
||||
|
||||
Then follow kodi https://kodi.wiki/view/Add-on_manager#How_to_install_from_a_ZIP_file from the created file.
|
42
addon.xml
42
addon.xml
|
@ -1,37 +1,37 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||
<addon id="plugin.video.peertube" name="PeerTube" version="0.3.2" provider-name="Cyrille B. + Thomas B.">
|
||||
<addon id="plugin.video.peertube" name="PeerTube" version="1.2.0" provider-name="Cyrille B. + Thomas B.">
|
||||
<requires>
|
||||
<import addon="xbmc.python" version="2.25.0"/>
|
||||
<import addon="script.module.addon.signals" version="0.0.3"/>
|
||||
<import addon="script.module.requests" version="2.22.0"/>
|
||||
<import addon="xbmc.python" version="3.0.0"/>
|
||||
<import addon="script.module.addon.signals" version="0.0.6"/>
|
||||
<import addon="script.module.requests" version="2.25.1"/>
|
||||
<!-- <import addon="script.module.libtorrent" version="1.2.0"/> -->
|
||||
</requires>
|
||||
<extension point="xbmc.python.pluginsource" library="peertube.py">
|
||||
<extension point="xbmc.python.pluginsource" library="main.py">
|
||||
<provides>video</provides>
|
||||
</extension>
|
||||
<extension point="xbmc.service" library="service.py"/>
|
||||
<extension point="xbmc.addon.metadata">
|
||||
<summary lang="en_GB">Plugin for PeerTube</summary>
|
||||
<description lang="en_GB">PeerTube is a federated open source alternative to YouTube</description>
|
||||
<disclaimer lang="en_GB"></disclaimer>
|
||||
<language></language>
|
||||
<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>
|
||||
<disclaimer lang="de_DE">Dieses Add-on wird nicht vom PeerTube-Team entwickelt.</disclaimer>
|
||||
<summary lang="en_GB">Add-on for PeerTube</summary>
|
||||
<description lang="en_GB">PeerTube is a free, decentralized and federated alternative to other video platforms (like YouTube).</description>
|
||||
<disclaimer lang="en_GB">This add-on is not developed by PeerTube team.</disclaimer>
|
||||
<summary lang="fr_FR">Extension pour PeerTube</summary>
|
||||
<description lang="fr_FR">PeerTube est une alternative libre, décentralisée et fédérée aux autres plateformes vidéo (comme YouTube).</description>
|
||||
<disclaimer lang="fr_FR">Cette extension n'est pas développée par l'équipe de PeerTube.</disclaimer>
|
||||
<platform>all</platform>
|
||||
<license>GNU GENERAL PUBLIC LICENSE. Version 3, 29 June 2007</license>
|
||||
<forum></forum>
|
||||
<website>https://joinpeertube.org</website>
|
||||
<source>https://framagit.org/StCyr/plugin.video.peertube</source>
|
||||
<news>
|
||||
0.3.2
|
||||
Bug fixes and improvements
|
||||
Fixes:
|
||||
- the search filter 'all-local' was selected by default which resulted in errors
|
||||
Improvements:
|
||||
- Replace urllib with requests to simplify the code
|
||||
- Handle better the errors when sending requests to the PeerTube instance
|
||||
- Create a logging function to improve maintainability
|
||||
- Warn the user that the 'all-local' filter requires admin privileges
|
||||
<news>Version 1.2.0 (19th May 2021)
|
||||
|
||||
New features:
|
||||
* Use the add-on script.module.libtorrent to import libtorrent
|
||||
* Translate the add-on into German (thank you @QNET17)
|
||||
</news>
|
||||
<assets>
|
||||
<icon>icon.png</icon>
|
||||
<icon>resources/icon.png</icon>
|
||||
</assets>
|
||||
</extension>
|
||||
</addon>
|
||||
|
|
|
@ -0,0 +1,172 @@
|
|||
# Contribution Guidelines
|
||||
|
||||
Thank you for deciding to contribute to this project :)
|
||||
Please follow these guidelines when implementing your code.
|
||||
|
||||
[[_TOC_]]
|
||||
|
||||
## Change workflow
|
||||
|
||||
The `main` branch contains the latest version of the code. This branch must be
|
||||
stable and working at any time. To ensure this CI pipelines are used.
|
||||
|
||||
The workflow is the following:
|
||||
1. create a branch on the main repository with an explicit name
|
||||
1. create a merge request from your branch against the `main` branch
|
||||
1. a pipeline is created each time you push commits in a merge request but it
|
||||
will not start automatically: the user may start it. Since a merge request
|
||||
cannot be merged until the associated pipeline passed, start the `blocked
|
||||
pipeline` associated with the latest commit in your merge request when your
|
||||
change is ready.
|
||||
1. if the pipeline passed, the merge request may be merged by one of the
|
||||
maintainers. Note that the preferred option is to squash commits.
|
||||
|
||||
More information about the pipeline is available in the
|
||||
[CI file](.gitlab-ci.yml).
|
||||
|
||||
## Design
|
||||
|
||||
Basically the add-on is composed of:
|
||||
* a service which will download the torrent videos in the background
|
||||
* classes which will retrieved information and play videos from a PeerTube
|
||||
instance
|
||||
|
||||
The add-on is based on the following python modules:
|
||||
|
||||
| Name | Description |
|
||||
| ------ | ------ |
|
||||
| main.py | Entry point of the add-on. |
|
||||
| service.py | Service responsible for downloading torrent files in the background. |
|
||||
| resources/lib/addon.py | Handles the routing and the interaction between the other modules. |
|
||||
| resources/lib/peertube.py | Responsible for interacting with PeerTube. |
|
||||
| resources/lib/kodi_utils.py | Provides utility functions to interact easily with Kodi. |
|
||||
|
||||
### main.py
|
||||
|
||||
The file `peertube.py` is the entry point of the add-on.
|
||||
|
||||
This module must be as short as possible (15 effective lines of code maximum)
|
||||
to comply with Kodi add-on development best practices (checked by the
|
||||
[Kodi add-on checker](https://github.com/xbmc/addon-check)).
|
||||
|
||||
### service.py
|
||||
|
||||
Note: the design of this module is still based on the alpha version.
|
||||
|
||||
It contains 2 classes:
|
||||
* PeertubeService: code of the service which is run by Kodi. It will
|
||||
instantiate `PeertubeDownloader` when the signal to start a download is
|
||||
received from `addon.py`
|
||||
* PeertubeDownloader: downloads torrent in an independent thread
|
||||
|
||||
This module should be as short as possible (15 effective lines of code maximum)
|
||||
to comply with Kodi add-on development best practices (checked by the
|
||||
[Kodi add-on checker](https://github.com/xbmc/addon-check)).
|
||||
|
||||
### addon.py
|
||||
|
||||
This module contains the class `PeerTubeAddon` which is the main class of the
|
||||
add-on. It is responsible for calling the other modules and classes to provide
|
||||
the features of the add-on.
|
||||
|
||||
### peertube.py
|
||||
|
||||
This file contains:
|
||||
* the class `PeerTube` which provides simple method to send REST APIs to a
|
||||
PeerTube instance
|
||||
* the function `list_instances` which lists the PeerTube instances from
|
||||
joinpeertube.org. The URL of the API used by this function and the structure
|
||||
of the response in case of errors is different than the other PeerTube APIs
|
||||
(which are sent to a specific instance) so it made sense to have it as a
|
||||
dedicated function. If more instance-related API are used in the future, a
|
||||
class could be created.
|
||||
|
||||
### kodi_utils.py
|
||||
|
||||
This module contains the class `KodiUtils` which provides utility methods
|
||||
to the other modules so that the Kodi APIs can be called easily. It imports the
|
||||
xbmc file and the other modules should not import any xmbc file.
|
||||
|
||||
A global instance of the class `KodiUtils` which is called `kodi` is defined in
|
||||
this file so that it can be reused easily anywhere in the add-on by simply
|
||||
importing this module.
|
||||
|
||||
Some important features provided by this module:
|
||||
* The methods `get_property` and `set_property` allows to manage data which
|
||||
will remain available when the current call of the add-on ends. It can also
|
||||
be used to share information between the service and the rest of the add-on.
|
||||
* There are some helper functions which make the creation of items in Kodi UI
|
||||
easier.
|
||||
`generate_item_info` creates a dict with the required information to create
|
||||
an item: it allows to define only the parameters that are useful for a given
|
||||
items and the method will use a correct value for the other parameters.
|
||||
Then `create_items_in_ui` is called with the information generated by
|
||||
`generate_item_info` to actually create the items in the UI.
|
||||
|
||||
## Coding style
|
||||
|
||||
Here are the rules to follow when modifying the code:
|
||||
* document the usage of functions following [Sphinx
|
||||
format](https://www.sphinx-doc.org/en/master/usage/restructuredtext/domains.html#python-signatures)
|
||||
* use double quotes instead of single quotes when defining strings (to avoid
|
||||
escaping apostrophes which are common characters in English and other
|
||||
languages)
|
||||
* follow [PEP 8](https://www.python.org/dev/peps/pep-0008/) conventions. The
|
||||
compliance is checked in the `quality` job (more details are available in the
|
||||
[CI file](.gitlab-ci.yml)). Pylint can also be run locally with the
|
||||
following commands:
|
||||
|
||||
```python
|
||||
python3 -m pip install -r misc/python_requirements.txt
|
||||
python3 -m pylint --rcfile=misc/pylint-rcfile.txt
|
||||
```
|
||||
|
||||
Note: pylint is run with python3 to have latest features even though the add-on
|
||||
only supports Kodi v18 Leia (which uses python2)
|
||||
|
||||
## How to release a new version of this add-on
|
||||
|
||||
These steps should be followed only by maintainers.
|
||||
|
||||
1. Create a release branch whose name follows this format:
|
||||
`release/<release_name>`
|
||||
2. On this branch don't commit any new feature. Only commit changes related to
|
||||
the release process like:
|
||||
- a bump of the add-on version in `addon.xml` (note that the version
|
||||
numbering must follow the [semantic versioning](https://semver.org/))
|
||||
- the update of the change log in the `news` tag in `addon.xml` (using
|
||||
Markdown syntax since it will be re-used automatically in the release
|
||||
notes)
|
||||
3. Merge the merge request (maintainers only)
|
||||
4. A new pipeline with the job `create-release` will be created: run the job
|
||||
manually since it should be `blocked` (maintainers only)
|
||||
5. The new release will be available on the releases page.
|
||||
|
||||
## Translation
|
||||
|
||||
To translate the add-on you may:
|
||||
* update an existing translation → edit the corresponding
|
||||
`strings.po` file in the folder of `resources/language/resource.language.<lang>`
|
||||
* translate the add-on into a new language:
|
||||
* create a new `strings.po` file for your language in
|
||||
`resources/language/resource.language.<lang>`
|
||||
* translate the `summary`, `description` and `disclaimer` tags in the file
|
||||
`addon.xml`
|
||||
|
||||
More information about the translation system used by Kodi and its add-ons is
|
||||
available [here](https://kodi.wiki/view/Language_support).
|
||||
|
||||
While translating please take care to:
|
||||
* Keep the `{}`: they will be replaced by variables in the code
|
||||
* Keep the punctuation but adapt it to your language's rules (for instance the
|
||||
number of spaces around `:` varies from one language to another)
|
||||
* Translate using the meaning of the original string but try to not exceed too
|
||||
much the length of the original string (otherwise it may have a negative
|
||||
impact on the user experience e.g. with overlapping strings)
|
||||
* If you hesitate between several translations for a "technical" word, try to
|
||||
use the translation of this word from the Kodi interface
|
||||
|
||||
A CI job called `translation` is available in each merge request which contains
|
||||
changes in strings.po files. It checks that the reference strings in the
|
||||
translation files are the same as in the reference file
|
||||
([resources/language/resource.language.en_gb/strings.po](./resources/language/resource.language.en_gb/strings.po)).
|
|
@ -1,29 +0,0 @@
|
|||
#!/bin/bash
|
||||
|
||||
echo "Create a zip package to be able to install it in kodi as external source"
|
||||
|
||||
package=plugin.video.peertube
|
||||
|
||||
# very lazzy pattern extraction from addon.xml
|
||||
version=$(grep "name=\"PeerTube\" version=" addon.xml | sed 's/^.*version="\([^"]*\).*$/\1/g')
|
||||
|
||||
zip_package=$package-$version.zip
|
||||
|
||||
if [[ -d $package ]]
|
||||
then
|
||||
echo "[ERROR] '$package' directory does already exists, please remove it or move it away" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ -e $zip_package ]]
|
||||
then
|
||||
echo "[WARNING] '$zip_package' zip already exists, it will be updated. Please remove it or move it away or change version in addon.xml next time..." >&2
|
||||
fi
|
||||
|
||||
mkdir $package
|
||||
|
||||
cp -r addon.xml icon.png fanart.jpg peertube.py LICENSE.txt resources/ service.py $package
|
||||
zip -r $zip_package $package
|
||||
|
||||
echo
|
||||
echo "[INFO] '$(pwd)/$zip_package' created. You can import it in kodi"
|
BIN
fanart.jpg
BIN
fanart.jpg
Binary file not shown.
Before Width: | Height: | Size: 21 KiB |
|
@ -0,0 +1,29 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Entry point of the add-on
|
||||
|
||||
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
|
||||
|
||||
from resources.lib.addon import PeerTubeAddon
|
||||
from resources.lib.kodi_utils import kodi
|
||||
|
||||
def main(argv):
|
||||
"""First function called by the add-on
|
||||
|
||||
This function is created to be able to test the code in this module easily.
|
||||
"""
|
||||
# Update the kodi object with the system arguments of this call
|
||||
kodi.update_call_info(argv)
|
||||
# Initialize the main class of the add-on
|
||||
addon = PeerTubeAddon()
|
||||
# Call the router function to execute the requested action
|
||||
addon.router(kodi.get_run_parameters())
|
||||
|
||||
if __name__ == "__main__":
|
||||
main(sys.argv)
|
|
@ -0,0 +1,846 @@
|
|||
[MASTER]
|
||||
|
||||
# A comma-separated list of package or module names from where C extensions may
|
||||
# be loaded. Extensions are loading into the active Python interpreter and may
|
||||
# run arbitrary code.
|
||||
extension-pkg-whitelist=
|
||||
|
||||
# Add files or directories to the blacklist. They should be base names, not
|
||||
# paths.
|
||||
ignore=CVS
|
||||
|
||||
# Add files or directories matching the regex patterns to the blacklist. The
|
||||
# regex matches against base names, not paths.
|
||||
ignore-patterns=
|
||||
|
||||
# Python code to execute, usually for sys.path manipulation such as
|
||||
# pygtk.require().
|
||||
#init-hook=
|
||||
|
||||
# Use multiple processes to speed up Pylint. Specifying 0 will auto-detect the
|
||||
# number of processors available to use.
|
||||
jobs=1
|
||||
|
||||
# Control the amount of potential inferred values when inferring a single
|
||||
# object. This can help the performance when dealing with large functions or
|
||||
# complex, nested conditions.
|
||||
limit-inference-results=100
|
||||
|
||||
# List of plugins (as comma separated values of python module names) to load,
|
||||
# usually to register additional checkers.
|
||||
load-plugins=
|
||||
|
||||
# Pickle collected data for later comparisons.
|
||||
persistent=yes
|
||||
|
||||
# Specify a configuration file.
|
||||
#rcfile=
|
||||
|
||||
# When enabled, pylint would attempt to guess common misconfiguration and emit
|
||||
# user-friendly hints instead of false-positive error messages.
|
||||
suggestion-mode=yes
|
||||
|
||||
# Allow loading of arbitrary C extensions. Extensions are imported into the
|
||||
# active Python interpreter and may run arbitrary code.
|
||||
unsafe-load-any-extension=no
|
||||
|
||||
|
||||
[MESSAGES CONTROL]
|
||||
|
||||
# Only show warnings with the listed confidence levels. Leave empty to show
|
||||
# all. Valid levels: HIGH, INFERENCE, INFERENCE_FAILURE, UNDEFINED.
|
||||
confidence=
|
||||
|
||||
# Disable the message, report, category or checker with the given id(s). You
|
||||
# can either give multiple identifiers separated by comma (,) or put this
|
||||
# option multiple times (only on the command line, not in the configuration
|
||||
# file where it should appear only once). You can also use "--disable=all" to
|
||||
# disable everything first and then reenable specific checks. For example, if
|
||||
# you want to run only the similarities checker, you can use "--disable=all
|
||||
# --enable=similarities". If you want to run only the classes checker, but have
|
||||
# no Warning level messages displayed, use "--disable=all --enable=classes
|
||||
# --disable=W".
|
||||
disable=blacklisted-name,
|
||||
invalid-name,
|
||||
empty-docstring,
|
||||
unneeded-not,
|
||||
missing-module-docstring,
|
||||
missing-class-docstring,
|
||||
missing-function-docstring,
|
||||
singleton-comparison,
|
||||
misplaced-comparison-constant,
|
||||
unidiomatic-typecheck,
|
||||
consider-using-enumerate,
|
||||
consider-iterating-dictionary,
|
||||
bad-classmethod-argument,
|
||||
bad-mcs-method-argument,
|
||||
bad-mcs-classmethod-argument,
|
||||
single-string-used-for-slots,
|
||||
too-many-lines,
|
||||
trailing-whitespace,
|
||||
missing-final-newline,
|
||||
trailing-newlines,
|
||||
multiple-statements,
|
||||
bad-whitespace,
|
||||
mixed-line-endings,
|
||||
unexpected-line-ending-format,
|
||||
wrong-spelling-in-comment,
|
||||
wrong-spelling-in-docstring,
|
||||
invalid-characters-in-docstring,
|
||||
multiple-imports,
|
||||
wrong-import-order,
|
||||
ungrouped-imports,
|
||||
useless-import-alias,
|
||||
import-outside-toplevel,
|
||||
len-as-condition,
|
||||
raw-checker-failed,
|
||||
bad-inline-option,
|
||||
locally-disabled,
|
||||
file-ignored,
|
||||
suppressed-message,
|
||||
useless-suppression,
|
||||
deprecated-pragma,
|
||||
use-symbolic-message-instead,
|
||||
c-extension-no-member,
|
||||
literal-comparison,
|
||||
comparison-with-itself,
|
||||
no-self-use,
|
||||
no-classmethod-decorator,
|
||||
no-staticmethod-decorator,
|
||||
useless-object-inheritance,
|
||||
property-with-parameters,
|
||||
cyclic-import,
|
||||
duplicate-code,
|
||||
too-many-ancestors,
|
||||
too-many-instance-attributes,
|
||||
too-few-public-methods,
|
||||
too-many-public-methods,
|
||||
too-many-return-statements,
|
||||
too-many-branches,
|
||||
too-many-arguments,
|
||||
too-many-locals,
|
||||
too-many-statements,
|
||||
too-many-boolean-expressions,
|
||||
consider-merging-isinstance,
|
||||
too-many-nested-blocks,
|
||||
simplifiable-if-statement,
|
||||
redefined-argument-from-local,
|
||||
no-else-return,
|
||||
consider-using-ternary,
|
||||
trailing-comma-tuple,
|
||||
stop-iteration-return,
|
||||
simplify-boolean-expression,
|
||||
inconsistent-return-statements,
|
||||
useless-return,
|
||||
consider-swap-variables,
|
||||
consider-using-join,
|
||||
consider-using-in,
|
||||
consider-using-get,
|
||||
chained-comparison,
|
||||
consider-using-dict-comprehension,
|
||||
consider-using-set-comprehension,
|
||||
simplifiable-if-expression,
|
||||
no-else-raise,
|
||||
unnecessary-comprehension,
|
||||
no-else-break,
|
||||
no-else-continue,
|
||||
dangerous-default-value,
|
||||
pointless-statement,
|
||||
pointless-string-statement,
|
||||
expression-not-assigned,
|
||||
unnecessary-pass,
|
||||
unnecessary-lambda,
|
||||
assign-to-new-keyword,
|
||||
useless-else-on-loop,
|
||||
exec-used,
|
||||
eval-used,
|
||||
confusing-with-statement,
|
||||
using-constant-test,
|
||||
missing-parentheses-for-call-in-test,
|
||||
self-assigning-variable,
|
||||
redeclared-assigned-name,
|
||||
comparison-with-callable,
|
||||
lost-exception,
|
||||
assert-on-tuple,
|
||||
bad-staticmethod-argument,
|
||||
protected-access,
|
||||
arguments-differ,
|
||||
signature-differs,
|
||||
abstract-method,
|
||||
super-init-not-called,
|
||||
no-init,
|
||||
non-parent-init-called,
|
||||
useless-super-delegation,
|
||||
invalid-overridden-method,
|
||||
bad-indentation,
|
||||
mixed-indentation,
|
||||
wildcard-import,
|
||||
deprecated-module,
|
||||
reimported,
|
||||
import-self,
|
||||
preferred-module,
|
||||
misplaced-future,
|
||||
fixme,
|
||||
global-variable-undefined,
|
||||
global-statement,
|
||||
global-at-module-level,
|
||||
unused-argument,
|
||||
unused-wildcard-import,
|
||||
redefine-in-handler,
|
||||
undefined-loop-variable,
|
||||
unbalanced-tuple-unpacking,
|
||||
cell-var-from-loop,
|
||||
possibly-unused-variable,
|
||||
self-cls-assignment,
|
||||
bare-except,
|
||||
broad-except,
|
||||
duplicate-except,
|
||||
try-except-raise,
|
||||
raising-format-tuple,
|
||||
wrong-exception-operation,
|
||||
keyword-arg-before-vararg,
|
||||
arguments-out-of-order,
|
||||
logging-not-lazy,
|
||||
logging-format-interpolation,
|
||||
bad-format-string-key,
|
||||
unused-format-string-key,
|
||||
missing-format-argument-key,
|
||||
unused-format-string-argument,
|
||||
format-combined-specification,
|
||||
missing-format-attribute,
|
||||
invalid-format-index,
|
||||
duplicate-string-formatting-argument,
|
||||
anomalous-unicode-escape-in-string,
|
||||
implicit-str-concat-in-sequence,
|
||||
boolean-datetime,
|
||||
redundant-unittest-assert,
|
||||
deprecated-method,
|
||||
bad-thread-instantiation,
|
||||
shallow-copy-environ,
|
||||
invalid-envvar-default,
|
||||
subprocess-popen-preexec-fn,
|
||||
subprocess-run-check,
|
||||
apply-builtin,
|
||||
basestring-builtin,
|
||||
buffer-builtin,
|
||||
cmp-builtin,
|
||||
coerce-builtin,
|
||||
execfile-builtin,
|
||||
file-builtin,
|
||||
long-builtin,
|
||||
raw_input-builtin,
|
||||
reduce-builtin,
|
||||
standarderror-builtin,
|
||||
unicode-builtin,
|
||||
xrange-builtin,
|
||||
coerce-method,
|
||||
delslice-method,
|
||||
getslice-method,
|
||||
setslice-method,
|
||||
no-absolute-import,
|
||||
old-division,
|
||||
dict-iter-method,
|
||||
dict-view-method,
|
||||
next-method-called,
|
||||
metaclass-assignment,
|
||||
indexing-exception,
|
||||
raising-string,
|
||||
reload-builtin,
|
||||
oct-method,
|
||||
hex-method,
|
||||
nonzero-method,
|
||||
cmp-method,
|
||||
input-builtin,
|
||||
round-builtin,
|
||||
intern-builtin,
|
||||
unichr-builtin,
|
||||
map-builtin-not-iterating,
|
||||
zip-builtin-not-iterating,
|
||||
range-builtin-not-iterating,
|
||||
filter-builtin-not-iterating,
|
||||
using-cmp-argument,
|
||||
eq-without-hash,
|
||||
div-method,
|
||||
idiv-method,
|
||||
rdiv-method,
|
||||
exception-message-attribute,
|
||||
invalid-str-codec,
|
||||
sys-max-int,
|
||||
bad-python3-import,
|
||||
deprecated-string-function,
|
||||
deprecated-str-translate-call,
|
||||
deprecated-itertools-function,
|
||||
deprecated-types-field,
|
||||
next-method-defined,
|
||||
dict-items-not-iterating,
|
||||
dict-keys-not-iterating,
|
||||
dict-values-not-iterating,
|
||||
deprecated-operator-function,
|
||||
deprecated-urllib-function,
|
||||
xreadlines-attribute,
|
||||
deprecated-sys-function,
|
||||
exception-escape,
|
||||
comprehension-escape
|
||||
|
||||
# Enable the message, report, category or checker with the given id(s). You can
|
||||
# either give multiple identifier separated by comma (,) or put this option
|
||||
# multiple time (only on the command line, not in the configuration file where
|
||||
# it should appear only once). See also the "--disable" option for examples.
|
||||
enable=redefined-builtin,
|
||||
superfluous-parens,
|
||||
unused-import,
|
||||
consider-using-sys-exit,
|
||||
attribute-defined-outside-init,
|
||||
redefined-outer-name,
|
||||
bad-continuation,
|
||||
line-too-long,
|
||||
wrong-import-position,
|
||||
syntax-error,
|
||||
unrecognized-inline-option,
|
||||
bad-option-value,
|
||||
init-is-generator,
|
||||
return-in-init,
|
||||
function-redefined,
|
||||
not-in-loop,
|
||||
return-outside-function,
|
||||
yield-outside-function,
|
||||
return-arg-in-generator,
|
||||
nonexistent-operator,
|
||||
duplicate-argument-name,
|
||||
abstract-class-instantiated,
|
||||
bad-reversed-sequence,
|
||||
too-many-star-expressions,
|
||||
invalid-star-assignment-target,
|
||||
star-needs-assignment-target,
|
||||
nonlocal-and-global,
|
||||
continue-in-finally,
|
||||
nonlocal-without-binding,
|
||||
used-prior-global-declaration,
|
||||
misplaced-format-function,
|
||||
method-hidden,
|
||||
access-member-before-definition,
|
||||
no-method-argument,
|
||||
no-self-argument,
|
||||
invalid-slots-object,
|
||||
assigning-non-slot,
|
||||
invalid-slots,
|
||||
inherit-non-class,
|
||||
inconsistent-mro,
|
||||
duplicate-bases,
|
||||
class-variable-slots-conflict,
|
||||
non-iterator-returned,
|
||||
unexpected-special-method-signature,
|
||||
invalid-length-returned,
|
||||
import-error,
|
||||
relative-beyond-top-level,
|
||||
used-before-assignment,
|
||||
undefined-variable,
|
||||
undefined-all-variable,
|
||||
invalid-all-object,
|
||||
no-name-in-module,
|
||||
unpacking-non-sequence,
|
||||
bad-except-order,
|
||||
raising-bad-type,
|
||||
bad-exception-context,
|
||||
misplaced-bare-raise,
|
||||
raising-non-exception,
|
||||
notimplemented-raised,
|
||||
catching-non-exception,
|
||||
bad-super-call,
|
||||
no-member,
|
||||
not-callable,
|
||||
assignment-from-no-return,
|
||||
no-value-for-parameter,
|
||||
too-many-function-args,
|
||||
unexpected-keyword-arg,
|
||||
redundant-keyword-arg,
|
||||
missing-kwoa,
|
||||
invalid-sequence-index,
|
||||
invalid-slice-index,
|
||||
assignment-from-none,
|
||||
not-context-manager,
|
||||
invalid-unary-operand-type,
|
||||
unsupported-binary-operation,
|
||||
repeated-keyword,
|
||||
not-an-iterable,
|
||||
not-a-mapping,
|
||||
unsupported-membership-test,
|
||||
unsubscriptable-object,
|
||||
unsupported-assignment-operation,
|
||||
unsupported-delete-operation,
|
||||
invalid-metaclass,
|
||||
unhashable-dict-key,
|
||||
dict-iter-missing-items,
|
||||
logging-unsupported-format,
|
||||
logging-format-truncated,
|
||||
logging-too-many-args,
|
||||
logging-too-few-args,
|
||||
bad-format-character,
|
||||
truncated-format-string,
|
||||
mixed-format-string,
|
||||
format-needs-mapping,
|
||||
missing-format-string-key,
|
||||
too-many-format-args,
|
||||
too-few-format-args,
|
||||
bad-string-format-type,
|
||||
bad-str-strip-call,
|
||||
invalid-envvar-value,
|
||||
print-statement,
|
||||
parameter-unpacking,
|
||||
unpacking-in-except,
|
||||
old-raise-syntax,
|
||||
backtick,
|
||||
long-suffix,
|
||||
old-ne-operator,
|
||||
old-octal-literal,
|
||||
import-star-module-level,
|
||||
non-ascii-bytes-literal,
|
||||
yield-inside-async-function,
|
||||
not-async-context-manager,
|
||||
fatal,
|
||||
astroid-error,
|
||||
parse-error,
|
||||
method-check-failed,
|
||||
unreachable,
|
||||
duplicate-key,
|
||||
unnecessary-semicolon,
|
||||
global-variable-not-assigned,
|
||||
unused-variable,
|
||||
binary-op-exception,
|
||||
bad-format-string,
|
||||
anomalous-backslash-in-string,
|
||||
bad-open-mode
|
||||
|
||||
|
||||
[REPORTS]
|
||||
|
||||
# Python expression which should return a score less than or equal to 10. You
|
||||
# have access to the variables 'error', 'warning', 'refactor', and 'convention'
|
||||
# which contain the number of messages in each category, as well as 'statement'
|
||||
# which is the total number of statements analyzed. This score is used by the
|
||||
# global evaluation report (RP0004).
|
||||
evaluation=10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10)
|
||||
|
||||
# Template used to display messages. This is a python new-style format string
|
||||
# used to format the message information. See doc for all details.
|
||||
#msg-template=
|
||||
|
||||
# Set the output format. Available formats are text, parseable, colorized, json
|
||||
# and msvs (visual studio). You can also give a reporter class, e.g.
|
||||
# mypackage.mymodule.MyReporterClass.
|
||||
output-format=text
|
||||
|
||||
# Tells whether to display a full report or only the messages.
|
||||
reports=no
|
||||
|
||||
# Activate the evaluation score.
|
||||
score=yes
|
||||
|
||||
|
||||
[REFACTORING]
|
||||
|
||||
# Maximum number of nested blocks for function / method body
|
||||
max-nested-blocks=5
|
||||
|
||||
# Complete name of functions that never returns. When checking for
|
||||
# inconsistent-return-statements if a never returning function is called then
|
||||
# it will be considered as an explicit return statement and no message will be
|
||||
# printed.
|
||||
never-returning-functions=sys.exit
|
||||
|
||||
|
||||
[BASIC]
|
||||
|
||||
# Naming style matching correct argument names.
|
||||
argument-naming-style=snake_case
|
||||
|
||||
# Regular expression matching correct argument names. Overrides argument-
|
||||
# naming-style.
|
||||
#argument-rgx=
|
||||
|
||||
# Naming style matching correct attribute names.
|
||||
attr-naming-style=snake_case
|
||||
|
||||
# Regular expression matching correct attribute names. Overrides attr-naming-
|
||||
# style.
|
||||
#attr-rgx=
|
||||
|
||||
# Bad variable names which should always be refused, separated by a comma.
|
||||
bad-names=foo,
|
||||
bar,
|
||||
baz,
|
||||
toto,
|
||||
tutu,
|
||||
tata
|
||||
|
||||
# Naming style matching correct class attribute names.
|
||||
class-attribute-naming-style=any
|
||||
|
||||
# Regular expression matching correct class attribute names. Overrides class-
|
||||
# attribute-naming-style.
|
||||
#class-attribute-rgx=
|
||||
|
||||
# Naming style matching correct class names.
|
||||
class-naming-style=PascalCase
|
||||
|
||||
# Regular expression matching correct class names. Overrides class-naming-
|
||||
# style.
|
||||
#class-rgx=
|
||||
|
||||
# Naming style matching correct constant names.
|
||||
const-naming-style=UPPER_CASE
|
||||
|
||||
# Regular expression matching correct constant names. Overrides const-naming-
|
||||
# style.
|
||||
#const-rgx=
|
||||
|
||||
# Minimum line length for functions/classes that require docstrings, shorter
|
||||
# ones are exempt.
|
||||
docstring-min-length=-1
|
||||
|
||||
# Naming style matching correct function names.
|
||||
function-naming-style=snake_case
|
||||
|
||||
# Regular expression matching correct function names. Overrides function-
|
||||
# naming-style.
|
||||
#function-rgx=
|
||||
|
||||
# Good variable names which should always be accepted, separated by a comma.
|
||||
good-names=i,
|
||||
j,
|
||||
k,
|
||||
ex,
|
||||
Run,
|
||||
_
|
||||
|
||||
# Include a hint for the correct naming format with invalid-name.
|
||||
include-naming-hint=no
|
||||
|
||||
# Naming style matching correct inline iteration names.
|
||||
inlinevar-naming-style=any
|
||||
|
||||
# Regular expression matching correct inline iteration names. Overrides
|
||||
# inlinevar-naming-style.
|
||||
#inlinevar-rgx=
|
||||
|
||||
# Naming style matching correct method names.
|
||||
method-naming-style=snake_case
|
||||
|
||||
# Regular expression matching correct method names. Overrides method-naming-
|
||||
# style.
|
||||
#method-rgx=
|
||||
|
||||
# Naming style matching correct module names.
|
||||
module-naming-style=snake_case
|
||||
|
||||
# Regular expression matching correct module names. Overrides module-naming-
|
||||
# style.
|
||||
#module-rgx=
|
||||
|
||||
# Colon-delimited sets of names that determine each other's naming style when
|
||||
# the name regexes allow several styles.
|
||||
name-group=
|
||||
|
||||
# Regular expression which should only match function or class names that do
|
||||
# not require a docstring.
|
||||
no-docstring-rgx=^_
|
||||
|
||||
# List of decorators that produce properties, such as abc.abstractproperty. Add
|
||||
# to this list to register other decorators that produce valid properties.
|
||||
# These decorators are taken in consideration only for invalid-name.
|
||||
property-classes=abc.abstractproperty
|
||||
|
||||
# Naming style matching correct variable names.
|
||||
variable-naming-style=snake_case
|
||||
|
||||
# Regular expression matching correct variable names. Overrides variable-
|
||||
# naming-style.
|
||||
#variable-rgx=
|
||||
|
||||
|
||||
[FORMAT]
|
||||
|
||||
# Expected format of line ending, e.g. empty (any line ending), LF or CRLF.
|
||||
expected-line-ending-format=
|
||||
|
||||
# Regexp for a line that is allowed to be longer than the limit.
|
||||
ignore-long-lines=^\s*(# )?<?https?://\S+>?$
|
||||
|
||||
# Number of spaces of indent required inside a hanging or continued line.
|
||||
indent-after-paren=4
|
||||
|
||||
# String used as indentation unit. This is usually " " (4 spaces) or "\t" (1
|
||||
# tab).
|
||||
indent-string=' '
|
||||
|
||||
# Maximum number of characters on a single line.
|
||||
max-line-length=80
|
||||
|
||||
# Maximum number of lines in a module.
|
||||
max-module-lines=1000
|
||||
|
||||
# List of optional constructs for which whitespace checking is disabled. `dict-
|
||||
# separator` is used to allow tabulation in dicts, etc.: {1 : 1,\n222: 2}.
|
||||
# `trailing-comma` allows a space between comma and closing bracket: (a, ).
|
||||
# `empty-line` allows space-only lines.
|
||||
no-space-check=trailing-comma,
|
||||
dict-separator
|
||||
|
||||
# Allow the body of a class to be on the same line as the declaration if body
|
||||
# contains single statement.
|
||||
single-line-class-stmt=no
|
||||
|
||||
# Allow the body of an if to be on the same line as the test if there is no
|
||||
# else.
|
||||
single-line-if-stmt=no
|
||||
|
||||
|
||||
[LOGGING]
|
||||
|
||||
# Format style used to check logging format string. `old` means using %
|
||||
# formatting, `new` is for `{}` formatting,and `fstr` is for f-strings.
|
||||
logging-format-style=old
|
||||
|
||||
# Logging modules to check that the string format arguments are in logging
|
||||
# function parameter format.
|
||||
logging-modules=logging
|
||||
|
||||
|
||||
[MISCELLANEOUS]
|
||||
|
||||
# List of note tags to take in consideration, separated by a comma.
|
||||
notes=FIXME,
|
||||
XXX,
|
||||
TODO
|
||||
|
||||
|
||||
[SIMILARITIES]
|
||||
|
||||
# Ignore comments when computing similarities.
|
||||
ignore-comments=yes
|
||||
|
||||
# Ignore docstrings when computing similarities.
|
||||
ignore-docstrings=yes
|
||||
|
||||
# Ignore imports when computing similarities.
|
||||
ignore-imports=no
|
||||
|
||||
# Minimum lines number of a similarity.
|
||||
min-similarity-lines=4
|
||||
|
||||
|
||||
[SPELLING]
|
||||
|
||||
# Limits count of emitted suggestions for spelling mistakes.
|
||||
max-spelling-suggestions=4
|
||||
|
||||
# Spelling dictionary name. Available dictionaries: none. To make it work,
|
||||
# install the python-enchant package.
|
||||
spelling-dict=
|
||||
|
||||
# List of comma separated words that should not be checked.
|
||||
spelling-ignore-words=
|
||||
|
||||
# A path to a file that contains the private dictionary; one word per line.
|
||||
spelling-private-dict-file=
|
||||
|
||||
# Tells whether to store unknown words to the private dictionary (see the
|
||||
# --spelling-private-dict-file option) instead of raising a message.
|
||||
spelling-store-unknown-words=no
|
||||
|
||||
|
||||
[STRING]
|
||||
|
||||
# This flag controls whether the implicit-str-concat-in-sequence should
|
||||
# generate a warning on implicit string concatenation in sequences defined over
|
||||
# several lines.
|
||||
check-str-concat-over-line-jumps=no
|
||||
|
||||
|
||||
[TYPECHECK]
|
||||
|
||||
# List of decorators that produce context managers, such as
|
||||
# contextlib.contextmanager. Add to this list to register other decorators that
|
||||
# produce valid context managers.
|
||||
contextmanager-decorators=contextlib.contextmanager
|
||||
|
||||
# List of members which are set dynamically and missed by pylint inference
|
||||
# system, and so shouldn't trigger E1101 when accessed. Python regular
|
||||
# expressions are accepted.
|
||||
generated-members=
|
||||
|
||||
# Tells whether missing members accessed in mixin class should be ignored. A
|
||||
# mixin class is detected if its name ends with "mixin" (case insensitive).
|
||||
ignore-mixin-members=yes
|
||||
|
||||
# Tells whether to warn about missing members when the owner of the attribute
|
||||
# is inferred to be None.
|
||||
ignore-none=yes
|
||||
|
||||
# This flag controls whether pylint should warn about no-member and similar
|
||||
# checks whenever an opaque object is returned when inferring. The inference
|
||||
# can return multiple potential results while evaluating a Python object, but
|
||||
# some branches might not be evaluated, which results in partial inference. In
|
||||
# that case, it might be useful to still emit no-member and other checks for
|
||||
# the rest of the inferred objects.
|
||||
ignore-on-opaque-inference=yes
|
||||
|
||||
# List of class names for which member attributes should not be checked (useful
|
||||
# for classes with dynamically set attributes). This supports the use of
|
||||
# qualified names.
|
||||
ignored-classes=optparse.Values,thread._local,_thread._local
|
||||
|
||||
# List of module names for which member attributes should not be checked
|
||||
# (useful for modules/projects where namespaces are manipulated during runtime
|
||||
# and thus existing member attributes cannot be deduced by static analysis). It
|
||||
# supports qualified module names, as well as Unix pattern matching.
|
||||
ignored-modules=
|
||||
|
||||
# Show a hint with possible names when a member name was not found. The aspect
|
||||
# of finding the hint is based on edit distance.
|
||||
missing-member-hint=yes
|
||||
|
||||
# The minimum edit distance a name should have in order to be considered a
|
||||
# similar match for a missing member name.
|
||||
missing-member-hint-distance=1
|
||||
|
||||
# The total number of similar names that should be taken in consideration when
|
||||
# showing a hint for a missing member.
|
||||
missing-member-max-choices=1
|
||||
|
||||
# List of decorators that change the signature of a decorated function.
|
||||
signature-mutators=
|
||||
|
||||
|
||||
[VARIABLES]
|
||||
|
||||
# List of additional names supposed to be defined in builtins. Remember that
|
||||
# you should avoid defining new builtins when possible.
|
||||
additional-builtins=
|
||||
|
||||
# Tells whether unused global variables should be treated as a violation.
|
||||
allow-global-unused-variables=yes
|
||||
|
||||
# List of strings which can identify a callback function by name. A callback
|
||||
# name must start or end with one of those strings.
|
||||
callbacks=cb_,
|
||||
_cb
|
||||
|
||||
# A regular expression matching the name of dummy variables (i.e. expected to
|
||||
# not be used).
|
||||
dummy-variables-rgx=_+$|(_[a-zA-Z0-9_]*[a-zA-Z0-9]+?$)|dummy|^ignored_|^unused_
|
||||
|
||||
# Argument names that match this expression will be ignored. Default to name
|
||||
# with leading underscore.
|
||||
ignored-argument-names=_.*|^ignored_|^unused_
|
||||
|
||||
# Tells whether we should check for unused import in __init__ files.
|
||||
init-import=no
|
||||
|
||||
# List of qualified module names which can have objects that can redefine
|
||||
# builtins.
|
||||
redefining-builtins-modules=six.moves,past.builtins,future.builtins,builtins,io
|
||||
|
||||
|
||||
[CLASSES]
|
||||
|
||||
# List of method names used to declare (i.e. assign) instance attributes.
|
||||
defining-attr-methods=__init__,
|
||||
__new__,
|
||||
setUp,
|
||||
__post_init__
|
||||
|
||||
# List of member names, which should be excluded from the protected access
|
||||
# warning.
|
||||
exclude-protected=_asdict,
|
||||
_fields,
|
||||
_replace,
|
||||
_source,
|
||||
_make
|
||||
|
||||
# List of valid names for the first argument in a class method.
|
||||
valid-classmethod-first-arg=cls
|
||||
|
||||
# List of valid names for the first argument in a metaclass class method.
|
||||
valid-metaclass-classmethod-first-arg=cls
|
||||
|
||||
|
||||
[DESIGN]
|
||||
|
||||
# Maximum number of arguments for function / method.
|
||||
max-args=5
|
||||
|
||||
# Maximum number of attributes for a class (see R0902).
|
||||
max-attributes=7
|
||||
|
||||
# Maximum number of boolean expressions in an if statement (see R0916).
|
||||
max-bool-expr=5
|
||||
|
||||
# Maximum number of branch for function / method body.
|
||||
max-branches=12
|
||||
|
||||
# Maximum number of locals for function / method body.
|
||||
max-locals=15
|
||||
|
||||
# Maximum number of parents for a class (see R0901).
|
||||
max-parents=7
|
||||
|
||||
# Maximum number of public methods for a class (see R0904).
|
||||
max-public-methods=20
|
||||
|
||||
# Maximum number of return / yield for function / method body.
|
||||
max-returns=6
|
||||
|
||||
# Maximum number of statements in function / method body.
|
||||
max-statements=50
|
||||
|
||||
# Minimum number of public methods for a class (see R0903).
|
||||
min-public-methods=2
|
||||
|
||||
|
||||
[IMPORTS]
|
||||
|
||||
# List of modules that can be imported at any level, not just the top level
|
||||
# one.
|
||||
allow-any-import-level=
|
||||
|
||||
# Allow wildcard imports from modules that define __all__.
|
||||
allow-wildcard-with-all=no
|
||||
|
||||
# Analyse import fallback blocks. This can be used to support both Python 2 and
|
||||
# 3 compatible code, which means that the block might have code that exists
|
||||
# only in one or another interpreter, leading to false positives when analysed.
|
||||
analyse-fallback-blocks=no
|
||||
|
||||
# Deprecated modules which should not be used, separated by a comma.
|
||||
deprecated-modules=optparse,tkinter.tix
|
||||
|
||||
# Create a graph of external dependencies in the given file (report RP0402 must
|
||||
# not be disabled).
|
||||
ext-import-graph=
|
||||
|
||||
# Create a graph of every (i.e. internal and external) dependencies in the
|
||||
# given file (report RP0402 must not be disabled).
|
||||
import-graph=
|
||||
|
||||
# Create a graph of internal dependencies in the given file (report RP0402 must
|
||||
# not be disabled).
|
||||
int-import-graph=
|
||||
|
||||
# Force import order to recognize a module as part of the standard
|
||||
# compatibility libraries.
|
||||
known-standard-library=
|
||||
|
||||
# Force import order to recognize a module as part of a third party library.
|
||||
known-third-party=enchant
|
||||
|
||||
# Couples of modules and preferred modules, separated by a comma.
|
||||
preferred-modules=
|
||||
|
||||
|
||||
[EXCEPTIONS]
|
||||
|
||||
# Exceptions that will emit a warning when being caught. Defaults to
|
||||
# "BaseException, Exception".
|
||||
overgeneral-exceptions=BaseException,
|
||||
Exception
|
|
@ -0,0 +1,3 @@
|
|||
Kodistubs>=19.0.0
|
||||
pylint
|
||||
requests
|
515
peertube.py
515
peertube.py
|
@ -1,515 +0,0 @@
|
|||
# A Kodi Addon 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
|
||||
import sys
|
||||
|
||||
try:
|
||||
# Python 3.x
|
||||
from urllib.parse import parse_qsl
|
||||
except ImportError:
|
||||
# Python 2.x
|
||||
from urlparse import parse_qsl
|
||||
|
||||
import AddonSignals
|
||||
import requests
|
||||
from requests.compat import urljoin, urlencode
|
||||
import xbmc
|
||||
import xbmcaddon
|
||||
import xbmcgui
|
||||
import xbmcplugin
|
||||
import xbmcvfs
|
||||
|
||||
|
||||
class PeertubeAddon():
|
||||
"""
|
||||
Main class of the addon
|
||||
"""
|
||||
|
||||
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
|
||||
# the add-on name
|
||||
# Get an Addon instance
|
||||
addon = xbmcaddon.Addon()
|
||||
# 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
|
||||
|
||||
# Select preferred instance by default
|
||||
self.selected_inst = addon.getSetting('preferred_instance')
|
||||
|
||||
# Get the number of videos to show per page
|
||||
self.items_per_page = int(addon.getSetting('items_per_page'))
|
||||
|
||||
# Get the video sort method
|
||||
self.sort_method = addon.getSetting('video_sort_method')
|
||||
|
||||
# Get the preferred resolution for video
|
||||
self.preferred_resolution = addon.getSetting('preferred_resolution')
|
||||
|
||||
# Nothing to play at initialisation
|
||||
self.play = 0
|
||||
self.torrent_name = ''
|
||||
|
||||
# Get the video filter from the settings that will be used when
|
||||
# browsing the videos. The value from the settings is converted into
|
||||
# one of the expected values by the REST APIs ("local" or "all-local")
|
||||
if 'all-local' in addon.getSetting('video_filter'):
|
||||
self.video_filter = 'all-local'
|
||||
else:
|
||||
self.video_filter = 'local'
|
||||
|
||||
return None
|
||||
|
||||
def debug(self, message):
|
||||
"""Log a message in Kodi's log with the level xbmc.LOGDEBUG
|
||||
|
||||
:param message: Message to log
|
||||
:type message: str
|
||||
"""
|
||||
xbmc.log('{0}: {1}'.format(self.addon_name, message), xbmc.LOGDEBUG)
|
||||
|
||||
def query_peertube(self, req):
|
||||
"""
|
||||
Issue a PeerTube API request and return the results
|
||||
:param req: str
|
||||
:result data: dict
|
||||
"""
|
||||
|
||||
# Send the PeerTube REST API request
|
||||
self.debug('Issuing request {0}'.format(req))
|
||||
response = requests.get(url=req)
|
||||
data = response.json()
|
||||
|
||||
# Use Request.raise_for_status() to raise an exception if the HTTP
|
||||
# request returned an unsuccessful status code.
|
||||
try:
|
||||
response.raise_for_status()
|
||||
except requests.HTTPError as e:
|
||||
xbmcgui.Dialog().notification('Communication error',
|
||||
'Error during request on {0}'
|
||||
.format(self.selected_inst),
|
||||
xbmcgui.NOTIFICATION_ERROR)
|
||||
# Print the JSON as it may contain an 'error' key with the details
|
||||
# of the error
|
||||
self.debug('Error => "{}"'.format(data['error']))
|
||||
raise e
|
||||
|
||||
# Return when no results are found
|
||||
if data['total'] == 0:
|
||||
self.debug('No result found')
|
||||
return None
|
||||
else:
|
||||
self.debug('Found {0} results'.format(data['total']))
|
||||
|
||||
return data
|
||||
|
||||
def create_list(self, lst, data_type, start):
|
||||
"""
|
||||
Create an array of xmbcgui.ListItem's from the lst parameter
|
||||
:param lst, data_type, start: dict, str, str
|
||||
:result listing: array
|
||||
"""
|
||||
# Create a list for our items.
|
||||
listing = []
|
||||
for data in lst['data']:
|
||||
|
||||
# Create a list item with a text label
|
||||
list_item = xbmcgui.ListItem(label=data['name'])
|
||||
|
||||
if data_type == 'videos':
|
||||
# Add thumbnail
|
||||
list_item.setArt({'thumb': self.selected_inst + '/' + data['thumbnailPath']})
|
||||
|
||||
# Set a fanart image for the list item.
|
||||
# list_item.setProperty('fanart_image', data['thumb'])
|
||||
|
||||
# Compute media info from item's metadata
|
||||
info = {'title': data['name'],
|
||||
'playcount': data['views'],
|
||||
'plotoutline': data['description'],
|
||||
'duration': data['duration']
|
||||
}
|
||||
|
||||
# For videos, add a rating based on likes and dislikes
|
||||
if data['likes'] > 0 or data['dislikes'] > 0:
|
||||
info['rating'] = data['likes']/(data['likes'] + data['dislikes'])
|
||||
|
||||
# Set additional info for the list item.
|
||||
list_item.setInfo('video', info)
|
||||
|
||||
# Videos are playable
|
||||
list_item.setProperty('IsPlayable', 'true')
|
||||
|
||||
# Find the URL of the best possible video matching user's preferences
|
||||
# TODO: Error handling
|
||||
current_res = 0
|
||||
higher_res = -1
|
||||
torrent_url = ''
|
||||
response = requests.get(self.selected_inst + '/api/v1/videos/'
|
||||
+ data['uuid'])
|
||||
metadata = response.json()
|
||||
self.debug('Looking for the best possible video quality matching user preferences')
|
||||
for f in metadata['files']:
|
||||
# Get file resolution
|
||||
res = f['resolution']['id']
|
||||
if res == self.preferred_resolution:
|
||||
# Stop directly, when we find the exact same resolution as the user's preferred one
|
||||
self.debug('Found video with preferred resolution')
|
||||
torrent_url = f['torrentUrl']
|
||||
break
|
||||
elif res < self.preferred_resolution and res > current_res:
|
||||
# Else, try to find the best one just below the user's preferred one
|
||||
self.debug('Found video with good lower resolution'
|
||||
'({0})'.format(f['resolution']['label']))
|
||||
torrent_url = f['torrentUrl']
|
||||
current_res = res
|
||||
elif res > self.preferred_resolution and (res < higher_res or higher_res == -1):
|
||||
# In the worst case, we'll take the one just above the user's preferred one
|
||||
self.debug('Saving video with higher resolution ({0})'
|
||||
'as a possible alternative'
|
||||
.format(f['resolution']['label']))
|
||||
backup_url = f['torrentUrl']
|
||||
higher_res = res
|
||||
|
||||
# Use smallest file with an higher resolution, when we didn't find a resolution equal or
|
||||
# lower than the user's preferred one
|
||||
if not torrent_url:
|
||||
self.debug('Using video with higher resolution as alternative')
|
||||
torrent_url = backup_url
|
||||
|
||||
# Compose the correct URL for Kodi
|
||||
url = self.build_kodi_url({
|
||||
'action': 'play_video',
|
||||
'url': torrent_url
|
||||
})
|
||||
|
||||
elif data_type == 'instances':
|
||||
# TODO: Add a context menu to select instance as preferred instance
|
||||
# Instances are not playable
|
||||
list_item.setProperty('IsPlayable', 'false')
|
||||
|
||||
# Set URL to select this instance
|
||||
url = self.build_kodi_url({
|
||||
'action': 'select_instance',
|
||||
'url': data['host']
|
||||
})
|
||||
|
||||
# Add our item to the listing as a 3-element tuple.
|
||||
listing.append((url, list_item, False))
|
||||
|
||||
# Add a 'Next page' button when there are more data to show
|
||||
start = int(start) + self.items_per_page
|
||||
if lst['total'] > start:
|
||||
list_item = xbmcgui.ListItem(label='Next page ({0})'
|
||||
.format(start/self.items_per_page))
|
||||
url = self.build_kodi_url({
|
||||
'action': 'browse_{0}'.format(data_type),
|
||||
'start': start})
|
||||
listing.append((url, list_item, True))
|
||||
|
||||
return listing
|
||||
|
||||
def build_video_rest_api_request(self, search, start):
|
||||
"""Build the URL of an HTTP request using the PeerTube videos REST API.
|
||||
|
||||
The same function is used for browsing and searching videos.
|
||||
|
||||
:param search: keywords to search
|
||||
:type search: string
|
||||
:param start: offset
|
||||
:type start: int
|
||||
:return: the URL of the request
|
||||
:rtype: str
|
||||
|
||||
Didn't yet find a correct way to do a search with a filter set to
|
||||
local. Then if a search value is given it won't filter on local
|
||||
"""
|
||||
|
||||
# Common parameters of the request
|
||||
params = {
|
||||
'count': self.items_per_page,
|
||||
'start': start,
|
||||
'sort': self.sort_method
|
||||
}
|
||||
|
||||
# Depending on the type of request (search or list videos), add
|
||||
# specific parameters and define the API to use
|
||||
if search is None:
|
||||
# Video API does not provide "search" but provides "filter" so add
|
||||
# it to the parameters
|
||||
params.update({'filter': self.video_filter})
|
||||
api_url = '/api/v1/videos'
|
||||
else:
|
||||
# Search API does not provide "filter" but provides "search" so add
|
||||
# it to the parameters
|
||||
params.update({'search': search})
|
||||
api_url = '/api/v1/search/videos'
|
||||
|
||||
# Build the full URL of the request (instance + API + parameters)
|
||||
req = '{0}?{1}'.format(urljoin(self.selected_inst, api_url),
|
||||
urlencode(params))
|
||||
|
||||
return req
|
||||
|
||||
def build_browse_instances_rest_api_request(self, start):
|
||||
"""Build the URL of an HTTP request using the PeerTube REST API to
|
||||
browse the PeerTube instances.
|
||||
|
||||
:param start: offset
|
||||
:type start: int
|
||||
:return: the URL of the request
|
||||
:rtype: str
|
||||
"""
|
||||
|
||||
# Create the parameters of the request
|
||||
params = {
|
||||
'count': self.items_per_page,
|
||||
'start': start
|
||||
}
|
||||
|
||||
# Join the base URL with the REST API and the parameters
|
||||
req = '{0}?{1}'.format('https://instances.joinpeertube.org/api/v1/instances',
|
||||
urlencode(params))
|
||||
|
||||
return req
|
||||
|
||||
def search_videos(self, start):
|
||||
"""
|
||||
Function to search for videos on a PeerTube instance and navigate in the results
|
||||
:param start: string
|
||||
:result: None
|
||||
"""
|
||||
|
||||
# Show a 'Search videos' dialog
|
||||
search = xbmcgui.Dialog().input(heading='Search videos on ' + self.selected_inst, type=xbmcgui.INPUT_ALPHANUM)
|
||||
|
||||
# Go back to main menu when user cancels
|
||||
if not search:
|
||||
return None
|
||||
|
||||
# Create the PeerTube REST API request for searching videos
|
||||
req = self.build_video_rest_api_request(search, start)
|
||||
|
||||
# Send the query
|
||||
results = self.query_peertube(req)
|
||||
|
||||
# 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
|
||||
|
||||
# Create array of xmbcgui.ListItem's
|
||||
listing = self.create_list(results, 'videos', start)
|
||||
|
||||
# Add our listing to Kodi.
|
||||
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
|
||||
req = self.build_video_rest_api_request(None, start)
|
||||
|
||||
# Send the query
|
||||
results = self.query_peertube(req)
|
||||
|
||||
# Create array of xmbcgui.ListItem's
|
||||
listing = self.create_list(results, 'videos', start)
|
||||
|
||||
# Add our listing to Kodi.
|
||||
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
|
||||
req = self.build_browse_instances_rest_api_request(start)
|
||||
|
||||
# Send the query
|
||||
results = self.query_peertube(req)
|
||||
|
||||
# Create array of xmbcgui.ListItem's
|
||||
listing = self.create_list(results, 'instances', start)
|
||||
|
||||
# Add our listing to Kodi.
|
||||
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('Received metadata_downloaded signal, will start playing media')
|
||||
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
|
||||
"""
|
||||
|
||||
self.debug('Starting torrent download ({0})'.format(torrent_url))
|
||||
|
||||
# Start a downloader thread
|
||||
AddonSignals.sendSignal('start_download', {'url': torrent_url})
|
||||
|
||||
# Wait until the PeerTubeDownloader has downloaded all the torrent's metadata
|
||||
AddonSignals.registerSlot('plugin.video.peertube', 'metadata_downloaded', self.play_video_continue)
|
||||
timeout = 0
|
||||
while self.play == 0 and timeout < 10:
|
||||
xbmc.sleep(1000)
|
||||
timeout += 1
|
||||
|
||||
# Abort in case of timeout
|
||||
if timeout == 10:
|
||||
xbmcgui.Dialog().notification('Download timeout', 'Timeout fetching ' + torrent_url, xbmcgui.NOTIFICATION_ERROR)
|
||||
return None
|
||||
else:
|
||||
# Wait a little before starting playing the torrent
|
||||
xbmc.sleep(3000)
|
||||
|
||||
# Pass the item to the Kodi player for actual playback.
|
||||
self.debug('Starting video playback ({0})'.format(torrent_url))
|
||||
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://' + instance
|
||||
xbmcgui.Dialog().notification('Current instance changed', 'Changed current instance to {0}'.format(self.selected_inst), xbmcgui.NOTIFICATION_INFO)
|
||||
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.
|
||||
|
||||
:param parameters: dict containing all the parameters that will be
|
||||
encoded in the URL
|
||||
"""
|
||||
|
||||
return '{0}?{1}'.format(self.plugin_url, urlencode(parameters))
|
||||
|
||||
def main_menu(self):
|
||||
"""
|
||||
Addon's main menu
|
||||
:param: None
|
||||
:return: None
|
||||
"""
|
||||
|
||||
# Create a list for our items.
|
||||
listing = []
|
||||
|
||||
# 1st menu entry
|
||||
list_item = xbmcgui.ListItem(label='Browse selected instance')
|
||||
url = self.build_kodi_url({'action': 'browse_videos', 'start': 0})
|
||||
listing.append((url, list_item, True))
|
||||
|
||||
# 2nd menu entry
|
||||
list_item = xbmcgui.ListItem(label='Search on selected instance')
|
||||
url = self.build_kodi_url({'action': 'search_videos', 'start': 0})
|
||||
listing.append((url, list_item, True))
|
||||
|
||||
# 3rd menu entry
|
||||
list_item = xbmcgui.ListItem(label='Select other instance')
|
||||
url = self.build_kodi_url({'action': 'browse_instances', 'start': 0})
|
||||
listing.append((url, list_item, True))
|
||||
|
||||
# Add our listing to Kodi.
|
||||
xbmcplugin.addDirectoryItems(self.plugin_id, listing, len(listing))
|
||||
|
||||
# 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
|
||||
# {<parameter>: <value>} elements
|
||||
params = dict(parse_qsl(paramstring[1:]))
|
||||
|
||||
# Check the parameters passed to the plugin
|
||||
if params:
|
||||
action = params['action']
|
||||
if action == 'browse_videos':
|
||||
# Browse videos on selected instance
|
||||
self.browse_videos(params['start'])
|
||||
elif action == 'search_videos':
|
||||
# Search for videos on selected instance
|
||||
self.search_videos(params['start'])
|
||||
elif action == 'browse_instances':
|
||||
# Browse peerTube instances
|
||||
self.browse_instances(params['start'])
|
||||
elif action == 'play_video':
|
||||
# Play video from provided URL.
|
||||
self.play_video(params['url'])
|
||||
elif action == 'select_instance':
|
||||
self.select_instance(params['url'])
|
||||
else:
|
||||
# Display the addon's main menu when the plugin is called from Kodi UI without any parameters
|
||||
self.main_menu()
|
||||
|
||||
return None
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
|
||||
# Initialise addon
|
||||
addon = PeertubeAddon(sys.argv[0], int(sys.argv[1]))
|
||||
# Call the router function and pass the plugin call parameters to it.
|
||||
addon.router(sys.argv[2])
|
|
@ -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.
|
||||
"""
|
Binary file not shown.
After Width: | Height: | Size: 6.4 KiB |
|
@ -0,0 +1,196 @@
|
|||
# Kodi Media Center language file
|
||||
# Addon Name: Peertube
|
||||
# Addon id: plugin.video.peertube
|
||||
# Addon Provider: Cyrille B. + Thomas B.
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: Kodi Addons\n"
|
||||
"Report-Msgid-Bugs-To: https://framagit.org/StCyr/plugin.video.peertube\n"
|
||||
"Last-Translator: QNET17\n"
|
||||
"Language-Team: \n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Language: de_DE\n"
|
||||
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
||||
|
||||
# Only IDs from 30000 to 30999 can be used by plugins.
|
||||
# (from https://kodi.wiki/view/Language_support#String_ID_range)
|
||||
|
||||
# -----------------------------------
|
||||
# Settings (from 30000 to 30399)
|
||||
# -----------------------------------
|
||||
|
||||
msgctxt "#30000"
|
||||
msgid "Instance URL"
|
||||
msgstr "URL der Instanz"
|
||||
|
||||
msgctxt "#30001"
|
||||
msgid "Number of items to show per page"
|
||||
msgstr "Anzahl der Elemente, die pro Seite angezeigt werden sollen"
|
||||
|
||||
msgctxt "#30002"
|
||||
msgid "Sort videos by"
|
||||
msgstr "Videos sortieren nach"
|
||||
|
||||
msgctxt "#30003"
|
||||
msgid "Preferred video resolution"
|
||||
msgstr "Bevorzugte Videoauflösung"
|
||||
|
||||
#msgctxt "#30004"
|
||||
#msgid "Delete downloaded videos when Kodi exits"
|
||||
#msgstr "Heruntergeladene Videos löschen, wenn Kodi beendet wird"
|
||||
|
||||
msgctxt "#30005"
|
||||
msgid "List only videos with scope"
|
||||
msgstr "Nur Videos des Typs anzeigen"
|
||||
|
||||
msgctxt "#30006"
|
||||
msgid "General"
|
||||
msgstr "Allgemein"
|
||||
|
||||
msgctxt "#30007"
|
||||
msgid "Browsing/Searching"
|
||||
msgstr "Blättern/Suchen"
|
||||
|
||||
msgctxt "#30008"
|
||||
msgid "Video playback"
|
||||
msgstr "Video-Wiedergabe"
|
||||
|
||||
msgctxt "#30009"
|
||||
msgid "local"
|
||||
msgstr "lokal"
|
||||
|
||||
msgctxt "#30010"
|
||||
msgid "all-local (admins only)"
|
||||
msgstr "alles-lokal (nur für Administratoren)"
|
||||
|
||||
msgctxt "#30011"
|
||||
msgid "likes"
|
||||
msgstr "Gefällt mir"
|
||||
|
||||
msgctxt "#30012"
|
||||
msgid "views"
|
||||
msgstr "Angesehen"
|
||||
|
||||
msgctxt "#30013"
|
||||
msgid "Display a notification when the service starts"
|
||||
msgstr "Eine Benachrichtigung anzeigen, wenn der Dienst startet"
|
||||
|
||||
msgctxt "#30014"
|
||||
msgid "URL of the PeerTube instance to use (with or without the HTTP(s) prefix)"
|
||||
msgstr ""
|
||||
|
||||
msgctxt "#30015"
|
||||
msgid "This notification is especially useful on slow devices where the service"
|
||||
" takes time to start."
|
||||
msgstr ""
|
||||
|
||||
msgctxt "#30016"
|
||||
msgid "The greater the longer the pages will take to load."
|
||||
msgstr ""
|
||||
|
||||
msgctxt "#30017"
|
||||
msgid "Note: only ascending order is supported."
|
||||
msgstr ""
|
||||
|
||||
msgctxt "#30018"
|
||||
msgid "[local] Only display the videos which are local to the selected instance"
|
||||
"\n[all-local] Only display the videos which are local to the selected instance"
|
||||
" [B]plus[/B] the private and unlisted videos (requires admin privileges)"
|
||||
msgstr ""
|
||||
|
||||
msgctxt "#30019"
|
||||
msgid "If the preferred resolution is not available for a given video,"
|
||||
" the lower resolution that is the closest to your preference will be used.\n"
|
||||
"If not available, the higher resolution that is the closest from your"
|
||||
" preference will be used."
|
||||
msgstr ""
|
||||
|
||||
# -----------------------------------
|
||||
# Other strings (from 30400 to 30999)
|
||||
# -----------------------------------
|
||||
|
||||
msgctxt "#30400"
|
||||
msgid "PeerTube service started"
|
||||
msgstr "PeerTube-Dienst gestartet"
|
||||
|
||||
msgctxt "#30401"
|
||||
msgid "Torrents can now be downloaded."
|
||||
msgstr "Torrents können jetzt heruntergeladen werden."
|
||||
|
||||
msgctxt "#30402"
|
||||
msgid "Request error"
|
||||
msgstr "Anforderungsfehler"
|
||||
|
||||
msgctxt "#30403"
|
||||
msgid "No details returned by the server. Check the log for more information."
|
||||
msgstr "Keine Details vom Server zurückgegeben. Prüfen Sie das Protokoll auf weitere Informationen."
|
||||
|
||||
msgctxt "#30404"
|
||||
msgid "{}\n\n----------\nNumber of local videos: {}\nNumber of users: {}"
|
||||
msgstr "{}\n\n----------\nAnzahl der lokalen Videos: {}\nAnzahl der Benutzer: {}"
|
||||
|
||||
msgctxt "#30405"
|
||||
msgid "Next page"
|
||||
msgstr "Nächste Seite"
|
||||
|
||||
msgctxt "#30406"
|
||||
msgid "Browse videos on the selected instance"
|
||||
msgstr "Videos auf der ausgewählten Instanz durchsuchen"
|
||||
|
||||
msgctxt "#30407"
|
||||
msgid "Search videos on the selected instance"
|
||||
msgstr "Videos auf der ausgewählten Instanz suchen"
|
||||
|
||||
msgctxt "#30408"
|
||||
msgid "Select another instance"
|
||||
msgstr "Andere Instanz wählen"
|
||||
|
||||
msgctxt "#30409"
|
||||
msgid "Search videos on {}"
|
||||
msgstr "Videos suchen unter {}"
|
||||
|
||||
msgctxt "#30410"
|
||||
msgid "No videos found"
|
||||
msgstr "Keine Videos gefunden"
|
||||
|
||||
msgctxt "#30411"
|
||||
msgid "No videos found matching the keywords '{}'"
|
||||
msgstr "Keine Videos zu den Stichworten gefunden '{}'"
|
||||
|
||||
msgctxt "#30412"
|
||||
msgid "Error: libtorrent could not be imported"
|
||||
msgstr "Fehler: libtorrent konnte nicht importiert werden"
|
||||
|
||||
msgctxt "#30413"
|
||||
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 {}"
|
||||
|
||||
msgctxt "#30414"
|
||||
msgid "Download started"
|
||||
msgstr "Download gestartet"
|
||||
|
||||
msgctxt "#30415"
|
||||
msgid "The video will be played soon."
|
||||
msgstr "Das Video wird in kürze abgespielt."
|
||||
|
||||
msgctxt "#30416"
|
||||
msgid "Download timeout"
|
||||
msgstr "Zeitüberschreitung beim Herunterladen"
|
||||
|
||||
msgctxt "#30417"
|
||||
msgid "Timeout while downloading {}"
|
||||
msgstr "Timeout beim Herunterladen {}"
|
||||
|
||||
msgctxt "#30418"
|
||||
msgid "Instance changed"
|
||||
msgstr "Instanz gewechselt"
|
||||
|
||||
msgctxt "#30419"
|
||||
msgid "{} is now the selected instance."
|
||||
msgstr "{} ist nun die ausgewählte Instanz."
|
||||
|
||||
msgctxt "#30420"
|
||||
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 {}"
|
|
@ -1,43 +1,196 @@
|
|||
# Kodi Media Center language file
|
||||
# Addon Name: Peertube
|
||||
# Addon id: plugin.video.peertube
|
||||
# Addon Provider: Cyrille Bollu
|
||||
# Addon Provider: Cyrille B. + Thomas B.
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: Kodi Addons\n"
|
||||
"Report-Msgid-Bugs-To: alanwww1@kodi.org\n"
|
||||
"POT-Creation-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||
"Last-Translator: Cyrille Bollu\n"
|
||||
"Report-Msgid-Bugs-To: https://framagit.org/StCyr/plugin.video.peertube\n"
|
||||
"Last-Translator: Thomas B.\n"
|
||||
"Language-Team: \n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Language: en\n"
|
||||
"Language: en_GB\n"
|
||||
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
||||
|
||||
# Settings (from 30000 to 30019)
|
||||
# Only IDs from 30000 to 30999 can be used by plugins.
|
||||
# (from https://kodi.wiki/view/Language_support#String_ID_range)
|
||||
|
||||
# -----------------------------------
|
||||
# Settings (from 30000 to 30399)
|
||||
# -----------------------------------
|
||||
|
||||
msgctxt "#30000"
|
||||
msgid "Preferred instance"
|
||||
msgid "Instance URL"
|
||||
msgstr ""
|
||||
|
||||
msgctxt "#30001"
|
||||
msgid "Number of videos to show per page"
|
||||
msgid "Number of items to show per page"
|
||||
msgstr ""
|
||||
|
||||
msgctxt "#30002"
|
||||
msgid "Sort method to be used when listing videos"
|
||||
msgid "Sort videos by"
|
||||
msgstr ""
|
||||
|
||||
msgctxt "#30003"
|
||||
msgid "Preferred video resolution"
|
||||
msgstr ""
|
||||
|
||||
msgctxt "#30004"
|
||||
msgid "Delete downloaded videos when Kodi exits"
|
||||
msgstr ""
|
||||
#msgctxt "#30004"
|
||||
#msgid "Delete downloaded videos when Kodi exits"
|
||||
#msgstr ""
|
||||
|
||||
msgctxt "#30005"
|
||||
msgid "Filter local or all videos (for browsing only)"
|
||||
msgid "List only videos with scope"
|
||||
msgstr ""
|
||||
|
||||
msgctxt "#30006"
|
||||
msgid "General"
|
||||
msgstr ""
|
||||
|
||||
msgctxt "#30007"
|
||||
msgid "Browsing/Searching"
|
||||
msgstr ""
|
||||
|
||||
msgctxt "#30008"
|
||||
msgid "Video playback"
|
||||
msgstr ""
|
||||
|
||||
msgctxt "#30009"
|
||||
msgid "local"
|
||||
msgstr ""
|
||||
|
||||
msgctxt "#30010"
|
||||
msgid "all-local (admins only)"
|
||||
msgstr ""
|
||||
|
||||
msgctxt "#30011"
|
||||
msgid "likes"
|
||||
msgstr ""
|
||||
|
||||
msgctxt "#30012"
|
||||
msgid "views"
|
||||
msgstr ""
|
||||
|
||||
msgctxt "#30013"
|
||||
msgid "Display a notification when the service starts"
|
||||
msgstr ""
|
||||
|
||||
msgctxt "#30014"
|
||||
msgid "URL of the PeerTube instance to use (with or without the HTTP(s) prefix)"
|
||||
msgstr ""
|
||||
|
||||
msgctxt "#30015"
|
||||
msgid "This notification is especially useful on slow devices where the service"
|
||||
" takes time to start."
|
||||
msgstr ""
|
||||
|
||||
msgctxt "#30016"
|
||||
msgid "The greater the longer the pages will take to load."
|
||||
msgstr ""
|
||||
|
||||
msgctxt "#30017"
|
||||
msgid "Note: only ascending order is supported."
|
||||
msgstr ""
|
||||
|
||||
msgctxt "#30018"
|
||||
msgid "[local] Only display the videos which are local to the selected instance"
|
||||
"\n[all-local] Only display the videos which are local to the selected instance"
|
||||
" [B]plus[/B] the private and unlisted videos (requires admin privileges)"
|
||||
msgstr ""
|
||||
|
||||
msgctxt "#30019"
|
||||
msgid "If the preferred resolution is not available for a given video,"
|
||||
" the lower resolution that is the closest to your preference will be used.\n"
|
||||
"If not available, the higher resolution that is the closest from your"
|
||||
" preference will be used."
|
||||
msgstr ""
|
||||
|
||||
# -----------------------------------
|
||||
# Other strings (from 30400 to 30999)
|
||||
# -----------------------------------
|
||||
|
||||
msgctxt "#30400"
|
||||
msgid "PeerTube service started"
|
||||
msgstr ""
|
||||
|
||||
msgctxt "#30401"
|
||||
msgid "Torrents can now be downloaded."
|
||||
msgstr ""
|
||||
|
||||
msgctxt "#30402"
|
||||
msgid "Request error"
|
||||
msgstr ""
|
||||
|
||||
msgctxt "#30403"
|
||||
msgid "No details returned by the server. Check the log for more information."
|
||||
msgstr ""
|
||||
|
||||
msgctxt "#30404"
|
||||
msgid "{}\n\n----------\nNumber of local videos: {}\nNumber of users: {}"
|
||||
msgstr ""
|
||||
|
||||
msgctxt "#30405"
|
||||
msgid "Next page"
|
||||
msgstr ""
|
||||
|
||||
msgctxt "#30406"
|
||||
msgid "Browse videos on the selected instance"
|
||||
msgstr ""
|
||||
|
||||
msgctxt "#30407"
|
||||
msgid "Search videos on the selected instance"
|
||||
msgstr ""
|
||||
|
||||
msgctxt "#30408"
|
||||
msgid "Select another instance"
|
||||
msgstr ""
|
||||
|
||||
msgctxt "#30409"
|
||||
msgid "Search videos on {}"
|
||||
msgstr ""
|
||||
|
||||
msgctxt "#30410"
|
||||
msgid "No videos found"
|
||||
msgstr ""
|
||||
|
||||
msgctxt "#30411"
|
||||
msgid "No videos found matching the keywords '{}'"
|
||||
msgstr ""
|
||||
|
||||
msgctxt "#30412"
|
||||
msgid "Error: libtorrent could not be imported"
|
||||
msgstr ""
|
||||
|
||||
msgctxt "#30413"
|
||||
msgid "PeerTube cannot play videos without libtorrent\nPlease follow the instructions at {}"
|
||||
msgstr ""
|
||||
|
||||
msgctxt "#30414"
|
||||
msgid "Download started"
|
||||
msgstr ""
|
||||
|
||||
msgctxt "#30415"
|
||||
msgid "The video will be played soon."
|
||||
msgstr ""
|
||||
|
||||
msgctxt "#30416"
|
||||
msgid "Download timeout"
|
||||
msgstr ""
|
||||
|
||||
msgctxt "#30417"
|
||||
msgid "Timeout while downloading {}"
|
||||
msgstr ""
|
||||
|
||||
msgctxt "#30418"
|
||||
msgid "Instance changed"
|
||||
msgstr ""
|
||||
|
||||
msgctxt "#30419"
|
||||
msgid "{} is now the selected instance."
|
||||
msgstr ""
|
||||
|
||||
msgctxt "#30420"
|
||||
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 ""
|
||||
|
|
|
@ -0,0 +1,202 @@
|
|||
# Kodi Media Center language file
|
||||
# Addon Name: Peertube
|
||||
# Addon id: plugin.video.peertube
|
||||
# Addon Provider: Cyrille B. + Thomas B.
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: Kodi Addons\n"
|
||||
"Report-Msgid-Bugs-To: https://framagit.org/StCyr/plugin.video.peertube\n"
|
||||
"Last-Translator: Thomas B.\n"
|
||||
"Language-Team: \n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Language: fr_FR\n"
|
||||
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
||||
|
||||
# Only IDs from 30000 to 30999 can be used by plugins.
|
||||
# (from https://kodi.wiki/view/Language_support#String_ID_range)
|
||||
|
||||
# -----------------------------------
|
||||
# Settings (from 30000 to 30399)
|
||||
# -----------------------------------
|
||||
|
||||
msgctxt "#30000"
|
||||
msgid "Instance URL"
|
||||
msgstr "URL de l'instance"
|
||||
|
||||
msgctxt "#30001"
|
||||
msgid "Number of items to show per page"
|
||||
msgstr "Nombre d'éléments à afficher par page"
|
||||
|
||||
msgctxt "#30002"
|
||||
msgid "Sort videos by"
|
||||
msgstr "Trier les vidéos par"
|
||||
|
||||
msgctxt "#30003"
|
||||
msgid "Preferred video resolution"
|
||||
msgstr "Résolution vidéo préférée"
|
||||
|
||||
#msgctxt "#30004"
|
||||
#msgid "Delete downloaded videos when Kodi exits"
|
||||
#msgstr "Supprimer les vidéos téléchargées à la fermeture de Kodi"
|
||||
|
||||
msgctxt "#30005"
|
||||
msgid "List only videos with scope"
|
||||
msgstr "Afficher uniquement les vidéos de type"
|
||||
|
||||
msgctxt "#30006"
|
||||
msgid "General"
|
||||
msgstr "Général"
|
||||
|
||||
msgctxt "#30007"
|
||||
msgid "Browsing/Searching"
|
||||
msgstr "Navigation/Recherche"
|
||||
|
||||
msgctxt "#30008"
|
||||
msgid "Video playback"
|
||||
msgstr "Lecture"
|
||||
|
||||
msgctxt "#30009"
|
||||
msgid "local"
|
||||
msgstr "local"
|
||||
|
||||
msgctxt "#30010"
|
||||
msgid "all-local (admins only)"
|
||||
msgstr "all-local (admins seulement)"
|
||||
|
||||
msgctxt "#30011"
|
||||
msgid "likes"
|
||||
msgstr "nombre de j'aime"
|
||||
|
||||
msgctxt "#30012"
|
||||
msgid "views"
|
||||
msgstr "nombre de vues"
|
||||
|
||||
msgctxt "#30013"
|
||||
msgid "Display a notification when the service starts"
|
||||
msgstr "Afficher une notification au démarrage du service"
|
||||
|
||||
msgctxt "#30014"
|
||||
msgid "URL of the PeerTube instance to use (with or without the HTTP(s) prefix)"
|
||||
msgstr "URL de l'instance PeerTube à utiliser (avec ou sans le préfixe HTTP(s))"
|
||||
|
||||
msgctxt "#30015"
|
||||
msgid "This notification is especially useful on slow devices where the service"
|
||||
" takes time to start."
|
||||
msgstr "Cette notification est particulièrement utile sur les appareils lents"
|
||||
" où le service met du temps à démarrer."
|
||||
|
||||
msgctxt "#30016"
|
||||
msgid "The greater the longer the pages will take to load."
|
||||
msgstr "Plus ce nombre est élevé plus les pages mettront du temps à charger."
|
||||
|
||||
msgctxt "#30017"
|
||||
msgid "Note: only ascending order is supported."
|
||||
msgstr "Note : les élements ne seront classés que dans l'ordre ascendant."
|
||||
|
||||
msgctxt "#30018"
|
||||
msgid "[local] Only display the videos which are local to the selected instance"
|
||||
"\n[all-local] Only display the videos which are local to the selected instance"
|
||||
" [B]plus[/B] the private and unlisted videos (requires admin privileges)"
|
||||
msgstr "[local] Affiche seulement les vidéos qui sont locales à l'instance sélectionnée"
|
||||
"\n[all-local] Affiche seulement les vidéos qui sont locales à l'instance"
|
||||
" sélectionnée [B]plus[/B] les vidéos privées and non référencées (nécessite les droits admin)"
|
||||
|
||||
msgctxt "#30019"
|
||||
msgid "If the preferred resolution is not available for a given video,"
|
||||
" the lower resolution that is the closest to your preference will be used.\n"
|
||||
"If not available, the higher resolution that is the closest from your"
|
||||
" preference will be used."
|
||||
msgstr "Si la résolution préférée n'est pas disponible pour une vidéo donnée,"
|
||||
" la résolution plus basse et la plus proche de votre préférence sera utilisée.\n"
|
||||
"Si elle n'est pas disponible, la résolution plus haute et la plus proche de"
|
||||
" votre préférence sera utilisée."
|
||||
|
||||
# -----------------------------------
|
||||
# Other strings (from 30400 to 30999)
|
||||
# -----------------------------------
|
||||
|
||||
msgctxt "#30400"
|
||||
msgid "PeerTube service started"
|
||||
msgstr "Le service PeerTube a démarré"
|
||||
|
||||
msgctxt "#30401"
|
||||
msgid "Torrents can now be downloaded."
|
||||
msgstr "Les torrents peuvent maintenant être téléchargés."
|
||||
|
||||
msgctxt "#30402"
|
||||
msgid "Request error"
|
||||
msgstr "Erreur de la requête"
|
||||
|
||||
msgctxt "#30403"
|
||||
msgid "No details returned by the server. Check the log for more information."
|
||||
msgstr "Le serveur n'a pas renvoyé de détails. Voir le journal pour plus d'informations."
|
||||
|
||||
msgctxt "#30404"
|
||||
msgid "{}\n\n----------\nNumber of local videos: {}\nNumber of users: {}"
|
||||
msgstr "{}\n\n----------\nNombre de vidéos locales: {}\nNombre d'utilisateurs: {}"
|
||||
|
||||
msgctxt "#30405"
|
||||
msgid "Next page"
|
||||
msgstr "Page suivante"
|
||||
|
||||
msgctxt "#30406"
|
||||
msgid "Browse videos on the selected instance"
|
||||
msgstr "Parcourir les vidéos sur l'instance sélectionnée"
|
||||
|
||||
msgctxt "#30407"
|
||||
msgid "Search videos on the selected instance"
|
||||
msgstr "Rechercher des vidéos sur l'instance sélectionnée"
|
||||
|
||||
msgctxt "#30408"
|
||||
msgid "Select another instance"
|
||||
msgstr "Sélectionner une autre instance"
|
||||
|
||||
msgctxt "#30409"
|
||||
msgid "Search videos on {}"
|
||||
msgstr "Rechercher des vidéos sur {}"
|
||||
|
||||
msgctxt "#30410"
|
||||
msgid "No videos found"
|
||||
msgstr "Aucune vidéo trouvée"
|
||||
|
||||
msgctxt "#30411"
|
||||
msgid "No videos found matching the keywords '{}'"
|
||||
msgstr "Aucune vidéo ne correspond aux mots-clés '{}'"
|
||||
|
||||
msgctxt "#30412"
|
||||
msgid "Error: libtorrent could not be imported"
|
||||
msgstr "Erreur: libtorrent n'a pas pu être importé"
|
||||
|
||||
msgctxt "#30413"
|
||||
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 {}"
|
||||
|
||||
msgctxt "#30414"
|
||||
msgid "Download started"
|
||||
msgstr "Démarrage du téléchargement"
|
||||
|
||||
msgctxt "#30415"
|
||||
msgid "The video will be played soon."
|
||||
msgstr "La video va être bientôt lue."
|
||||
|
||||
msgctxt "#30416"
|
||||
msgid "Download timeout"
|
||||
msgstr "Délai d'attente dépassé"
|
||||
|
||||
msgctxt "#30417"
|
||||
msgid "Timeout while downloading {}"
|
||||
msgstr "Délai d'attente dépassé lors du téléchargement de {}"
|
||||
|
||||
msgctxt "#30418"
|
||||
msgid "Instance changed"
|
||||
msgstr "Instance modifiée"
|
||||
|
||||
msgctxt "#30419"
|
||||
msgid "{} is now the selected instance."
|
||||
msgstr "{} est maintenant l'instance sélectionnée."
|
||||
|
||||
msgctxt "#30420"
|
||||
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 {}"
|
|
@ -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,463 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Main class used by the add-on
|
||||
|
||||
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 # Module exists only in Kodi - pylint: disable=import-error
|
||||
|
||||
from resources.lib.kodi_utils import kodi
|
||||
from resources.lib.peertube import PeerTube, list_instances
|
||||
|
||||
|
||||
class PeerTubeAddon():
|
||||
"""
|
||||
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):
|
||||
"""Initialize parameters and create a PeerTube instance"""
|
||||
|
||||
# Get the number of items to show per page
|
||||
self.items_per_page = int(kodi.get_setting("items_per_page"))
|
||||
|
||||
# Get the preferred resolution for video
|
||||
self.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
|
||||
# only by this object are directly retrieved from the settings
|
||||
self.peertube = PeerTube(
|
||||
instance=kodi.get_setting("preferred_instance"),
|
||||
count=self.items_per_page)
|
||||
|
||||
def _browse_videos(self, start):
|
||||
"""Display the list of all the videos published on a PeerTube instance
|
||||
|
||||
:param int start: index of the first video to display (pagination)
|
||||
"""
|
||||
|
||||
# Use the API to get the list of the videos
|
||||
results = self.peertube.list_videos(start)
|
||||
|
||||
# Extract the information of each video from the API response
|
||||
list_of_videos = self._create_list_of_videos(results, start)
|
||||
|
||||
# Create the associated items in Kodi
|
||||
kodi.create_items_in_ui(list_of_videos)
|
||||
|
||||
def _browse_instances(self, start):
|
||||
"""
|
||||
Function to navigate through all the PeerTube instances
|
||||
|
||||
:param int start: index of the first instance to display (pagination)
|
||||
"""
|
||||
|
||||
# Use the API to get the list of the instances
|
||||
results = list_instances(start)
|
||||
|
||||
# Extract the information of each instance from the API response
|
||||
list_of_instances = self._create_list_of_instances(results, start)
|
||||
|
||||
# Create the associated items in Kodi
|
||||
kodi.create_items_in_ui(list_of_instances)
|
||||
|
||||
def _create_list_of_instances(self, response, start):
|
||||
"""Generator of instance items to be added in Kodi UI
|
||||
|
||||
:param dict response: data returned by joinpeertube
|
||||
:param int start: index of the first item to display (pagination)
|
||||
:return: yield the information of each item
|
||||
:rtype: dict
|
||||
"""
|
||||
|
||||
for data in response["data"]:
|
||||
|
||||
# The description of each instance in Kodi will be composed of:
|
||||
# * the description of the instance (from joinpeertube.org)
|
||||
# * the number of local videos hosted on this instance
|
||||
# * the number of users on this instance
|
||||
description = kodi.get_string(30404).format(
|
||||
data["shortDescription"],
|
||||
data["totalLocalVideos"],
|
||||
data["totalUsers"]
|
||||
)
|
||||
|
||||
instance_info = kodi.generate_item_info(
|
||||
name=data["name"],
|
||||
url=kodi.build_kodi_url({
|
||||
"action": "select_instance",
|
||||
"url": data["host"]
|
||||
}
|
||||
),
|
||||
is_folder=True,
|
||||
plot=description
|
||||
)
|
||||
|
||||
yield instance_info
|
||||
else:
|
||||
# Add a "Next page" button when there are more items to show
|
||||
next_page_item = self._create_next_page_item(
|
||||
total=int(response["total"]),
|
||||
current_index=start,
|
||||
url=kodi.build_kodi_url(
|
||||
{
|
||||
"action": "browse_instances",
|
||||
"start": start + self.items_per_page
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
if next_page_item:
|
||||
yield next_page_item
|
||||
|
||||
def _create_list_of_videos(self, response, start):
|
||||
"""Generator of video items to be added in Kodi UI
|
||||
|
||||
:param dict response: data returned by PeerTube
|
||||
:param int start: index of the first item to display (pagination)
|
||||
:return: yield the information of each item
|
||||
:rtype: dict
|
||||
"""
|
||||
|
||||
for data in response["data"]:
|
||||
|
||||
video_info = kodi.generate_item_info(
|
||||
name=data["name"],
|
||||
url=kodi.build_kodi_url(
|
||||
{
|
||||
"action": "play_video",
|
||||
"id": data["uuid"]
|
||||
}
|
||||
),
|
||||
is_folder=False,
|
||||
plot=data["description"],
|
||||
duration=data["duration"],
|
||||
thumbnail="{0}/{1}".format(self.peertube.instance,
|
||||
data["thumbnailPath"]),
|
||||
aired=data["publishedAt"]
|
||||
)
|
||||
# Note: the type of video (live or not) is available in "response"
|
||||
# but this information is ignored here so that the "play_video"
|
||||
# action is the same whatever the type of the video. The goal is to
|
||||
# allow external users of the API to play a video only with its ID
|
||||
# without knowing its type.
|
||||
# The information about the type of the video will anyway be
|
||||
# available in the response used to get the URL of a video so this
|
||||
# solution does not impact the performance.
|
||||
|
||||
yield video_info
|
||||
else:
|
||||
# Add a "Next page" button when there are more items to show
|
||||
next_page_item = self._create_next_page_item(
|
||||
total=int(response["total"]),
|
||||
current_index=start,
|
||||
url=kodi.build_kodi_url(
|
||||
{
|
||||
"action": "browse_videos",
|
||||
"start": start + self.items_per_page
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
if next_page_item:
|
||||
yield next_page_item
|
||||
|
||||
def _create_next_page_item(self, total, current_index, url):
|
||||
"""Return the info required to create an item to go to the next page
|
||||
|
||||
:param int total: total number of elements
|
||||
:param int current_index: index of the first element currently used
|
||||
:param str url: URL to reach when the "Next page" item is run
|
||||
:return: yield the info to create a "Next page" item in Kodi UI if
|
||||
there are more items to show
|
||||
:rtype: dict
|
||||
"""
|
||||
next_index = current_index + self.items_per_page
|
||||
if total > next_index:
|
||||
next_page = (next_index // self.items_per_page) + 1
|
||||
total_pages = (total // self.items_per_page) + 1
|
||||
|
||||
next_page_item = kodi.generate_item_info(
|
||||
name="{} ({}/{})".format(kodi.get_string(30405),
|
||||
next_page,
|
||||
total_pages),
|
||||
url=url
|
||||
)
|
||||
|
||||
return next_page_item
|
||||
|
||||
def _get_video_url(self, video_id, instance=None):
|
||||
"""Return the URL of a video and its type (live or not)
|
||||
|
||||
Find the URL of the video with the best possible quality matching
|
||||
user's preferences.
|
||||
The information whether the video is live or not will also be returned.
|
||||
|
||||
:param str video_id: ID of the torrent linked with the video
|
||||
:param str instance: PeerTube instance hosting the video (optional)
|
||||
: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
|
||||
string
|
||||
:rtype: tuple
|
||||
"""
|
||||
# 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
|
||||
current_resolution = 0
|
||||
higher_resolution = -1
|
||||
url = ""
|
||||
is_live = False
|
||||
for video in video_files:
|
||||
# Get the 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:
|
||||
# Stop directly when we find the exact same resolution as the
|
||||
# user's preferred one
|
||||
kodi.debug("Found video with preferred resolution ({})"
|
||||
.format(self.preferred_resolution))
|
||||
url = video["url"]
|
||||
return (is_live, url)
|
||||
elif (resolution < self.preferred_resolution
|
||||
and resolution > current_resolution):
|
||||
# Otherwise, try to find the best one just below the user's
|
||||
# preferred one
|
||||
kodi.debug("Found video with good lower resolution ({})"
|
||||
.format(resolution))
|
||||
url = video["url"]
|
||||
current_resolution = resolution
|
||||
elif (resolution > self.preferred_resolution and
|
||||
(resolution < higher_resolution or
|
||||
higher_resolution == -1)):
|
||||
# In the worst case, we'll take the one just above the user's
|
||||
# preferred one
|
||||
kodi.debug("Saving video with higher resolution ({}) as a"
|
||||
" possible alternative".format(resolution))
|
||||
backup_url = video["url"]
|
||||
higher_resolution = resolution
|
||||
else:
|
||||
kodi.debug("Ignoring the resolution '{}'".format(resolution))
|
||||
|
||||
# When we didn't find a resolution equal or lower than the user's
|
||||
# preferred one, use the resolution just above the preferred one
|
||||
if not url:
|
||||
kodi.debug("Using video with higher resolution as alternative ({})"
|
||||
.format(higher_resolution))
|
||||
url = backup_url
|
||||
|
||||
return (is_live, url)
|
||||
|
||||
def _home_page(self):
|
||||
"""Display the items of the home page of the add-on"""
|
||||
|
||||
home_page_items = [
|
||||
kodi.generate_item_info(
|
||||
name=kodi.get_string(30406),
|
||||
url=kodi.build_kodi_url({"action": "browse_videos","start": 0})
|
||||
),
|
||||
kodi.generate_item_info(
|
||||
name=kodi.get_string(30407),
|
||||
url=kodi.build_kodi_url({"action": "search_videos","start": 0})
|
||||
),
|
||||
kodi.generate_item_info(
|
||||
name=kodi.get_string(30408),
|
||||
url=kodi.build_kodi_url({
|
||||
"action": "browse_instances",
|
||||
"start": 0
|
||||
}
|
||||
)
|
||||
)
|
||||
]
|
||||
|
||||
kodi.create_items_in_ui(home_page_items)
|
||||
|
||||
def _search_videos(self, start):
|
||||
"""
|
||||
Function to search for videos on a PeerTube instance
|
||||
|
||||
:param str start: index of the first video to display (pagination)
|
||||
"""
|
||||
|
||||
# Ask the user which keywords must be searched for
|
||||
keywords = kodi.open_input_box(
|
||||
title=kodi.get_string(30409).format(self.peertube.instance))
|
||||
|
||||
# Go back to the home page when the user cancels or didn't enter any
|
||||
# string
|
||||
if not keywords:
|
||||
return
|
||||
|
||||
# Use the API to search for videos
|
||||
results = self.peertube.search_videos(keywords, start)
|
||||
|
||||
# Exit directly when no result is found
|
||||
if not results:
|
||||
kodi.notif_warning(
|
||||
title=kodi.get_string(30410),
|
||||
message=kodi.get_string(30411).format(keywords))
|
||||
return
|
||||
|
||||
# Extract the information of each video from the API response
|
||||
list_of_videos = self._create_list_of_videos(results, start)
|
||||
|
||||
# Create the associated items in Kodi
|
||||
kodi.create_items_in_ui(list_of_videos)
|
||||
|
||||
def _play_video(self, torrent_url):
|
||||
"""
|
||||
Start the torrent's download and play it while being downloaded
|
||||
|
||||
:param str torrent_url: URL of the torrent file to download and play
|
||||
"""
|
||||
# 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:
|
||||
kodi.open_dialog(
|
||||
title=kodi.get_string(30412),
|
||||
message=kodi.get_string(30413).format(self.HELP_URL))
|
||||
return
|
||||
|
||||
kodi.debug("Starting torrent download ({})".format(torrent_url))
|
||||
kodi.notif_info(title=kodi.get_string(30414),
|
||||
message=kodi.get_string(30415))
|
||||
|
||||
# Start a downloader thread
|
||||
AddonSignals.sendSignal("start_download", {"url": torrent_url})
|
||||
|
||||
# Wait until the PeerTubeDownloader has downloaded all the torrent's
|
||||
# metadata
|
||||
AddonSignals.registerSlot(kodi.addon_id,
|
||||
"metadata_downloaded",
|
||||
self._play_video_continue)
|
||||
timeout = 0
|
||||
while not self.play and timeout < 10:
|
||||
kodi.sleep(1000)
|
||||
timeout += 1
|
||||
|
||||
# Abort in case of timeout
|
||||
if timeout == 10:
|
||||
kodi.notif_error(
|
||||
title=kodi.get_string(30416),
|
||||
message=kodi.get_string(30417).format(torrent_url))
|
||||
return
|
||||
else:
|
||||
# Wait a little before starting playing the torrent
|
||||
kodi.sleep(3000)
|
||||
|
||||
# Pass the item to the Kodi player for actual playback.
|
||||
kodi.debug("Starting video playback ({})".format(self.torrent_file))
|
||||
kodi.play(self.torrent_file)
|
||||
|
||||
def _play_video_continue(self, data):
|
||||
"""
|
||||
Callback function to let the _play_video method resume when the
|
||||
PeertubeDownloader has downloaded all the torrent's metadata
|
||||
|
||||
:param data: dict of information sent from PeertubeDownloader
|
||||
"""
|
||||
|
||||
kodi.debug(
|
||||
"Received metadata_downloaded signal, will start playing media")
|
||||
self.play = True
|
||||
self.torrent_file = data["file"].encode("utf-8")
|
||||
|
||||
def _select_instance(self, instance):
|
||||
"""
|
||||
Change currently selected instance to "instance" parameter
|
||||
|
||||
:param str instance: URL of the new instance
|
||||
"""
|
||||
|
||||
# Update the PeerTube object attribute even though it is not used
|
||||
# currently (because the value will be retrieved from the settings on
|
||||
# the next run of the add-on but it may be useful in case
|
||||
# reuselanguageinvoker is enabled)
|
||||
self.peertube.set_instance(instance)
|
||||
|
||||
# Update the preferred instance in the settings so that this choice is
|
||||
# reused on the next runs and the next calls of the add-on
|
||||
kodi.set_setting("preferred_instance", instance)
|
||||
|
||||
# Notify the user and log the event
|
||||
kodi.notif_info(
|
||||
title=kodi.get_string(30418),
|
||||
message=kodi.get_string(30419).format(self.peertube.instance))
|
||||
|
||||
kodi.debug("{} is now the selected instance"
|
||||
.format(self.peertube.instance))
|
||||
|
||||
def router(self, params):
|
||||
"""Route the add-on to the requested actions
|
||||
|
||||
:param dict params: Parameters the add-on was called with
|
||||
"""
|
||||
|
||||
# Check the parameters passed to the plugin
|
||||
if params:
|
||||
action = params["action"]
|
||||
if action == "browse_videos":
|
||||
# Browse videos on the selected instance
|
||||
self._browse_videos(int(params["start"]))
|
||||
elif action == "search_videos":
|
||||
# Search for videos on the selected instance
|
||||
self._search_videos(int(params["start"]))
|
||||
elif action == "browse_instances":
|
||||
# Browse PeerTube instances
|
||||
self._browse_instances(int(params["start"]))
|
||||
elif action == "play_video":
|
||||
# This action comes with the id of the video to play as
|
||||
# parameter. The instance may also be in the parameters. Use
|
||||
# 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":
|
||||
# Set the selected instance as the preferred instance
|
||||
self._select_instance(params["url"])
|
||||
else:
|
||||
# Display the addon's main menu when the plugin is called from
|
||||
# Kodi UI without any parameters
|
||||
self._home_page()
|
||||
|
||||
# Display a warning if libtorrent could not be imported
|
||||
if not self.libtorrent_imported:
|
||||
kodi.open_dialog(
|
||||
title=kodi.get_string(30412),
|
||||
message=kodi.get_string(30420).format(self.HELP_URL))
|
|
@ -0,0 +1,270 @@
|
|||
# -*- 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 os
|
||||
|
||||
from requests.compat import urlencode
|
||||
from urllib.parse import parse_qsl
|
||||
|
||||
import xbmc
|
||||
import xbmcaddon
|
||||
import xbmcgui
|
||||
import xbmcplugin
|
||||
|
||||
|
||||
class KodiUtils:
|
||||
"""Utility class to call Kodi APIs"""
|
||||
|
||||
def __init__(self):
|
||||
"""Initialize the object with information about the add-on"""
|
||||
self.addon_name = xbmcaddon.Addon().getAddonInfo("name")
|
||||
self.addon_id = xbmcaddon.Addon().getAddonInfo("id")
|
||||
self.addon_media = os.path.join(xbmcaddon.Addon().getAddonInfo("path"),
|
||||
"resources", "media")
|
||||
|
||||
# Prepare other attributes that will be initialized with sys.argv
|
||||
self.addon_url = ""
|
||||
self.addon_handle = 0
|
||||
self.addon_parameters = ""
|
||||
|
||||
def build_kodi_url(self, parameters):
|
||||
"""Build a Kodi URL based on the parameters.
|
||||
|
||||
This URL will be used to call the add-on with the expected parameters.
|
||||
|
||||
:param dict parameters: The parameters that will be encoded in the URL
|
||||
"""
|
||||
|
||||
return "{}?{}".format(self.addon_url, urlencode(parameters))
|
||||
|
||||
def create_items_in_ui(self, items_info):
|
||||
"""Create items in Kodi UI
|
||||
|
||||
:param list items_info: A list of dict containing all the required
|
||||
information to create the items (i.e. the return value of the method
|
||||
generate_item_info)
|
||||
"""
|
||||
# Tell Kodi to use the "video" viewtypes
|
||||
xbmcplugin.setContent(handle=self.addon_handle, content="videos")
|
||||
|
||||
list_of_items = []
|
||||
|
||||
for info in items_info:
|
||||
# Create the ListItem object
|
||||
list_item = xbmcgui.ListItem(label=info["name"])
|
||||
|
||||
# Add the general info of the item
|
||||
list_item.setInfo("video", info["info"])
|
||||
|
||||
# Add the art info of the item
|
||||
list_item.setArt(info["art"])
|
||||
|
||||
if not info["is_folder"]:
|
||||
list_item.setProperty("IsPlayable", "true")
|
||||
|
||||
# Add to the list the tuple expected by addDirectoryItems
|
||||
list_of_items.append((info["url"], list_item, info["is_folder"]))
|
||||
|
||||
# Create the items
|
||||
xbmcplugin.addDirectoryItems(
|
||||
handle=self.addon_handle,
|
||||
items=list_of_items,
|
||||
totalItems=len(list_of_items)
|
||||
)
|
||||
|
||||
# Terminate the items creation
|
||||
xbmcplugin.endOfDirectory(self.addon_handle)
|
||||
|
||||
def debug(self, message, prefix=None):
|
||||
"""Log a message in Kodi's log with the level xbmc.LOGDEBUG
|
||||
|
||||
The message will be prefixed with the prefix passed as argument or with
|
||||
the name of the add-on.
|
||||
|
||||
:param str message: Message to log
|
||||
:param str prefix: String to prefix the message with
|
||||
"""
|
||||
if not prefix:
|
||||
prefix = self.addon_name
|
||||
|
||||
xbmc.log("[{}] {}".format(prefix, message), xbmc.LOGDEBUG)
|
||||
|
||||
def generate_item_info(self, name, url, is_folder=True, thumbnail="",
|
||||
aired="", duration=0, plot="",):
|
||||
"""Return all the information required to create an item in Kodi UI
|
||||
|
||||
This function makes the creation of an item easier: it allows to pass
|
||||
to the function only the known information about an item, and it will
|
||||
return a dict with all the keys expected by create_items_in_ui
|
||||
correctly initialized (including the ones that were not passed).
|
||||
|
||||
:param str name: Name of the item
|
||||
:param str url: URL to reach when the item is used
|
||||
:param bool is_folder: Whether the item is a folder or is playable
|
||||
:param <other>: The other parameters are the ones expected by
|
||||
ListItem.setInfo() and ListItem.setArt()
|
||||
:return: Information required to create the item in Kodi UI
|
||||
:rtype: dict
|
||||
"""
|
||||
return {
|
||||
"name": name,
|
||||
"url": url,
|
||||
"is_folder": is_folder,
|
||||
"art": {
|
||||
"thumb": thumbnail,
|
||||
},
|
||||
"info": {
|
||||
"aired": aired,
|
||||
"duration": duration,
|
||||
"plot": plot,
|
||||
"title": name
|
||||
}
|
||||
}
|
||||
|
||||
def get_property(self, 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: Value of the window property
|
||||
:rtype: str
|
||||
"""
|
||||
return xbmcgui.Window(10000).getProperty("peertube_{}".format(name))
|
||||
|
||||
def get_run_parameters(self):
|
||||
"""Return the parameter the add-on was called with
|
||||
|
||||
The parameters are read in the method "update_call_info"
|
||||
|
||||
:return: The extracted parameters
|
||||
:rtype: dict
|
||||
"""
|
||||
# The first character ("?") is skipped
|
||||
return dict(parse_qsl(self.addon_parameters[1:]))
|
||||
|
||||
def get_setting(self, setting_name):
|
||||
"""Retrieve the value of a setting
|
||||
|
||||
:param str setting_name: Name of the setting
|
||||
:return: Value of the setting named setting_name
|
||||
:rtype: str
|
||||
"""
|
||||
return xbmcaddon.Addon().getSetting(setting_name)
|
||||
|
||||
def get_string(self, string_id):
|
||||
"""Retrieve a localized string
|
||||
|
||||
:param int string_id: ID of the string in strings.po
|
||||
:return: the localized value of the string
|
||||
:rtype: str
|
||||
"""
|
||||
return xbmcaddon.Addon().getLocalizedString(string_id)
|
||||
|
||||
def notif_error(self, 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=os.path.join(self.addon_media, "icon_error.png"))
|
||||
|
||||
def notif_info(self, 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=os.path.join(self.addon_media, "icon_info.png"))
|
||||
|
||||
def notif_warning(self, 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=os.path.join(self.addon_media, "icon_warning.png"))
|
||||
|
||||
def open_dialog(self, 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, message=message)
|
||||
|
||||
def open_input_box(self, title):
|
||||
"""Open a box for the user to input alphanumeric data
|
||||
|
||||
:param str title: Title of the box
|
||||
:return: Entered data as a unicode string
|
||||
:rtype: str
|
||||
"""
|
||||
entered_string = xbmcgui.Dialog().input(heading=title,
|
||||
type=xbmcgui.INPUT_ALPHANUM)
|
||||
|
||||
# Check the type of the string against the type "bytes" to confirm if
|
||||
# it is a unicode or a byte string ("bytes" is known in both python 2
|
||||
# and 3).
|
||||
if isinstance(entered_string, bytes):
|
||||
return entered_string.decode("utf-8")
|
||||
else:
|
||||
return entered_string
|
||||
|
||||
def play(self, url):
|
||||
"""Play the media behind the URL
|
||||
|
||||
:param str url: URL of the media to play
|
||||
"""
|
||||
xbmcplugin.setResolvedUrl(handle=self.addon_handle,
|
||||
succeeded=True,
|
||||
listitem=xbmcgui.ListItem(path=url))
|
||||
|
||||
def set_property(self, 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)
|
||||
|
||||
def set_setting(self, setting_name, setting_value):
|
||||
"""Modify the value of a setting
|
||||
|
||||
:param str setting_name: Name of the setting
|
||||
:param str setting_value: New value of the setting
|
||||
"""
|
||||
xbmcaddon.Addon().setSetting(setting_name, setting_value)
|
||||
|
||||
def sleep(self, time_us):
|
||||
"""Sleep for some micro seconds
|
||||
|
||||
:param int time_us: Sleep time in micro seconds
|
||||
"""
|
||||
xbmc.sleep(time_us)
|
||||
|
||||
def update_call_info(self, argv):
|
||||
"""Update the attributes related to the current call of the add-on
|
||||
|
||||
:param list argv: System arguments
|
||||
"""
|
||||
self.addon_url = argv[0]
|
||||
self.addon_handle = int(argv[1])
|
||||
self.addon_parameters = argv[2]
|
||||
|
||||
kodi = KodiUtils()
|
|
@ -0,0 +1,285 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
PeerTube related classes and functions
|
||||
|
||||
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 requests
|
||||
from requests.compat import urljoin
|
||||
|
||||
from resources.lib.kodi_utils import kodi
|
||||
|
||||
|
||||
class PeerTube:
|
||||
"""A class to interact easily with PeerTube instances using REST APIs"""
|
||||
|
||||
def __init__(self, instance, count):
|
||||
"""Initialize the parameters that will be used in the requests
|
||||
|
||||
Some values are retrieved directly from the settings, others come as
|
||||
arguments because they are used somewhere else in the add-on.
|
||||
|
||||
:param str instance: URL of the PeerTube instance
|
||||
:param int count: number of items to display
|
||||
"""
|
||||
self.set_instance(instance)
|
||||
|
||||
self.list_settings = {
|
||||
"sort": self._get_sort_method(),
|
||||
"count": count
|
||||
}
|
||||
|
||||
self.filter = self._get_video_filter()
|
||||
|
||||
def _request(self, method, url, params=None, data=None, instance=None):
|
||||
"""Call a REST API on the instance
|
||||
|
||||
:param str method: REST API method (get, post, put, delete, etc.)
|
||||
:param str url: URL of the REST API endpoint relative to the PeerTube
|
||||
instance
|
||||
:param dict params: dict of the parameters to send in the request
|
||||
:param dict data: dict of the data to send with the request
|
||||
:param str instance: URL of the instance hosting the video. The
|
||||
configured instance will be used if empty.
|
||||
:return: the response as JSON data
|
||||
:rtype: dict
|
||||
"""
|
||||
# If no instance was provided, use the one from the settings (which was
|
||||
# used when instantianting this object)
|
||||
if instance is None:
|
||||
instance = self.instance
|
||||
else:
|
||||
# If an instance was provided ensure the URL is prefixed with HTTPS
|
||||
if not instance.startswith("https://"):
|
||||
instance = "https://{}".format(instance)
|
||||
|
||||
# Build the URL of the REST API
|
||||
api_url = urljoin("{}/api/v1/".format(instance), url)
|
||||
|
||||
# Send a request with a time-out of 5 seconds
|
||||
response = requests.request(method=method,
|
||||
url=api_url,
|
||||
timeout=5,
|
||||
params=params,
|
||||
data=data)
|
||||
|
||||
json = response.json()
|
||||
|
||||
# Use Request.raise_for_status() to raise an exception if the HTTP
|
||||
# request didn't succeed
|
||||
try:
|
||||
response.raise_for_status()
|
||||
except requests.HTTPError as exception:
|
||||
# Print in Kodi's log some information about the request
|
||||
kodi.debug("Error when sending a {} request to {} with params={}"
|
||||
" and data={}".format(method, url, params, data))
|
||||
|
||||
# Report the error to the user with a notification: if the response
|
||||
# contains an "error" attribute, use it as error message, otherwise
|
||||
# use a default message.
|
||||
# Note: in case the error attribute is used, the message will be in
|
||||
# English whatever the language configured by the user. It's better
|
||||
# to share the information with the user (even if it's not in its
|
||||
# language) rather than always redirecting to the Kodi's log.
|
||||
if "error" in json:
|
||||
message = json["error"]
|
||||
kodi.debug(message)
|
||||
else:
|
||||
message = kodi.get_string(30403)
|
||||
|
||||
kodi.notif_error(title=kodi.get_string(30402), message=message)
|
||||
raise exception
|
||||
|
||||
return json
|
||||
|
||||
def _build_params(self, **kwargs):
|
||||
"""Build the parameters to send with a request from the common settings
|
||||
|
||||
This method returns a dictionnary containing the common settings from
|
||||
self.list_settings plus the arguments passed to this function. The keys
|
||||
in the dictionnary will have the same name as the arguments passed to
|
||||
this function.
|
||||
|
||||
:return: the common settings plus other parameters
|
||||
:rtype: dict
|
||||
"""
|
||||
# Initialize the dict from the common settings (the common settings are
|
||||
# copied otherwise any modification will also impact the attribute).
|
||||
params = self.list_settings.copy()
|
||||
|
||||
# Add all the arguments to the dict
|
||||
params.update(kwargs)
|
||||
|
||||
return params
|
||||
|
||||
def _get_video_filter(self):
|
||||
"""Get the video filter from the settings
|
||||
|
||||
The value of the associated setting is localized so a list is used to
|
||||
get the value expected by the API based on the index of the value used.
|
||||
|
||||
:return: value of the video_filter setting
|
||||
:rtype: str
|
||||
"""
|
||||
|
||||
filters = ["local", "all-local"]
|
||||
return filters[int(kodi.get_setting("video_filter"))]
|
||||
|
||||
def _get_sort_method(self):
|
||||
"""Get the sort method from the settings
|
||||
|
||||
The value of the associated setting is localized so a list is used to
|
||||
get the value expected by the API based on the index of the value used.
|
||||
|
||||
:return: value of the video_sort_method setting
|
||||
:rtype: str
|
||||
"""
|
||||
|
||||
sort_methods = ["likes", "views"]
|
||||
return sort_methods[int(kodi.get_setting("video_sort_method"))]
|
||||
|
||||
def get_video_urls(self, video_id, instance=None):
|
||||
"""Return the URLs of a video
|
||||
|
||||
PeerTube creates 1 URL for each resolution of a video so this method
|
||||
returns a list of URL/resolution pairs. In the case of a live video,
|
||||
only an URL will be returned (no resolution).
|
||||
|
||||
:param str video_id: ID or UUID of the video
|
||||
:param str instance: URL of the instance hosting the video. The
|
||||
configured instance will be used if empty.
|
||||
:return: pair(s) of URL/resolution
|
||||
:rtype: generator
|
||||
"""
|
||||
# Get the information about the video
|
||||
metadata = self._request(method="GET",
|
||||
url="videos/{}".format(video_id),
|
||||
instance=instance)
|
||||
|
||||
if metadata["isLive"]:
|
||||
# When the video is a live, yield the unique playlist URL (there is
|
||||
# no resolution in this case)
|
||||
yield {
|
||||
"url": metadata['streamingPlaylists'][0]['playlistUrl'],
|
||||
}
|
||||
else:
|
||||
# For non live videos, the files corresponding to different
|
||||
# resolutions available for a video may be stored in "files" or
|
||||
# "streamingPlaylists[].files" depending if WebTorrent is enabled
|
||||
# or not. Note that "files" will always exist in the response but
|
||||
# may be empty so len() must be used.
|
||||
if len(metadata["files"]) != 0:
|
||||
files = metadata["files"]
|
||||
else:
|
||||
files = metadata["streamingPlaylists"][0]["files"]
|
||||
|
||||
for file in files:
|
||||
yield {
|
||||
"resolution": int(file["resolution"]["id"]),
|
||||
"url": file["torrentUrl"],
|
||||
}
|
||||
|
||||
def list_videos(self, start):
|
||||
"""List the videos in the instance
|
||||
|
||||
:param int start: index of the first video to display
|
||||
:return: the list of videos as returned by the REST API
|
||||
:rtype: dict
|
||||
"""
|
||||
# Build the parameters that will be sent in the request
|
||||
params = self._build_params(filter=self.filter, start=start)
|
||||
|
||||
return self._request(method="GET", url="videos", params=params)
|
||||
|
||||
def search_videos(self, keywords, start):
|
||||
"""Search for videos on the instance and beyond.
|
||||
|
||||
:param str keywords: keywords to seach for
|
||||
:param int start: index of the first video to display
|
||||
:return: the videos matching the keywords as returned by the REST API
|
||||
or None if there are no matches returned
|
||||
:rtype: dict
|
||||
"""
|
||||
# Build the parameters that will be sent in the request
|
||||
params = self._build_params(search=keywords,
|
||||
filter=self.filter,
|
||||
start=start)
|
||||
|
||||
response = self._request(method="GET",
|
||||
url="search/videos",
|
||||
params=params)
|
||||
|
||||
if response["total"] == 0:
|
||||
return None
|
||||
else:
|
||||
return response
|
||||
|
||||
def set_instance(self, instance):
|
||||
"""Set the URL of the current instance with the right format
|
||||
|
||||
The URL of the instance may not be prefixed with HTTPS, for instance:
|
||||
* in the settings the URL does not use this prefix to allow the user to
|
||||
change it easily
|
||||
* the URL from the list of instances is not prefixed
|
||||
This method is used to ensure the URL is correctly prefixed with HTTPS
|
||||
|
||||
:param str instance: URL of the instance
|
||||
"""
|
||||
if not instance.startswith("https://"):
|
||||
instance = "https://{}".format(instance)
|
||||
|
||||
self.instance = instance
|
||||
|
||||
|
||||
def list_instances(start):
|
||||
"""List all the peertube instances from joinpeertube.org
|
||||
|
||||
:param int start: index of the first instance to display
|
||||
:return: the list of instances as returned by the REST API
|
||||
:rtype: dict
|
||||
"""
|
||||
# URL of the REST API
|
||||
api_url = "https://instances.joinpeertube.org/api/v1/instances"
|
||||
# Build the parameters that will be sent in the request from the settings
|
||||
params = {
|
||||
"count": kodi.get_setting("items_per_page"),
|
||||
"start": start
|
||||
}
|
||||
|
||||
# Send a request with a time-out of 5 seconds
|
||||
response = requests.get(url=api_url, timeout=5, params=params)
|
||||
|
||||
json = response.json()
|
||||
|
||||
# Use Request.raise_for_status() to raise an exception if the HTTP
|
||||
# request didn't succeed
|
||||
try:
|
||||
response.raise_for_status()
|
||||
except requests.HTTPError as exception:
|
||||
# Print in Kodi's log some information about the request
|
||||
kodi.debug("Error when getting the list of instances with params={}"
|
||||
.format(params))
|
||||
|
||||
# Report the error to the user with a notification: use the details of
|
||||
# the error if it exists in the response, otherwise use a default
|
||||
# message.
|
||||
# Note: in case the error is reused from the response, the message will
|
||||
# be in English whatever the language configured by the user. It's
|
||||
# better to share the information with the user (even if it's not in
|
||||
# its language) rather than always redirecting to the Kodi's log.
|
||||
try:
|
||||
# Convert the reponse to a list to get the first error whatever its
|
||||
# name. Then get the second element in the sublist which contains
|
||||
# the details of the error.
|
||||
message = list(json["errors"].items())[0][1]["msg"]
|
||||
kodi.debug(message)
|
||||
except KeyError:
|
||||
message = kodi.get_string(30403)
|
||||
kodi.notif_error(title=kodi.get_string(30402), message=message)
|
||||
raise exception
|
||||
|
||||
return json
|
Binary file not shown.
After Width: | Height: | Size: 8.1 KiB |
Binary file not shown.
After Width: | Height: | Size: 10 KiB |
Binary file not shown.
After Width: | Height: | Size: 11 KiB |
|
@ -1,11 +1,82 @@
|
|||
<?xml version="1.0" encoding="utf-8" standalone="yes"?>
|
||||
<settings>
|
||||
<setting id="preferred_instance" type="text" default="https://framatube.org" label="30000"/>
|
||||
<setting type="sep"/>
|
||||
<setting id="items_per_page" type="select" values="10|20|50|100" default="20" label="30001"/>
|
||||
<setting id="video_sort_method" type="select" values="likes|views" default='views' label="30002"/>
|
||||
<setting id="preferred_resolution" type="select" values="1080|720|480|360|240" default='480' label="30003"/>
|
||||
<setting type="sep"/>
|
||||
<setting id="delete_files" type="bool" default="true" label="30004"/>
|
||||
<setting id="video_filter" type="select" values="local|all-local (requires admin privileges)" default="local" label="30005"/>
|
||||
<?xml version="1.0" ?>
|
||||
<settings version="1">
|
||||
<section id="plugin.video.peertube">
|
||||
<category help="" id="general" label="30006">
|
||||
<group id="1">
|
||||
<setting help="30014" id="preferred_instance" label="30000" type="string">
|
||||
<level>0</level>
|
||||
<default>framatube.org</default>
|
||||
<control format="string" type="edit">
|
||||
<heading>30000</heading>
|
||||
</control>
|
||||
</setting>
|
||||
<setting help="30015" id="service_start_notif" label="30013" type="boolean">
|
||||
<level>0</level>
|
||||
<default>true</default>
|
||||
<control type="toggle"/>
|
||||
</setting>
|
||||
</group>
|
||||
<group id="2" label="30007">
|
||||
<setting help="30016" id="items_per_page" label="30001" type="string">
|
||||
<level>0</level>
|
||||
<default>20</default>
|
||||
<constraints>
|
||||
<options>
|
||||
<option>10</option>
|
||||
<option>20</option>
|
||||
<option>50</option>
|
||||
<option>100</option>
|
||||
</options>
|
||||
</constraints>
|
||||
<control format="string" type="list">
|
||||
<heading>30001</heading>
|
||||
</control>
|
||||
</setting>
|
||||
<setting help="30017" id="video_sort_method" label="30002" type="integer">
|
||||
<level>0</level>
|
||||
<default>1</default>
|
||||
<constraints>
|
||||
<options>
|
||||
<option label="30011">0</option>
|
||||
<option label="30012">1</option>
|
||||
</options>
|
||||
</constraints>
|
||||
<control format="string" type="list">
|
||||
<heading>30002</heading>
|
||||
</control>
|
||||
</setting>
|
||||
<setting help="30018" id="video_filter" label="30005" type="integer">
|
||||
<level>0</level>
|
||||
<default>0</default>
|
||||
<constraints>
|
||||
<options>
|
||||
<option label="30009">0</option>
|
||||
<option label="30010">1</option>
|
||||
</options>
|
||||
</constraints>
|
||||
<control format="string" type="list">
|
||||
<heading>30005</heading>
|
||||
</control>
|
||||
</setting>
|
||||
</group>
|
||||
<group id="3" label="30008">
|
||||
<setting help="30019" id="preferred_resolution" label="30003" type="string">
|
||||
<level>0</level>
|
||||
<default>480</default>
|
||||
<constraints>
|
||||
<options>
|
||||
<option>1080</option>
|
||||
<option>720</option>
|
||||
<option>480</option>
|
||||
<option>360</option>
|
||||
<option>240</option>
|
||||
</options>
|
||||
</constraints>
|
||||
<control format="string" type="list">
|
||||
<heading>30003</heading>
|
||||
</control>
|
||||
</setting>
|
||||
</group>
|
||||
</category>
|
||||
</section>
|
||||
</settings>
|
||||
|
|
121
service.py
121
service.py
|
@ -1,17 +1,31 @@
|
|||
import libtorrent
|
||||
import time, sys
|
||||
import xbmc, xbmcvfs
|
||||
import AddonSignals
|
||||
# -*- 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 # Module exists only in Kodi - pylint: disable=import-error
|
||||
from threading import Thread
|
||||
import xbmc
|
||||
import xbmcvfs
|
||||
|
||||
from resources.lib.kodi_utils import kodi
|
||||
|
||||
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
|
||||
"""
|
||||
|
@ -19,6 +33,14 @@ class PeertubeDownloader(Thread):
|
|||
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)
|
||||
"""
|
||||
kodi.debug(message=message, prefix="PeertubeDownloader")
|
||||
|
||||
def run(self):
|
||||
"""
|
||||
Download the torrent specified by self.torrent
|
||||
|
@ -26,88 +48,113 @@ 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)
|
||||
h = ses.add_torrent({'url': self.torrent, 'save_path': self.temp_dir})
|
||||
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})
|
||||
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)
|
||||
"""
|
||||
kodi.debug(message=message, prefix="PeertubeService")
|
||||
|
||||
def download_torrent(self, data):
|
||||
"""
|
||||
Start a downloader thread to download torrent specified by data['url']
|
||||
Start a downloader thread to download torrent specified by data["url"]
|
||||
:param data: dict
|
||||
: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(kodi.addon_id,
|
||||
"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")
|
||||
if kodi.get_setting("service_start_notif") == "true":
|
||||
kodi.notif_info(title=kodi.get_string(30400),
|
||||
message=kodi.get_string(30401))
|
||||
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)
|
||||
if __name__ == "__main__":
|
||||
# Create a PeertubeService instance
|
||||
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
|
||||
service.run()
|
||||
xbmc.log('PeertubeService: Exiting', xbmc.LOGDEBUG)
|
||||
|
|
Loading…
Reference in New Issue