[nytimes] Add support for podcasts
[youtube-dl] / youtube_dl / extractor / nytimes.py
1 # coding: utf-8
2 from __future__ import unicode_literals
3
4 import hmac
5 import hashlib
6 import base64
7
8 from .common import InfoExtractor
9 from ..utils import (
10     float_or_none,
11     int_or_none,
12     js_to_json,
13     parse_iso8601,
14     mimetype2ext,
15     determine_ext,
16 )
17
18
19 class NYTimesBaseIE(InfoExtractor):
20     _SECRET = b'pX(2MbU2);4N{7J8)>YwKRJ+/pQ3JkiU2Q^V>mFYv6g6gYvt6v'
21
22     def _extract_video_from_id(self, video_id):
23         # Authorization generation algorithm is reverse engineered from `signer` in
24         # http://graphics8.nytimes.com/video/vhs/vhs-2.x.min.js
25         path = '/svc/video/api/v3/video/' + video_id
26         hm = hmac.new(self._SECRET, (path + ':vhs').encode(), hashlib.sha512).hexdigest()
27         video_data = self._download_json('http://www.nytimes.com' + path, video_id, 'Downloading video JSON', headers={
28             'Authorization': 'NYTV ' + base64.b64encode(hm.encode()).decode(),
29             'X-NYTV': 'vhs',
30         }, fatal=False)
31         if not video_data:
32             video_data = self._download_json(
33                 'http://www.nytimes.com/svc/video/api/v2/video/' + video_id,
34                 video_id, 'Downloading video JSON')
35
36         title = video_data['headline']
37
38         def get_file_size(file_size):
39             if isinstance(file_size, int):
40                 return file_size
41             elif isinstance(file_size, dict):
42                 return int(file_size.get('value', 0))
43             else:
44                 return None
45
46         urls = []
47         formats = []
48         for video in video_data.get('renditions', []):
49             video_url = video.get('url')
50             format_id = video.get('type')
51             if not video_url or format_id == 'thumbs' or video_url in urls:
52                 continue
53             urls.append(video_url)
54             ext = mimetype2ext(video.get('mimetype')) or determine_ext(video_url)
55             if ext == 'm3u8':
56                 formats.extend(self._extract_m3u8_formats(
57                     video_url, video_id, 'mp4', 'm3u8_native',
58                     m3u8_id=format_id or 'hls', fatal=False))
59             elif ext == 'mpd':
60                 continue
61             #     formats.extend(self._extract_mpd_formats(
62             #         video_url, video_id, format_id or 'dash', fatal=False))
63             else:
64                 formats.append({
65                     'url': video_url,
66                     'format_id': format_id,
67                     'vcodec': video.get('videoencoding') or video.get('video_codec'),
68                     'width': int_or_none(video.get('width')),
69                     'height': int_or_none(video.get('height')),
70                     'filesize': get_file_size(video.get('file_size') or video.get('fileSize')),
71                     'tbr': int_or_none(video.get('bitrate'), 1000),
72                     'ext': ext,
73                 })
74         self._sort_formats(formats)
75
76         thumbnails = []
77         for image in video_data.get('images', []):
78             image_url = image.get('url')
79             if not image_url:
80                 continue
81             thumbnails.append({
82                 'url': 'http://www.nytimes.com/' + image_url,
83                 'width': int_or_none(image.get('width')),
84                 'height': int_or_none(image.get('height')),
85             })
86
87         publication_date = video_data.get('publication_date')
88         timestamp = parse_iso8601(publication_date[:-8]) if publication_date else None
89
90         return {
91             'id': video_id,
92             'title': title,
93             'description': video_data.get('summary'),
94             'timestamp': timestamp,
95             'uploader': video_data.get('byline'),
96             'duration': float_or_none(video_data.get('duration'), 1000),
97             'formats': formats,
98             'thumbnails': thumbnails,
99         }
100
101     def _extract_podcast_from_json(self, json, page_id, webpage):
102         audio_data = self._parse_json(json, page_id, transform_source=js_to_json)['data']
103         
104         description = audio_data['track'].get('description')
105         if not description:
106             description = self._html_search_meta(['og:description', 'twitter:description'], webpage)
107
108         episode_title = audio_data['track']['title']
109         episode_number = None
110         episode = audio_data['podcast']['episode'].split()
111         if episode:
112             episode_number = int_or_none(episode[-1])
113             video_id = episode[-1]
114         else:
115             video_id = page_id
116
117         podcast_title = audio_data['podcast']['title']
118         title = None
119         if podcast_title:
120             title = "%s: %s" % (podcast_title, episode_title)
121         else:
122             title = episode_title
123         
124         info_dict = {
125             'id': video_id,
126             'title': title,
127             'creator': audio_data['track'].get('credit'),
128             'series': podcast_title,
129             'episode': episode_title,
130             'episode_number': episode_number,
131             'url': audio_data['track']['source'],
132             'duration': audio_data['track'].get('duration'),
133             'description': description,
134         }
135         
136         return info_dict
137
138
139 class NYTimesIE(NYTimesBaseIE):
140     _VALID_URL = r'https?://(?:(?:www\.)?nytimes\.com/video/(?:[^/]+/)+?|graphics8\.nytimes\.com/bcvideo/\d+(?:\.\d+)?/iframe/embed\.html\?videoId=)(?P<id>\d+)'
141
142     _TESTS = [{
143         'url': 'http://www.nytimes.com/video/opinion/100000002847155/verbatim-what-is-a-photocopier.html?playlistId=100000001150263',
144         'md5': 'd665342765db043f7e225cff19df0f2d',
145         'info_dict': {
146             'id': '100000002847155',
147             'ext': 'mov',
148             'title': 'Verbatim: What Is a Photocopier?',
149             'description': 'md5:93603dada88ddbda9395632fdc5da260',
150             'timestamp': 1398631707,
151             'upload_date': '20140427',
152             'uploader': 'Brett Weiner',
153             'duration': 419,
154         }
155     }, {
156         'url': 'http://www.nytimes.com/video/travel/100000003550828/36-hours-in-dubai.html',
157         'only_matching': True,
158     }]
159
160     def _real_extract(self, url):
161         video_id = self._match_id(url)
162
163         return self._extract_video_from_id(video_id)
164
165
166 class NYTimesArticleIE(NYTimesBaseIE):
167     _VALID_URL = r'https?://(?:www\.)?nytimes\.com/(.(?<!video))*?/(?:[^/]+/)*(?P<id>[^.]+)(?:\.html)?'
168     _TESTS = [{
169         'url': 'http://www.nytimes.com/2015/04/14/business/owner-of-gravity-payments-a-credit-card-processor-is-setting-a-new-minimum-wage-70000-a-year.html?_r=0',
170         'md5': 'e2076d58b4da18e6a001d53fd56db3c9',
171         'info_dict': {
172             'id': '100000003628438',
173             'ext': 'mov',
174             'title': 'New Minimum Wage: $70,000 a Year',
175             'description': 'Dan Price, C.E.O. of Gravity Payments, surprised his 120-person staff by announcing that he planned over the next three years to raise the salary of every employee to $70,000 a year.',
176             'timestamp': 1429033037,
177             'upload_date': '20150414',
178             'uploader': 'Matthew Williams',
179         }
180     }, {
181         'url': 'http://www.nytimes.com/2016/10/14/podcasts/revelations-from-the-final-weeks.html',
182         'md5': 'e0d52040cafb07662acf3c9132db3575',
183         'info_dict': {
184             'id': '20',
185             'title': "The Run-Up: \u2018He Was Like an Octopus\u2019",
186             'ext': 'mp3',
187             'description': 'We go behind the story of the two women who told us that Donald Trump touched them inappropriately (which he denies) and check in on Hillary Clinton’s campaign.',
188         }
189     }, {
190         'url': 'http://www.nytimes.com/2016/10/16/books/review/inside-the-new-york-times-book-review-the-rise-of-hitler.html',
191         'md5': '66fb5471d7ef15da98af176dc1af4cb9',
192         'info_dict': {
193             'id': 'inside-the-new-york-times-book-review-the-rise-of-hitler',
194             'title': "The Rise of Hitler",
195             'ext': 'mp3',
196             'description': 'Adam Kirsch discusses Volker Ullrich\'s new biography of Hitler; Billy Collins talks about his latest collection of poems; and iO Tillett Wright on his new memoir, "Darling Days."',
197             }
198     }, {
199         'url': 'http://www.nytimes.com/news/minute/2014/03/17/times-minute-whats-next-in-crimea/?_php=true&_type=blogs&_php=true&_type=blogs&_r=1',
200         'only_matching': True,
201     }]
202
203     def _real_extract(self, url):
204         page_id = self._match_id(url)
205
206         webpage = self._download_webpage(url, page_id)
207
208         video_id = self._html_search_regex(r'data-videoid="(\d+)"', webpage, 'video id', None, False)
209         if video_id is not None:
210             return self._extract_video_from_id(video_id)
211         
212         data_json = self._html_search_regex(r'NYTD\.FlexTypes\.push\(({.*})\);', webpage, 'json data')
213         return self._extract_podcast_from_json(data_json, page_id, webpage)