From e07edfa658f1b2d68efc5a52b284e9aebeea1b22 Mon Sep 17 00:00:00 2001 From: mediaminister <45148099+mediaminister@users.noreply.github.com> Date: Wed, 15 Sep 2021 16:39:40 +0200 Subject: [PATCH 01/32] Prepare for v0.4.4 (#95) --- CHANGELOG.md | 8 ++++++++ addon.xml | 8 +++----- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fc681c4..a28ecb4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,13 @@ # Changelog +## [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 catalogue and recommendations 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) diff --git a/addon.xml b/addon.xml index 5a5286a..85e4f80 100644 --- a/addon.xml +++ b/addon.xml @@ -1,5 +1,5 @@ - + @@ -22,10 +22,8 @@ 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. all GPL-3.0-only - v0.4.3 (2021-04-24) -- Add support for Play7. -- Improve error handling. -- Pass a week of EPG data to IPTV Manager. + v0.4.4 (2021-09-15) +- Fix catalogue and recommendations menu https://github.com/add-ons/plugin.video.viervijfzes resources/icon.png From 13af7437af2b5555f6e547dcb428abcb9ee014a6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C3=ABl=20Arnauts?= Date: Thu, 21 Oct 2021 15:19:10 +0200 Subject: [PATCH 02/32] Various fixes due to layout changes (#97) * Various fixes due to layout changes * Update .pylintrc --- .pylintrc | 1 + resources/lib/modules/catalog.py | 5 +++-- resources/lib/viervijfzes/content.py | 2 +- tests/test_api.py | 2 +- 4 files changed, 6 insertions(+), 4 deletions(-) diff --git a/.pylintrc b/.pylintrc index 59f9258..248d282 100644 --- a/.pylintrc +++ b/.pylintrc @@ -21,3 +21,4 @@ disable= super-with-arguments, # Python 2.7 compatibility raise-missing-from, # Python 2.7 compatibility + consider-using-f-string, # Python 2.7 compatibility diff --git a/resources/lib/modules/catalog.py b/resources/lib/modules/catalog.py index 66ca7da..46bb5f4 100644 --- a/resources/lib/modules/catalog.py +++ b/resources/lib/modules/catalog.py @@ -113,7 +113,7 @@ class Catalog: }, info_dict={ '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, 'set': program.title, } @@ -243,7 +243,8 @@ class Catalog: listing = [] for episode in episodes: title_item = Menu.generate_titleitem(episode) - title_item.info_dict['title'] = episode.program_title + ' - ' + title_item.title + if episode.program_title: + title_item.info_dict['title'] = episode.program_title + ' - ' + title_item.title listing.append(title_item) for program in programs: diff --git a/resources/lib/viervijfzes/content.py b/resources/lib/viervijfzes/content.py index 246a304..23c4d9b 100644 --- a/resources/lib/viervijfzes/content.py +++ b/resources/lib/viervijfzes/content.py @@ -454,7 +454,7 @@ class ContentApi: # Categories regexes regex_articles = re.compile(r']+>(.*?)', re.DOTALL) - regex_category = re.compile(r'(.*?)(?:.*?
(.*?)
)?', re.DOTALL) + regex_category = re.compile(r'(.*?)(?:.*?
(.*?)
)?', re.DOTALL) categories = [] for result in regex_articles.finditer(raw_html): diff --git a/tests/test_api.py b/tests/test_api.py index 6561b61..381b790 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -75,7 +75,7 @@ class TestApi(unittest.TestCase): @unittest.skipUnless(kodiutils.get_setting('username') and kodiutils.get_setting('password'), 'Skipping since we have no credentials.') def test_get_drm_stream(self): - resolved_stream = self._api.get_stream_by_uuid('c6c67b09-8068-4121-8030-8708735f1429') # NCIS 18x10 + resolved_stream = self._api.get_stream_by_uuid('2a17b793-7407-42b2-9851-5487db6d3a6a') # NCIS LA 10x6 self.assertIsInstance(resolved_stream, ResolvedStream) From 8b22d285e7c5baa56ce88b7c775501f804640538 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C3=ABl=20Arnauts?= Date: Thu, 21 Oct 2021 15:24:46 +0200 Subject: [PATCH 03/32] Remove dependency on inputstream.adaptive (#98) --- addon.xml | 1 - 1 file changed, 1 deletion(-) diff --git a/addon.xml b/addon.xml index 85e4f80..4ac598a 100644 --- a/addon.xml +++ b/addon.xml @@ -7,7 +7,6 @@ -
video From d93de09d2fa958e00cccdaff71f9c132f0e3210c Mon Sep 17 00:00:00 2001 From: michaelarnauts Date: Thu, 21 Oct 2021 15:25:40 +0200 Subject: [PATCH 04/32] Prepare for v0.4.5 --- CHANGELOG.md | 16 +++++++++++++++- addon.xml | 6 +++--- 2 files changed, 18 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a28ecb4..0cb0541 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,12 +1,26 @@ # Changelog +## [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 catalogue and recommendations menu [\#93](https://github.com/add-ons/plugin.video.viervijfzes/pull/93) ([mediaminister](https://github.com/mediaminister)) +- Fix tests [\#94](https://github.com/add-ons/plugin.video.viervijfzes/pull/94) ([mediaminister](https://github.com/mediaminister)) +- Fix menu [\#93](https://github.com/add-ons/plugin.video.viervijfzes/pull/93) ([mediaminister](https://github.com/mediaminister)) + +**Merged pull requests:** + +- Prepare for v0.4.4 [\#95](https://github.com/add-ons/plugin.video.viervijfzes/pull/95) ([mediaminister](https://github.com/mediaminister)) ## [v0.4.3](https://github.com/add-ons/plugin.video.viervijfzes/tree/v0.4.3) (2021-04-24) diff --git a/addon.xml b/addon.xml index 4ac598a..543d179 100644 --- a/addon.xml +++ b/addon.xml @@ -1,5 +1,5 @@ - + @@ -21,8 +21,8 @@ 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. all GPL-3.0-only - v0.4.4 (2021-09-15) -- Fix catalogue and recommendations menu + v0.4.5 (2021-10-21) +- Various fixes due to changes on the GoPlay website. https://github.com/add-ons/plugin.video.viervijfzes resources/icon.png From 4b24396aa01c8fb2babe4cadb3c2abb71d754f5f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C3=ABl=20Arnauts?= Date: Wed, 2 Feb 2022 17:09:01 +0100 Subject: [PATCH 05/32] Update to actions/setup-python@v2 --- .github/workflows/ci.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index bf2c873..b0218c1 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -31,7 +31,7 @@ jobs: uses: actions/checkout@v2 - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v1 + uses: actions/setup-python@v2 with: python-version: ${{ matrix.python-version }} @@ -64,4 +64,4 @@ jobs: PYTHON: ${{ matrix.python-version }} with: flags: unittests - env_vars: OS,PYTHON \ No newline at end of file + env_vars: OS,PYTHON From bcb5caceeb48b0d31039960b559430f75c10c0ef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C3=ABl=20Arnauts?= Date: Wed, 2 Feb 2022 17:45:56 +0100 Subject: [PATCH 06/32] Fix playback of DRM protected content (#101) --- resources/lib/viervijfzes/content.py | 2 +- tests/test_api.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/resources/lib/viervijfzes/content.py b/resources/lib/viervijfzes/content.py index 23c4d9b..34dc9ce 100644 --- a/resources/lib/viervijfzes/content.py +++ b/resources/lib/viervijfzes/content.py @@ -366,7 +366,7 @@ class ContentApi: drm_key = data['drmKey']['S'] _LOGGER.debug('Fetching Authentication XML with drm_key %s', drm_key) - response_drm = self._get_url(self.API_GOPLAY + '/restricted/decode/%s' % drm_key, authentication=True) + response_drm = self._get_url(self.API_GOPLAY + '/video/xml/%s' % drm_key, authentication=True) data_drm = json.loads(response_drm) return ResolvedStream( diff --git a/tests/test_api.py b/tests/test_api.py index 381b790..90a8679 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -75,7 +75,7 @@ class TestApi(unittest.TestCase): @unittest.skipUnless(kodiutils.get_setting('username') and kodiutils.get_setting('password'), 'Skipping since we have no credentials.') def test_get_drm_stream(self): - resolved_stream = self._api.get_stream_by_uuid('2a17b793-7407-42b2-9851-5487db6d3a6a') # NCIS LA 10x6 + resolved_stream = self._api.get_stream_by_uuid('32ac67c6-9b15-4e3e-9440-6de04e586a6e') # NCIS 12x8 self.assertIsInstance(resolved_stream, ResolvedStream) From 16bb1ceab33917df83382893724f7ed14047097b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C3=ABl=20Arnauts?= Date: Wed, 2 Feb 2022 17:49:48 +0100 Subject: [PATCH 07/32] Modify repository so HEAD contains the Matrix version (#100) --- .env.example | 8 + .gitattributes | 1 + .github/workflows/addon-check.yml | 7 +- .github/workflows/ci.yml | 6 +- .github/workflows/release.yml | 64 +---- .gitignore | 2 + Makefile | 45 +--- addon.xml | 2 +- resources/lib/modules/iptvmanager.py | 2 +- resources/lib/viervijfzes/aws/cognito_sync.py | 1 + resources/lib/viervijfzes/content.py | 2 +- resources/lib/viervijfzes/search.py | 2 +- scripts/build.py | 89 +++++++ .../check_for_unused_translations.py | 0 scripts/publish.py | 225 ++++++++++++++++++ scripts/update_translations.py | 40 ++++ 16 files changed, 397 insertions(+), 99 deletions(-) create mode 100755 scripts/build.py rename {tests => scripts}/check_for_unused_translations.py (100%) create mode 100755 scripts/publish.py create mode 100755 scripts/update_translations.py diff --git a/.env.example b/.env.example index 2ab758e..1ec142b 100644 --- a/.env.example +++ b/.env.example @@ -4,3 +4,11 @@ ADDON_PASSWORD= KODI_HOME=tests/home KODI_INTERACTIVE=0 KODI_STUB_VERBOSE=1 +KODI_STUB_RPC_RESPONSES=tests/rpc + +#HTTP_PROXY= +#HTTPS_PROXY= + +GH_USERNAME= +GH_TOKEN= +EMAIL= \ No newline at end of file diff --git a/.gitattributes b/.gitattributes index 9897fc2..b989aec 100644 --- a/.gitattributes +++ b/.gitattributes @@ -6,3 +6,4 @@ tests/ export-ignore .pylintrc export-ignore Makefile export-ignore requirements.txt export-ignore +scripts/ export-ignore diff --git a/.github/workflows/addon-check.yml b/.github/workflows/addon-check.yml index aa15f01..1531b3a 100644 --- a/.github/workflows/addon-check.yml +++ b/.github/workflows/addon-check.yml @@ -11,10 +11,6 @@ jobs: kodi-addon-checker: name: Addon checker runs-on: ubuntu-latest - strategy: - fail-fast: false - matrix: - kodi-version: [ leia, matrix ] steps: - name: Check out ${{ github.sha }} from repository ${{ github.repository }} uses: actions/checkout@v2 @@ -22,6 +18,5 @@ jobs: - name: Run kodi-addon-checker uses: xbmc/action-kodi-addon-checker@v1.2 with: - kodi-version: ${{ matrix.kodi-version }} - rewrite-for-matrix: ${{ matrix.kodi-version == 'matrix' }} + kodi-version: matrix addon-id: ${{ github.event.repository.name }} \ No newline at end of file diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b0218c1..3d0c3ca 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -3,10 +3,10 @@ on: # Run action when pushed to master, or for commits in a pull request. push: branches: - - master + - master pull_request: branches: - - master + - master jobs: tests: name: Add-on testing @@ -54,7 +54,7 @@ jobs: KODI_INTERACTIVE: 0 KODI_STUB_RPC_RESPONSES: ${{ github.workspace }}/tests/rpc 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 uses: codecov/codecov-action@v1 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index c3085af..7548edc 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -6,22 +6,13 @@ on: jobs: build: name: Release plugin.video.viervijfzes - if: startsWith(github.ref, 'refs/tags/') # prevent from running if it's not a tag runs-on: ubuntu-latest steps: - - name: Checkout code + - name: Check out ${{ github.sha }} from repository ${{ github.repository }} uses: actions/checkout@v2 - - name: Build zip files - id: build - run: | - sudo apt-get install libxml2-utils - make multizip 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 + - name: Get changelog + id: get-changelog run: | description=$(sed '1,6d;/^## /,$d' CHANGELOG.md) echo $description @@ -30,47 +21,14 @@ jobs: description="${description//$'\r'/'%0D'}" echo ::set-output name=body::$description - - name: Create Release - id: create_release - uses: actions/create-release@v1 - env: - GITHUB_TOKEN: ${{ secrets.GH_TOKEN }} + - name: Generate distribution zips + run: scripts/build.py + + - name: Create Release on Github + uses: softprops/action-gh-release@v1 with: - tag_name: ${{ github.ref }} - release_name: ${{ github.ref }} - body: ${{ steps.get-body.outputs.body }} + body: ${{ steps.get-changelog.outputs.body }} draft: false prerelease: false - - - name: Upload Leia zip - uses: actions/upload-release-asset@v1 - env: - GITHUB_TOKEN: ${{ secrets.GH_TOKEN }} - with: - upload_url: ${{ steps.create_release.outputs.upload_url }} - asset_name: ${{ steps.build.outputs.leia-filename }} - asset_path: ../${{ steps.build.outputs.leia-filename }} - asset_content_type: application/zip - - - name: Upload Matrix zip - uses: actions/upload-release-asset@v1 - env: - GITHUB_TOKEN: ${{ secrets.GH_TOKEN }} - with: - upload_url: ${{ steps.create_release.outputs.upload_url }} - asset_name: ${{ steps.build.outputs.matrix-filename }} - asset_path: ../${{ steps.build.outputs.matrix-filename }} - asset_content_type: application/zip - - - name: Generate distribution zip and submit to official kodi repository - id: kodi-addon-submitter - uses: xbmc/action-kodi-addon-submitter@v1.2 - with: - kodi-repository: repo-plugins - addon-id: plugin.video.viervijfzes - kodi-version: leia - kodi-matrix: true - env: - GH_USERNAME: ${{ secrets.GH_USERNAME }} - GH_TOKEN: ${{ secrets.GH_TOKEN }} - EMAIL: ${{ secrets.EMAIL }} + files: "dist/*.zip" + token: ${{ secrets.GH_TOKEN }} diff --git a/.gitignore b/.gitignore index bad9394..e94e306 100644 --- a/.gitignore +++ b/.gitignore @@ -18,3 +18,5 @@ tests/home/userdata/addon_data Pipfile Pipfile.lock + +dist/ diff --git a/Makefile b/Makefile index 8f0d3e5..becb558 100644 --- a/Makefile +++ b/Makefile @@ -1,25 +1,12 @@ export KODI_HOME := $(CURDIR)/tests/home export KODI_INTERACTIVE := 0 PYTHON := python -KODI_PYTHON_ABIS := 3.0.0 2.26.0 - -# Collect information to build as sensible package name -name = $(shell xmllint --xpath 'string(/addon/@id)' addon.xml) -version = $(shell xmllint --xpath 'string(/addon/@version)' addon.xml) -git_branch = $(shell git rev-parse --abbrev-ref HEAD) -git_hash = $(shell git rev-parse --short HEAD) - -ifdef release - zip_name = $(name)-$(version).zip -else - zip_name = $(name)-$(version)-$(git_branch)-$(git_hash).zip -endif -zip_dir = $(name)/ languages = $(filter-out en_gb, $(patsubst resources/language/resource.language.%, %, $(wildcard resources/language/*))) all: check test build zip: build +multizip: build check: check-pylint check-translations @@ -30,15 +17,15 @@ check-pylint: check-translations: @printf ">>> Running translation checks\n" @$(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: clean build +check-addon: build @printf ">>> Running addon checks\n" $(eval TMPDIR := $(shell mktemp -d)) - @unzip ../${zip_name} -d ${TMPDIR} - cd ${TMPDIR} && kodi-addon-checker --branch=leia + @unzip dist/plugin.video.viervijfzes-*+matrix.1.zip -d ${TMPDIR} + cd ${TMPDIR} && kodi-addon-checker --branch=matrix @rm -rf ${TMPDIR} codefix: @@ -56,17 +43,16 @@ clean: @find . -name '__pycache__' -type d -delete @rm -rf .pytest_cache/ tests/cdm tests/userdata/temp @rm -f *.log .coverage + @rm -rf dist/ build: clean - @printf ">>> Building package\n" - @rm -f ../$(zip_name) - @git archive --format zip --worktree-attributes -v -o ../$(zip_name) --prefix $(zip_dir) $(or $(shell git stash create), HEAD) - @printf ">>> Successfully wrote package as: ../$(zip_name)\n" + @printf ">>> Building add-on\n" + @scripts/build.py + @ls -lah dist/*.zip -# You first need to run sudo gem install github_changelog_generator for this 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; \ date=$(shell date '+%Y-%m-%d'); \ @@ -80,11 +66,4 @@ else @printf "Usage: make release release=1.0.0\n" endif -multizip: clean - @-$(foreach abi,$(KODI_PYTHON_ABIS), \ - printf "cd /addon/requires/import[@addon='xbmc.python']/@version\nset $(abi)\nsave\nbye\n" | xmllint --shell addon.xml; \ - matrix=$(findstring $(abi), $(word 1,$(KODI_PYTHON_ABIS))); \ - if [ $$matrix ]; then version=$(version)+matrix.1; else version=$(version); fi; \ - printf "cd /addon/@version\nset $$version\nsave\nbye\n" | xmllint --shell addon.xml; \ - make build; \ - ) +.PHONY: check codefix test clean build release diff --git a/addon.xml b/addon.xml index 543d179..9010b47 100644 --- a/addon.xml +++ b/addon.xml @@ -1,7 +1,7 @@ - + diff --git a/resources/lib/modules/iptvmanager.py b/resources/lib/modules/iptvmanager.py index 5ed7ffb..a5e7f54 100644 --- a/resources/lib/modules/iptvmanager.py +++ b/resources/lib/modules/iptvmanager.py @@ -4,7 +4,7 @@ from __future__ import absolute_import, division, unicode_literals import logging -from datetime import timedelta, datetime +from datetime import datetime, timedelta from resources.lib import kodiutils from resources.lib.viervijfzes import CHANNELS diff --git a/resources/lib/viervijfzes/aws/cognito_sync.py b/resources/lib/viervijfzes/aws/cognito_sync.py index b39d20f..df1be82 100644 --- a/resources/lib/viervijfzes/aws/cognito_sync.py +++ b/resources/lib/viervijfzes/aws/cognito_sync.py @@ -15,6 +15,7 @@ try: # Python 3 from urllib.parse import quote, urlparse except ImportError: # Python 2 from urllib import quote + from urlparse import urlparse _LOGGER = logging.getLogger(__name__) diff --git a/resources/lib/viervijfzes/content.py b/resources/lib/viervijfzes/content.py index 34dc9ce..e4e12be 100644 --- a/resources/lib/viervijfzes/content.py +++ b/resources/lib/viervijfzes/content.py @@ -13,7 +13,7 @@ from datetime import datetime import requests -from resources.lib.kodiutils import html_to_kodi, STREAM_DASH, STREAM_HLS +from resources.lib.kodiutils import STREAM_DASH, STREAM_HLS, html_to_kodi from resources.lib.viervijfzes import ResolvedStream try: # Python 3 diff --git a/resources/lib/viervijfzes/search.py b/resources/lib/viervijfzes/search.py index 7a26237..15f401a 100644 --- a/resources/lib/viervijfzes/search.py +++ b/resources/lib/viervijfzes/search.py @@ -9,7 +9,7 @@ import logging import requests 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__) diff --git a/scripts/build.py b/scripts/build.py new file mode 100755 index 0000000..a465f7a --- /dev/null +++ b/scripts/build.py @@ -0,0 +1,89 @@ +#!/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 sys +import xml.etree.ElementTree as ET + +BRANDS_DIR = 'brands' +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('\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) diff --git a/tests/check_for_unused_translations.py b/scripts/check_for_unused_translations.py similarity index 100% rename from tests/check_for_unused_translations.py rename to scripts/check_for_unused_translations.py diff --git a/scripts/publish.py b/scripts/publish.py new file mode 100755 index 0000000..f408e0c --- /dev/null +++ b/scripts/publish.py @@ -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', 'git://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:] + + for filename in filenames: + # 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) + + 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) diff --git a/scripts/update_translations.py b/scripts/update_translations.py new file mode 100755 index 0000000..60a9cce --- /dev/null +++ b/scripts/update_translations.py @@ -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) From cfeae8263665c5183dce55df631ef64f2629c195 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C3=ABl=20Arnauts?= Date: Wed, 2 Feb 2022 17:52:24 +0100 Subject: [PATCH 08/32] Prepare for v0.4.6 --- CHANGELOG.md | 42 ++++++++---------------------------------- addon.xml | 6 +++--- 2 files changed, 11 insertions(+), 37 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0cb0541..9ae9666 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,13 @@ # Changelog +## [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) @@ -15,13 +23,8 @@ **Fixed bugs:** -- Fix tests [\#94](https://github.com/add-ons/plugin.video.viervijfzes/pull/94) ([mediaminister](https://github.com/mediaminister)) - Fix menu [\#93](https://github.com/add-ons/plugin.video.viervijfzes/pull/93) ([mediaminister](https://github.com/mediaminister)) -**Merged pull requests:** - -- Prepare for v0.4.4 [\#95](https://github.com/add-ons/plugin.video.viervijfzes/pull/95) ([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) @@ -59,11 +62,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)) -**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) [Full Changelog](https://github.com/add-ons/plugin.video.viervijfzes/compare/v0.3.1...v0.4.0) @@ -72,12 +70,6 @@ - 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) [Full Changelog](https://github.com/add-ons/plugin.video.viervijfzes/compare/v0.3.0...v0.3.1) @@ -86,10 +78,6 @@ - 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) [Full Changelog](https://github.com/add-ons/plugin.video.viervijfzes/compare/v0.2.0...v0.3.0) @@ -109,11 +97,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)) - 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) [Full Changelog](https://github.com/add-ons/plugin.video.viervijfzes/compare/v0.1.0...v0.2.0) @@ -138,11 +121,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 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) [Full Changelog](https://github.com/add-ons/plugin.video.viervijfzes/compare/89f55f70b017d0add645d1e1d88f0ce8192d11c4...v0.1.0) @@ -158,11 +136,7 @@ **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)) -- 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)) diff --git a/addon.xml b/addon.xml index 9010b47..1e0f9a5 100644 --- a/addon.xml +++ b/addon.xml @@ -1,5 +1,5 @@ - + @@ -21,8 +21,8 @@ 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. all GPL-3.0-only - v0.4.5 (2021-10-21) -- Various fixes due to changes on the GoPlay website. + v0.4.6 (2022-02-02) +- Fix playback of DRM protected content. https://github.com/add-ons/plugin.video.viervijfzes resources/icon.png From 21f877fb8d822aea093a75d14cb5385e8b8e5d38 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C3=ABl=20Arnauts?= Date: Wed, 2 Feb 2022 17:54:07 +0100 Subject: [PATCH 09/32] Update LICENSE to latest version --- LICENSE | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/LICENSE b/LICENSE index 9cecc1d..f288702 100644 --- a/LICENSE +++ b/LICENSE @@ -1,7 +1,7 @@ GNU GENERAL PUBLIC LICENSE Version 3, 29 June 2007 - Copyright (C) 2007 Free Software Foundation, Inc. + Copyright (C) 2007 Free Software Foundation, Inc. Everyone is permitted to copy and distribute verbatim copies 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 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.} - Copyright (C) {year} {name of author} + + Copyright (C) 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 @@ -645,14 +645,14 @@ the "copyright" line and a pointer to where the full notice is found. GNU General Public License for more details. You should have received a copy of the GNU General Public License - along with this program. If not, see . + along with this program. If not, see . Also add information on how to contact you by electronic and paper mail. If the program does terminal interaction, make it output a short notice like this when it starts in an interactive mode: - {project} Copyright (C) {year} {fullname} + Copyright (C) This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. This is free software, and you are welcome to redistribute it 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, 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 -. +. The GNU General Public License does not permit incorporating your program into proprietary programs. If your program is a subroutine library, you 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 Public License instead of this License. But first, please read -. +. From 02b2d4dbb17b6f4c8ab93bdb4bbba2e411c3f8e6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C3=ABl=20Arnauts?= Date: Thu, 3 Feb 2022 18:38:34 +0100 Subject: [PATCH 10/32] Fix empty My List due to unknown items (#102) * Ignore unavailable items on My List * Rework My List API --- resources/lib/modules/catalog.py | 47 ++--------------- resources/lib/viervijfzes/content.py | 76 +++++++++++++++++++++++++--- 2 files changed, 75 insertions(+), 48 deletions(-) diff --git a/resources/lib/modules/catalog.py b/resources/lib/modules/catalog.py index 46bb5f4..92a0515 100644 --- a/resources/lib/modules/catalog.py +++ b/resources/lib/modules/catalog.py @@ -4,9 +4,6 @@ from __future__ import absolute_import, division, unicode_literals import logging -from datetime import datetime - -import dateutil.tz from resources.lib import kodiutils from resources.lib.kodiutils import TitleItem @@ -253,22 +250,10 @@ class Catalog: kodiutils.show_listing(listing, 30005, content='tvshows') def show_mylist(self): - """ Show all the programs of all channels """ - try: - mylist, _ = self._auth.get_dataset('myList', 'myList') - except Exception as ex: - kodiutils.notification(message=str(ex)) - raise + """ Show the programs of My List """ + mylist = self._api.get_mylist() - items = [] - if mylist: - for item in mylist: - program = self._api.get_program_by_uuid(item.get('id')) - if program: - program.my_list = True - items.append(program) - - listing = [Menu.generate_titleitem(item) for item in items] + listing = [Menu.generate_titleitem(item) for item in mylist] # Sort items by title # Used for A-Z listing or when movies and episodes are mixed. @@ -280,23 +265,7 @@ class Catalog: kodiutils.end_of_directory() return - mylist, sync_info = self._auth.get_dataset('myList', 'myList') - - if not mylist: - mylist = [] - - if uuid not in [item.get('id') for item in mylist]: - # Python 2.7 doesn't support .timestamp(), and windows doesn't do '%s', so we need to calculate it ourself - epoch = datetime(1970, 1, 1, tzinfo=dateutil.tz.gettz('UTC')) - now = datetime.now(tz=dateutil.tz.gettz('UTC')) - timestamp = int((now - epoch).total_seconds()) * 1000 - - mylist.append({ - 'id': uuid, - 'timestamp': timestamp, - }) - - self._auth.put_dataset('myList', 'myList', mylist, sync_info) + self._api.mylist_add(uuid) kodiutils.end_of_directory() @@ -306,12 +275,6 @@ class Catalog: kodiutils.end_of_directory() return - mylist, sync_info = self._auth.get_dataset('myList', 'myList') - - if not mylist: - mylist = [] - - new_mylist = [item for item in mylist if item.get('id') != uuid] - self._auth.put_dataset('myList', 'myList', new_mylist, sync_info) + self._api.mylist_del(uuid) kodiutils.end_of_directory() diff --git a/resources/lib/viervijfzes/content.py b/resources/lib/viervijfzes/content.py index e4e12be..225b14c 100644 --- a/resources/lib/viervijfzes/content.py +++ b/resources/lib/viervijfzes/content.py @@ -354,7 +354,7 @@ class ContentApi: :type uuid: str :rtype str """ - response = self._get_url(self.API_VIERVIJFZES + '/content/%s' % uuid, authentication=True) + response = self._get_url(self.API_VIERVIJFZES + '/content/%s' % uuid, authentication=self._auth.get_token()) data = json.loads(response) if not data: @@ -366,7 +366,7 @@ class ContentApi: drm_key = data['drmKey']['S'] _LOGGER.debug('Fetching Authentication XML with drm_key %s', drm_key) - response_drm = self._get_url(self.API_GOPLAY + '/video/xml/%s' % drm_key, authentication=True) + response_drm = self._get_url(self.API_GOPLAY + '/video/xml/%s' % drm_key, authentication=self._auth.get_token()) data_drm = json.loads(response_drm) return ResolvedStream( @@ -484,6 +484,33 @@ class ContentApi: 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 def _extract_programs(html): """ Extract Programs from HTML code @@ -669,16 +696,15 @@ class ContentApi: ) return episode - def _get_url(self, url, params=None, authentication=False): + def _get_url(self, url, params=None, authentication=None): """ Makes a GET request for the specified URL. :type url: str + :type authentication: str :rtype str """ if authentication: - if not self._auth: - raise Exception('Requested to authenticate, but not auth object passed') response = self._session.get(url, params=params, headers={ - 'authorization': self._auth.get_token(), + 'authorization': authentication, }) else: response = self._session.get(url, params=params) @@ -689,6 +715,44 @@ class ContentApi: 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, + }) + else: + response = self._session.post(url, params=params, json=data) + + if response.status_code != 200: + _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, + }) + else: + response = self._session.delete(url, params=params) + + if response.status_code != 200: + _LOGGER.error(response.text) + raise Exception('Could not fetch data') + + return response.text + def _handle_cache(self, key, cache_mode, update, ttl=30 * 24 * 60 * 60): """ Fetch something from the cache, and update if needed """ if cache_mode in [CACHE_AUTO, CACHE_ONLY]: From d5a905db5cb20fd4c787fa8aebd53a414deabb20 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C3=ABl=20Arnauts?= Date: Fri, 4 Feb 2022 21:36:53 +0100 Subject: [PATCH 11/32] Prepare for v0.4.7 --- CHANGELOG.md | 8 ++++++++ addon.xml | 6 +++--- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9ae9666..b82aa50 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,13 @@ # Changelog +## [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) diff --git a/addon.xml b/addon.xml index 1e0f9a5..897b6b1 100644 --- a/addon.xml +++ b/addon.xml @@ -1,5 +1,5 @@ - + @@ -21,8 +21,8 @@ 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. all GPL-3.0-only - v0.4.6 (2022-02-02) -- Fix playback of DRM protected content. + v0.4.7 (2022-02-04) +- Fix My List. https://github.com/add-ons/plugin.video.viervijfzes resources/icon.png From a26121ee377d9fd90b2dda28aef9d0274e4be5c5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C3=ABl=20Arnauts?= Date: Thu, 7 Jul 2022 15:51:56 +0200 Subject: [PATCH 12/32] Fix API (#105) --- requirements.txt | 2 +- resources/lib/viervijfzes/content.py | 4 ++-- tests/test_api.py | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/requirements.txt b/requirements.txt index 5559ba9..d0448ee 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,6 +5,6 @@ pytest-cov pytest-timeout python-dateutil 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 sakee diff --git a/resources/lib/viervijfzes/content.py b/resources/lib/viervijfzes/content.py index 225b14c..f957ce6 100644 --- a/resources/lib/viervijfzes/content.py +++ b/resources/lib/viervijfzes/content.py @@ -689,8 +689,8 @@ class ContentApi: season=data.get('seasonNumber'), season_uuid=season_uuid, number=episode_number, - aired=datetime.fromtimestamp(data.get('createdDate')), - expiry=datetime.fromtimestamp(data.get('unpublishDate')) if data.get('unpublishDate') else None, + aired=datetime.fromtimestamp(int(data.get('createdDate'))), + expiry=datetime.fromtimestamp(int(data.get('unpublishDate'))) if data.get('unpublishDate') else None, rating=data.get('parentalRating'), stream=data.get('path'), ) diff --git a/tests/test_api.py b/tests/test_api.py index 90a8679..c80991e 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -75,7 +75,7 @@ class TestApi(unittest.TestCase): @unittest.skipUnless(kodiutils.get_setting('username') and kodiutils.get_setting('password'), 'Skipping since we have no credentials.') def test_get_drm_stream(self): - resolved_stream = self._api.get_stream_by_uuid('32ac67c6-9b15-4e3e-9440-6de04e586a6e') # NCIS 12x8 + resolved_stream = self._api.get_stream_by_uuid('b74882b7-7b19-4bfe-9773-22d8ce474aa5') # https://www.goplay.be/video/ncis-los-angeles/ncis-los-angeles-s3/ncis-los-angeles-s3-aflevering-3 self.assertIsInstance(resolved_stream, ResolvedStream) From 7ac3a21d1a234bca8d544df3aab1919f7a9b1b1c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C3=ABl=20Arnauts?= Date: Thu, 7 Jul 2022 15:52:36 +0200 Subject: [PATCH 13/32] Fix publish.py script --- scripts/publish.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/scripts/publish.py b/scripts/publish.py index f408e0c..f7b7b4c 100755 --- a/scripts/publish.py +++ b/scripts/publish.py @@ -99,7 +99,7 @@ def create_addon_branch(repo, branch, source, addon_info, gh_username, gh_token, shell('git', 'fetch', '-f', 'upstream', branch) # Fetch upstream else: # Clone the upstream repo - shell('git', 'clone', '--branch', branch, '--origin', 'upstream', '--single-branch', 'git://github.com/xbmc/{}.git'.format(repo)) + shell('git', 'clone', '--branch', branch, '--origin', 'upstream', '--single-branch', 'https://github.com/xbmc/{}.git'.format(repo)) os.chdir(repo) # Create local branch @@ -200,11 +200,11 @@ def create_pull_request(repo, branch, addon_info, gh_username, gh_token): if __name__ == '__main__': filenames = sys.argv[1:] - for filename in filenames: - # 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) + # 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 From 6d1c08e9c7233a63c29ad8749a10236a1acdfef1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C3=ABl=20Arnauts?= Date: Thu, 7 Jul 2022 16:00:54 +0200 Subject: [PATCH 14/32] Prepare for v0.4.8 --- CHANGELOG.md | 8 ++++++++ addon.xml | 6 +++--- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b82aa50..8a5bca9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,13 @@ # Changelog +## [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) diff --git a/addon.xml b/addon.xml index 897b6b1..d6dea12 100644 --- a/addon.xml +++ b/addon.xml @@ -1,5 +1,5 @@ - + @@ -21,8 +21,8 @@ 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. all GPL-3.0-only - v0.4.7 (2022-02-04) -- Fix My List. + v0.4.8 (2022-07-07) +- Fix Addon due to API changes. https://github.com/add-ons/plugin.video.viervijfzes resources/icon.png From 9f4bba446ad211c5691778da3a6f6e8e0cfd80f0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C3=ABl=20Arnauts?= Date: Sun, 16 Oct 2022 15:32:56 +0200 Subject: [PATCH 15/32] Fix clips (#108) --- resources/lib/viervijfzes/content.py | 17 +++++++++++++++-- tests/test_api.py | 2 +- 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/resources/lib/viervijfzes/content.py b/resources/lib/viervijfzes/content.py index f957ce6..3517219 100644 --- a/resources/lib/viervijfzes/content.py +++ b/resources/lib/viervijfzes/content.py @@ -309,7 +309,7 @@ class ContentApi: result = regex_video_data.search(page) if result: 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) return dict(video=video_json) @@ -336,7 +336,7 @@ class ContentApi: if 'video' in data and data['video']: # We have found detailed episode information - episode = self._parse_episode_data(data['video']) + episode = self._parse_clip_data(data['video']) return episode if 'program' in data and 'episode' in data and data['program'] and data['episode']: @@ -696,6 +696,19 @@ class ContentApi: ) return episode + @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 + def _get_url(self, url, params=None, authentication=None): """ Makes a GET request for the specified URL. :type url: str diff --git a/tests/test_api.py b/tests/test_api.py index c80991e..c78cc9f 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -55,7 +55,7 @@ class TestApi(unittest.TestCase): self.assertIsInstance(program.episodes[0], Episode) 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) self.assertIsInstance(program.clips, list) From 98a0ffb1620c0612ce1ecc599995618e37ceed36 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C3=ABl=20Arnauts?= Date: Sun, 16 Oct 2022 15:36:00 +0200 Subject: [PATCH 16/32] Support Python 3.10 (#107) * Support Python 3.10 * Add timeout * Fix test --- .github/workflows/ci.yml | 12 +++++++----- resources/lib/service.py | 2 +- tests/test_api.py | 2 +- 3 files changed, 9 insertions(+), 7 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3d0c3ca..28a7943 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -15,17 +15,19 @@ jobs: fail-fast: false matrix: os: [ ubuntu-latest ] - python-version: [ 2.7, 3.5, 3.6, 3.7, 3.8, 3.9 ] + python-version: [ "2.7", "3.5", "3.6", "3.7", "3.8", "3.9", "3.10" ] include: # Kodi Leia on Windows uses a bundled Python 2.7. - os: windows-latest - python-version: 2.7 + python-version: "2.7" - # Kodi Matrix on Windows uses a bundled Python 3.8, but we test 3.9 also to be sure. + # Kodi Matrix on Windows uses a bundled Python 3.8, but we test 3.9 and 3.10 also to be sure. - os: windows-latest - python-version: 3.8 + python-version: "3.8" - os: windows-latest - python-version: 3.9 + python-version: "3.9" + - os: windows-latest + python-version: "3.10" steps: - name: Check out ${{ github.sha }} from repository ${{ github.repository }} uses: actions/checkout@v2 diff --git a/resources/lib/service.py b/resources/lib/service.py index ae8cc5f..bba5fb6 100644 --- a/resources/lib/service.py +++ b/resources/lib/service.py @@ -125,7 +125,7 @@ class KodiPlayer(Player): if not self.av_started: # Check stream path import requests - response = requests.get(self.stream_path) + response = requests.get(self.stream_path, timeout=5) if response.status_code == 403: message_id = 30720 else: diff --git a/tests/test_api.py b/tests/test_api.py index c78cc9f..77edf18 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -75,7 +75,7 @@ class TestApi(unittest.TestCase): @unittest.skipUnless(kodiutils.get_setting('username') and kodiutils.get_setting('password'), 'Skipping since we have no credentials.') def test_get_drm_stream(self): - resolved_stream = self._api.get_stream_by_uuid('b74882b7-7b19-4bfe-9773-22d8ce474aa5') # https://www.goplay.be/video/ncis-los-angeles/ncis-los-angeles-s3/ncis-los-angeles-s3-aflevering-3 + resolved_stream = self._api.get_stream_by_uuid('62e04ab5-1f3c-4385-ad7a-e2943ddb1849') # https://www.goplay.be/video/ncis-los-angeles/ncis-los-angeles-s10/ncis-los-angeles-s10-aflevering-12 self.assertIsInstance(resolved_stream, ResolvedStream) From b7a479e585a3d22d8aad4664c394e1e3443ae023 Mon Sep 17 00:00:00 2001 From: mediaminister <45148099+mediaminister@users.noreply.github.com> Date: Thu, 15 Dec 2022 11:36:51 +0100 Subject: [PATCH 17/32] Add support for unprotected MPEG-DASH streams (#111) --- resources/lib/viervijfzes/content.py | 28 +++++++++++++++++++--------- 1 file changed, 19 insertions(+), 9 deletions(-) diff --git a/resources/lib/viervijfzes/content.py b/resources/lib/viervijfzes/content.py index 3517219..4c9f8cd 100644 --- a/resources/lib/viervijfzes/content.py +++ b/resources/lib/viervijfzes/content.py @@ -361,23 +361,33 @@ class ContentApi: raise UnavailableException if 'videoDash' in data: - # DRM protected stream - # 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) - response_drm = self._get_url(self.API_GOPLAY + '/video/xml/%s' % drm_key, authentication=self._auth.get_token()) - data_drm = json.loads(response_drm) + if 'drmKey' in data: + # DRM protected stream + # 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) + response_drm = self._get_url(self.API_GOPLAY + '/video/xml/%s' % drm_key, authentication=self._auth.get_token()) + data_drm = json.loads(response_drm) + + # DRM protected DASH stream + return ResolvedStream( + uuid=uuid, + url=data['videoDash']['S'], + stream_type=STREAM_DASH, + license_url='https://wv-keyos.licensekeyserver.com/', + auth=data_drm.get('auth'), + ) + + # Unprotected DASH stream return ResolvedStream( uuid=uuid, url=data['videoDash']['S'], stream_type=STREAM_DASH, - license_url='https://wv-keyos.licensekeyserver.com/', - auth=data_drm.get('auth'), ) - # Normal HLS stream + # Unprotected HLS stream return ResolvedStream( uuid=uuid, url=data['video']['S'], From f7f1921c2b19af068328b8914a9ff1fec6ad835f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C3=ABl=20Arnauts?= Date: Wed, 4 Jan 2023 14:01:05 +0100 Subject: [PATCH 18/32] Prepare for v0.4.9 --- CHANGELOG.md | 9 +++++++++ addon.xml | 6 +++--- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8a5bca9..cb968b1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,14 @@ # Changelog +## [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) diff --git a/addon.xml b/addon.xml index d6dea12..4124932 100644 --- a/addon.xml +++ b/addon.xml @@ -1,5 +1,5 @@ - + @@ -21,8 +21,8 @@ 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. all GPL-3.0-only - v0.4.8 (2022-07-07) -- Fix Addon due to API changes. + v0.4.9 (2023-01-04) +- Add support for unprotected HD MPEG-DASH streams. https://github.com/add-ons/plugin.video.viervijfzes resources/icon.png From 5ce74e4ec3cc3c47e43604c744a9545c2189dd58 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C3=ABl=20Arnauts?= Date: Wed, 4 Jan 2023 14:06:58 +0100 Subject: [PATCH 19/32] Update CI pipeline --- .github/workflows/ci.yml | 27 ++++++++++++++------------- tests/test_api.py | 6 +++--- 2 files changed, 17 insertions(+), 16 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 28a7943..d2407b3 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -14,26 +14,27 @@ jobs: strategy: fail-fast: false matrix: - os: [ ubuntu-latest ] - python-version: [ "2.7", "3.5", "3.6", "3.7", "3.8", "3.9", "3.10" ] + os: [ ubuntu-latest, windows-latest ] + python-version: ["3.8", "3.9", "3.10"] include: + # End-of-life Python versions are not available anymore with ubuntu-latest + - os: ubuntu-20.04 + python-version: "2.7" + - os: ubuntu-20.04 + python-version: "3.5" + - os: ubuntu-20.04 + python-version: "3.6" + - os: ubuntu-20.04 + python-version: "3.7" # Kodi Leia on Windows uses a bundled Python 2.7. - os: windows-latest python-version: "2.7" - - # Kodi Matrix on Windows uses a bundled Python 3.8, but we test 3.9 and 3.10 also to be sure. - - os: windows-latest - python-version: "3.8" - - os: windows-latest - python-version: "3.9" - - os: windows-latest - python-version: "3.10" steps: - name: Check out ${{ github.sha }} from repository ${{ github.repository }} - uses: actions/checkout@v2 + uses: actions/checkout@v3 - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v2 + uses: actions/setup-python@v4 with: python-version: ${{ matrix.python-version }} @@ -59,7 +60,7 @@ jobs: run: pytest -x -v --cov=./ --cov-report=xml tests - name: Upload code coverage to CodeCov - uses: codecov/codecov-action@v1 + uses: codecov/codecov-action@v3 continue-on-error: true env: OS: ${{ matrix.os }} diff --git a/tests/test_api.py b/tests/test_api.py index 77edf18..56ce607 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -47,7 +47,7 @@ class TestApi(unittest.TestCase): self.assertIsInstance(programs[0], Program) 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) self.assertIsInstance(program, Program) self.assertIsInstance(program.seasons, dict) @@ -66,7 +66,7 @@ class TestApi(unittest.TestCase): @unittest.skipUnless(kodiutils.get_setting('username') and kodiutils.get_setting('password'), 'Skipping since we have no credentials.') def test_get_stream(self): - program = self._api.get_program('auwch') + program = self._api.get_program('gentwest') self.assertIsInstance(program, Program) episode = program.episodes[0] @@ -75,7 +75,7 @@ class TestApi(unittest.TestCase): @unittest.skipUnless(kodiutils.get_setting('username') and kodiutils.get_setting('password'), 'Skipping since we have no credentials.') def test_get_drm_stream(self): - resolved_stream = self._api.get_stream_by_uuid('62e04ab5-1f3c-4385-ad7a-e2943ddb1849') # https://www.goplay.be/video/ncis-los-angeles/ncis-los-angeles-s10/ncis-los-angeles-s10-aflevering-12 + resolved_stream = self._api.get_stream_by_uuid('e7faa457-5768-4abd-bf4f-5a0e1055bbd3') # https://www.goplay.be/video/ncis-los-angeles/ncis-los-angeles-s13/ncis-los-angeles-s13-aflevering-1 self.assertIsInstance(resolved_stream, ResolvedStream) From 89f4b457df745170b400c61a93e0be6cbd6e415a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C3=ABl=20Arnauts?= Date: Wed, 4 Jan 2023 14:22:01 +0100 Subject: [PATCH 20/32] Update README.md --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 06de3e1..e0fae9f 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@ -[![GitHub release](https://img.shields.io/github/release/add-ons/plugin.video.viervijfzes.svg?include_prereleases)](https://github.com/add-ons/plugin.video.viervijfzes/releases) -[![Build Status](https://img.shields.io/github/workflow/status/add-ons/plugin.video.viervijfzes/CI/master)](https://github.com/add-ons/plugin.video.viervijfzes/actions?query=branch%3Amaster) +[![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/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) [![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) From 9cdebb6c7f21ce64d49c1a2d02841e5377455487 Mon Sep 17 00:00:00 2001 From: mediaminister <45148099+mediaminister@users.noreply.github.com> Date: Mon, 16 Jan 2023 17:59:29 +0100 Subject: [PATCH 21/32] Update api (#114) --- resources/lib/addon.py | 7 +++- resources/lib/modules/menu.py | 2 +- resources/lib/modules/player.py | 12 +++--- resources/lib/viervijfzes/content.py | 57 ++++++++++++++++++---------- tests/test_api.py | 4 +- 5 files changed, 51 insertions(+), 31 deletions(-) diff --git a/resources/lib/addon.py b/resources/lib/addon.py index 4410a1d..a55e072 100644 --- a/resources/lib/addon.py +++ b/resources/lib/addon.py @@ -161,10 +161,13 @@ def play_epg(channel, timestamp): @routing.route('/play/catalog') @routing.route('/play/catalog/') -def play_catalog(uuid=None): +@routing.route('/play/catalog//') +def play_catalog(uuid=None, islongform=False): """ Play the requested item """ + from ast import literal_eval from resources.lib.modules.player import Player - Player().play(uuid) + # Convert string to bool using literal_eval + Player().play(uuid, literal_eval(islongform)) @routing.route('/play/page/') diff --git a/resources/lib/modules/menu.py b/resources/lib/modules/menu.py index 6ea515e..d434e7e 100644 --- a/resources/lib/modules/menu.py +++ b/resources/lib/modules/menu.py @@ -183,7 +183,7 @@ class Menu: if item.uuid: # 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, islongform=item.islongform) else: # 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='')) diff --git a/resources/lib/modules/player.py b/resources/lib/modules/player.py index c993f26..437e0a6 100644 --- a/resources/lib/modules/player.py +++ b/resources/lib/modules/player.py @@ -74,7 +74,7 @@ class Player: if episode.uuid: # Lookup the stream - resolved_stream = self._resolve_stream(episode.uuid) + resolved_stream = self._resolve_stream(episode.uuid, episode.islongform) _LOGGER.debug('Resolved stream: %s', resolved_stream) if resolved_stream: @@ -95,16 +95,17 @@ class Player: art_dict=titleitem.art_dict, prop_dict=titleitem.prop_dict) - def play(self, uuid): + def play(self, uuid, islongform): """ Play the requested item. :type uuid: string + :type islongform: bool """ if not uuid: kodiutils.ok_dialog(message=kodiutils.localize(30712)) # The video is unavailable... return # Lookup the stream - resolved_stream = self._resolve_stream(uuid) + resolved_stream = self._resolve_stream(uuid, islongform) if resolved_stream.license_url: # Generate license key license_key = self.create_license_key(resolved_stream.license_url, key_headers=dict( @@ -116,9 +117,10 @@ class Player: kodiutils.play(resolved_stream.url, resolved_stream.stream_type, license_key) @staticmethod - def _resolve_stream(uuid): + def _resolve_stream(uuid, islongform): """ Resolve the stream for the requested item :type uuid: string + :type islongform: bool """ try: # Check if we have credentials @@ -135,7 +137,7 @@ class Player: auth = AuthApi(kodiutils.get_setting('username'), kodiutils.get_setting('password'), kodiutils.get_tokens_path()) # Get stream information - resolved_stream = ContentApi(auth).get_stream_by_uuid(uuid) + resolved_stream = ContentApi(auth).get_stream_by_uuid(uuid, islongform) return resolved_stream except (InvalidLoginException, AuthenticationException) as ex: diff --git a/resources/lib/viervijfzes/content.py b/resources/lib/viervijfzes/content.py index 4c9f8cd..1913e24 100644 --- a/resources/lib/viervijfzes/content.py +++ b/resources/lib/viervijfzes/content.py @@ -109,7 +109,7 @@ class 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, - 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, islongform=False): """ :type uuid: str :type nodeid: str @@ -127,6 +127,7 @@ class Episode: :type aired: datetime :type expiry: datetime :type stream: string + :type islongform: bool """ self.uuid = uuid self.nodeid = nodeid @@ -144,6 +145,7 @@ class Episode: self.aired = aired self.expiry = expiry self.stream = stream + self.islongform = islongform def __repr__(self): return "%r" % self.__dict__ @@ -349,50 +351,63 @@ class ContentApi: return None - def get_stream_by_uuid(self, uuid): + def get_stream_by_uuid(self, uuid, islongform): """ Get the stream URL to use for this video. :type uuid: str + :type islongform: bool :rtype str """ - response = self._get_url(self.API_VIERVIJFZES + '/content/%s' % uuid, authentication=self._auth.get_token()) + mode = 'long-form' if islongform else 'short-form' + response = self._get_url(self.API_GOPLAY + '/web/v1/videos/%s/%s' % (mode, uuid), authentication='Bearer %s' % self._auth.get_token()) data = json.loads(response) if not data: raise UnavailableException - if 'videoDash' in data: + if data.get('manifestUrls'): - if 'drmKey' in data: + if data.get('drmXml'): # DRM protected stream # 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) - response_drm = self._get_url(self.API_GOPLAY + '/video/xml/%s' % drm_key, authentication=self._auth.get_token()) - data_drm = json.loads(response_drm) # DRM protected DASH stream return ResolvedStream( uuid=uuid, - url=data['videoDash']['S'], + url=data['manifestUrls']['dash'], stream_type=STREAM_DASH, license_url='https://wv-keyos.licensekeyserver.com/', - auth=data_drm.get('auth'), + auth=data['drmXml'], ) + if data.get('manifestUrls').get('dash'): + # Unprotected DASH stream + return ResolvedStream( + uuid=uuid, + url=data['manifestUrls']['dash'], + stream_type=STREAM_DASH, + ) + + # Unprotected HLS stream + return ResolvedStream( + uuid=uuid, + url=data['manifestUrls']['hls'], + stream_type=STREAM_HLS, + ) + + # 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='')) + # Unprotected DASH stream return ResolvedStream( uuid=uuid, - url=data['videoDash']['S'], + url=ad_data['stream_manifest'], stream_type=STREAM_DASH, ) - # Unprotected HLS stream - return ResolvedStream( - uuid=uuid, - url=data['video']['S'], - stream_type=STREAM_HLS, - ) + raise UnavailableException + def get_program_tree(self, cache=CACHE_AUTO): """ Get a content tree with information about all the programs. @@ -675,7 +690,6 @@ class ContentApi: :type season_uuid: str :rtype Episode """ - if data.get('episodeNumber'): episode_number = data.get('episodeNumber') else: @@ -703,6 +717,7 @@ class ContentApi: expiry=datetime.fromtimestamp(int(data.get('unpublishDate'))) if data.get('unpublishDate') else None, rating=data.get('parentalRating'), stream=data.get('path'), + islongform=data.get('isLongForm'), ) return episode @@ -751,7 +766,7 @@ class ContentApi: else: response = self._session.post(url, params=params, json=data) - if response.status_code != 200: + if response.status_code not in (200, 201): _LOGGER.error(response.text) raise Exception('Could not fetch data') diff --git a/tests/test_api.py b/tests/test_api.py index 56ce607..bd600d2 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -70,12 +70,12 @@ class TestApi(unittest.TestCase): self.assertIsInstance(program, Program) 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) @unittest.skipUnless(kodiutils.get_setting('username') and kodiutils.get_setting('password'), 'Skipping since we have no credentials.') def test_get_drm_stream(self): - resolved_stream = self._api.get_stream_by_uuid('e7faa457-5768-4abd-bf4f-5a0e1055bbd3') # https://www.goplay.be/video/ncis-los-angeles/ncis-los-angeles-s13/ncis-los-angeles-s13-aflevering-1 + resolved_stream = self._api.get_stream_by_uuid('e7faa457-5768-4abd-bf4f-5a0e1055bbd3', True) # https://www.goplay.be/video/ncis-los-angeles/ncis-los-angeles-s13/ncis-los-angeles-s13-aflevering-1 self.assertIsInstance(resolved_stream, ResolvedStream) From 7f61533c4a534c4848dfda5761d03fde6e3bda01 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C3=ABl=20Arnauts?= Date: Mon, 16 Jan 2023 18:03:30 +0100 Subject: [PATCH 22/32] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index e0fae9f..75b3684 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,7 @@ 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). -> Let op dat er geen live-streams beschikbaar zijn op het GoPlay platform en je dus enkel kan kijken de beschikbare programma's in de catalogus. +> Let op dat er geen live-streams beschikbaar zijn op het GoPlay platform en je dus enkel kan kijken naar de beschikbare programma's in de catalogus. ## Features From be42bc6e1c5524413658f5c89aa7ffbde5a37f73 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C3=ABl=20Arnauts?= Date: Mon, 16 Jan 2023 18:23:30 +0100 Subject: [PATCH 23/32] Remove obsolete API_VIERVIJFZES --- resources/lib/viervijfzes/content.py | 1 - 1 file changed, 1 deletion(-) diff --git a/resources/lib/viervijfzes/content.py b/resources/lib/viervijfzes/content.py index 1913e24..81e9753 100644 --- a/resources/lib/viervijfzes/content.py +++ b/resources/lib/viervijfzes/content.py @@ -175,7 +175,6 @@ class Category: class ContentApi: """ GoPlay Content API""" SITE_URL = 'https://www.goplay.be' - API_VIERVIJFZES = 'https://api.viervijfzes.be' API_GOPLAY = 'https://api.goplay.be' def __init__(self, auth=None, cache_path=None): From 9cd8688fee3173cfb2a8e6559df531b0c1bbd1d6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C3=ABl=20Arnauts?= Date: Mon, 16 Jan 2023 18:24:39 +0100 Subject: [PATCH 24/32] Prepare for v0.4.10 --- CHANGELOG.md | 8 ++++++++ addon.xml | 6 +++--- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index cb968b1..2651d9e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,13 @@ # Changelog +## [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) diff --git a/addon.xml b/addon.xml index 4124932..c82b208 100644 --- a/addon.xml +++ b/addon.xml @@ -1,5 +1,5 @@ - + @@ -21,8 +21,8 @@ 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. all GPL-3.0-only - v0.4.9 (2023-01-04) -- Add support for unprotected HD MPEG-DASH streams. + v0.4.10 (2023-01-16) +- Update API https://github.com/add-ons/plugin.video.viervijfzes resources/icon.png From 7504d1a6ec65d03b035df74f742e86cb17d6847b Mon Sep 17 00:00:00 2001 From: mediaminister <45148099+mediaminister@users.noreply.github.com> Date: Fri, 21 Jul 2023 22:12:09 +0200 Subject: [PATCH 25/32] Use inputstreamhelper for unprotected MPEG-DASH (#118) --- resources/lib/kodiutils.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/resources/lib/kodiutils.py b/resources/lib/kodiutils.py index 673299f..b4b2886 100644 --- a/resources/lib/kodiutils.py +++ b/resources/lib/kodiutils.py @@ -259,12 +259,17 @@ def play(stream, stream_type=STREAM_HLS, license_key=None, title=None, art_dict= elif stream_type == STREAM_DASH: play_item.setProperty('inputstream.adaptive.manifest_type', 'mpd') play_item.setMimeType('application/dash+xml') + import inputstreamhelper if license_key is not None: - import inputstreamhelper + # DRM protected MPEG-DASH is_helper = inputstreamhelper.Helper('mpd', drm='com.widevine.alpha') if is_helper.check_inputstream(): play_item.setProperty('inputstream.adaptive.license_type', 'com.widevine.alpha') 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) From dc446332abc31cd08b28c8936074841f5b86fb20 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C3=ABl=20Arnauts?= Date: Fri, 21 Jul 2023 22:15:07 +0200 Subject: [PATCH 26/32] Fix tests --- tests/test_api.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_api.py b/tests/test_api.py index bd600d2..790e92f 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -75,7 +75,7 @@ class TestApi(unittest.TestCase): @unittest.skipUnless(kodiutils.get_setting('username') and kodiutils.get_setting('password'), 'Skipping since we have no credentials.') def test_get_drm_stream(self): - resolved_stream = self._api.get_stream_by_uuid('e7faa457-5768-4abd-bf4f-5a0e1055bbd3', True) # https://www.goplay.be/video/ncis-los-angeles/ncis-los-angeles-s13/ncis-los-angeles-s13-aflevering-1 + 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) From c9ae78f83bf5b453e223f087726cc02654b7c82e Mon Sep 17 00:00:00 2001 From: mediaminister <45148099+mediaminister@users.noreply.github.com> Date: Fri, 21 Jul 2023 22:16:07 +0200 Subject: [PATCH 27/32] Add DRM support for all streams (#121) --- resources/lib/modules/player.py | 49 +------------------- resources/lib/viervijfzes/__init__.py | 8 ++-- resources/lib/viervijfzes/content.py | 66 +++++++++++++++++++-------- 3 files changed, 53 insertions(+), 70 deletions(-) diff --git a/resources/lib/modules/player.py b/resources/lib/modules/player.py index 437e0a6..3d2e466 100644 --- a/resources/lib/modules/player.py +++ b/resources/lib/modules/player.py @@ -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.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__) @@ -79,18 +74,9 @@ class Player: if resolved_stream: 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, resolved_stream.stream_type, - license_key, + resolved_stream.license_key, info_dict=titleitem.info_dict, art_dict=titleitem.art_dict, prop_dict=titleitem.prop_dict) @@ -106,15 +92,7 @@ class Player: # Lookup the stream resolved_stream = self._resolve_stream(uuid, islongform) - if resolved_stream.license_url: - # Generate license key - license_key = self.create_license_key(resolved_stream.license_url, key_headers=dict( - customdata=resolved_stream.auth, - )) - else: - license_key = None - - kodiutils.play(resolved_stream.url, resolved_stream.stream_type, license_key) + kodiutils.play(resolved_stream.url, resolved_stream.stream_type, resolved_stream.license_key) @staticmethod def _resolve_stream(uuid, islongform): @@ -153,26 +131,3 @@ class Player: except UnavailableException: kodiutils.ok_dialog(message=kodiutils.localize(30712)) # The video is unavailable... 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) diff --git a/resources/lib/viervijfzes/__init__.py b/resources/lib/viervijfzes/__init__.py index a5dcb63..46cb626 100644 --- a/resources/lib/viervijfzes/__init__.py +++ b/resources/lib/viervijfzes/__init__.py @@ -79,19 +79,17 @@ STREAM_DICT = { class ResolvedStream: """ 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 url: str :type stream_type: str - :type license_url: str - :type auth: str + :type license_key: str """ self.uuid = uuid self.url = url self.stream_type = stream_type - self.license_url = license_url - self.auth = auth + self.license_key = license_key def __repr__(self): return "%r" % self.__dict__ diff --git a/resources/lib/viervijfzes/content.py b/resources/lib/viervijfzes/content.py index 81e9753..097b2bf 100644 --- a/resources/lib/viervijfzes/content.py +++ b/resources/lib/viervijfzes/content.py @@ -351,10 +351,10 @@ class ContentApi: return None def get_stream_by_uuid(self, uuid, islongform): - """ Get the stream URL to use for this video. + """ Return a ResolvedStream for this video. :type uuid: str :type islongform: bool - :rtype str + :rtype: ResolvedStream """ mode = 'long-form' if islongform else 'short-form' response = self._get_url(self.API_GOPLAY + '/web/v1/videos/%s/%s' % (mode, uuid), authentication='Bearer %s' % self._auth.get_token()) @@ -363,34 +363,35 @@ class ContentApi: if not data: 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 + + # Generate license key + license_key = self.create_license_key('https://wv-keyos.licensekeyserver.com/', key_headers=dict( + customdata=data['drmXml'], + )) + + # Get manifest url if data.get('manifestUrls'): - if data.get('drmXml'): - # DRM protected stream - # See https://docs.unified-streaming.com/documentation/drm/buydrm.html#setting-up-the-client - - # DRM protected DASH stream - return ResolvedStream( - uuid=uuid, - url=data['manifestUrls']['dash'], - stream_type=STREAM_DASH, - license_url='https://wv-keyos.licensekeyserver.com/', - auth=data['drmXml'], - ) - if data.get('manifestUrls').get('dash'): - # Unprotected DASH stream + # DASH stream return ResolvedStream( uuid=uuid, url=data['manifestUrls']['dash'], stream_type=STREAM_DASH, + license_key=license_key, ) - # Unprotected HLS stream + # HLS stream return ResolvedStream( uuid=uuid, url=data['manifestUrls']['hls'], stream_type=STREAM_HLS, + license_key=license_key, ) # No manifest url found, get manifest from Server-Side Ad Insertion service @@ -398,11 +399,12 @@ class ContentApi: 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='')) - # Unprotected DASH stream + # 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 @@ -733,6 +735,34 @@ class ContentApi: ) 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. :type url: str From 48c993a4e7833cf15e7e485ebce6fc1495d03c1e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C3=ABl=20Arnauts?= Date: Fri, 21 Jul 2023 22:28:50 +0200 Subject: [PATCH 28/32] Fix pylint warnings/errors --- .pylintrc | 1 + resources/lib/kodiutils.py | 22 ++--- resources/lib/modules/iptvmanager.py | 49 +++++------ resources/lib/modules/menu.py | 84 +++++++++---------- resources/lib/modules/player.py | 2 +- resources/lib/viervijfzes/__init__.py | 112 ++++++++++++-------------- resources/lib/viervijfzes/auth.py | 10 +-- resources/lib/viervijfzes/content.py | 10 +-- 8 files changed, 140 insertions(+), 150 deletions(-) diff --git a/.pylintrc b/.pylintrc index 248d282..283b504 100644 --- a/.pylintrc +++ b/.pylintrc @@ -18,6 +18,7 @@ disable= 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 diff --git a/resources/lib/kodiutils.py b/resources/lib/kodiutils.py index b4b2886..bdf7e69 100644 --- a/resources/lib/kodiutils.py +++ b/resources/lib/kodiutils.py @@ -21,15 +21,15 @@ except ImportError: # Python 2 ADDON = xbmcaddon.Addon() -SORT_METHODS = dict( - unsorted=xbmcplugin.SORT_METHOD_UNSORTED, - label=xbmcplugin.SORT_METHOD_LABEL_IGNORE_FOLDERS, - title=xbmcplugin.SORT_METHOD_TITLE, - episode=xbmcplugin.SORT_METHOD_EPISODE, - duration=xbmcplugin.SORT_METHOD_DURATION, - year=xbmcplugin.SORT_METHOD_VIDEO_YEAR, - date=xbmcplugin.SORT_METHOD_DATE, -) +SORT_METHODS = { + 'unsorted': xbmcplugin.SORT_METHOD_UNSORTED, + 'label': xbmcplugin.SORT_METHOD_LABEL_IGNORE_FOLDERS, + 'title': xbmcplugin.SORT_METHOD_TITLE, + 'episode': xbmcplugin.SORT_METHOD_EPISODE, + 'duration': xbmcplugin.SORT_METHOD_DURATION, + 'year': xbmcplugin.SORT_METHOD_VIDEO_YEAR, + 'date': xbmcplugin.SORT_METHOD_DATE +} DEFAULT_SORT_METHODS = [ 'unsorted', 'title' ] @@ -469,13 +469,13 @@ def open_settings(): def get_global_setting(key): """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') def set_global_setting(key, value): """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 get_cond_visibility(condition): diff --git a/resources/lib/modules/iptvmanager.py b/resources/lib/modules/iptvmanager.py index a5e7f54..84f9589 100644 --- a/resources/lib/modules/iptvmanager.py +++ b/resources/lib/modules/iptvmanager.py @@ -42,17 +42,17 @@ class IPTVManager: streams = [] for key, channel in CHANNELS.items(): if channel.get('iptv_id'): - streams.append(dict( - id=channel.get('iptv_id'), - name=channel.get('name'), - logo='special://home/addons/{addon}/resources/logos/{logo}'.format(addon=kodiutils.addon_id(), - logo=channel.get('logo')), - preset=channel.get('iptv_preset'), - stream='plugin://plugin.video.viervijfzes/play/live/{channel}'.format(channel=key), - vod='plugin://plugin.video.viervijfzes/play/epg/{channel}/{{date}}'.format(channel=key) - )) + streams.append({ + 'id': channel.get('iptv_id'), + 'name': channel.get('name'), + 'logo': 'special://home/addons/{addon}/resources/logos/{logo}'.format(addon=kodiutils.addon_id(), + logo=channel.get('logo')), + 'preset': channel.get('iptv_preset'), + 'stream': 'plugin://plugin.video.viervijfzes/play/live/{channel}'.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 def send_epg(): # pylint: disable=no-method-argument @@ -78,20 +78,21 @@ class IPTVManager: epg = epg_api.get_epg(key, date.strftime('%Y-%m-%d')) results[iptv_id].extend([ - dict( - start=program.start.isoformat(), - stop=(program.start + timedelta(seconds=program.duration)).isoformat(), - title=program.program_title, - subtitle=program.episode_title, - description=program.description, - episode='S%sE%s' % (program.season, program.number) if program.season and program.number else None, - genre=program.genre, - genre_id=program.genre_id, - image=program.thumb, - stream=kodiutils.url_for('play_from_page', - channel=key, - page=quote(program.video_url, safe='')) if program.video_url else None) + { + 'start': program.start.isoformat(), + 'stop': (program.start + timedelta(seconds=program.duration)).isoformat(), + 'title': program.program_title, + 'subtitle': program.episode_title, + 'description': program.description, + 'episode': 'S%sE%s' % (program.season, program.number) if program.season and program.number else None, + 'genre': program.genre, + 'genre_id': program.genre_id, + 'image': program.thumb, + 'stream': kodiutils.url_for('play_from_page', + channel=key, + page=quote(program.video_url, safe='')) if program.video_url else None + } for program in epg if program.duration ]) - return dict(version=1, epg=results) + return {'version': 1, 'epg': results} diff --git a/resources/lib/modules/menu.py b/resources/lib/modules/menu.py index d434e7e..0bacfff 100644 --- a/resources/lib/modules/menu.py +++ b/resources/lib/modules/menu.py @@ -31,68 +31,68 @@ class Menu: TitleItem( title=kodiutils.localize(30001), # A-Z path=kodiutils.url_for('show_catalog'), - art_dict=dict( - icon='DefaultMovieTitle.png', - fanart=kodiutils.get_addon_info('fanart'), - ), - info_dict=dict( - plot=kodiutils.localize(30002), - ) + art_dict={ + 'icon': 'DefaultMovieTitle.png', + 'fanart': kodiutils.get_addon_info('fanart') + }, + info_dict={ + 'plot': kodiutils.localize(30002) + } ), TitleItem( title=kodiutils.localize(30007), # TV Channels path=kodiutils.url_for('show_channels'), - art_dict=dict( - icon='DefaultAddonPVRClient.png', - fanart=kodiutils.get_addon_info('fanart'), - ), - info_dict=dict( - plot=kodiutils.localize(30008), - ) + art_dict={ + 'icon': 'DefaultAddonPVRClient.png', + 'fanart': kodiutils.get_addon_info('fanart') + }, + info_dict={ + 'plot': kodiutils.localize(30008) + } ), TitleItem( title=kodiutils.localize(30003), # Catalog path=kodiutils.url_for('show_categories'), - art_dict=dict( - icon='DefaultGenre.png', - fanart=kodiutils.get_addon_info('fanart'), - ), - info_dict=dict( - plot=kodiutils.localize(30004), - ) + art_dict={ + 'icon': 'DefaultGenre.png', + 'fanart': kodiutils.get_addon_info('fanart') + }, + info_dict={ + 'plot': kodiutils.localize(30004) + } ), TitleItem( title=kodiutils.localize(30005), # Recommendations path=kodiutils.url_for('show_recommendations'), - art_dict=dict( - icon='DefaultFavourites.png', - fanart=kodiutils.get_addon_info('fanart'), - ), - info_dict=dict( - plot=kodiutils.localize(30006), - ) + art_dict={ + 'icon': 'DefaultFavourites.png', + 'fanart': kodiutils.get_addon_info('fanart') + }, + info_dict={ + 'plot': kodiutils.localize(30006) + } ), TitleItem( title=kodiutils.localize(30011), # My List path=kodiutils.url_for('show_mylist'), - art_dict=dict( - icon='DefaultPlaylist.png', - fanart=kodiutils.get_addon_info('fanart'), - ), - info_dict=dict( - plot=kodiutils.localize(30012), - ) + art_dict={ + 'icon': 'DefaultPlaylist.png', + 'fanart': kodiutils.get_addon_info('fanart') + }, + info_dict={ + 'plot': kodiutils.localize(30012) + } ), TitleItem( title=kodiutils.localize(30009), # Search path=kodiutils.url_for('show_search'), - art_dict=dict( - icon='DefaultAddonsSearch.png', - fanart=kodiutils.get_addon_info('fanart'), - ), - info_dict=dict( - plot=kodiutils.localize(30010), - ) + art_dict={ + 'icon': 'DefaultAddonsSearch.png', + 'fanart': kodiutils.get_addon_info('fanart') + }, + info_dict={ + 'plot': kodiutils.localize(30010) + } ) ] diff --git a/resources/lib/modules/player.py b/resources/lib/modules/player.py index 3d2e466..5497f93 100644 --- a/resources/lib/modules/player.py +++ b/resources/lib/modules/player.py @@ -38,7 +38,7 @@ class Player: # self.play_from_page(broadcast.video_url) # return - channel_name = CHANNELS.get(channel, dict(name=channel)) + channel_name = CHANNELS.get(channel, {'name': channel}) kodiutils.ok_dialog(message=kodiutils.localize(30718, channel=channel_name.get('name'))) # There is no live stream available for {channel}. kodiutils.end_of_directory() diff --git a/resources/lib/viervijfzes/__init__.py b/resources/lib/viervijfzes/__init__.py index 46cb626..91fe30b 100644 --- a/resources/lib/viervijfzes/__init__.py +++ b/resources/lib/viervijfzes/__init__.py @@ -5,68 +5,56 @@ from __future__ import absolute_import, division, unicode_literals from collections import OrderedDict CHANNELS = OrderedDict([ - ('Play4', dict( - name='Play4', - epg_id='vier', - logo='play4.png', - background='play4-background.png', - iptv_preset=4, - iptv_id='play4.be', - youtube=[ - dict( - label='GoPlay', - logo='goplay.png', - path='plugin://plugin.video.youtube/user/viertv/', - ), - ], - )), - ('Play5', dict( - name='Play5', - epg_id='vijf', - logo='play5.png', - background='play5-background.png', - iptv_preset=5, - iptv_id='play5.be', - youtube=[ - dict( - label='GoPlay', - logo='goplay.png', - path='plugin://plugin.video.youtube/user/viertv/', - ), - ], - )), - ('Play6', dict( - name='Play6', - epg_id='zes', - logo='play6.png', - background='play6-background.png', - iptv_preset=6, - iptv_id='play6.be', - youtube=[ - dict( - label='GoPlay', - logo='goplay.png', - path='plugin://plugin.video.youtube/user/viertv/', - ), - ], - )), - ('Play7', dict( - name='Play7', - epg_id='zeven', - url='https://www.goplay.be', - logo='play7.png', - background='play7-background.png', - iptv_preset=17, - iptv_id='play7.be', - youtube=[], - )), - ('GoPlay', dict( - name='Go Play', - url='https://www.goplay.be', - logo='goplay.png', - background='goplay-background.png', - youtube=[], - )) + ('Play4', { + 'name': 'Play4', + 'epg_id': 'vier', + 'logo': 'play4.png', + 'background': 'play4-background.png', + 'iptv_preset': 4, + 'iptv_id': 'play4.be', + 'youtube': [ + {'label': 'GoPlay', 'logo': 'goplay.png', 'path': 'plugin://plugin.video.youtube/user/viertv/'}, + ] + }), + ('Play5', { + 'name': 'Play5', + 'epg_id': 'vijf', + 'logo': 'play5.png', + 'background': 'play5-background.png', + 'iptv_preset': 5, + 'iptv_id': 'play5.be', + 'youtube': [ + {'label': 'GoPlay', 'logo': 'goplay.png', 'path': 'plugin://plugin.video.youtube/user/viertv/'}, + ] + }), + ('Play6', { + 'name': 'Play6', + 'epg_id': 'zes', + 'logo': 'play6.png', + 'background': 'play6-background.png', + 'iptv_preset': 6, + 'iptv_id': 'play6.be', + 'youtube': [ + {'label': 'GoPlay', 'logo': 'goplay.png', 'path': 'plugin://plugin.video.youtube/user/viertv/'}, + ] + }), + ('Play7', { + 'name': 'Play7', + 'epg_id': 'zeven', + 'url': 'https://www.goplay.be', + 'logo': 'play7.png', + 'background': 'play7-background.png', + 'iptv_preset': 17, + 'iptv_id': 'play7.be', + 'youtube': [] + }), + ('GoPlay', { + 'name': 'Go Play', + 'url': 'https://www.goplay.be', + 'logo': 'goplay.png', + 'background': 'goplay-background.png', + 'youtube': [] + }) ]) STREAM_DICT = { diff --git a/resources/lib/viervijfzes/auth.py b/resources/lib/viervijfzes/auth.py index 04dfa46..ed31d61 100644 --- a/resources/lib/viervijfzes/auth.py +++ b/resources/lib/viervijfzes/auth.py @@ -79,11 +79,11 @@ class AuthApi: if not os.path.exists(self._token_path): os.makedirs(self._token_path) with open(os.path.join(self._token_path, self.TOKEN_FILE), 'w') as fdesc: - data = json.dumps(dict( - id_token=self._id_token, - refresh_token=self._refresh_token, - expiry=self._expiry, - )) + data = json.dumps({ + 'id_token': self._id_token, + 'refresh_token': self._refresh_token, + 'expiry': self._expiry + }) fdesc.write(kodiutils.from_unicode(data)) return self._id_token diff --git a/resources/lib/viervijfzes/content.py b/resources/lib/viervijfzes/content.py index 097b2bf..7c0c2f7 100644 --- a/resources/lib/viervijfzes/content.py +++ b/resources/lib/viervijfzes/content.py @@ -312,7 +312,7 @@ class ContentApi: video_id = json.loads(unescape(result.group(1)))['id'] video_json_data = self._get_url('%s/web/v1/videos/short-form/%s' % (self.API_GOPLAY, video_id)) video_json = json.loads(video_json_data) - return dict(video=video_json) + return {'video': video_json} # Extract program JSON regex_program = re.compile(r'data-hero="([^"]+)', re.DOTALL) @@ -328,7 +328,7 @@ class ContentApi: episode_json_data = unescape(result.group(1)) 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 data = self._handle_cache(key=['episode', path], cache_mode=cache, update=update) @@ -370,9 +370,9 @@ class ContentApi: # See https://docs.unified-streaming.com/documentation/drm/buydrm.html#setting-up-the-client # Generate license key - license_key = self.create_license_key('https://wv-keyos.licensekeyserver.com/', key_headers=dict( - customdata=data['drmXml'], - )) + license_key = self.create_license_key('https://wv-keyos.licensekeyserver.com/', key_headers={ + 'customdata': data['drmXml'] + }) # Get manifest url if data.get('manifestUrls'): From bc82711886f3c682e6d817f7dccbb120e6318dc0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C3=ABl=20Arnauts?= Date: Fri, 21 Jul 2023 22:54:53 +0200 Subject: [PATCH 29/32] Add support for proxies (#126) * Add support for proxies --- requirements.txt | 2 + resources/lib/kodiutils.py | 62 +++++++++++++++++++ .../lib/viervijfzes/aws/cognito_identity.py | 2 - resources/lib/viervijfzes/aws/cognito_idp.py | 1 - resources/lib/viervijfzes/content.py | 19 +++--- resources/lib/viervijfzes/epg.py | 6 +- resources/lib/viervijfzes/search.py | 6 +- 7 files changed, 84 insertions(+), 14 deletions(-) diff --git a/requirements.txt b/requirements.txt index d0448ee..45634bd 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,7 +4,9 @@ pytest pytest-cov pytest-timeout python-dateutil +pysocks requests git+https://github.com/tamland/kodi-plugin-routing@master#egg=routing six sakee +win-inet-pton; platform_system=="Windows" \ No newline at end of file diff --git a/resources/lib/kodiutils.py b/resources/lib/kodiutils.py index bdf7e69..564de8e 100644 --- a/resources/lib/kodiutils.py +++ b/resources/lib/kodiutils.py @@ -17,6 +17,7 @@ try: # Python 3 from html import unescape except ImportError: # Python 2 from HTMLParser import HTMLParser + unescape = HTMLParser().unescape ADDON = xbmcaddon.Addon() @@ -478,6 +479,67 @@ def set_global_setting(key, 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): """Test a condition in XBMC""" return xbmc.getCondVisibility(condition) diff --git a/resources/lib/viervijfzes/aws/cognito_identity.py b/resources/lib/viervijfzes/aws/cognito_identity.py index 49fcb85..b99713b 100644 --- a/resources/lib/viervijfzes/aws/cognito_identity.py +++ b/resources/lib/viervijfzes/aws/cognito_identity.py @@ -44,7 +44,6 @@ class CognitoIdentity: 'x-amz-target': 'AWSCognitoIdentityService.GetId', 'content-type': 'application/x-amz-json-1.1', }) - _LOGGER.debug(response.text) result = json.loads(response.text) @@ -64,7 +63,6 @@ class CognitoIdentity: 'x-amz-target': 'AWSCognitoIdentityService.GetCredentialsForIdentity', 'content-type': 'application/x-amz-json-1.1', }) - _LOGGER.debug(response.text) result = json.loads(response.text) diff --git a/resources/lib/viervijfzes/aws/cognito_idp.py b/resources/lib/viervijfzes/aws/cognito_idp.py index c9e87ce..aeb9db9 100644 --- a/resources/lib/viervijfzes/aws/cognito_idp.py +++ b/resources/lib/viervijfzes/aws/cognito_idp.py @@ -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.small_a_value = self.__generate_random_small_a() self.large_a_value = self.__calculate_a() - _LOGGER.debug("Created %s", self) def authenticate(self, username, password): """ Authenticate with a username and password. """ diff --git a/resources/lib/viervijfzes/content.py b/resources/lib/viervijfzes/content.py index 7c0c2f7..219278c 100644 --- a/resources/lib/viervijfzes/content.py +++ b/resources/lib/viervijfzes/content.py @@ -13,6 +13,7 @@ from datetime import datetime import requests +from resources.lib import kodiutils from resources.lib.kodiutils import STREAM_DASH, STREAM_HLS, html_to_kodi from resources.lib.viervijfzes import ResolvedStream @@ -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_PREVENT = 3 # Don't use the cache +PROXIES = kodiutils.get_proxies() + class UnavailableException(Exception): """ Is thrown when an item is unavailable. """ @@ -396,7 +399,8 @@ class ContentApi: # 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')) + 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 @@ -409,7 +413,6 @@ class ContentApi: raise UnavailableException - def get_program_tree(self, cache=CACHE_AUTO): """ Get a content tree with information about all the programs. :type cache: str @@ -772,9 +775,9 @@ class ContentApi: if authentication: response = self._session.get(url, params=params, headers={ 'authorization': authentication, - }) + }, proxies=PROXIES) 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) @@ -791,9 +794,9 @@ class ContentApi: 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) + response = self._session.post(url, params=params, json=data, proxies=PROXIES) if response.status_code not in (200, 201): _LOGGER.error(response.text) @@ -810,9 +813,9 @@ class ContentApi: if authentication: response = self._session.delete(url, params=params, headers={ 'authorization': authentication, - }) + }, proxies=PROXIES) else: - response = self._session.delete(url, params=params) + response = self._session.delete(url, params=params, proxies=PROXIES) if response.status_code != 200: _LOGGER.error(response.text) diff --git a/resources/lib/viervijfzes/epg.py b/resources/lib/viervijfzes/epg.py index 6e616b1..6174adc 100644 --- a/resources/lib/viervijfzes/epg.py +++ b/resources/lib/viervijfzes/epg.py @@ -11,6 +11,8 @@ import dateutil.parser import dateutil.tz import requests +from resources.lib import kodiutils + _LOGGER = logging.getLogger(__name__) GENRE_MAPPING = { @@ -31,6 +33,8 @@ GENRE_MAPPING = { 'Voetbal': 0x43, } +PROXIES = kodiutils.get_proxies() + class EpgProgram: """ Defines a Program in the EPG. """ @@ -177,7 +181,7 @@ class EpgApi: :type url: str :rtype str """ - response = self._session.get(url) + response = self._session.get(url, proxies=PROXIES) if response.status_code != 200: raise Exception('Could not fetch data') diff --git a/resources/lib/viervijfzes/search.py b/resources/lib/viervijfzes/search.py index 15f401a..ed46b29 100644 --- a/resources/lib/viervijfzes/search.py +++ b/resources/lib/viervijfzes/search.py @@ -13,6 +13,8 @@ from resources.lib.viervijfzes.content import CACHE_ONLY, ContentApi, Program _LOGGER = logging.getLogger(__name__) +PROXIES = kodiutils.get_proxies() + class SearchApi: """ GoPlay Search API """ @@ -37,9 +39,9 @@ class SearchApi: "query": query, "page": 0, "mode": "programs" - } + }, + proxies=PROXIES ) - _LOGGER.debug(response.content) response.raise_for_status() data = json.loads(response.text) From dd1f49b362a29c434798081972138cb3bc1a5fd5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C3=ABl=20Arnauts?= Date: Fri, 21 Jul 2023 22:59:27 +0200 Subject: [PATCH 30/32] Drop testing for Python 2.7 (#127) * Drop testing for python 2.7 --- .github/workflows/ci.yml | 5 ----- scripts/build.py | 6 ++---- 2 files changed, 2 insertions(+), 9 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d2407b3..8a92efd 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -18,17 +18,12 @@ jobs: python-version: ["3.8", "3.9", "3.10"] include: # End-of-life Python versions are not available anymore with ubuntu-latest - - os: ubuntu-20.04 - python-version: "2.7" - os: ubuntu-20.04 python-version: "3.5" - os: ubuntu-20.04 python-version: "3.6" - os: ubuntu-20.04 python-version: "3.7" - # Kodi Leia on Windows uses a bundled Python 2.7. - - os: windows-latest - python-version: "2.7" steps: - name: Check out ${{ github.sha }} from repository ${{ github.repository }} uses: actions/checkout@v3 diff --git a/scripts/build.py b/scripts/build.py index a465f7a..af88180 100755 --- a/scripts/build.py +++ b/scripts/build.py @@ -5,10 +5,8 @@ from __future__ import absolute_import, division, unicode_literals import os import shutil -import sys import xml.etree.ElementTree as ET -BRANDS_DIR = 'brands' DIST_DIR = 'dist' @@ -85,5 +83,5 @@ if __name__ == '__main__': 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) + # 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) From e01b6b5c81c0ae15a6d85a63c3f2bfa57e8e523a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C3=ABl=20Arnauts?= Date: Wed, 26 Jul 2023 15:18:52 +0200 Subject: [PATCH 31/32] Prepare for v0.4.11 --- CHANGELOG.md | 13 +++++++++++++ addon.xml | 6 +++--- 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2651d9e..73c986e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,18 @@ # Changelog +## [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) diff --git a/addon.xml b/addon.xml index c82b208..ad5f3d6 100644 --- a/addon.xml +++ b/addon.xml @@ -1,5 +1,5 @@ - + @@ -21,8 +21,8 @@ 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. all GPL-3.0-only - v0.4.10 (2023-01-16) -- Update API + v0.4.11 (2023-07-26) +- Fixes for DRM protected VOD. https://github.com/add-ons/plugin.video.viervijfzes resources/icon.png From 0ddcdb1b0b8d2de62e53b1681a00bdb5750fb908 Mon Sep 17 00:00:00 2001 From: mediaminister Date: Thu, 21 Sep 2023 09:52:57 +0200 Subject: [PATCH 32/32] Add live channels --- .../resource.language.en_gb/strings.po | 4 ++ .../resource.language.nl_nl/strings.po | 4 ++ resources/lib/addon.py | 9 +-- resources/lib/modules/channels.py | 18 ++++++ resources/lib/modules/menu.py | 2 +- resources/lib/modules/player.py | 23 ++++--- resources/lib/viervijfzes/__init__.py | 5 +- resources/lib/viervijfzes/content.py | 61 ++++++++++++------- 8 files changed, 84 insertions(+), 42 deletions(-) diff --git a/resources/language/resource.language.en_gb/strings.po b/resources/language/resource.language.en_gb/strings.po index aedd6d9..5234c1b 100644 --- a/resources/language/resource.language.en_gb/strings.po +++ b/resources/language/resource.language.en_gb/strings.po @@ -60,6 +60,10 @@ msgstr "" ### SUBMENUS +msgctxt "#30052" +msgid "Watch live [B]{channel}[/B]" +msgstr "" + msgctxt "#30053" msgid "TV Guide for [B]{channel}[/B]" msgstr "" diff --git a/resources/language/resource.language.nl_nl/strings.po b/resources/language/resource.language.nl_nl/strings.po index 833e3d2..6c3a86f 100644 --- a/resources/language/resource.language.nl_nl/strings.po +++ b/resources/language/resource.language.nl_nl/strings.po @@ -61,6 +61,10 @@ msgstr "Tv-gids" ### SUBMENUS +msgctxt "#30052" +msgid "Watch live [B]{channel}[/B]" +msgstr "Kijk live [B]{channel}[/B]" + msgctxt "#30053" msgid "TV Guide for [B]{channel}[/B]" msgstr "Tv-gids voor [B]{channel}[/B]" diff --git a/resources/lib/addon.py b/resources/lib/addon.py index a55e072..bc403ea 100644 --- a/resources/lib/addon.py +++ b/resources/lib/addon.py @@ -160,14 +160,11 @@ def play_epg(channel, timestamp): @routing.route('/play/catalog') -@routing.route('/play/catalog/') -@routing.route('/play/catalog//') -def play_catalog(uuid=None, islongform=False): +@routing.route('/play/catalog//') +def play_catalog(uuid=None, content_type=None): """ Play the requested item """ - from ast import literal_eval from resources.lib.modules.player import Player - # Convert string to bool using literal_eval - Player().play(uuid, literal_eval(islongform)) + Player().play(uuid, content_type) @routing.route('/play/page/') diff --git a/resources/lib/modules/channels.py b/resources/lib/modules/channels.py index 88b4eee..a5482e6 100644 --- a/resources/lib/modules/channels.py +++ b/resources/lib/modules/channels.py @@ -71,9 +71,27 @@ class Channels: # 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')) + icon = '{path}/resources/logos/{logo}'.format(path=kodiutils.addon_path(), logo=channel_info.get('logo')) 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'): listing.append( TitleItem( diff --git a/resources/lib/modules/menu.py b/resources/lib/modules/menu.py index 0bacfff..c30ecd0 100644 --- a/resources/lib/modules/menu.py +++ b/resources/lib/modules/menu.py @@ -183,7 +183,7 @@ class Menu: if item.uuid: # We have an UUID and can play this item directly - path = kodiutils.url_for('play_catalog', uuid=item.uuid, islongform=item.islongform) + path = kodiutils.url_for('play_catalog', uuid=item.uuid, content_type=item.content_type) else: # 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='')) diff --git a/resources/lib/modules/player.py b/resources/lib/modules/player.py index 5497f93..e03329e 100644 --- a/resources/lib/modules/player.py +++ b/resources/lib/modules/player.py @@ -26,8 +26,7 @@ class Player: # Workaround for Raspberry Pi 3 and older kodiutils.set_global_setting('videoplayer.useomxplayer', True) - @staticmethod - def live(channel): + def live(self, channel): """ Play the live channel. :type channel: string """ @@ -38,9 +37,9 @@ class Player: # self.play_from_page(broadcast.video_url) # return - channel_name = CHANNELS.get(channel, {'name': channel}) - kodiutils.ok_dialog(message=kodiutils.localize(30718, channel=channel_name.get('name'))) # There is no live stream available for {channel}. - kodiutils.end_of_directory() + channel_url = CHANNELS.get(channel, {'url': channel}).get('url') + + self.play_from_page(channel_url) def play_from_page(self, path): """ Play the requested item. @@ -69,7 +68,7 @@ class Player: if episode.uuid: # Lookup the stream - resolved_stream = self._resolve_stream(episode.uuid, episode.islongform) + resolved_stream = self._resolve_stream(episode.uuid, episode.content_type) _LOGGER.debug('Resolved stream: %s', resolved_stream) if resolved_stream: @@ -81,24 +80,24 @@ class Player: art_dict=titleitem.art_dict, prop_dict=titleitem.prop_dict) - def play(self, uuid, islongform): + def play(self, uuid, content_type): """ Play the requested item. :type uuid: string - :type islongform: bool + :type content_type: string """ if not uuid: kodiutils.ok_dialog(message=kodiutils.localize(30712)) # The video is unavailable... return # Lookup the stream - resolved_stream = self._resolve_stream(uuid, islongform) + resolved_stream = self._resolve_stream(uuid, content_type) kodiutils.play(resolved_stream.url, resolved_stream.stream_type, resolved_stream.license_key) @staticmethod - def _resolve_stream(uuid, islongform): + def _resolve_stream(uuid, content_type): """ Resolve the stream for the requested item :type uuid: string - :type islongform: bool + :type content_type: string """ try: # Check if we have credentials @@ -115,7 +114,7 @@ class Player: auth = AuthApi(kodiutils.get_setting('username'), kodiutils.get_setting('password'), kodiutils.get_tokens_path()) # Get stream information - resolved_stream = ContentApi(auth).get_stream_by_uuid(uuid, islongform) + resolved_stream = ContentApi(auth).get_stream_by_uuid(uuid, content_type) return resolved_stream except (InvalidLoginException, AuthenticationException) as ex: diff --git a/resources/lib/viervijfzes/__init__.py b/resources/lib/viervijfzes/__init__.py index 91fe30b..16947b0 100644 --- a/resources/lib/viervijfzes/__init__.py +++ b/resources/lib/viervijfzes/__init__.py @@ -7,6 +7,7 @@ from collections import OrderedDict CHANNELS = OrderedDict([ ('Play4', { 'name': 'Play4', + 'url': 'live-kijken/play-4', 'epg_id': 'vier', 'logo': 'play4.png', 'background': 'play4-background.png', @@ -18,6 +19,7 @@ CHANNELS = OrderedDict([ }), ('Play5', { 'name': 'Play5', + 'url': 'live-kijken/play-5', 'epg_id': 'vijf', 'logo': 'play5.png', 'background': 'play5-background.png', @@ -29,6 +31,7 @@ CHANNELS = OrderedDict([ }), ('Play6', { 'name': 'Play6', + 'url': 'live-kijken/play-6', 'epg_id': 'zes', 'logo': 'play6.png', 'background': 'play6-background.png', @@ -40,8 +43,8 @@ CHANNELS = OrderedDict([ }), ('Play7', { 'name': 'Play7', + 'url': 'live-kijken/play-7', 'epg_id': 'zeven', - 'url': 'https://www.goplay.be', 'logo': 'play7.png', 'background': 'play7-background.png', 'iptv_preset': 17, diff --git a/resources/lib/viervijfzes/content.py b/resources/lib/viervijfzes/content.py index 219278c..6b15885 100644 --- a/resources/lib/viervijfzes/content.py +++ b/resources/lib/viervijfzes/content.py @@ -112,7 +112,7 @@ class 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, - season=None, season_uuid=None, number=None, rating=None, aired=None, expiry=None, stream=None, islongform=False): + season=None, season_uuid=None, number=None, rating=None, aired=None, expiry=None, stream=None, content_type=None): """ :type uuid: str :type nodeid: str @@ -130,7 +130,7 @@ class Episode: :type aired: datetime :type expiry: datetime :type stream: string - :type islongform: bool + :type content_type: string """ self.uuid = uuid self.nodeid = nodeid @@ -148,7 +148,7 @@ class Episode: self.aired = aired self.expiry = expiry self.stream = stream - self.islongform = islongform + self.content_type = content_type def __repr__(self): return "%r" % self.__dict__ @@ -338,6 +338,14 @@ class ContentApi: if not data: 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']: # We have found detailed episode information episode = self._parse_clip_data(data['video']) @@ -353,14 +361,19 @@ class ContentApi: return None - def get_stream_by_uuid(self, uuid, islongform): + def get_stream_by_uuid(self, uuid, content_type): """ Return a ResolvedStream for this video. - :type uuid: str - :type islongform: bool + :type uuid: string + :type content_type: string :rtype: ResolvedStream """ - mode = 'long-form' if islongform else 'short-form' - response = self._get_url(self.API_GOPLAY + '/web/v1/videos/%s/%s' % (mode, uuid), authentication='Bearer %s' % self._auth.get_token()) + 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) if not data: @@ -482,8 +495,8 @@ class ContentApi: raw_html = self._get_url(self.SITE_URL) # Categories regexes - regex_articles = re.compile(r']+>(.*?)', re.DOTALL) - regex_category = re.compile(r'(.*?)(?:.*?
(.*?)
)?', re.DOTALL) + regex_articles = re.compile(r']+>([\s\S]*?)', re.DOTALL) + regex_category = re.compile(r'(.*?)(?:.*?
(.*?)
)?', re.DOTALL) categories = [] for result in regex_articles.finditer(raw_html): @@ -492,9 +505,9 @@ class ContentApi: match_category = regex_category.search(article_html) category_title = None if match_category: - category_title = match_category.group(1).strip() + category_title = unescape(match_category.group(1).strip()) 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 @@ -547,8 +560,8 @@ class ContentApi: :rtype list[Program] """ # Item regexes - regex_item = re.compile(r']+?href="(?P[^"]+)"[^>]+?>' - r'.*?

