7 from .gigya
import GigyaBaseIE
14 get_element_html_by_class
,
29 class VRTBaseIE(GigyaBaseIE
):
32 'platform': 'desktop',
37 'device': 'undefined (undefined)',
43 'name': 'VRT web player',
44 'version': '2.7.4-prod-2023-04-19T06:05:45'
47 # From https://player.vrt.be/vrtnws/js/main.js & https://player.vrt.be/ketnet/js/main.fd1de01a40a1e3d842ea.js
48 _JWT_KEY_ID
= '0-0Fp51UZykfaiCJrfTE3+oMI8zvDteYfPtR+2n1R+z8w='
49 _JWT_SIGNING_KEY
= '2a9251d782700769fb856da5725daf38661874ca6f80ae7dc2b05ec1a81a24ae'
51 def _extract_formats_and_subtitles(self
, data
, video_id
):
52 if traverse_obj(data
, 'drm'):
53 self
.report_drm(video_id
)
55 formats
, subtitles
= [], {}
56 for target
in traverse_obj(data
, ('targetUrls', lambda _
, v
: url_or_none(v
['url']) and v
['type'])):
57 format_type
= target
['type'].upper()
58 format_url
= target
['url']
59 if format_type
in ('HLS', 'HLS_AES'):
60 fmts
, subs
= self
._extract
_m
3u8_formats
_and
_subtitles
(
61 format_url
, video_id
, 'mp4', m3u8_id
=format_type
, fatal
=False)
63 self
._merge
_subtitles
(subs
, target
=subtitles
)
64 elif format_type
== 'HDS':
65 formats
.extend(self
._extract
_f
4m
_formats
(
66 format_url
, video_id
, f4m_id
=format_type
, fatal
=False))
67 elif format_type
== 'MPEG_DASH':
68 fmts
, subs
= self
._extract
_mpd
_formats
_and
_subtitles
(
69 format_url
, video_id
, mpd_id
=format_type
, fatal
=False)
71 self
._merge
_subtitles
(subs
, target
=subtitles
)
72 elif format_type
== 'HSS':
73 fmts
, subs
= self
._extract
_ism
_formats
_and
_subtitles
(
74 format_url
, video_id
, ism_id
='mss', fatal
=False)
76 self
._merge
_subtitles
(subs
, target
=subtitles
)
79 'format_id': format_type
,
83 for sub
in traverse_obj(data
, ('subtitleUrls', lambda _
, v
: v
['url'] and v
['type'] == 'CLOSED')):
84 subtitles
.setdefault('nl', []).append({'url': sub['url']}
)
86 return formats
, subtitles
88 def _call_api(self
, video_id
, client
='null', id_token
=None, version
='v2'):
89 player_info
= {'exp': (round(time.time(), 3) + 900), **self._PLAYER_INFO}
90 player_token
= self
._download
_json
(
91 'https://media-services-public.vrt.be/vualto-video-aggregator-web/rest/external/v2/tokens',
92 video_id
, 'Downloading player token', headers
={
93 **self
.geo_verification_headers(),
94 'Content-Type': 'application/json',
96 'identityToken': id_token
or {},
97 'playerInfo': jwt_encode_hs256(player_info
, self
._JWT
_SIGNING
_KEY
, headers
={
98 'kid': self
._JWT
_KEY
_ID
100 }, separators
=(',', ':')).encode())['vrtPlayerToken']
102 return self
._download
_json
(
103 f
'https://media-services-public.vrt.be/media-aggregator/{version}/media-items/{video_id}',
104 video_id
, 'Downloading API JSON', query
={
105 'vrtPlayerToken': player_token
,
107 }, expected_status
=400)
110 class VRTIE(VRTBaseIE
):
111 IE_DESC
= 'VRT NWS, Flanders News, Flandern Info and Sporza'
112 _VALID_URL
= r
'https?://(?:www\.)?(?P<site>vrt\.be/vrtnws|sporza\.be)/[a-z]{2}/\d{4}/\d{2}/\d{2}/(?P<id>[^/?&#]+)'
114 'url': 'https://www.vrt.be/vrtnws/nl/2019/05/15/beelden-van-binnenkant-notre-dame-een-maand-na-de-brand/',
116 'id': 'pbs-pub-7855fc7b-1448-49bc-b073-316cb60caa71$vid-2ca50305-c38a-4762-9890-65cbd098b7bd',
118 'title': 'Beelden van binnenkant Notre-Dame, één maand na de brand',
119 'description': 'md5:6fd85f999b2d1841aa5568f4bf02c3ff',
121 'thumbnail': 'https://images.vrt.be/orig/2019/05/15/2d914d61-7710-11e9-abcc-02b7b76bf47f.jpg',
123 'params': {'skip_download': 'm3u8'}
,
125 'url': 'https://sporza.be/nl/2019/05/15/de-belgian-cats-zijn-klaar-voor-het-ek/',
127 'id': 'pbs-pub-f2c86a46-8138-413a-a4b9-a0015a16ce2c$vid-1f112b31-e58e-4379-908d-aca6d80f8818',
129 'title': 'De Belgian Cats zijn klaar voor het EK',
130 'description': 'Video: De Belgian Cats zijn klaar voor het EK mét Ann Wauters | basketbal, sport in het journaal',
132 'thumbnail': 'https://images.vrt.be/orig/2019/05/15/11c0dba3-770e-11e9-abcc-02b7b76bf47f.jpg',
134 'params': {'skip_download': 'm3u8'}
,
137 'vrt.be/vrtnws': 'vrtnieuws',
138 'sporza.be': 'sporza',
141 def _real_extract(self
, url
):
142 site
, display_id
= self
._match
_valid
_url
(url
).groups()
143 webpage
= self
._download
_webpage
(url
, display_id
)
144 attrs
= extract_attributes(get_element_html_by_class('vrtvideo', webpage
) or '')
146 asset_id
= attrs
.get('data-video-id') or attrs
['data-videoid']
147 publication_id
= traverse_obj(attrs
, 'data-publication-id', 'data-publicationid')
149 asset_id
= f
'{publication_id}${asset_id}'
150 client
= traverse_obj(attrs
, 'data-client-code', 'data-client') or self
._CLIENT
_MAP
[site
]
152 data
= self
._call
_api
(asset_id
, client
)
153 formats
, subtitles
= self
._extract
_formats
_and
_subtitles
(data
, asset_id
)
155 description
= self
._html
_search
_meta
(
156 ['og:description', 'twitter:description', 'description'], webpage
)
157 if description
== '…':
163 'subtitles': subtitles
,
164 'description': description
,
165 'thumbnail': url_or_none(attrs
.get('data-posterimage')),
166 'duration': float_or_none(attrs
.get('data-duration'), 1000),
167 '_old_archive_ids': [make_archive_id('Canvas', asset_id
)],
168 **traverse_obj(data
, {
169 'title': ('title', {str}
),
170 'description': ('shortDescription', {str}
),
171 'duration': ('duration', {functools.partial(float_or_none, scale=1000)}
),
172 'thumbnail': ('posterImageUrl', {url_or_none}
),
177 class VrtNUIE(VRTBaseIE
):
179 _VALID_URL
= r
'https?://(?:www\.)?vrt\.be/vrtnu/a-z/(?:[^/]+/){2}(?P<id>[^/?#&]+)'
181 # CONTENT_IS_AGE_RESTRICTED
182 'url': 'https://www.vrt.be/vrtnu/a-z/de-ideale-wereld/2023-vj/de-ideale-wereld-d20230116/',
184 'id': 'pbs-pub-855b00a8-6ce2-4032-ac4f-1fcf3ae78524$vid-d2243aa1-ec46-4e34-a55b-92568459906f',
187 'description': 'Satirisch actualiteitenmagazine met Ella Leyers. Tom Waes is te gast.',
188 'timestamp': 1673905125,
189 'release_timestamp': 1673905125,
190 'series': 'De ideale wereld',
191 'season_id': '1672830988794',
192 'episode': 'Aflevering 1',
194 'episode_id': '1672830988861',
195 'display_id': 'de-ideale-wereld-d20230116',
198 'thumbnail': 'https://images.vrt.be/orig/2023/01/10/1bb39cb3-9115-11ed-b07d-02b7b76bf47f.jpg',
199 'release_date': '20230116',
200 'upload_date': '20230116',
204 'url': 'https://www.vrt.be/vrtnu/a-z/buurman--wat-doet-u-nu-/6/buurman--wat-doet-u-nu--s6-trailer/',
206 'id': 'pbs-pub-ad4050eb-d9e5-48c2-9ec8-b6c355032361$vid-0465537a-34a8-4617-8352-4d8d983b4eee',
208 'title': 'Trailer seizoen 6 \'Buurman, wat doet u nu?\'',
209 'description': 'md5:197424726c61384b4e5c519f16c0cf02',
210 'timestamp': 1652940000,
211 'release_timestamp': 1652940000,
212 'series': 'Buurman, wat doet u nu?',
213 'season': 'Seizoen 6',
215 'season_id': '1652344200907',
216 'episode': 'Aflevering 0',
218 'episode_id': '1652951873524',
219 'display_id': 'buurman--wat-doet-u-nu--s6-trailer',
222 'thumbnail': 'https://images.vrt.be/orig/2022/05/23/3c234d21-da83-11ec-b07d-02b7b76bf47f.jpg',
223 'release_date': '20220519',
224 'upload_date': '20220519',
226 'params': {'skip_download': 'm3u8'}
,
228 _NETRC_MACHINE
= 'vrtnu'
229 _authenticated
= False
231 def _perform_login(self
, username
, password
):
232 auth_info
= self
._gigya
_login
({
233 'APIKey': '3_0Z2HujMtiWq_pkAjgnS2Md2E11a1AwZjYiBETtwNE-EoEHDINgtnvcAOpNgmrVGy',
234 'targetEnv': 'jssdk',
236 'password': password
,
237 'authMode': 'cookie',
240 if auth_info
.get('errorDetails'):
241 raise ExtractorError(f
'Unable to login. VrtNU said: {auth_info["errorDetails"]}', expected
=True)
243 # Sometimes authentication fails for no good reason, retry
244 for retry
in self
.RetryManager():
245 if retry
.attempt
> 1:
248 self
._request
_webpage
(
249 'https://token.vrt.be/vrtnuinitlogin', None, note
='Requesting XSRF Token',
250 errnote
='Could not get XSRF Token', query
={
252 'destination': 'https://www.vrt.be/vrtnu/',
254 self
._request
_webpage
(
255 'https://login.vrt.be/perform_login', None,
256 note
='Performing login', errnote
='Login failed',
257 query
={'client_id': 'vrtnu-site'}
, data
=urlencode_postdata({
258 'UID': auth_info
['UID'],
259 'UIDSignature': auth_info
['UIDSignature'],
260 'signatureTimestamp': auth_info
['signatureTimestamp'],
261 '_csrf': self
._get
_cookies
('https://login.vrt.be').get('OIDCXSRF').value
,
263 except ExtractorError
as e
:
264 if isinstance(e
.cause
, urllib
.error
.HTTPError
) and e
.cause
.code
== 401:
269 self
._authenticated
= True
271 def _real_extract(self
, url
):
272 display_id
= self
._match
_id
(url
)
273 parsed_url
= urllib
.parse
.urlparse(url
)
274 details
= self
._download
_json
(
275 f
'{parsed_url.scheme}://{parsed_url.netloc}{parsed_url.path.rstrip("/")}.model.json',
276 display_id
, 'Downloading asset JSON', 'Unable to download asset JSON')['details']
278 watch_info
= traverse_obj(details
, (
279 'actions', lambda _
, v
: v
['type'] == 'watch-episode', {dict}
), get_all
=False) or {}
280 video_id
= join_nonempty(
281 'episodePublicationId', 'episodeVideoId', delim
='$', from_dict
=watch_info
)
282 if '$' not in video_id
:
283 raise ExtractorError('Unable to extract video ID')
285 vrtnutoken
= self
._download
_json
(
286 'https://token.vrt.be/refreshtoken', video_id
, note
='Retrieving vrtnutoken',
287 errnote
='Token refresh failed')['vrtnutoken'] if self
._authenticated
else None
289 video_info
= self
._call
_api
(video_id
, 'vrtnu-web@PROD', vrtnutoken
)
291 if 'title' not in video_info
:
292 code
= video_info
.get('code')
293 if code
in ('AUTHENTICATION_REQUIRED', 'CONTENT_IS_AGE_RESTRICTED'):
294 self
.raise_login_required(code
, method
='password')
295 elif code
in ('INVALID_LOCATION', 'CONTENT_AVAILABLE_ONLY_IN_BE'):
296 self
.raise_geo_restricted(countries
=['BE'])
297 elif code
== 'CONTENT_AVAILABLE_ONLY_FOR_BE_RESIDENTS_AND_EXPATS':
298 if not self
._authenticated
:
299 self
.raise_login_required(code
, method
='password')
300 self
.raise_geo_restricted(countries
=['BE'])
301 raise ExtractorError(code
, expected
=True)
303 formats
, subtitles
= self
._extract
_formats
_and
_subtitles
(video_info
, video_id
)
306 **traverse_obj(details
, {
308 'description': ('description', {clean_html}
),
309 'timestamp': ('data', 'episode', 'onTime', 'raw', {parse_iso8601}
),
310 'release_timestamp': ('data', 'episode', 'onTime', 'raw', {parse_iso8601}
),
311 'series': ('data', 'program', 'title'),
312 'season': ('data', 'season', 'title', 'value'),
313 'season_number': ('data', 'season', 'title', 'raw', {int_or_none}
),
314 'season_id': ('data', 'season', 'id', {str_or_none}
),
315 'episode': ('data', 'episode', 'number', 'value', {str_or_none}
),
316 'episode_number': ('data', 'episode', 'number', 'raw', {int_or_none}
),
317 'episode_id': ('data', 'episode', 'id', {str_or_none}
),
318 'age_limit': ('data', 'episode', 'age', 'raw', {parse_age_limit}
),
321 'display_id': display_id
,
324 'duration': float_or_none(video_info
.get('duration'), 1000),
325 'thumbnail': url_or_none(video_info
.get('posterImageUrl')),
326 'subtitles': subtitles
,
327 '_old_archive_ids': [make_archive_id('Canvas', video_id
)],
331 class KetnetIE(VRTBaseIE
):
332 _VALID_URL
= r
'https?://(?:www\.)?ketnet\.be/(?P<id>(?:[^/]+/)*[^/?#&]+)'
334 'url': 'https://www.ketnet.be/kijken/m/meisjes/6/meisjes-s6a5',
336 'id': 'pbs-pub-39f8351c-a0a0-43e6-8394-205d597d6162$vid-5e306921-a9aa-4fa9-9f39-5b82c8f1028e',
339 'episode': 'Reeks 6: Week 5',
342 'timestamp': 1685251800,
343 'upload_date': '20230528',
345 'params': {'skip_download': 'm3u8'}
,
348 def _real_extract(self
, url
):
349 display_id
= self
._match
_id
(url
)
351 video
= self
._download
_json
(
352 'https://senior-bff.ketnet.be/graphql', display_id
, query
={
354 video(id: "content/ketnet/nl/%s.model.json") {
368 video_id
= urllib
.parse
.unquote(video
['mediaReference'])
369 data
= self
._call
_api
(video_id
, 'ketnet@PROD', version
='v1')
370 formats
, subtitles
= self
._extract
_formats
_and
_subtitles
(data
, video_id
)
375 'subtitles': subtitles
,
376 '_old_archive_ids': [make_archive_id('Canvas', video_id
)],
377 **traverse_obj(video
, {
378 'title': ('titleVideodetail', {str}
),
379 'description': ('description', {str}
),
380 'thumbnail': ('thumbnail', {url_or_none}
),
381 'timestamp': ('publicationDate', {parse_iso8601}
),
382 'series': ('programTitle', {str}
),
383 'season': ('seasonTitle', {str}
),
384 'episode': ('subtitleVideodetail', {str}
),
385 'episode_number': ('episodeNr', {int_or_none}
),
390 class DagelijkseKostIE(VRTBaseIE
):
391 IE_DESC
= 'dagelijksekost.een.be'
392 _VALID_URL
= r
'https?://dagelijksekost\.een\.be/gerechten/(?P<id>[^/?#&]+)'
394 'url': 'https://dagelijksekost.een.be/gerechten/hachis-parmentier-met-witloof',
396 'id': 'md-ast-27a4d1ff-7d7b-425e-b84f-a4d227f592fa',
398 'title': 'Hachis parmentier met witloof',
399 'description': 'md5:9960478392d87f63567b5b117688cdc5',
400 'display_id': 'hachis-parmentier-met-witloof',
402 'params': {'skip_download': 'm3u8'}
,
405 def _real_extract(self
, url
):
406 display_id
= self
._match
_id
(url
)
407 webpage
= self
._download
_webpage
(url
, display_id
)
408 video_id
= self
._html
_search
_regex
(
409 r
'data-url=(["\'])(?P
<id>(?
:(?
!\
1).)+)\
1', webpage, 'video
id', group='id')
411 data = self._call_api(video_id, 'dako
@prod', version='v1
')
412 formats, subtitles = self._extract_formats_and_subtitles(data, video_id)
417 'subtitles
': subtitles,
418 'display_id
': display_id,
419 'title
': strip_or_none(get_element_by_class(
420 'dish
-metadata__title
', webpage) or self._html_search_meta('twitter
:title
', webpage)),
421 'description
': clean_html(get_element_by_class(
422 'dish
-description
', webpage)) or self._html_search_meta(
423 ['description
', 'twitter
:description
', 'og
:description
'], webpage),
424 '_old_archive_ids
': [make_archive_id('Canvas
', video_id)],