tvnow.py 23 KB

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