[ie/wistia] Support password-protected videos (#11100)
[yt-dlp3.git] / yt_dlp / extractor / zattoo.py
blob161804b604cef88560e6e7d4734011fb09b0b6b4
1 import re
2 import uuid
4 from .common import InfoExtractor
5 from ..networking.exceptions import HTTPError
6 from ..utils import (
7 ExtractorError,
8 int_or_none,
9 join_nonempty,
10 try_get,
11 url_or_none,
12 urlencode_postdata,
16 class ZattooPlatformBaseIE(InfoExtractor):
17 _power_guide_hash = None
19 def _host_url(self):
20 return 'https://%s' % (self._API_HOST if hasattr(self, '_API_HOST') else self._HOST)
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')
26 def _perform_login(self, username, password):
27 try:
28 data = self._download_json(
29 f'{self._host_url()}/zapi/v2/account/login', None, 'Logging in',
30 data=urlencode_postdata({
31 'login': username,
32 'password': password,
33 'remember': 'true',
34 }), headers={
35 'Referer': f'{self._host_url()}/login',
36 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8',
38 except ExtractorError as e:
39 if isinstance(e.cause, HTTPError) and e.cause.status == 400:
40 raise ExtractorError(
41 'Unable to login: incorrect username and/or password',
42 expected=True)
43 raise
45 self._power_guide_hash = data['session']['power_guide_hash']
47 def _initialize_pre_login(self):
48 session_token = self._download_json(
49 f'{self._host_url()}/token.json', None, 'Downloading session token')['session_token']
51 # Will setup appropriate cookies
52 self._request_webpage(
53 f'{self._host_url()}/zapi/v3/session/hello', None,
54 'Opening session', data=urlencode_postdata({
55 'uuid': str(uuid.uuid4()),
56 'lang': 'en',
57 'app_version': '1.8.2',
58 'format': 'json',
59 'client_app_token': session_token,
60 }))
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')
72 def _extract_cid(self, video_id, channel_name):
73 channel_groups = self._download_json(
74 f'{self._host_url()}/zapi/v2/cached/channels/{self._power_guide_hash}',
75 video_id, 'Downloading channel list',
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
83 if chan.get('cid') and (
84 chan.get('display_alias') == channel_name
85 or chan.get('cid') == channel_name))
86 except StopIteration:
87 raise ExtractorError('Could not extract channel id')
89 def _extract_cid_and_video_info(self, video_id):
90 data = self._download_json(
91 f'{self._host_url()}/zapi/v2/cached/program/power_details/{self._power_guide_hash}',
92 video_id,
93 'Downloading video information',
94 query={
95 'program_ids': video_id,
96 'complete': True,
99 p = data['programs'][0]
100 cid = p['cid']
102 info_dict = {
103 'id': video_id,
104 'title': p.get('t') or p['et'],
105 'description': p.get('d'),
106 'thumbnail': p.get('i_url'),
107 'creator': p.get('channel_name'),
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')),
111 'release_year': int_or_none(p.get('year')),
112 'categories': try_get(p, lambda x: x['c'], list),
113 'tags': try_get(p, lambda x: x['g'], list),
116 return cid, info_dict
118 def _extract_ondemand_info(self, ondemand_id):
120 @returns (ondemand_token, ondemand_type, info_dict)
122 data = self._download_json(
123 f'{self._host_url()}/zapi/vod/movies/{ondemand_id}',
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),
135 return data['terms_catalog'][0]['terms'][0]['token'], data['type'], info_dict
137 def _extract_formats(self, cid, video_id, record_id=None, ondemand_id=None, ondemand_termtoken=None, ondemand_type=None, is_live=False):
138 postdata_common = {
139 'https_watch_urls': True,
142 if is_live:
143 postdata_common.update({'timeshift': 10800})
144 url = f'{self._host_url()}/zapi/watch/live/{cid}'
145 elif record_id:
146 url = f'{self._host_url()}/zapi/watch/recording/{record_id}'
147 elif ondemand_id:
148 postdata_common.update({
149 'teasable_id': ondemand_id,
150 'term_token': ondemand_termtoken,
151 'teasable_type': ondemand_type,
153 url = f'{self._host_url()}/zapi/watch/vod/video'
154 else:
155 url = f'{self._host_url()}/zapi/v3/watch/replay/{cid}/{video_id}'
156 formats = []
157 subtitles = {}
158 for stream_type in ('dash', 'hls7'):
159 postdata = postdata_common.copy()
160 postdata['stream_type'] = stream_type
162 data = self._download_json(
163 url, video_id, f'Downloading {stream_type.upper()} formats',
164 data=urlencode_postdata(postdata), fatal=False)
165 if not data:
166 continue
168 watch_urls = try_get(
169 data, lambda x: x['stream']['watch_urls'], list)
170 if not watch_urls:
171 continue
173 for watch in watch_urls:
174 if not isinstance(watch, dict):
175 continue
176 watch_url = url_or_none(watch.get('url'))
177 if not watch_url:
178 continue
179 audio_channel = watch.get('audio_channel')
180 preference = 1 if audio_channel == 'A' else None
181 format_id = join_nonempty(stream_type, watch.get('maxrate'), audio_channel)
182 if stream_type.startswith('dash'):
183 this_formats, subs = self._extract_mpd_formats_and_subtitles(
184 watch_url, video_id, mpd_id=format_id, fatal=False)
185 self._merge_subtitles(subs, target=subtitles)
186 elif stream_type.startswith('hls'):
187 this_formats, subs = self._extract_m3u8_formats_and_subtitles(
188 watch_url, video_id, 'mp4',
189 entry_protocol='m3u8_native', m3u8_id=format_id,
190 fatal=False)
191 self._merge_subtitles(subs, target=subtitles)
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:
201 this_format['quality'] = preference
202 formats.extend(this_formats)
203 return formats, subtitles
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)
208 return info_dict
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,
217 'formats': formats,
218 'subtitles': subtitles,
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
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
234 def _real_extract(self, url):
235 video_id, record_id = self._match_valid_url(url).groups()
236 return getattr(self, f'_extract_{self._TYPE}')(video_id or record_id)
239 def _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 )'''
247 class ZattooBaseIE(ZattooPlatformBaseIE):
248 _NETRC_MACHINE = 'zattoo'
249 _HOST = 'zattoo.com'
252 class ZattooIE(ZattooBaseIE):
253 _VALID_URL = _create_valid_url(ZattooBaseIE._HOST, r'\d+', 'program', '(?:program|watch)/[^/]+')
254 _TYPE = 'video'
255 _TESTS = [{
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',
267 'tags': 'count:2',
269 'params': {'skip_download': 'm3u8'},
270 }, {
271 'url': 'https://zattoo.com/program/daserste/210177916',
272 'only_matching': True,
273 }, {
274 'url': 'https://zattoo.com/guide/german?channel=srf1&program=169860555',
275 'only_matching': True,
279 class ZattooLiveIE(ZattooBaseIE):
280 _VALID_URL = _create_valid_url(ZattooBaseIE._HOST, r'[^/?&#]+', 'channel', 'live')
281 _TYPE = 'live'
282 _TESTS = [{
283 'url': 'https://zattoo.com/channels/german?channel=srf_zwei',
284 'only_matching': True,
285 }, {
286 'url': 'https://zattoo.com/live/srf1',
287 'only_matching': True,
290 @classmethod
291 def suitable(cls, url):
292 return False if ZattooIE.suitable(url) else super().suitable(url)
295 class ZattooMoviesIE(ZattooBaseIE):
296 _VALID_URL = _create_valid_url(ZattooBaseIE._HOST, r'\w+', 'movie_id', 'vod/movies')
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,
307 class ZattooRecordingsIE(ZattooBaseIE):
308 _VALID_URL = _create_valid_url('zattoo.com', r'\d+', 'recording')
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,
319 class NetPlusTVBaseIE(ZattooPlatformBaseIE):
320 _NETRC_MACHINE = 'netplus'
321 _HOST = 'netplus.tv'
322 _API_HOST = f'www.{_HOST}'
325 class 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,
337 class NetPlusTVLiveIE(NetPlusTVBaseIE):
338 _VALID_URL = _create_valid_url(NetPlusTVBaseIE._HOST, r'[^/?&#]+', 'channel', 'live')
339 _TYPE = 'live'
340 _TESTS = [{
341 'url': 'https://netplus.tv/channels/german?channel=srf_zwei',
342 'only_matching': True,
343 }, {
344 'url': 'https://netplus.tv/live/srf1',
345 'only_matching': True,
348 @classmethod
349 def suitable(cls, url):
350 return False if NetPlusTVIE.suitable(url) else super().suitable(url)
353 class 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,
365 class MNetTVBaseIE(ZattooPlatformBaseIE):
366 _NETRC_MACHINE = 'mnettv'
367 _HOST = 'tvplus.m-net.de'
370 class 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,
382 class 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,
393 @classmethod
394 def suitable(cls, url):
395 return False if MNetTVIE.suitable(url) else super().suitable(url)
398 class MNetTVRecordingsIE(MNetTVBaseIE):
399 _VALID_URL = _create_valid_url(MNetTVBaseIE._HOST, r'\d+', 'recording')
400 _TYPE = 'record'
401 _TESTS = [{
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',
406 'only_matching': True,
410 class WalyTVBaseIE(ZattooPlatformBaseIE):
411 _NETRC_MACHINE = 'walytv'
412 _HOST = 'player.waly.tv'
415 class 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,
427 class 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,
438 @classmethod
439 def suitable(cls, url):
440 return False if WalyTVIE.suitable(url) else super().suitable(url)
443 class WalyTVRecordingsIE(WalyTVBaseIE):
444 _VALID_URL = _create_valid_url(WalyTVBaseIE._HOST, r'\d+', 'recording')
445 _TYPE = 'record'
446 _TESTS = [{
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',
451 'only_matching': True,
455 class BBVTVBaseIE(ZattooPlatformBaseIE):
456 _NETRC_MACHINE = 'bbvtv'
457 _HOST = 'bbv-tv.net'
458 _API_HOST = f'www.{_HOST}'
461 class 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,
473 class BBVTVLiveIE(BBVTVBaseIE):
474 _VALID_URL = _create_valid_url(BBVTVBaseIE._HOST, r'[^/?&#]+', 'channel', 'live')
475 _TYPE = 'live'
476 _TESTS = [{
477 'url': 'https://bbv-tv.net/channels/german?channel=srf_zwei',
478 'only_matching': True,
479 }, {
480 'url': 'https://bbv-tv.net/live/srf1',
481 'only_matching': True,
484 @classmethod
485 def suitable(cls, url):
486 return False if BBVTVIE.suitable(url) else super().suitable(url)
489 class 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,
501 class VTXTVBaseIE(ZattooPlatformBaseIE):
502 _NETRC_MACHINE = 'vtxtv'
503 _HOST = 'vtxtv.ch'
504 _API_HOST = f'www.{_HOST}'
507 class 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,
519 class 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,
530 @classmethod
531 def suitable(cls, url):
532 return False if VTXTVIE.suitable(url) else super().suitable(url)
535 class VTXTVRecordingsIE(VTXTVBaseIE):
536 _VALID_URL = _create_valid_url(VTXTVBaseIE._HOST, r'\d+', 'recording')
537 _TYPE = 'record'
538 _TESTS = [{
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',
543 'only_matching': True,
547 class GlattvisionTVBaseIE(ZattooPlatformBaseIE):
548 _NETRC_MACHINE = 'glattvisiontv'
549 _HOST = 'iptv.glattvision.ch'
552 class GlattvisionTVIE(GlattvisionTVBaseIE):
553 _VALID_URL = _create_valid_url(GlattvisionTVBaseIE._HOST, r'\d+', 'program', '(?:program|watch)/[^/]+')
554 _TYPE = 'video'
555 _TESTS = [{
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',
560 'only_matching': True,
564 class 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,
575 @classmethod
576 def suitable(cls, url):
577 return False if GlattvisionTVIE.suitable(url) else super().suitable(url)
580 class 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,
592 class SAKTVBaseIE(ZattooPlatformBaseIE):
593 _NETRC_MACHINE = 'saktv'
594 _HOST = 'saktv.ch'
595 _API_HOST = f'www.{_HOST}'
598 class 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,
610 class SAKTVLiveIE(SAKTVBaseIE):
611 _VALID_URL = _create_valid_url(SAKTVBaseIE._HOST, r'[^/?&#]+', 'channel', 'live')
612 _TYPE = 'live'
613 _TESTS = [{
614 'url': 'https://saktv.ch/channels/german?channel=srf_zwei',
615 'only_matching': True,
616 }, {
617 'url': 'https://saktv.ch/live/srf1',
618 'only_matching': True,
621 @classmethod
622 def suitable(cls, url):
623 return False if SAKTVIE.suitable(url) else super().suitable(url)
626 class 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,
638 class EWETVBaseIE(ZattooPlatformBaseIE):
639 _NETRC_MACHINE = 'ewetv'
640 _HOST = 'tvonline.ewe.de'
643 class 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,
655 class 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,
666 @classmethod
667 def suitable(cls, url):
668 return False if EWETVIE.suitable(url) else super().suitable(url)
671 class EWETVRecordingsIE(EWETVBaseIE):
672 _VALID_URL = _create_valid_url(EWETVBaseIE._HOST, r'\d+', 'recording')
673 _TYPE = 'record'
674 _TESTS = [{
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',
679 'only_matching': True,
683 class QuantumTVBaseIE(ZattooPlatformBaseIE):
684 _NETRC_MACHINE = 'quantumtv'
685 _HOST = 'quantum-tv.com'
686 _API_HOST = f'www.{_HOST}'
689 class QuantumTVIE(QuantumTVBaseIE):
690 _VALID_URL = _create_valid_url(QuantumTVBaseIE._HOST, r'\d+', 'program', '(?:program|watch)/[^/]+')
691 _TYPE = 'video'
692 _TESTS = [{
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,
701 class 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,
712 @classmethod
713 def suitable(cls, url):
714 return False if QuantumTVIE.suitable(url) else super().suitable(url)
717 class 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',
725 'only_matching': True,
729 class OsnatelTVBaseIE(ZattooPlatformBaseIE):
730 _NETRC_MACHINE = 'osnateltv'
731 _HOST = 'tvonline.osnatel.de'
734 class 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,
746 class OsnatelTVLiveIE(OsnatelTVBaseIE):
747 _VALID_URL = _create_valid_url(OsnatelTVBaseIE._HOST, r'[^/?&#]+', 'channel', 'live')
748 _TYPE = 'live'
749 _TESTS = [{
750 'url': 'https://tvonline.osnatel.de/channels/german?channel=srf_zwei',
751 'only_matching': True,
752 }, {
753 'url': 'https://tvonline.osnatel.de/live/srf1',
754 'only_matching': True,
757 @classmethod
758 def suitable(cls, url):
759 return False if OsnatelTVIE.suitable(url) else super().suitable(url)
762 class 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,
774 class EinsUndEinsTVBaseIE(ZattooPlatformBaseIE):
775 _NETRC_MACHINE = '1und1tv'
776 _HOST = '1und1.tv'
777 _API_HOST = f'www.{_HOST}'
780 class EinsUndEinsTVIE(EinsUndEinsTVBaseIE):
781 _VALID_URL = _create_valid_url(EinsUndEinsTVBaseIE._HOST, r'\d+', 'program', '(?:program|watch)/[^/]+')
782 _TYPE = 'video'
783 _TESTS = [{
784 'url': 'https://1und1.tv/program/daserste/210177916',
785 'only_matching': True,
786 }, {
787 'url': 'https://1und1.tv/guide/german?channel=srf1&program=169860555',
788 'only_matching': True,
792 class 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,
803 @classmethod
804 def suitable(cls, url):
805 return False if EinsUndEinsTVIE.suitable(url) else super().suitable(url)
808 class 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,
820 class SaltTVBaseIE(ZattooPlatformBaseIE):
821 _NETRC_MACHINE = 'salttv'
822 _HOST = 'tv.salt.ch'
825 class 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,
837 class SaltTVLiveIE(SaltTVBaseIE):
838 _VALID_URL = _create_valid_url(SaltTVBaseIE._HOST, r'[^/?&#]+', 'channel', 'live')
839 _TYPE = 'live'
840 _TESTS = [{
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,
848 @classmethod
849 def suitable(cls, url):
850 return False if SaltTVIE.suitable(url) else super().suitable(url)
853 class 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',
861 'only_matching': True,