4 from .common
import InfoExtractor
19 class ZattooPlatformBaseIE(InfoExtractor
):
20 _power_guide_hash
= None
23 return 'https://%s' % (self
._API
_HOST
if hasattr(self
, '_API_HOST') else self
._HOST
)
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')
29 def _perform_login(self
, username
, password
):
31 data
= self
._download
_json
(
32 '%s/zapi/v2/account/login' % self
._host
_url
(), None, 'Logging in',
33 data
=urlencode_postdata({
38 'Referer': '%s/login' % self
._host
_url
(),
39 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8',
41 except ExtractorError
as e
:
42 if isinstance(e
.cause
, compat_HTTPError
) and e
.cause
.code
== 400:
44 'Unable to login: incorrect username and/or password',
48 self
._power
_guide
_hash
= data
['session']['power_guide_hash']
50 def _initialize_pre_login(self
):
51 session_token
= self
._download
_json
(
52 f
'{self._host_url()}/token.json', None, 'Downloading session token')['session_token']
54 # Will setup appropriate cookies
55 self
._request
_webpage
(
56 '%s/zapi/v3/session/hello' % self
._host
_url
(), None,
57 'Opening session', data
=urlencode_postdata({
58 'uuid': compat_str(uuid4()),
60 'app_version': '1.8.2',
62 'client_app_token': session_token
,
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')
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')
75 def _extract_cid(self
, video_id
, channel_name
):
76 channel_groups
= self
._download
_json
(
77 '%s/zapi/v2/cached/channels/%s' % (self
._host
_url
(),
78 self
._power
_guide
_hash
),
79 video_id
, 'Downloading channel list',
80 query
={'details': False}
)['channel_groups']
82 for chgrp
in channel_groups
:
83 channel_list
.extend(chgrp
['channels'])
86 chan
['cid'] for chan
in channel_list
87 if chan
.get('cid') and (
88 chan
.get('display_alias') == channel_name
89 or chan
.get('cid') == channel_name
))
91 raise ExtractorError('Could not extract channel id')
93 def _extract_cid_and_video_info(self
, video_id
):
94 data
= self
._download
_json
(
95 '%s/zapi/v2/cached/program/power_details/%s' % (
96 self
._host
_url
(), self
._power
_guide
_hash
),
98 'Downloading video information',
100 'program_ids': video_id
,
104 p
= data
['programs'][0]
109 'title': p
.get('t') or p
['et'],
110 'description': p
.get('d'),
111 'thumbnail': p
.get('i_url'),
112 'creator': p
.get('channel_name'),
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')),
116 'release_year': int_or_none(p
.get('year')),
117 'categories': try_get(p
, lambda x
: x
['c'], list),
118 'tags': try_get(p
, lambda x
: x
['g'], list)
121 return cid
, info_dict
123 def _extract_ondemand_info(self
, ondemand_id
):
125 @returns (ondemand_token, ondemand_type, info_dict)
127 data
= self
._download
_json
(
128 '%s/zapi/vod/movies/%s' % (self
._host
_url
(), ondemand_id
),
129 ondemand_id
, 'Downloading ondemand information')
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),
140 return data
['terms_catalog'][0]['terms'][0]['token'], data
['type'], info_dict
142 def _extract_formats(self
, cid
, video_id
, record_id
=None, ondemand_id
=None, ondemand_termtoken
=None, ondemand_type
=None, is_live
=False):
144 'https_watch_urls': True,
148 postdata_common
.update({'timeshift': 10800}
)
149 url
= '%s/zapi/watch/live/%s' % (self
._host
_url
(), cid
)
151 url
= '%s/zapi/watch/recording/%s' % (self
._host
_url
(), record_id
)
153 postdata_common
.update({
154 'teasable_id': ondemand_id
,
155 'term_token': ondemand_termtoken
,
156 'teasable_type': ondemand_type
158 url
= '%s/zapi/watch/vod/video' % self
._host
_url
()
160 url
= '%s/zapi/v3/watch/replay/%s/%s' % (self
._host
_url
(), cid
, video_id
)
163 for stream_type
in ('dash', 'hls7'):
164 postdata
= postdata_common
.copy()
165 postdata
['stream_type'] = stream_type
167 data
= self
._download
_json
(
168 url
, video_id
, 'Downloading %s formats' % stream_type
.upper(),
169 data
=urlencode_postdata(postdata
), fatal
=False)
173 watch_urls
= try_get(
174 data
, lambda x
: x
['stream']['watch_urls'], list)
178 for watch
in watch_urls
:
179 if not isinstance(watch
, dict):
181 watch_url
= url_or_none(watch
.get('url'))
184 audio_channel
= watch
.get('audio_channel')
185 preference
= 1 if audio_channel
== 'A' else None
186 format_id
= join_nonempty(stream_type
, watch
.get('maxrate'), audio_channel
)
187 if stream_type
.startswith('dash'):
188 this_formats
, subs
= self
._extract
_mpd
_formats
_and
_subtitles
(
189 watch_url
, video_id
, mpd_id
=format_id
, fatal
=False)
190 self
._merge
_subtitles
(subs
, target
=subtitles
)
191 elif stream_type
.startswith('hls'):
192 this_formats
, subs
= self
._extract
_m
3u8_formats
_and
_subtitles
(
193 watch_url
, video_id
, 'mp4',
194 entry_protocol
='m3u8_native', m3u8_id
=format_id
,
196 self
._merge
_subtitles
(subs
, target
=subtitles
)
197 elif stream_type
== 'hds':
198 this_formats
= self
._extract
_f
4m
_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)
205 for this_format
in this_formats
:
206 this_format
['quality'] = preference
207 formats
.extend(this_formats
)
208 self
._sort
_formats
(formats
)
209 return formats
, subtitles
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
)
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)
221 'title': channel_name
,
224 'subtitles': subtitles
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
)
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
)
241 def _make_valid_url(host
):
242 return rf
'https?://(?:www\.)?{re.escape(host)}/watch/[^/]+?/(?P<id>[0-9]+)[^/]+(?:/(?P<recid>[0-9]+))?'
245 class ZattooBaseIE(ZattooPlatformBaseIE
):
246 _NETRC_MACHINE
= 'zattoo'
250 def _create_valid_url(match
, qs
, base_re
=None):
251 match_base
= fr
'|{base_re}/(?P<vid1>{match})' if base_re
else '(?P<vid1>)'
252 return rf
'''(?x)https?://(?:www\.)?zattoo\.com/(?:
253 [^?#]+\?(?:[^#]+&)?{qs}=(?P<vid2>{match})
257 def _real_extract(self
, url
):
258 vid1
, vid2
= self
._match
_valid
_url
(url
).group('vid1', 'vid2')
259 return getattr(self
, f
'_extract_{self._TYPE}')(vid1
or vid2
)
262 class ZattooIE(ZattooBaseIE
):
263 _VALID_URL
= ZattooBaseIE
._create
_valid
_url
(r
'\d+', 'program', '(?:program|watch)/[^/]+')
266 'url': 'https://zattoo.com/program/zdf/250170418',
270 'title': 'Markus Lanz',
271 'description': 'md5:e41cb1257de008ca62a73bb876ffa7fc',
272 'thumbnail': 're:http://images.zattic.com/cms/.+/format_480x360.jpg',
274 'release_year': 2022,
275 'episode': 'Folge 1655',
276 'categories': 'count:1',
279 'params': {'skip_download': 'm3u8'}
281 'url': 'https://zattoo.com/program/daserste/210177916',
282 'only_matching': True,
284 'url': 'https://zattoo.com/guide/german?channel=srf1&program=169860555',
285 'only_matching': True,
289 class ZattooLiveIE(ZattooBaseIE
):
290 _VALID_URL
= ZattooBaseIE
._create
_valid
_url
(r
'[^/?&#]+', 'channel', 'live')
293 'url': 'https://zattoo.com/channels/german?channel=srf_zwei',
294 'only_matching': True,
296 'url': 'https://zattoo.com/live/srf1',
297 'only_matching': True,
301 def suitable(cls
, url
):
302 return False if ZattooIE
.suitable(url
) else super().suitable(url
)
305 class ZattooMoviesIE(ZattooBaseIE
):
306 _VALID_URL
= ZattooBaseIE
._create
_valid
_url
(r
'\w+', 'movie_id', 'vod/movies')
309 'url': 'https://zattoo.com/vod/movies/7521',
310 'only_matching': True,
312 'url': 'https://zattoo.com/ondemand?movie_id=7521&term_token=9f00f43183269484edde',
313 'only_matching': True,
317 class ZattooRecordingsIE(ZattooBaseIE
):
318 _VALID_URL
= ZattooBaseIE
._create
_valid
_url
(r
'\d+', 'recording')
321 'url': 'https://zattoo.com/recordings?recording=193615508',
322 'only_matching': True,
324 'url': 'https://zattoo.com/tc/ptc_recordings_all_recordings?recording=193615420',
325 'only_matching': True,
329 class NetPlusIE(ZattooPlatformBaseIE
):
330 _NETRC_MACHINE
= 'netplus'
332 _API_HOST
= 'www.%s' % _HOST
333 _VALID_URL
= _make_valid_url(_HOST
)
336 'url': 'https://www.netplus.tv/watch/abc/123-abc',
337 'only_matching': True,
341 class MNetTVIE(ZattooPlatformBaseIE
):
342 _NETRC_MACHINE
= 'mnettv'
343 _HOST
= 'tvplus.m-net.de'
344 _VALID_URL
= _make_valid_url(_HOST
)
347 'url': 'https://tvplus.m-net.de/watch/abc/123-abc',
348 'only_matching': True,
352 class WalyTVIE(ZattooPlatformBaseIE
):
353 _NETRC_MACHINE
= 'walytv'
354 _HOST
= 'player.waly.tv'
355 _VALID_URL
= _make_valid_url(_HOST
)
358 'url': 'https://player.waly.tv/watch/abc/123-abc',
359 'only_matching': True,
363 class BBVTVIE(ZattooPlatformBaseIE
):
364 _NETRC_MACHINE
= 'bbvtv'
366 _API_HOST
= 'www.%s' % _HOST
367 _VALID_URL
= _make_valid_url(_HOST
)
370 'url': 'https://www.bbv-tv.net/watch/abc/123-abc',
371 'only_matching': True,
375 class VTXTVIE(ZattooPlatformBaseIE
):
376 _NETRC_MACHINE
= 'vtxtv'
378 _API_HOST
= 'www.%s' % _HOST
379 _VALID_URL
= _make_valid_url(_HOST
)
382 'url': 'https://www.vtxtv.ch/watch/abc/123-abc',
383 'only_matching': True,
387 class GlattvisionTVIE(ZattooPlatformBaseIE
):
388 _NETRC_MACHINE
= 'glattvisiontv'
389 _HOST
= 'iptv.glattvision.ch'
390 _VALID_URL
= _make_valid_url(_HOST
)
393 'url': 'https://iptv.glattvision.ch/watch/abc/123-abc',
394 'only_matching': True,
398 class SAKTVIE(ZattooPlatformBaseIE
):
399 _NETRC_MACHINE
= 'saktv'
401 _API_HOST
= 'www.%s' % _HOST
402 _VALID_URL
= _make_valid_url(_HOST
)
405 'url': 'https://www.saktv.ch/watch/abc/123-abc',
406 'only_matching': True,
410 class EWETVIE(ZattooPlatformBaseIE
):
411 _NETRC_MACHINE
= 'ewetv'
412 _HOST
= 'tvonline.ewe.de'
413 _VALID_URL
= _make_valid_url(_HOST
)
416 'url': 'https://tvonline.ewe.de/watch/abc/123-abc',
417 'only_matching': True,
421 class QuantumTVIE(ZattooPlatformBaseIE
):
422 _NETRC_MACHINE
= 'quantumtv'
423 _HOST
= 'quantum-tv.com'
424 _API_HOST
= 'www.%s' % _HOST
425 _VALID_URL
= _make_valid_url(_HOST
)
428 'url': 'https://www.quantum-tv.com/watch/abc/123-abc',
429 'only_matching': True,
433 class OsnatelTVIE(ZattooPlatformBaseIE
):
434 _NETRC_MACHINE
= 'osnateltv'
435 _HOST
= 'tvonline.osnatel.de'
436 _VALID_URL
= _make_valid_url(_HOST
)
439 'url': 'https://tvonline.osnatel.de/watch/abc/123-abc',
440 'only_matching': True,
444 class EinsUndEinsTVIE(ZattooPlatformBaseIE
):
445 _NETRC_MACHINE
= '1und1tv'
447 _API_HOST
= 'www.%s' % _HOST
448 _VALID_URL
= _make_valid_url(_HOST
)
451 'url': 'https://www.1und1.tv/watch/abc/123-abc',
452 'only_matching': True,
456 class SaltTVIE(ZattooPlatformBaseIE
):
457 _NETRC_MACHINE
= 'salttv'
459 _VALID_URL
= _make_valid_url(_HOST
)
462 'url': 'https://tv.salt.ch/watch/abc/123-abc',
463 'only_matching': True,