Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions plugin.video.vrt.nu/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,9 @@ leave a message at [our Facebook page](https://facebook.com/kodivrtnu/).
</table>

## Releases
### v2.5.38 (2025-05-12)
- Fix Ketnet Jr livestream (@mediaminister)

### v2.5.37 (2025-04-04)
- Fix DRM streams (@mediaminister)

Expand Down
5 changes: 4 additions & 1 deletion plugin.video.vrt.nu/addon.xml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<addon id="plugin.video.vrt.nu" name="VRT MAX" version="2.5.37+matrix.1" provider-name="Martijn Moreel, dagwieers, mediaminister">
<addon id="plugin.video.vrt.nu" name="VRT MAX" version="2.5.38+matrix.1" provider-name="Martijn Moreel, dagwieers, mediaminister">
<requires>
<import addon="resource.images.studios.white" version="0.0.22"/>
<import addon="script.module.beautifulsoup4" version="4.6.2"/>
Expand Down Expand Up @@ -42,6 +42,9 @@
<website>https://github.com/add-ons/plugin.video.vrt.nu/wiki</website>
<source>https://github.com/add-ons/plugin.video.vrt.nu</source>
<news>
v2.5.38 (2025-05-12)
- Fix Ketnet Jr livestream

v2.5.37 (2025-04-04)
- Fix DRM streams

Expand Down
6 changes: 5 additions & 1 deletion plugin.video.vrt.nu/resources/lib/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -1084,7 +1084,7 @@ def get_latest_episode(program_name):
if most_relevant_ep and most_relevant_ep.get('title') == 'Meest recente aflevering':
latest_episode = most_relevant_ep
else:
items = page.get('components')[1].get('items')[0].get('components')[0]
items = page.get('components')[0].get('items')[0].get('components')[0]
if not items.get('paginatedItems'):
items = items.get('items')[0].get('components')[0]
edges = items.get('paginatedItems').get('edges')
Expand Down Expand Up @@ -1256,6 +1256,8 @@ def get_episodes(program_name, season_name=None, end_cursor=''):
if program_name and season_name:
if season_name.startswith('parsys'):
list_id = 'static:/vrtnu/a-z/{}.model.json@{}'.format(program_name, season_name)
elif season_name.startswith('dynamic_'):
list_id = 'dynamic:/vrtnu/a-z/{}.model.json@{}'.format(program_name, season_name.split('dynamic_')[1])
else:
list_id = 'static:/vrtnu/a-z/{}/{}.episodes-list.json'.format(program_name, season_name)
api_data = get_paginated_episodes(list_id=list_id, page_size=page_size, end_cursor=end_cursor)
Expand Down Expand Up @@ -1309,6 +1311,8 @@ def create_season_dict(data_json):
# season name
if '.episodes-list.json' in list_id:
season_dict['name'] = list_id.split('.episodes-list.json')[0].split('/')[-1]
elif list_id.startswith('dynamic:/'):
season_dict['name'] = 'dynamic_' + list_id.split('@')[-1]
else:
season_dict['name'] = list_id.split('@')[-1]
return season_dict
Expand Down
4 changes: 2 additions & 2 deletions plugin.video.vrt.nu/resources/lib/data.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
{'name': 'Cultuur', 'id': 'cultuur', 'msgctxt': 30071},
{'name': 'Docu', 'id': 'docu', 'msgctxt': 30072},
{'name': 'Entertainment', 'id': 'entertainment', 'msgctxt': 30073},
{'name': 'Film', 'id': 'films', 'msgctxt': 30074},
{'name': 'Films', 'id': 'films', 'msgctxt': 30074},
{'name': 'Human interest', 'id': 'human-interest', 'msgctxt': 30075},
{'name': 'Humor', 'id': 'humor', 'msgctxt': 30076},
{'name': 'Kinderen en jongeren', 'id': 'voor-kinderen', 'msgctxt': 30077},
Expand All @@ -28,7 +28,7 @@
{'name': 'Sport', 'id': 'sport', 'msgctxt': 30083},
{'name': 'Talkshows', 'id': 'talkshows', 'msgctxt': 30084},
{'name': 'Vlaamse Gebarentaal', 'id': 'met-gebarentaal', 'msgctxt': 30085},
{'name': 'Wetenschap & natuur', 'id': 'wetenschap-en-natuur', 'msgctxt': 30086},
{'name': 'Wetenschap en natuur', 'id': 'wetenschap-en-natuur', 'msgctxt': 30086},
]

# TODO: Find a solution for the below VRT YouTube channels
Expand Down
3 changes: 0 additions & 3 deletions plugin.video.vrt.nu/resources/lib/streamservice.py
Original file line number Diff line number Diff line change
Expand Up @@ -193,9 +193,6 @@ def get_stream(self, video, roaming=False, api_data=None):
querystring = ''
manifest_url = '{}?t={}-{}{}'.format(uri, video.get('start_date'), video.get('end_date'), querystring)

# FIXME: Remove '#' URI fragment identifier from manifest url because InputStream Adaptive refuses to play these urls
manifest_url = manifest_url.replace('#', '')

# Fix virtual subclip
from datetime import timedelta
duration = timedelta(milliseconds=stream_json.get('duration', 0))
Expand Down
156 changes: 90 additions & 66 deletions plugin.video.vrt.nu/resources/lib/tokenresolver.py
Original file line number Diff line number Diff line change
Expand Up @@ -279,6 +279,7 @@ def _get_playertoken(self, variant, roaming):

@staticmethod
def _generate_playerinfo():
"""Generate playerinfo json for playertoken request"""
import time
from json import dumps
import base64
Expand All @@ -287,73 +288,96 @@ def _generate_playerinfo():
import re

