]>
Commit | Line | Data |
---|---|---|
aa6cd05e | 1 | # -*- coding: utf-8 -*- |
ed86f38a JMF |
2 | from __future__ import unicode_literals |
3 | ||
e9bf7479 | 4 | import re |
9fb2f1cd S |
5 | import time |
6 | import hmac | |
7 | import binascii | |
8 | import hashlib | |
9 | ||
e9bf7479 | 10 | |
9f02ff53 | 11 | from .once import OnceIE |
05fe2594 YCH |
12 | from ..compat import ( |
13 | compat_parse_qs, | |
14 | compat_urllib_parse_urlparse, | |
15 | ) | |
1cc79574 | 16 | from ..utils import ( |
22a0a952 | 17 | determine_ext, |
f8b56e95 | 18 | ExtractorError, |
18e4088f | 19 | float_or_none, |
402a3efc | 20 | int_or_none, |
18e4088f S |
21 | sanitized_Request, |
22 | unsmuggle_url, | |
22a0a952 | 23 | update_url_query, |
18e4088f | 24 | xpath_with_ns, |
cafcf657 | 25 | mimetype2ext, |
4459bef2 | 26 | find_xpath_attr, |
e9bf7479 JMF |
27 | ) |
28 | ||
f877c6ae YCH |
29 | default_ns = 'http://www.w3.org/2005/SMIL21/Language' |
30 | _x = lambda p: xpath_with_ns(p, {'smil': default_ns}) | |
e9bf7479 JMF |
31 | |
32 | ||
9f02ff53 | 33 | class ThePlatformBaseIE(OnceIE): |
c687ac74 | 34 | def _extract_theplatform_smil(self, smil_url, video_id, note='Downloading SMIL data'): |
87c03c6b | 35 | meta = self._download_xml(smil_url, video_id, note=note, query={'format': 'SMIL'}) |
cae21032 S |
36 | error_element = find_xpath_attr(meta, _x('.//smil:ref'), 'src') |
37 | if error_element is not None and error_element.attrib['src'].startswith( | |
38 | 'http://link.theplatform.com/s/errorFiles/Unavailable.'): | |
4459bef2 | 39 | raise ExtractorError(error_element.attrib['abstract'], expected=True) |
26e1c351 | 40 | |
9f02ff53 | 41 | smil_formats = self._parse_smil_formats( |
26e1c351 YCH |
42 | meta, smil_url, video_id, namespace=default_ns, |
43 | # the parameters are from syfy.com, other sites may use others, | |
44 | # they also work for nbc.com | |
45 | f4m_params={'g': 'UXWGVKRWHFSP', 'hdcore': '3.0.3'}, | |
46 | transform_rtmp_url=lambda streamer, src: (streamer, 'mp4:' + src)) | |
47 | ||
9f02ff53 | 48 | formats = [] |
49 | for _format in smil_formats: | |
50 | if OnceIE.suitable(_format['url']): | |
51 | formats.extend(self._extract_once_formats(_format['url'])) | |
52 | else: | |
22a0a952 YCH |
53 | media_url = _format['url'] |
54 | if determine_ext(media_url) == 'm3u8': | |
55 | hdnea2 = self._get_cookies(media_url).get('hdnea2') | |
56 | if hdnea2: | |
57 | _format['url'] = update_url_query(media_url, {'hdnea3': hdnea2.value}) | |
58 | ||
9f02ff53 | 59 | formats.append(_format) |
26e1c351 | 60 | |
c687ac74 YCH |
61 | subtitles = self._parse_smil_subtitles(meta, default_ns) |
62 | ||
63 | return formats, subtitles | |
26e1c351 YCH |
64 | |
65 | def get_metadata(self, path, video_id): | |
66 | info_url = 'http://link.theplatform.com/s/%s?format=preview' % path | |
dd565ac1 | 67 | info = self._download_json(info_url, video_id) |
26e1c351 YCH |
68 | |
69 | subtitles = {} | |
70 | captions = info.get('captions') | |
71 | if isinstance(captions, list): | |
72 | for caption in captions: | |
73 | lang, src, mime = caption.get('lang', 'en'), caption.get('src'), caption.get('type') | |
74 | subtitles[lang] = [{ | |
cafcf657 | 75 | 'ext': mimetype2ext(mime), |
26e1c351 YCH |
76 | 'url': src, |
77 | }] | |
78 | ||
79 | return { | |
80 | 'title': info['title'], | |
81 | 'subtitles': subtitles, | |
82 | 'description': info['description'], | |
83 | 'thumbnail': info['defaultThumbnailUrl'], | |
84 | 'duration': int_or_none(info.get('duration'), 1000), | |
79ba9140 | 85 | 'timestamp': int_or_none(info.get('pubDate'), 1000) or None, |
86 | 'uploader': info.get('billingCode'), | |
26e1c351 YCH |
87 | } |
88 | ||
89 | ||
90 | class ThePlatformIE(ThePlatformBaseIE): | |
a97bcd80 | 91 | _VALID_URL = r'''(?x) |
9fb2f1cd | 92 | (?:https?://(?:link|player)\.theplatform\.com/[sp]/(?P<provider_id>[^/]+)/ |
c02ec7d4 | 93 | (?:(?:(?:[^/]+/)+select/)?(?P<media>media/(?:guid/\d+/)?)|(?P<config>(?:[^/\?]+/(?:swf|config)|onsite)/select/))? |
a97bcd80 | 94 | |theplatform:)(?P<id>[^/\?&]+)''' |
e9bf7479 | 95 | |
bd7a6478 | 96 | _TESTS = [{ |
e9bf7479 | 97 | # from http://www.metacafe.com/watch/cb-e9I_cZgTgIPd/blackberrys_big_bold_z30/ |
ed86f38a JMF |
98 | 'url': 'http://link.theplatform.com/s/dJ5BDC/e9I_cZgTgIPd/meta.smil?format=smil&Tracking=true&mbr=true', |
99 | 'info_dict': { | |
100 | 'id': 'e9I_cZgTgIPd', | |
101 | 'ext': 'flv', | |
102 | 'title': 'Blackberry\'s big, bold Z30', | |
103 | 'description': 'The Z30 is Blackberry\'s biggest, baddest mobile messaging device yet.', | |
104 | 'duration': 247, | |
79ba9140 | 105 | 'timestamp': 1383239700, |
106 | 'upload_date': '20131031', | |
107 | 'uploader': 'CBSI-NEW', | |
e9bf7479 | 108 | }, |
ed86f38a | 109 | 'params': { |
e9bf7479 | 110 | # rtmp download |
ed86f38a | 111 | 'skip_download': True, |
e9bf7479 | 112 | }, |
bd7a6478 | 113 | }, { |
372f08c9 | 114 | # from http://www.cnet.com/videos/tesla-model-s-a-second-step-towards-a-cleaner-motoring-future/ |
bd7a6478 YCH |
115 | 'url': 'http://link.theplatform.com/s/kYEXFC/22d_qsQ6MIRT', |
116 | 'info_dict': { | |
117 | 'id': '22d_qsQ6MIRT', | |
118 | 'ext': 'flv', | |
119 | 'description': 'md5:ac330c9258c04f9d7512cf26b9595409', | |
120 | 'title': 'Tesla Model S: A second step towards a cleaner motoring future', | |
79ba9140 | 121 | 'timestamp': 1426176191, |
122 | 'upload_date': '20150312', | |
123 | 'uploader': 'CBSI-NEW', | |
bd7a6478 YCH |
124 | }, |
125 | 'params': { | |
126 | # rtmp download | |
127 | 'skip_download': True, | |
128 | } | |
6e054aac S |
129 | }, { |
130 | 'url': 'https://player.theplatform.com/p/D6x-PC/pulse_preview/embed/select/media/yMBg9E8KFxZD', | |
131 | 'info_dict': { | |
132 | 'id': 'yMBg9E8KFxZD', | |
133 | 'ext': 'mp4', | |
134 | 'description': 'md5:644ad9188d655b742f942bf2e06b002d', | |
135 | 'title': 'HIGHLIGHTS: USA bag first ever series Cup win', | |
79ba9140 | 136 | 'uploader': 'EGSM', |
6e054aac S |
137 | } |
138 | }, { | |
139 | 'url': 'http://player.theplatform.com/p/NnzsPC/widget/select/media/4Y0TlYUr_ZT7', | |
140 | 'only_matching': True, | |
05fe2594 YCH |
141 | }, { |
142 | 'url': 'http://player.theplatform.com/p/2E2eJC/nbcNewsOffsite?guid=tdy_or_siri_150701', | |
9f02ff53 | 143 | 'md5': 'fb96bb3d85118930a5b055783a3bd992', |
05fe2594 YCH |
144 | 'info_dict': { |
145 | 'id': 'tdy_or_siri_150701', | |
146 | 'ext': 'mp4', | |
147 | 'title': 'iPhone Siri’s sassy response to a math question has people talking', | |
148 | 'description': 'md5:a565d1deadd5086f3331d57298ec6333', | |
149 | 'duration': 83.0, | |
150 | 'thumbnail': 're:^https?://.*\.jpg$', | |
151 | 'timestamp': 1435752600, | |
152 | 'upload_date': '20150701', | |
79ba9140 | 153 | 'uploader': 'NBCU-NEWS', |
05fe2594 | 154 | }, |
9a4acbfa S |
155 | }, { |
156 | # From http://www.nbc.com/the-blacklist/video/sir-crispin-crandall/2928790?onid=137781#vc137781=1 | |
157 | # geo-restricted (US), HLS encrypted with AES-128 | |
158 | 'url': 'http://player.theplatform.com/p/NnzsPC/onsite_universal/select/media/guid/2410887629/2928790?fwsitesection=nbc_the_blacklist_video_library&autoPlay=true&carouselID=137781', | |
159 | 'only_matching': True, | |
bd7a6478 | 160 | }] |
5f6a1245 | 161 | |
898f4b49 S |
162 | @classmethod |
163 | def _extract_urls(cls, webpage): | |
164 | m = re.search( | |
165 | r'''(?x) | |
166 | <meta\s+ | |
167 | property=(["'])(?:og:video(?::(?:secure_)?url)?|twitter:player)\1\s+ | |
168 | content=(["'])(?P<url>https?://player\.theplatform\.com/p/.+?)\2 | |
169 | ''', webpage) | |
170 | if m: | |
171 | return [m.group('url')] | |
172 | ||
173 | matches = re.findall( | |
174 | r'<(?:iframe|script)[^>]+src=(["\'])((?:https?:)?//player\.theplatform\.com/p/.+?)\1', webpage) | |
175 | if matches: | |
176 | return list(zip(*matches))[1] | |
177 | ||
9fb2f1cd S |
178 | @staticmethod |
179 | def _sign_url(url, sig_key, sig_secret, life=600, include_qs=False): | |
180 | flags = '10' if include_qs else '00' | |
181 | expiration_date = '%x' % (int(time.time()) + life) | |
182 | ||
183 | def str_to_hex(str): | |
184 | return binascii.b2a_hex(str.encode('ascii')).decode('ascii') | |
185 | ||
dcf094d6 YCH |
186 | def hex_to_bytes(hex): |
187 | return binascii.a2b_hex(hex.encode('ascii')) | |
9fb2f1cd | 188 | |
db1c969d | 189 | relative_path = re.match(r'https?://link.theplatform.com/s/([^?]+)', url).group(1) |
dcf094d6 | 190 | clear_text = hex_to_bytes(flags + expiration_date + str_to_hex(relative_path)) |
9fb2f1cd S |
191 | checksum = hmac.new(sig_key.encode('ascii'), clear_text, hashlib.sha1).hexdigest() |
192 | sig = flags + expiration_date + checksum + str_to_hex(sig_secret) | |
193 | return '%s&sig=%s' % (url, sig) | |
194 | ||
10e3d734 | 195 | def _real_extract(self, url): |
9fb2f1cd S |
196 | url, smuggled_data = unsmuggle_url(url, {}) |
197 | ||
10e3d734 | 198 | mobj = re.match(self._VALID_URL, url) |
9fb2f1cd | 199 | provider_id = mobj.group('provider_id') |
10e3d734 | 200 | video_id = mobj.group('id') |
9fb2f1cd S |
201 | |
202 | if not provider_id: | |
203 | provider_id = 'dJ5BDC' | |
204 | ||
c02ec7d4 | 205 | path = provider_id + '/' |
6e054aac | 206 | if mobj.group('media'): |
c02ec7d4 | 207 | path += mobj.group('media') |
208 | path += video_id | |
6e054aac | 209 | |
05fe2594 YCH |
210 | qs_dict = compat_parse_qs(compat_urllib_parse_urlparse(url).query) |
211 | if 'guid' in qs_dict: | |
212 | webpage = self._download_webpage(url, video_id) | |
213 | scripts = re.findall(r'<script[^>]+src="([^"]+)"', webpage) | |
214 | feed_id = None | |
215 | # feed id usually locates in the last script. | |
216 | # Seems there's no pattern for the interested script filename, so | |
217 | # I try one by one | |
218 | for script in reversed(scripts): | |
ee5cd841 | 219 | feed_script = self._download_webpage( |
325bb615 S |
220 | self._proto_relative_url(script, 'http:'), |
221 | video_id, 'Downloading feed script') | |
222 | feed_id = self._search_regex( | |
223 | r'defaultFeedId\s*:\s*"([^"]+)"', feed_script, | |
224 | 'default feed id', default=None) | |
05fe2594 YCH |
225 | if feed_id is not None: |
226 | break | |
227 | if feed_id is None: | |
228 | raise ExtractorError('Unable to find feed id') | |
229 | return self.url_result('http://feed.theplatform.com/f/%s/%s?byGuid=%s' % ( | |
230 | provider_id, feed_id, qs_dict['guid'][0])) | |
231 | ||
6140baf4 JMF |
232 | if smuggled_data.get('force_smil_url', False): |
233 | smil_url = url | |
ad1f4e79 S |
234 | # Explicitly specified SMIL (see https://github.com/rg3/youtube-dl/issues/7385) |
235 | elif '/guid/' in url: | |
18e4088f S |
236 | headers = {} |
237 | source_url = smuggled_data.get('source_url') | |
238 | if source_url: | |
239 | headers['Referer'] = source_url | |
240 | request = sanitized_Request(url, headers=headers) | |
241 | webpage = self._download_webpage(request, video_id) | |
ad1f4e79 S |
242 | smil_url = self._search_regex( |
243 | r'<link[^>]+href=(["\'])(?P<url>.+?)\1[^>]+type=["\']application/smil\+xml', | |
244 | webpage, 'smil url', group='url') | |
245 | path = self._search_regex( | |
246 | r'link\.theplatform\.com/s/((?:[^/?#&]+/)+[^/?#&]+)', smil_url, 'path') | |
4c92fd2e | 247 | smil_url += '?' if '?' not in smil_url else '&' + 'formats=m3u,mpeg4' |
6140baf4 | 248 | elif mobj.group('config'): |
5f6a1245 | 249 | config_url = url + '&form=json' |
10e3d734 PH |
250 | config_url = config_url.replace('swf/', 'config/') |
251 | config_url = config_url.replace('onsite/', 'onsite/config/') | |
252 | config = self._download_json(config_url, video_id, 'Downloading config') | |
28479149 YCH |
253 | if 'releaseUrl' in config: |
254 | release_url = config['releaseUrl'] | |
255 | else: | |
256 | release_url = 'http://link.theplatform.com/s/%s?mbr=true' % path | |
4c92fd2e | 257 | smil_url = release_url + '&formats=MPEG4&manifest=f4m' |
10e3d734 | 258 | else: |
4c92fd2e | 259 | smil_url = 'http://link.theplatform.com/s/%s?mbr=true' % path |
9fb2f1cd S |
260 | |
261 | sig = smuggled_data.get('sig') | |
262 | if sig: | |
263 | smil_url = self._sign_url(smil_url, sig['key'], sig['secret']) | |
e9bf7479 | 264 | |
c687ac74 | 265 | formats, subtitles = self._extract_theplatform_smil(smil_url, video_id) |
a71fca85 | 266 | self._sort_formats(formats) |
f8b56e95 | 267 | |
26e1c351 | 268 | ret = self.get_metadata(path, video_id) |
c687ac74 | 269 | combined_subtitles = self._merge_subtitles(ret.get('subtitles', {}), subtitles) |
26e1c351 YCH |
270 | ret.update({ |
271 | 'id': video_id, | |
272 | 'formats': formats, | |
c687ac74 | 273 | 'subtitles': combined_subtitles, |
26e1c351 | 274 | }) |
e9bf7479 | 275 | |
26e1c351 | 276 | return ret |
748ec667 | 277 | |
f877c6ae | 278 | |
26e1c351 YCH |
279 | class ThePlatformFeedIE(ThePlatformBaseIE): |
280 | _URL_TEMPLATE = '%s//feed.theplatform.com/f/%s/%s?form=json&byGuid=%s' | |
281 | _VALID_URL = r'https?://feed\.theplatform\.com/f/(?P<provider_id>[^/]+)/(?P<feed_id>[^?/]+)\?(?:[^&]+&)*byGuid=(?P<id>[a-zA-Z0-9_]+)' | |
282 | _TEST = { | |
283 | # From http://player.theplatform.com/p/7wvmTC/MSNBCEmbeddedOffSite?guid=n_hardball_5biden_140207 | |
284 | 'url': 'http://feed.theplatform.com/f/7wvmTC/msnbc_video-p-test?form=json&pretty=true&range=-40&byGuid=n_hardball_5biden_140207', | |
9f02ff53 | 285 | 'md5': '6e32495b5073ab414471b615c5ded394', |
26e1c351 YCH |
286 | 'info_dict': { |
287 | 'id': 'n_hardball_5biden_140207', | |
288 | 'ext': 'mp4', | |
289 | 'title': 'The Biden factor: will Joe run in 2016?', | |
290 | 'description': 'Could Vice President Joe Biden be preparing a 2016 campaign? Mark Halperin and Sam Stein weigh in.', | |
291 | 'thumbnail': 're:^https?://.*\.jpg$', | |
292 | 'upload_date': '20140208', | |
293 | 'timestamp': 1391824260, | |
294 | 'duration': 467.0, | |
295 | 'categories': ['MSNBC/Issues/Democrats', 'MSNBC/Issues/Elections/Election 2016'], | |
0738187f | 296 | 'uploader': 'NBCU-NEWS', |
26e1c351 YCH |
297 | }, |
298 | } | |
299 | ||
300 | def _real_extract(self, url): | |
301 | mobj = re.match(self._VALID_URL, url) | |
302 | ||
303 | video_id = mobj.group('id') | |
304 | provider_id = mobj.group('provider_id') | |
305 | feed_id = mobj.group('feed_id') | |
306 | ||
307 | real_url = self._URL_TEMPLATE % (self.http_scheme(), provider_id, feed_id, video_id) | |
308 | feed = self._download_json(real_url, video_id) | |
309 | entry = feed['entries'][0] | |
310 | ||
311 | formats = [] | |
c687ac74 | 312 | subtitles = {} |
26e1c351 YCH |
313 | first_video_id = None |
314 | duration = None | |
315 | for item in entry['media$content']: | |
4c92fd2e | 316 | smil_url = item['plfile$url'] + '&mbr=true' |
3dc71d82 | 317 | cur_video_id = ThePlatformIE._match_id(smil_url) |
26e1c351 YCH |
318 | if first_video_id is None: |
319 | first_video_id = cur_video_id | |
320 | duration = float_or_none(item.get('plfile$duration')) | |
c687ac74 YCH |
321 | cur_formats, cur_subtitles = self._extract_theplatform_smil(smil_url, video_id, 'Downloading SMIL data for %s' % cur_video_id) |
322 | formats.extend(cur_formats) | |
323 | subtitles = self._merge_subtitles(subtitles, cur_subtitles) | |
f877c6ae YCH |
324 | |
325 | self._sort_formats(formats) | |
e9bf7479 | 326 | |
26e1c351 YCH |
327 | thumbnails = [{ |
328 | 'url': thumbnail['plfile$url'], | |
329 | 'width': int_or_none(thumbnail.get('plfile$width')), | |
330 | 'height': int_or_none(thumbnail.get('plfile$height')), | |
331 | } for thumbnail in entry.get('media$thumbnails', [])] | |
332 | ||
333 | timestamp = int_or_none(entry.get('media$availableDate'), scale=1000) | |
334 | categories = [item['media$name'] for item in entry.get('media$categories', [])] | |
335 | ||
336 | ret = self.get_metadata('%s/%s' % (provider_id, first_video_id), video_id) | |
c687ac74 | 337 | subtitles = self._merge_subtitles(subtitles, ret['subtitles']) |
26e1c351 | 338 | ret.update({ |
e9bf7479 | 339 | 'id': video_id, |
e9bf7479 | 340 | 'formats': formats, |
c687ac74 | 341 | 'subtitles': subtitles, |
26e1c351 YCH |
342 | 'thumbnails': thumbnails, |
343 | 'duration': duration, | |
344 | 'timestamp': timestamp, | |
345 | 'categories': categories, | |
346 | }) | |
347 | ||
348 | return ret |