]> jfr.im git - yt-dlp.git/blame - yt_dlp/extractor/zattoo.py
[extractor] Add `_perform_login` function (#2943)
[yt-dlp.git] / yt_dlp / extractor / zattoo.py
CommitLineData
4a733545
AS
1# coding: utf-8
2from __future__ import unicode_literals
3
4a733545 4import re
67ca1a8e 5from uuid import uuid4
4a733545
AS
6
7from .common import InfoExtractor
67ca1a8e
S
8from ..compat import (
9 compat_HTTPError,
4a733545 10 compat_str,
67ca1a8e
S
11)
12from ..utils import (
4a733545 13 ExtractorError,
67ca1a8e 14 int_or_none,
34921b43 15 join_nonempty,
67ca1a8e 16 try_get,
3052a30d 17 url_or_none,
4a733545
AS
18 urlencode_postdata,
19)
20
21
f6d7f7b4 22class ZattooPlatformBaseIE(InfoExtractor):
4a733545
AS
23 _power_guide_hash = None
24
f6d7f7b4 25 def _host_url(self):
16d896b2 26 return 'https://%s' % (self._API_HOST if hasattr(self, '_API_HOST') else self._HOST)
f6d7f7b4 27
52efa4b3 28 def _real_initialize(self):
29 if not self._power_guide_hash:
30 self.raise_login_required('An account is needed to access this media', method='password')
67ca1a8e 31
52efa4b3 32 def _perform_login(self, username, password):
67ca1a8e
S
33 try:
34 data = self._download_json(
f6d7f7b4 35 '%s/zapi/v2/account/login' % self._host_url(), None, 'Logging in',
67ca1a8e
S
36 data=urlencode_postdata({
37 'login': username,
38 'password': password,
39 'remember': 'true',
40 }), headers={
f6d7f7b4 41 'Referer': '%s/login' % self._host_url(),
67ca1a8e
S
42 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8',
43 })
44 except ExtractorError as e:
45 if isinstance(e.cause, compat_HTTPError) and e.cause.code == 400:
46 raise ExtractorError(
47 'Unable to login: incorrect username and/or password',
48 expected=True)
49 raise
50
51 self._power_guide_hash = data['session']['power_guide_hash']
52
52efa4b3 53 def _initialize_pre_login(self):
67ca1a8e 54 webpage = self._download_webpage(
f6d7f7b4 55 self._host_url(), None, 'Downloading app token')
4a733545 56 app_token = self._html_search_regex(
67ca1a8e
S
57 r'appToken\s*=\s*(["\'])(?P<token>(?:(?!\1).)+?)\1',
58 webpage, 'app token', group='token')
4a733545 59 app_version = self._html_search_regex(
67ca1a8e
S
60 r'<!--\w+-(.+?)-', webpage, 'app version', default='2.8.2')
61
62 # Will setup appropriate cookies
63 self._request_webpage(
f6d7f7b4 64 '%s/zapi/v2/session/hello' % self._host_url(), None,
67ca1a8e
S
65 'Opening session', data=urlencode_postdata({
66 'client_app_token': app_token,
67 'uuid': compat_str(uuid4()),
68 'lang': 'en',
69 'app_version': app_version,
70 'format': 'json',
71 }))
4a733545 72
4a733545
AS
73 def _extract_cid(self, video_id, channel_name):
74 channel_groups = self._download_json(
f6d7f7b4 75 '%s/zapi/v2/cached/channels/%s' % (self._host_url(),
4a733545 76 self._power_guide_hash),
67ca1a8e 77 video_id, 'Downloading channel list',
4a733545
AS
78 query={'details': False})['channel_groups']
79 channel_list = []
80 for chgrp in channel_groups:
81 channel_list.extend(chgrp['channels'])
82 try:
83 return next(
84 chan['cid'] for chan in channel_list
67ca1a8e 85 if chan.get('cid') and (
3089bc74
S
86 chan.get('display_alias') == channel_name
87 or chan.get('cid') == channel_name))
4a733545
AS
88 except StopIteration:
89 raise ExtractorError('Could not extract channel id')
90
91 def _extract_cid_and_video_info(self, video_id):
92 data = self._download_json(
21160a17 93 '%s/zapi/v2/cached/program/power_details/%s' % (
f6d7f7b4 94 self._host_url(), self._power_guide_hash),
4a733545
AS
95 video_id,
96 'Downloading video information',
97 query={
21160a17
AS
98 'program_ids': video_id,
99 'complete': True,
4a733545
AS
100 })
101
21160a17 102 p = data['programs'][0]
67ca1a8e
S
103 cid = p['cid']
104
4a733545
AS
105 info_dict = {
106 'id': video_id,
21160a17
AS
107 'title': p.get('t') or p['et'],
108 'description': p.get('d'),
109 'thumbnail': p.get('i_url'),
67ca1a8e 110 'creator': p.get('channel_name'),
21160a17
AS
111 'episode': p.get('et'),
112 'episode_number': int_or_none(p.get('e_no')),
113 'season_number': int_or_none(p.get('s_no')),
67ca1a8e 114 'release_year': int_or_none(p.get('year')),
21160a17
AS
115 'categories': try_get(p, lambda x: x['c'], list),
116 'tags': try_get(p, lambda x: x['g'], list)
4a733545 117 }
67ca1a8e 118
4a733545
AS
119 return cid, info_dict
120
121 def _extract_formats(self, cid, video_id, record_id=None, is_live=False):
67ca1a8e 122 postdata_common = {
4a733545
AS
123 'https_watch_urls': True,
124 }
4a733545
AS
125
126 if is_live:
67ca1a8e 127 postdata_common.update({'timeshift': 10800})
f6d7f7b4 128 url = '%s/zapi/watch/live/%s' % (self._host_url(), cid)
67ca1a8e 129 elif record_id:
f6d7f7b4 130 url = '%s/zapi/watch/recording/%s' % (self._host_url(), record_id)
67ca1a8e 131 else:
f6d7f7b4 132 url = '%s/zapi/watch/recall/%s/%s' % (self._host_url(), cid, video_id)
4a733545
AS
133
134 formats = []
67ca1a8e
S
135 for stream_type in ('dash', 'hls', 'hls5', 'hds'):
136 postdata = postdata_common.copy()
137 postdata['stream_type'] = stream_type
138
139 data = self._download_json(
140 url, video_id, 'Downloading %s formats' % stream_type.upper(),
141 data=urlencode_postdata(postdata), fatal=False)
142 if not data:
143 continue
144
145 watch_urls = try_get(
146 data, lambda x: x['stream']['watch_urls'], list)
147 if not watch_urls:
148 continue
149
150 for watch in watch_urls:
151 if not isinstance(watch, dict):
152 continue
3052a30d
S
153 watch_url = url_or_none(watch.get('url'))
154 if not watch_url:
67ca1a8e 155 continue
67ca1a8e 156 audio_channel = watch.get('audio_channel')
67ca1a8e 157 preference = 1 if audio_channel == 'A' else None
34921b43 158 format_id = join_nonempty(stream_type, watch.get('maxrate'), audio_channel)
67ca1a8e
S
159 if stream_type in ('dash', 'dash_widevine', 'dash_playready'):
160 this_formats = self._extract_mpd_formats(
161 watch_url, video_id, mpd_id=format_id, fatal=False)
162 elif stream_type in ('hls', 'hls5', 'hls5_fairplay'):
163 this_formats = self._extract_m3u8_formats(
164 watch_url, video_id, 'mp4',
165 entry_protocol='m3u8_native', m3u8_id=format_id,
166 fatal=False)
167 elif stream_type == 'hds':
168 this_formats = self._extract_f4m_formats(
169 watch_url, video_id, f4m_id=format_id, fatal=False)
170 elif stream_type == 'smooth_playready':
171 this_formats = self._extract_ism_formats(
172 watch_url, video_id, ism_id=format_id, fatal=False)
173 else:
174 assert False
175 for this_format in this_formats:
f983b875 176 this_format['quality'] = preference
67ca1a8e 177 formats.extend(this_formats)
4a733545
AS
178 self._sort_formats(formats)
179 return formats
180
4a733545
AS
181 def _extract_video(self, channel_name, video_id, record_id=None, is_live=False):
182 if is_live:
183 cid = self._extract_cid(video_id, channel_name)
184 info_dict = {
185 'id': channel_name,
39ca3b5c 186 'title': channel_name,
4a733545
AS
187 'is_live': True,
188 }
189 else:
190 cid, info_dict = self._extract_cid_and_video_info(video_id)
191 formats = self._extract_formats(
192 cid, video_id, record_id=record_id, is_live=is_live)
193 info_dict['formats'] = formats
194 return info_dict
195
196
f6d7f7b4 197class QuicklineBaseIE(ZattooPlatformBaseIE):
4a733545 198 _NETRC_MACHINE = 'quickline'
f6d7f7b4 199 _HOST = 'mobiltv.quickline.com'
4a733545
AS
200
201
202class QuicklineIE(QuicklineBaseIE):
f6d7f7b4 203 _VALID_URL = r'https?://(?:www\.)?%s/watch/(?P<channel>[^/]+)/(?P<id>[0-9]+)' % re.escape(QuicklineBaseIE._HOST)
4a733545 204
67ca1a8e
S
205 _TEST = {
206 'url': 'https://mobiltv.quickline.com/watch/prosieben/130671867-maze-runner-die-auserwaehlten-in-der-brandwueste',
207 'only_matching': True,
208 }
209
4a733545 210 def _real_extract(self, url):
5ad28e7f 211 channel_name, video_id = self._match_valid_url(url).groups()
4a733545
AS
212 return self._extract_video(channel_name, video_id)
213
214
215class QuicklineLiveIE(QuicklineBaseIE):
f6d7f7b4 216 _VALID_URL = r'https?://(?:www\.)?%s/watch/(?P<id>[^/]+)' % re.escape(QuicklineBaseIE._HOST)
67ca1a8e
S
217
218 _TEST = {
219 'url': 'https://mobiltv.quickline.com/watch/srf1',
220 'only_matching': True,
221 }
222
223 @classmethod
224 def suitable(cls, url):
225 return False if QuicklineIE.suitable(url) else super(QuicklineLiveIE, cls).suitable(url)
4a733545
AS
226
227 def _real_extract(self, url):
228 channel_name = video_id = self._match_id(url)
229 return self._extract_video(channel_name, video_id, is_live=True)
230
231
f6d7f7b4
S
232class ZattooBaseIE(ZattooPlatformBaseIE):
233 _NETRC_MACHINE = 'zattoo'
234 _HOST = 'zattoo.com'
235
236
237def _make_valid_url(tmpl, host):
238 return tmpl % re.escape(host)
239
240
4a733545 241class ZattooIE(ZattooBaseIE):
f6d7f7b4
S
242 _VALID_URL_TEMPLATE = r'https?://(?:www\.)?%s/watch/(?P<channel>[^/]+?)/(?P<id>[0-9]+)[^/]+(?:/(?P<recid>[0-9]+))?'
243 _VALID_URL = _make_valid_url(_VALID_URL_TEMPLATE, ZattooBaseIE._HOST)
4a733545
AS
244
245 # Since regular videos are only available for 7 days and recorded videos
246 # are only available for a specific user, we cannot have detailed tests.
247 _TESTS = [{
248 'url': 'https://zattoo.com/watch/prosieben/130671867-maze-runner-die-auserwaehlten-in-der-brandwueste',
249 'only_matching': True,
250 }, {
251 'url': 'https://zattoo.com/watch/srf_zwei/132905652-eishockey-spengler-cup/102791477/1512211800000/1514433500000/92000',
252 'only_matching': True,
253 }]
254
255 def _real_extract(self, url):
5ad28e7f 256 channel_name, video_id, record_id = self._match_valid_url(url).groups()
4a733545
AS
257 return self._extract_video(channel_name, video_id, record_id)
258
259
260class ZattooLiveIE(ZattooBaseIE):
67ca1a8e 261 _VALID_URL = r'https?://(?:www\.)?zattoo\.com/watch/(?P<id>[^/]+)'
4a733545
AS
262
263 _TEST = {
264 'url': 'https://zattoo.com/watch/srf1',
265 'only_matching': True,
266 }
267
67ca1a8e
S
268 @classmethod
269 def suitable(cls, url):
270 return False if ZattooIE.suitable(url) else super(ZattooLiveIE, cls).suitable(url)
271
4a733545
AS
272 def _real_extract(self, url):
273 channel_name = video_id = self._match_id(url)
274 return self._extract_video(channel_name, video_id, is_live=True)
f6d7f7b4
S
275
276
277class NetPlusIE(ZattooIE):
278 _NETRC_MACHINE = 'netplus'
279 _HOST = 'netplus.tv'
16d896b2 280 _API_HOST = 'www.%s' % _HOST
f6d7f7b4
S
281 _VALID_URL = _make_valid_url(ZattooIE._VALID_URL_TEMPLATE, _HOST)
282
283 _TESTS = [{
284 'url': 'https://www.netplus.tv/watch/abc/123-abc',
285 'only_matching': True,
286 }]
287
288
289class MNetTVIE(ZattooIE):
290 _NETRC_MACHINE = 'mnettv'
291 _HOST = 'tvplus.m-net.de'
292 _VALID_URL = _make_valid_url(ZattooIE._VALID_URL_TEMPLATE, _HOST)
293
294 _TESTS = [{
16d896b2 295 'url': 'https://tvplus.m-net.de/watch/abc/123-abc',
f6d7f7b4
S
296 'only_matching': True,
297 }]
298
299
300class WalyTVIE(ZattooIE):
301 _NETRC_MACHINE = 'walytv'
302 _HOST = 'player.waly.tv'
303 _VALID_URL = _make_valid_url(ZattooIE._VALID_URL_TEMPLATE, _HOST)
304
305 _TESTS = [{
16d896b2 306 'url': 'https://player.waly.tv/watch/abc/123-abc',
f6d7f7b4
S
307 'only_matching': True,
308 }]
309
310
311class BBVTVIE(ZattooIE):
312 _NETRC_MACHINE = 'bbvtv'
313 _HOST = 'bbv-tv.net'
16d896b2 314 _API_HOST = 'www.%s' % _HOST
f6d7f7b4
S
315 _VALID_URL = _make_valid_url(ZattooIE._VALID_URL_TEMPLATE, _HOST)
316
317 _TESTS = [{
318 'url': 'https://www.bbv-tv.net/watch/abc/123-abc',
319 'only_matching': True,
320 }]
321
322
323class VTXTVIE(ZattooIE):
324 _NETRC_MACHINE = 'vtxtv'
325 _HOST = 'vtxtv.ch'
16d896b2 326 _API_HOST = 'www.%s' % _HOST
f6d7f7b4
S
327 _VALID_URL = _make_valid_url(ZattooIE._VALID_URL_TEMPLATE, _HOST)
328
329 _TESTS = [{
330 'url': 'https://www.vtxtv.ch/watch/abc/123-abc',
331 'only_matching': True,
332 }]
333
334
335class MyVisionTVIE(ZattooIE):
336 _NETRC_MACHINE = 'myvisiontv'
337 _HOST = 'myvisiontv.ch'
16d896b2 338 _API_HOST = 'www.%s' % _HOST
f6d7f7b4
S
339 _VALID_URL = _make_valid_url(ZattooIE._VALID_URL_TEMPLATE, _HOST)
340
341 _TESTS = [{
342 'url': 'https://www.myvisiontv.ch/watch/abc/123-abc',
343 'only_matching': True,
344 }]
345
346
347class GlattvisionTVIE(ZattooIE):
348 _NETRC_MACHINE = 'glattvisiontv'
349 _HOST = 'iptv.glattvision.ch'
350 _VALID_URL = _make_valid_url(ZattooIE._VALID_URL_TEMPLATE, _HOST)
351
352 _TESTS = [{
16d896b2 353 'url': 'https://iptv.glattvision.ch/watch/abc/123-abc',
f6d7f7b4
S
354 'only_matching': True,
355 }]
356
357
358class SAKTVIE(ZattooIE):
359 _NETRC_MACHINE = 'saktv'
360 _HOST = 'saktv.ch'
16d896b2 361 _API_HOST = 'www.%s' % _HOST
f6d7f7b4
S
362 _VALID_URL = _make_valid_url(ZattooIE._VALID_URL_TEMPLATE, _HOST)
363
364 _TESTS = [{
365 'url': 'https://www.saktv.ch/watch/abc/123-abc',
366 'only_matching': True,
367 }]
368
369
370class EWETVIE(ZattooIE):
371 _NETRC_MACHINE = 'ewetv'
372 _HOST = 'tvonline.ewe.de'
373 _VALID_URL = _make_valid_url(ZattooIE._VALID_URL_TEMPLATE, _HOST)
374
375 _TESTS = [{
16d896b2 376 'url': 'https://tvonline.ewe.de/watch/abc/123-abc',
f6d7f7b4
S
377 'only_matching': True,
378 }]
379
380
381class QuantumTVIE(ZattooIE):
382 _NETRC_MACHINE = 'quantumtv'
383 _HOST = 'quantum-tv.com'
16d896b2 384 _API_HOST = 'www.%s' % _HOST
f6d7f7b4
S
385 _VALID_URL = _make_valid_url(ZattooIE._VALID_URL_TEMPLATE, _HOST)
386
387 _TESTS = [{
388 'url': 'https://www.quantum-tv.com/watch/abc/123-abc',
389 'only_matching': True,
390 }]
391
392
393class OsnatelTVIE(ZattooIE):
394 _NETRC_MACHINE = 'osnateltv'
2004e221 395 _HOST = 'tvonline.osnatel.de'
f6d7f7b4
S
396 _VALID_URL = _make_valid_url(ZattooIE._VALID_URL_TEMPLATE, _HOST)
397
398 _TESTS = [{
16d896b2 399 'url': 'https://tvonline.osnatel.de/watch/abc/123-abc',
f6d7f7b4
S
400 'only_matching': True,
401 }]
402
403
404class EinsUndEinsTVIE(ZattooIE):
405 _NETRC_MACHINE = '1und1tv'
406 _HOST = '1und1.tv'
16d896b2 407 _API_HOST = 'www.%s' % _HOST
f6d7f7b4
S
408 _VALID_URL = _make_valid_url(ZattooIE._VALID_URL_TEMPLATE, _HOST)
409
410 _TESTS = [{
411 'url': 'https://www.1und1.tv/watch/abc/123-abc',
412 'only_matching': True,
413 }]
a81daba2
AS
414
415
416class SaltTVIE(ZattooIE):
417 _NETRC_MACHINE = 'salttv'
418 _HOST = 'tv.salt.ch'
419 _VALID_URL = _make_valid_url(ZattooIE._VALID_URL_TEMPLATE, _HOST)
420
421 _TESTS = [{
422 'url': 'https://tv.salt.ch/watch/abc/123-abc',
423 'only_matching': True,
424 }]