Compare commits

..

44 Commits

Author SHA1 Message Date
1118468427
Added playcrime channel 2024-05-08 15:39:53 +02:00
97d80f2115
Merge remote-tracking branch 'mediaminister/live' into main 2024-05-08 14:53:23 +02:00
mediaminister
0ddcdb1b0b Add live channels 2023-09-21 15:28:52 +02:00
Michaël Arnauts
e01b6b5c81 Prepare for v0.4.11 2023-07-26 15:18:52 +02:00
Michaël Arnauts
dd1f49b362
Drop testing for Python 2.7 (#127)
* Drop testing for python 2.7
2023-07-21 22:59:27 +02:00
Michaël Arnauts
bc82711886
Add support for proxies (#126)
* Add support for proxies
2023-07-21 22:54:53 +02:00
Michaël Arnauts
48c993a4e7 Fix pylint warnings/errors 2023-07-21 22:28:50 +02:00
mediaminister
c9ae78f83b
Add DRM support for all streams (#121) 2023-07-21 22:16:07 +02:00
Michaël Arnauts
dc446332ab Fix tests 2023-07-21 22:15:07 +02:00
mediaminister
7504d1a6ec
Use inputstreamhelper for unprotected MPEG-DASH (#118) 2023-07-21 22:12:09 +02:00
Michaël Arnauts
9cd8688fee Prepare for v0.4.10 2023-01-16 18:24:39 +01:00
Michaël Arnauts
be42bc6e1c Remove obsolete API_VIERVIJFZES 2023-01-16 18:23:30 +01:00
Michaël Arnauts
7f61533c4a
Update README.md 2023-01-16 18:03:30 +01:00
mediaminister
9cdebb6c7f
Update api (#114) 2023-01-16 17:59:29 +01:00
Michaël Arnauts
89f4b457df Update README.md 2023-01-04 14:22:01 +01:00
Michaël Arnauts
5ce74e4ec3 Update CI pipeline 2023-01-04 14:11:54 +01:00
Michaël Arnauts
f7f1921c2b Prepare for v0.4.9 2023-01-04 14:01:05 +01:00
mediaminister
b7a479e585
Add support for unprotected MPEG-DASH streams (#111) 2022-12-15 11:36:51 +01:00
Michaël Arnauts
98a0ffb162
Support Python 3.10 (#107)
* Support Python 3.10

* Add timeout

* Fix test
2022-10-16 15:36:00 +02:00
Michaël Arnauts
9f4bba446a
Fix clips (#108) 2022-10-16 15:32:56 +02:00
Michaël Arnauts
6d1c08e9c7 Prepare for v0.4.8 2022-07-07 16:00:54 +02:00
Michaël Arnauts
7ac3a21d1a Fix publish.py script 2022-07-07 15:52:36 +02:00
Michaël Arnauts
a26121ee37
Fix API (#105) 2022-07-07 15:51:56 +02:00
Michaël Arnauts
d5a905db5c Prepare for v0.4.7 2022-02-04 21:36:53 +01:00
Michaël Arnauts
02b2d4dbb1
Fix empty My List due to unknown items (#102)
* Ignore unavailable items on My List

* Rework My List API
2022-02-03 18:38:34 +01:00
Michaël Arnauts
21f877fb8d Update LICENSE to latest version 2022-02-02 17:54:07 +01:00
Michaël Arnauts
cfeae82636 Prepare for v0.4.6 2022-02-02 17:52:24 +01:00
Michaël Arnauts
16bb1ceab3
Modify repository so HEAD contains the Matrix version (#100) 2022-02-02 17:49:48 +01:00
Michaël Arnauts
bcb5caceeb
Fix playback of DRM protected content (#101) 2022-02-02 17:45:56 +01:00
Michaël Arnauts
4b24396aa0 Update to actions/setup-python@v2 2022-02-02 17:09:01 +01:00
michaelarnauts
d93de09d2f Prepare for v0.4.5 2021-10-21 15:25:40 +02:00
Michaël Arnauts
8b22d285e7
Remove dependency on inputstream.adaptive (#98) 2021-10-21 15:24:46 +02:00
Michaël Arnauts
13af7437af
Various fixes due to layout changes (#97)
* Various fixes due to layout changes

* Update .pylintrc
2021-10-21 15:19:10 +02:00
mediaminister
e07edfa658
Prepare for v0.4.4 (#95) 2021-09-15 16:39:40 +02:00
mediaminister
3a9848d0f0
Fix tests (#94) 2021-09-15 16:28:47 +02:00
mediaminister
9c44d323e6
Fix menu (#93) 2021-09-15 16:18:53 +02:00
michaelarnauts
f221d0040f Fix test 2021-07-12 20:15:55 +02:00
michaelarnauts
94752d2c6a Update .pylintrc 2021-07-12 20:09:54 +02:00
michaelarnauts
bfe7036999 Add issue templates 2021-07-12 19:46:38 +02:00
michaelarnauts
c5dee12e46 Prepare for v0.4.3 2021-04-24 18:30:45 +02:00
Michaël Arnauts
37121fa6dd
Fetch a week of EPG data for IPTV Manager (#89) 2021-04-24 18:27:26 +02:00
michaelarnauts
9d6eabc43e Update README.md 2021-04-24 18:24:04 +02:00
Michaël Arnauts
0edbad10df
Improve playback error handling (#87) 2021-04-23 23:27:55 +02:00
Michaël Arnauts
d5551f4e9e
Add support for Play7 (#86) 2021-04-06 18:07:15 +02:00
49 changed files with 1145 additions and 565 deletions

View File

@ -4,3 +4,11 @@ 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
View File

@ -6,3 +6,4 @@ 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 Normal file
View File

@ -0,0 +1,36 @@
---
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.

View File

@ -0,0 +1,20 @@
---
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.

View File

@ -1,4 +1,4 @@
name: Kodi Addon-Check name: Kodi
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,18 +11,12 @@ 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: Install dependencies - name: Run kodi-addon-checker
run: | uses: xbmc/action-kodi-addon-checker@v1.2
sudo apt-get install libxml2-utils with:
sudo python -m pip install kodi-addon-checker kodi-version: matrix
addon-id: ${{ github.event.repository.name }}
- name: Run Addon Check for Kodi ${{matrix.kodi-version}}
run: make check-addon-${{matrix.kodi-version}}

View File

@ -14,24 +14,22 @@ jobs:
strategy: strategy:
fail-fast: false fail-fast: false
matrix: matrix:
os: [ ubuntu-latest ] os: [ ubuntu-latest, windows-latest ]
python-version: [ 2.7, 3.5, 3.6, 3.7, 3.8, 3.9 ] python-version: ["3.8", "3.9", "3.10"]
include: include:
# Kodi Leia on Windows uses a bundled Python 2.7. # End-of-life Python versions are not available anymore with ubuntu-latest
- os: windows-latest - os: ubuntu-20.04
python-version: 2.7 python-version: "3.5"
- os: ubuntu-20.04
# Kodi Matrix on Windows uses a bundled Python 3.8, but we test 3.9 also to be sure. python-version: "3.6"
- os: windows-latest - os: ubuntu-20.04
python-version: 3.8 python-version: "3.7"
- 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@v2 uses: actions/checkout@v3
- name: Set up Python ${{ matrix.python-version }} - name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v1 uses: actions/setup-python@v4
with: with:
python-version: ${{ matrix.python-version }} python-version: ${{ matrix.python-version }}
@ -54,10 +52,10 @@ 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 -v --cov=./ --cov-report=xml tests run: pytest -x -v --cov=./ --cov-report=xml tests
- name: Upload code coverage to CodeCov - name: Upload code coverage to CodeCov
uses: codecov/codecov-action@v1 uses: codecov/codecov-action@v3
continue-on-error: true continue-on-error: true
env: env:
OS: ${{ matrix.os }} OS: ${{ matrix.os }}

View File

@ -6,24 +6,13 @@ 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: Checkout code - name: Check out ${{ github.sha }} from repository ${{ github.repository }}
uses: actions/checkout@v2 uses: actions/checkout@v2
- name: Install dependencies - name: Get changelog
run: sudo apt-get install libxml2-utils id: get-changelog
- 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
@ -32,47 +21,14 @@ jobs:
description="${description//$'\r'/'%0D'}" description="${description//$'\r'/'%0D'}"
echo ::set-output name=body::$description echo ::set-output name=body::$description
- name: Create Release - name: Generate distribution zips
id: create_release run: scripts/build.py
uses: actions/create-release@v1
env: - name: Create Release on Github
GITHUB_TOKEN: ${{ secrets.GH_TOKEN }} uses: softprops/action-gh-release@v1
with: with:
tag_name: ${{ github.ref }} body: ${{ steps.get-changelog.outputs.body }}
release_name: ${{ github.ref }}
body: ${{ steps.get-body.outputs.body }}
draft: false draft: false
prerelease: false prerelease: false
files: "dist/*.zip"
- name: Upload Leia zip token: ${{ secrets.GH_TOKEN }}
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 }}

1
.gitignore vendored
View File

@ -18,4 +18,5 @@ tests/home/userdata/addon_data
Pipfile Pipfile
Pipfile.lock Pipfile.lock
dist/ dist/

View File

@ -1,16 +1,25 @@
[MESSAGES CONTROL] [MESSAGES CONTROL]
disable= disable=
bad-option-value, bad-option-value,
cyclic-import, # This shoud be fixed cyclic-import, # This should 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

View File

@ -1,5 +1,94 @@
# 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)
@ -27,11 +116,6 @@
- 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)
@ -40,12 +124,6 @@
- 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)
@ -54,10 +132,6 @@
- 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)
@ -77,11 +151,6 @@
- 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)
@ -106,11 +175,6 @@
- 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)
@ -126,11 +190,7 @@
**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
View File

@ -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. <http://fsf.org/> Copyright (C) 2007 Free Software Foundation, Inc. <https://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 <http://www.gnu.org/licenses/>. along with this program. If not, see <https://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:
{project} Copyright (C) {year} {fullname} <program> Copyright (C) <year> <name of author>
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
<http://www.gnu.org/licenses/>. <https://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
<http://www.gnu.org/philosophy/why-not-lgpl.html>. <https://www.gnu.org/licenses/why-not-lgpl.html>.

View File

@ -2,20 +2,11 @@ export KODI_HOME := $(CURDIR)/tests/home
export KODI_INTERACTIVE := 0 export KODI_INTERACTIVE := 0
PYTHON := python PYTHON := python
# Collect information to build as sensible package name languages = $(filter-out en_gb, $(patsubst resources/language/resource.language.%, %, $(wildcard resources/language/*)))
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-all multizip: build
check: check-pylint check-translations check: check-pylint check-translations
@ -25,20 +16,17 @@ check-pylint:
check-translations: check-translations:
@printf ">>> Running translation checks\n" @printf ">>> Running translation checks\n"
@$(foreach lang,$(filter-out en_gb, $(patsubst resources/language/resource.language.%, %, $(wildcard resources/language/*))), \ @$(foreach lang,$(languages), \
msgcmp resources/language/resource.language.$(lang)/strings.po resources/language/resource.language.en_gb/strings.po; \ msgcmp --use-untranslated resources/language/resource.language.$(lang)/strings.po resources/language/resource.language.en_gb/strings.po; \
) )
@tests/check_for_unused_translations.py @scripts/check_for_unused_translations.py
check-addon: check-addon-matrix check-addon: build
check-addon-leia:
@printf ">>> Running addon checks\n" @printf ">>> Running addon checks\n"
@make -s build-leia && cd dist/ && kodi-addon-checker --branch=leia $(eval TMPDIR := $(shell mktemp -d))
@unzip dist/plugin.video.viervijfzes-*+matrix.1.zip -d ${TMPDIR}
check-addon-matrix: cd ${TMPDIR} && kodi-addon-checker --branch=matrix
@printf ">>> Running addon checks\n" @rm -rf ${TMPDIR}
@make -s build-matrix && cd dist/ && kodi-addon-checker --branch=matrix
codefix: codefix:
@isort -l 160 resources/ @isort -l 160 resources/
@ -57,32 +45,14 @@ clean:
@rm -f *.log .coverage @rm -f *.log .coverage
@rm -rf dist/ @rm -rf dist/
build: build-matrix build: clean
@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),)
@github_changelog_generator -u add-ons -p $(name) --no-issues --exclude-labels duplicate,question,invalid,wontfix release --future-release v$(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)
@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'); \
@ -95,3 +65,5 @@ 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

View File

@ -1,21 +1,20 @@
[![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) [![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)
[![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) [![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)
[![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. *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
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 en Play6 * Bekijk on-demand content van Play4, Play5, Play6 en Play7
* 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
@ -34,4 +33,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 en Play6 zijn eigendom van SBS België. De logo's van GoPlay, Play4, Play5, Play6 en Play7 zijn eigendom van SBS België.

View File

@ -1,13 +1,12 @@
<?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.2" provider-name="Michaël Arnauts"> <addon id="plugin.video.viervijfzes" name="GoPlay" version="0.4.12" 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>
@ -22,9 +21,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.2 (2021-03-22) <news>v0.4.12 (2024-05-08)
- Improve descriptions and images. - Added live channels (by mediaminister)
- Fix playback of (incorrectly) cached content.</news> - Added PlayCrime Channel</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>

View File

@ -4,7 +4,9 @@ pytest
pytest-cov pytest-cov
pytest-timeout pytest-timeout
python-dateutil python-dateutil
pysocks
requests requests
git+git://github.com/dagwieers/kodi-plugin-routing.git@setup#egg=routing git+https://github.com/tamland/kodi-plugin-routing@master#egg=routing
six six
sakee sakee
win-inet-pton; platform_system=="Windows"

View File

@ -60,6 +60,10 @@ 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 ""
@ -136,18 +140,10 @@ 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 ""

View File

@ -61,6 +61,10 @@ 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]"
@ -137,18 +141,10 @@ 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."

View File

@ -159,11 +159,12 @@ def play_epg(channel, timestamp):
TvGuide().play_epg_datetime(channel, timestamp) TvGuide().play_epg_datetime(channel, timestamp)
@routing.route('/play/catalog/<uuid>') @routing.route('/play/catalog')
def play_catalog(uuid): @routing.route('/play/catalog/<uuid>/<content_type>')
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) Player().play(uuid, content_type)
@routing.route('/play/page/<page>') @routing.route('/play/page/<page>')

View File

@ -17,19 +17,20 @@ 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 = dict( SORT_METHODS = {
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'
] ]
@ -259,12 +260,17 @@ 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')
if license_key is not None:
import inputstreamhelper import inputstreamhelper
if license_key is not None:
# DRM protected MPEG-DASH
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)
@ -464,13 +470,74 @@ 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=dict(setting=key)) result = jsonrpc(method='Settings.GetSettingValue', params={'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=dict(setting=key, value=value)) return jsonrpc(method='Settings.SetSettingValue', params={'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):

View File

@ -4,9 +4,6 @@
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
@ -113,7 +110,7 @@ class Catalog:
}, },
info_dict={ info_dict={
'tvshowtitle': program.title, 'tvshowtitle': program.title,
'title': kodiutils.localize(30205, season=season.number), # Season {season} 'title': kodiutils.localize(30205, season=season.number) if season.number else season.title, # Season {season}
'plot': season.description or program.description, 'plot': season.description or program.description,
'set': program.title, 'set': program.title,
} }
@ -243,6 +240,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)
@ -252,22 +250,10 @@ 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 all the programs of all channels """ """ Show the programs of My List """
try: mylist = self._api.get_mylist()
mylist, _ = self._auth.get_dataset('myList', 'myList')
except Exception as ex:
kodiutils.notification(message=str(ex))
raise
items = [] listing = [Menu.generate_titleitem(item) for item in mylist]
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.
@ -279,23 +265,7 @@ class Catalog:
kodiutils.end_of_directory() kodiutils.end_of_directory()
return return
mylist, sync_info = self._auth.get_dataset('myList', 'myList') self._api.mylist_add(uuid)
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()
@ -305,12 +275,6 @@ class Catalog:
kodiutils.end_of_directory() kodiutils.end_of_directory()
return return
mylist, sync_info = self._auth.get_dataset('myList', 'myList') self._api.mylist_del(uuid)
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()

View File

@ -71,9 +71,27 @@ 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(

View File

@ -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 timedelta from datetime import datetime, 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(dict( streams.append({
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 dict(version=1, streams=streams) return {'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,30 +64,35 @@ class IPTVManager:
except ImportError: # Python 2 except ImportError: # Python 2
from urllib import quote from urllib import quote
results = dict() today = datetime.today()
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']:
epg = epg_api.get_epg(key, date) for i in range(-3, 7):
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 dict(version=1, epg=results) return {'version': 1, 'epg': results}

View File

@ -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=dict( art_dict={
icon='DefaultMovieTitle.png', 'icon': 'DefaultMovieTitle.png',
fanart=kodiutils.get_addon_info('fanart'), 'fanart': kodiutils.get_addon_info('fanart')
), },
info_dict=dict( info_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=dict( art_dict={
icon='DefaultAddonPVRClient.png', 'icon': 'DefaultAddonPVRClient.png',
fanart=kodiutils.get_addon_info('fanart'), 'fanart': kodiutils.get_addon_info('fanart')
), },
info_dict=dict( info_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=dict( art_dict={
icon='DefaultGenre.png', 'icon': 'DefaultGenre.png',
fanart=kodiutils.get_addon_info('fanart'), 'fanart': kodiutils.get_addon_info('fanart')
), },
info_dict=dict( info_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=dict( art_dict={
icon='DefaultFavourites.png', 'icon': 'DefaultFavourites.png',
fanart=kodiutils.get_addon_info('fanart'), 'fanart': kodiutils.get_addon_info('fanart')
), },
info_dict=dict( info_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=dict( art_dict={
icon='DefaultPlaylist.png', 'icon': 'DefaultPlaylist.png',
fanart=kodiutils.get_addon_info('fanart'), 'fanart': kodiutils.get_addon_info('fanart')
), },
info_dict=dict( info_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=dict( art_dict={
icon='DefaultAddonsSearch.png', 'icon': 'DefaultAddonsSearch.png',
fanart=kodiutils.get_addon_info('fanart'), 'fanart': kodiutils.get_addon_info('fanart')
), },
info_dict=dict( info_dict={
plot=kodiutils.localize(30010), 'plot': kodiutils.localize(30010)
) }
) )
] ]
@ -128,13 +128,6 @@ class Menu:
} }
visible = True visible = True
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 title = item.title
context_menu = [] context_menu = []
@ -190,7 +183,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) path = kodiutils.url_for('play_catalog', uuid=item.uuid, content_type=item.content_type)
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=''))

View File

@ -12,11 +12,6 @@ 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__)
@ -31,19 +26,29 @@ 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)
@staticmethod def live(self, channel):
def live(channel):
""" Play the live channel. """ Play the live channel.
:type channel: string :type channel: string
""" """
channel_name = CHANNELS.get(channel, dict(name=channel)) # TODO: this doesn't work correctly, playing a live program from the PVR won't play something from the beginning
kodiutils.ok_dialog(message=kodiutils.localize(30718, channel=channel_name.get('name'))) # There is no live stream available for {channel}. # Lookup current program
kodiutils.end_of_directory() # broadcast = self._epg.get_broadcast(channel, datetime.datetime.now().isoformat())
# 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
@ -63,47 +68,36 @@ class Player:
if episode.uuid: if episode.uuid:
# Lookup the stream # Lookup the stream
resolved_stream = self._resolve_stream(episode.uuid) resolved_stream = self._resolve_stream(episode.uuid, episode.content_type)
_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,
license_key, resolved_stream.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): def play(self, uuid, content_type):
""" Play the requested item. """ Play the requested item.
:type uuid: string :type uuid: string
:type content_type: string
""" """
# Lookup the stream if not uuid:
resolved_stream = self._resolve_stream(uuid) kodiutils.ok_dialog(message=kodiutils.localize(30712)) # The video is unavailable...
if resolved_stream.license_url: return
# 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) # Lookup the stream
resolved_stream = self._resolve_stream(uuid, content_type)
kodiutils.play(resolved_stream.url, resolved_stream.stream_type, resolved_stream.license_key)
@staticmethod @staticmethod
def _resolve_stream(uuid): def _resolve_stream(uuid, content_type):
""" 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
@ -120,7 +114,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) resolved_stream = ContentApi(auth).get_stream_by_uuid(uuid, content_type)
return resolved_stream return resolved_stream
except (InvalidLoginException, AuthenticationException) as ex: except (InvalidLoginException, AuthenticationException) as ex:
@ -130,32 +124,9 @@ class Player:
return None return None
except GeoblockedException: except GeoblockedException:
kodiutils.ok_dialog(heading=kodiutils.localize(30709), message=kodiutils.localize(30710)) # This video is geo-blocked... kodiutils.ok_dialog(message=kodiutils.localize(30710)) # This video is geo-blocked...
return None return None
except UnavailableException: except UnavailableException:
kodiutils.ok_dialog(heading=kodiutils.localize(30711), message=kodiutils.localize(30712)) # The video is unavailable... kodiutils.ok_dialog(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)

View File

@ -36,8 +36,8 @@ class TvGuide:
dates = [] dates = []
today = datetime.today() today = datetime.today()
# The API provides 7 days in the past and 13 days in the future # The API provides 7 days in the past and 8 days in the future
for i in range(-7, 13): for i in range(-7, 8):
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 = None path = kodiutils.url_for('play_catalog', uuid='')
title = '[COLOR gray]' + title + '[/COLOR]' title = '[COLOR gray]' + title + '[/COLOR]'
stream_dict = STREAM_DICT.copy() stream_dict = STREAM_DICT.copy()

View File

@ -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) response = requests.get(self.stream_path, timeout=5)
if response.status_code == 403: if response.status_code == 403:
message_id = 30720 message_id = 30720
else: else:

View File

@ -5,68 +5,69 @@ from __future__ import absolute_import, division, unicode_literals
from collections import OrderedDict from collections import OrderedDict
CHANNELS = OrderedDict([ CHANNELS = OrderedDict([
('Play4', dict( ('Play4', {
name='Play4', 'name': 'Play4',
epg_id='vier', 'url': 'live-kijken/play-4',
logo='play4.png', 'epg_id': 'vier',
background='play4-background.png', 'logo': 'play4.png',
iptv_preset=4, 'background': 'play4-background.png',
iptv_id='play4.be', 'iptv_preset': 4,
youtube=[ 'iptv_id': 'play4.be',
dict( 'youtube': [
label='GoPlay', {'label': 'GoPlay', 'logo': 'goplay.png', 'path': 'plugin://plugin.video.youtube/user/viertv/'},
logo='goplay.png', ]
path='plugin://plugin.video.youtube/user/viertv/', }),
), ('Play5', {
], 'name': 'Play5',
)), 'url': 'live-kijken/play-5',
('Play5', dict( 'epg_id': 'vijf',
name='Play5', 'logo': 'play5.png',
epg_id='vijf', 'background': 'play5-background.png',
logo='play5.png', 'iptv_preset': 5,
background='play5-background.png', 'iptv_id': 'play5.be',
iptv_preset=5, 'youtube': [
iptv_id='play5.be', {'label': 'GoPlay', 'logo': 'goplay.png', 'path': 'plugin://plugin.video.youtube/user/viertv/'},
youtube=[ ]
dict( }),
label='GoPlay', ('Play6', {
logo='goplay.png', 'name': 'Play6',
path='plugin://plugin.video.youtube/user/viertv/', 'url': 'live-kijken/play-6',
), 'epg_id': 'zes',
], 'logo': 'play6.png',
)), 'background': 'play6-background.png',
('Play6', dict( 'iptv_preset': 6,
name='Play6', 'iptv_id': 'play6.be',
epg_id='zes', 'youtube': [
logo='play6.png', {'label': 'GoPlay', 'logo': 'goplay.png', 'path': 'plugin://plugin.video.youtube/user/viertv/'},
background='play6-background.png', ]
iptv_preset=6, }),
iptv_id='play6.be', ('Play7', {
youtube=[ 'name': 'Play7',
dict( 'url': 'live-kijken/play-7',
label='GoPlay', 'epg_id': 'zeven',
logo='goplay.png', 'logo': 'play7.png',
path='plugin://plugin.video.youtube/user/viertv/', 'background': 'play7-background.png',
), 'iptv_preset': 17,
], 'iptv_id': 'play7.be',
)), 'youtube': []
# ('Play7', dict( }),
# name='Play7', ('PlayCrime', {
# epg_id='zeven', 'name': 'PlayCrime',
# url='https://www.goplay.be', 'url': 'live-kijken/play-crime',
# logo='play7.png', 'epg_id': 'crime',
# background='play7-background.png', 'logo': 'playcrime.png',
# iptv_preset=7, 'background': 'playcrime-background.png',
# iptv_id='play7.be', 'iptv_preset': 18,
# youtube=[], 'iptv_id': 'playcrime.be',
# )), 'youtube': []
('GoPlay', dict( }),
name='Go Play', ('GoPlay', {
url='https://www.goplay.be', 'name': 'Go Play',
logo='goplay.png', 'url': 'https://www.goplay.be',
background='goplay-background.png', 'logo': 'goplay.png',
youtube=[], 'background': 'goplay-background.png',
)) 'youtube': []
})
]) ])
STREAM_DICT = { STREAM_DICT = {
@ -79,19 +80,17 @@ 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_url=None, auth=None): def __init__(self, uuid=None, url=None, stream_type=None, license_key=None):
""" """
:type uuid: str :type uuid: str
:type url: str :type url: str
:type stream_type: str :type stream_type: str
:type license_url: str :type license_key: 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_url = license_url self.license_key = license_key
self.auth = auth
def __repr__(self): def __repr__(self):
return "%r" % self.__dict__ return "%r" % self.__dict__

View File

@ -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(dict( data = json.dumps({
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

View File

@ -44,7 +44,6 @@ 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)
@ -64,7 +63,6 @@ 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)

View File

@ -77,7 +77,6 @@ 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. """

View File

@ -15,6 +15,7 @@ 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__)

View File

@ -13,7 +13,8 @@ from datetime import datetime
import requests import requests
from resources.lib.kodiutils import html_to_kodi, STREAM_DASH, STREAM_HLS from resources.lib import kodiutils
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
@ -29,6 +30,8 @@ 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. """
@ -109,7 +112,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): season=None, season_uuid=None, number=None, rating=None, aired=None, expiry=None, stream=None, content_type=None):
""" """
:type uuid: str :type uuid: str
:type nodeid: str :type nodeid: str
@ -127,6 +130,7 @@ 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
@ -144,6 +148,7 @@ 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__
@ -173,7 +178,6 @@ 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):
@ -309,9 +313,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/api/video/%s' % (self.SITE_URL, video_id)) video_json_data = self._get_url('%s/web/v1/videos/short-form/%s' % (self.API_GOPLAY, video_id))
video_json = json.loads(video_json_data) video_json = json.loads(video_json_data)
return dict(video=video_json) return {'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)
@ -327,16 +331,24 @@ 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 dict(program=program_json, episode=episode_json) return {'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_episode_data(data['video']) episode = self._parse_clip_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']:
@ -349,38 +361,71 @@ class ContentApi:
return None return None
def get_stream_by_uuid(self, uuid): def get_stream_by_uuid(self, uuid, content_type):
""" Get the stream URL to use for this video. """ Return a ResolvedStream for this video.
:type uuid: str :type uuid: string
:rtype str :type content_type: string
:rtype: ResolvedStream
""" """
response = self._get_url(self.API_VIERVIJFZES + '/content/%s' % uuid, authentication=True) if content_type in ('video-long_form', 'long_form'):
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 'videoDash' in data: if not data:
# DRM protected stream raise UnavailableException
# 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']
_LOGGER.debug('Fetching Authentication XML with drm_key %s', drm_key) # Generate license key
response_drm = self._get_url(self.API_GOPLAY + '/restricted/decode/%s' % drm_key, authentication=True) license_key = self.create_license_key('https://wv-keyos.licensekeyserver.com/', key_headers={
data_drm = json.loads(response_drm) 'customdata': data['drmXml']
})
# Get manifest url
if data.get('manifestUrls'):
if data.get('manifestUrls').get('dash'):
# DASH stream
return ResolvedStream( return ResolvedStream(
uuid=uuid, uuid=uuid,
url=data['videoDash']['S'], url=data['manifestUrls']['dash'],
stream_type=STREAM_DASH, stream_type=STREAM_DASH,
license_url='https://wv-keyos.licensekeyserver.com/', license_key=license_key,
auth=data_drm.get('auth'),
) )
# Normal HLS stream # HLS stream
return ResolvedStream( return ResolvedStream(
uuid=uuid, uuid=uuid,
url=data['video']['S'], url=data['manifestUrls']['hls'],
stream_type=STREAM_HLS, 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,
license_key=license_key,
)
raise UnavailableException
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.
:type cache: str :type cache: str
@ -450,18 +495,21 @@ 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[^>]+>(.*?)</article>', re.DOTALL) regex_articles = re.compile(r'<article[^>]+>([\s\S]*?)</article>', re.DOTALL)
regex_category = re.compile(r'<h1.*?>(.*?)</h1>(?:.*?<div class="visually-hidden">(.*?)</div>)?', re.DOTALL) regex_category = re.compile(r'<h2.*?>(.*?)</h2>(?:.*?<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 = match_category.group(1).strip() category_title = None
if match_category:
category_title = unescape(match_category.group(1).strip())
if match_category.group(2): if match_category.group(2):
category_title += ' [B]%s[/B]' % match_category.group(2).strip() 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):
@ -478,6 +526,33 @@ class ContentApi:
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
@ -485,8 +560,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'.*?<h3 class="poster-teaser__title">(?P<title>[^<]*)</h3>.*?data-background-image="(?P<image>.*?)".*?' r'[\s\S]*?<h3 class=\"poster-teaser__title\">(?P<title>[^<]*)</h3>[\s\S]*?poster-teaser__image\" src=\"(?P<image>[\s\S]*?)\"[\s\S]*?'
r'</a>', re.DOTALL) r'</a>', re.DOTALL)
# Extract items # Extract items
@ -512,20 +587,21 @@ class ContentApi:
:rtype list[Episode] :rtype list[Episode]
""" """
# Item regexes # Item regexes
regex_item = re.compile(r'<a[^>]+?href="(?P<path>[^"]+)"[^>]+?>.*?</a>', re.DOTALL) regex_item = re.compile(r'<a[^>]+?class=\"(?P<item_type>[^\"]+)\"[^>]+?href=\"(?P<path>[^\"]+)\"[^>]+?>[\s\S]*?</a>', re.DOTALL)
regex_episode_program = re.compile(r'<h3 class="episode-teaser__subtitle">([^<]*)</h3>') regex_episode_program = re.compile(r'<(?:div|h3) class=\"episode-teaser__subtitle\">([^<]*)</(?:div|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'data-background-image="([^"]*)"') regex_episode_image = re.compile(r'<img class=\"episode-teaser__header\" src=\"([^<\"]*)\"')
regex_episode_badge = re.compile(r'<div class="(?:poster|card|image|episode)-teaser__badge badge">([^<]*)</div>') regex_episode_badge = re.compile(r'<div class=\"badge (?:poster|card|image|episode)-teaser__badge (?:poster|card|image|episode)-teaser__badge--default\">([^<]*)</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:
@ -570,6 +646,8 @@ 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('/'),
@ -580,6 +658,7 @@ 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
@ -592,35 +671,35 @@ class ContentApi:
""" """
# Create Program info # Create Program info
program = Program( program = Program(
uuid=data['id'], uuid=data.get('id'),
path=data['link'].lstrip('/'), path=data.get('link').lstrip('/'),
channel=data['pageInfo']['brand'], channel=data.get('pageInfo').get('brand'),
title=data['title'], title=data.get('title'),
description=html_to_kodi(data['description']), description=html_to_kodi(data.get('description')),
aired=datetime.fromtimestamp(data.get('pageInfo', {}).get('publishDate')), aired=datetime.fromtimestamp(data.get('pageInfo', {}).get('publishDate', 0.0)),
poster=data['images']['poster'], poster=data.get('images').get('poster'),
thumb=data['images']['teaser'], thumb=data.get('images').get('teaser'),
fanart=data['images']['hero'], fanart=data.get('images').get('teaser'),
) )
# Create Season info # Create Season info
program.seasons = { program.seasons = {
key: Season( key: Season(
uuid=playlist['id'], uuid=playlist.get('id'),
path=playlist['link'].lstrip('/'), path=playlist.get('link').lstrip('/'),
channel=playlist['pageInfo']['brand'], channel=playlist.get('pageInfo').get('brand'),
title=playlist['title'], title=playlist.get('title'),
description=html_to_kodi(playlist.get('description')), description=html_to_kodi(playlist.get('description')),
number=playlist['episodes'][0]['seasonNumber'], # You did not see this number=playlist.get('episodes')[0].get('seasonNumber'), # You did not see this
) )
for key, playlist in enumerate(data['playlists']) if playlist['episodes'] for key, playlist in enumerate(data.get('playlists', [])) if playlist.get('episodes')
} }
# Create Episodes info # Create Episodes info
program.episodes = [ program.episodes = [
ContentApi._parse_episode_data(episode, playlist['id']) ContentApi._parse_episode_data(episode, playlist.get('id'))
for playlist in data['playlists'] for playlist in data.get('playlists', [])
for episode in playlist['episodes'] for episode in playlist.get('episodes')
] ]
return program return program
@ -632,7 +711,6 @@ 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:
@ -656,26 +734,105 @@ 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(data.get('createdDate')), aired=datetime.fromtimestamp(int(data.get('createdDate'))),
expiry=datetime.fromtimestamp(data.get('unpublishDate')) if data.get('unpublishDate') else None, expiry=datetime.fromtimestamp(int(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
def _get_url(self, url, params=None, authentication=False): @staticmethod
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': self._auth.get_token(), 'authorization': authentication,
}) }, proxies=PROXIES)
else: else:
response = self._session.get(url, params=params) response = self._session.get(url, params=params, proxies=PROXIES)
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)

View File

@ -11,6 +11,8 @@ 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 = {
@ -31,6 +33,8 @@ 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. """
@ -72,7 +76,9 @@ 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'
@ -176,7 +182,7 @@ class EpgApi:
:type url: str :type url: str
:rtype str :rtype str
""" """
response = self._session.get(url) response = self._session.get(url, proxies=PROXIES)
if response.status_code != 200: if response.status_code != 200:
raise Exception('Could not fetch data') raise Exception('Could not fetch data')

View File

@ -9,10 +9,12 @@ import logging
import requests import requests
from resources.lib import kodiutils from resources.lib import kodiutils
from resources.lib.viervijfzes.content import Program, ContentApi, CACHE_ONLY from resources.lib.viervijfzes.content import CACHE_ONLY, ContentApi, Program
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
PROXIES = kodiutils.get_proxies()
class SearchApi: class SearchApi:
""" GoPlay Search API """ """ GoPlay Search API """
@ -37,9 +39,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)

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.0 KiB

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.3 KiB

After

Width:  |  Height:  |  Size: 8.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.5 KiB

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

BIN
resources/logos/play7.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

87
scripts/build.py Executable file
View File

@ -0,0 +1,87 @@
#!/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)

225
scripts/publish.py Executable file
View File

@ -0,0 +1,225 @@
#!/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)

40
scripts/update_translations.py Executable file
View File

@ -0,0 +1,40 @@
#!/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)

View 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 ['auwch', 'zo-man-zo-vrouw']: for program in ['gentwest', '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 ['gert-late-night']: for program in ['de-tafel-van-vier']:
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('auwch') program = self._api.get_program('gentwest')
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) resolved_stream = self._api.get_stream_by_uuid(episode.uuid, episode.islongform)
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('01998ce7-b2ad-4524-a786-33d419a29d7b') # CSI 12x22 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
self.assertIsInstance(resolved_stream, ResolvedStream) self.assertIsInstance(resolved_stream, ResolvedStream)