Compare commits

...

31 Commits
0.3.2 ... main

Author SHA1 Message Date
Thomas 11b92d5896 Use the new format for settings.xml
* Convert settings.xml to the new format supported in Kodi 19
* Create help strings to guide the users

Note: the translation of the new help strings in German is missing.
2021-09-03 16:47:47 +00:00
Thomas 8dec0f316e Update code for python 3
Drop support of python 2 and make the code compatible with python 3 so that the add-on works on Kodi 19.

Update Kodistubs version in the CI and get rid of the python 2 actions.

Note: script.module.libtorrent is disabled because it will not be used on Matrix (and is not compatible).
It will be replaced by vfs.libtorrent[1] when ready.

[1]: https://framagit.org/thombet/vfs.libtorrent
2021-09-01 22:17:39 +00:00
Thomas Bétous b629ee43a6 Release 1.2.0 2021-05-19 19:22:26 +02:00
Thomas 6f3759a928 Use script.module.libtorrent to import libtorrent
In order to support more systems (especially the ones where the
libtorrent python bindings cannot be installed manually), the add-on
script.module.libtorrent is now used to import libtorrent.
2021-05-19 17:18:00 +00:00
Thomas Bétous 2dc6e8a29f Add German to the list of languages in the README 2021-05-14 13:13:17 +02:00
Thomas 9e79850eee Add German translation and fix encoding errors
While adding the German translation some encodings errors occurred due
to non-ASCII characters. Only these errors were fixed because a full
analysis of the strings will be performed when implementing support for
python3.
2021-05-11 20:42:50 +00:00
Thomas Bétous b1b8106275 Release 1.1.0 2021-04-30 22:15:33 +02:00
Thomas 7297f8cef0 Various small updates before releasing v1.1
* Remove empty tags in addon.xml and add a disclaimer
* Update the description of the add-on in addon.xml
* Move icon.png into the "resources" folder to match Kodi guidelines (a
  solid white background is added automatically by Kodi so the icon was
  modified with a white background to avoid unexpected display and to
  match Kodi guidelines)
* Improve the translation guidelines
* Add a missing dot in a localized string
2021-04-30 16:35:20 +00:00
Thomas 134d2ea974 Localize and translate into French the add-on
* Localize all the strings so that the whole add-on can be translated
  (menus, notifications, etc.)
* Translate all the strings into French
* Add advice for future translators in the contribution guidelines
* List the supported languages in the README and a link to the
  translation guidelines
* Rearrange the parts of the README to have the most used information at
  the top
2021-04-29 20:38:14 +00:00
Thomas Bétous 939f1f0ea5 Create a template for bug reports 2021-04-28 23:42:12 +02:00
Thomas afd8756b38 Use PeerTube mascot as icon in the notifications 2021-04-28 21:16:20 +00:00
Thomas cb1825c1f9 Notify the user when the service started
Display a notification when the PeerTube service started so that the
user is aware that the add-on can be used.
This notification will be useful especially on slow devices.

The notification can be disabled in the settings.
2021-04-28 21:02:24 +00:00
Thomas Bétous d87f3038d9 Fix an unreachable branch in the search function
The warning notification was never displayed because PeerTube class
always returned a value even if no videos matching the keywords were
found.
2021-04-28 22:49:40 +02:00
Thomas 17f602da1a Create a CI job to check strings.po files
The new "translation" job that will be available only when strings.po
files are modified.
It will use the msgcmp tool to check that the translation files use the
correct reference strings.

A new part is also added in the contribution guidelines to help future
translators to start up.

See merge request StCyr/plugin.video.peertube!21 for more information
2021-04-27 20:52:59 +00:00
Thomas 13186dc697 Improve the settings layout and translation
* Turn the name of the main category of the settings into a localized
  string
* Add separators to group settings per theme
* Make some settings name more explicit
* Translate the possible values of the settings video_filter and
  video_sort_method

