Compare commits
1 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
a05d396897 |
@ -4,11 +4,3 @@ ADDON_PASSWORD=
|
|||||||
KODI_HOME=tests/home
|
KODI_HOME=tests/home
|
||||||
KODI_INTERACTIVE=0
|
KODI_INTERACTIVE=0
|
||||||
KODI_STUB_VERBOSE=1
|
KODI_STUB_VERBOSE=1
|
||||||
KODI_STUB_RPC_RESPONSES=tests/rpc
|
|
||||||
|
|
||||||
#HTTP_PROXY=
|
|
||||||
#HTTPS_PROXY=
|
|
||||||
|
|
||||||
GH_USERNAME=
|
|
||||||
GH_TOKEN=
|
|
||||||
EMAIL=
|
|
1
.gitattributes
vendored
@ -6,4 +6,3 @@ tests/ export-ignore
|
|||||||
.pylintrc export-ignore
|
.pylintrc export-ignore
|
||||||
Makefile export-ignore
|
Makefile export-ignore
|
||||||
requirements.txt export-ignore
|
requirements.txt export-ignore
|
||||||
scripts/ export-ignore
|
|
||||||
|
36
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@ -1,36 +0,0 @@
|
|||||||
---
|
|
||||||
name: Bug report
|
|
||||||
about: Create a report to help us improve this project
|
|
||||||
title: ''
|
|
||||||
labels: ''
|
|
||||||
assignees: ''
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
**Describe the bug**
|
|
||||||
A clear and concise description of what the bug is.
|
|
||||||
|
|
||||||
**To Reproduce**
|
|
||||||
Steps to reproduce the behavior:
|
|
||||||
1.
|
|
||||||
2.
|
|
||||||
3.
|
|
||||||
|
|
||||||
**Expected behavior**
|
|
||||||
A clear and concise description of what you expected to happen.
|
|
||||||
|
|
||||||
**Screenshots**
|
|
||||||
If applicable, add screenshots to help explain your problem.
|
|
||||||
|
|
||||||
**Logs**
|
|
||||||
Add debug logs to help troubleshoot the issue. See https://kodi.wiki/view/Log_file/Easy for more info.
|
|
||||||
|
|
||||||
**System**
|
|
||||||
- Addon version:
|
|
||||||
- Kodi version:
|
|
||||||
- Inputstream adaptive version:
|
|
||||||
- Operating System (Windows / Mac OS / Android / LibreElec / OSMC / ...):
|
|
||||||
- Special Hardware (RPI / Vero4K+ / ...):
|
|
||||||
|
|
||||||
**Additional context**
|
|
||||||
Add any other context about the problem here.
|
|
20
.github/ISSUE_TEMPLATE/feature_request.md
vendored
@ -1,20 +0,0 @@
|
|||||||
---
|
|
||||||
name: Feature request
|
|
||||||
about: Suggest an idea for this project
|
|
||||||
title: ''
|
|
||||||
labels: ''
|
|
||||||
assignees: ''
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
**Is your feature request related to a problem? Please describe.**
|
|
||||||
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
|
|
||||||
|
|
||||||
**Describe the solution you'd like**
|
|
||||||
A clear and concise description of what you want to happen.
|
|
||||||
|
|
||||||
**Describe alternatives you've considered**
|
|
||||||
A clear and concise description of any alternative solutions or features you've considered.
|
|
||||||
|
|
||||||
**Additional context**
|
|
||||||
Add any other context or screenshots about the feature request here.
|
|
18
.github/workflows/addon-check.yml
vendored
@ -1,4 +1,4 @@
|
|||||||
name: Kodi
|
name: Kodi Addon-Check
|
||||||
on:
|
on:
|
||||||
# Run action when pushed to master, or for commits in a pull request.
|
# Run action when pushed to master, or for commits in a pull request.
|
||||||
push:
|
push:
|
||||||
@ -11,12 +11,18 @@ jobs:
|
|||||||
kodi-addon-checker:
|
kodi-addon-checker:
|
||||||
name: Addon checker
|
name: Addon checker
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
kodi-version: [ leia, matrix ]
|
||||||
steps:
|
steps:
|
||||||
- name: Check out ${{ github.sha }} from repository ${{ github.repository }}
|
- name: Check out ${{ github.sha }} from repository ${{ github.repository }}
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v2
|
||||||
|
|
||||||
- name: Run kodi-addon-checker
|
- name: Install dependencies
|
||||||
uses: xbmc/action-kodi-addon-checker@v1.2
|
run: |
|
||||||
with:
|
sudo apt-get install libxml2-utils
|
||||||
kodi-version: matrix
|
sudo python -m pip install kodi-addon-checker
|
||||||
addon-id: ${{ github.event.repository.name }}
|
|
||||||
|
- name: Run Addon Check for Kodi ${{matrix.kodi-version}}
|
||||||
|
run: make check-addon-${{matrix.kodi-version}}
|
||||||
|
34
.github/workflows/ci.yml
vendored
@ -3,10 +3,10 @@ on:
|
|||||||
# Run action when pushed to master, or for commits in a pull request.
|
# Run action when pushed to master, or for commits in a pull request.
|
||||||
push:
|
push:
|
||||||
branches:
|
branches:
|
||||||
- master
|
- master
|
||||||
pull_request:
|
pull_request:
|
||||||
branches:
|
branches:
|
||||||
- master
|
- master
|
||||||
jobs:
|
jobs:
|
||||||
tests:
|
tests:
|
||||||
name: Add-on testing
|
name: Add-on testing
|
||||||
@ -14,22 +14,24 @@ jobs:
|
|||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
os: [ ubuntu-latest, windows-latest ]
|
os: [ ubuntu-latest ]
|
||||||
python-version: ["3.8", "3.9", "3.10"]
|
python-version: [ 2.7, 3.5, 3.6, 3.7, 3.8, 3.9 ]
|
||||||
include:
|
include:
|
||||||
# End-of-life Python versions are not available anymore with ubuntu-latest
|
# Kodi Leia on Windows uses a bundled Python 2.7.
|
||||||
- os: ubuntu-20.04
|
- os: windows-latest
|
||||||
python-version: "3.5"
|
python-version: 2.7
|
||||||
- os: ubuntu-20.04
|
|
||||||
python-version: "3.6"
|
# Kodi Matrix on Windows uses a bundled Python 3.8, but we test 3.9 also to be sure.
|
||||||
- os: ubuntu-20.04
|
- os: windows-latest
|
||||||
python-version: "3.7"
|
python-version: 3.8
|
||||||
|
- os: windows-latest
|
||||||
|
python-version: 3.9
|
||||||
steps:
|
steps:
|
||||||
- name: Check out ${{ github.sha }} from repository ${{ github.repository }}
|
- name: Check out ${{ github.sha }} from repository ${{ github.repository }}
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v2
|
||||||
|
|
||||||
- name: Set up Python ${{ matrix.python-version }}
|
- name: Set up Python ${{ matrix.python-version }}
|
||||||
uses: actions/setup-python@v4
|
uses: actions/setup-python@v1
|
||||||
with:
|
with:
|
||||||
python-version: ${{ matrix.python-version }}
|
python-version: ${{ matrix.python-version }}
|
||||||
|
|
||||||
@ -52,14 +54,14 @@ jobs:
|
|||||||
KODI_INTERACTIVE: 0
|
KODI_INTERACTIVE: 0
|
||||||
KODI_STUB_RPC_RESPONSES: ${{ github.workspace }}/tests/rpc
|
KODI_STUB_RPC_RESPONSES: ${{ github.workspace }}/tests/rpc
|
||||||
HTTP_PROXY: ${{ secrets.HTTP_PROXY }}
|
HTTP_PROXY: ${{ secrets.HTTP_PROXY }}
|
||||||
run: pytest -x -v --cov=./ --cov-report=xml tests
|
run: pytest -v --cov=./ --cov-report=xml tests
|
||||||
|
|
||||||
- name: Upload code coverage to CodeCov
|
- name: Upload code coverage to CodeCov
|
||||||
uses: codecov/codecov-action@v3
|
uses: codecov/codecov-action@v1
|
||||||
continue-on-error: true
|
continue-on-error: true
|
||||||
env:
|
env:
|
||||||
OS: ${{ matrix.os }}
|
OS: ${{ matrix.os }}
|
||||||
PYTHON: ${{ matrix.python-version }}
|
PYTHON: ${{ matrix.python-version }}
|
||||||
with:
|
with:
|
||||||
flags: unittests
|
flags: unittests
|
||||||
env_vars: OS,PYTHON
|
env_vars: OS,PYTHON
|
66
.github/workflows/release.yml
vendored
@ -6,13 +6,24 @@ on:
|
|||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
name: Release plugin.video.viervijfzes
|
name: Release plugin.video.viervijfzes
|
||||||
|
if: startsWith(github.ref, 'refs/tags/') # prevent from running if it's not a tag
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Check out ${{ github.sha }} from repository ${{ github.repository }}
|
- name: Checkout code
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v2
|
||||||
|
|
||||||
- name: Get changelog
|
- name: Install dependencies
|
||||||
id: get-changelog
|
run: sudo apt-get install libxml2-utils
|
||||||
|
|
||||||
|
- name: Build zip files
|
||||||
|
id: build
|
||||||
|
run: |
|
||||||
|
make build-all release=1
|
||||||
|
echo ::set-output name=leia-filename::$(cd ..;ls plugin.video.viervijfzes*.zip | grep -v '+matrix.' | head -1)
|
||||||
|
echo ::set-output name=matrix-filename::$(cd ..;ls plugin.video.viervijfzes*+matrix.*.zip | head -1)
|
||||||
|
|
||||||
|
- name: Get body
|
||||||
|
id: get-body
|
||||||
run: |
|
run: |
|
||||||
description=$(sed '1,6d;/^## /,$d' CHANGELOG.md)
|
description=$(sed '1,6d;/^## /,$d' CHANGELOG.md)
|
||||||
echo $description
|
echo $description
|
||||||
@ -21,14 +32,47 @@ jobs:
|
|||||||
description="${description//$'\r'/'%0D'}"
|
description="${description//$'\r'/'%0D'}"
|
||||||
echo ::set-output name=body::$description
|
echo ::set-output name=body::$description
|
||||||
|
|
||||||
- name: Generate distribution zips
|
- name: Create Release
|
||||||
run: scripts/build.py
|
id: create_release
|
||||||
|
uses: actions/create-release@v1
|
||||||
- name: Create Release on Github
|
env:
|
||||||
uses: softprops/action-gh-release@v1
|
GITHUB_TOKEN: ${{ secrets.GH_TOKEN }}
|
||||||
with:
|
with:
|
||||||
body: ${{ steps.get-changelog.outputs.body }}
|
tag_name: ${{ github.ref }}
|
||||||
|
release_name: ${{ github.ref }}
|
||||||
|
body: ${{ steps.get-body.outputs.body }}
|
||||||
draft: false
|
draft: false
|
||||||
prerelease: false
|
prerelease: false
|
||||||
files: "dist/*.zip"
|
|
||||||
token: ${{ secrets.GH_TOKEN }}
|
- name: Upload Leia zip
|
||||||
|
uses: actions/upload-release-asset@v1
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GH_TOKEN }}
|
||||||
|
with:
|
||||||
|
upload_url: ${{ steps.create_release.outputs.upload_url }}
|
||||||
|
asset_name: ${{ steps.build.outputs.leia-filename }}
|
||||||
|
asset_path: ../${{ steps.build.outputs.leia-filename }}
|
||||||
|
asset_content_type: application/zip
|
||||||
|
|
||||||
|
- name: Upload Matrix zip
|
||||||
|
uses: actions/upload-release-asset@v1
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GH_TOKEN }}
|
||||||
|
with:
|
||||||
|
upload_url: ${{ steps.create_release.outputs.upload_url }}
|
||||||
|
asset_name: ${{ steps.build.outputs.matrix-filename }}
|
||||||
|
asset_path: ../${{ steps.build.outputs.matrix-filename }}
|
||||||
|
asset_content_type: application/zip
|
||||||
|
|
||||||
|
- name: Generate distribution zip and submit to official kodi repository
|
||||||
|
id: kodi-addon-submitter
|
||||||
|
uses: xbmc/action-kodi-addon-submitter@v1.2
|
||||||
|
with:
|
||||||
|
kodi-repository: repo-plugins
|
||||||
|
addon-id: plugin.video.viervijfzes
|
||||||
|
kodi-version: leia
|
||||||
|
kodi-matrix: true
|
||||||
|
env:
|
||||||
|
GH_USERNAME: ${{ secrets.GH_USERNAME }}
|
||||||
|
GH_TOKEN: ${{ secrets.GH_TOKEN }}
|
||||||
|
EMAIL: ${{ secrets.EMAIL }}
|
||||||
|
3
.gitignore
vendored
@ -18,5 +18,4 @@ tests/home/userdata/addon_data
|
|||||||
|
|
||||||
Pipfile
|
Pipfile
|
||||||
Pipfile.lock
|
Pipfile.lock
|
||||||
|
dist/
|
||||||
dist/
|
|
13
.pylintrc
@ -1,25 +1,16 @@
|
|||||||
[MESSAGES CONTROL]
|
[MESSAGES CONTROL]
|
||||||
disable=
|
disable=
|
||||||
bad-option-value,
|
bad-option-value,
|
||||||
cyclic-import, # This should be fixed
|
cyclic-import, # This shoud be fixed
|
||||||
duplicate-code,
|
duplicate-code,
|
||||||
fixme,
|
fixme,
|
||||||
import-outside-toplevel,
|
import-outside-toplevel,
|
||||||
line-too-long,
|
line-too-long,
|
||||||
no-init,
|
|
||||||
old-style-class,
|
old-style-class,
|
||||||
|
super-with-arguments,
|
||||||
too-few-public-methods,
|
too-few-public-methods,
|
||||||
too-many-arguments,
|
too-many-arguments,
|
||||||
too-many-branches,
|
too-many-branches,
|
||||||
too-many-instance-attributes,
|
too-many-instance-attributes,
|
||||||
too-many-locals,
|
too-many-locals,
|
||||||
too-many-public-methods,
|
too-many-public-methods,
|
||||||
too-many-statements,
|
|
||||||
use-maxsplit-arg,
|
|
||||||
consider-using-from-import,
|
|
||||||
unspecified-encoding,
|
|
||||||
broad-exception-raised,
|
|
||||||
|
|
||||||
super-with-arguments, # Python 2.7 compatibility
|
|
||||||
raise-missing-from, # Python 2.7 compatibility
|
|
||||||
consider-using-f-string, # Python 2.7 compatibility
|
|
||||||
|
118
CHANGELOG.md
@ -1,94 +1,5 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
[Full Changelog](https://git.jeroened.be/JeroenED/plugin.video.viervijfzes/compare/v0.4.11...v0.4.12)
|
|
||||||
|
|
||||||
**Implemented enhancements:**
|
|
||||||
|
|
||||||
- Add live channels [\#129](https://github.com/add-ons/plugin.video.viervijfzes/pull/129) ([mediaminister](https://github.com/mediaminister))
|
|
||||||
- Add PlayCrime Channel
|
|
||||||
|
|
||||||
|
|
||||||
## [v0.4.11](https://github.com/add-ons/plugin.video.viervijfzes/tree/v0.4.11) (2023-07-26)
|
|
||||||
|
|
||||||
[Full Changelog](https://github.com/add-ons/plugin.video.viervijfzes/compare/v0.4.10...v0.4.11)
|
|
||||||
|
|
||||||
**Implemented enhancements:**
|
|
||||||
|
|
||||||
- Add support for proxies [\#126](https://github.com/add-ons/plugin.video.viervijfzes/pull/126) ([michaelarnauts](https://github.com/michaelarnauts))
|
|
||||||
|
|
||||||
**Fixed bugs:**
|
|
||||||
|
|
||||||
- Add DRM support for all streams [\#121](https://github.com/add-ons/plugin.video.viervijfzes/pull/121) ([mediaminister](https://github.com/mediaminister))
|
|
||||||
- Use inputstreamhelper for unprotected MPEG-DASH [\#118](https://github.com/add-ons/plugin.video.viervijfzes/pull/118) ([mediaminister](https://github.com/mediaminister))
|
|
||||||
|
|
||||||
## [v0.4.10](https://github.com/add-ons/plugin.video.viervijfzes/tree/v0.4.10) (2023-01-16)
|
|
||||||
|
|
||||||
[Full Changelog](https://github.com/add-ons/plugin.video.viervijfzes/compare/v0.4.9...v0.4.10)
|
|
||||||
|
|
||||||
**Fixed bugs:**
|
|
||||||
|
|
||||||
- Update api [\#114](https://github.com/add-ons/plugin.video.viervijfzes/pull/114) ([mediaminister](https://github.com/mediaminister))
|
|
||||||
|
|
||||||
## [v0.4.9](https://github.com/add-ons/plugin.video.viervijfzes/tree/v0.4.9) (2023-01-04)
|
|
||||||
|
|
||||||
[Full Changelog](https://github.com/add-ons/plugin.video.viervijfzes/compare/v0.4.8...v0.4.9)
|
|
||||||
|
|
||||||
**Fixed bugs:**
|
|
||||||
|
|
||||||
- Add support for unprotected MPEG-DASH streams [\#111](https://github.com/add-ons/plugin.video.viervijfzes/pull/111) ([mediaminister](https://github.com/mediaminister))
|
|
||||||
- Fix clips [\#108](https://github.com/add-ons/plugin.video.viervijfzes/pull/108) ([michaelarnauts](https://github.com/michaelarnauts))
|
|
||||||
|
|
||||||
## [v0.4.8](https://github.com/add-ons/plugin.video.viervijfzes/tree/v0.4.8) (2022-07-07)
|
|
||||||
|
|
||||||
[Full Changelog](https://github.com/add-ons/plugin.video.viervijfzes/compare/v0.4.7...v0.4.8)
|
|
||||||
|
|
||||||
**Fixed bugs:**
|
|
||||||
|
|
||||||
- Fix API [\#105](https://github.com/add-ons/plugin.video.viervijfzes/pull/105) ([michaelarnauts](https://github.com/michaelarnauts))
|
|
||||||
|
|
||||||
## [v0.4.7](https://github.com/add-ons/plugin.video.viervijfzes/tree/v0.4.7) (2022-02-04)
|
|
||||||
|
|
||||||
[Full Changelog](https://github.com/add-ons/plugin.video.viervijfzes/compare/v0.4.6...v0.4.7)
|
|
||||||
|
|
||||||
**Fixed bugs:**
|
|
||||||
|
|
||||||
- Fix empty My List due to unknown items [\#102](https://github.com/add-ons/plugin.video.viervijfzes/pull/102) ([michaelarnauts](https://github.com/michaelarnauts))
|
|
||||||
|
|
||||||
## [v0.4.6](https://github.com/add-ons/plugin.video.viervijfzes/tree/v0.4.6) (2022-02-02)
|
|
||||||
|
|
||||||
[Full Changelog](https://github.com/add-ons/plugin.video.viervijfzes/compare/v0.4.5...v0.4.6)
|
|
||||||
|
|
||||||
**Fixed bugs:**
|
|
||||||
|
|
||||||
- Fix playback of DRM protected content [\#101](https://github.com/add-ons/plugin.video.viervijfzes/pull/101) ([michaelarnauts](https://github.com/michaelarnauts))
|
|
||||||
|
|
||||||
## [v0.4.5](https://github.com/add-ons/plugin.video.viervijfzes/tree/v0.4.5) (2021-10-21)
|
|
||||||
|
|
||||||
[Full Changelog](https://github.com/add-ons/plugin.video.viervijfzes/compare/v0.4.4...v0.4.5)
|
|
||||||
|
|
||||||
**Fixed bugs:**
|
|
||||||
|
|
||||||
- Remove dependency on inputstream.adaptive [\#98](https://github.com/add-ons/plugin.video.viervijfzes/pull/98) ([michaelarnauts](https://github.com/michaelarnauts))
|
|
||||||
- Various fixes due to layout changes [\#97](https://github.com/add-ons/plugin.video.viervijfzes/pull/97) ([michaelarnauts](https://github.com/michaelarnauts))
|
|
||||||
|
|
||||||
## [v0.4.4](https://github.com/add-ons/plugin.video.viervijfzes/tree/v0.4.4) (2021-09-15)
|
|
||||||
|
|
||||||
[Full Changelog](https://github.com/add-ons/plugin.video.viervijfzes/compare/v0.4.3...v0.4.4)
|
|
||||||
|
|
||||||
**Fixed bugs:**
|
|
||||||
|
|
||||||
- Fix menu [\#93](https://github.com/add-ons/plugin.video.viervijfzes/pull/93) ([mediaminister](https://github.com/mediaminister))
|
|
||||||
|
|
||||||
## [v0.4.3](https://github.com/add-ons/plugin.video.viervijfzes/tree/v0.4.3) (2021-04-24)
|
|
||||||
|
|
||||||
[Full Changelog](https://github.com/add-ons/plugin.video.viervijfzes/compare/v0.4.2...v0.4.3)
|
|
||||||
|
|
||||||
**Implemented enhancements:**
|
|
||||||
|
|
||||||
- Fetch a week of EPG data for IPTV Manager [\#89](https://github.com/add-ons/plugin.video.viervijfzes/pull/89) ([michaelarnauts](https://github.com/michaelarnauts))
|
|
||||||
- Improve playback error handling [\#87](https://github.com/add-ons/plugin.video.viervijfzes/pull/87) ([michaelarnauts](https://github.com/michaelarnauts))
|
|
||||||
- Add support for Play7 [\#86](https://github.com/add-ons/plugin.video.viervijfzes/pull/86) ([michaelarnauts](https://github.com/michaelarnauts))
|
|
||||||
|
|
||||||
## [v0.4.2](https://github.com/add-ons/plugin.video.viervijfzes/tree/v0.4.2) (2021-03-22)
|
## [v0.4.2](https://github.com/add-ons/plugin.video.viervijfzes/tree/v0.4.2) (2021-03-22)
|
||||||
|
|
||||||
[Full Changelog](https://github.com/add-ons/plugin.video.viervijfzes/compare/v0.4.1...v0.4.2)
|
[Full Changelog](https://github.com/add-ons/plugin.video.viervijfzes/compare/v0.4.1...v0.4.2)
|
||||||
@ -116,6 +27,11 @@
|
|||||||
|
|
||||||
- Fix error when requesting a My List that has not been created yet. [\#73](https://github.com/add-ons/plugin.video.viervijfzes/pull/73) ([michaelarnauts](https://github.com/michaelarnauts))
|
- Fix error when requesting a My List that has not been created yet. [\#73](https://github.com/add-ons/plugin.video.viervijfzes/pull/73) ([michaelarnauts](https://github.com/michaelarnauts))
|
||||||
|
|
||||||
|
**Merged pull requests:**
|
||||||
|
|
||||||
|
- Cleanup CI [\#72](https://github.com/add-ons/plugin.video.viervijfzes/pull/72) ([michaelarnauts](https://github.com/michaelarnauts))
|
||||||
|
- Remove dependency on tox [\#70](https://github.com/add-ons/plugin.video.viervijfzes/pull/70) ([michaelarnauts](https://github.com/michaelarnauts))
|
||||||
|
|
||||||
## [v0.4.0](https://github.com/add-ons/plugin.video.viervijfzes/tree/v0.4.0) (2021-02-04)
|
## [v0.4.0](https://github.com/add-ons/plugin.video.viervijfzes/tree/v0.4.0) (2021-02-04)
|
||||||
|
|
||||||
[Full Changelog](https://github.com/add-ons/plugin.video.viervijfzes/compare/v0.3.1...v0.4.0)
|
[Full Changelog](https://github.com/add-ons/plugin.video.viervijfzes/compare/v0.3.1...v0.4.0)
|
||||||
@ -124,6 +40,12 @@
|
|||||||
|
|
||||||
- Rebranding to GoPlay [\#64](https://github.com/add-ons/plugin.video.viervijfzes/pull/64) ([michaelarnauts](https://github.com/michaelarnauts))
|
- Rebranding to GoPlay [\#64](https://github.com/add-ons/plugin.video.viervijfzes/pull/64) ([michaelarnauts](https://github.com/michaelarnauts))
|
||||||
|
|
||||||
|
**Merged pull requests:**
|
||||||
|
|
||||||
|
- Make use of git archive [\#66](https://github.com/add-ons/plugin.video.viervijfzes/pull/66) ([dagwieers](https://github.com/dagwieers))
|
||||||
|
- Run CI on Windows [\#62](https://github.com/add-ons/plugin.video.viervijfzes/pull/62) ([michaelarnauts](https://github.com/michaelarnauts))
|
||||||
|
- Add support for Python 3.9 [\#60](https://github.com/add-ons/plugin.video.viervijfzes/pull/60) ([michaelarnauts](https://github.com/michaelarnauts))
|
||||||
|
|
||||||
## [v0.3.1](https://github.com/add-ons/plugin.video.viervijfzes/tree/v0.3.1) (2020-11-28)
|
## [v0.3.1](https://github.com/add-ons/plugin.video.viervijfzes/tree/v0.3.1) (2020-11-28)
|
||||||
|
|
||||||
[Full Changelog](https://github.com/add-ons/plugin.video.viervijfzes/compare/v0.3.0...v0.3.1)
|
[Full Changelog](https://github.com/add-ons/plugin.video.viervijfzes/compare/v0.3.0...v0.3.1)
|
||||||
@ -132,6 +54,10 @@
|
|||||||
|
|
||||||
- Fix authentication on some older Android devices [\#58](https://github.com/add-ons/plugin.video.viervijfzes/pull/58) ([michaelarnauts](https://github.com/michaelarnauts))
|
- Fix authentication on some older Android devices [\#58](https://github.com/add-ons/plugin.video.viervijfzes/pull/58) ([michaelarnauts](https://github.com/michaelarnauts))
|
||||||
|
|
||||||
|
**Merged pull requests:**
|
||||||
|
|
||||||
|
- Fix CI tests [\#59](https://github.com/add-ons/plugin.video.viervijfzes/pull/59) ([michaelarnauts](https://github.com/michaelarnauts))
|
||||||
|
|
||||||
## [v0.3.0](https://github.com/add-ons/plugin.video.viervijfzes/tree/v0.3.0) (2020-11-17)
|
## [v0.3.0](https://github.com/add-ons/plugin.video.viervijfzes/tree/v0.3.0) (2020-11-17)
|
||||||
|
|
||||||
[Full Changelog](https://github.com/add-ons/plugin.video.viervijfzes/compare/v0.2.0...v0.3.0)
|
[Full Changelog](https://github.com/add-ons/plugin.video.viervijfzes/compare/v0.2.0...v0.3.0)
|
||||||
@ -151,6 +77,11 @@
|
|||||||
- Opening some programs without a title could throw an error [\#45](https://github.com/add-ons/plugin.video.viervijfzes/pull/45) ([dagwieers](https://github.com/dagwieers))
|
- Opening some programs without a title could throw an error [\#45](https://github.com/add-ons/plugin.video.viervijfzes/pull/45) ([dagwieers](https://github.com/dagwieers))
|
||||||
- Show message when Kodi Player fails to get the stream [\#40](https://github.com/add-ons/plugin.video.viervijfzes/pull/40) ([mediaminister](https://github.com/mediaminister))
|
- Show message when Kodi Player fails to get the stream [\#40](https://github.com/add-ons/plugin.video.viervijfzes/pull/40) ([mediaminister](https://github.com/mediaminister))
|
||||||
|
|
||||||
|
**Merged pull requests:**
|
||||||
|
|
||||||
|
- Various fixes [\#46](https://github.com/add-ons/plugin.video.viervijfzes/pull/46) ([michaelarnauts](https://github.com/michaelarnauts))
|
||||||
|
- Use sake for Kodi stubs [\#36](https://github.com/add-ons/plugin.video.viervijfzes/pull/36) ([michaelarnauts](https://github.com/michaelarnauts))
|
||||||
|
|
||||||
## [v0.2.0](https://github.com/add-ons/plugin.video.viervijfzes/tree/v0.2.0) (2020-06-19)
|
## [v0.2.0](https://github.com/add-ons/plugin.video.viervijfzes/tree/v0.2.0) (2020-06-19)
|
||||||
|
|
||||||
[Full Changelog](https://github.com/add-ons/plugin.video.viervijfzes/compare/v0.1.0...v0.2.0)
|
[Full Changelog](https://github.com/add-ons/plugin.video.viervijfzes/compare/v0.1.0...v0.2.0)
|
||||||
@ -175,6 +106,11 @@
|
|||||||
- Fix multi-line text in progress dialog [\#21](https://github.com/add-ons/plugin.video.viervijfzes/pull/21) ([mediaminister](https://github.com/mediaminister))
|
- Fix multi-line text in progress dialog [\#21](https://github.com/add-ons/plugin.video.viervijfzes/pull/21) ([mediaminister](https://github.com/mediaminister))
|
||||||
- Fix token encoding in auth [\#19](https://github.com/add-ons/plugin.video.viervijfzes/pull/19) ([michaelarnauts](https://github.com/michaelarnauts))
|
- Fix token encoding in auth [\#19](https://github.com/add-ons/plugin.video.viervijfzes/pull/19) ([michaelarnauts](https://github.com/michaelarnauts))
|
||||||
|
|
||||||
|
**Merged pull requests:**
|
||||||
|
|
||||||
|
- Check for unused translations [\#24](https://github.com/add-ons/plugin.video.viervijfzes/pull/24) ([michaelarnauts](https://github.com/michaelarnauts))
|
||||||
|
- Move test/ to tests/ [\#17](https://github.com/add-ons/plugin.video.viervijfzes/pull/17) ([dagwieers](https://github.com/dagwieers))
|
||||||
|
|
||||||
## [v0.1.0](https://github.com/add-ons/plugin.video.viervijfzes/tree/v0.1.0) (2020-03-27)
|
## [v0.1.0](https://github.com/add-ons/plugin.video.viervijfzes/tree/v0.1.0) (2020-03-27)
|
||||||
|
|
||||||
[Full Changelog](https://github.com/add-ons/plugin.video.viervijfzes/compare/89f55f70b017d0add645d1e1d88f0ce8192d11c4...v0.1.0)
|
[Full Changelog](https://github.com/add-ons/plugin.video.viervijfzes/compare/89f55f70b017d0add645d1e1d88f0ce8192d11c4...v0.1.0)
|
||||||
@ -190,7 +126,11 @@
|
|||||||
|
|
||||||
**Merged pull requests:**
|
**Merged pull requests:**
|
||||||
|
|
||||||
|
- Improve CI tests [\#14](https://github.com/add-ons/plugin.video.viervijfzes/pull/14) ([michaelarnauts](https://github.com/michaelarnauts))
|
||||||
- Small translation fixes [\#12](https://github.com/add-ons/plugin.video.viervijfzes/pull/12) ([dagwieers](https://github.com/dagwieers))
|
- Small translation fixes [\#12](https://github.com/add-ons/plugin.video.viervijfzes/pull/12) ([dagwieers](https://github.com/dagwieers))
|
||||||
|
- Various check fixes [\#11](https://github.com/add-ons/plugin.video.viervijfzes/pull/11) ([dagwieers](https://github.com/dagwieers))
|
||||||
|
- Replace Travis with GitHub Actions [\#10](https://github.com/add-ons/plugin.video.viervijfzes/pull/10) ([michaelarnauts](https://github.com/michaelarnauts))
|
||||||
|
- Improve code coverage [\#9](https://github.com/add-ons/plugin.video.viervijfzes/pull/9) ([michaelarnauts](https://github.com/michaelarnauts))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
14
LICENSE
@ -1,7 +1,7 @@
|
|||||||
GNU GENERAL PUBLIC LICENSE
|
GNU GENERAL PUBLIC LICENSE
|
||||||
Version 3, 29 June 2007
|
Version 3, 29 June 2007
|
||||||
|
|
||||||
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
|
Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
|
||||||
Everyone is permitted to copy and distribute verbatim copies
|
Everyone is permitted to copy and distribute verbatim copies
|
||||||
of this license document, but changing it is not allowed.
|
of this license document, but changing it is not allowed.
|
||||||
|
|
||||||
@ -631,8 +631,8 @@ to attach them to the start of each source file to most effectively
|
|||||||
state the exclusion of warranty; and each file should have at least
|
state the exclusion of warranty; and each file should have at least
|
||||||
the "copyright" line and a pointer to where the full notice is found.
|
the "copyright" line and a pointer to where the full notice is found.
|
||||||
|
|
||||||
<one line to give the program's name and a brief idea of what it does.>
|
{one line to give the program's name and a brief idea of what it does.}
|
||||||
Copyright (C) <year> <name of author>
|
Copyright (C) {year} {name of author}
|
||||||
|
|
||||||
This program is free software: you can redistribute it and/or modify
|
This program is free software: you can redistribute it and/or modify
|
||||||
it under the terms of the GNU General Public License as published by
|
it under the terms of the GNU General Public License as published by
|
||||||
@ -645,14 +645,14 @@ the "copyright" line and a pointer to where the full notice is found.
|
|||||||
GNU General Public License for more details.
|
GNU General Public License for more details.
|
||||||
|
|
||||||
You should have received a copy of the GNU General Public License
|
You should have received a copy of the GNU General Public License
|
||||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
Also add information on how to contact you by electronic and paper mail.
|
Also add information on how to contact you by electronic and paper mail.
|
||||||
|
|
||||||
If the program does terminal interaction, make it output a short
|
If the program does terminal interaction, make it output a short
|
||||||
notice like this when it starts in an interactive mode:
|
notice like this when it starts in an interactive mode:
|
||||||
|
|
||||||
<program> Copyright (C) <year> <name of author>
|
{project} Copyright (C) {year} {fullname}
|
||||||
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
|
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
|
||||||
This is free software, and you are welcome to redistribute it
|
This is free software, and you are welcome to redistribute it
|
||||||
under certain conditions; type `show c' for details.
|
under certain conditions; type `show c' for details.
|
||||||
@ -664,11 +664,11 @@ might be different; for a GUI interface, you would use an "about box".
|
|||||||
You should also get your employer (if you work as a programmer) or school,
|
You should also get your employer (if you work as a programmer) or school,
|
||||||
if any, to sign a "copyright disclaimer" for the program, if necessary.
|
if any, to sign a "copyright disclaimer" for the program, if necessary.
|
||||||
For more information on this, and how to apply and follow the GNU GPL, see
|
For more information on this, and how to apply and follow the GNU GPL, see
|
||||||
<https://www.gnu.org/licenses/>.
|
<http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
The GNU General Public License does not permit incorporating your program
|
The GNU General Public License does not permit incorporating your program
|
||||||
into proprietary programs. If your program is a subroutine library, you
|
into proprietary programs. If your program is a subroutine library, you
|
||||||
may consider it more useful to permit linking proprietary applications with
|
may consider it more useful to permit linking proprietary applications with
|
||||||
the library. If this is what you want to do, use the GNU Lesser General
|
the library. If this is what you want to do, use the GNU Lesser General
|
||||||
Public License instead of this License. But first, please read
|
Public License instead of this License. But first, please read
|
||||||
<https://www.gnu.org/licenses/why-not-lgpl.html>.
|
<http://www.gnu.org/philosophy/why-not-lgpl.html>.
|
||||||
|
62
Makefile
@ -2,11 +2,20 @@ export KODI_HOME := $(CURDIR)/tests/home
|
|||||||
export KODI_INTERACTIVE := 0
|
export KODI_INTERACTIVE := 0
|
||||||
PYTHON := python
|
PYTHON := python
|
||||||
|
|
||||||
languages = $(filter-out en_gb, $(patsubst resources/language/resource.language.%, %, $(wildcard resources/language/*)))
|
# Collect information to build as sensible package name
|
||||||
|
name = $(shell xmllint --xpath 'string(/addon/@id)' addon.xml)
|
||||||
|
version = $(shell xmllint --xpath 'string(/addon/@version)' addon.xml)
|
||||||
|
ifdef release
|
||||||
|
zip_name = $(name)-$(version).zip
|
||||||
|
else
|
||||||
|
git_branch = $(shell git rev-parse --abbrev-ref HEAD)
|
||||||
|
git_hash = $(shell git rev-parse --short HEAD)
|
||||||
|
zip_name = $(name)-$(version)-$(git_branch)-$(git_hash).zip
|
||||||
|
endif
|
||||||
|
|
||||||
all: check test build
|
all: check test build
|
||||||
zip: build
|
zip: build
|
||||||
multizip: build
|
multizip: build-all
|
||||||
|
|
||||||
check: check-pylint check-translations
|
check: check-pylint check-translations
|
||||||
|
|
||||||
@ -16,17 +25,20 @@ check-pylint:
|
|||||||
|
|
||||||
check-translations:
|
check-translations:
|
||||||
@printf ">>> Running translation checks\n"
|
@printf ">>> Running translation checks\n"
|
||||||
@$(foreach lang,$(languages), \
|
@$(foreach lang,$(filter-out en_gb, $(patsubst resources/language/resource.language.%, %, $(wildcard resources/language/*))), \
|
||||||
msgcmp --use-untranslated resources/language/resource.language.$(lang)/strings.po resources/language/resource.language.en_gb/strings.po; \
|
msgcmp resources/language/resource.language.$(lang)/strings.po resources/language/resource.language.en_gb/strings.po; \
|
||||||
)
|
)
|
||||||
@scripts/check_for_unused_translations.py
|
@tests/check_for_unused_translations.py
|
||||||
|
|
||||||
check-addon: build
|
check-addon: check-addon-matrix
|
||||||
|
|
||||||
|
check-addon-leia:
|
||||||
@printf ">>> Running addon checks\n"
|
@printf ">>> Running addon checks\n"
|
||||||
$(eval TMPDIR := $(shell mktemp -d))
|
@make -s build-leia && cd dist/ && kodi-addon-checker --branch=leia
|
||||||
@unzip dist/plugin.video.viervijfzes-*+matrix.1.zip -d ${TMPDIR}
|
|
||||||
cd ${TMPDIR} && kodi-addon-checker --branch=matrix
|
check-addon-matrix:
|
||||||
@rm -rf ${TMPDIR}
|
@printf ">>> Running addon checks\n"
|
||||||
|
@make -s build-matrix && cd dist/ && kodi-addon-checker --branch=matrix
|
||||||
|
|
||||||
codefix:
|
codefix:
|
||||||
@isort -l 160 resources/
|
@isort -l 160 resources/
|
||||||
@ -45,14 +57,32 @@ clean:
|
|||||||
@rm -f *.log .coverage
|
@rm -f *.log .coverage
|
||||||
@rm -rf dist/
|
@rm -rf dist/
|
||||||
|
|
||||||
build: clean
|
build: build-matrix
|
||||||
@printf ">>> Building add-on\n"
|
|
||||||
@scripts/build.py
|
|
||||||
@ls -lah dist/*.zip
|
|
||||||
|
|
||||||
|
build-all: build-leia build-matrix
|
||||||
|
|
||||||
|
build-leia: clean
|
||||||
|
$(eval abi=2.26.0)
|
||||||
|
@rm -rf dist/ && mkdir dist/
|
||||||
|
@git archive --format tar --worktree-attributes --prefix $(name)/ $(or $(shell git stash create), HEAD) | (cd dist/ && tar -xf -)
|
||||||
|
@sed -i -E "s/(<!--)?(\s*<import addon=\"xbmc\.python\" version=\")[0-9\.]*(\"\/>)(\s*-->)?/\2$(abi)\3/" dist/$(name)/addon.xml
|
||||||
|
@cd dist && zip --quiet -9 -r ../../$(zip_name) $(name)
|
||||||
|
@printf ">>> Successfully wrote package for Kodi 18 as ../$(zip_name)\n"
|
||||||
|
|
||||||
|
build-matrix: clean
|
||||||
|
$(eval abi=3.0.0)
|
||||||
|
$(eval version=$(version)+matrix.1)
|
||||||
|
@rm -rf dist/ && mkdir dist/
|
||||||
|
@git archive --format tar --worktree-attributes --prefix $(name)/ $(or $(shell git stash create), HEAD) | (cd dist/ && tar -xf -)
|
||||||
|
@sed -i -E "s/(<!--)?(\s*<import addon=\"xbmc\.python\" version=\")[0-9\.]*(\"\/>)(\s*-->)?/\2$(abi)\3/" dist/$(name)/addon.xml
|
||||||
|
@printf "cd /addon/@version\nset $(version)\nsave\nbye\n" | xmllint --shell dist/$(name)/addon.xml > /dev/null
|
||||||
|
@cd dist && zip --quiet -9 -r ../../$(zip_name) $(name)
|
||||||
|
@printf ">>> Successfully wrote package for Kodi 19 as ../$(zip_name)\n"
|
||||||
|
|
||||||
|
# You first need to run sudo gem install github_changelog_generator for this
|
||||||
release:
|
release:
|
||||||
ifneq ($(release),)
|
ifneq ($(release),)
|
||||||
docker run -it --rm --env CHANGELOG_GITHUB_TOKEN=$(GH_TOKEN) -v "$(shell pwd)":/usr/local/src/your-app githubchangeloggenerator/github-changelog-generator -u add-ons -p plugin.video.viervijfzes --no-issues --exclude-labels duplicate,question,invalid,wontfix,release,testing --future-release v$(release)
|
@github_changelog_generator -u add-ons -p $(name) --no-issues --exclude-labels duplicate,question,invalid,wontfix release --future-release v$(release);
|
||||||
|
|
||||||
@printf "cd /addon/@version\nset $$release\nsave\nbye\n" | xmllint --shell addon.xml; \
|
@printf "cd /addon/@version\nset $$release\nsave\nbye\n" | xmllint --shell addon.xml; \
|
||||||
date=$(shell date '+%Y-%m-%d'); \
|
date=$(shell date '+%Y-%m-%d'); \
|
||||||
@ -65,5 +95,3 @@ ifneq ($(release),)
|
|||||||
else
|
else
|
||||||
@printf "Usage: make release release=1.0.0\n"
|
@printf "Usage: make release release=1.0.0\n"
|
||||||
endif
|
endif
|
||||||
|
|
||||||
.PHONY: check codefix test clean build release
|
|
||||||
|
13
README.md
@ -1,20 +1,21 @@
|
|||||||
[![GitHub release](https://img.shields.io/github/v/release/add-ons/plugin.video.viervijfzes?display_name=tag)](https://github.com/add-ons/plugin.video.viervijfzes/releases)
|
[![GitHub release](https://img.shields.io/github/release/add-ons/plugin.video.viervijfzes.svg?include_prereleases)](https://github.com/add-ons/plugin.video.viervijfzes/releases)
|
||||||
[![Build Status](https://img.shields.io/github/actions/workflow/status/add-ons/plugin.video.viervijfzes/ci.yml?branch=master)](https://github.com/add-ons/plugin.video.viervijfzes/actions?query=branch%3Amaster)
|
[![Build Status](https://img.shields.io/github/workflow/status/add-ons/plugin.video.viervijfzes/CI/master)](https://github.com/add-ons/plugin.video.viervijfzes/actions?query=branch%3Amaster)
|
||||||
[![Codecov status](https://img.shields.io/codecov/c/github/add-ons/plugin.video.viervijfzes/master)](https://codecov.io/gh/add-ons/plugin.video.viervijfzes/branch/master)
|
[![Codecov status](https://img.shields.io/codecov/c/github/add-ons/plugin.video.viervijfzes/master)](https://codecov.io/gh/add-ons/plugin.video.viervijfzes/branch/master)
|
||||||
[![License: GPLv3](https://img.shields.io/badge/License-GPLv3-yellow.svg)](https://opensource.org/licenses/GPL-3.0)
|
[![License: GPLv3](https://img.shields.io/badge/License-GPLv3-yellow.svg)](https://opensource.org/licenses/GPL-3.0)
|
||||||
[![Contributors](https://img.shields.io/github/contributors/add-ons/plugin.video.viervijfzes.svg)](https://github.com/add-ons/plugin.video.viervijfzes/graphs/contributors)
|
[![Contributors](https://img.shields.io/github/contributors/add-ons/plugin.video.viervijfzes.svg)](https://github.com/add-ons/plugin.video.viervijfzes/graphs/contributors)
|
||||||
|
|
||||||
# GoPlay Kodi add-on
|
# GoPlay Kodi add-on
|
||||||
|
|
||||||
*plugin.video.viervijfzes* is een Kodi add-on om de video-on-demand content van [GoPlay](https://www.goplay.be/) te bekijken. Je moet hiervoor wel eerst een
|
*plugin.video.viervijfzes* is een Kodi add-on om de video-on-demand content van [GoPlay](https://www.goplay.be/) te bekijken.
|
||||||
account aanmaken op [goplay.be](https://www.goplay.be/).
|
|
||||||
|
> Note: Je moet eerst een account aanmaken op [goplay.be](https://www.goplay.be/).
|
||||||
|
|
||||||
Meer informatie kan je vinden op de [Wiki pagina](https://github.com/add-ons/plugin.video.viervijfzes/wiki).
|
Meer informatie kan je vinden op de [Wiki pagina](https://github.com/add-ons/plugin.video.viervijfzes/wiki).
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
De volgende features worden ondersteund:
|
De volgende features worden ondersteund:
|
||||||
* Bekijk on-demand content van Play4, Play5, Play6 en Play7
|
* Bekijk on-demand content van Play4, Play5 en Play6
|
||||||
* Programma's rechtstreeks afspelen vanuit de tv-gids
|
* Programma's rechtstreeks afspelen vanuit de tv-gids
|
||||||
* Doorzoeken van alle programma's
|
* Doorzoeken van alle programma's
|
||||||
* Afspelen van gerelateerde Youtube content
|
* Afspelen van gerelateerde Youtube content
|
||||||
@ -33,4 +34,4 @@ De volgende features worden ondersteund:
|
|||||||
## Disclaimer
|
## Disclaimer
|
||||||
|
|
||||||
Deze add-on wordt niet ondersteund door SBS Belgium, en wordt aangeboden 'as is', zonder enige garantie.
|
Deze add-on wordt niet ondersteund door SBS Belgium, en wordt aangeboden 'as is', zonder enige garantie.
|
||||||
De logo's van GoPlay, Play4, Play5, Play6 en Play7 zijn eigendom van SBS België.
|
De logo's van GoPlay, Play4, Play5 en Play6 zijn eigendom van SBS België.
|
||||||
|
11
addon.xml
@ -1,12 +1,13 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||||
<addon id="plugin.video.viervijfzes" name="GoPlay" version="0.4.12" provider-name="Michaël Arnauts">
|
<addon id="plugin.video.viervijfzes" name="GoPlay" version="0.4.2" provider-name="Michaël Arnauts">
|
||||||
<requires>
|
<requires>
|
||||||
<import addon="xbmc.python" version="3.0.0"/>
|
<!--<import addon="xbmc.python" version="3.0.0"/>-->
|
||||||
<import addon="script.module.dateutil" version="2.6.0"/>
|
<import addon="script.module.dateutil" version="2.6.0"/>
|
||||||
<import addon="script.module.inputstreamhelper" version="0.5.1"/>
|
<import addon="script.module.inputstreamhelper" version="0.5.1"/>
|
||||||
<import addon="script.module.pysocks" version="1.6.8" optional="true"/>
|
<import addon="script.module.pysocks" version="1.6.8" optional="true"/>
|
||||||
<import addon="script.module.requests" version="2.22.0"/>
|
<import addon="script.module.requests" version="2.22.0"/>
|
||||||
<import addon="script.module.routing" version="0.2.0"/>
|
<import addon="script.module.routing" version="0.2.0"/>
|
||||||
|
<import addon="inputstream.adaptive" version="2.4.3"/>
|
||||||
</requires>
|
</requires>
|
||||||
<extension point="xbmc.python.pluginsource" library="addon_entry.py">
|
<extension point="xbmc.python.pluginsource" library="addon_entry.py">
|
||||||
<provides>video</provides>
|
<provides>video</provides>
|
||||||
@ -21,9 +22,9 @@
|
|||||||
<disclaimer lang="en_GB">This add-on is not officially commissioned/supported by SBS Belgium and is provided 'as is' without any warranty of any kind. The Play4, Play5 and Play6 logos are property of SBS Belgium.</disclaimer>
|
<disclaimer lang="en_GB">This add-on is not officially commissioned/supported by SBS Belgium and is provided 'as is' without any warranty of any kind. The Play4, Play5 and Play6 logos are property of SBS Belgium.</disclaimer>
|
||||||
<platform>all</platform>
|
<platform>all</platform>
|
||||||
<license>GPL-3.0-only</license>
|
<license>GPL-3.0-only</license>
|
||||||
<news>v0.4.12 (2024-05-08)
|
<news>v0.4.2 (2021-03-22)
|
||||||
- Added live channels (by mediaminister)
|
- Improve descriptions and images.
|
||||||
- Added PlayCrime Channel</news>
|
- Fix playback of (incorrectly) cached content.</news>
|
||||||
<source>https://github.com/add-ons/plugin.video.viervijfzes</source>
|
<source>https://github.com/add-ons/plugin.video.viervijfzes</source>
|
||||||
<assets>
|
<assets>
|
||||||
<icon>resources/icon.png</icon>
|
<icon>resources/icon.png</icon>
|
||||||
|
@ -4,9 +4,7 @@ pytest
|
|||||||
pytest-cov
|
pytest-cov
|
||||||
pytest-timeout
|
pytest-timeout
|
||||||
python-dateutil
|
python-dateutil
|
||||||
pysocks
|
|
||||||
requests
|
requests
|
||||||
git+https://github.com/tamland/kodi-plugin-routing@master#egg=routing
|
git+git://github.com/dagwieers/kodi-plugin-routing.git@setup#egg=routing
|
||||||
six
|
six
|
||||||
sakee
|
sakee
|
||||||
win-inet-pton; platform_system=="Windows"
|
|
@ -60,10 +60,6 @@ msgstr ""
|
|||||||
|
|
||||||
|
|
||||||
### SUBMENUS
|
### SUBMENUS
|
||||||
msgctxt "#30052"
|
|
||||||
msgid "Watch live [B]{channel}[/B]"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgctxt "#30053"
|
msgctxt "#30053"
|
||||||
msgid "TV Guide for [B]{channel}[/B]"
|
msgid "TV Guide for [B]{channel}[/B]"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
@ -140,10 +136,18 @@ msgctxt "#30702"
|
|||||||
msgid "An error occurred while authenticating: {error}."
|
msgid "An error occurred while authenticating: {error}."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
msgctxt "#30709"
|
||||||
|
msgid "Geo-blocked video"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
msgctxt "#30710"
|
msgctxt "#30710"
|
||||||
msgid "This video is geo-blocked and can't be played from your location."
|
msgid "This video is geo-blocked and can't be played from your location."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
msgctxt "#30711"
|
||||||
|
msgid "Unavailable video"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
msgctxt "#30712"
|
msgctxt "#30712"
|
||||||
msgid "The video is unavailable and can't be played right now."
|
msgid "The video is unavailable and can't be played right now."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
@ -61,10 +61,6 @@ msgstr "Tv-gids"
|
|||||||
|
|
||||||
|
|
||||||
### SUBMENUS
|
### SUBMENUS
|
||||||
msgctxt "#30052"
|
|
||||||
msgid "Watch live [B]{channel}[/B]"
|
|
||||||
msgstr "Kijk live [B]{channel}[/B]"
|
|
||||||
|
|
||||||
msgctxt "#30053"
|
msgctxt "#30053"
|
||||||
msgid "TV Guide for [B]{channel}[/B]"
|
msgid "TV Guide for [B]{channel}[/B]"
|
||||||
msgstr "Tv-gids voor [B]{channel}[/B]"
|
msgstr "Tv-gids voor [B]{channel}[/B]"
|
||||||
@ -141,10 +137,18 @@ msgctxt "#30702"
|
|||||||
msgid "An error occurred while authenticating: {error}."
|
msgid "An error occurred while authenticating: {error}."
|
||||||
msgstr "Er is een fout opgetreden tijdens het aanmelden: {error}."
|
msgstr "Er is een fout opgetreden tijdens het aanmelden: {error}."
|
||||||
|
|
||||||
|
msgctxt "#30709"
|
||||||
|
msgid "Geo-blocked video"
|
||||||
|
msgstr "Video is geografisch geblokkeerd"
|
||||||
|
|
||||||
msgctxt "#30710"
|
msgctxt "#30710"
|
||||||
msgid "This video is geo-blocked and can't be played from your location."
|
msgid "This video is geo-blocked and can't be played from your location."
|
||||||
msgstr "Deze video is geografisch geblokkeerd en kan niet worden afgespeeld vanaf je locatie."
|
msgstr "Deze video is geografisch geblokkeerd en kan niet worden afgespeeld vanaf je locatie."
|
||||||
|
|
||||||
|
msgctxt "#30711"
|
||||||
|
msgid "Unavailable video"
|
||||||
|
msgstr "Onbeschikbare video"
|
||||||
|
|
||||||
msgctxt "#30712"
|
msgctxt "#30712"
|
||||||
msgid "The video is unavailable and can't be played right now."
|
msgid "The video is unavailable and can't be played right now."
|
||||||
msgstr "Deze video is niet beschikbaar en kan nu niet worden afgespeeld."
|
msgstr "Deze video is niet beschikbaar en kan nu niet worden afgespeeld."
|
||||||
|
@ -159,12 +159,11 @@ def play_epg(channel, timestamp):
|
|||||||
TvGuide().play_epg_datetime(channel, timestamp)
|
TvGuide().play_epg_datetime(channel, timestamp)
|
||||||
|
|
||||||
|
|
||||||
@routing.route('/play/catalog')
|
@routing.route('/play/catalog/<uuid>')
|
||||||
@routing.route('/play/catalog/<uuid>/<content_type>')
|
def play_catalog(uuid):
|
||||||
def play_catalog(uuid=None, content_type=None):
|
|
||||||
""" Play the requested item """
|
""" Play the requested item """
|
||||||
from resources.lib.modules.player import Player
|
from resources.lib.modules.player import Player
|
||||||
Player().play(uuid, content_type)
|
Player().play(uuid)
|
||||||
|
|
||||||
|
|
||||||
@routing.route('/play/page/<page>')
|
@routing.route('/play/page/<page>')
|
||||||
|
@ -17,20 +17,19 @@ try: # Python 3
|
|||||||
from html import unescape
|
from html import unescape
|
||||||
except ImportError: # Python 2
|
except ImportError: # Python 2
|
||||||
from HTMLParser import HTMLParser
|
from HTMLParser import HTMLParser
|
||||||
|
|
||||||
unescape = HTMLParser().unescape
|
unescape = HTMLParser().unescape
|
||||||
|
|
||||||
ADDON = xbmcaddon.Addon()
|
ADDON = xbmcaddon.Addon()
|
||||||
|
|
||||||
SORT_METHODS = {
|
SORT_METHODS = dict(
|
||||||
'unsorted': xbmcplugin.SORT_METHOD_UNSORTED,
|
unsorted=xbmcplugin.SORT_METHOD_UNSORTED,
|
||||||
'label': xbmcplugin.SORT_METHOD_LABEL_IGNORE_FOLDERS,
|
label=xbmcplugin.SORT_METHOD_LABEL_IGNORE_FOLDERS,
|
||||||
'title': xbmcplugin.SORT_METHOD_TITLE,
|
title=xbmcplugin.SORT_METHOD_TITLE,
|
||||||
'episode': xbmcplugin.SORT_METHOD_EPISODE,
|
episode=xbmcplugin.SORT_METHOD_EPISODE,
|
||||||
'duration': xbmcplugin.SORT_METHOD_DURATION,
|
duration=xbmcplugin.SORT_METHOD_DURATION,
|
||||||
'year': xbmcplugin.SORT_METHOD_VIDEO_YEAR,
|
year=xbmcplugin.SORT_METHOD_VIDEO_YEAR,
|
||||||
'date': xbmcplugin.SORT_METHOD_DATE
|
date=xbmcplugin.SORT_METHOD_DATE,
|
||||||
}
|
)
|
||||||
DEFAULT_SORT_METHODS = [
|
DEFAULT_SORT_METHODS = [
|
||||||
'unsorted', 'title'
|
'unsorted', 'title'
|
||||||
]
|
]
|
||||||
@ -260,17 +259,12 @@ def play(stream, stream_type=STREAM_HLS, license_key=None, title=None, art_dict=
|
|||||||
elif stream_type == STREAM_DASH:
|
elif stream_type == STREAM_DASH:
|
||||||
play_item.setProperty('inputstream.adaptive.manifest_type', 'mpd')
|
play_item.setProperty('inputstream.adaptive.manifest_type', 'mpd')
|
||||||
play_item.setMimeType('application/dash+xml')
|
play_item.setMimeType('application/dash+xml')
|
||||||
import inputstreamhelper
|
|
||||||
if license_key is not None:
|
if license_key is not None:
|
||||||
# DRM protected MPEG-DASH
|
import inputstreamhelper
|
||||||
is_helper = inputstreamhelper.Helper('mpd', drm='com.widevine.alpha')
|
is_helper = inputstreamhelper.Helper('mpd', drm='com.widevine.alpha')
|
||||||
if is_helper.check_inputstream():
|
if is_helper.check_inputstream():
|
||||||
play_item.setProperty('inputstream.adaptive.license_type', 'com.widevine.alpha')
|
play_item.setProperty('inputstream.adaptive.license_type', 'com.widevine.alpha')
|
||||||
play_item.setProperty('inputstream.adaptive.license_key', license_key)
|
play_item.setProperty('inputstream.adaptive.license_key', license_key)
|
||||||
else:
|
|
||||||
# Unprotected MPEG-DASH
|
|
||||||
is_helper = inputstreamhelper.Helper('mpd')
|
|
||||||
is_helper.check_inputstream()
|
|
||||||
|
|
||||||
play_item.setContentLookup(False)
|
play_item.setContentLookup(False)
|
||||||
|
|
||||||
@ -470,74 +464,13 @@ def open_settings():
|
|||||||
|
|
||||||
def get_global_setting(key):
|
def get_global_setting(key):
|
||||||
"""Get a Kodi setting"""
|
"""Get a Kodi setting"""
|
||||||
result = jsonrpc(method='Settings.GetSettingValue', params={'setting': key})
|
result = jsonrpc(method='Settings.GetSettingValue', params=dict(setting=key))
|
||||||
return result.get('result', {}).get('value')
|
return result.get('result', {}).get('value')
|
||||||
|
|
||||||
|
|
||||||
def set_global_setting(key, value):
|
def set_global_setting(key, value):
|
||||||
"""Set a Kodi setting"""
|
"""Set a Kodi setting"""
|
||||||
return jsonrpc(method='Settings.SetSettingValue', params={'setting': key, 'value': value})
|
return jsonrpc(method='Settings.SetSettingValue', params=dict(setting=key, value=value))
|
||||||
|
|
||||||
|
|
||||||
def has_socks():
|
|
||||||
"""Test if socks is installed, and use a static variable to remember"""
|
|
||||||
if hasattr(has_socks, 'cached'):
|
|
||||||
return getattr(has_socks, 'cached')
|
|
||||||
try:
|
|
||||||
import socks # noqa: F401; pylint: disable=unused-variable,unused-import
|
|
||||||
except ImportError:
|
|
||||||
has_socks.cached = False
|
|
||||||
return None # Detect if this is the first run
|
|
||||||
has_socks.cached = True
|
|
||||||
return True
|
|
||||||
|
|
||||||
|
|
||||||
def get_proxies():
|
|
||||||
"""Return a usable proxies dictionary from Kodi proxy settings"""
|
|
||||||
# Use proxy settings from environment variables
|
|
||||||
env_http_proxy = os.environ.get('HTTP_PROXY')
|
|
||||||
env_https_proxy = os.environ.get('HTTPS_PROXY')
|
|
||||||
if env_http_proxy:
|
|
||||||
return {'http': env_http_proxy, 'https': env_https_proxy or env_http_proxy}
|
|
||||||
|
|
||||||
usehttpproxy = get_global_setting('network.usehttpproxy')
|
|
||||||
if usehttpproxy is not True:
|
|
||||||
return None
|
|
||||||
|
|
||||||
try:
|
|
||||||
httpproxytype = int(get_global_setting('network.httpproxytype'))
|
|
||||||
except ValueError:
|
|
||||||
httpproxytype = 0
|
|
||||||
|
|
||||||
socks_supported = has_socks()
|
|
||||||
if httpproxytype != 0 and not socks_supported:
|
|
||||||
# Only open the dialog the first time (to avoid multiple popups)
|
|
||||||
if socks_supported is None:
|
|
||||||
ok_dialog('', localize(30966)) # Requires PySocks
|
|
||||||
return None
|
|
||||||
|
|
||||||
proxy_types = ['http', 'socks4', 'socks4a', 'socks5', 'socks5h']
|
|
||||||
|
|
||||||
proxy = {
|
|
||||||
'scheme': proxy_types[httpproxytype] if 0 <= httpproxytype < 5 else 'http',
|
|
||||||
'server': get_global_setting('network.httpproxyserver'),
|
|
||||||
'port': get_global_setting('network.httpproxyport'),
|
|
||||||
'username': get_global_setting('network.httpproxyusername'),
|
|
||||||
'password': get_global_setting('network.httpproxypassword')
|
|
||||||
}
|
|
||||||
|
|
||||||
if proxy.get('username') and proxy.get('password') and proxy.get('server') and proxy.get('port'):
|
|
||||||
proxy_address = '{scheme}://{username}:{password}@{server}:{port}'.format(**proxy)
|
|
||||||
elif proxy.get('username') and proxy.get('server') and proxy.get('port'):
|
|
||||||
proxy_address = '{scheme}://{username}@{server}:{port}'.format(**proxy)
|
|
||||||
elif proxy.get('server') and proxy.get('port'):
|
|
||||||
proxy_address = '{scheme}://{server}:{port}'.format(**proxy)
|
|
||||||
elif proxy.get('server'):
|
|
||||||
proxy_address = '{scheme}://{server}'.format(**proxy)
|
|
||||||
else:
|
|
||||||
return None
|
|
||||||
|
|
||||||
return {'http': proxy_address, 'https': proxy_address}
|
|
||||||
|
|
||||||
|
|
||||||
def get_cond_visibility(condition):
|
def get_cond_visibility(condition):
|
||||||
|
@ -4,6 +4,9 @@
|
|||||||
from __future__ import absolute_import, division, unicode_literals
|
from __future__ import absolute_import, division, unicode_literals
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
import dateutil.tz
|
||||||
|
|
||||||
from resources.lib import kodiutils
|
from resources.lib import kodiutils
|
||||||
from resources.lib.kodiutils import TitleItem
|
from resources.lib.kodiutils import TitleItem
|
||||||
@ -110,7 +113,7 @@ class Catalog:
|
|||||||
},
|
},
|
||||||
info_dict={
|
info_dict={
|
||||||
'tvshowtitle': program.title,
|
'tvshowtitle': program.title,
|
||||||
'title': kodiutils.localize(30205, season=season.number) if season.number else season.title, # Season {season}
|
'title': kodiutils.localize(30205, season=season.number), # Season {season}
|
||||||
'plot': season.description or program.description,
|
'plot': season.description or program.description,
|
||||||
'set': program.title,
|
'set': program.title,
|
||||||
}
|
}
|
||||||
@ -240,8 +243,7 @@ class Catalog:
|
|||||||
listing = []
|
listing = []
|
||||||
for episode in episodes:
|
for episode in episodes:
|
||||||
title_item = Menu.generate_titleitem(episode)
|
title_item = Menu.generate_titleitem(episode)
|
||||||
if episode.program_title:
|
title_item.info_dict['title'] = episode.program_title + ' - ' + title_item.title
|
||||||
title_item.info_dict['title'] = episode.program_title + ' - ' + title_item.title
|
|
||||||
listing.append(title_item)
|
listing.append(title_item)
|
||||||
|
|
||||||
for program in programs:
|
for program in programs:
|
||||||
@ -250,10 +252,22 @@ class Catalog:
|
|||||||
kodiutils.show_listing(listing, 30005, content='tvshows')
|
kodiutils.show_listing(listing, 30005, content='tvshows')
|
||||||
|
|
||||||
def show_mylist(self):
|
def show_mylist(self):
|
||||||
""" Show the programs of My List """
|
""" Show all the programs of all channels """
|
||||||
mylist = self._api.get_mylist()
|
try:
|
||||||
|
mylist, _ = self._auth.get_dataset('myList', 'myList')
|
||||||
|
except Exception as ex:
|
||||||
|
kodiutils.notification(message=str(ex))
|
||||||
|
raise
|
||||||
|
|
||||||
listing = [Menu.generate_titleitem(item) for item in mylist]
|
items = []
|
||||||
|
if mylist:
|
||||||
|
for item in mylist:
|
||||||
|
program = self._api.get_program_by_uuid(item.get('id'))
|
||||||
|
if program:
|
||||||
|
program.my_list = True
|
||||||
|
items.append(program)
|
||||||
|
|
||||||
|
listing = [Menu.generate_titleitem(item) for item in items]
|
||||||
|
|
||||||
# Sort items by title
|
# Sort items by title
|
||||||
# Used for A-Z listing or when movies and episodes are mixed.
|
# Used for A-Z listing or when movies and episodes are mixed.
|
||||||
@ -265,7 +279,23 @@ class Catalog:
|
|||||||
kodiutils.end_of_directory()
|
kodiutils.end_of_directory()
|
||||||
return
|
return
|
||||||
|
|
||||||
self._api.mylist_add(uuid)
|
mylist, sync_info = self._auth.get_dataset('myList', 'myList')
|
||||||
|
|
||||||
|
if not mylist:
|
||||||
|
mylist = []
|
||||||
|
|
||||||
|
if uuid not in [item.get('id') for item in mylist]:
|
||||||
|
# Python 2.7 doesn't support .timestamp(), and windows doesn't do '%s', so we need to calculate it ourself
|
||||||
|
epoch = datetime(1970, 1, 1, tzinfo=dateutil.tz.gettz('UTC'))
|
||||||
|
now = datetime.now(tz=dateutil.tz.gettz('UTC'))
|
||||||
|
timestamp = int((now - epoch).total_seconds()) * 1000
|
||||||
|
|
||||||
|
mylist.append({
|
||||||
|
'id': uuid,
|
||||||
|
'timestamp': timestamp,
|
||||||
|
})
|
||||||
|
|
||||||
|
self._auth.put_dataset('myList', 'myList', mylist, sync_info)
|
||||||
|
|
||||||
kodiutils.end_of_directory()
|
kodiutils.end_of_directory()
|
||||||
|
|
||||||
@ -275,6 +305,12 @@ class Catalog:
|
|||||||
kodiutils.end_of_directory()
|
kodiutils.end_of_directory()
|
||||||
return
|
return
|
||||||
|
|
||||||
self._api.mylist_del(uuid)
|
mylist, sync_info = self._auth.get_dataset('myList', 'myList')
|
||||||
|
|
||||||
|
if not mylist:
|
||||||
|
mylist = []
|
||||||
|
|
||||||
|
new_mylist = [item for item in mylist if item.get('id') != uuid]
|
||||||
|
self._auth.put_dataset('myList', 'myList', new_mylist, sync_info)
|
||||||
|
|
||||||
kodiutils.end_of_directory()
|
kodiutils.end_of_directory()
|
||||||
|
@ -71,27 +71,9 @@ class Channels:
|
|||||||
|
|
||||||
# Lookup the high resolution logo based on the channel name
|
# Lookup the high resolution logo based on the channel name
|
||||||
fanart = '{path}/resources/logos/{logo}'.format(path=kodiutils.addon_path(), logo=channel_info.get('background'))
|
fanart = '{path}/resources/logos/{logo}'.format(path=kodiutils.addon_path(), logo=channel_info.get('background'))
|
||||||
icon = '{path}/resources/logos/{logo}'.format(path=kodiutils.addon_path(), logo=channel_info.get('logo'))
|
|
||||||
|
|
||||||
listing = []
|
listing = []
|
||||||
|
|
||||||
listing.append(
|
|
||||||
TitleItem(
|
|
||||||
title=kodiutils.localize(30052, channel=channel_info.get('name')), # Watch live {channel}
|
|
||||||
path=kodiutils.url_for('play_live', channel=channel_info.get('name')) + '?.pvr',
|
|
||||||
art_dict={
|
|
||||||
'icon': icon,
|
|
||||||
'fanart': fanart,
|
|
||||||
},
|
|
||||||
info_dict={
|
|
||||||
'plot': kodiutils.localize(30052, channel=channel_info.get('name')), # Watch live {channel}
|
|
||||||
'playcount': 0,
|
|
||||||
'mediatype': 'video',
|
|
||||||
},
|
|
||||||
is_playable=True,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
if channel_info.get('epg_id'):
|
if channel_info.get('epg_id'):
|
||||||
listing.append(
|
listing.append(
|
||||||
TitleItem(
|
TitleItem(
|
||||||
|
@ -4,7 +4,7 @@
|
|||||||
from __future__ import absolute_import, division, unicode_literals
|
from __future__ import absolute_import, division, unicode_literals
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
from datetime import datetime, timedelta
|
from datetime import timedelta
|
||||||
|
|
||||||
from resources.lib import kodiutils
|
from resources.lib import kodiutils
|
||||||
from resources.lib.viervijfzes import CHANNELS
|
from resources.lib.viervijfzes import CHANNELS
|
||||||
@ -42,17 +42,17 @@ class IPTVManager:
|
|||||||
streams = []
|
streams = []
|
||||||
for key, channel in CHANNELS.items():
|
for key, channel in CHANNELS.items():
|
||||||
if channel.get('iptv_id'):
|
if channel.get('iptv_id'):
|
||||||
streams.append({
|
streams.append(dict(
|
||||||
'id': channel.get('iptv_id'),
|
id=channel.get('iptv_id'),
|
||||||
'name': channel.get('name'),
|
name=channel.get('name'),
|
||||||
'logo': 'special://home/addons/{addon}/resources/logos/{logo}'.format(addon=kodiutils.addon_id(),
|
logo='special://home/addons/{addon}/resources/logos/{logo}'.format(addon=kodiutils.addon_id(),
|
||||||
logo=channel.get('logo')),
|
logo=channel.get('logo')),
|
||||||
'preset': channel.get('iptv_preset'),
|
preset=channel.get('iptv_preset'),
|
||||||
'stream': 'plugin://plugin.video.viervijfzes/play/live/{channel}'.format(channel=key),
|
stream='plugin://plugin.video.viervijfzes/play/live/{channel}'.format(channel=key),
|
||||||
'vod': 'plugin://plugin.video.viervijfzes/play/epg/{channel}/{{date}}'.format(channel=key)
|
vod='plugin://plugin.video.viervijfzes/play/epg/{channel}/{{date}}'.format(channel=key)
|
||||||
})
|
))
|
||||||
|
|
||||||
return {'version': 1, 'streams': streams}
|
return dict(version=1, streams=streams)
|
||||||
|
|
||||||
@via_socket
|
@via_socket
|
||||||
def send_epg(): # pylint: disable=no-method-argument
|
def send_epg(): # pylint: disable=no-method-argument
|
||||||
@ -64,35 +64,30 @@ class IPTVManager:
|
|||||||
except ImportError: # Python 2
|
except ImportError: # Python 2
|
||||||
from urllib import quote
|
from urllib import quote
|
||||||
|
|
||||||
today = datetime.today()
|
results = dict()
|
||||||
|
|
||||||
results = {}
|
|
||||||
for key, channel in CHANNELS.items():
|
for key, channel in CHANNELS.items():
|
||||||
iptv_id = channel.get('iptv_id')
|
iptv_id = channel.get('iptv_id')
|
||||||
|
|
||||||
if channel.get('iptv_id'):
|
if channel.get('iptv_id'):
|
||||||
results[iptv_id] = []
|
results[iptv_id] = []
|
||||||
|
for date in ['yesterday', 'today', 'tomorrow']:
|
||||||
for i in range(-3, 7):
|
epg = epg_api.get_epg(key, date)
|
||||||
date = today + timedelta(days=i)
|
|
||||||
epg = epg_api.get_epg(key, date.strftime('%Y-%m-%d'))
|
|
||||||
|
|
||||||
results[iptv_id].extend([
|
results[iptv_id].extend([
|
||||||
{
|
dict(
|
||||||
'start': program.start.isoformat(),
|
start=program.start.isoformat(),
|
||||||
'stop': (program.start + timedelta(seconds=program.duration)).isoformat(),
|
stop=(program.start + timedelta(seconds=program.duration)).isoformat(),
|
||||||
'title': program.program_title,
|
title=program.program_title,
|
||||||
'subtitle': program.episode_title,
|
subtitle=program.episode_title,
|
||||||
'description': program.description,
|
description=program.description,
|
||||||
'episode': 'S%sE%s' % (program.season, program.number) if program.season and program.number else None,
|
episode='S%sE%s' % (program.season, program.number) if program.season and program.number else None,
|
||||||
'genre': program.genre,
|
genre=program.genre,
|
||||||
'genre_id': program.genre_id,
|
genre_id=program.genre_id,
|
||||||
'image': program.thumb,
|
image=program.thumb,
|
||||||
'stream': kodiutils.url_for('play_from_page',
|
stream=kodiutils.url_for('play_from_page',
|
||||||
channel=key,
|
channel=key,
|
||||||
page=quote(program.video_url, safe='')) if program.video_url else None
|
page=quote(program.video_url, safe='')) if program.video_url else None)
|
||||||
}
|
|
||||||
for program in epg if program.duration
|
for program in epg if program.duration
|
||||||
])
|
])
|
||||||
|
|
||||||
return {'version': 1, 'epg': results}
|
return dict(version=1, epg=results)
|
||||||
|
@ -31,68 +31,68 @@ class Menu:
|
|||||||
TitleItem(
|
TitleItem(
|
||||||
title=kodiutils.localize(30001), # A-Z
|
title=kodiutils.localize(30001), # A-Z
|
||||||
path=kodiutils.url_for('show_catalog'),
|
path=kodiutils.url_for('show_catalog'),
|
||||||
art_dict={
|
art_dict=dict(
|
||||||
'icon': 'DefaultMovieTitle.png',
|
icon='DefaultMovieTitle.png',
|
||||||
'fanart': kodiutils.get_addon_info('fanart')
|
fanart=kodiutils.get_addon_info('fanart'),
|
||||||
},
|
),
|
||||||
info_dict={
|
info_dict=dict(
|
||||||
'plot': kodiutils.localize(30002)
|
plot=kodiutils.localize(30002),
|
||||||
}
|
)
|
||||||
),
|
),
|
||||||
TitleItem(
|
TitleItem(
|
||||||
title=kodiutils.localize(30007), # TV Channels
|
title=kodiutils.localize(30007), # TV Channels
|
||||||
path=kodiutils.url_for('show_channels'),
|
path=kodiutils.url_for('show_channels'),
|
||||||
art_dict={
|
art_dict=dict(
|
||||||
'icon': 'DefaultAddonPVRClient.png',
|
icon='DefaultAddonPVRClient.png',
|
||||||
'fanart': kodiutils.get_addon_info('fanart')
|
fanart=kodiutils.get_addon_info('fanart'),
|
||||||
},
|
),
|
||||||
info_dict={
|
info_dict=dict(
|
||||||
'plot': kodiutils.localize(30008)
|
plot=kodiutils.localize(30008),
|
||||||
}
|
)
|
||||||
),
|
),
|
||||||
TitleItem(
|
TitleItem(
|
||||||
title=kodiutils.localize(30003), # Catalog
|
title=kodiutils.localize(30003), # Catalog
|
||||||
path=kodiutils.url_for('show_categories'),
|
path=kodiutils.url_for('show_categories'),
|
||||||
art_dict={
|
art_dict=dict(
|
||||||
'icon': 'DefaultGenre.png',
|
icon='DefaultGenre.png',
|
||||||
'fanart': kodiutils.get_addon_info('fanart')
|
fanart=kodiutils.get_addon_info('fanart'),
|
||||||
},
|
),
|
||||||
info_dict={
|
info_dict=dict(
|
||||||
'plot': kodiutils.localize(30004)
|
plot=kodiutils.localize(30004),
|
||||||
}
|
)
|
||||||
),
|
),
|
||||||
TitleItem(
|
TitleItem(
|
||||||
title=kodiutils.localize(30005), # Recommendations
|
title=kodiutils.localize(30005), # Recommendations
|
||||||
path=kodiutils.url_for('show_recommendations'),
|
path=kodiutils.url_for('show_recommendations'),
|
||||||
art_dict={
|
art_dict=dict(
|
||||||
'icon': 'DefaultFavourites.png',
|
icon='DefaultFavourites.png',
|
||||||
'fanart': kodiutils.get_addon_info('fanart')
|
fanart=kodiutils.get_addon_info('fanart'),
|
||||||
},
|
),
|
||||||
info_dict={
|
info_dict=dict(
|
||||||
'plot': kodiutils.localize(30006)
|
plot=kodiutils.localize(30006),
|
||||||
}
|
)
|
||||||
),
|
),
|
||||||
TitleItem(
|
TitleItem(
|
||||||
title=kodiutils.localize(30011), # My List
|
title=kodiutils.localize(30011), # My List
|
||||||
path=kodiutils.url_for('show_mylist'),
|
path=kodiutils.url_for('show_mylist'),
|
||||||
art_dict={
|
art_dict=dict(
|
||||||
'icon': 'DefaultPlaylist.png',
|
icon='DefaultPlaylist.png',
|
||||||
'fanart': kodiutils.get_addon_info('fanart')
|
fanart=kodiutils.get_addon_info('fanart'),
|
||||||
},
|
),
|
||||||
info_dict={
|
info_dict=dict(
|
||||||
'plot': kodiutils.localize(30012)
|
plot=kodiutils.localize(30012),
|
||||||
}
|
)
|
||||||
),
|
),
|
||||||
TitleItem(
|
TitleItem(
|
||||||
title=kodiutils.localize(30009), # Search
|
title=kodiutils.localize(30009), # Search
|
||||||
path=kodiutils.url_for('show_search'),
|
path=kodiutils.url_for('show_search'),
|
||||||
art_dict={
|
art_dict=dict(
|
||||||
'icon': 'DefaultAddonsSearch.png',
|
icon='DefaultAddonsSearch.png',
|
||||||
'fanart': kodiutils.get_addon_info('fanart')
|
fanart=kodiutils.get_addon_info('fanart'),
|
||||||
},
|
),
|
||||||
info_dict={
|
info_dict=dict(
|
||||||
'plot': kodiutils.localize(30010)
|
plot=kodiutils.localize(30010),
|
||||||
}
|
)
|
||||||
)
|
)
|
||||||
]
|
]
|
||||||
|
|
||||||
@ -128,7 +128,14 @@ class Menu:
|
|||||||
}
|
}
|
||||||
|
|
||||||
visible = True
|
visible = True
|
||||||
title = item.title
|
if isinstance(item.episodes, list) and not item.episodes:
|
||||||
|
# We know that we don't have episodes
|
||||||
|
title = '[COLOR gray]' + item.title + '[/COLOR]'
|
||||||
|
visible = kodiutils.get_setting_bool('interface_show_unavailable')
|
||||||
|
|
||||||
|
else:
|
||||||
|
# We have episodes, or we don't know it
|
||||||
|
title = item.title
|
||||||
|
|
||||||
context_menu = []
|
context_menu = []
|
||||||
if item.uuid:
|
if item.uuid:
|
||||||
@ -183,7 +190,7 @@ class Menu:
|
|||||||
|
|
||||||
if item.uuid:
|
if item.uuid:
|
||||||
# We have an UUID and can play this item directly
|
# We have an UUID and can play this item directly
|
||||||
path = kodiutils.url_for('play_catalog', uuid=item.uuid, content_type=item.content_type)
|
path = kodiutils.url_for('play_catalog', uuid=item.uuid)
|
||||||
else:
|
else:
|
||||||
# We don't have an UUID, and first need to fetch the video information from the page
|
# We don't have an UUID, and first need to fetch the video information from the page
|
||||||
path = kodiutils.url_for('play_from_page', page=quote(item.path, safe=''))
|
path = kodiutils.url_for('play_from_page', page=quote(item.path, safe=''))
|
||||||
|
@ -12,6 +12,11 @@ from resources.lib.viervijfzes.auth import AuthApi
|
|||||||
from resources.lib.viervijfzes.aws.cognito_idp import AuthenticationException, InvalidLoginException
|
from resources.lib.viervijfzes.aws.cognito_idp import AuthenticationException, InvalidLoginException
|
||||||
from resources.lib.viervijfzes.content import CACHE_PREVENT, ContentApi, GeoblockedException, UnavailableException
|
from resources.lib.viervijfzes.content import CACHE_PREVENT, ContentApi, GeoblockedException, UnavailableException
|
||||||
|
|
||||||
|
try: # Python 3
|
||||||
|
from urllib.parse import quote, urlencode
|
||||||
|
except ImportError: # Python 2
|
||||||
|
from urllib import quote, urlencode
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
@ -26,29 +31,19 @@ class Player:
|
|||||||
# Workaround for Raspberry Pi 3 and older
|
# Workaround for Raspberry Pi 3 and older
|
||||||
kodiutils.set_global_setting('videoplayer.useomxplayer', True)
|
kodiutils.set_global_setting('videoplayer.useomxplayer', True)
|
||||||
|
|
||||||
def live(self, channel):
|
@staticmethod
|
||||||
|
def live(channel):
|
||||||
""" Play the live channel.
|
""" Play the live channel.
|
||||||
:type channel: string
|
:type channel: string
|
||||||
"""
|
"""
|
||||||
# TODO: this doesn't work correctly, playing a live program from the PVR won't play something from the beginning
|
channel_name = CHANNELS.get(channel, dict(name=channel))
|
||||||
# Lookup current program
|
kodiutils.ok_dialog(message=kodiutils.localize(30718, channel=channel_name.get('name'))) # There is no live stream available for {channel}.
|
||||||
# broadcast = self._epg.get_broadcast(channel, datetime.datetime.now().isoformat())
|
kodiutils.end_of_directory()
|
||||||
# if broadcast and broadcast.video_url:
|
|
||||||
# self.play_from_page(broadcast.video_url)
|
|
||||||
# return
|
|
||||||
|
|
||||||
channel_url = CHANNELS.get(channel, {'url': channel}).get('url')
|
|
||||||
|
|
||||||
self.play_from_page(channel_url)
|
|
||||||
|
|
||||||
def play_from_page(self, path):
|
def play_from_page(self, path):
|
||||||
""" Play the requested item.
|
""" Play the requested item.
|
||||||
:type path: string
|
:type path: string
|
||||||
"""
|
"""
|
||||||
if not path:
|
|
||||||
kodiutils.ok_dialog(message=kodiutils.localize(30712)) # The video is unavailable...
|
|
||||||
return
|
|
||||||
|
|
||||||
# Get episode information
|
# Get episode information
|
||||||
episode = self._api.get_episode(path, cache=CACHE_PREVENT)
|
episode = self._api.get_episode(path, cache=CACHE_PREVENT)
|
||||||
resolved_stream = None
|
resolved_stream = None
|
||||||
@ -68,36 +63,47 @@ class Player:
|
|||||||
|
|
||||||
if episode.uuid:
|
if episode.uuid:
|
||||||
# Lookup the stream
|
# Lookup the stream
|
||||||
resolved_stream = self._resolve_stream(episode.uuid, episode.content_type)
|
resolved_stream = self._resolve_stream(episode.uuid)
|
||||||
_LOGGER.debug('Resolved stream: %s', resolved_stream)
|
_LOGGER.debug('Resolved stream: %s', resolved_stream)
|
||||||
|
|
||||||
if resolved_stream:
|
if resolved_stream:
|
||||||
titleitem = Menu.generate_titleitem(episode)
|
titleitem = Menu.generate_titleitem(episode)
|
||||||
|
if resolved_stream.license_url:
|
||||||
|
# Generate license key
|
||||||
|
license_key = self.create_license_key(resolved_stream.license_url,
|
||||||
|
key_headers=dict(
|
||||||
|
customdata=resolved_stream.auth,
|
||||||
|
))
|
||||||
|
else:
|
||||||
|
license_key = None
|
||||||
|
|
||||||
kodiutils.play(resolved_stream.url,
|
kodiutils.play(resolved_stream.url,
|
||||||
resolved_stream.stream_type,
|
resolved_stream.stream_type,
|
||||||
resolved_stream.license_key,
|
license_key,
|
||||||
info_dict=titleitem.info_dict,
|
info_dict=titleitem.info_dict,
|
||||||
art_dict=titleitem.art_dict,
|
art_dict=titleitem.art_dict,
|
||||||
prop_dict=titleitem.prop_dict)
|
prop_dict=titleitem.prop_dict)
|
||||||
|
|
||||||
def play(self, uuid, content_type):
|
def play(self, uuid):
|
||||||
""" Play the requested item.
|
""" Play the requested item.
|
||||||
:type uuid: string
|
:type uuid: string
|
||||||
:type content_type: string
|
|
||||||
"""
|
"""
|
||||||
if not uuid:
|
|
||||||
kodiutils.ok_dialog(message=kodiutils.localize(30712)) # The video is unavailable...
|
|
||||||
return
|
|
||||||
|
|
||||||
# Lookup the stream
|
# Lookup the stream
|
||||||
resolved_stream = self._resolve_stream(uuid, content_type)
|
resolved_stream = self._resolve_stream(uuid)
|
||||||
kodiutils.play(resolved_stream.url, resolved_stream.stream_type, resolved_stream.license_key)
|
if resolved_stream.license_url:
|
||||||
|
# Generate license key
|
||||||
|
license_key = self.create_license_key(resolved_stream.license_url, key_headers=dict(
|
||||||
|
customdata=resolved_stream.auth,
|
||||||
|
))
|
||||||
|
else:
|
||||||
|
license_key = None
|
||||||
|
|
||||||
|
kodiutils.play(resolved_stream.url, resolved_stream.stream_type, license_key)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _resolve_stream(uuid, content_type):
|
def _resolve_stream(uuid):
|
||||||
""" Resolve the stream for the requested item
|
""" Resolve the stream for the requested item
|
||||||
:type uuid: string
|
:type uuid: string
|
||||||
:type content_type: string
|
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
# Check if we have credentials
|
# Check if we have credentials
|
||||||
@ -114,7 +120,7 @@ class Player:
|
|||||||
auth = AuthApi(kodiutils.get_setting('username'), kodiutils.get_setting('password'), kodiutils.get_tokens_path())
|
auth = AuthApi(kodiutils.get_setting('username'), kodiutils.get_setting('password'), kodiutils.get_tokens_path())
|
||||||
|
|
||||||
# Get stream information
|
# Get stream information
|
||||||
resolved_stream = ContentApi(auth).get_stream_by_uuid(uuid, content_type)
|
resolved_stream = ContentApi(auth).get_stream_by_uuid(uuid)
|
||||||
return resolved_stream
|
return resolved_stream
|
||||||
|
|
||||||
except (InvalidLoginException, AuthenticationException) as ex:
|
except (InvalidLoginException, AuthenticationException) as ex:
|
||||||
@ -124,9 +130,32 @@ class Player:
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
except GeoblockedException:
|
except GeoblockedException:
|
||||||
kodiutils.ok_dialog(message=kodiutils.localize(30710)) # This video is geo-blocked...
|
kodiutils.ok_dialog(heading=kodiutils.localize(30709), message=kodiutils.localize(30710)) # This video is geo-blocked...
|
||||||
return None
|
return None
|
||||||
|
|
||||||
except UnavailableException:
|
except UnavailableException:
|
||||||
kodiutils.ok_dialog(message=kodiutils.localize(30712)) # The video is unavailable...
|
kodiutils.ok_dialog(heading=kodiutils.localize(30711), message=kodiutils.localize(30712)) # The video is unavailable...
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def create_license_key(key_url, key_type='R', key_headers=None, key_value=None):
|
||||||
|
""" Create a license key string that we need for inputstream.adaptive.
|
||||||
|
|
||||||
|
:param str key_url:
|
||||||
|
:param str key_type:
|
||||||
|
:param dict[str, str] key_headers:
|
||||||
|
:param str key_value:
|
||||||
|
:rtype: str
|
||||||
|
"""
|
||||||
|
header = ''
|
||||||
|
if key_headers:
|
||||||
|
header = urlencode(key_headers)
|
||||||
|
|
||||||
|
if key_type in ('A', 'R', 'B'):
|
||||||
|
key_value = key_type + '{SSM}'
|
||||||
|
elif key_type == 'D':
|
||||||
|
if 'D{SSM}' not in key_value:
|
||||||
|
raise ValueError('Missing D{SSM} placeholder')
|
||||||
|
key_value = quote(key_value)
|
||||||
|
|
||||||
|
return '%s|%s|%s|' % (key_url, header, key_value)
|
||||||
|
@ -36,8 +36,8 @@ class TvGuide:
|
|||||||
dates = []
|
dates = []
|
||||||
today = datetime.today()
|
today = datetime.today()
|
||||||
|
|
||||||
# The API provides 7 days in the past and 8 days in the future
|
# The API provides 7 days in the past and 13 days in the future
|
||||||
for i in range(-7, 8):
|
for i in range(-7, 13):
|
||||||
day = today + timedelta(days=i)
|
day = today + timedelta(days=i)
|
||||||
|
|
||||||
if i == -1:
|
if i == -1:
|
||||||
@ -131,7 +131,7 @@ class TvGuide:
|
|||||||
if program.video_url:
|
if program.video_url:
|
||||||
path = kodiutils.url_for('play_from_page', channel=channel, page=quote(program.video_url, safe=''))
|
path = kodiutils.url_for('play_from_page', channel=channel, page=quote(program.video_url, safe=''))
|
||||||
else:
|
else:
|
||||||
path = kodiutils.url_for('play_catalog', uuid='')
|
path = None
|
||||||
title = '[COLOR gray]' + title + '[/COLOR]'
|
title = '[COLOR gray]' + title + '[/COLOR]'
|
||||||
|
|
||||||
stream_dict = STREAM_DICT.copy()
|
stream_dict = STREAM_DICT.copy()
|
||||||
|
@ -125,7 +125,7 @@ class KodiPlayer(Player):
|
|||||||
if not self.av_started:
|
if not self.av_started:
|
||||||
# Check stream path
|
# Check stream path
|
||||||
import requests
|
import requests
|
||||||
response = requests.get(self.stream_path, timeout=5)
|
response = requests.get(self.stream_path)
|
||||||
if response.status_code == 403:
|
if response.status_code == 403:
|
||||||
message_id = 30720
|
message_id = 30720
|
||||||
else:
|
else:
|
||||||
|
@ -5,69 +5,68 @@ from __future__ import absolute_import, division, unicode_literals
|
|||||||
from collections import OrderedDict
|
from collections import OrderedDict
|
||||||
|
|
||||||
CHANNELS = OrderedDict([
|
CHANNELS = OrderedDict([
|
||||||
('Play4', {
|
('Play4', dict(
|
||||||
'name': 'Play4',
|
name='Play4',
|
||||||
'url': 'live-kijken/play-4',
|
epg_id='vier',
|
||||||
'epg_id': 'vier',
|
logo='play4.png',
|
||||||
'logo': 'play4.png',
|
background='play4-background.png',
|
||||||
'background': 'play4-background.png',
|
iptv_preset=4,
|
||||||
'iptv_preset': 4,
|
iptv_id='play4.be',
|
||||||
'iptv_id': 'play4.be',
|
youtube=[
|
||||||
'youtube': [
|
dict(
|
||||||
{'label': 'GoPlay', 'logo': 'goplay.png', 'path': 'plugin://plugin.video.youtube/user/viertv/'},
|
label='GoPlay',
|
||||||
]
|
logo='goplay.png',
|
||||||
}),
|
path='plugin://plugin.video.youtube/user/viertv/',
|
||||||
('Play5', {
|
),
|
||||||
'name': 'Play5',
|
],
|
||||||
'url': 'live-kijken/play-5',
|
)),
|
||||||
'epg_id': 'vijf',
|
('Play5', dict(
|
||||||
'logo': 'play5.png',
|
name='Play5',
|
||||||
'background': 'play5-background.png',
|
epg_id='vijf',
|
||||||
'iptv_preset': 5,
|
logo='play5.png',
|
||||||
'iptv_id': 'play5.be',
|
background='play5-background.png',
|
||||||
'youtube': [
|
iptv_preset=5,
|
||||||
{'label': 'GoPlay', 'logo': 'goplay.png', 'path': 'plugin://plugin.video.youtube/user/viertv/'},
|
iptv_id='play5.be',
|
||||||
]
|
youtube=[
|
||||||
}),
|
dict(
|
||||||
('Play6', {
|
label='GoPlay',
|
||||||
'name': 'Play6',
|
logo='goplay.png',
|
||||||
'url': 'live-kijken/play-6',
|
path='plugin://plugin.video.youtube/user/viertv/',
|
||||||
'epg_id': 'zes',
|
),
|
||||||
'logo': 'play6.png',
|
],
|
||||||
'background': 'play6-background.png',
|
)),
|
||||||
'iptv_preset': 6,
|
('Play6', dict(
|
||||||
'iptv_id': 'play6.be',
|
name='Play6',
|
||||||
'youtube': [
|
epg_id='zes',
|
||||||
{'label': 'GoPlay', 'logo': 'goplay.png', 'path': 'plugin://plugin.video.youtube/user/viertv/'},
|
logo='play6.png',
|
||||||
]
|
background='play6-background.png',
|
||||||
}),
|
iptv_preset=6,
|
||||||
('Play7', {
|
iptv_id='play6.be',
|
||||||
'name': 'Play7',
|
youtube=[
|
||||||
'url': 'live-kijken/play-7',
|
dict(
|
||||||
'epg_id': 'zeven',
|
label='GoPlay',
|
||||||
'logo': 'play7.png',
|
logo='goplay.png',
|
||||||
'background': 'play7-background.png',
|
path='plugin://plugin.video.youtube/user/viertv/',
|
||||||
'iptv_preset': 17,
|
),
|
||||||
'iptv_id': 'play7.be',
|
],
|
||||||
'youtube': []
|
)),
|
||||||
}),
|
# ('Play7', dict(
|
||||||
('PlayCrime', {
|
# name='Play7',
|
||||||
'name': 'PlayCrime',
|
# epg_id='zeven',
|
||||||
'url': 'live-kijken/play-crime',
|
# url='https://www.goplay.be',
|
||||||
'epg_id': 'crime',
|
# logo='play7.png',
|
||||||
'logo': 'playcrime.png',
|
# background='play7-background.png',
|
||||||
'background': 'playcrime-background.png',
|
# iptv_preset=7,
|
||||||
'iptv_preset': 18,
|
# iptv_id='play7.be',
|
||||||
'iptv_id': 'playcrime.be',
|
# youtube=[],
|
||||||
'youtube': []
|
# )),
|
||||||
}),
|
('GoPlay', dict(
|
||||||
('GoPlay', {
|
name='Go Play',
|
||||||
'name': 'Go Play',
|
url='https://www.goplay.be',
|
||||||
'url': 'https://www.goplay.be',
|
logo='goplay.png',
|
||||||
'logo': 'goplay.png',
|
background='goplay-background.png',
|
||||||
'background': 'goplay-background.png',
|
youtube=[],
|
||||||
'youtube': []
|
))
|
||||||
})
|
|
||||||
])
|
])
|
||||||
|
|
||||||
STREAM_DICT = {
|
STREAM_DICT = {
|
||||||
@ -80,17 +79,19 @@ STREAM_DICT = {
|
|||||||
class ResolvedStream:
|
class ResolvedStream:
|
||||||
""" Defines a stream that we can play"""
|
""" Defines a stream that we can play"""
|
||||||
|
|
||||||
def __init__(self, uuid=None, url=None, stream_type=None, license_key=None):
|
def __init__(self, uuid=None, url=None, stream_type=None, license_url=None, auth=None):
|
||||||
"""
|
"""
|
||||||
:type uuid: str
|
:type uuid: str
|
||||||
:type url: str
|
:type url: str
|
||||||
:type stream_type: str
|
:type stream_type: str
|
||||||
:type license_key: str
|
:type license_url: str
|
||||||
|
:type auth: str
|
||||||
"""
|
"""
|
||||||
self.uuid = uuid
|
self.uuid = uuid
|
||||||
self.url = url
|
self.url = url
|
||||||
self.stream_type = stream_type
|
self.stream_type = stream_type
|
||||||
self.license_key = license_key
|
self.license_url = license_url
|
||||||
|
self.auth = auth
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return "%r" % self.__dict__
|
return "%r" % self.__dict__
|
||||||
|
@ -79,11 +79,11 @@ class AuthApi:
|
|||||||
if not os.path.exists(self._token_path):
|
if not os.path.exists(self._token_path):
|
||||||
os.makedirs(self._token_path)
|
os.makedirs(self._token_path)
|
||||||
with open(os.path.join(self._token_path, self.TOKEN_FILE), 'w') as fdesc:
|
with open(os.path.join(self._token_path, self.TOKEN_FILE), 'w') as fdesc:
|
||||||
data = json.dumps({
|
data = json.dumps(dict(
|
||||||
'id_token': self._id_token,
|
id_token=self._id_token,
|
||||||
'refresh_token': self._refresh_token,
|
refresh_token=self._refresh_token,
|
||||||
'expiry': self._expiry
|
expiry=self._expiry,
|
||||||
})
|
))
|
||||||
fdesc.write(kodiutils.from_unicode(data))
|
fdesc.write(kodiutils.from_unicode(data))
|
||||||
|
|
||||||
return self._id_token
|
return self._id_token
|
||||||
|
@ -44,6 +44,7 @@ class CognitoIdentity:
|
|||||||
'x-amz-target': 'AWSCognitoIdentityService.GetId',
|
'x-amz-target': 'AWSCognitoIdentityService.GetId',
|
||||||
'content-type': 'application/x-amz-json-1.1',
|
'content-type': 'application/x-amz-json-1.1',
|
||||||
})
|
})
|
||||||
|
_LOGGER.debug(response.text)
|
||||||
|
|
||||||
result = json.loads(response.text)
|
result = json.loads(response.text)
|
||||||
|
|
||||||
@ -63,6 +64,7 @@ class CognitoIdentity:
|
|||||||
'x-amz-target': 'AWSCognitoIdentityService.GetCredentialsForIdentity',
|
'x-amz-target': 'AWSCognitoIdentityService.GetCredentialsForIdentity',
|
||||||
'content-type': 'application/x-amz-json-1.1',
|
'content-type': 'application/x-amz-json-1.1',
|
||||||
})
|
})
|
||||||
|
_LOGGER.debug(response.text)
|
||||||
|
|
||||||
result = json.loads(response.text)
|
result = json.loads(response.text)
|
||||||
|
|
||||||
|
@ -77,6 +77,7 @@ class CognitoIdp:
|
|||||||
self.k = self.__hex_to_long(self.__hex_hash('00' + self.n_hex + '0' + self.g_hex)) # pylint: disable=invalid-name
|
self.k = self.__hex_to_long(self.__hex_hash('00' + self.n_hex + '0' + self.g_hex)) # pylint: disable=invalid-name
|
||||||
self.small_a_value = self.__generate_random_small_a()
|
self.small_a_value = self.__generate_random_small_a()
|
||||||
self.large_a_value = self.__calculate_a()
|
self.large_a_value = self.__calculate_a()
|
||||||
|
_LOGGER.debug("Created %s", self)
|
||||||
|
|
||||||
def authenticate(self, username, password):
|
def authenticate(self, username, password):
|
||||||
""" Authenticate with a username and password. """
|
""" Authenticate with a username and password. """
|
||||||
|
@ -15,7 +15,6 @@ try: # Python 3
|
|||||||
from urllib.parse import quote, urlparse
|
from urllib.parse import quote, urlparse
|
||||||
except ImportError: # Python 2
|
except ImportError: # Python 2
|
||||||
from urllib import quote
|
from urllib import quote
|
||||||
|
|
||||||
from urlparse import urlparse
|
from urlparse import urlparse
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
@ -13,8 +13,7 @@ from datetime import datetime
|
|||||||
|
|
||||||
import requests
|
import requests
|
||||||
|
|
||||||
from resources.lib import kodiutils
|
from resources.lib.kodiutils import html_to_kodi, STREAM_DASH, STREAM_HLS
|
||||||
from resources.lib.kodiutils import STREAM_DASH, STREAM_HLS, html_to_kodi
|
|
||||||
from resources.lib.viervijfzes import ResolvedStream
|
from resources.lib.viervijfzes import ResolvedStream
|
||||||
|
|
||||||
try: # Python 3
|
try: # Python 3
|
||||||
@ -30,8 +29,6 @@ CACHE_AUTO = 1 # Allow to use the cache, and query the API if no cache is avail
|
|||||||
CACHE_ONLY = 2 # Only use the cache, don't use the API
|
CACHE_ONLY = 2 # Only use the cache, don't use the API
|
||||||
CACHE_PREVENT = 3 # Don't use the cache
|
CACHE_PREVENT = 3 # Don't use the cache
|
||||||
|
|
||||||
PROXIES = kodiutils.get_proxies()
|
|
||||||
|
|
||||||
|
|
||||||
class UnavailableException(Exception):
|
class UnavailableException(Exception):
|
||||||
""" Is thrown when an item is unavailable. """
|
""" Is thrown when an item is unavailable. """
|
||||||
@ -112,7 +109,7 @@ class Episode:
|
|||||||
""" Defines an Episode. """
|
""" Defines an Episode. """
|
||||||
|
|
||||||
def __init__(self, uuid=None, nodeid=None, path=None, channel=None, program_title=None, title=None, description=None, thumb=None, duration=None,
|
def __init__(self, uuid=None, nodeid=None, path=None, channel=None, program_title=None, title=None, description=None, thumb=None, duration=None,
|
||||||
season=None, season_uuid=None, number=None, rating=None, aired=None, expiry=None, stream=None, content_type=None):
|
season=None, season_uuid=None, number=None, rating=None, aired=None, expiry=None, stream=None):
|
||||||
"""
|
"""
|
||||||
:type uuid: str
|
:type uuid: str
|
||||||
:type nodeid: str
|
:type nodeid: str
|
||||||
@ -130,7 +127,6 @@ class Episode:
|
|||||||
:type aired: datetime
|
:type aired: datetime
|
||||||
:type expiry: datetime
|
:type expiry: datetime
|
||||||
:type stream: string
|
:type stream: string
|
||||||
:type content_type: string
|
|
||||||
"""
|
"""
|
||||||
self.uuid = uuid
|
self.uuid = uuid
|
||||||
self.nodeid = nodeid
|
self.nodeid = nodeid
|
||||||
@ -148,7 +144,6 @@ class Episode:
|
|||||||
self.aired = aired
|
self.aired = aired
|
||||||
self.expiry = expiry
|
self.expiry = expiry
|
||||||
self.stream = stream
|
self.stream = stream
|
||||||
self.content_type = content_type
|
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return "%r" % self.__dict__
|
return "%r" % self.__dict__
|
||||||
@ -178,6 +173,7 @@ class Category:
|
|||||||
class ContentApi:
|
class ContentApi:
|
||||||
""" GoPlay Content API"""
|
""" GoPlay Content API"""
|
||||||
SITE_URL = 'https://www.goplay.be'
|
SITE_URL = 'https://www.goplay.be'
|
||||||
|
API_VIERVIJFZES = 'https://api.viervijfzes.be'
|
||||||
API_GOPLAY = 'https://api.goplay.be'
|
API_GOPLAY = 'https://api.goplay.be'
|
||||||
|
|
||||||
def __init__(self, auth=None, cache_path=None):
|
def __init__(self, auth=None, cache_path=None):
|
||||||
@ -313,9 +309,9 @@ class ContentApi:
|
|||||||
result = regex_video_data.search(page)
|
result = regex_video_data.search(page)
|
||||||
if result:
|
if result:
|
||||||
video_id = json.loads(unescape(result.group(1)))['id']
|
video_id = json.loads(unescape(result.group(1)))['id']
|
||||||
video_json_data = self._get_url('%s/web/v1/videos/short-form/%s' % (self.API_GOPLAY, video_id))
|
video_json_data = self._get_url('%s/api/video/%s' % (self.SITE_URL, video_id))
|
||||||
video_json = json.loads(video_json_data)
|
video_json = json.loads(video_json_data)
|
||||||
return {'video': video_json}
|
return dict(video=video_json)
|
||||||
|
|
||||||
# Extract program JSON
|
# Extract program JSON
|
||||||
regex_program = re.compile(r'data-hero="([^"]+)', re.DOTALL)
|
regex_program = re.compile(r'data-hero="([^"]+)', re.DOTALL)
|
||||||
@ -331,24 +327,16 @@ class ContentApi:
|
|||||||
episode_json_data = unescape(result.group(1))
|
episode_json_data = unescape(result.group(1))
|
||||||
episode_json = json.loads(episode_json_data)
|
episode_json = json.loads(episode_json_data)
|
||||||
|
|
||||||
return {'program': program_json, 'episode': episode_json}
|
return dict(program=program_json, episode=episode_json)
|
||||||
|
|
||||||
# Fetch listing from cache or update if needed
|
# Fetch listing from cache or update if needed
|
||||||
data = self._handle_cache(key=['episode', path], cache_mode=cache, update=update)
|
data = self._handle_cache(key=['episode', path], cache_mode=cache, update=update)
|
||||||
if not data:
|
if not data:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
if 'episode' in data and data['episode']['pageInfo']['type'] == 'live_channel':
|
|
||||||
episode = Episode(
|
|
||||||
uuid=data['episode']['pageInfo']['nodeUuid'],
|
|
||||||
program_title=data['episode']['pageInfo']['title'],
|
|
||||||
content_type=data['episode']['pageInfo']['type'],
|
|
||||||
)
|
|
||||||
return episode
|
|
||||||
|
|
||||||
if 'video' in data and data['video']:
|
if 'video' in data and data['video']:
|
||||||
# We have found detailed episode information
|
# We have found detailed episode information
|
||||||
episode = self._parse_clip_data(data['video'])
|
episode = self._parse_episode_data(data['video'])
|
||||||
return episode
|
return episode
|
||||||
|
|
||||||
if 'program' in data and 'episode' in data and data['program'] and data['episode']:
|
if 'program' in data and 'episode' in data and data['program'] and data['episode']:
|
||||||
@ -361,70 +349,37 @@ class ContentApi:
|
|||||||
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def get_stream_by_uuid(self, uuid, content_type):
|
def get_stream_by_uuid(self, uuid):
|
||||||
""" Return a ResolvedStream for this video.
|
""" Get the stream URL to use for this video.
|
||||||
:type uuid: string
|
:type uuid: str
|
||||||
:type content_type: string
|
:rtype str
|
||||||
:rtype: ResolvedStream
|
|
||||||
"""
|
"""
|
||||||
if content_type in ('video-long_form', 'long_form'):
|
response = self._get_url(self.API_VIERVIJFZES + '/content/%s' % uuid, authentication=True)
|
||||||
mode = 'videos/long-form'
|
|
||||||
elif content_type == 'video-short_form':
|
|
||||||
mode = 'videos/short-form'
|
|
||||||
elif content_type == 'live_channel':
|
|
||||||
mode = 'liveStreams'
|
|
||||||
response = self._get_url(self.API_GOPLAY + '/web/v1/%s/%s' % (mode, uuid), authentication='Bearer %s' % self._auth.get_token())
|
|
||||||
data = json.loads(response)
|
data = json.loads(response)
|
||||||
|
|
||||||
if not data:
|
if 'videoDash' in data:
|
||||||
raise UnavailableException
|
# DRM protected stream
|
||||||
|
|
||||||
# Get DRM license
|
|
||||||
license_key = None
|
|
||||||
if data.get('drmXml'):
|
|
||||||
# BuyDRM format
|
|
||||||
# See https://docs.unified-streaming.com/documentation/drm/buydrm.html#setting-up-the-client
|
# See https://docs.unified-streaming.com/documentation/drm/buydrm.html#setting-up-the-client
|
||||||
|
drm_key = data['drmKey']['S']
|
||||||
|
|
||||||
# Generate license key
|
_LOGGER.debug('Fetching Authentication XML with drm_key %s', drm_key)
|
||||||
license_key = self.create_license_key('https://wv-keyos.licensekeyserver.com/', key_headers={
|
response_drm = self._get_url(self.API_GOPLAY + '/restricted/decode/%s' % drm_key, authentication=True)
|
||||||
'customdata': data['drmXml']
|
data_drm = json.loads(response_drm)
|
||||||
})
|
|
||||||
|
|
||||||
# Get manifest url
|
|
||||||
if data.get('manifestUrls'):
|
|
||||||
|
|
||||||
if data.get('manifestUrls').get('dash'):
|
|
||||||
# DASH stream
|
|
||||||
return ResolvedStream(
|
|
||||||
uuid=uuid,
|
|
||||||
url=data['manifestUrls']['dash'],
|
|
||||||
stream_type=STREAM_DASH,
|
|
||||||
license_key=license_key,
|
|
||||||
)
|
|
||||||
|
|
||||||
# HLS stream
|
|
||||||
return ResolvedStream(
|
return ResolvedStream(
|
||||||
uuid=uuid,
|
uuid=uuid,
|
||||||
url=data['manifestUrls']['hls'],
|
url=data['videoDash']['S'],
|
||||||
stream_type=STREAM_HLS,
|
|
||||||
license_key=license_key,
|
|
||||||
)
|
|
||||||
|
|
||||||
# No manifest url found, get manifest from Server-Side Ad Insertion service
|
|
||||||
if data.get('adType') == 'SSAI' and data.get('ssai'):
|
|
||||||
url = 'https://pubads.g.doubleclick.net/ondemand/dash/content/%s/vid/%s/streams' % (
|
|
||||||
data.get('ssai').get('contentSourceID'), data.get('ssai').get('videoID'))
|
|
||||||
ad_data = json.loads(self._post_url(url, data=''))
|
|
||||||
|
|
||||||
# Server-Side Ad Insertion DASH stream
|
|
||||||
return ResolvedStream(
|
|
||||||
uuid=uuid,
|
|
||||||
url=ad_data['stream_manifest'],
|
|
||||||
stream_type=STREAM_DASH,
|
stream_type=STREAM_DASH,
|
||||||
license_key=license_key,
|
license_url='https://wv-keyos.licensekeyserver.com/',
|
||||||
|
auth=data_drm.get('auth'),
|
||||||
)
|
)
|
||||||
|
|
||||||
raise UnavailableException
|
# Normal HLS stream
|
||||||
|
return ResolvedStream(
|
||||||
|
uuid=uuid,
|
||||||
|
url=data['video']['S'],
|
||||||
|
stream_type=STREAM_HLS,
|
||||||
|
)
|
||||||
|
|
||||||
def get_program_tree(self, cache=CACHE_AUTO):
|
def get_program_tree(self, cache=CACHE_AUTO):
|
||||||
""" Get a content tree with information about all the programs.
|
""" Get a content tree with information about all the programs.
|
||||||
@ -495,64 +450,34 @@ class ContentApi:
|
|||||||
raw_html = self._get_url(self.SITE_URL)
|
raw_html = self._get_url(self.SITE_URL)
|
||||||
|
|
||||||
# Categories regexes
|
# Categories regexes
|
||||||
regex_articles = re.compile(r'<article[^>]+>([\s\S]*?)</article>', re.DOTALL)
|
regex_articles = re.compile(r'<article[^>]+>(.*?)</article>', re.DOTALL)
|
||||||
regex_category = re.compile(r'<h2.*?>(.*?)</h2>(?:.*?<div class=\"visually-hidden\">(.*?)</div>)?', re.DOTALL)
|
regex_category = re.compile(r'<h1.*?>(.*?)</h1>(?:.*?<div class="visually-hidden">(.*?)</div>)?', re.DOTALL)
|
||||||
|
|
||||||
categories = []
|
categories = []
|
||||||
for result in regex_articles.finditer(raw_html):
|
for result in regex_articles.finditer(raw_html):
|
||||||
article_html = result.group(1)
|
article_html = result.group(1)
|
||||||
|
|
||||||
match_category = regex_category.search(article_html)
|
match_category = regex_category.search(article_html)
|
||||||
category_title = None
|
category_title = match_category.group(1).strip()
|
||||||
if match_category:
|
if match_category.group(2):
|
||||||
category_title = unescape(match_category.group(1).strip())
|
category_title += ' [B]%s[/B]' % match_category.group(2).strip()
|
||||||
if match_category.group(2):
|
|
||||||
category_title += ' [B]%s[/B]' % unescape(match_category.group(2).strip())
|
|
||||||
|
|
||||||
if category_title:
|
# Extract programs and lookup in all_programs so we have more metadata
|
||||||
# Extract programs and lookup in all_programs so we have more metadata
|
programs = []
|
||||||
programs = []
|
for program in self._extract_programs(article_html):
|
||||||
for program in self._extract_programs(article_html):
|
try:
|
||||||
try:
|
rich_program = next(rich_program for rich_program in all_programs if rich_program.path == program.path)
|
||||||
rich_program = next(rich_program for rich_program in all_programs if rich_program.path == program.path)
|
programs.append(rich_program)
|
||||||
programs.append(rich_program)
|
except StopIteration:
|
||||||
except StopIteration:
|
programs.append(program)
|
||||||
programs.append(program)
|
|
||||||
|
|
||||||
episodes = self._extract_videos(article_html)
|
episodes = self._extract_videos(article_html)
|
||||||
|
|
||||||
categories.append(
|
categories.append(
|
||||||
Category(uuid=hashlib.md5(category_title.encode('utf-8')).hexdigest(), title=category_title, programs=programs, episodes=episodes))
|
Category(uuid=hashlib.md5(category_title.encode('utf-8')).hexdigest(), title=category_title, programs=programs, episodes=episodes))
|
||||||
|
|
||||||
return categories
|
return categories
|
||||||
|
|
||||||
def get_mylist(self):
|
|
||||||
""" Get the content of My List
|
|
||||||
:rtype list[Program]
|
|
||||||
"""
|
|
||||||
data = self._get_url(self.API_GOPLAY + '/my-list', authentication='Bearer %s' % self._auth.get_token())
|
|
||||||
result = json.loads(data)
|
|
||||||
|
|
||||||
items = []
|
|
||||||
for item in result:
|
|
||||||
try:
|
|
||||||
program = self.get_program_by_uuid(item.get('programId'))
|
|
||||||
if program:
|
|
||||||
program.my_list = True
|
|
||||||
items.append(program)
|
|
||||||
except Exception as exc: # pylint: disable=broad-except
|
|
||||||
_LOGGER.warning(exc)
|
|
||||||
|
|
||||||
return items
|
|
||||||
|
|
||||||
def mylist_add(self, program_id):
|
|
||||||
""" Add a program on My List """
|
|
||||||
self._post_url(self.API_GOPLAY + '/my-list', data={'programId': program_id}, authentication='Bearer %s' % self._auth.get_token())
|
|
||||||
|
|
||||||
def mylist_del(self, program_id):
|
|
||||||
""" Remove a program on My List """
|
|
||||||
self._delete_url(self.API_GOPLAY + '/my-list-item', params={'programId': program_id}, authentication='Bearer %s' % self._auth.get_token())
|
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _extract_programs(html):
|
def _extract_programs(html):
|
||||||
""" Extract Programs from HTML code
|
""" Extract Programs from HTML code
|
||||||
@ -560,8 +485,8 @@ class ContentApi:
|
|||||||
:rtype list[Program]
|
:rtype list[Program]
|
||||||
"""
|
"""
|
||||||
# Item regexes
|
# Item regexes
|
||||||
regex_item = re.compile(r'<a[^>]+?href=\"(?P<path>[^\"]+)\"[^>]+?>'
|
regex_item = re.compile(r'<a[^>]+?href="(?P<path>[^"]+)"[^>]+?>'
|
||||||
r'[\s\S]*?<h3 class=\"poster-teaser__title\">(?P<title>[^<]*)</h3>[\s\S]*?poster-teaser__image\" src=\"(?P<image>[\s\S]*?)\"[\s\S]*?'
|
r'.*?<h3 class="poster-teaser__title">(?P<title>[^<]*)</h3>.*?data-background-image="(?P<image>.*?)".*?'
|
||||||
r'</a>', re.DOTALL)
|
r'</a>', re.DOTALL)
|
||||||
|
|
||||||
# Extract items
|
# Extract items
|
||||||
@ -587,21 +512,20 @@ class ContentApi:
|
|||||||
:rtype list[Episode]
|
:rtype list[Episode]
|
||||||
"""
|
"""
|
||||||
# Item regexes
|
# Item regexes
|
||||||
regex_item = re.compile(r'<a[^>]+?class=\"(?P<item_type>[^\"]+)\"[^>]+?href=\"(?P<path>[^\"]+)\"[^>]+?>[\s\S]*?</a>', re.DOTALL)
|
regex_item = re.compile(r'<a[^>]+?href="(?P<path>[^"]+)"[^>]+?>.*?</a>', re.DOTALL)
|
||||||
|
|
||||||
regex_episode_program = re.compile(r'<(?:div|h3) class=\"episode-teaser__subtitle\">([^<]*)</(?:div|h3)>')
|
regex_episode_program = re.compile(r'<h3 class="episode-teaser__subtitle">([^<]*)</h3>')
|
||||||
regex_episode_title = re.compile(r'<(?:div|h3) class=\"(?:poster|card|image|episode)-teaser__title\">(?:<span>)?([^<]*)(?:</span>)?</(?:div|h3)>')
|
regex_episode_title = re.compile(r'<(?:div|h3) class="(?:poster|card|image|episode)-teaser__title">(?:<span>)?([^<]*)(?:</span>)?</(?:div|h3)>')
|
||||||
regex_episode_duration = re.compile(r'data-duration=\"([^\"]*)\"')
|
regex_episode_duration = re.compile(r'data-duration="([^"]*)"')
|
||||||
regex_episode_video_id = re.compile(r'data-video-id=\"([^\"]*)\"')
|
regex_episode_video_id = re.compile(r'data-video-id="([^"]*)"')
|
||||||
regex_episode_image = re.compile(r'<img class=\"episode-teaser__header\" src=\"([^<\"]*)\"')
|
regex_episode_image = re.compile(r'data-background-image="([^"]*)"')
|
||||||
regex_episode_badge = re.compile(r'<div class=\"badge (?:poster|card|image|episode)-teaser__badge (?:poster|card|image|episode)-teaser__badge--default\">([^<]*)</div>')
|
regex_episode_badge = re.compile(r'<div class="(?:poster|card|image|episode)-teaser__badge badge">([^<]*)</div>')
|
||||||
|
|
||||||
# Extract items
|
# Extract items
|
||||||
episodes = []
|
episodes = []
|
||||||
for item in regex_item.finditer(html):
|
for item in regex_item.finditer(html):
|
||||||
item_html = item.group(0)
|
item_html = item.group(0)
|
||||||
path = item.group('path')
|
path = item.group('path')
|
||||||
item_type = item.group('item_type')
|
|
||||||
|
|
||||||
# Extract title
|
# Extract title
|
||||||
try:
|
try:
|
||||||
@ -646,8 +570,6 @@ class ContentApi:
|
|||||||
if episode_badge:
|
if episode_badge:
|
||||||
description += "\n\n[B]%s[/B]" % episode_badge
|
description += "\n\n[B]%s[/B]" % episode_badge
|
||||||
|
|
||||||
content_type = 'video-short_form' if 'card-' in item_type else 'video-long_form'
|
|
||||||
|
|
||||||
# Episode
|
# Episode
|
||||||
episodes.append(Episode(
|
episodes.append(Episode(
|
||||||
path=path.lstrip('/'),
|
path=path.lstrip('/'),
|
||||||
@ -658,7 +580,6 @@ class ContentApi:
|
|||||||
uuid=episode_video_id,
|
uuid=episode_video_id,
|
||||||
thumb=episode_image,
|
thumb=episode_image,
|
||||||
program_title=episode_program,
|
program_title=episode_program,
|
||||||
content_type=content_type
|
|
||||||
))
|
))
|
||||||
|
|
||||||
return episodes
|
return episodes
|
||||||
@ -671,35 +592,35 @@ class ContentApi:
|
|||||||
"""
|
"""
|
||||||
# Create Program info
|
# Create Program info
|
||||||
program = Program(
|
program = Program(
|
||||||
uuid=data.get('id'),
|
uuid=data['id'],
|
||||||
path=data.get('link').lstrip('/'),
|
path=data['link'].lstrip('/'),
|
||||||
channel=data.get('pageInfo').get('brand'),
|
channel=data['pageInfo']['brand'],
|
||||||
title=data.get('title'),
|
title=data['title'],
|
||||||
description=html_to_kodi(data.get('description')),
|
description=html_to_kodi(data['description']),
|
||||||
aired=datetime.fromtimestamp(data.get('pageInfo', {}).get('publishDate', 0.0)),
|
aired=datetime.fromtimestamp(data.get('pageInfo', {}).get('publishDate')),
|
||||||
poster=data.get('images').get('poster'),
|
poster=data['images']['poster'],
|
||||||
thumb=data.get('images').get('teaser'),
|
thumb=data['images']['teaser'],
|
||||||
fanart=data.get('images').get('teaser'),
|
fanart=data['images']['hero'],
|
||||||
)
|
)
|
||||||
|
|
||||||
# Create Season info
|
# Create Season info
|
||||||
program.seasons = {
|
program.seasons = {
|
||||||
key: Season(
|
key: Season(
|
||||||
uuid=playlist.get('id'),
|
uuid=playlist['id'],
|
||||||
path=playlist.get('link').lstrip('/'),
|
path=playlist['link'].lstrip('/'),
|
||||||
channel=playlist.get('pageInfo').get('brand'),
|
channel=playlist['pageInfo']['brand'],
|
||||||
title=playlist.get('title'),
|
title=playlist['title'],
|
||||||
description=html_to_kodi(playlist.get('description')),
|
description=html_to_kodi(playlist.get('description')),
|
||||||
number=playlist.get('episodes')[0].get('seasonNumber'), # You did not see this
|
number=playlist['episodes'][0]['seasonNumber'], # You did not see this
|
||||||
)
|
)
|
||||||
for key, playlist in enumerate(data.get('playlists', [])) if playlist.get('episodes')
|
for key, playlist in enumerate(data['playlists']) if playlist['episodes']
|
||||||
}
|
}
|
||||||
|
|
||||||
# Create Episodes info
|
# Create Episodes info
|
||||||
program.episodes = [
|
program.episodes = [
|
||||||
ContentApi._parse_episode_data(episode, playlist.get('id'))
|
ContentApi._parse_episode_data(episode, playlist['id'])
|
||||||
for playlist in data.get('playlists', [])
|
for playlist in data['playlists']
|
||||||
for episode in playlist.get('episodes')
|
for episode in playlist['episodes']
|
||||||
]
|
]
|
||||||
|
|
||||||
return program
|
return program
|
||||||
@ -711,6 +632,7 @@ class ContentApi:
|
|||||||
:type season_uuid: str
|
:type season_uuid: str
|
||||||
:rtype Episode
|
:rtype Episode
|
||||||
"""
|
"""
|
||||||
|
|
||||||
if data.get('episodeNumber'):
|
if data.get('episodeNumber'):
|
||||||
episode_number = data.get('episodeNumber')
|
episode_number = data.get('episodeNumber')
|
||||||
else:
|
else:
|
||||||
@ -734,105 +656,26 @@ class ContentApi:
|
|||||||
season=data.get('seasonNumber'),
|
season=data.get('seasonNumber'),
|
||||||
season_uuid=season_uuid,
|
season_uuid=season_uuid,
|
||||||
number=episode_number,
|
number=episode_number,
|
||||||
aired=datetime.fromtimestamp(int(data.get('createdDate'))),
|
aired=datetime.fromtimestamp(data.get('createdDate')),
|
||||||
expiry=datetime.fromtimestamp(int(data.get('unpublishDate'))) if data.get('unpublishDate') else None,
|
expiry=datetime.fromtimestamp(data.get('unpublishDate')) if data.get('unpublishDate') else None,
|
||||||
rating=data.get('parentalRating'),
|
rating=data.get('parentalRating'),
|
||||||
stream=data.get('path'),
|
stream=data.get('path'),
|
||||||
content_type=data.get('type'),
|
|
||||||
)
|
)
|
||||||
return episode
|
return episode
|
||||||
|
|
||||||
@staticmethod
|
def _get_url(self, url, params=None, authentication=False):
|
||||||
def _parse_clip_data(data):
|
|
||||||
""" Parse the Clip JSON.
|
|
||||||
:type data: dict
|
|
||||||
:rtype Episode
|
|
||||||
"""
|
|
||||||
episode = Episode(
|
|
||||||
uuid=data.get('videoUuid'),
|
|
||||||
program_title=data.get('title'),
|
|
||||||
title=data.get('title'),
|
|
||||||
)
|
|
||||||
return episode
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def create_license_key(key_url, key_type='R', key_headers=None, key_value='', response_value=''):
|
|
||||||
""" Create a license key string that we need for inputstream.adaptive.
|
|
||||||
:type key_url: str
|
|
||||||
:type key_type: str
|
|
||||||
:type key_headers: dict[str, str]
|
|
||||||
:type key_value: str
|
|
||||||
:type response_value: str
|
|
||||||
:rtype str
|
|
||||||
"""
|
|
||||||
try: # Python 3
|
|
||||||
from urllib.parse import quote, urlencode
|
|
||||||
except ImportError: # Python 2
|
|
||||||
from urllib import quote, urlencode
|
|
||||||
|
|
||||||
header = ''
|
|
||||||
if key_headers:
|
|
||||||
header = urlencode(key_headers)
|
|
||||||
|
|
||||||
if key_type in ('A', 'R', 'B'):
|
|
||||||
key_value = key_type + '{SSM}'
|
|
||||||
elif key_type == 'D':
|
|
||||||
if 'D{SSM}' not in key_value:
|
|
||||||
raise ValueError('Missing D{SSM} placeholder')
|
|
||||||
key_value = quote(key_value)
|
|
||||||
|
|
||||||
return '%s|%s|%s|%s' % (key_url, header, key_value, response_value)
|
|
||||||
|
|
||||||
def _get_url(self, url, params=None, authentication=None):
|
|
||||||
""" Makes a GET request for the specified URL.
|
""" Makes a GET request for the specified URL.
|
||||||
:type url: str
|
:type url: str
|
||||||
:type authentication: str
|
|
||||||
:rtype str
|
:rtype str
|
||||||
"""
|
"""
|
||||||
if authentication:
|
if authentication:
|
||||||
|
if not self._auth:
|
||||||
|
raise Exception('Requested to authenticate, but not auth object passed')
|
||||||
response = self._session.get(url, params=params, headers={
|
response = self._session.get(url, params=params, headers={
|
||||||
'authorization': authentication,
|
'authorization': self._auth.get_token(),
|
||||||
}, proxies=PROXIES)
|
})
|
||||||
else:
|
else:
|
||||||
response = self._session.get(url, params=params, proxies=PROXIES)
|
response = self._session.get(url, params=params)
|
||||||
|
|
||||||
if response.status_code != 200:
|
|
||||||
_LOGGER.error(response.text)
|
|
||||||
raise Exception('Could not fetch data')
|
|
||||||
|
|
||||||
return response.text
|
|
||||||
|
|
||||||
def _post_url(self, url, params=None, data=None, authentication=None):
|
|
||||||
""" Makes a POST request for the specified URL.
|
|
||||||
:type url: str
|
|
||||||
:type authentication: str
|
|
||||||
:rtype str
|
|
||||||
"""
|
|
||||||
if authentication:
|
|
||||||
response = self._session.post(url, params=params, json=data, headers={
|
|
||||||
'authorization': authentication,
|
|
||||||
}, proxies=PROXIES)
|
|
||||||
else:
|
|
||||||
response = self._session.post(url, params=params, json=data, proxies=PROXIES)
|
|
||||||
|
|
||||||
if response.status_code not in (200, 201):
|
|
||||||
_LOGGER.error(response.text)
|
|
||||||
raise Exception('Could not fetch data')
|
|
||||||
|
|
||||||
return response.text
|
|
||||||
|
|
||||||
def _delete_url(self, url, params=None, authentication=None):
|
|
||||||
""" Makes a DELETE request for the specified URL.
|
|
||||||
:type url: str
|
|
||||||
:type authentication: str
|
|
||||||
:rtype str
|
|
||||||
"""
|
|
||||||
if authentication:
|
|
||||||
response = self._session.delete(url, params=params, headers={
|
|
||||||
'authorization': authentication,
|
|
||||||
}, proxies=PROXIES)
|
|
||||||
else:
|
|
||||||
response = self._session.delete(url, params=params, proxies=PROXIES)
|
|
||||||
|
|
||||||
if response.status_code != 200:
|
if response.status_code != 200:
|
||||||
_LOGGER.error(response.text)
|
_LOGGER.error(response.text)
|
||||||
|
@ -11,8 +11,6 @@ import dateutil.parser
|
|||||||
import dateutil.tz
|
import dateutil.tz
|
||||||
import requests
|
import requests
|
||||||
|
|
||||||
from resources.lib import kodiutils
|
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
GENRE_MAPPING = {
|
GENRE_MAPPING = {
|
||||||
@ -33,8 +31,6 @@ GENRE_MAPPING = {
|
|||||||
'Voetbal': 0x43,
|
'Voetbal': 0x43,
|
||||||
}
|
}
|
||||||
|
|
||||||
PROXIES = kodiutils.get_proxies()
|
|
||||||
|
|
||||||
|
|
||||||
class EpgProgram:
|
class EpgProgram:
|
||||||
""" Defines a Program in the EPG. """
|
""" Defines a Program in the EPG. """
|
||||||
@ -76,9 +72,7 @@ class EpgApi:
|
|||||||
EPG_ENDPOINTS = {
|
EPG_ENDPOINTS = {
|
||||||
'Play4': 'https://www.goplay.be/api/epg/vier/{date}',
|
'Play4': 'https://www.goplay.be/api/epg/vier/{date}',
|
||||||
'Play5': 'https://www.goplay.be/api/epg/vijf/{date}',
|
'Play5': 'https://www.goplay.be/api/epg/vijf/{date}',
|
||||||
'Play6': 'https://www.goplay.be/api/epg/zes/{date}',
|
'Play6': 'https://www.goplay.be/api/epg/zes/{date}'
|
||||||
'Play7': 'https://www.goplay.be/api/epg/zeven/{date}',
|
|
||||||
'PlayCrime': 'https://www.goplay.be/api/epg/crime/{date}',
|
|
||||||
}
|
}
|
||||||
|
|
||||||
EPG_NO_BROADCAST = 'Geen uitzending'
|
EPG_NO_BROADCAST = 'Geen uitzending'
|
||||||
@ -182,7 +176,7 @@ class EpgApi:
|
|||||||
:type url: str
|
:type url: str
|
||||||
:rtype str
|
:rtype str
|
||||||
"""
|
"""
|
||||||
response = self._session.get(url, proxies=PROXIES)
|
response = self._session.get(url)
|
||||||
|
|
||||||
if response.status_code != 200:
|
if response.status_code != 200:
|
||||||
raise Exception('Could not fetch data')
|
raise Exception('Could not fetch data')
|
||||||
|
@ -9,12 +9,10 @@ import logging
|
|||||||
import requests
|
import requests
|
||||||
|
|
||||||
from resources.lib import kodiutils
|
from resources.lib import kodiutils
|
||||||
from resources.lib.viervijfzes.content import CACHE_ONLY, ContentApi, Program
|
from resources.lib.viervijfzes.content import Program, ContentApi, CACHE_ONLY
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
PROXIES = kodiutils.get_proxies()
|
|
||||||
|
|
||||||
|
|
||||||
class SearchApi:
|
class SearchApi:
|
||||||
""" GoPlay Search API """
|
""" GoPlay Search API """
|
||||||
@ -39,9 +37,9 @@ class SearchApi:
|
|||||||
"query": query,
|
"query": query,
|
||||||
"page": 0,
|
"page": 0,
|
||||||
"mode": "programs"
|
"mode": "programs"
|
||||||
},
|
}
|
||||||
proxies=PROXIES
|
|
||||||
)
|
)
|
||||||
|
_LOGGER.debug(response.content)
|
||||||
response.raise_for_status()
|
response.raise_for_status()
|
||||||
|
|
||||||
data = json.loads(response.text)
|
data = json.loads(response.text)
|
||||||
|
Before Width: | Height: | Size: 41 KiB After Width: | Height: | Size: 15 KiB |
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 4.0 KiB |
Before Width: | Height: | Size: 33 KiB After Width: | Height: | Size: 13 KiB |
Before Width: | Height: | Size: 8.2 KiB After Width: | Height: | Size: 3.3 KiB |
Before Width: | Height: | Size: 48 KiB After Width: | Height: | Size: 18 KiB |
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 4.5 KiB |
Before Width: | Height: | Size: 39 KiB |
Before Width: | Height: | Size: 9.7 KiB |
Before Width: | Height: | Size: 44 KiB |
Before Width: | Height: | Size: 12 KiB |
@ -1,87 +0,0 @@
|
|||||||
#!/usr/bin/env python
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
""" Build ZIP files for all brands. """
|
|
||||||
from __future__ import absolute_import, division, unicode_literals
|
|
||||||
|
|
||||||
import os
|
|
||||||
import shutil
|
|
||||||
import xml.etree.ElementTree as ET
|
|
||||||
|
|
||||||
DIST_DIR = 'dist'
|
|
||||||
|
|
||||||
|
|
||||||
def get_files():
|
|
||||||
""" Get a list of files that we should package. """
|
|
||||||
# Start with all non-hidden files
|
|
||||||
files = [f for f in os.listdir() if not f.startswith('.')]
|
|
||||||
|
|
||||||
# Exclude files from .gitattributes
|
|
||||||
with open('.gitattributes', 'r') as f:
|
|
||||||
for line in f.read().splitlines():
|
|
||||||
filename, mode = line.split(' ')
|
|
||||||
filename = filename.strip('/')
|
|
||||||
if mode == 'export-ignore' and filename in files:
|
|
||||||
files.remove(filename)
|
|
||||||
|
|
||||||
# Exclude files from .gitignore. I know, this won't do matching
|
|
||||||
with open('.gitignore', 'r') as f:
|
|
||||||
for filename in f.read().splitlines():
|
|
||||||
filename = filename.strip('/')
|
|
||||||
if filename in files:
|
|
||||||
files.remove(filename)
|
|
||||||
|
|
||||||
return files
|
|
||||||
|
|
||||||
|
|
||||||
def modify_xml(file, version, news, python=None):
|
|
||||||
""" Modify an addon.xml. """
|
|
||||||
with open(file, 'r+') as f:
|
|
||||||
tree = ET.fromstring(f.read())
|
|
||||||
|
|
||||||
# Update values
|
|
||||||
tree.set('version', version)
|
|
||||||
tree.find("./extension[@point='xbmc.addon.metadata']/news").text = news
|
|
||||||
if python:
|
|
||||||
tree.find("./requires/import[@addon='xbmc.python']").set('version', python)
|
|
||||||
|
|
||||||
# Save file
|
|
||||||
f.seek(0)
|
|
||||||
f.truncate()
|
|
||||||
f.write('<?xml version="1.0" encoding="UTF-8" standalone="yes"?>\n' +
|
|
||||||
ET.tostring(tree, encoding='UTF-8').decode())
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
# Read base addon.xml info
|
|
||||||
with open('addon.xml', 'r') as f:
|
|
||||||
tree = ET.fromstring(f.read())
|
|
||||||
addon_info = {
|
|
||||||
'id': tree.get('id'),
|
|
||||||
'version': tree.get('version'),
|
|
||||||
'news': tree.find("./extension[@point='xbmc.addon.metadata']/news").text
|
|
||||||
}
|
|
||||||
|
|
||||||
# Make sure dist folder exists
|
|
||||||
if not os.path.isdir(DIST_DIR):
|
|
||||||
os.mkdir(DIST_DIR)
|
|
||||||
|
|
||||||
# Build addon
|
|
||||||
brand = addon_info['id']
|
|
||||||
dest = os.path.join(DIST_DIR, brand)
|
|
||||||
if not os.path.isdir(dest):
|
|
||||||
os.mkdir(dest)
|
|
||||||
|
|
||||||
# Copy files from add-on source
|
|
||||||
for f in get_files():
|
|
||||||
if os.path.isfile(f):
|
|
||||||
shutil.copy(f, dest)
|
|
||||||
else:
|
|
||||||
shutil.copytree(f, os.path.join(dest, f), dirs_exist_ok=True)
|
|
||||||
|
|
||||||
# Update addon.xml for matrix and create zip
|
|
||||||
modify_xml(os.path.join(dest, 'addon.xml'), addon_info['version'] + '+matrix.1', addon_info['news'])
|
|
||||||
shutil.make_archive(os.path.join(DIST_DIR, "%s-%s+matrix.1" % (brand, addon_info['version'])), 'zip', DIST_DIR, brand)
|
|
||||||
|
|
||||||
# Modify addon.xml for leia and create zip
|
|
||||||
# modify_xml(os.path.join(dest, 'addon.xml'), addon_info['version'], addon_info['news'], '2.26.0')
|
|
||||||
# shutil.make_archive(os.path.join(DIST_DIR, "%s-%s" % (brand, addon_info['version'])), 'zip', DIST_DIR, brand)
|
|
@ -1,225 +0,0 @@
|
|||||||
#!/usr/bin/env python
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
""" Publish a ZIP file to the Kodi repository. """
|
|
||||||
|
|
||||||
# Based on code from https://github.com/xbmc/kodi-addon-submitter
|
|
||||||
|
|
||||||
from __future__ import absolute_import, division, unicode_literals
|
|
||||||
|
|
||||||
import logging
|
|
||||||
import os
|
|
||||||
import shutil
|
|
||||||
import subprocess
|
|
||||||
import sys
|
|
||||||
import time
|
|
||||||
import xml.etree.ElementTree as ET
|
|
||||||
from pprint import pformat
|
|
||||||
from tempfile import TemporaryDirectory
|
|
||||||
from zipfile import ZipFile
|
|
||||||
|
|
||||||
import requests
|
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
GH_REPO = 'repo-plugins'
|
|
||||||
GH_USERNAME = os.getenv('GH_USERNAME')
|
|
||||||
GH_TOKEN = os.getenv('GH_TOKEN')
|
|
||||||
GH_EMAIL = os.getenv('EMAIL')
|
|
||||||
|
|
||||||
|
|
||||||
def get_addon_info(xml: str):
|
|
||||||
""" Parse the passed addon.xml file and extract some information. """
|
|
||||||
tree = ET.fromstring(xml)
|
|
||||||
return {
|
|
||||||
'id': tree.get('id'),
|
|
||||||
'name': tree.get('name'),
|
|
||||||
'version': tree.get('version'),
|
|
||||||
'description': tree.find("./extension[@point='xbmc.addon.metadata']/description").text,
|
|
||||||
'news': tree.find("./extension[@point='xbmc.addon.metadata']/news").text,
|
|
||||||
'python': tree.find("./requires/import[@addon='xbmc.python']").get('version'),
|
|
||||||
'source': tree.find("./extension[@point='xbmc.addon.metadata']/source").text,
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def user_fork_exists(repo, gh_username, gh_token):
|
|
||||||
""" Check if the user has a fork of the repository on Github. """
|
|
||||||
resp = requests.get(
|
|
||||||
'https://api.github.com/repos/{}/{}'.format(
|
|
||||||
gh_username,
|
|
||||||
repo
|
|
||||||
),
|
|
||||||
headers={'Accept': 'application/vnd.github.v3+json'},
|
|
||||||
params={
|
|
||||||
'type': 'all'
|
|
||||||
},
|
|
||||||
auth=(gh_username, gh_token)
|
|
||||||
)
|
|
||||||
resp_json = resp.json()
|
|
||||||
return resp.ok and resp_json.get('fork')
|
|
||||||
|
|
||||||
|
|
||||||
def create_personal_fork(repo, gh_username, gh_token):
|
|
||||||
"""Create a personal fork for the official repo on GitHub. """
|
|
||||||
resp = requests.post(
|
|
||||||
'https://api.github.com/repos/xbmc/{}/forks'.format(
|
|
||||||
repo
|
|
||||||
),
|
|
||||||
headers={'Accept': 'application/vnd.github.v3+json'},
|
|
||||||
auth=(gh_username, gh_token)
|
|
||||||
)
|
|
||||||
if resp.ok:
|
|
||||||
elapsed_time = 0
|
|
||||||
while elapsed_time < 5 * 60:
|
|
||||||
if not user_fork_exists(repo, gh_username, gh_token):
|
|
||||||
time.sleep(20)
|
|
||||||
elapsed_time += 20
|
|
||||||
else:
|
|
||||||
return
|
|
||||||
raise Exception("Timeout waiting for fork creation exceeded")
|
|
||||||
raise Exception('GitHub API error: {}\n{}'.format(resp.status_code, resp.text))
|
|
||||||
|
|
||||||
|
|
||||||
def shell(*args):
|
|
||||||
""" Execute a shell command. """
|
|
||||||
subprocess.run(args, check=True)
|
|
||||||
|
|
||||||
|
|
||||||
def create_addon_branch(repo, branch, source, addon_info, gh_username, gh_token, gh_email):
|
|
||||||
""" Create and addon branch in your fork of the respective addon repo. """
|
|
||||||
cur_dir = os.getcwd()
|
|
||||||
os.chdir('dist')
|
|
||||||
|
|
||||||
local_branch_name = '{}@{}'.format(addon_info['id'], branch)
|
|
||||||
|
|
||||||
if os.path.isdir(repo):
|
|
||||||
# We already have a checked out repo locally, update this with upstream code
|
|
||||||
os.chdir(repo)
|
|
||||||
shell('git', 'reset', '--hard') # Remove all local changes
|
|
||||||
shell('git', 'remote', 'set-branches', '--add', 'upstream', branch) # Make sure the upstream branch exists
|
|
||||||
shell('git', 'fetch', '-f', 'upstream', branch) # Fetch upstream
|
|
||||||
else:
|
|
||||||
# Clone the upstream repo
|
|
||||||
shell('git', 'clone', '--branch', branch, '--origin', 'upstream', '--single-branch', 'https://github.com/xbmc/{}.git'.format(repo))
|
|
||||||
os.chdir(repo)
|
|
||||||
|
|
||||||
# Create local branch
|
|
||||||
shell('git', 'checkout', '-B', local_branch_name, 'upstream/{}'.format(branch))
|
|
||||||
|
|
||||||
# Remove current code
|
|
||||||
if os.path.isdir(addon_info['id']):
|
|
||||||
shutil.rmtree(addon_info['id'], ignore_errors=False)
|
|
||||||
|
|
||||||
# Add new code
|
|
||||||
shutil.copytree(source, addon_info['id'])
|
|
||||||
shell('git', 'add', '--', addon_info['id'])
|
|
||||||
shell('git', 'status')
|
|
||||||
|
|
||||||
# Create a commit with the new code
|
|
||||||
shell('git', 'config', 'user.name', gh_username)
|
|
||||||
shell('git', 'config', 'user.email', gh_email)
|
|
||||||
shell('git', 'commit', '-m', '[{}] {}'.format(addon_info['id'], addon_info['version']))
|
|
||||||
|
|
||||||
# Push branch to fork
|
|
||||||
shell('git', 'push', '-f', 'https://{}:{}@github.com/{}/{}.git'.format(gh_username, gh_token, gh_username, repo), local_branch_name)
|
|
||||||
|
|
||||||
# Restore working directory
|
|
||||||
os.chdir(cur_dir)
|
|
||||||
|
|
||||||
|
|
||||||
def create_pull_request(repo, branch, addon_info, gh_username, gh_token):
|
|
||||||
""" Create a pull request in the official repo on GitHub. """
|
|
||||||
|
|
||||||
local_branch_name = '{}@{}'.format(addon_info['id'], branch)
|
|
||||||
|
|
||||||
# Check if pull request already exists.
|
|
||||||
resp = requests.get(
|
|
||||||
'https://api.github.com/repos/xbmc/{}/pulls'.format(repo),
|
|
||||||
params={
|
|
||||||
'head': '{}:{}'.format(gh_username, local_branch_name),
|
|
||||||
'base': branch,
|
|
||||||
},
|
|
||||||
headers={'Accept': 'application/vnd.github.v3+json'},
|
|
||||||
auth=(gh_username, gh_token)
|
|
||||||
)
|
|
||||||
|
|
||||||
if resp.status_code == 200 and not resp.json():
|
|
||||||
# Create a new Pull Request
|
|
||||||
template = """### Description
|
|
||||||
|
|
||||||
- **General**
|
|
||||||
- Add-on name: {name}
|
|
||||||
- Add-on ID: {id}
|
|
||||||
- Version number: {version}
|
|
||||||
- Kodi/repository version: {kodi_repo_branch}
|
|
||||||
|
|
||||||
- **Code location**
|
|
||||||
- URL: {source}
|
|
||||||
|
|
||||||
{description}
|
|
||||||
|
|
||||||
### What's new
|
|
||||||
|
|
||||||
{news}
|
|
||||||
|
|
||||||
### Checklist:
|
|
||||||
- [X] My code follows the [add-on rules](http://kodi.wiki/view/Add-on_rules) and [piracy stance](http://kodi.wiki/view/Official:Forum_rules#Piracy_Policy) of this project.
|
|
||||||
- [X] I have read the [CONTRIBUTING](https://github.com/xbmc/repo-plugins/blob/master/CONTRIBUTING.md) document
|
|
||||||
- [X] Each add-on submission should be a single commit with using the following style: [plugin.video.foo] v1.0.0
|
|
||||||
"""
|
|
||||||
pr_body = template.format(
|
|
||||||
name=addon_info['name'],
|
|
||||||
id=addon_info['id'],
|
|
||||||
version=addon_info['version'],
|
|
||||||
kodi_repo_branch=branch,
|
|
||||||
source=addon_info['source'],
|
|
||||||
description=addon_info['description'],
|
|
||||||
news=addon_info['news']
|
|
||||||
)
|
|
||||||
resp = requests.post(
|
|
||||||
'https://api.github.com/repos/xbmc/{}/pulls'.format(repo),
|
|
||||||
json={
|
|
||||||
'title': '[{}] {}'.format(local_branch_name, addon_info['version']),
|
|
||||||
'head': '{}:{}'.format(gh_username, local_branch_name),
|
|
||||||
'base': branch,
|
|
||||||
'body': pr_body,
|
|
||||||
'maintainer_can_modify': True,
|
|
||||||
},
|
|
||||||
headers={'Accept': 'application/vnd.github.v3+json'},
|
|
||||||
auth=(gh_username, gh_token)
|
|
||||||
)
|
|
||||||
if resp.status_code != 201:
|
|
||||||
raise Exception('GitHub API error: {}\n{}'.format(resp.status_code, pformat(resp.json())))
|
|
||||||
|
|
||||||
elif resp.status_code == 200 and resp.json():
|
|
||||||
_LOGGER.info('Pull request in {} for {}:{} already exists.'.format(branch, gh_username, local_branch_name))
|
|
||||||
|
|
||||||
else:
|
|
||||||
raise Exception('Unexpected GitHub error: {}\n{}'.format(resp.status_code, pformat(resp.json())))
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
filenames = sys.argv[1:]
|
|
||||||
|
|
||||||
# Fork the repo if the user does not have a personal repo fork
|
|
||||||
if not user_fork_exists(GH_REPO, GH_USERNAME, GH_TOKEN):
|
|
||||||
create_personal_fork(GH_REPO, GH_USERNAME, GH_TOKEN)
|
|
||||||
|
|
||||||
for filename in filenames:
|
|
||||||
with TemporaryDirectory() as extract_dir:
|
|
||||||
with ZipFile(filename) as z:
|
|
||||||
# Look for addon.xml in zip and load the details
|
|
||||||
xmlfile = next(f.filename for f in z.filelist if f.filename.endswith('addon.xml'))
|
|
||||||
addon_info = get_addon_info(z.read(xmlfile).decode('utf-8'))
|
|
||||||
if addon_info['python'] != '3.0.0':
|
|
||||||
branch = 'leia'
|
|
||||||
else:
|
|
||||||
branch = 'matrix'
|
|
||||||
|
|
||||||
# Extract the ZIP file to the extract_dir
|
|
||||||
z.extractall(extract_dir)
|
|
||||||
|
|
||||||
# Checkout the fork locally and create a branch with our new code from the extract_dir
|
|
||||||
create_addon_branch(GH_REPO, branch, os.path.join(extract_dir, addon_info['id']), addon_info, GH_USERNAME, GH_TOKEN, GH_EMAIL)
|
|
||||||
|
|
||||||
# Create pull request
|
|
||||||
create_pull_request(GH_REPO, branch, addon_info, GH_USERNAME, GH_TOKEN)
|
|
@ -1,40 +0,0 @@
|
|||||||
#!/usr/bin/env python
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
|
|
||||||
# pylint: disable=missing-docstring,no-self-use,wrong-import-order,wrong-import-position,invalid-name
|
|
||||||
|
|
||||||
import sys
|
|
||||||
from glob import glob
|
|
||||||
|
|
||||||
import polib
|
|
||||||
|
|
||||||
original_file = 'resources/language/resource.language.en_gb/strings.po'
|
|
||||||
original = polib.pofile(original_file, wrapwidth=0)
|
|
||||||
|
|
||||||
for translated_file in glob('resources/language/resource.language.*/strings.po'):
|
|
||||||
# Skip original file
|
|
||||||
if translated_file == original_file:
|
|
||||||
continue
|
|
||||||
print('Updating %s...' % translated_file)
|
|
||||||
|
|
||||||
# Load po-files
|
|
||||||
translated = polib.pofile(translated_file, wrapwidth=0)
|
|
||||||
|
|
||||||
for entry in original:
|
|
||||||
# Find a translation
|
|
||||||
translation = translated.find(entry.msgctxt, 'msgctxt')
|
|
||||||
|
|
||||||
if translation and entry.msgid == translation.msgid:
|
|
||||||
entry.msgstr = translation.msgstr
|
|
||||||
|
|
||||||
original.metadata = translated.metadata
|
|
||||||
|
|
||||||
if sys.platform.startswith('win'):
|
|
||||||
# On Windows save the file keeping the Linux return character
|
|
||||||
with open(translated_file, 'wb') as _file:
|
|
||||||
content = str(original).encode('utf-8')
|
|
||||||
content = content.replace(b'\r\n', b'\n')
|
|
||||||
_file.write(content)
|
|
||||||
else:
|
|
||||||
# Save it now over the translation
|
|
||||||
original.save(translated_file)
|
|
@ -47,7 +47,7 @@ class TestApi(unittest.TestCase):
|
|||||||
self.assertIsInstance(programs[0], Program)
|
self.assertIsInstance(programs[0], Program)
|
||||||
|
|
||||||
def test_episodes(self):
|
def test_episodes(self):
|
||||||
for program in ['gentwest', 'zo-man-zo-vrouw']:
|
for program in ['auwch', 'zo-man-zo-vrouw']:
|
||||||
program = self._api.get_program(program, cache=CACHE_PREVENT)
|
program = self._api.get_program(program, cache=CACHE_PREVENT)
|
||||||
self.assertIsInstance(program, Program)
|
self.assertIsInstance(program, Program)
|
||||||
self.assertIsInstance(program.seasons, dict)
|
self.assertIsInstance(program.seasons, dict)
|
||||||
@ -55,7 +55,7 @@ class TestApi(unittest.TestCase):
|
|||||||
self.assertIsInstance(program.episodes[0], Episode)
|
self.assertIsInstance(program.episodes[0], Episode)
|
||||||
|
|
||||||
def test_clips(self):
|
def test_clips(self):
|
||||||
for program in ['de-tafel-van-vier']:
|
for program in ['gert-late-night']:
|
||||||
program = self._api.get_program(program, extract_clips=True, cache=CACHE_PREVENT)
|
program = self._api.get_program(program, extract_clips=True, cache=CACHE_PREVENT)
|
||||||
|
|
||||||
self.assertIsInstance(program.clips, list)
|
self.assertIsInstance(program.clips, list)
|
||||||
@ -66,16 +66,16 @@ class TestApi(unittest.TestCase):
|
|||||||
|
|
||||||
@unittest.skipUnless(kodiutils.get_setting('username') and kodiutils.get_setting('password'), 'Skipping since we have no credentials.')
|
@unittest.skipUnless(kodiutils.get_setting('username') and kodiutils.get_setting('password'), 'Skipping since we have no credentials.')
|
||||||
def test_get_stream(self):
|
def test_get_stream(self):
|
||||||
program = self._api.get_program('gentwest')
|
program = self._api.get_program('auwch')
|
||||||
self.assertIsInstance(program, Program)
|
self.assertIsInstance(program, Program)
|
||||||
|
|
||||||
episode = program.episodes[0]
|
episode = program.episodes[0]
|
||||||
resolved_stream = self._api.get_stream_by_uuid(episode.uuid, episode.islongform)
|
resolved_stream = self._api.get_stream_by_uuid(episode.uuid)
|
||||||
self.assertIsInstance(resolved_stream, ResolvedStream)
|
self.assertIsInstance(resolved_stream, ResolvedStream)
|
||||||
|
|
||||||
@unittest.skipUnless(kodiutils.get_setting('username') and kodiutils.get_setting('password'), 'Skipping since we have no credentials.')
|
@unittest.skipUnless(kodiutils.get_setting('username') and kodiutils.get_setting('password'), 'Skipping since we have no credentials.')
|
||||||
def test_get_drm_stream(self):
|
def test_get_drm_stream(self):
|
||||||
resolved_stream = self._api.get_stream_by_uuid('cc77be47-0256-4254-acbf-28a03fcac423', True) # https://www.goplay.be/video/ncis-los-angeles/ncis-los-angeles-s14/ncis-los-angeles-s14-aflevering-1
|
resolved_stream = self._api.get_stream_by_uuid('01998ce7-b2ad-4524-a786-33d419a29d7b') # CSI 12x22
|
||||||
self.assertIsInstance(resolved_stream, ResolvedStream)
|
self.assertIsInstance(resolved_stream, ResolvedStream)
|
||||||
|
|
||||||
|
|
||||||
|