tvnow.py 9.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279
  1. # coding: utf-8
  2. from __future__ import unicode_literals
  3. import re
  4. from .common import InfoExtractor
  5. from ..compat import compat_str
  6. from ..utils import (
  7. ExtractorError,
  8. int_or_none,
  9. parse_iso8601,
  10. parse_duration,
  11. try_get,
  12. update_url_query,
  13. )
  14. class TVNowBaseIE(InfoExtractor):
  15. _VIDEO_FIELDS = (
  16. 'id', 'title', 'free', 'geoblocked', 'articleLong', 'articleShort',
  17. 'broadcastStartDate', 'isDrm', 'duration', 'season', 'episode',
  18. 'manifest.dashclear', 'format.title', 'format.defaultImage169Format',
  19. 'format.defaultImage169Logo')
  20. def _call_api(self, path, video_id, query):
  21. return self._download_json(
  22. 'https://api.tvnow.de/v3/' + path,
  23. video_id, query=query)
  24. def _extract_video(self, info, display_id):
  25. video_id = compat_str(info['id'])
  26. title = info['title']
  27. mpd_url = info['manifest']['dashclear']
  28. if not mpd_url:
  29. if info.get('isDrm'):
  30. raise ExtractorError(
  31. 'Video %s is DRM protected' % video_id, expected=True)
  32. if info.get('geoblocked'):
  33. raise ExtractorError(
  34. 'Video %s is not available from your location due to geo restriction' % video_id,
  35. expected=True)
  36. if not info.get('free', True):
  37. raise ExtractorError(
  38. 'Video %s is not available for free' % video_id, expected=True)
  39. mpd_url = update_url_query(mpd_url, {'filter': ''})
  40. formats = self._extract_mpd_formats(mpd_url, video_id, mpd_id='dash', fatal=False)
  41. formats.extend(self._extract_ism_formats(
  42. mpd_url.replace('dash.', 'hss.').replace('/.mpd', '/Manifest'),
  43. video_id, ism_id='mss', fatal=False))
  44. formats.extend(self._extract_m3u8_formats(
  45. mpd_url.replace('dash.', 'hls.').replace('/.mpd', '/.m3u8'),
  46. video_id, 'mp4', 'm3u8_native', m3u8_id='hls', fatal=False))
  47. self._sort_formats(formats)
  48. description = info.get('articleLong') or info.get('articleShort')
  49. timestamp = parse_iso8601(info.get('broadcastStartDate'), ' ')
  50. duration = parse_duration(info.get('duration'))
  51. f = info.get('format', {})
  52. thumbnails = [{
  53. 'url': 'https://aistvnow-a.akamaihd.net/tvnow/movie/%s' % video_id,
  54. }]
  55. thumbnail = f.get('defaultImage169Format') or f.get('defaultImage169Logo')
  56. if thumbnail:
  57. thumbnails.append({
  58. 'url': thumbnail,
  59. })
  60. return {
  61. 'id': video_id,
  62. 'display_id': display_id,
  63. 'title': title,
  64. 'description': description,
  65. 'thumbnails': thumbnails,
  66. 'timestamp': timestamp,
  67. 'duration': duration,
  68. 'series': f.get('title'),
  69. 'season_number': int_or_none(info.get('season')),
  70. 'episode_number': int_or_none(info.get('episode')),
  71. 'episode': title,
  72. 'formats': formats,
  73. }
  74. class TVNowIE(TVNowBaseIE):
  75. _VALID_URL = r'''(?x)
  76. https?://
  77. (?:www\.)?tvnow\.(?:de|at|ch)/[^/]+/
  78. (?P<show_id>[^/]+)/
  79. (?!(?:list|jahr)(?:/|$))(?P<id>[^/?\#&]+)
  80. '''
  81. _TESTS = [{
  82. 'url': 'https://www.tvnow.de/rtl2/grip-das-motormagazin/der-neue-porsche-911-gt-3/player',
  83. 'info_dict': {
  84. 'id': '331082',
  85. 'display_id': 'grip-das-motormagazin/der-neue-porsche-911-gt-3',
  86. 'ext': 'mp4',
  87. 'title': 'Der neue Porsche 911 GT 3',
  88. 'description': 'md5:6143220c661f9b0aae73b245e5d898bb',
  89. 'thumbnail': r're:^https?://.*\.jpg$',
  90. 'timestamp': 1495994400,
  91. 'upload_date': '20170528',
  92. 'duration': 5283,
  93. 'series': 'GRIP - Das Motormagazin',
  94. 'season_number': 14,
  95. 'episode_number': 405,
  96. 'episode': 'Der neue Porsche 911 GT 3',
  97. },
  98. }, {
  99. # rtl2
  100. 'url': 'https://www.tvnow.de/rtl2/armes-deutschland/episode-0008/player',
  101. 'only_matching': True,
  102. }, {
  103. # rtlnitro
  104. 'url': 'https://www.tvnow.de/nitro/alarm-fuer-cobra-11-die-autobahnpolizei/auf-eigene-faust-pilot/player',
  105. 'only_matching': True,
  106. }, {
  107. # superrtl
  108. 'url': 'https://www.tvnow.de/superrtl/die-lustigsten-schlamassel-der-welt/u-a-ketchup-effekt/player',
  109. 'only_matching': True,
  110. }, {
  111. # ntv
  112. 'url': 'https://www.tvnow.de/ntv/startup-news/goetter-in-weiss/player',
  113. 'only_matching': True,
  114. }, {
  115. # vox
  116. 'url': 'https://www.tvnow.de/vox/auto-mobil/neues-vom-automobilmarkt-2017-11-19-17-00-00/player',
  117. 'only_matching': True,
  118. }, {
  119. # rtlplus
  120. 'url': 'https://www.tvnow.de/rtlplus/op-ruft-dr-bruckner/die-vernaehte-frau/player',
  121. 'only_matching': True,
  122. }, {
  123. 'url': 'https://www.tvnow.de/rtl2/grip-das-motormagazin/der-neue-porsche-911-gt-3',
  124. 'only_matching': True,
  125. }]
  126. def _real_extract(self, url):
  127. display_id = '%s/%s' % re.match(self._VALID_URL, url).groups()
  128. info = self._call_api(
  129. 'movies/' + display_id, display_id, query={
  130. 'fields': ','.join(self._VIDEO_FIELDS),
  131. })
  132. return self._extract_video(info, display_id)
  133. class TVNowListBaseIE(TVNowBaseIE):
  134. _SHOW_VALID_URL = r'''(?x)
  135. (?P<base_url>
  136. https?://
  137. (?:www\.)?tvnow\.(?:de|at|ch)/[^/]+/
  138. (?P<show_id>[^/]+)
  139. )
  140. '''
  141. def _extract_list_info(self, display_id, show_id):
  142. fields = list(self._SHOW_FIELDS)
  143. fields.extend('formatTabs.%s' % field for field in self._SEASON_FIELDS)
  144. fields.extend(
  145. 'formatTabs.formatTabPages.container.movies.%s' % field
  146. for field in self._VIDEO_FIELDS)
  147. return self._call_api(
  148. 'formats/seo', display_id, query={
  149. 'fields': ','.join(fields),
  150. 'name': show_id + '.php'
  151. })
  152. class TVNowListIE(TVNowListBaseIE):
  153. _VALID_URL = r'%s/(?:list|jahr)/(?P<id>[^?\#&]+)' % TVNowListBaseIE._SHOW_VALID_URL
  154. _SHOW_FIELDS = ('title', )
  155. _SEASON_FIELDS = ('id', 'headline', 'seoheadline', )
  156. _VIDEO_FIELDS = ('id', 'headline', 'seoUrl', )
  157. _TESTS = [{
  158. 'url': 'https://www.tvnow.de/rtl/30-minuten-deutschland/list/aktuell',
  159. 'info_dict': {
  160. 'id': '28296',
  161. 'title': '30 Minuten Deutschland - Aktuell',
  162. },
  163. 'playlist_mincount': 1,
  164. }, {
  165. 'url': 'https://www.tvnow.de/vox/ab-ins-beet/list/staffel-14',
  166. 'only_matching': True,
  167. }, {
  168. 'url': 'https://www.tvnow.de/rtl2/grip-das-motormagazin/jahr/2018/3',
  169. 'only_matching': True,
  170. }]
  171. @classmethod
  172. def suitable(cls, url):
  173. return (False if TVNowIE.suitable(url)
  174. else super(TVNowListIE, cls).suitable(url))
  175. def _real_extract(self, url):
  176. base_url, show_id, season_id = re.match(self._VALID_URL, url).groups()
  177. list_info = self._extract_list_info(season_id, show_id)
  178. season = next(
  179. season for season in list_info['formatTabs']['items']
  180. if season.get('seoheadline') == season_id)
  181. title = list_info.get('title')
  182. headline = season.get('headline')
  183. if title and headline:
  184. title = '%s - %s' % (title, headline)
  185. else:
  186. title = headline or title
  187. entries = []
  188. for container in season['formatTabPages']['items']:
  189. items = try_get(
  190. container, lambda x: x['container']['movies']['items'],
  191. list) or []
  192. for info in items:
  193. seo_url = info.get('seoUrl')
  194. if not seo_url:
  195. continue
  196. video_id = info.get('id')
  197. entries.append(self.url_result(
  198. '%s/%s/player' % (base_url, seo_url), TVNowIE.ie_key(),
  199. compat_str(video_id) if video_id else None))
  200. return self.playlist_result(
  201. entries, compat_str(season.get('id') or season_id), title)
  202. class TVNowShowIE(TVNowListBaseIE):
  203. _VALID_URL = TVNowListBaseIE._SHOW_VALID_URL
  204. _SHOW_FIELDS = ('id', 'title', )
  205. _SEASON_FIELDS = ('id', 'headline', 'seoheadline', )
  206. _VIDEO_FIELDS = ()
  207. _TESTS = [{
  208. 'url': 'https://www.tvnow.at/vox/ab-ins-beet',
  209. 'info_dict': {
  210. 'id': 'ab-ins-beet',
  211. 'title': 'Ab ins Beet!',
  212. },
  213. 'playlist_mincount': 7,
  214. }, {
  215. 'url': 'https://www.tvnow.at/vox/ab-ins-beet/list',
  216. 'only_matching': True,
  217. }, {
  218. 'url': 'https://www.tvnow.de/rtl2/grip-das-motormagazin/jahr/',
  219. 'only_matching': True,
  220. }]
  221. @classmethod
  222. def suitable(cls, url):
  223. return (False if TVNowIE.suitable(url) or TVNowListIE.suitable(url)
  224. else super(TVNowShowIE, cls).suitable(url))
  225. def _real_extract(self, url):
  226. base_url, show_id = re.match(self._VALID_URL, url).groups()
  227. list_info = self._extract_list_info(show_id, show_id)
  228. entries = []
  229. for season_info in list_info['formatTabs']['items']:
  230. season_url = season_info.get('seoheadline')
  231. if not season_url:
  232. continue
  233. season_id = season_info.get('id')
  234. entries.append(self.url_result(
  235. '%s/list/%s' % (base_url, season_url), TVNowListIE.ie_key(),
  236. compat_str(season_id) if season_id else None,
  237. season_info.get('headline')))
  238. return self.playlist_result(entries, show_id, list_info.get('title'))