See merge request StCyr/plugin.video.peertube!20 for more information
2021-04-27 20:24:24 +00:00
Thomas Bétous 50a886d8cd Fix bug when video name contains non-ascii symbols 2021-04-25 23:07:33 +02:00
Thomas Bétous cc09006bd3 Fix the "Select instance" item in the home page 2021-04-24 23:03:51 +02:00
Thomas 4f8af35035 Support live streams
Now the type of a video is defined when trying to play a video so that
live streams (.m3u8) are directly played by Kodi (no download required).

We are able to know if a video is live from the response of the "list"
REST API but it was decided to ignore this information in order to have
a single action to play video (whether it is live or not).
The goal was to keep the API of the add-on as simple as possible so that
externel users only have to call the add-on's API with the ID of a
video, without having to add the type of the video.
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.
2021-04-24 14:55:31 +00:00
DavidHenryThoreau ed39c453e9 Add French translation 2021-04-22 21:02:40 +00:00
Thomas 7a1a4e8485 Redesign the main file of the add-on
New features:
* Add the description of each video and each instance. The total number
  of local videos and users of an instance are also added to the
  description of the instance in Kodi.
* Add the total number of pages in the "Next page" item (+ fix the
  number of the current page)
* Display a notification when the download of the torrent starts (will
  help the user to know that something is going on, especially on slow
  machines)
* Support instance URL that are prefixed with "https://" in the settings

Internal changes:
* Create a smaller entry point file to match Kodi's best practices
  (main.py)
* Create a new main module (addon.py) containing only the code related
  to the add-on execution. The other lines of code were moved to the
  classes PeerTube or KodiUtils.
* KodiUtils is now a class and an instance of this class is made
  available to all the modules of the add-on to reuse easily its methods
  and attributes.
* Create helper functions in KodiUtils for creating items in Kodi UI
  easily

See merge request StCyr/plugin.video.peertube!17 for more information
2021-04-22 20:48:20 +00:00
Thomas 6f94e05398 Replace single quotes with double quotes
Make this a coding rule so that apostrophes don't need to be escaped in
strings (they are common in English and other languages).
2021-04-19 13:20:47 +00:00
Thomas 074be7aa12 Create a dedicated class to interact with PeerTube
The PeerTube class is responsible for providing methods to call easily
the PeerTube REST APIs.

Other changes:
* the video filter is now also used when searching videos
* in case of error when sending a request, the message from the response
  is displayed on the screen (even when listing the instances)
* all the debug messages are now prefixed with the name of the add-on
  directly in kodi_utils: it allows an easier usage of this function
  anywhere in the add-on
* first version of the design of the add-on added in contributing.md

See merge request StCyr/plugin.video.peertube!14 for more information
2021-04-19 13:04:57 +00:00
Thomas Bétous 142df05350 Release 1.0.1 2021-04-12 23:13:50 +02:00
Thomas 0efeb9ffdf Support videos when WebTorrent is disabled
The URL of the video is not stored in the same attribute of the response
if WebTorrent is enabled or not.
It caused a bug when trying to play a video which do not use WebTorrent.

Also create a "quality" job to run pylint automatically on merge
requests. The contributing guidelines are updated with this information
and the remaining pylint violations were fixed in the code.

See merge request StCyr/plugin.video.peertube!11 for more information
2021-04-11 21:51:35 +00:00
Thomas f138f2595a Create a CI job to release the add-on
The CI job will take care of all the steps to create a new release in
GitLab:
* creation of the tag
* create of the release object with the release notes
All the required information will be extracted from the addon.xml file.

A "pre-release" job is also added to validate the changes in addon.xml
before the actual release is done.

All these steps are explained in the contribution guidelines.

Finally the files TESTME.md and createaddon.sh are removed since the
installation steps are explained in the wiki and the archive of the
add-on is created automatically by GitLab in the release.

See merge request StCyr/plugin.video.peertube!12 for more information
2021-04-11 20:40:44 +00:00
Thomas c723ced0c6 Fix the selection of instances
When a new instance was selected from the list of instances, it had no
effect because the new instance URL was saved in an attribute that was
reset at the next call of the add-on.
Now when the user selects a new instance, the associated setting is
updated so that this value can be reused the next time the add-on is
called or started.

