Skip to content

Commit b65d608

Browse files
authored
Merge pull request #4665 from mediaminister/plugin.video.vrt.nu@matrix
[plugin.video.vrt.nu@matrix] 2.5.38+matrix.1
2 parents 8b7df01 + 6399dd0 commit b65d608

10 files changed

Lines changed: 104 additions & 73 deletions

File tree

plugin.video.vrt.nu/README.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,9 @@ leave a message at [our Facebook page](https://facebook.com/kodivrtnu/).
5959
</table>
6060

6161
## Releases
62+
### v2.5.38 (2025-05-12)
63+
- Fix Ketnet Jr livestream (@mediaminister)
64+
6265
### v2.5.37 (2025-04-04)
6366
- Fix DRM streams (@mediaminister)
6467

plugin.video.vrt.nu/addon.xml

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
2-
<addon id="plugin.video.vrt.nu" name="VRT MAX" version="2.5.37+matrix.1" provider-name="Martijn Moreel, dagwieers, mediaminister">
2+
<addon id="plugin.video.vrt.nu" name="VRT MAX" version="2.5.38+matrix.1" provider-name="Martijn Moreel, dagwieers, mediaminister">
33
<requires>
44
<import addon="resource.images.studios.white" version="0.0.22"/>
55
<import addon="script.module.beautifulsoup4" version="4.6.2"/>
@@ -42,6 +42,9 @@
4242
<website>https://github.com/add-ons/plugin.video.vrt.nu/wiki</website>
4343
<source>https://github.com/add-ons/plugin.video.vrt.nu</source>
4444
<news>
45+
v2.5.38 (2025-05-12)
46+
- Fix Ketnet Jr livestream
47+
4548
v2.5.37 (2025-04-04)
4649
- Fix DRM streams
4750

plugin.video.vrt.nu/resources/lib/api.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1084,7 +1084,7 @@ def get_latest_episode(program_name):
10841084
if most_relevant_ep and most_relevant_ep.get('title') == 'Meest recente aflevering':
10851085
latest_episode = most_relevant_ep
10861086
else:
1087-
items = page.get('components')[1].get('items')[0].get('components')[0]
1087+
items = page.get('components')[0].get('items')[0].get('components')[0]
10881088
if not items.get('paginatedItems'):
10891089
items = items.get('items')[0].get('components')[0]
10901090
edges = items.get('paginatedItems').get('edges')
@@ -1256,6 +1256,8 @@ def get_episodes(program_name, season_name=None, end_cursor=''):
12561256
if program_name and season_name:
12571257
if season_name.startswith('parsys'):
12581258
list_id = 'static:/vrtnu/a-z/{}.model.json@{}'.format(program_name, season_name)
1259+
elif season_name.startswith('dynamic_'):
1260+
list_id = 'dynamic:/vrtnu/a-z/{}.model.json@{}'.format(program_name, season_name.split('dynamic_')[1])
12591261
else:
12601262
list_id = 'static:/vrtnu/a-z/{}/{}.episodes-list.json'.format(program_name, season_name)
12611263
api_data = get_paginated_episodes(list_id=list_id, page_size=page_size, end_cursor=end_cursor)
@@ -1309,6 +1311,8 @@ def create_season_dict(data_json):
13091311
# season name
13101312
if '.episodes-list.json' in list_id:
13111313
season_dict['name'] = list_id.split('.episodes-list.json')[0].split('/')[-1]
1314+
elif list_id.startswith('dynamic:/'):
1315+
season_dict['name'] = 'dynamic_' + list_id.split('@')[-1]
13121316
else:
13131317
season_dict['name'] = list_id.split('@')[-1]
13141318
return season_dict

plugin.video.vrt.nu/resources/lib/data.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
{'name': 'Cultuur', 'id': 'cultuur', 'msgctxt': 30071},
1515
{'name': 'Docu', 'id': 'docu', 'msgctxt': 30072},
1616
{'name': 'Entertainment', 'id': 'entertainment', 'msgctxt': 30073},
17-
{'name': 'Film', 'id': 'films', 'msgctxt': 30074},
17+
{'name': 'Films', 'id': 'films', 'msgctxt': 30074},
1818
{'name': 'Human interest', 'id': 'human-interest', 'msgctxt': 30075},
1919
{'name': 'Humor', 'id': 'humor', 'msgctxt': 30076},
2020
{'name': 'Kinderen en jongeren', 'id': 'voor-kinderen', 'msgctxt': 30077},
@@ -28,7 +28,7 @@
2828
{'name': 'Sport', 'id': 'sport', 'msgctxt': 30083},
2929
{'name': 'Talkshows', 'id': 'talkshows', 'msgctxt': 30084},
3030
{'name': 'Vlaamse Gebarentaal', 'id': 'met-gebarentaal', 'msgctxt': 30085},
31-
{'name': 'Wetenschap & natuur', 'id': 'wetenschap-en-natuur', 'msgctxt': 30086},
31+
{'name': 'Wetenschap en natuur', 'id': 'wetenschap-en-natuur', 'msgctxt': 30086},
3232
]
3333