playerinfo = None
data = None

# Get data from player javascript
player_url = 'https://player.vrt.be/vrtnu/js/main.js'
crypt_data = None
while not crypt_data:
response = open_url(player_url)
if response:
data = response.read().decode('utf-8')

if data:
# Extract JWT key id and secret
crypt_rx = re.compile(r'atob\(\"(==[A-Za-z0-9+/]*)\"')
crypt_data = re.findall(crypt_rx, data)
if not crypt_data:
# Try redirect
redirect_rx = re.compile(r"'([a-z]+\.[a-z0-9]{20}\.js)';")
redirect_path = re.search(redirect_rx, data)
if redirect_path:
player_url = '{}/{}'.format(player_url[:player_url.rfind('/')], redirect_path.group(1))
else:
return playerinfo

kid_source = crypt_data[0]
secret_source = crypt_data[-1]
kid = base64.b64decode(kid_source[::-1]).decode('utf-8')
secret = base64.b64decode(secret_source[::-1]).decode('utf-8')

# Extract player version
player_version = '3.1.1'
pv_rx = re.compile(r'playerVersion:\"(\S*)\"')
match = re.search(pv_rx, data)
if match:
player_version = match.group(1)

# Generate JWT
segments = []
header = {
'alg': 'HS256',
'kid': kid
}
payload = {
'exp': time.time() + 1000,
'platform': 'desktop',
'app': {
'type': 'browser',
'name': 'Firefox',
'version': '114.0'
},
'device': 'undefined (undefined)',
'os': {
'name': 'Linux',
'version': 'x86_64'
},
'player': {
'name': 'VRT web player',
'version': player_version
seen_urls = set()
result_urls = []
base_url = 'https://player.vrt.be/vrtmax/js/player-lib.js'
folder = '/'.join(base_url.split('/')[:-1])
player_version = '5.2.2'
atobs = None
kid = None
secret = None

main_script = open_url(base_url).read().decode('utf-8')
first_level_pattern = r'"\.(/[a-z0-9-/]+[a-z0-9]{8}\.js)";'
first_level_paths = re.findall(first_level_pattern, main_script)

for path in first_level_paths:
script_url = folder + path
if script_url in seen_urls:
continue
seen_urls.add(script_url)

if 'drm' in script_url or 'bootstrapper' in script_url:
result_urls.append(script_url)

script_content = open_url(script_url).read().decode('utf-8')
second_level_pattern = r'import\(\"\.(/[a-z0-9-]+\.js)\"\)'
second_level_paths = re.findall(second_level_pattern, script_content)

for sub_path in second_level_paths:
sub_script_url = folder + sub_path
if sub_script_url in seen_urls:
continue
seen_urls.add(sub_script_url)

if 'drm' in sub_script_url or 'bootstrapper' in sub_script_url:
result_urls.append(sub_script_url)

pattern_version = re.compile(r'\s"(\d{1}\.\d{1}\.\d{1}-[a-zA-Z0-9\-:]*)"')
pattern_atob = re.compile(r'atob\(\"(==[A-Za-z0-9+/]*)\"')

for url in result_urls:
content = open_url(url).read().decode('utf-8')
version_match = pattern_version.search(content)
if version_match:
player_version = version_match.group(1)
atobs = pattern_atob.findall(content)

if atobs:
# first atob reversed
kid = base64.b64decode(atobs[0][::-1]).decode('utf-8')
# last atob reversed
secret = base64.b64decode(atobs[-1][::-1]).decode('utf-8')

log(2, kid)
log(2, secret)

# Generate JWT
segments = []
header = {
'alg': 'HS256',
'kid': kid
}
}
json_header = dumps(header).encode()
json_payload = dumps(payload).encode()
segments.append(base64.urlsafe_b64encode(json_header).decode('utf-8').replace('=', ''))
segments.append(base64.urlsafe_b64encode(json_payload).decode('utf-8').replace('=', ''))
signing_input = '.'.join(segments).encode()
signature = hmac.new(secret.encode(), signing_input, hashlib.sha256).digest()
segments.append(base64.urlsafe_b64encode(signature).decode('utf-8').replace('=', ''))
playerinfo = '.'.join(segments)
payload = {
'drm': {
'widevine': 'L3'
},
'exp': round(time.time() + 3600, 3),
'platform': 'desktop',
'app': {
'type': 'browser',
'name': 'Firefox',
'version': '137.0',
},
'device': 'undefined (undefined)',
'os': {
'name': 'Linux',
'version': 'x86_64',
},
'player': {
'name': 'VRT web player',
'version': player_version,
}
}
json_header = dumps(header).encode()
json_payload = dumps(payload).encode()
segments.append(base64.urlsafe_b64encode(json_header).rstrip(b'=').decode('utf-8'))
segments.append(base64.urlsafe_b64encode(json_payload).rstrip(b'=').decode('utf-8'))
signing_input = '.'.join(segments).encode()
signature = hmac.new(secret.encode(), signing_input, hashlib.sha256).digest()
segments.append(base64.urlsafe_b64encode(signature).rstrip(b'=').decode('utf-8'))
playerinfo = '.'.join(segments)
log(2, playerinfo)
return playerinfo

def delete_tokens(self):
Expand Down
Binary file modified plugin.video.vrt.nu/resources/media/screenshot01.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified plugin.video.vrt.nu/resources/media/screenshot02.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified plugin.video.vrt.nu/resources/media/screenshot03.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified plugin.video.vrt.nu/resources/media/screenshot04.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.