Also took this opportunity to refactor the access to the add-on's
settings: there are now wrapper methods in kodi_utils.py which
encapsulates the call to Kodi APIs to make the code simpler.

See merge request StCyr/plugin.video.peertube!10 for more information
2021-04-11 08:44:18 +00:00
Thomas Bétous a88a60376f Disable the 'delete_files' setting
This setting is not used so it is disabled until the feature
is implemented to avoid confusing the users.
2021-04-10 22:52:49 +02:00
Thomas 77ce68a637 Release 1.0.0
Update the major version because the external API to play videos
received non-backward compatible changes.
2021-04-08 22:05:12 +00:00
Thomas Bétous a86f2b8f09 Update the URL for libtorrent install
The short URL was pointing to the wrong wiki (the one in my fork)
2021-04-08 23:42:32 +02:00
Thomas 4346178db9 Guide the user when libtorrent cannot be imported
Libtorrent is required to play videos but its installation is still
manual so now a message is displayed when libtorrent could not be
imported instead of having a "service could not start" error at Kodi
startup.
The message contains a link to a page which explains how to install
libtorrent. It will be displayed when:
* the add-on starts
* the user selects a video to play (including when called externally)

Other additions:
* Create a kodi_utils module to centralize some calls to the Kodi API
* Add license information in the header of the files
* Ignore some files in Git (python cache and Mac OS system file)
2021-04-08 23:25:49 +02:00
Thomas 7a21bd92ac Allow playing videos only with the video ID
Now the "play_video" action can be called with the ID of the video (and
optionally the URL of the instance hosting the video) as parameter
instead of the full URL: it will allow other add-ons to call the add-on
to play videos since the full URL contains the resolution which is not
known.

It led to some refactoring and changes in the code:
* Only the instance and the id of a video is retrieved when browsing and
  listing videos which improves the performance a lot (the video URL and
  the resolution are defined only when the video is played)
* the "https://" prefix is now automatically added to the instances URL
  because the instance-related REST APIs use URLs without this prefix.
  It also simplifies the external API because the user does not have to
  provide this prefix.
  Consequently the prefix was removed from the default value of the
  selected instance in the settings: it simplifies the code but it
  generates a non-backward compatible change. The impact is limited
  because it can be easily fixed by resetting the settings to the
  default value and there are very few users currently.

Other changes:
 - manage errors when retrieving the information of a video
 - fix some PEP 8 errors
2021-04-08 23:16:39 +02:00
28 changed files with 3040 additions and 661 deletions

7
.gitignore vendored Normal file
View File

@ -0,0 +1,7 @@
# Visual Studio Code files
*.code-workspace
.vscode/*
.DS_Store
*.pyo
*.pyc

102
.gitlab-ci.yml Normal file
View File

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

View File

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

View File

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

View File

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

View File

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

172
contributing.md Normal file
View File

@ -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 &rarr; 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)).

View File

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 21 KiB

BIN
icon.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

29
main.py Normal file
View File

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

846
misc/pylint-rcfile.txt Normal file
View File

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

View File

@ -0,0 +1,3 @@
Kodistubs>=19.0.0
pylint
requests

View File

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

7
resources/__init__.py Normal file
View File

@ -0,0 +1,7 @@
# -*- coding: utf-8 -*-
"""
Copyright (C) 2021 Thomas Bétous
SPDX-License-Identifier: GPL-3.0-only
See LICENSE.txt for more information.
"""

BIN
resources/icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.4 KiB

View File

@ -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 {}"

View File

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

View File

@ -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 {}"

View File

@ -0,0 +1,7 @@
# -*- coding: utf-8 -*-
"""
Copyright (C) 2021 Thomas Bétous
SPDX-License-Identifier: GPL-3.0-only
See LICENSE.txt for more information.
"""

463
resources/lib/addon.py Normal file
View File

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

270
resources/lib/kodi_utils.py Normal file
View File

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

285
resources/lib/peertube.py Normal file
View File

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

View File

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

View File

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