3434
# TODO: Find a solution for the below VRT YouTube channels

plugin.video.vrt.nu/resources/lib/streamservice.py

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -193,9 +193,6 @@ def get_stream(self, video, roaming=False, api_data=None):
193193
querystring = ''
194194
manifest_url = '{}?t={}-{}{}'.format(uri, video.get('start_date'), video.get('end_date'), querystring)
195195

196-
# FIXME: Remove '#' URI fragment identifier from manifest url because InputStream Adaptive refuses to play these urls
197-
manifest_url = manifest_url.replace('#', '')
198-
199196
# Fix virtual subclip
200197
from datetime import timedelta
201198
duration = timedelta(milliseconds=stream_json.get('duration', 0))

plugin.video.vrt.nu/resources/lib/tokenresolver.py

Lines changed: 90 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -279,6 +279,7 @@ def _get_playertoken(self, variant, roaming):
279279

280280
@staticmethod
281281
def _generate_playerinfo():
282+
"""Generate playerinfo json for playertoken request"""
282283
import time
283284
from json import dumps
284285
import base64
@@ -287,73 +288,96 @@ def _generate_playerinfo():
287288
import re
288289

289290
playerinfo = None
290-
data = None
291-
292-
# Get data from player javascript
293-
player_url = 'https://player.vrt.be/vrtnu/js/main.js'
294-
crypt_data = None
295-
while not crypt_data:
296-
response = open_url(player_url)
297-
if response:
298-
data = response.read().decode('utf-8')
299-
300-
if data:
301-
# Extract JWT key id and secret
302-
crypt_rx = re.compile(r'atob\(\"(==[A-Za-z0-9+/]*)\"')
303-
crypt_data = re.findall(crypt_rx, data)
304-
if not crypt_data:
305-
# Try redirect
306-
redirect_rx = re.compile(r"'([a-z]+\.[a-z0-9]{20}\.js)';")
307-
redirect_path = re.search(redirect_rx, data)
308-
if redirect_path:
309-
player_url = '{}/{}'.format(player_url[:player_url.rfind('/')], redirect_path.group(1))
310-
else:
311-
return playerinfo
312-
313-
kid_source = crypt_data[0]
314-
secret_source = crypt_data[-1]
315-
kid = base64.b64decode(kid_source[::-1]).decode('utf-8')
316-
secret = base64.b64decode(secret_source[::-1]).decode('utf-8')
317-
318-
# Extract player version
319-
player_version = '3.1.1'
320-
pv_rx = re.compile(r'playerVersion:\"(\S*)\"')
321-
match = re.search(pv_rx, data)
322-
if match:
323-
player_version = match.group(1)
324-
325-
# Generate JWT
326-
segments = []
327-
header = {
328-
'alg': 'HS256',
329-
'kid': kid
330-
}
331-
payload = {
332-
'exp': time.time() + 1000,
333-
'platform': 'desktop',
334-
'app': {
335-
'type': 'browser',
336-
'name': 'Firefox',
337-
'version': '114.0'
338-
},
339-
'device': 'undefined (undefined)',
340-
'os': {
341-
'name': 'Linux',
342-
'version': 'x86_64'
343-
},
344-
'player': {
345-
'name': 'VRT web player',
346-
'version': player_version
291+
seen_urls = set()
292+
result_urls = []
293+
base_url = 'https://player.vrt.be/vrtmax/js/player-lib.js'
294+
folder = '/'.join(base_url.split('/')[:-1])
295+
player_version = '5.2.2'
296+
atobs = None
297+
kid = None
298+
secret = None
299+
300+
main_script = open_url(base_url).read().decode('utf-8')
301+
first_level_pattern = r'"\.(/[a-z0-9-/]+[a-z0-9]{8}\.js)";'
302+
first_level_paths = re.findall(first_level_pattern, main_script)
303+
304+
for path in first_level_paths:
305+
script_url = folder + path
306+
if script_url in seen_urls:
307+
continue
308+
seen_urls.add(script_url)
309+
310+
if 'drm' in script_url or 'bootstrapper' in script_url:
311+
result_urls.append(script_url)
312+
313+
script_content = open_url(script_url).read().decode('utf-8')
314+
second_level_pattern = r'import\(\"\.(/[a-z0-9-]+\.js)\"\)'
315+
second_level_paths = re.findall(second_level_pattern, script_content)
316+
317+
for sub_path in second_level_paths:
318+
sub_script_url = folder + sub_path
319+
if sub_script_url in seen_urls:
320+
continue
321+
seen_urls.add(sub_script_url)
322+
323+
if 'drm' in sub_script_url or 'bootstrapper' in sub_script_url:
324+
result_urls.append(sub_script_url)
325+
326+
pattern_version = re.compile(r'\s"(\d{1}\.\d{1}\.\d{1}-[a-zA-Z0-9\-:]*)"')
327+
pattern_atob = re.compile(r'atob\(\"(==[A-Za-z0-9+/]*)\"')
328+
329+
for url in result_urls:
330+
content = open_url(url).read().decode('utf-8')
331+
version_match = pattern_version.search(content)
332+
if version_match:
333+
player_version = version_match.group(1)
334+
atobs = pattern_atob.findall(content)
335+
336+
if atobs:
337+
# first atob reversed
338+
kid = base64.b64decode(atobs[0][::-1]).decode('utf-8')
339+
# last atob reversed
340+
secret = base64.b64decode(atobs[-1][::-1]).decode('utf-8')
341+
342+
log(2, kid)
343+
log(2, secret)
344+
345+
# Generate JWT
346+
segments = []
347+
header = {
348+
'alg': 'HS256',
349+
'kid': kid
347350
}
348-
}
349-
json_header = dumps(header).encode()
350-
json_payload = dumps(payload).encode()
351-
segments.append(base64.urlsafe_b64encode(json_header).decode('utf-8').replace('=', ''))
352-
segments.append(base64.urlsafe_b64encode(json_payload).decode('utf-8').replace('=', ''))
353-
signing_input = '.'.join(segments).encode()
354-
signature = hmac.new(secret.encode(), signing_input, hashlib.sha256).digest()
355-
segments.append(base64.urlsafe_b64encode(signature).decode('utf-8').replace('=', ''))
356-
playerinfo = '.'.join(segments)
351+
payload = {
352+
'drm': {
353+
'widevine': 'L3'
354+
},
355+
'exp': round(time.time() + 3600, 3),
356+
'platform': 'desktop',
357+
'app': {
358+
'type': 'browser',
359+
'name': 'Firefox',
360+
'version': '137.0',
361+
},
362+
'device': 'undefined (undefined)',
363+
'os': {
364+
'name': 'Linux',
365+
'version': 'x86_64',
366+
},
367+
'player': {
368+
'name': 'VRT web player',
369+
'version': player_version,
370+
}
371+
}
372+
json_header = dumps(header).encode()
373+
json_payload = dumps(payload).encode()
374+
segments.append(base64.urlsafe_b64encode(json_header).rstrip(b'=').decode('utf-8'))
375+
segments.append(base64.urlsafe_b64encode(json_payload).rstrip(b'=').decode('utf-8'))
376+
signing_input = '.'.join(segments).encode()
377+
signature = hmac.new(secret.encode(), signing_input, hashlib.sha256).digest()
378+
segments.append(base64.urlsafe_b64encode(signature).rstrip(b'=').decode('utf-8'))
379+
playerinfo = '.'.join(segments)
380+
log(2, playerinfo)
357381
return playerinfo
358382

359383
def delete_tokens(self):
261 KB
Loading
62.3 KB
Loading
246 KB
Loading
139 KB
Loading

0 commit comments

Comments
 (0)