(?P[^<]*)</h3>.*?data-background-image="(?P<image>.*?)".*?' + regex_item = re.compile(r'<a[^>]+?href=\"(?P<path>[^\"]+)\"[^>]+?>' + r'[\s\S]*?<h3 class=\"poster-teaser__title\">(?P<title>[^<]*)</h3>[\s\S]*?poster-teaser__image\" src=\"(?P<image>[\s\S]*?)\"[\s\S]*?' r'</a>', re.DOTALL) # Extract items @@ -574,20 +587,21 @@ class ContentApi: :rtype list[Episode] """ # 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_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_video_id = re.compile(r'data-video-id="([^"]*)"') - regex_episode_image = re.compile(r'data-background-image="([^"]*)"') - regex_episode_badge = re.compile(r'<div class="(?:poster|card|image|episode)-teaser__badge badge">([^<]*)</div>') + 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_duration = re.compile(r'data-duration=\"([^\"]*)\"') + regex_episode_video_id = re.compile(r'data-video-id=\"([^\"]*)\"') + regex_episode_image = re.compile(r'<img class=\"episode-teaser__header\" src=\"([^<\"]*)\"') + 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 episodes = [] for item in regex_item.finditer(html): item_html = item.group(0) path = item.group('path') + item_type = item.group('item_type') # Extract title try: @@ -632,6 +646,8 @@ class ContentApi: if 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 episodes.append(Episode( path=path.lstrip('/'), @@ -642,6 +658,7 @@ class ContentApi: uuid=episode_video_id, thumb=episode_image, program_title=episode_program, + content_type=content_type )) return episodes @@ -721,7 +738,7 @@ class ContentApi: expiry=datetime.fromtimestamp(int(data.get('unpublishDate'))) if data.get('unpublishDate') else None, rating=data.get('parentalRating'), stream=data.get('path'), - islongform=data.get('isLongForm'), + content_type=data.get('type'), ) return episode