]> jfr.im git - yt-dlp.git/blame - yt_dlp/extractor/zattoo.py
[ie/youtube] Suppress "Unavailable videos are hidden" warning (#10159)
[yt-dlp.git] / yt_dlp / extractor / zattoo.py
CommitLineData
4a733545 1import re
e3a3ed8a 2import uuid
4a733545
AS
3
4from .common import InfoExtractor
3d2623a8 5from ..networking.exceptions import HTTPError
67ca1a8e 6from ..utils import (
4a733545 7 ExtractorError,
67ca1a8e 8 int_or_none,
34921b43 9 join_nonempty,
67ca1a8e 10 try_get,
3052a30d 11 url_or_none,
4a733545
AS
12 urlencode_postdata,
13)
14
15
f6d7f7b4 16class ZattooPlatformBaseIE(InfoExtractor):
4a733545
AS
17 _power_guide_hash = None
18
f6d7f7b4 19 def _host_url(self):
16d896b2 20 return 'https://%s' % (self._API_HOST if hasattr(self, '_API_HOST') else self._HOST)
f6d7f7b4 21
52efa4b3 22 def _real_initialize(self):
23 if not self._power_guide_hash:
24 self.raise_login_required('An account is needed to access this media', method='password')
67ca1a8e 25
52efa4b3 26 def _perform_login(self, username, password):
67ca1a8e
S
27 try:
28 data = self._download_json(
add96eb9 29 f'{self._host_url()}/zapi/v2/account/login', None, 'Logging in',
67ca1a8e
S
30 data=urlencode_postdata({
31 'login': username,
32 'password': password,
33 'remember': 'true',
34 }), headers={
add96eb9 35 'Referer': f'{self._host_url()}/login',
67ca1a8e
S
36 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8',
37 })
38 except ExtractorError as e:
3d2623a8 39 if isinstance(e.cause, HTTPError) and e.cause.status == 400:
67ca1a8e
S
40 raise ExtractorError(
41 'Unable to login: incorrect username and/or password',
42 expected=True)
43 raise
44
45 self._power_guide_hash = data['session']['power_guide_hash']
46
52efa4b3 47 def _initialize_pre_login(self):
9b8b7a7b
AS
48 session_token = self._download_json(
49 f'{self._host_url()}/token.json', None, 'Downloading session token')['session_token']
67ca1a8e
S
50
51 # Will setup appropriate cookies
52 self._request_webpage(
add96eb9 53 f'{self._host_url()}/zapi/v3/session/hello', None,
67ca1a8e 54 'Opening session', data=urlencode_postdata({
add96eb9 55 'uuid': str(uuid.uuid4()),
67ca1a8e 56 'lang': 'en',
9b8b7a7b 57 'app_version': '1.8.2',
67ca1a8e 58 'format': 'json',
9b8b7a7b 59 'client_app_token': session_token,
67ca1a8e 60 }))
4a733545 61
9b8b7a7b
AS
62 def _extract_video_id_from_recording(self, recid):
63 playlist = self._download_json(
64 f'{self._host_url()}/zapi/v2/playlist', recid, 'Downloading playlist')
65 try:
66 return next(
67 str(item['program_id']) for item in playlist['recordings']
68 if item.get('program_id') and str(item.get('id')) == recid)
69 except (StopIteration, KeyError):
70 raise ExtractorError('Could not extract video id from recording')
71
4a733545
AS
72 def _extract_cid(self, video_id, channel_name):
73 channel_groups = self._download_json(
add96eb9 74 f'{self._host_url()}/zapi/v2/cached/channels/{self._power_guide_hash}',
67ca1a8e 75 video_id, 'Downloading channel list',
4a733545
AS
76 query={'details': False})['channel_groups']
77 channel_list = []
78 for chgrp in channel_groups:
79 channel_list.extend(chgrp['channels'])
80 try:
81 return next(
82 chan['cid'] for chan in channel_list
67ca1a8e 83 if chan.get('cid') and (
3089bc74
S
84 chan.get('display_alias') == channel_name
85 or chan.get('cid') == channel_name))
4a733545
AS
86 except StopIteration:
87 raise ExtractorError('Could not extract channel id')
88
89 def _extract_cid_and_video_info(self, video_id):
90 data = self._download_json(
add96eb9 91 f'{self._host_url()}/zapi/v2/cached/program/power_details/{self._power_guide_hash}',
4a733545
AS
92 video_id,
93 'Downloading video information',
94 query={
21160a17
AS
95 'program_ids': video_id,
96 'complete': True,
4a733545
AS
97 })
98
21160a17 99 p = data['programs'][0]
67ca1a8e
S
100 cid = p['cid']
101
4a733545
AS
102 info_dict = {
103 'id': video_id,
21160a17
AS
104 'title': p.get('t') or p['et'],
105 'description': p.get('d'),
106 'thumbnail': p.get('i_url'),
67ca1a8e 107 'creator': p.get('channel_name'),
21160a17
AS
108 'episode': p.get('et'),
109 'episode_number': int_or_none(p.get('e_no')),
110 'season_number': int_or_none(p.get('s_no')),
67ca1a8e 111 'release_year': int_or_none(p.get('year')),
21160a17 112 'categories': try_get(p, lambda x: x['c'], list),
add96eb9 113 'tags': try_get(p, lambda x: x['g'], list),
4a733545 114 }
67ca1a8e 115
4a733545
AS
116 return cid, info_dict
117
9b8b7a7b
AS
118 def _extract_ondemand_info(self, ondemand_id):
119 """
120 @returns (ondemand_token, ondemand_type, info_dict)
121 """
122 data = self._download_json(
add96eb9 123 f'{self._host_url()}/zapi/vod/movies/{ondemand_id}',
9b8b7a7b
AS
124 ondemand_id, 'Downloading ondemand information')
125 info_dict = {
126 'id': ondemand_id,
127 'title': data.get('title'),
128 'description': data.get('description'),
129 'duration': int_or_none(data.get('duration')),
130 'release_year': int_or_none(data.get('year')),
131 'episode_number': int_or_none(data.get('episode_number')),
132 'season_number': int_or_none(data.get('season_number')),
133 'categories': try_get(data, lambda x: x['categories'], list),
134 }
135 return data['terms_catalog'][0]['terms'][0]['token'], data['type'], info_dict
136
137 def _extract_formats(self, cid, video_id, record_id=None, ondemand_id=None, ondemand_termtoken=None, ondemand_type=None, is_live=False):
67ca1a8e 138 postdata_common = {
4a733545
AS
139 'https_watch_urls': True,
140 }
4a733545
AS
141
142 if is_live:
67ca1a8e 143 postdata_common.update({'timeshift': 10800})
add96eb9 144 url = f'{self._host_url()}/zapi/watch/live/{cid}'
67ca1a8e 145 elif record_id:
add96eb9 146 url = f'{self._host_url()}/zapi/watch/recording/{record_id}'
9b8b7a7b
AS
147 elif ondemand_id:
148 postdata_common.update({
149 'teasable_id': ondemand_id,
150 'term_token': ondemand_termtoken,
add96eb9 151 'teasable_type': ondemand_type,
9b8b7a7b 152 })
add96eb9 153 url = f'{self._host_url()}/zapi/watch/vod/video'
67ca1a8e 154 else:
add96eb9 155 url = f'{self._host_url()}/zapi/v3/watch/replay/{cid}/{video_id}'
4a733545 156 formats = []
9b8b7a7b
AS
157 subtitles = {}
158 for stream_type in ('dash', 'hls7'):
67ca1a8e
S
159 postdata = postdata_common.copy()
160 postdata['stream_type'] = stream_type
161
162 data = self._download_json(
add96eb9 163 url, video_id, f'Downloading {stream_type.upper()} formats',
67ca1a8e
S
164 data=urlencode_postdata(postdata), fatal=False)
165 if not data:
166 continue
167
168 watch_urls = try_get(
169 data, lambda x: x['stream']['watch_urls'], list)
170 if not watch_urls:
171 continue
172
173 for watch in watch_urls:
174 if not isinstance(watch, dict):
175 continue
3052a30d
S
176 watch_url = url_or_none(watch.get('url'))
177 if not watch_url:
67ca1a8e 178 continue
67ca1a8e 179 audio_channel = watch.get('audio_channel')
67ca1a8e 180 preference = 1 if audio_channel == 'A' else None
34921b43 181 format_id = join_nonempty(stream_type, watch.get('maxrate'), audio_channel)
9b8b7a7b
AS
182 if stream_type.startswith('dash'):
183 this_formats, subs = self._extract_mpd_formats_and_subtitles(
67ca1a8e 184 watch_url, video_id, mpd_id=format_id, fatal=False)
9b8b7a7b
AS
185 self._merge_subtitles(subs, target=subtitles)
186 elif stream_type.startswith('hls'):
187 this_formats, subs = self._extract_m3u8_formats_and_subtitles(
67ca1a8e
S
188 watch_url, video_id, 'mp4',
189 entry_protocol='m3u8_native', m3u8_id=format_id,
190 fatal=False)
9b8b7a7b 191 self._merge_subtitles(subs, target=subtitles)
67ca1a8e
S
192 elif stream_type == 'hds':
193 this_formats = self._extract_f4m_formats(
194 watch_url, video_id, f4m_id=format_id, fatal=False)
195 elif stream_type == 'smooth_playready':
196 this_formats = self._extract_ism_formats(
197 watch_url, video_id, ism_id=format_id, fatal=False)
198 else:
199 assert False
200 for this_format in this_formats:
f983b875 201 this_format['quality'] = preference
67ca1a8e 202 formats.extend(this_formats)
9b8b7a7b 203 return formats, subtitles
4a733545 204
9b8b7a7b
AS
205 def _extract_video(self, video_id, record_id=None):
206 cid, info_dict = self._extract_cid_and_video_info(video_id)
207 info_dict['formats'], info_dict['subtitles'] = self._extract_formats(cid, video_id, record_id=record_id)
4a733545
AS
208 return info_dict
209
9b8b7a7b
AS
210 def _extract_live(self, channel_name):
211 cid = self._extract_cid(channel_name, channel_name)
212 formats, subtitles = self._extract_formats(cid, cid, is_live=True)
213 return {
214 'id': channel_name,
215 'title': channel_name,
216 'is_live': True,
520876fa 217 'formats': formats,
add96eb9 218 'subtitles': subtitles,
9b8b7a7b 219 }
4a733545 220
9b8b7a7b
AS
221 def _extract_record(self, record_id):
222 video_id = self._extract_video_id_from_recording(record_id)
223 cid, info_dict = self._extract_cid_and_video_info(video_id)
224 info_dict['formats'], info_dict['subtitles'] = self._extract_formats(cid, video_id, record_id=record_id)
225 return info_dict
67ca1a8e 226
9b8b7a7b
AS
227 def _extract_ondemand(self, ondemand_id):
228 ondemand_termtoken, ondemand_type, info_dict = self._extract_ondemand_info(ondemand_id)
229 info_dict['formats'], info_dict['subtitles'] = self._extract_formats(
230 None, ondemand_id, ondemand_id=ondemand_id,
231 ondemand_termtoken=ondemand_termtoken, ondemand_type=ondemand_type)
232 return info_dict
67ca1a8e 233
1155ecef 234 def _real_extract(self, url):
48732bec 235 video_id, record_id = self._match_valid_url(url).groups()
f60ef663 236 return getattr(self, f'_extract_{self._TYPE}')(video_id or record_id)
1155ecef 237
4a733545 238
f60ef663
AS
239def _create_valid_url(host, match, qs, base_re=None):
240 match_base = fr'|{base_re}/(?P<vid1>{match})' if base_re else '(?P<vid1>)'
241 return rf'''(?x)https?://(?:www\.)?{re.escape(host)}/(?:
242 [^?#]+\?(?:[^#]+&)?{qs}=(?P<vid2>{match})
243 {match_base}
244 )'''
4a733545
AS
245
246
f6d7f7b4
S
247class ZattooBaseIE(ZattooPlatformBaseIE):
248 _NETRC_MACHINE = 'zattoo'
249 _HOST = 'zattoo.com'
250
f6d7f7b4 251
4a733545 252class ZattooIE(ZattooBaseIE):
f60ef663 253 _VALID_URL = _create_valid_url(ZattooBaseIE._HOST, r'\d+', 'program', '(?:program|watch)/[^/]+')
9b8b7a7b 254 _TYPE = 'video'
4a733545 255 _TESTS = [{
9b8b7a7b
AS
256 'url': 'https://zattoo.com/program/zdf/250170418',
257 'info_dict': {
258 'id': '250170418',
259 'ext': 'mp4',
260 'title': 'Markus Lanz',
261 'description': 'md5:e41cb1257de008ca62a73bb876ffa7fc',
262 'thumbnail': 're:http://images.zattic.com/cms/.+/format_480x360.jpg',
263 'creator': 'ZDF HD',
264 'release_year': 2022,
265 'episode': 'Folge 1655',
266 'categories': 'count:1',
add96eb9 267 'tags': 'count:2',
9b8b7a7b 268 },
add96eb9 269 'params': {'skip_download': 'm3u8'},
9b8b7a7b
AS
270 }, {
271 'url': 'https://zattoo.com/program/daserste/210177916',
4a733545
AS
272 'only_matching': True,
273 }, {
9b8b7a7b 274 'url': 'https://zattoo.com/guide/german?channel=srf1&program=169860555',
4a733545
AS
275 'only_matching': True,
276 }]
277
4a733545
AS
278
279class ZattooLiveIE(ZattooBaseIE):
f60ef663 280 _VALID_URL = _create_valid_url(ZattooBaseIE._HOST, r'[^/?&#]+', 'channel', 'live')
9b8b7a7b
AS
281 _TYPE = 'live'
282 _TESTS = [{
283 'url': 'https://zattoo.com/channels/german?channel=srf_zwei',
4a733545 284 'only_matching': True,
9b8b7a7b
AS
285 }, {
286 'url': 'https://zattoo.com/live/srf1',
287 'only_matching': True,
288 }]
4a733545 289
67ca1a8e
S
290 @classmethod
291 def suitable(cls, url):
9b8b7a7b 292 return False if ZattooIE.suitable(url) else super().suitable(url)
67ca1a8e 293
9b8b7a7b
AS
294
295class ZattooMoviesIE(ZattooBaseIE):
f60ef663 296 _VALID_URL = _create_valid_url(ZattooBaseIE._HOST, r'\w+', 'movie_id', 'vod/movies')
9b8b7a7b
AS
297 _TYPE = 'ondemand'
298 _TESTS = [{
299 'url': 'https://zattoo.com/vod/movies/7521',
300 'only_matching': True,
301 }, {
302 'url': 'https://zattoo.com/ondemand?movie_id=7521&term_token=9f00f43183269484edde',
303 'only_matching': True,
304 }]
f6d7f7b4
S
305
306
9b8b7a7b 307class ZattooRecordingsIE(ZattooBaseIE):
f60ef663 308 _VALID_URL = _create_valid_url('zattoo.com', r'\d+', 'recording')
9b8b7a7b
AS
309 _TYPE = 'record'
310 _TESTS = [{
311 'url': 'https://zattoo.com/recordings?recording=193615508',
312 'only_matching': True,
313 }, {
314 'url': 'https://zattoo.com/tc/ptc_recordings_all_recordings?recording=193615420',
315 'only_matching': True,
316 }]
317
318
f60ef663 319class NetPlusTVBaseIE(ZattooPlatformBaseIE):
a831c2ea 320 _NETRC_MACHINE = 'netplus'
f6d7f7b4 321 _HOST = 'netplus.tv'
add96eb9 322 _API_HOST = f'www.{_HOST}'
f6d7f7b4 323
f60ef663
AS
324
325class NetPlusTVIE(NetPlusTVBaseIE):
326 _VALID_URL = _create_valid_url(NetPlusTVBaseIE._HOST, r'\d+', 'program', '(?:program|watch)/[^/]+')
327 _TYPE = 'video'
328 _TESTS = [{
329 'url': 'https://netplus.tv/program/daserste/210177916',
330 'only_matching': True,
331 }, {
332 'url': 'https://netplus.tv/guide/german?channel=srf1&program=169860555',
333 'only_matching': True,
334 }]
335
336
337class NetPlusTVLiveIE(NetPlusTVBaseIE):
338 _VALID_URL = _create_valid_url(NetPlusTVBaseIE._HOST, r'[^/?&#]+', 'channel', 'live')
339 _TYPE = 'live'
f6d7f7b4 340 _TESTS = [{
f60ef663
AS
341 'url': 'https://netplus.tv/channels/german?channel=srf_zwei',
342 'only_matching': True,
343 }, {
344 'url': 'https://netplus.tv/live/srf1',
f6d7f7b4
S
345 'only_matching': True,
346 }]
347
f60ef663
AS
348 @classmethod
349 def suitable(cls, url):
350 return False if NetPlusTVIE.suitable(url) else super().suitable(url)
351
f6d7f7b4 352
f60ef663
AS
353class NetPlusTVRecordingsIE(NetPlusTVBaseIE):
354 _VALID_URL = _create_valid_url(NetPlusTVBaseIE._HOST, r'\d+', 'recording')
355 _TYPE = 'record'
356 _TESTS = [{
357 'url': 'https://netplus.tv/recordings?recording=193615508',
358 'only_matching': True,
359 }, {
360 'url': 'https://netplus.tv/tc/ptc_recordings_all_recordings?recording=193615420',
361 'only_matching': True,
362 }]
363
364
365class MNetTVBaseIE(ZattooPlatformBaseIE):
f6d7f7b4
S
366 _NETRC_MACHINE = 'mnettv'
367 _HOST = 'tvplus.m-net.de'
f6d7f7b4 368
f60ef663
AS
369
370class MNetTVIE(MNetTVBaseIE):
371 _VALID_URL = _create_valid_url(MNetTVBaseIE._HOST, r'\d+', 'program', '(?:program|watch)/[^/]+')
372 _TYPE = 'video'
373 _TESTS = [{
374 'url': 'https://tvplus.m-net.de/program/daserste/210177916',
375 'only_matching': True,
376 }, {
377 'url': 'https://tvplus.m-net.de/guide/german?channel=srf1&program=169860555',
378 'only_matching': True,
379 }]
380
381
382class MNetTVLiveIE(MNetTVBaseIE):
383 _VALID_URL = _create_valid_url(MNetTVBaseIE._HOST, r'[^/?&#]+', 'channel', 'live')
384 _TYPE = 'live'
385 _TESTS = [{
386 'url': 'https://tvplus.m-net.de/channels/german?channel=srf_zwei',
387 'only_matching': True,
388 }, {
389 'url': 'https://tvplus.m-net.de/live/srf1',
390 'only_matching': True,
391 }]
392
393 @classmethod
394 def suitable(cls, url):
395 return False if MNetTVIE.suitable(url) else super().suitable(url)
396
397
398class MNetTVRecordingsIE(MNetTVBaseIE):
399 _VALID_URL = _create_valid_url(MNetTVBaseIE._HOST, r'\d+', 'recording')
400 _TYPE = 'record'
f6d7f7b4 401 _TESTS = [{
f60ef663
AS
402 'url': 'https://tvplus.m-net.de/recordings?recording=193615508',
403 'only_matching': True,
404 }, {
405 'url': 'https://tvplus.m-net.de/tc/ptc_recordings_all_recordings?recording=193615420',
f6d7f7b4
S
406 'only_matching': True,
407 }]
408
409
f60ef663 410class WalyTVBaseIE(ZattooPlatformBaseIE):
f6d7f7b4
S
411 _NETRC_MACHINE = 'walytv'
412 _HOST = 'player.waly.tv'
f6d7f7b4 413
f60ef663
AS
414
415class WalyTVIE(WalyTVBaseIE):
416 _VALID_URL = _create_valid_url(WalyTVBaseIE._HOST, r'\d+', 'program', '(?:program|watch)/[^/]+')
417 _TYPE = 'video'
418 _TESTS = [{
419 'url': 'https://player.waly.tv/program/daserste/210177916',
420 'only_matching': True,
421 }, {
422 'url': 'https://player.waly.tv/guide/german?channel=srf1&program=169860555',
423 'only_matching': True,
424 }]
425
426
427class WalyTVLiveIE(WalyTVBaseIE):
428 _VALID_URL = _create_valid_url(WalyTVBaseIE._HOST, r'[^/?&#]+', 'channel', 'live')
429 _TYPE = 'live'
430 _TESTS = [{
431 'url': 'https://player.waly.tv/channels/german?channel=srf_zwei',
432 'only_matching': True,
433 }, {
434 'url': 'https://player.waly.tv/live/srf1',
435 'only_matching': True,
436 }]
437
438 @classmethod
439 def suitable(cls, url):
440 return False if WalyTVIE.suitable(url) else super().suitable(url)
441
442
443class WalyTVRecordingsIE(WalyTVBaseIE):
444 _VALID_URL = _create_valid_url(WalyTVBaseIE._HOST, r'\d+', 'recording')
445 _TYPE = 'record'
f6d7f7b4 446 _TESTS = [{
f60ef663
AS
447 'url': 'https://player.waly.tv/recordings?recording=193615508',
448 'only_matching': True,
449 }, {
450 'url': 'https://player.waly.tv/tc/ptc_recordings_all_recordings?recording=193615420',
f6d7f7b4
S
451 'only_matching': True,
452 }]
453
454
f60ef663 455class BBVTVBaseIE(ZattooPlatformBaseIE):
f6d7f7b4
S
456 _NETRC_MACHINE = 'bbvtv'
457 _HOST = 'bbv-tv.net'
add96eb9 458 _API_HOST = f'www.{_HOST}'
f6d7f7b4 459
f60ef663
AS
460
461class BBVTVIE(BBVTVBaseIE):
462 _VALID_URL = _create_valid_url(BBVTVBaseIE._HOST, r'\d+', 'program', '(?:program|watch)/[^/]+')
463 _TYPE = 'video'
464 _TESTS = [{
465 'url': 'https://bbv-tv.net/program/daserste/210177916',
466 'only_matching': True,
467 }, {
468 'url': 'https://bbv-tv.net/guide/german?channel=srf1&program=169860555',
469 'only_matching': True,
470 }]
471
472
473class BBVTVLiveIE(BBVTVBaseIE):
474 _VALID_URL = _create_valid_url(BBVTVBaseIE._HOST, r'[^/?&#]+', 'channel', 'live')
475 _TYPE = 'live'
f6d7f7b4 476 _TESTS = [{
f60ef663
AS
477 'url': 'https://bbv-tv.net/channels/german?channel=srf_zwei',
478 'only_matching': True,
479 }, {
480 'url': 'https://bbv-tv.net/live/srf1',
f6d7f7b4
S
481 'only_matching': True,
482 }]
483
f60ef663
AS
484 @classmethod
485 def suitable(cls, url):
486 return False if BBVTVIE.suitable(url) else super().suitable(url)
f6d7f7b4 487
f60ef663
AS
488
489class BBVTVRecordingsIE(BBVTVBaseIE):
490 _VALID_URL = _create_valid_url(BBVTVBaseIE._HOST, r'\d+', 'recording')
491 _TYPE = 'record'
492 _TESTS = [{
493 'url': 'https://bbv-tv.net/recordings?recording=193615508',
494 'only_matching': True,
495 }, {
496 'url': 'https://bbv-tv.net/tc/ptc_recordings_all_recordings?recording=193615420',
497 'only_matching': True,
498 }]
499
500
501class VTXTVBaseIE(ZattooPlatformBaseIE):
f6d7f7b4
S
502 _NETRC_MACHINE = 'vtxtv'
503 _HOST = 'vtxtv.ch'
add96eb9 504 _API_HOST = f'www.{_HOST}'
f6d7f7b4 505
f60ef663
AS
506
507class VTXTVIE(VTXTVBaseIE):
508 _VALID_URL = _create_valid_url(VTXTVBaseIE._HOST, r'\d+', 'program', '(?:program|watch)/[^/]+')
509 _TYPE = 'video'
510 _TESTS = [{
511 'url': 'https://vtxtv.ch/program/daserste/210177916',
512 'only_matching': True,
513 }, {
514 'url': 'https://vtxtv.ch/guide/german?channel=srf1&program=169860555',
515 'only_matching': True,
516 }]
517
518
519class VTXTVLiveIE(VTXTVBaseIE):
520 _VALID_URL = _create_valid_url(VTXTVBaseIE._HOST, r'[^/?&#]+', 'channel', 'live')
521 _TYPE = 'live'
522 _TESTS = [{
523 'url': 'https://vtxtv.ch/channels/german?channel=srf_zwei',
524 'only_matching': True,
525 }, {
526 'url': 'https://vtxtv.ch/live/srf1',
527 'only_matching': True,
528 }]
529
530 @classmethod
531 def suitable(cls, url):
532 return False if VTXTVIE.suitable(url) else super().suitable(url)
533
534
535class VTXTVRecordingsIE(VTXTVBaseIE):
536 _VALID_URL = _create_valid_url(VTXTVBaseIE._HOST, r'\d+', 'recording')
537 _TYPE = 'record'
f6d7f7b4 538 _TESTS = [{
f60ef663
AS
539 'url': 'https://vtxtv.ch/recordings?recording=193615508',
540 'only_matching': True,
541 }, {
542 'url': 'https://vtxtv.ch/tc/ptc_recordings_all_recordings?recording=193615420',
f6d7f7b4
S
543 'only_matching': True,
544 }]
545
546
f60ef663 547class GlattvisionTVBaseIE(ZattooPlatformBaseIE):
f6d7f7b4
S
548 _NETRC_MACHINE = 'glattvisiontv'
549 _HOST = 'iptv.glattvision.ch'
f6d7f7b4 550
f60ef663
AS
551
552class GlattvisionTVIE(GlattvisionTVBaseIE):
553 _VALID_URL = _create_valid_url(GlattvisionTVBaseIE._HOST, r'\d+', 'program', '(?:program|watch)/[^/]+')
554 _TYPE = 'video'
f6d7f7b4 555 _TESTS = [{
f60ef663
AS
556 'url': 'https://iptv.glattvision.ch/program/daserste/210177916',
557 'only_matching': True,
558 }, {
559 'url': 'https://iptv.glattvision.ch/guide/german?channel=srf1&program=169860555',
f6d7f7b4
S
560 'only_matching': True,
561 }]
562
563
f60ef663
AS
564class GlattvisionTVLiveIE(GlattvisionTVBaseIE):
565 _VALID_URL = _create_valid_url(GlattvisionTVBaseIE._HOST, r'[^/?&#]+', 'channel', 'live')
566 _TYPE = 'live'
567 _TESTS = [{
568 'url': 'https://iptv.glattvision.ch/channels/german?channel=srf_zwei',
569 'only_matching': True,
570 }, {
571 'url': 'https://iptv.glattvision.ch/live/srf1',
572 'only_matching': True,
573 }]
574
575 @classmethod
576 def suitable(cls, url):
577 return False if GlattvisionTVIE.suitable(url) else super().suitable(url)
578
579
580class GlattvisionTVRecordingsIE(GlattvisionTVBaseIE):
581 _VALID_URL = _create_valid_url(GlattvisionTVBaseIE._HOST, r'\d+', 'recording')
582 _TYPE = 'record'
583 _TESTS = [{
584 'url': 'https://iptv.glattvision.ch/recordings?recording=193615508',
585 'only_matching': True,
586 }, {
587 'url': 'https://iptv.glattvision.ch/tc/ptc_recordings_all_recordings?recording=193615420',
588 'only_matching': True,
589 }]
590
591
592class SAKTVBaseIE(ZattooPlatformBaseIE):
f6d7f7b4
S
593 _NETRC_MACHINE = 'saktv'
594 _HOST = 'saktv.ch'
add96eb9 595 _API_HOST = f'www.{_HOST}'
f6d7f7b4 596
f60ef663
AS
597
598class SAKTVIE(SAKTVBaseIE):
599 _VALID_URL = _create_valid_url(SAKTVBaseIE._HOST, r'\d+', 'program', '(?:program|watch)/[^/]+')
600 _TYPE = 'video'
601 _TESTS = [{
602 'url': 'https://saktv.ch/program/daserste/210177916',
603 'only_matching': True,
604 }, {
605 'url': 'https://saktv.ch/guide/german?channel=srf1&program=169860555',
606 'only_matching': True,
607 }]
608
609
610class SAKTVLiveIE(SAKTVBaseIE):
611 _VALID_URL = _create_valid_url(SAKTVBaseIE._HOST, r'[^/?&#]+', 'channel', 'live')
612 _TYPE = 'live'
f6d7f7b4 613 _TESTS = [{
f60ef663
AS
614 'url': 'https://saktv.ch/channels/german?channel=srf_zwei',
615 'only_matching': True,
616 }, {
617 'url': 'https://saktv.ch/live/srf1',
f6d7f7b4
S
618 'only_matching': True,
619 }]
620
f60ef663
AS
621 @classmethod
622 def suitable(cls, url):
623 return False if SAKTVIE.suitable(url) else super().suitable(url)
624
f6d7f7b4 625
f60ef663
AS
626class SAKTVRecordingsIE(SAKTVBaseIE):
627 _VALID_URL = _create_valid_url(SAKTVBaseIE._HOST, r'\d+', 'recording')
628 _TYPE = 'record'
629 _TESTS = [{
630 'url': 'https://saktv.ch/recordings?recording=193615508',
631 'only_matching': True,
632 }, {
633 'url': 'https://saktv.ch/tc/ptc_recordings_all_recordings?recording=193615420',
634 'only_matching': True,
635 }]
636
637
638class EWETVBaseIE(ZattooPlatformBaseIE):
f6d7f7b4
S
639 _NETRC_MACHINE = 'ewetv'
640 _HOST = 'tvonline.ewe.de'
f6d7f7b4 641
f60ef663
AS
642
643class EWETVIE(EWETVBaseIE):
644 _VALID_URL = _create_valid_url(EWETVBaseIE._HOST, r'\d+', 'program', '(?:program|watch)/[^/]+')
645 _TYPE = 'video'
646 _TESTS = [{
647 'url': 'https://tvonline.ewe.de/program/daserste/210177916',
648 'only_matching': True,
649 }, {
650 'url': 'https://tvonline.ewe.de/guide/german?channel=srf1&program=169860555',
651 'only_matching': True,
652 }]
653
654
655class EWETVLiveIE(EWETVBaseIE):
656 _VALID_URL = _create_valid_url(EWETVBaseIE._HOST, r'[^/?&#]+', 'channel', 'live')
657 _TYPE = 'live'
658 _TESTS = [{
659 'url': 'https://tvonline.ewe.de/channels/german?channel=srf_zwei',
660 'only_matching': True,
661 }, {
662 'url': 'https://tvonline.ewe.de/live/srf1',
663 'only_matching': True,
664 }]
665
666 @classmethod
667 def suitable(cls, url):
668 return False if EWETVIE.suitable(url) else super().suitable(url)
669
670
671class EWETVRecordingsIE(EWETVBaseIE):
672 _VALID_URL = _create_valid_url(EWETVBaseIE._HOST, r'\d+', 'recording')
673 _TYPE = 'record'
f6d7f7b4 674 _TESTS = [{
f60ef663
AS
675 'url': 'https://tvonline.ewe.de/recordings?recording=193615508',
676 'only_matching': True,
677 }, {
678 'url': 'https://tvonline.ewe.de/tc/ptc_recordings_all_recordings?recording=193615420',
f6d7f7b4
S
679 'only_matching': True,
680 }]
681
682
f60ef663 683class QuantumTVBaseIE(ZattooPlatformBaseIE):
f6d7f7b4
S
684 _NETRC_MACHINE = 'quantumtv'
685 _HOST = 'quantum-tv.com'
add96eb9 686 _API_HOST = f'www.{_HOST}'
f6d7f7b4 687
f60ef663
AS
688
689class QuantumTVIE(QuantumTVBaseIE):
690 _VALID_URL = _create_valid_url(QuantumTVBaseIE._HOST, r'\d+', 'program', '(?:program|watch)/[^/]+')
691 _TYPE = 'video'
f6d7f7b4 692 _TESTS = [{
f60ef663
AS
693 'url': 'https://quantum-tv.com/program/daserste/210177916',
694 'only_matching': True,
695 }, {
696 'url': 'https://quantum-tv.com/guide/german?channel=srf1&program=169860555',
697 'only_matching': True,
698 }]
699
700
701class QuantumTVLiveIE(QuantumTVBaseIE):
702 _VALID_URL = _create_valid_url(QuantumTVBaseIE._HOST, r'[^/?&#]+', 'channel', 'live')
703 _TYPE = 'live'
704 _TESTS = [{
705 'url': 'https://quantum-tv.com/channels/german?channel=srf_zwei',
706 'only_matching': True,
707 }, {
708 'url': 'https://quantum-tv.com/live/srf1',
709 'only_matching': True,
710 }]
711
712 @classmethod
713 def suitable(cls, url):
714 return False if QuantumTVIE.suitable(url) else super().suitable(url)
715
716
717class QuantumTVRecordingsIE(QuantumTVBaseIE):
718 _VALID_URL = _create_valid_url(QuantumTVBaseIE._HOST, r'\d+', 'recording')
719 _TYPE = 'record'
720 _TESTS = [{
721 'url': 'https://quantum-tv.com/recordings?recording=193615508',
722 'only_matching': True,
723 }, {
724 'url': 'https://quantum-tv.com/tc/ptc_recordings_all_recordings?recording=193615420',
f6d7f7b4
S
725 'only_matching': True,
726 }]
727
728
f60ef663 729class OsnatelTVBaseIE(ZattooPlatformBaseIE):
f6d7f7b4 730 _NETRC_MACHINE = 'osnateltv'
2004e221 731 _HOST = 'tvonline.osnatel.de'
f6d7f7b4 732
f60ef663
AS
733
734class OsnatelTVIE(OsnatelTVBaseIE):
735 _VALID_URL = _create_valid_url(OsnatelTVBaseIE._HOST, r'\d+', 'program', '(?:program|watch)/[^/]+')
736 _TYPE = 'video'
737 _TESTS = [{
738 'url': 'https://tvonline.osnatel.de/program/daserste/210177916',
739 'only_matching': True,
740 }, {
741 'url': 'https://tvonline.osnatel.de/guide/german?channel=srf1&program=169860555',
742 'only_matching': True,
743 }]
744
745
746class OsnatelTVLiveIE(OsnatelTVBaseIE):
747 _VALID_URL = _create_valid_url(OsnatelTVBaseIE._HOST, r'[^/?&#]+', 'channel', 'live')
748 _TYPE = 'live'
f6d7f7b4 749 _TESTS = [{
f60ef663
AS
750 'url': 'https://tvonline.osnatel.de/channels/german?channel=srf_zwei',
751 'only_matching': True,
752 }, {
753 'url': 'https://tvonline.osnatel.de/live/srf1',
f6d7f7b4
S
754 'only_matching': True,
755 }]
756
f60ef663
AS
757 @classmethod
758 def suitable(cls, url):
759 return False if OsnatelTVIE.suitable(url) else super().suitable(url)
f6d7f7b4 760
f60ef663
AS
761
762class OsnatelTVRecordingsIE(OsnatelTVBaseIE):
763 _VALID_URL = _create_valid_url(OsnatelTVBaseIE._HOST, r'\d+', 'recording')
764 _TYPE = 'record'
765 _TESTS = [{
766 'url': 'https://tvonline.osnatel.de/recordings?recording=193615508',
767 'only_matching': True,
768 }, {
769 'url': 'https://tvonline.osnatel.de/tc/ptc_recordings_all_recordings?recording=193615420',
770 'only_matching': True,
771 }]
772
773
774class EinsUndEinsTVBaseIE(ZattooPlatformBaseIE):
f6d7f7b4
S
775 _NETRC_MACHINE = '1und1tv'
776 _HOST = '1und1.tv'
add96eb9 777 _API_HOST = f'www.{_HOST}'
f6d7f7b4 778
f60ef663
AS
779
780class EinsUndEinsTVIE(EinsUndEinsTVBaseIE):
781 _VALID_URL = _create_valid_url(EinsUndEinsTVBaseIE._HOST, r'\d+', 'program', '(?:program|watch)/[^/]+')
782 _TYPE = 'video'
f6d7f7b4 783 _TESTS = [{
f60ef663
AS
784 'url': 'https://1und1.tv/program/daserste/210177916',
785 'only_matching': True,
786 }, {
787 'url': 'https://1und1.tv/guide/german?channel=srf1&program=169860555',
f6d7f7b4
S
788 'only_matching': True,
789 }]
a81daba2
AS
790
791
f60ef663
AS
792class EinsUndEinsTVLiveIE(EinsUndEinsTVBaseIE):
793 _VALID_URL = _create_valid_url(EinsUndEinsTVBaseIE._HOST, r'[^/?&#]+', 'channel', 'live')
794 _TYPE = 'live'
795 _TESTS = [{
796 'url': 'https://1und1.tv/channels/german?channel=srf_zwei',
797 'only_matching': True,
798 }, {
799 'url': 'https://1und1.tv/live/srf1',
800 'only_matching': True,
801 }]
802
803 @classmethod
804 def suitable(cls, url):
805 return False if EinsUndEinsTVIE.suitable(url) else super().suitable(url)
806
807
808class EinsUndEinsTVRecordingsIE(EinsUndEinsTVBaseIE):
809 _VALID_URL = _create_valid_url(EinsUndEinsTVBaseIE._HOST, r'\d+', 'recording')
810 _TYPE = 'record'
811 _TESTS = [{
812 'url': 'https://1und1.tv/recordings?recording=193615508',
813 'only_matching': True,
814 }, {
815 'url': 'https://1und1.tv/tc/ptc_recordings_all_recordings?recording=193615420',
816 'only_matching': True,
817 }]
818
819
820class SaltTVBaseIE(ZattooPlatformBaseIE):
a81daba2
AS
821 _NETRC_MACHINE = 'salttv'
822 _HOST = 'tv.salt.ch'
a81daba2 823
f60ef663
AS
824
825class SaltTVIE(SaltTVBaseIE):
826 _VALID_URL = _create_valid_url(SaltTVBaseIE._HOST, r'\d+', 'program', '(?:program|watch)/[^/]+')
827 _TYPE = 'video'
828 _TESTS = [{
829 'url': 'https://tv.salt.ch/program/daserste/210177916',
830 'only_matching': True,
831 }, {
832 'url': 'https://tv.salt.ch/guide/german?channel=srf1&program=169860555',
833 'only_matching': True,
834 }]
835
836
837class SaltTVLiveIE(SaltTVBaseIE):
838 _VALID_URL = _create_valid_url(SaltTVBaseIE._HOST, r'[^/?&#]+', 'channel', 'live')
839 _TYPE = 'live'
a81daba2 840 _TESTS = [{
f60ef663
AS
841 'url': 'https://tv.salt.ch/channels/german?channel=srf_zwei',
842 'only_matching': True,
843 }, {
844 'url': 'https://tv.salt.ch/live/srf1',
845 'only_matching': True,
846 }]
847
848 @classmethod
849 def suitable(cls, url):
850 return False if SaltTVIE.suitable(url) else super().suitable(url)
851
852
853class SaltTVRecordingsIE(SaltTVBaseIE):
854 _VALID_URL = _create_valid_url(SaltTVBaseIE._HOST, r'\d+', 'recording')
855 _TYPE = 'record'
856 _TESTS = [{
857 'url': 'https://tv.salt.ch/recordings?recording=193615508',
858 'only_matching': True,
859 }, {
860 'url': 'https://tv.salt.ch/tc/ptc_recordings_all_recordings?recording=193615420',
a81daba2
AS
861 'only_matching': True,
862 }]