]> jfr.im git - yt-dlp.git/blame - yt_dlp/extractor/zattoo.py
[jsinterp] Bring or-par with youtube-dl
[yt-dlp.git] / yt_dlp / extractor / zattoo.py
CommitLineData
4a733545 1import re
67ca1a8e 2from uuid import uuid4
4a733545
AS
3
4from .common import InfoExtractor
67ca1a8e
S
5from ..compat import (
6 compat_HTTPError,
4a733545 7 compat_str,
67ca1a8e
S
8)
9from ..utils import (
4a733545 10 ExtractorError,
67ca1a8e 11 int_or_none,
34921b43 12 join_nonempty,
67ca1a8e 13 try_get,
3052a30d 14 url_or_none,
4a733545
AS
15 urlencode_postdata,
16)
17
18
f6d7f7b4 19class ZattooPlatformBaseIE(InfoExtractor):
4a733545
AS
20 _power_guide_hash = None
21
f6d7f7b4 22 def _host_url(self):
16d896b2 23 return 'https://%s' % (self._API_HOST if hasattr(self, '_API_HOST') else self._HOST)
f6d7f7b4 24
52efa4b3 25 def _real_initialize(self):
26 if not self._power_guide_hash:
27 self.raise_login_required('An account is needed to access this media', method='password')
67ca1a8e 28
52efa4b3 29 def _perform_login(self, username, password):
67ca1a8e
S
30 try:
31 data = self._download_json(
f6d7f7b4 32 '%s/zapi/v2/account/login' % self._host_url(), None, 'Logging in',
67ca1a8e
S
33 data=urlencode_postdata({
34 'login': username,
35 'password': password,
36 'remember': 'true',
37 }), headers={
f6d7f7b4 38 'Referer': '%s/login' % self._host_url(),
67ca1a8e
S
39 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8',
40 })
41 except ExtractorError as e:
42 if isinstance(e.cause, compat_HTTPError) and e.cause.code == 400:
43 raise ExtractorError(
44 'Unable to login: incorrect username and/or password',
45 expected=True)
46 raise
47
48 self._power_guide_hash = data['session']['power_guide_hash']
49
52efa4b3 50 def _initialize_pre_login(self):
9b8b7a7b
AS
51 session_token = self._download_json(
52 f'{self._host_url()}/token.json', None, 'Downloading session token')['session_token']
67ca1a8e
S
53
54 # Will setup appropriate cookies
55 self._request_webpage(
9b8b7a7b 56 '%s/zapi/v3/session/hello' % self._host_url(), None,
67ca1a8e 57 'Opening session', data=urlencode_postdata({
67ca1a8e
S
58 'uuid': compat_str(uuid4()),
59 'lang': 'en',
9b8b7a7b 60 'app_version': '1.8.2',
67ca1a8e 61 'format': 'json',
9b8b7a7b 62 'client_app_token': session_token,
67ca1a8e 63 }))
4a733545 64
9b8b7a7b
AS
65 def _extract_video_id_from_recording(self, recid):
66 playlist = self._download_json(
67 f'{self._host_url()}/zapi/v2/playlist', recid, 'Downloading playlist')
68 try:
69 return next(
70 str(item['program_id']) for item in playlist['recordings']
71 if item.get('program_id') and str(item.get('id')) == recid)
72 except (StopIteration, KeyError):
73 raise ExtractorError('Could not extract video id from recording')
74
4a733545
AS
75 def _extract_cid(self, video_id, channel_name):
76 channel_groups = self._download_json(
f6d7f7b4 77 '%s/zapi/v2/cached/channels/%s' % (self._host_url(),
4a733545 78 self._power_guide_hash),
67ca1a8e 79 video_id, 'Downloading channel list',
4a733545
AS
80 query={'details': False})['channel_groups']
81 channel_list = []
82 for chgrp in channel_groups:
83 channel_list.extend(chgrp['channels'])
84 try:
85 return next(
86 chan['cid'] for chan in channel_list
67ca1a8e 87 if chan.get('cid') and (
3089bc74
S
88 chan.get('display_alias') == channel_name
89 or chan.get('cid') == channel_name))
4a733545
AS
90 except StopIteration:
91 raise ExtractorError('Could not extract channel id')
92
93 def _extract_cid_and_video_info(self, video_id):
94 data = self._download_json(
21160a17 95 '%s/zapi/v2/cached/program/power_details/%s' % (
f6d7f7b4 96 self._host_url(), self._power_guide_hash),
4a733545
AS
97 video_id,
98 'Downloading video information',
99 query={
21160a17
AS
100 'program_ids': video_id,
101 'complete': True,
4a733545
AS
102 })
103
21160a17 104 p = data['programs'][0]
67ca1a8e
S
105 cid = p['cid']
106
4a733545
AS
107 info_dict = {
108 'id': video_id,
21160a17
AS
109 'title': p.get('t') or p['et'],
110 'description': p.get('d'),
111 'thumbnail': p.get('i_url'),
67ca1a8e 112 'creator': p.get('channel_name'),
21160a17
AS
113 'episode': p.get('et'),
114 'episode_number': int_or_none(p.get('e_no')),
115 'season_number': int_or_none(p.get('s_no')),
67ca1a8e 116 'release_year': int_or_none(p.get('year')),
21160a17
AS
117 'categories': try_get(p, lambda x: x['c'], list),
118 'tags': try_get(p, lambda x: x['g'], list)
4a733545 119 }
67ca1a8e 120
4a733545
AS
121 return cid, info_dict
122
9b8b7a7b
AS
123 def _extract_ondemand_info(self, ondemand_id):
124 """
125 @returns (ondemand_token, ondemand_type, info_dict)
126 """
127 data = self._download_json(
128 '%s/zapi/vod/movies/%s' % (self._host_url(), ondemand_id),
129 ondemand_id, 'Downloading ondemand information')
130 info_dict = {
131 'id': ondemand_id,
132 'title': data.get('title'),
133 'description': data.get('description'),
134 'duration': int_or_none(data.get('duration')),
135 'release_year': int_or_none(data.get('year')),
136 'episode_number': int_or_none(data.get('episode_number')),
137 'season_number': int_or_none(data.get('season_number')),
138 'categories': try_get(data, lambda x: x['categories'], list),
139 }
140 return data['terms_catalog'][0]['terms'][0]['token'], data['type'], info_dict
141
142 def _extract_formats(self, cid, video_id, record_id=None, ondemand_id=None, ondemand_termtoken=None, ondemand_type=None, is_live=False):
67ca1a8e 143 postdata_common = {
4a733545
AS
144 'https_watch_urls': True,
145 }
4a733545
AS
146
147 if is_live:
67ca1a8e 148 postdata_common.update({'timeshift': 10800})
f6d7f7b4 149 url = '%s/zapi/watch/live/%s' % (self._host_url(), cid)
67ca1a8e 150 elif record_id:
f6d7f7b4 151 url = '%s/zapi/watch/recording/%s' % (self._host_url(), record_id)
9b8b7a7b
AS
152 elif ondemand_id:
153 postdata_common.update({
154 'teasable_id': ondemand_id,
155 'term_token': ondemand_termtoken,
156 'teasable_type': ondemand_type
157 })
158 url = '%s/zapi/watch/vod/video' % self._host_url()
67ca1a8e 159 else:
9b8b7a7b 160 url = '%s/zapi/v3/watch/replay/%s/%s' % (self._host_url(), cid, video_id)
4a733545 161 formats = []
9b8b7a7b
AS
162 subtitles = {}
163 for stream_type in ('dash', 'hls7'):
67ca1a8e
S
164 postdata = postdata_common.copy()
165 postdata['stream_type'] = stream_type
166
167 data = self._download_json(
168 url, video_id, 'Downloading %s formats' % stream_type.upper(),
169 data=urlencode_postdata(postdata), fatal=False)
170 if not data:
171 continue
172
173 watch_urls = try_get(
174 data, lambda x: x['stream']['watch_urls'], list)
175 if not watch_urls:
176 continue
177
178 for watch in watch_urls:
179 if not isinstance(watch, dict):
180 continue
3052a30d
S
181 watch_url = url_or_none(watch.get('url'))
182 if not watch_url:
67ca1a8e 183 continue
67ca1a8e 184 audio_channel = watch.get('audio_channel')
67ca1a8e 185 preference = 1 if audio_channel == 'A' else None
34921b43 186 format_id = join_nonempty(stream_type, watch.get('maxrate'), audio_channel)
9b8b7a7b
AS
187 if stream_type.startswith('dash'):
188 this_formats, subs = self._extract_mpd_formats_and_subtitles(
67ca1a8e 189 watch_url, video_id, mpd_id=format_id, fatal=False)
9b8b7a7b
AS
190 self._merge_subtitles(subs, target=subtitles)
191 elif stream_type.startswith('hls'):
192 this_formats, subs = self._extract_m3u8_formats_and_subtitles(
67ca1a8e
S
193 watch_url, video_id, 'mp4',
194 entry_protocol='m3u8_native', m3u8_id=format_id,
195 fatal=False)
9b8b7a7b 196 self._merge_subtitles(subs, target=subtitles)
67ca1a8e
S
197 elif stream_type == 'hds':
198 this_formats = self._extract_f4m_formats(
199 watch_url, video_id, f4m_id=format_id, fatal=False)
200 elif stream_type == 'smooth_playready':
201 this_formats = self._extract_ism_formats(
202 watch_url, video_id, ism_id=format_id, fatal=False)
203 else:
204 assert False
205 for this_format in this_formats:
f983b875 206 this_format['quality'] = preference
67ca1a8e 207 formats.extend(this_formats)
4a733545 208 self._sort_formats(formats)
9b8b7a7b 209 return formats, subtitles
4a733545 210
9b8b7a7b
AS
211 def _extract_video(self, video_id, record_id=None):
212 cid, info_dict = self._extract_cid_and_video_info(video_id)
213 info_dict['formats'], info_dict['subtitles'] = self._extract_formats(cid, video_id, record_id=record_id)
4a733545
AS
214 return info_dict
215
9b8b7a7b
AS
216 def _extract_live(self, channel_name):
217 cid = self._extract_cid(channel_name, channel_name)
218 formats, subtitles = self._extract_formats(cid, cid, is_live=True)
219 return {
220 'id': channel_name,
221 'title': channel_name,
222 'is_live': True,
520876fa 223 'formats': formats,
9b8b7a7b
AS
224 'subtitles': subtitles
225 }
4a733545 226
9b8b7a7b
AS
227 def _extract_record(self, record_id):
228 video_id = self._extract_video_id_from_recording(record_id)
229 cid, info_dict = self._extract_cid_and_video_info(video_id)
230 info_dict['formats'], info_dict['subtitles'] = self._extract_formats(cid, video_id, record_id=record_id)
231 return info_dict
67ca1a8e 232
9b8b7a7b
AS
233 def _extract_ondemand(self, ondemand_id):
234 ondemand_termtoken, ondemand_type, info_dict = self._extract_ondemand_info(ondemand_id)
235 info_dict['formats'], info_dict['subtitles'] = self._extract_formats(
236 None, ondemand_id, ondemand_id=ondemand_id,
237 ondemand_termtoken=ondemand_termtoken, ondemand_type=ondemand_type)
238 return info_dict
67ca1a8e 239
1155ecef 240 def _real_extract(self, url):
48732bec 241 video_id, record_id = self._match_valid_url(url).groups()
242 return self._extract_video(video_id, record_id)
1155ecef 243
4a733545 244
9b8b7a7b
AS
245def _make_valid_url(host):
246 return rf'https?://(?:www\.)?{re.escape(host)}/watch/[^/]+?/(?P<id>[0-9]+)[^/]+(?:/(?P<recid>[0-9]+))?'
4a733545
AS
247
248
f6d7f7b4
S
249class ZattooBaseIE(ZattooPlatformBaseIE):
250 _NETRC_MACHINE = 'zattoo'
251 _HOST = 'zattoo.com'
252
9b8b7a7b
AS
253 @staticmethod
254 def _create_valid_url(match, qs, base_re=None):
255 match_base = fr'|{base_re}/(?P<vid1>{match})' if base_re else '(?P<vid1>)'
256 return rf'''(?x)https?://(?:www\.)?zattoo\.com/(?:
257 [^?#]+\?(?:[^#]+&)?{qs}=(?P<vid2>{match})
258 {match_base}
259 )'''
f6d7f7b4 260
48732bec 261 def _real_extract(self, url):
262 vid1, vid2 = self._match_valid_url(url).group('vid1', 'vid2')
263 return getattr(self, f'_extract_{self._TYPE}')(vid1 or vid2)
264
f6d7f7b4 265
4a733545 266class ZattooIE(ZattooBaseIE):
9b8b7a7b
AS
267 _VALID_URL = ZattooBaseIE._create_valid_url(r'\d+', 'program', '(?:program|watch)/[^/]+')
268 _TYPE = 'video'
4a733545 269 _TESTS = [{
9b8b7a7b
AS
270 'url': 'https://zattoo.com/program/zdf/250170418',
271 'info_dict': {
272 'id': '250170418',
273 'ext': 'mp4',
274 'title': 'Markus Lanz',
275 'description': 'md5:e41cb1257de008ca62a73bb876ffa7fc',
276 'thumbnail': 're:http://images.zattic.com/cms/.+/format_480x360.jpg',
277 'creator': 'ZDF HD',
278 'release_year': 2022,
279 'episode': 'Folge 1655',
280 'categories': 'count:1',
281 'tags': 'count:2'
282 },
283 'params': {'skip_download': 'm3u8'}
284 }, {
285 'url': 'https://zattoo.com/program/daserste/210177916',
4a733545
AS
286 'only_matching': True,
287 }, {
9b8b7a7b 288 'url': 'https://zattoo.com/guide/german?channel=srf1&program=169860555',
4a733545
AS
289 'only_matching': True,
290 }]
291
4a733545
AS
292
293class ZattooLiveIE(ZattooBaseIE):
9b8b7a7b
AS
294 _VALID_URL = ZattooBaseIE._create_valid_url(r'[^/?&#]+', 'channel', 'live')
295 _TYPE = 'live'
296 _TESTS = [{
297 'url': 'https://zattoo.com/channels/german?channel=srf_zwei',
4a733545 298 'only_matching': True,
9b8b7a7b
AS
299 }, {
300 'url': 'https://zattoo.com/live/srf1',
301 'only_matching': True,
302 }]
4a733545 303
67ca1a8e
S
304 @classmethod
305 def suitable(cls, url):
9b8b7a7b 306 return False if ZattooIE.suitable(url) else super().suitable(url)
67ca1a8e 307
9b8b7a7b
AS
308
309class ZattooMoviesIE(ZattooBaseIE):
310 _VALID_URL = ZattooBaseIE._create_valid_url(r'\w+', 'movie_id', 'vod/movies')
311 _TYPE = 'ondemand'
312 _TESTS = [{
313 'url': 'https://zattoo.com/vod/movies/7521',
314 'only_matching': True,
315 }, {
316 'url': 'https://zattoo.com/ondemand?movie_id=7521&term_token=9f00f43183269484edde',
317 'only_matching': True,
318 }]
f6d7f7b4
S
319
320
9b8b7a7b
AS
321class ZattooRecordingsIE(ZattooBaseIE):
322 _VALID_URL = ZattooBaseIE._create_valid_url(r'\d+', 'recording')
323 _TYPE = 'record'
324 _TESTS = [{
325 'url': 'https://zattoo.com/recordings?recording=193615508',
326 'only_matching': True,
327 }, {
328 'url': 'https://zattoo.com/tc/ptc_recordings_all_recordings?recording=193615420',
329 'only_matching': True,
330 }]
331
332
333class NetPlusIE(ZattooPlatformBaseIE):
f6d7f7b4
S
334 _NETRC_MACHINE = 'netplus'
335 _HOST = 'netplus.tv'
16d896b2 336 _API_HOST = 'www.%s' % _HOST
9b8b7a7b 337 _VALID_URL = _make_valid_url(_HOST)
f6d7f7b4
S
338
339 _TESTS = [{
340 'url': 'https://www.netplus.tv/watch/abc/123-abc',
341 'only_matching': True,
342 }]
343
344
9b8b7a7b 345class MNetTVIE(ZattooPlatformBaseIE):
f6d7f7b4
S
346 _NETRC_MACHINE = 'mnettv'
347 _HOST = 'tvplus.m-net.de'
9b8b7a7b 348 _VALID_URL = _make_valid_url(_HOST)
f6d7f7b4
S
349
350 _TESTS = [{
16d896b2 351 'url': 'https://tvplus.m-net.de/watch/abc/123-abc',
f6d7f7b4
S
352 'only_matching': True,
353 }]
354
355
9b8b7a7b 356class WalyTVIE(ZattooPlatformBaseIE):
f6d7f7b4
S
357 _NETRC_MACHINE = 'walytv'
358 _HOST = 'player.waly.tv'
9b8b7a7b 359 _VALID_URL = _make_valid_url(_HOST)
f6d7f7b4
S
360
361 _TESTS = [{
16d896b2 362 'url': 'https://player.waly.tv/watch/abc/123-abc',
f6d7f7b4
S
363 'only_matching': True,
364 }]
365
366
9b8b7a7b 367class BBVTVIE(ZattooPlatformBaseIE):
f6d7f7b4
S
368 _NETRC_MACHINE = 'bbvtv'
369 _HOST = 'bbv-tv.net'
16d896b2 370 _API_HOST = 'www.%s' % _HOST
9b8b7a7b 371 _VALID_URL = _make_valid_url(_HOST)
f6d7f7b4
S
372
373 _TESTS = [{
374 'url': 'https://www.bbv-tv.net/watch/abc/123-abc',
375 'only_matching': True,
376 }]
377
378
9b8b7a7b 379class VTXTVIE(ZattooPlatformBaseIE):
f6d7f7b4
S
380 _NETRC_MACHINE = 'vtxtv'
381 _HOST = 'vtxtv.ch'
16d896b2 382 _API_HOST = 'www.%s' % _HOST
9b8b7a7b 383 _VALID_URL = _make_valid_url(_HOST)
f6d7f7b4
S
384
385 _TESTS = [{
386 'url': 'https://www.vtxtv.ch/watch/abc/123-abc',
387 'only_matching': True,
388 }]
389
390
9b8b7a7b 391class GlattvisionTVIE(ZattooPlatformBaseIE):
f6d7f7b4
S
392 _NETRC_MACHINE = 'glattvisiontv'
393 _HOST = 'iptv.glattvision.ch'
9b8b7a7b 394 _VALID_URL = _make_valid_url(_HOST)
f6d7f7b4
S
395
396 _TESTS = [{
16d896b2 397 'url': 'https://iptv.glattvision.ch/watch/abc/123-abc',
f6d7f7b4
S
398 'only_matching': True,
399 }]
400
401
9b8b7a7b 402class SAKTVIE(ZattooPlatformBaseIE):
f6d7f7b4
S
403 _NETRC_MACHINE = 'saktv'
404 _HOST = 'saktv.ch'
16d896b2 405 _API_HOST = 'www.%s' % _HOST
9b8b7a7b 406 _VALID_URL = _make_valid_url(_HOST)
f6d7f7b4
S
407
408 _TESTS = [{
409 'url': 'https://www.saktv.ch/watch/abc/123-abc',
410 'only_matching': True,
411 }]
412
413
9b8b7a7b 414class EWETVIE(ZattooPlatformBaseIE):
f6d7f7b4
S
415 _NETRC_MACHINE = 'ewetv'
416 _HOST = 'tvonline.ewe.de'
9b8b7a7b 417 _VALID_URL = _make_valid_url(_HOST)
f6d7f7b4
S
418
419 _TESTS = [{
16d896b2 420 'url': 'https://tvonline.ewe.de/watch/abc/123-abc',
f6d7f7b4
S
421 'only_matching': True,
422 }]
423
424
9b8b7a7b 425class QuantumTVIE(ZattooPlatformBaseIE):
f6d7f7b4
S
426 _NETRC_MACHINE = 'quantumtv'
427 _HOST = 'quantum-tv.com'
16d896b2 428 _API_HOST = 'www.%s' % _HOST
9b8b7a7b 429 _VALID_URL = _make_valid_url(_HOST)
f6d7f7b4
S
430
431 _TESTS = [{
432 'url': 'https://www.quantum-tv.com/watch/abc/123-abc',
433 'only_matching': True,
434 }]
435
436
9b8b7a7b 437class OsnatelTVIE(ZattooPlatformBaseIE):
f6d7f7b4 438 _NETRC_MACHINE = 'osnateltv'
2004e221 439 _HOST = 'tvonline.osnatel.de'
9b8b7a7b 440 _VALID_URL = _make_valid_url(_HOST)
f6d7f7b4
S
441
442 _TESTS = [{
16d896b2 443 'url': 'https://tvonline.osnatel.de/watch/abc/123-abc',
f6d7f7b4
S
444 'only_matching': True,
445 }]
446
447
9b8b7a7b 448class EinsUndEinsTVIE(ZattooPlatformBaseIE):
f6d7f7b4
S
449 _NETRC_MACHINE = '1und1tv'
450 _HOST = '1und1.tv'
16d896b2 451 _API_HOST = 'www.%s' % _HOST
9b8b7a7b 452 _VALID_URL = _make_valid_url(_HOST)
f6d7f7b4
S
453
454 _TESTS = [{
455 'url': 'https://www.1und1.tv/watch/abc/123-abc',
456 'only_matching': True,
457 }]
a81daba2
AS
458
459
9b8b7a7b 460class SaltTVIE(ZattooPlatformBaseIE):
a81daba2
AS
461 _NETRC_MACHINE = 'salttv'
462 _HOST = 'tv.salt.ch'
9b8b7a7b 463 _VALID_URL = _make_valid_url(_HOST)
a81daba2
AS
464
465 _TESTS = [{
466 'url': 'https://tv.salt.ch/watch/abc/123-abc',
467 'only_matching': True,
468 }]