]>
jfr.im git - yt-dlp.git/blob - yt_dlp/extractor/redbee.py
7 from .common
import InfoExtractor
19 class RedBeeBaseIE(InfoExtractor
):
20 _DEVICE_ID
= str(uuid
.uuid4())
25 Ref: https://apidocs.emp.ebsd.ericsson.net
26 Subclasses must set _REDBEE_CUSTOMER, _REDBEE_BUSINESS_UNIT
28 return f
'https://exposure.api.redbee.live/v2/customer/{self._REDBEE_CUSTOMER}/businessunit/{self._REDBEE_BUSINESS_UNIT}'
30 def _get_bearer_token(self
, asset_id
, jwt
=None):
32 'deviceId': self
._DEVICE
_ID
,
34 'deviceId': self
._DEVICE
_ID
,
35 'name': 'Mozilla Firefox 102',
42 return self
._download
_json
(
43 f
'{self._API_URL}/auth/{"gigyaLogin" if jwt else "anonymous"}',
44 asset_id
, data
=json
.dumps(request
).encode('utf-8'), headers
={
45 'Content-Type': 'application/json;charset=utf-8'
48 def _get_formats_and_subtitles(self
, asset_id
, **kwargs
):
49 bearer_token
= self
._get
_bearer
_token
(asset_id
, **kwargs
)
50 api_response
= self
._download
_json
(
51 f
'{self._API_URL}/entitlement/{asset_id}/play',
53 'Authorization': f
'Bearer {bearer_token}',
54 'Accept': 'application/json, text/plain, */*'
57 formats
, subtitles
= [], {}
58 for format
in api_response
['formats']:
59 if not format
.get('mediaLocator'):
63 if format
.get('format') == 'DASH':
64 fmts
, subs
= self
._extract
_mpd
_formats
_and
_subtitles
(
65 format
['mediaLocator'], asset_id
, fatal
=False)
66 elif format
.get('format') == 'SMOOTHSTREAMING':
67 fmts
, subs
= self
._extract
_ism
_formats
_and
_subtitles
(
68 format
['mediaLocator'], asset_id
, fatal
=False)
69 elif format
.get('format') == 'HLS':
70 fmts
, subs
= self
._extract
_m
3u8_formats
_and
_subtitles
(
71 format
['mediaLocator'], asset_id
, fatal
=False)
78 self
._merge
_subtitles
(subs
, target
=subtitles
)
80 return formats
, subtitles
83 class ParliamentLiveUKIE(RedBeeBaseIE
):
84 IE_NAME
= 'parliamentlive.tv'
85 IE_DESC
= 'UK parliament videos'
86 _VALID_URL
= r
'(?i)https?://(?:www\.)?parliamentlive\.tv/Event/Index/(?P<id>[\da-f]{8}-[\da-f]{4}-[\da-f]{4}-[\da-f]{4}-[\da-f]{12})'
88 _REDBEE_CUSTOMER
= 'UKParliament'
89 _REDBEE_BUSINESS_UNIT
= 'ParliamentLive'
92 'url': 'http://parliamentlive.tv/Event/Index/c1e9d44d-fd6c-4263-b50f-97ed26cc998b',
94 'id': 'c1e9d44d-fd6c-4263-b50f-97ed26cc998b',
96 'title': 'Home Affairs Committee',
97 'timestamp': 1395153872,
98 'upload_date': '20140318',
99 'thumbnail': r
're:https?://[^?#]+c1e9d44d-fd6c-4263-b50f-97ed26cc998b[^/]*/thumbnail',
102 'url': 'http://parliamentlive.tv/event/index/3f24936f-130f-40bf-9a5d-b3d6479da6a4',
103 'only_matching': True,
105 'url': 'https://parliamentlive.tv/Event/Index/27cf25e4-e77b-42a3-93c5-c815cd6d7377',
107 'id': '27cf25e4-e77b-42a3-93c5-c815cd6d7377',
109 'title': 'House of Commons',
110 'timestamp': 1658392447,
111 'upload_date': '20220721',
112 'thumbnail': r
're:https?://[^?#]+27cf25e4-e77b-42a3-93c5-c815cd6d7377[^/]*/thumbnail',
116 def _real_extract(self
, url
):
117 video_id
= self
._match
_id
(url
)
119 formats
, subtitles
= self
._get
_formats
_and
_subtitles
(video_id
)
121 video_info
= self
._download
_json
(
122 f
'https://www.parliamentlive.tv/Event/GetShareVideo/{video_id}', video_id
, fatal
=False)
127 'subtitles': subtitles
,
128 'title': traverse_obj(video_info
, ('event', 'title')),
129 'thumbnail': traverse_obj(video_info
, 'thumbnailUrl'),
130 'timestamp': traverse_obj(
131 video_info
, ('event', 'publishedStartTime'), expected_type
=unified_timestamp
),
132 '_format_sort_fields': ('res', 'proto'),
136 class RTBFIE(RedBeeBaseIE
):
137 _VALID_URL
= r
'''(?x)
138 https?://(?:www\.)?rtbf\.be/
140 video/[^?]+\?.*\bid=|
141 ouftivi/(?:[^/]+/)*[^?]+\?.*\bvideoId=|
142 auvio/[^/]+\?.*\b(?P<live>l)?id=
144 _NETRC_MACHINE
= 'rtbf'
146 _REDBEE_CUSTOMER
= 'RTBF'
147 _REDBEE_BUSINESS_UNIT
= 'Auvio'
150 'url': 'https://www.rtbf.be/video/detail_les-diables-au-coeur-episode-2?id=1921274',
151 'md5': '8c876a1cceeb6cf31b476461ade72384',
155 'title': 'Les Diables au coeur (épisode 2)',
156 'description': '(du 25/04/2014)',
158 'upload_date': '20140425',
159 'timestamp': 1398456300,
161 'skip': 'No longer available',
164 'url': 'http://www.rtbf.be/ouftivi/heros/detail_scooby-doo-mysteres-associes?id=1097&videoId=2057442',
165 'only_matching': True,
167 'url': 'http://www.rtbf.be/ouftivi/niouzz?videoId=2055858',
168 'only_matching': True,
170 'url': 'http://www.rtbf.be/auvio/detail_jeudi-en-prime-siegfried-bracke?id=2102996',
171 'only_matching': True,
174 'url': 'https://www.rtbf.be/auvio/direct_pure-fm?lid=134775',
175 'only_matching': True,
178 'url': 'https://www.rtbf.be/auvio/detail_cinq-heures-cinema?id=2360811',
179 'only_matching': True,
182 'url': 'https://www.rtbf.be/auvio/detail_les-carnets-du-bourlingueur?id=2361588',
183 'only_matching': True,
185 'url': 'https://www.rtbf.be/auvio/detail_investigation?id=2921926',
186 'md5': 'd5d11bb62169fef38d7ce7ac531e034f',
190 'title': 'Le handicap un confinement perpétuel - Maladie de Lyme',
191 'description': 'md5:dcbd5dcf6015488c9069b057c15ccc52',
193 'upload_date': '20220727',
194 'timestamp': 1658934000,
195 'series': '#Investigation',
196 'thumbnail': r
're:^https?://[^?&]+\.jpg$',
199 'url': 'https://www.rtbf.be/auvio/detail_la-belgique-criminelle?id=2920492',
200 'md5': '054f9f143bc79c89647c35e5a7d35fa8',
204 'title': '04 - Le crime de la rue Royale',
205 'description': 'md5:0c3da1efab286df83f2ab3f8f96bd7a6',
207 'upload_date': '20220723',
208 'timestamp': 1658596887,
209 'series': 'La Belgique criminelle - TV',
210 'thumbnail': r
're:^https?://[^?&]+\.jpg$',
214 _IMAGE_HOST
= 'http://ds1.ds.static.rtbf.be'
216 'YOUTUBE': 'Youtube',
217 'DAILYMOTION': 'Dailymotion',
225 _LOGIN_URL
= 'https://login.rtbf.be/accounts.login'
226 _GIGYA_API_KEY
= '3_kWKuPgcdAybqnqxq_MvHVk0-6PN8Zk8pIIkJM_yXOu-qLPDDsGOtIDFfpGivtbeO'
227 _LOGIN_COOKIE_ID
= f
'glt_{_GIGYA_API_KEY}'
229 def _perform_login(self
, username
, password
):
230 if self
._get
_cookies
(self
._LOGIN
_URL
).get(self
._LOGIN
_COOKIE
_ID
):
233 self
._set
_cookie
('.rtbf.be', 'gmid', 'gmid.ver4', secure
=True, expire_time
=time
.time() + 3600)
235 login_response
= self
._download
_json
(
236 self
._LOGIN
_URL
, None, data
=urllib
.parse
.urlencode({
238 'password': password
,
239 'APIKey': self
._GIGYA
_API
_KEY
,
240 'targetEnv': 'jssdk',
241 'sessionExpiration': '-2',
242 }).encode('utf-8'), headers
={
243 'Content-Type': 'application/x-www-form-urlencoded',
246 if login_response
['statusCode'] != 200:
247 raise ExtractorError('Login failed. Server message: %s' % login_response
['errorMessage'], expected
=True)
249 self
._set
_cookie
('.rtbf.be', self
._LOGIN
_COOKIE
_ID
, login_response
['sessionInfo']['login_token'],
250 secure
=True, expire_time
=time
.time() + 3600)
252 def _get_formats_and_subtitles(self
, url
, media_id
):
253 login_token
= self
._get
_cookies
(url
).get(self
._LOGIN
_COOKIE
_ID
)
255 self
.raise_login_required()
257 session_jwt
= try_call(lambda: self
._get
_cookies
(url
)['rtbf_jwt'].value
) or self
._download
_json
(
258 'https://login.rtbf.be/accounts.getJWT', media_id
, query
={
259 'login_token': login_token
.value
,
260 'APIKey': self
._GIGYA
_API
_KEY
,
262 'authMode': 'cookie',
268 return super()._get
_formats
_and
_subtitles
(media_id
, jwt
=session_jwt
)
270 def _real_extract(self
, url
):
271 live
, media_id
= self
._match
_valid
_url
(url
).groups()
272 embed_page
= self
._download
_webpage
(
273 'https://www.rtbf.be/auvio/embed/' + ('direct' if live
else 'media'),
274 media_id
, query
={'id': media_id}
)
276 media_data
= self
._html
_search
_regex
(r
'data-media="([^"]+)"', embed_page
, 'media data', fatal
=False)
278 if re
.search(r
'<div[^>]+id="js-error-expired"[^>]+class="(?![^"]*hidden)', embed_page
):
279 raise ExtractorError('Livestream has ended.', expected
=True)
280 if re
.search(r
'<div[^>]+id="js-sso-connect"[^>]+class="(?![^"]*hidden)', embed_page
):
281 self
.raise_login_required()
283 raise ExtractorError('Could not find media data')
285 data
= self
._parse
_json
(media_data
, media_id
)
287 error
= data
.get('error')
289 raise ExtractorError('%s said: %s' % (self
.IE_NAME
, error
), expected
=True)
291 provider
= data
.get('provider')
292 if provider
in self
._PROVIDERS
:
293 return self
.url_result(data
['url'], self
._PROVIDERS
[provider
])
295 title
= traverse_obj(data
, 'subtitle', 'title')
296 is_live
= data
.get('isLive')
297 height_re
= r
'-(\d+)p\.'
298 formats
, subtitles
= [], {}
300 # The old api still returns m3u8 and mpd manifest for livestreams, but these are 'fake'
301 # since all they contain is a 20s video that is completely unrelated.
302 # https://github.com/yt-dlp/yt-dlp/issues/4656#issuecomment-1214461092
303 m3u8_url
= None if data
.get('isLive') else traverse_obj(data
, 'urlHlsAes128', 'urlHls')
305 fmts
, subs
= self
._extract
_m
3u8_formats
_and
_subtitles
(
306 m3u8_url
, media_id
, 'mp4', m3u8_id
='hls', fatal
=False)
308 self
._merge
_subtitles
(subs
, target
=subtitles
)
310 fix_url
= lambda x
: x
.replace('//rtbf-vod.', '//rtbf.') if '/geo/drm/' in x
else x
311 http_url
= data
.get('url')
312 if formats
and http_url
and re
.search(height_re
, http_url
):
313 http_url
= fix_url(http_url
)
314 for m3u8_f
in formats
[:]:
315 height
= m3u8_f
.get('height')
321 'format_id': m3u8_f
['format_id'].replace('hls-', 'http-'),
322 'url': re
.sub(height_re
, '-%dp.' % height
, http_url
),
326 sources
= data
.get('sources') or {}
327 for key
, format_id
in self
._QUALITIES
:
328 format_url
= sources
.get(key
)
331 height
= int_or_none(self
._search
_regex
(
332 height_re
, format_url
, 'height', default
=None))
334 'format_id': format_id
,
335 'url': fix_url(format_url
),
339 mpd_url
= None if data
.get('isLive') else data
.get('urlDash')
340 if mpd_url
and (self
.get_param('allow_unplayable_formats') or not data
.get('drm')):
341 fmts
, subs
= self
._extract
_mpd
_formats
_and
_subtitles
(
342 mpd_url
, media_id
, mpd_id
='dash', fatal
=False)
344 self
._merge
_subtitles
(subs
, target
=subtitles
)
346 audio_url
= data
.get('urlAudio')
349 'format_id': 'audio',
354 for track
in (data
.get('tracks') or {}).values():
355 sub_url
= track
.get('url')
358 subtitles
.setdefault(track
.get('lang') or 'fr', []).append({
363 fmts
, subs
= self
._get
_formats
_and
_subtitles
(url
, f
'live_{media_id}' if is_live
else media_id
)
365 self
._merge
_subtitles
(subs
, target
=subtitles
)
371 'description': strip_or_none(data
.get('description')),
372 'thumbnail': data
.get('thumbnail'),
373 'duration': float_or_none(data
.get('realDuration')),
374 'timestamp': int_or_none(data
.get('liveFrom')),
375 'series': data
.get('programLabel'),
376 'subtitles': subtitles
,
378 '_format_sort_fields': ('res', 'proto'),