4 from .common
import InfoExtractor
5 from ..compat
import compat_HTTPError
, compat_str
16 class ZattooPlatformBaseIE(InfoExtractor
):
17 _power_guide_hash
= None
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
):
28 data
= self
._download
_json
(
29 '%s/zapi/v2/account/login' % self
._host
_url
(), None, 'Logging in',
30 data
=urlencode_postdata({
35 'Referer': '%s/login' % self
._host
_url
(),
36 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8',
38 except ExtractorError
as e
:
39 if isinstance(e
.cause
, compat_HTTPError
) and e
.cause
.code
== 400:
41 'Unable to login: incorrect username and/or password',
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 '%s/zapi/v3/session/hello' % self
._host
_url
(), None,
54 'Opening session', data
=urlencode_postdata({
55 'uuid': compat_str(uuid4()),
57 'app_version': '1.8.2',
59 'client_app_token': session_token
,
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')
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 '%s/zapi/v2/cached/channels/%s' % (self
._host
_url
(),
75 self
._power
_guide
_hash
),
76 video_id
, 'Downloading channel list',
77 query
={'details': False}
)['channel_groups']
79 for chgrp
in channel_groups
:
80 channel_list
.extend(chgrp
['channels'])
83 chan
['cid'] for chan
in channel_list
84 if chan
.get('cid') and (
85 chan
.get('display_alias') == channel_name
86 or chan
.get('cid') == channel_name
))
88 raise ExtractorError('Could not extract channel id')
90 def _extract_cid_and_video_info(self
, video_id
):
91 data
= self
._download
_json
(
92 '%s/zapi/v2/cached/program/power_details/%s' % (
93 self
._host
_url
(), self
._power
_guide
_hash
),
95 'Downloading video information',
97 'program_ids': video_id
,
101 p
= data
['programs'][0]
106 'title': p
.get('t') or p
['et'],
107 'description': p
.get('d'),
108 'thumbnail': p
.get('i_url'),
109 'creator': p
.get('channel_name'),
110 'episode': p
.get('et'),
111 'episode_number': int_or_none(p
.get('e_no')),
112 'season_number': int_or_none(p
.get('s_no')),
113 'release_year': int_or_none(p
.get('year')),
114 'categories': try_get(p
, lambda x
: x
['c'], list),
115 'tags': try_get(p
, lambda x
: x
['g'], list)
118 return cid
, info_dict
120 def _extract_ondemand_info(self
, ondemand_id
):
122 @returns (ondemand_token, ondemand_type, info_dict)
124 data
= self
._download
_json
(
125 '%s/zapi/vod/movies/%s' % (self
._host
_url
(), ondemand_id
),
126 ondemand_id
, 'Downloading ondemand information')
129 'title': data
.get('title'),
130 'description': data
.get('description'),
131 'duration': int_or_none(data
.get('duration')),
132 'release_year': int_or_none(data
.get('year')),
133 'episode_number': int_or_none(data
.get('episode_number')),
134 'season_number': int_or_none(data
.get('season_number')),
135 'categories': try_get(data
, lambda x
: x
['categories'], list),
137 return data
['terms_catalog'][0]['terms'][0]['token'], data
['type'], info_dict
139 def _extract_formats(self
, cid
, video_id
, record_id
=None, ondemand_id
=None, ondemand_termtoken
=None, ondemand_type
=None, is_live
=False):
141 'https_watch_urls': True,
145 postdata_common
.update({'timeshift': 10800}
)
146 url
= '%s/zapi/watch/live/%s' % (self
._host
_url
(), cid
)
148 url
= '%s/zapi/watch/recording/%s' % (self
._host
_url
(), record_id
)
150 postdata_common
.update({
151 'teasable_id': ondemand_id
,
152 'term_token': ondemand_termtoken
,
153 'teasable_type': ondemand_type
155 url
= '%s/zapi/watch/vod/video' % self
._host
_url
()
157 url
= '%s/zapi/v3/watch/replay/%s/%s' % (self
._host
_url
(), cid
, video_id
)
160 for stream_type
in ('dash', 'hls7'):
161 postdata
= postdata_common
.copy()
162 postdata
['stream_type'] = stream_type
164 data
= self
._download
_json
(
165 url
, video_id
, 'Downloading %s formats' % stream_type
.upper(),
166 data
=urlencode_postdata(postdata
), fatal
=False)
170 watch_urls
= try_get(
171 data
, lambda x
: x
['stream']['watch_urls'], list)
175 for watch
in watch_urls
:
176 if not isinstance(watch
, dict):
178 watch_url
= url_or_none(watch
.get('url'))
181 audio_channel
= watch
.get('audio_channel')
182 preference
= 1 if audio_channel
== 'A' else None
183 format_id
= join_nonempty(stream_type
, watch
.get('maxrate'), audio_channel
)
184 if stream_type
.startswith('dash'):
185 this_formats
, subs
= self
._extract
_mpd
_formats
_and
_subtitles
(
186 watch_url
, video_id
, mpd_id
=format_id
, fatal
=False)
187 self
._merge
_subtitles
(subs
, target
=subtitles
)
188 elif stream_type
.startswith('hls'):
189 this_formats
, subs
= self
._extract
_m
3u8_formats
_and
_subtitles
(
190 watch_url
, video_id
, 'mp4',
191 entry_protocol
='m3u8_native', m3u8_id
=format_id
,
193 self
._merge
_subtitles
(subs
, target
=subtitles
)
194 elif stream_type
== 'hds':
195 this_formats
= self
._extract
_f
4m
_formats
(
196 watch_url
, video_id
, f4m_id
=format_id
, fatal
=False)
197 elif stream_type
== 'smooth_playready':
198 this_formats
= self
._extract
_ism
_formats
(
199 watch_url
, video_id
, ism_id
=format_id
, fatal
=False)
202 for this_format
in this_formats
:
203 this_format
['quality'] = preference
204 formats
.extend(this_formats
)
205 self
._sort
_formats
(formats
)
206 return formats
, subtitles
208 def _extract_video(self
, video_id
, record_id
=None):
209 cid
, info_dict
= self
._extract
_cid
_and
_video
_info
(video_id
)
210 info_dict
['formats'], info_dict
['subtitles'] = self
._extract
_formats
(cid
, video_id
, record_id
=record_id
)
213 def _extract_live(self
, channel_name
):
214 cid
= self
._extract
_cid
(channel_name
, channel_name
)
215 formats
, subtitles
= self
._extract
_formats
(cid
, cid
, is_live
=True)
218 'title': channel_name
,
221 'subtitles': subtitles
224 def _extract_record(self
, record_id
):
225 video_id
= self
._extract
_video
_id
_from
_recording
(record_id
)
226 cid
, info_dict
= self
._extract
_cid
_and
_video
_info
(video_id
)
227 info_dict
['formats'], info_dict
['subtitles'] = self
._extract
_formats
(cid
, video_id
, record_id
=record_id
)
230 def _extract_ondemand(self
, ondemand_id
):
231 ondemand_termtoken
, ondemand_type
, info_dict
= self
._extract
_ondemand
_info
(ondemand_id
)
232 info_dict
['formats'], info_dict
['subtitles'] = self
._extract
_formats
(
233 None, ondemand_id
, ondemand_id
=ondemand_id
,
234 ondemand_termtoken
=ondemand_termtoken
, ondemand_type
=ondemand_type
)
237 def _real_extract(self
, url
):
238 video_id
, record_id
= self
._match
_valid
_url
(url
).groups()
239 return self
._extract
_video
(video_id
, record_id
)
242 def _make_valid_url(host
):
243 return rf
'https?://(?:www\.)?{re.escape(host)}/watch/[^/]+?/(?P<id>[0-9]+)[^/]+(?:/(?P<recid>[0-9]+))?'
246 class ZattooBaseIE(ZattooPlatformBaseIE
):
247 _NETRC_MACHINE
= 'zattoo'
251 def _create_valid_url(match
, qs
, base_re
=None):
252 match_base
= fr
'|{base_re}/(?P<vid1>{match})' if base_re
else '(?P<vid1>)'
253 return rf
'''(?x)https?://(?:www\.)?zattoo\.com/(?:
254 [^?#]+\?(?:[^#]+&)?{qs}=(?P<vid2>{match})
258 def _real_extract(self
, url
):
259 vid1
, vid2
= self
._match
_valid
_url
(url
).group('vid1', 'vid2')
260 return getattr(self
, f
'_extract_{self._TYPE}')(vid1
or vid2
)
263 class ZattooIE(ZattooBaseIE
):
264 _VALID_URL
= ZattooBaseIE
._create
_valid
_url
(r
'\d+', 'program', '(?:program|watch)/[^/]+')
267 'url': 'https://zattoo.com/program/zdf/250170418',
271 'title': 'Markus Lanz',
272 'description': 'md5:e41cb1257de008ca62a73bb876ffa7fc',
273 'thumbnail': 're:http://images.zattic.com/cms/.+/format_480x360.jpg',
275 'release_year': 2022,
276 'episode': 'Folge 1655',
277 'categories': 'count:1',
280 'params': {'skip_download': 'm3u8'}
282 'url': 'https://zattoo.com/program/daserste/210177916',
283 'only_matching': True,
285 'url': 'https://zattoo.com/guide/german?channel=srf1&program=169860555',
286 'only_matching': True,
290 class ZattooLiveIE(ZattooBaseIE
):
291 _VALID_URL
= ZattooBaseIE
._create
_valid
_url
(r
'[^/?&#]+', 'channel', 'live')
294 'url': 'https://zattoo.com/channels/german?channel=srf_zwei',
295 'only_matching': True,
297 'url': 'https://zattoo.com/live/srf1',
298 'only_matching': True,
302 def suitable(cls
, url
):
303 return False if ZattooIE
.suitable(url
) else super().suitable(url
)
306 class ZattooMoviesIE(ZattooBaseIE
):
307 _VALID_URL
= ZattooBaseIE
._create
_valid
_url
(r
'\w+', 'movie_id', 'vod/movies')
310 'url': 'https://zattoo.com/vod/movies/7521',
311 'only_matching': True,
313 'url': 'https://zattoo.com/ondemand?movie_id=7521&term_token=9f00f43183269484edde',
314 'only_matching': True,
318 class ZattooRecordingsIE(ZattooBaseIE
):
319 _VALID_URL
= ZattooBaseIE
._create
_valid
_url
(r
'\d+', 'recording')
322 'url': 'https://zattoo.com/recordings?recording=193615508',
323 'only_matching': True,
325 'url': 'https://zattoo.com/tc/ptc_recordings_all_recordings?recording=193615420',
326 'only_matching': True,
330 class NetPlusIE(ZattooPlatformBaseIE
):
331 _NETRC_MACHINE
= 'netplus'
333 _API_HOST
= 'www.%s' % _HOST
334 _VALID_URL
= _make_valid_url(_HOST
)
337 'url': 'https://www.netplus.tv/watch/abc/123-abc',
338 'only_matching': True,
342 class MNetTVIE(ZattooPlatformBaseIE
):
343 _NETRC_MACHINE
= 'mnettv'
344 _HOST
= 'tvplus.m-net.de'
345 _VALID_URL
= _make_valid_url(_HOST
)
348 'url': 'https://tvplus.m-net.de/watch/abc/123-abc',
349 'only_matching': True,
353 class WalyTVIE(ZattooPlatformBaseIE
):
354 _NETRC_MACHINE
= 'walytv'
355 _HOST
= 'player.waly.tv'
356 _VALID_URL
= _make_valid_url(_HOST
)
359 'url': 'https://player.waly.tv/watch/abc/123-abc',
360 'only_matching': True,
364 class BBVTVIE(ZattooPlatformBaseIE
):
365 _NETRC_MACHINE
= 'bbvtv'
367 _API_HOST
= 'www.%s' % _HOST
368 _VALID_URL
= _make_valid_url(_HOST
)
371 'url': 'https://www.bbv-tv.net/watch/abc/123-abc',
372 'only_matching': True,
376 class VTXTVIE(ZattooPlatformBaseIE
):
377 _NETRC_MACHINE
= 'vtxtv'
379 _API_HOST
= 'www.%s' % _HOST
380 _VALID_URL
= _make_valid_url(_HOST
)
383 'url': 'https://www.vtxtv.ch/watch/abc/123-abc',
384 'only_matching': True,
388 class GlattvisionTVIE(ZattooPlatformBaseIE
):
389 _NETRC_MACHINE
= 'glattvisiontv'
390 _HOST
= 'iptv.glattvision.ch'
391 _VALID_URL
= _make_valid_url(_HOST
)
394 'url': 'https://iptv.glattvision.ch/watch/abc/123-abc',
395 'only_matching': True,
399 class SAKTVIE(ZattooPlatformBaseIE
):
400 _NETRC_MACHINE
= 'saktv'
402 _API_HOST
= 'www.%s' % _HOST
403 _VALID_URL
= _make_valid_url(_HOST
)
406 'url': 'https://www.saktv.ch/watch/abc/123-abc',
407 'only_matching': True,
411 class EWETVIE(ZattooPlatformBaseIE
):
412 _NETRC_MACHINE
= 'ewetv'
413 _HOST
= 'tvonline.ewe.de'
414 _VALID_URL
= _make_valid_url(_HOST
)
417 'url': 'https://tvonline.ewe.de/watch/abc/123-abc',
418 'only_matching': True,
422 class QuantumTVIE(ZattooPlatformBaseIE
):
423 _NETRC_MACHINE
= 'quantumtv'
424 _HOST
= 'quantum-tv.com'
425 _API_HOST
= 'www.%s' % _HOST
426 _VALID_URL
= _make_valid_url(_HOST
)
429 'url': 'https://www.quantum-tv.com/watch/abc/123-abc',
430 'only_matching': True,
434 class OsnatelTVIE(ZattooPlatformBaseIE
):
435 _NETRC_MACHINE
= 'osnateltv'
436 _HOST
= 'tvonline.osnatel.de'
437 _VALID_URL
= _make_valid_url(_HOST
)
440 'url': 'https://tvonline.osnatel.de/watch/abc/123-abc',
441 'only_matching': True,
445 class EinsUndEinsTVIE(ZattooPlatformBaseIE
):
446 _NETRC_MACHINE
= '1und1tv'
448 _API_HOST
= 'www.%s' % _HOST
449 _VALID_URL
= _make_valid_url(_HOST
)
452 'url': 'https://www.1und1.tv/watch/abc/123-abc',
453 'only_matching': True,
457 class SaltTVIE(ZattooPlatformBaseIE
):
458 _NETRC_MACHINE
= 'salttv'
460 _VALID_URL
= _make_valid_url(_HOST
)
463 'url': 'https://tv.salt.ch/watch/abc/123-abc',
464 'only_matching': True,