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