]> jfr.im git - yt-dlp.git/blame - yt_dlp/extractor/tvnow.py
[ie/box] Fix formats extraction (#8649)
[yt-dlp.git] / yt_dlp / extractor / tvnow.py
CommitLineData
23b6e230
RA
1import re
2
3from .common import InfoExtractor
4from ..compat import compat_str
5from ..utils import (
6 ExtractorError,
0d0c9e82 7 get_element_by_id,
574e9db2 8 int_or_none,
23b6e230
RA
9 parse_iso8601,
10 parse_duration,
de0359c0 11 str_or_none,
0d0c9e82 12 try_get,
23b6e230 13 update_url_query,
de0359c0 14 urljoin,
23b6e230
RA
15)
16
17
18class TVNowBaseIE(InfoExtractor):
19 _VIDEO_FIELDS = (
20 'id', 'title', 'free', 'geoblocked', 'articleLong', 'articleShort',
574e9db2 21 'broadcastStartDate', 'isDrm', 'duration', 'season', 'episode',
8ba84e46
RA
22 'manifest.dashclear', 'manifest.hlsclear', 'manifest.smoothclear',
23 'format.title', 'format.defaultImage169Format', 'format.defaultImage169Logo')
23b6e230
RA
24
25 def _call_api(self, path, video_id, query):
26 return self._download_json(
de0359c0 27 'https://api.tvnow.de/v3/' + path, video_id, query=query)
23b6e230
RA
28
29 def _extract_video(self, info, display_id):
30 video_id = compat_str(info['id'])
31 title = info['title']
32
8ba84e46
RA
33 paths = []
34 for manifest_url in (info.get('manifest') or {}).values():
35 if not manifest_url:
36 continue
37 manifest_url = update_url_query(manifest_url, {'filter': ''})
38 path = self._search_regex(r'https?://[^/]+/(.+?)\.ism/', manifest_url, 'path')
39 if path in paths:
40 continue
41 paths.append(path)
42
43 def url_repl(proto, suffix):
44 return re.sub(
45 r'(?:hls|dash|hss)([.-])', proto + r'\1', re.sub(
46 r'\.ism/(?:[^.]*\.(?:m3u8|mpd)|[Mm]anifest)',
47 '.ism/' + suffix, manifest_url))
48
e75220b1
S
49 def make_urls(proto, suffix):
50 urls = [url_repl(proto, suffix)]
51 hd_url = urls[0].replace('/manifest/', '/ngvod/')
52 if hd_url != urls[0]:
53 urls.append(hd_url)
54 return urls
55
56 for man_url in make_urls('dash', '.mpd'):
57 formats = self._extract_mpd_formats(
58 man_url, video_id, mpd_id='dash', fatal=False)
59 for man_url in make_urls('hss', 'Manifest'):
60 formats.extend(self._extract_ism_formats(
61 man_url, video_id, ism_id='mss', fatal=False))
62 for man_url in make_urls('hls', '.m3u8'):
63 formats.extend(self._extract_m3u8_formats(
64 man_url, video_id, 'mp4', 'm3u8_native', m3u8_id='hls',
65 fatal=False))
8ba84e46
RA
66 if formats:
67 break
68 else:
a06916d9 69 if not self.get_param('allow_unplayable_formats') and info.get('isDrm'):
23b6e230
RA
70 raise ExtractorError(
71 'Video %s is DRM protected' % video_id, expected=True)
72 if info.get('geoblocked'):
8ba84e46 73 raise self.raise_geo_restricted()
23b6e230
RA
74 if not info.get('free', True):
75 raise ExtractorError(
76 'Video %s is not available for free' % video_id, expected=True)
23b6e230
RA
77
78 description = info.get('articleLong') or info.get('articleShort')
79 timestamp = parse_iso8601(info.get('broadcastStartDate'), ' ')
80 duration = parse_duration(info.get('duration'))
81
82 f = info.get('format', {})
ea6679fb
S
83
84 thumbnails = [{
85 'url': 'https://aistvnow-a.akamaihd.net/tvnow/movie/%s' % video_id,
86 }]
87 thumbnail = f.get('defaultImage169Format') or f.get('defaultImage169Logo')
88 if thumbnail:
89 thumbnails.append({
90 'url': thumbnail,
91 })
23b6e230
RA
92
93 return {
94 'id': video_id,
95 'display_id': display_id,
96 'title': title,
97 'description': description,
ea6679fb 98 'thumbnails': thumbnails,
23b6e230
RA
99 'timestamp': timestamp,
100 'duration': duration,
574e9db2
S
101 'series': f.get('title'),
102 'season_number': int_or_none(info.get('season')),
103 'episode_number': int_or_none(info.get('episode')),
104 'episode': title,
23b6e230
RA
105 'formats': formats,
106 }
107
108
109class TVNowIE(TVNowBaseIE):
ea6679fb
S
110 _VALID_URL = r'''(?x)
111 https?://
8ba84e46 112 (?:www\.)?tvnow\.(?:de|at|ch)/(?P<station>[^/]+)/
ea6679fb
S
113 (?P<show_id>[^/]+)/
114 (?!(?:list|jahr)(?:/|$))(?P<id>[^/?\#&]+)
115 '''
23b6e230 116
de0359c0
S
117 @classmethod
118 def suitable(cls, url):
119 return (False if TVNowNewIE.suitable(url) or TVNowSeasonIE.suitable(url) or TVNowAnnualIE.suitable(url) or TVNowShowIE.suitable(url)
120 else super(TVNowIE, cls).suitable(url))
121
23b6e230 122 _TESTS = [{
574e9db2 123 'url': 'https://www.tvnow.de/rtl2/grip-das-motormagazin/der-neue-porsche-911-gt-3/player',
23b6e230 124 'info_dict': {
574e9db2
S
125 'id': '331082',
126 'display_id': 'grip-das-motormagazin/der-neue-porsche-911-gt-3',
23b6e230 127 'ext': 'mp4',
574e9db2
S
128 'title': 'Der neue Porsche 911 GT 3',
129 'description': 'md5:6143220c661f9b0aae73b245e5d898bb',
574e9db2
S
130 'timestamp': 1495994400,
131 'upload_date': '20170528',
132 'duration': 5283,
133 'series': 'GRIP - Das Motormagazin',
134 'season_number': 14,
135 'episode_number': 405,
136 'episode': 'Der neue Porsche 911 GT 3',
23b6e230
RA
137 },
138 }, {
139 # rtl2
140 'url': 'https://www.tvnow.de/rtl2/armes-deutschland/episode-0008/player',
ea6679fb 141 'only_matching': True,
23b6e230
RA
142 }, {
143 # rtlnitro
144 'url': 'https://www.tvnow.de/nitro/alarm-fuer-cobra-11-die-autobahnpolizei/auf-eigene-faust-pilot/player',
ea6679fb 145 'only_matching': True,
23b6e230
RA
146 }, {
147 # superrtl
148 'url': 'https://www.tvnow.de/superrtl/die-lustigsten-schlamassel-der-welt/u-a-ketchup-effekt/player',
ea6679fb 149 'only_matching': True,
23b6e230
RA
150 }, {
151 # ntv
152 'url': 'https://www.tvnow.de/ntv/startup-news/goetter-in-weiss/player',
ea6679fb 153 'only_matching': True,
23b6e230
RA
154 }, {
155 # vox
156 'url': 'https://www.tvnow.de/vox/auto-mobil/neues-vom-automobilmarkt-2017-11-19-17-00-00/player',
ea6679fb 157 'only_matching': True,
23b6e230
RA
158 }, {
159 # rtlplus
160 'url': 'https://www.tvnow.de/rtlplus/op-ruft-dr-bruckner/die-vernaehte-frau/player',
ea6679fb
S
161 'only_matching': True,
162 }, {
163 'url': 'https://www.tvnow.de/rtl2/grip-das-motormagazin/der-neue-porsche-911-gt-3',
164 'only_matching': True,
23b6e230
RA
165 }]
166
167 def _real_extract(self, url):
5ad28e7f 168 mobj = self._match_valid_url(url)
8ba84e46 169 display_id = '%s/%s' % mobj.group(2, 3)
23b6e230
RA
170
171 info = self._call_api(
172 'movies/' + display_id, display_id, query={
173 'fields': ','.join(self._VIDEO_FIELDS),
174 })
175
176 return self._extract_video(info, display_id)
177
178
de0359c0
S
179class TVNowNewIE(InfoExtractor):
180 _VALID_URL = r'''(?x)
181 (?P<base_url>https?://
182 (?:www\.)?tvnow\.(?:de|at|ch)/
183 (?:shows|serien))/
184 (?P<show>[^/]+)-\d+/
185 [^/]+/
186 episode-\d+-(?P<episode>[^/?$&]+)-(?P<id>\d+)
ea6679fb
S
187 '''
188
de0359c0
S
189 _TESTS = [{
190 'url': 'https://www.tvnow.de/shows/grip-das-motormagazin-1669/2017-05/episode-405-der-neue-porsche-911-gt-3-331082',
191 'only_matching': True,
192 }]
23b6e230 193
de0359c0 194 def _real_extract(self, url):
5ad28e7f 195 mobj = self._match_valid_url(url)
de0359c0
S
196 base_url = re.sub(r'(?:shows|serien)', '_', mobj.group('base_url'))
197 show, episode = mobj.group('show', 'episode')
198 return self.url_result(
199 # Rewrite new URLs to the old format and use extraction via old API
200 # at api.tvnow.de as a loophole for bypassing premium content checks
201 '%s/%s/%s' % (base_url, show, episode),
202 ie=TVNowIE.ie_key(), video_id=mobj.group('id'))
203
204
0d0c9e82
T
205class TVNowFilmIE(TVNowBaseIE):
206 _VALID_URL = r'''(?x)
207 (?P<base_url>https?://
208 (?:www\.)?tvnow\.(?:de|at|ch)/
209 (?:filme))/
210 (?P<title>[^/?$&]+)-(?P<id>\d+)
211 '''
212 _TESTS = [{
213 'url': 'https://www.tvnow.de/filme/lord-of-war-haendler-des-todes-7959',
214 'info_dict': {
215 'id': '1426690',
216 'display_id': 'lord-of-war-haendler-des-todes',
217 'ext': 'mp4',
218 'title': 'Lord of War',
219 'description': 'md5:5eda15c0d5b8cb70dac724c8a0ff89a9',
220 'timestamp': 1550010000,
221 'upload_date': '20190212',
222 'duration': 7016,
223 },
224 }, {
225 'url': 'https://www.tvnow.de/filme/the-machinist-12157',
226 'info_dict': {
227 'id': '328160',
228 'display_id': 'the-machinist',
229 'ext': 'mp4',
230 'title': 'The Machinist',
231 'description': 'md5:9a0e363fdd74b3a9e1cdd9e21d0ecc28',
232 'timestamp': 1496469720,
233 'upload_date': '20170603',
234 'duration': 5836,
235 },
236 }, {
237 'url': 'https://www.tvnow.de/filme/horst-schlaemmer-isch-kandidiere-17777',
238 'only_matching': True, # DRM protected
239 }]
240
241 def _real_extract(self, url):
5ad28e7f 242 mobj = self._match_valid_url(url)
0d0c9e82
T
243 display_id = mobj.group('title')
244
245 webpage = self._download_webpage(url, display_id, fatal=False)
246 if not webpage:
247 raise ExtractorError('Cannot download "%s"' % url, expected=True)
248
249 json_text = get_element_by_id('now-web-state', webpage)
250 if not json_text:
251 raise ExtractorError('Cannot read video data', expected=True)
252
253 json_data = self._parse_json(
254 json_text,
255 display_id,
256 transform_source=lambda x: x.replace('&q;', '"'),
257 fatal=False)
258 if not json_data:
259 raise ExtractorError('Cannot read video data', expected=True)
260
261 player_key = next(
262 (key for key in json_data.keys() if 'module/player' in key),
263 None)
264 page_key = next(
265 (key for key in json_data.keys() if 'page/filme' in key),
266 None)
267 movie_id = try_get(
268 json_data,
269 [
270 lambda x: x[player_key]['body']['id'],
271 lambda x: x[page_key]['body']['modules'][0]['id'],
272 lambda x: x[page_key]['body']['modules'][1]['id']],
273 int)
274 if not movie_id:
275 raise ExtractorError('Cannot extract movie ID', expected=True)
276
277 info = self._call_api(
278 'movies/%d' % movie_id,
279 display_id,
280 query={'fields': ','.join(self._VIDEO_FIELDS)})
281
282 return self._extract_video(info, display_id)
283
284
de0359c0
S
285class TVNowNewBaseIE(InfoExtractor):
286 def _call_api(self, path, video_id, query={}):
287 result = self._download_json(
288 'https://apigw.tvnow.de/module/' + path, video_id, query=query)
289 error = result.get('error')
290 if error:
291 raise ExtractorError(
292 '%s said: %s' % (self.IE_NAME, error), expected=True)
293 return result
294
295
d23e8551 296r"""
de0359c0
S
297TODO: new apigw.tvnow.de based version of TVNowIE. Replace old TVNowIE with it
298when api.tvnow.de is shut down. This version can't bypass premium checks though.
299class TVNowIE(TVNowNewBaseIE):
300 _VALID_URL = r'''(?x)
301 https?://
302 (?:www\.)?tvnow\.(?:de|at|ch)/
303 (?:shows|serien)/[^/]+/
304 (?:[^/]+/)+
305 (?P<display_id>[^/?$&]+)-(?P<id>\d+)
306 '''
23b6e230
RA
307
308 _TESTS = [{
de0359c0
S
309 # episode with annual navigation
310 'url': 'https://www.tvnow.de/shows/grip-das-motormagazin-1669/2017-05/episode-405-der-neue-porsche-911-gt-3-331082',
23b6e230 311 'info_dict': {
de0359c0
S
312 'id': '331082',
313 'display_id': 'grip-das-motormagazin/der-neue-porsche-911-gt-3',
314 'ext': 'mp4',
315 'title': 'Der neue Porsche 911 GT 3',
316 'description': 'md5:6143220c661f9b0aae73b245e5d898bb',
317 'thumbnail': r're:^https?://.*\.jpg$',
318 'timestamp': 1495994400,
319 'upload_date': '20170528',
320 'duration': 5283,
321 'series': 'GRIP - Das Motormagazin',
322 'season_number': 14,
323 'episode_number': 405,
324 'episode': 'Der neue Porsche 911 GT 3',
23b6e230 325 },
ea6679fb 326 }, {
de0359c0
S
327 # rtl2, episode with season navigation
328 'url': 'https://www.tvnow.de/shows/armes-deutschland-11471/staffel-3/episode-14-bernd-steht-seit-der-trennung-von-seiner-frau-allein-da-526124',
ea6679fb
S
329 'only_matching': True,
330 }, {
de0359c0
S
331 # rtlnitro
332 'url': 'https://www.tvnow.de/serien/alarm-fuer-cobra-11-die-autobahnpolizei-1815/staffel-13/episode-5-auf-eigene-faust-pilot-366822',
333 'only_matching': True,
334 }, {
335 # superrtl
336 'url': 'https://www.tvnow.de/shows/die-lustigsten-schlamassel-der-welt-1221/staffel-2/episode-14-u-a-ketchup-effekt-364120',
337 'only_matching': True,
338 }, {
339 # ntv
340 'url': 'https://www.tvnow.de/shows/startup-news-10674/staffel-2/episode-39-goetter-in-weiss-387630',
341 'only_matching': True,
342 }, {
343 # vox
344 'url': 'https://www.tvnow.de/shows/auto-mobil-174/2017-11/episode-46-neues-vom-automobilmarkt-2017-11-19-17-00-00-380072',
345 'only_matching': True,
346 }, {
347 'url': 'https://www.tvnow.de/shows/grip-das-motormagazin-1669/2017-05/episode-405-der-neue-porsche-911-gt-3-331082',
ea6679fb 348 'only_matching': True,
23b6e230
RA
349 }]
350
de0359c0
S
351 def _extract_video(self, info, url, display_id):
352 config = info['config']
353 source = config['source']
ea6679fb 354
de0359c0
S
355 video_id = compat_str(info.get('id') or source['videoId'])
356 title = source['title'].strip()
23b6e230 357
de0359c0
S
358 paths = []
359 for manifest_url in (info.get('manifest') or {}).values():
360 if not manifest_url:
361 continue
362 manifest_url = update_url_query(manifest_url, {'filter': ''})
363 path = self._search_regex(r'https?://[^/]+/(.+?)\.ism/', manifest_url, 'path')
364 if path in paths:
365 continue
366 paths.append(path)
23b6e230 367
de0359c0
S
368 def url_repl(proto, suffix):
369 return re.sub(
370 r'(?:hls|dash|hss)([.-])', proto + r'\1', re.sub(
371 r'\.ism/(?:[^.]*\.(?:m3u8|mpd)|[Mm]anifest)',
372 '.ism/' + suffix, manifest_url))
23b6e230 373
de0359c0
S
374 formats = self._extract_mpd_formats(
375 url_repl('dash', '.mpd'), video_id,
376 mpd_id='dash', fatal=False)
377 formats.extend(self._extract_ism_formats(
378 url_repl('hss', 'Manifest'),
379 video_id, ism_id='mss', fatal=False))
380 formats.extend(self._extract_m3u8_formats(
381 url_repl('hls', '.m3u8'), video_id, 'mp4',
382 'm3u8_native', m3u8_id='hls', fatal=False))
383 if formats:
384 break
ea6679fb 385 else:
de0359c0
S
386 if try_get(info, lambda x: x['rights']['isDrm']):
387 raise ExtractorError(
388 'Video %s is DRM protected' % video_id, expected=True)
389 if try_get(config, lambda x: x['boards']['geoBlocking']['block']):
390 raise self.raise_geo_restricted()
391 if not info.get('free', True):
392 raise ExtractorError(
393 'Video %s is not available for free' % video_id, expected=True)
de0359c0
S
394
395 description = source.get('description')
396 thumbnail = url_or_none(source.get('poster'))
397 timestamp = unified_timestamp(source.get('previewStart'))
398 duration = parse_duration(source.get('length'))
399
400 series = source.get('format')
401 season_number = int_or_none(self._search_regex(
402 r'staffel-(\d+)', url, 'season number', default=None))
403 episode_number = int_or_none(self._search_regex(
404 r'episode-(\d+)', url, 'episode number', default=None))
405
406 return {
407 'id': video_id,
408 'display_id': display_id,
409 'title': title,
410 'description': description,
411 'thumbnail': thumbnail,
412 'timestamp': timestamp,
413 'duration': duration,
414 'series': series,
415 'season_number': season_number,
416 'episode_number': episode_number,
417 'episode': title,
418 'formats': formats,
419 }
420
421 def _real_extract(self, url):
5ad28e7f 422 display_id, video_id = self._match_valid_url(url).groups()
de0359c0
S
423 info = self._call_api('player/' + video_id, video_id)
424 return self._extract_video(info, video_id, display_id)
0d0c9e82
T
425
426
6368e2e6 427class TVNowFilmIE(TVNowIE): # XXX: Do not subclass from concrete IE
0d0c9e82
T
428 _VALID_URL = r'''(?x)
429 (?P<base_url>https?://
430 (?:www\.)?tvnow\.(?:de|at|ch)/
431 (?:filme))/
432 (?P<title>[^/?$&]+)-(?P<id>\d+)
433 '''
434 _TESTS = [{
435 'url': 'https://www.tvnow.de/filme/lord-of-war-haendler-des-todes-7959',
436 'info_dict': {
437 'id': '1426690',
438 'display_id': 'lord-of-war-haendler-des-todes',
439 'ext': 'mp4',
440 'title': 'Lord of War',
441 'description': 'md5:5eda15c0d5b8cb70dac724c8a0ff89a9',
442 'timestamp': 1550010000,
443 'upload_date': '20190212',
444 'duration': 7016,
445 },
446 }, {
447 'url': 'https://www.tvnow.de/filme/the-machinist-12157',
448 'info_dict': {
449 'id': '328160',
450 'display_id': 'the-machinist',
451 'ext': 'mp4',
452 'title': 'The Machinist',
453 'description': 'md5:9a0e363fdd74b3a9e1cdd9e21d0ecc28',
454 'timestamp': 1496469720,
455 'upload_date': '20170603',
456 'duration': 5836,
457 },
458 }, {
459 'url': 'https://www.tvnow.de/filme/horst-schlaemmer-isch-kandidiere-17777',
460 'only_matching': True, # DRM protected
461 }]
462
463 def _real_extract(self, url):
5ad28e7f 464 mobj = self._match_valid_url(url)
0d0c9e82
T
465 display_id = mobj.group('title')
466
467 webpage = self._download_webpage(url, display_id, fatal=False)
468 if not webpage:
469 raise ExtractorError('Cannot download "%s"' % url, expected=True)
470
471 json_text = get_element_by_id('now-web-state', webpage)
472 if not json_text:
473 raise ExtractorError('Cannot read video data', expected=True)
474
475 json_data = self._parse_json(
476 json_text,
477 display_id,
478 transform_source=lambda x: x.replace('&q;', '"'),
479 fatal=False)
480 if not json_data:
481 raise ExtractorError('Cannot read video data', expected=True)
482
483 player_key = next(
484 (key for key in json_data.keys() if 'module/player' in key),
485 None)
486 page_key = next(
487 (key for key in json_data.keys() if 'page/filme' in key),
488 None)
489 movie_id = try_get(
490 json_data,
491 [
492 lambda x: x[player_key]['body']['id'],
493 lambda x: x[page_key]['body']['modules'][0]['id'],
494 lambda x: x[page_key]['body']['modules'][1]['id']],
495 int)
496 if not movie_id:
497 raise ExtractorError('Cannot extract movie ID', expected=True)
498
499 info = self._call_api('player/%d' % movie_id, display_id)
500 return self._extract_video(info, url, display_id)
de0359c0
S
501"""
502
503
504class TVNowListBaseIE(TVNowNewBaseIE):
505 _SHOW_VALID_URL = r'''(?x)
506 (?P<base_url>
507 https?://
508 (?:www\.)?tvnow\.(?:de|at|ch)/(?:shows|serien)/
509 [^/?#&]+-(?P<show_id>\d+)
510 )
511 '''
512
513 @classmethod
514 def suitable(cls, url):
515 return (False if TVNowNewIE.suitable(url)
516 else super(TVNowListBaseIE, cls).suitable(url))
517
518 def _extract_items(self, url, show_id, list_id, query):
519 items = self._call_api(
520 'teaserrow/format/episode/' + show_id, list_id,
521 query=query)['items']
23b6e230
RA
522
523 entries = []
de0359c0
S
524 for item in items:
525 if not isinstance(item, dict):
526 continue
527 item_url = urljoin(url, item.get('url'))
528 if not item_url:
529 continue
530 video_id = str_or_none(item.get('id') or item.get('videoId'))
531 item_title = item.get('subheadline') or item.get('text')
532 entries.append(self.url_result(
533 item_url, ie=TVNowNewIE.ie_key(), video_id=video_id,
534 video_title=item_title))
23b6e230 535
de0359c0 536 return self.playlist_result(entries, '%s/%s' % (show_id, list_id))
3acae1e0
A
537
538
de0359c0
S
539class TVNowSeasonIE(TVNowListBaseIE):
540 _VALID_URL = r'%s/staffel-(?P<id>\d+)' % TVNowListBaseIE._SHOW_VALID_URL
541 _TESTS = [{
542 'url': 'https://www.tvnow.de/serien/alarm-fuer-cobra-11-die-autobahnpolizei-1815/staffel-13',
543 'info_dict': {
544 'id': '1815/13',
545 },
546 'playlist_mincount': 22,
547 }]
548
549 def _real_extract(self, url):
5ad28e7f 550 _, show_id, season_id = self._match_valid_url(url).groups()
de0359c0
S
551 return self._extract_items(
552 url, show_id, season_id, {'season': season_id})
3acae1e0 553
3acae1e0 554
de0359c0
S
555class TVNowAnnualIE(TVNowListBaseIE):
556 _VALID_URL = r'%s/(?P<year>\d{4})-(?P<month>\d{2})' % TVNowListBaseIE._SHOW_VALID_URL
3acae1e0 557 _TESTS = [{
de0359c0 558 'url': 'https://www.tvnow.de/shows/grip-das-motormagazin-1669/2017-05',
ea6679fb 559 'info_dict': {
de0359c0 560 'id': '1669/2017-05',
ea6679fb 561 },
de0359c0
S
562 'playlist_mincount': 2,
563 }]
564
565 def _real_extract(self, url):
5ad28e7f 566 _, show_id, year, month = self._match_valid_url(url).groups()
de0359c0
S
567 return self._extract_items(
568 url, show_id, '%s-%s' % (year, month), {
569 'year': int(year),
570 'month': int(month),
571 })
572
573
574class TVNowShowIE(TVNowListBaseIE):
575 _VALID_URL = TVNowListBaseIE._SHOW_VALID_URL
576 _TESTS = [{
577 # annual navigationType
578 'url': 'https://www.tvnow.de/shows/grip-das-motormagazin-1669',
579 'info_dict': {
580 'id': '1669',
581 },
582 'playlist_mincount': 73,
ea6679fb 583 }, {
de0359c0
S
584 # season navigationType
585 'url': 'https://www.tvnow.de/shows/armes-deutschland-11471',
586 'info_dict': {
587 'id': '11471',
588 },
589 'playlist_mincount': 3,
3acae1e0
A
590 }]
591
592 @classmethod
593 def suitable(cls, url):
de0359c0 594 return (False if TVNowNewIE.suitable(url) or TVNowSeasonIE.suitable(url) or TVNowAnnualIE.suitable(url)
ea6679fb 595 else super(TVNowShowIE, cls).suitable(url))
3acae1e0
A
596
597 def _real_extract(self, url):
5ad28e7f 598 base_url, show_id = self._match_valid_url(url).groups()
3acae1e0 599
de0359c0
S
600 result = self._call_api(
601 'teaserrow/format/navigation/' + show_id, show_id)
602
603 items = result['items']
3acae1e0
A
604
605 entries = []
de0359c0
S
606 navigation = result.get('navigationType')
607 if navigation == 'annual':
608 for item in items:
609 if not isinstance(item, dict):
610 continue
611 year = int_or_none(item.get('year'))
612 if year is None:
613 continue
614 months = item.get('months')
615 if not isinstance(months, list):
616 continue
617 for month_dict in months:
618 if not isinstance(month_dict, dict) or not month_dict:
619 continue
620 month_number = int_or_none(list(month_dict.keys())[0])
621 if month_number is None:
622 continue
623 entries.append(self.url_result(
624 '%s/%04d-%02d' % (base_url, year, month_number),
625 ie=TVNowAnnualIE.ie_key()))
626 elif navigation == 'season':
627 for item in items:
628 if not isinstance(item, dict):
629 continue
630 season_number = int_or_none(item.get('season'))
631 if season_number is None:
632 continue
633 entries.append(self.url_result(
634 '%s/staffel-%d' % (base_url, season_number),
635 ie=TVNowSeasonIE.ie_key()))
636 else:
637 raise ExtractorError('Unknown navigationType')
3acae1e0 638
de0359c0 639 return self.playlist_result(entries, show_id)