Merge pull request #10596 from stepshal/r_prefix
[youtube-dl] / youtube_dl / extractor / twitter.py
1 # coding: utf-8
2 from __future__ import unicode_literals
3
4 import re
5
6 from .common import InfoExtractor
7 from ..utils import (
8     determine_ext,
9     float_or_none,
10     xpath_text,
11     remove_end,
12     int_or_none,
13     ExtractorError,
14 )
15
16
17 class TwitterBaseIE(InfoExtractor):
18     def _get_vmap_video_url(self, vmap_url, video_id):
19         vmap_data = self._download_xml(vmap_url, video_id)
20         return xpath_text(vmap_data, './/MediaFile').strip()
21
22
23 class TwitterCardIE(TwitterBaseIE):
24     IE_NAME = 'twitter:card'
25     _VALID_URL = r'https?://(?:www\.)?twitter\.com/i/(?:cards/tfw/v1|videos/tweet)/(?P<id>\d+)'
26     _TESTS = [
27         {
28             'url': 'https://twitter.com/i/cards/tfw/v1/560070183650213889',
29             # MD5 checksums are different in different places
30             'info_dict': {
31                 'id': '560070183650213889',
32                 'ext': 'mp4',
33                 'title': 'Twitter Card',
34                 'thumbnail': 're:^https?://.*\.jpg$',
35                 'duration': 30.033,
36             }
37         },
38         {
39             'url': 'https://twitter.com/i/cards/tfw/v1/623160978427936768',
40             'md5': '7ee2a553b63d1bccba97fbed97d9e1c8',
41             'info_dict': {
42                 'id': '623160978427936768',
43                 'ext': 'mp4',
44                 'title': 'Twitter Card',
45                 'thumbnail': 're:^https?://.*\.jpg',
46                 'duration': 80.155,
47             },
48         },
49         {
50             'url': 'https://twitter.com/i/cards/tfw/v1/654001591733886977',
51             'md5': 'd4724ffe6d2437886d004fa5de1043b3',
52             'info_dict': {
53                 'id': 'dq4Oj5quskI',
54                 'ext': 'mp4',
55                 'title': 'Ubuntu 11.10 Overview',
56                 'description': 'Take a quick peek at what\'s new and improved in Ubuntu 11.10.\n\nOnce installed take a look at 10 Things to Do After Installing: http://www.omgubuntu.co.uk/2011/10/10...',
57                 'upload_date': '20111013',
58                 'uploader': 'OMG! Ubuntu!',
59                 'uploader_id': 'omgubuntu',
60             },
61             'add_ie': ['Youtube'],
62         },
63         {
64             'url': 'https://twitter.com/i/cards/tfw/v1/665289828897005568',
65             'md5': 'ab2745d0b0ce53319a534fccaa986439',
66             'info_dict': {
67                 'id': 'iBb2x00UVlv',
68                 'ext': 'mp4',
69                 'upload_date': '20151113',
70                 'uploader_id': '1189339351084113920',
71                 'uploader': 'ArsenalTerje',
72                 'title': 'Vine by ArsenalTerje',
73             },
74             'add_ie': ['Vine'],
75         }, {
76             'url': 'https://twitter.com/i/videos/tweet/705235433198714880',
77             'md5': '3846d0a07109b5ab622425449b59049d',
78             'info_dict': {
79                 'id': '705235433198714880',
80                 'ext': 'mp4',
81                 'title': 'Twitter web player',
82                 'thumbnail': 're:^https?://.*\.jpg',
83             },
84         },
85     ]
86
87     def _real_extract(self, url):
88         video_id = self._match_id(url)
89
90         config = None
91         formats = []
92         duration = None
93
94         webpage = self._download_webpage(url, video_id)
95
96         iframe_url = self._html_search_regex(
97             r'<iframe[^>]+src="((?:https?:)?//(?:www.youtube.com/embed/[^"]+|(?:www\.)?vine\.co/v/\w+/card))"',
98             webpage, 'video iframe', default=None)
99         if iframe_url:
100             return self.url_result(iframe_url)
101
102         config = self._parse_json(self._html_search_regex(
103             r'data-(?:player-)?config="([^"]+)"', webpage, 'data player config'),
104             video_id)
105
106         if config.get('source_type') == 'vine':
107             return self.url_result(config['player_url'], 'Vine')
108
109         def _search_dimensions_in_video_url(a_format, video_url):
110             m = re.search(r'/(?P<width>\d+)x(?P<height>\d+)/', video_url)
111             if m:
112                 a_format.update({
113                     'width': int(m.group('width')),
114                     'height': int(m.group('height')),
115                 })
116
117         video_url = config.get('video_url') or config.get('playlist', [{}])[0].get('source')
118
119         if video_url:
120             if determine_ext(video_url) == 'm3u8':
121                 formats.extend(self._extract_m3u8_formats(video_url, video_id, ext='mp4', m3u8_id='hls'))
122             else:
123                 f = {
124                     'url': video_url,
125                 }
126
127                 _search_dimensions_in_video_url(f, video_url)
128
129                 formats.append(f)
130
131         vmap_url = config.get('vmapUrl') or config.get('vmap_url')
132         if vmap_url:
133             formats.append({
134                 'url': self._get_vmap_video_url(vmap_url, video_id),
135             })
136
137         media_info = None
138
139         for entity in config.get('status', {}).get('entities', []):
140             if 'mediaInfo' in entity:
141                 media_info = entity['mediaInfo']
142
143         if media_info:
144             for media_variant in media_info['variants']:
145                 media_url = media_variant['url']
146                 if media_url.endswith('.m3u8'):
147                     formats.extend(self._extract_m3u8_formats(media_url, video_id, ext='mp4', m3u8_id='hls'))
148                 elif media_url.endswith('.mpd'):
149                     formats.extend(self._extract_mpd_formats(media_url, video_id, mpd_id='dash'))
150                 else:
151                     vbr = int_or_none(media_variant.get('bitRate'), scale=1000)
152                     a_format = {
153                         'url': media_url,
154                         'format_id': 'http-%d' % vbr if vbr else 'http',
155                         'vbr': vbr,
156                     }
157                     # Reported bitRate may be zero
158                     if not a_format['vbr']:
159                         del a_format['vbr']
160
161                     _search_dimensions_in_video_url(a_format, media_url)
162
163                     formats.append(a_format)
164
165             duration = float_or_none(media_info.get('duration', {}).get('nanos'), scale=1e9)
166
167         self._sort_formats(formats)
168
169         title = self._search_regex(r'<title>([^<]+)</title>', webpage, 'title')
170         thumbnail = config.get('posterImageUrl') or config.get('image_src')
171         duration = float_or_none(config.get('duration')) or duration
172
173         return {
174             'id': video_id,
175             'title': title,
176             'thumbnail': thumbnail,
177             'duration': duration,
178             'formats': formats,
179         }
180
181
182 class TwitterIE(InfoExtractor):
183     IE_NAME = 'twitter'
184     _VALID_URL = r'https?://(?:www\.|m\.|mobile\.)?twitter\.com/(?P<user_id>[^/]+)/status/(?P<id>\d+)'
185     _TEMPLATE_URL = 'https://twitter.com/%s/status/%s'
186
187     _TESTS = [{
188         'url': 'https://twitter.com/freethenipple/status/643211948184596480',
189         'info_dict': {
190             'id': '643211948184596480',
191             'ext': 'mp4',
192             'title': 'FREE THE NIPPLE - FTN supporters on Hollywood Blvd today!',
193             'thumbnail': 're:^https?://.*\.jpg',
194             'description': 'FREE THE NIPPLE on Twitter: "FTN supporters on Hollywood Blvd today! http://t.co/c7jHH749xJ"',
195             'uploader': 'FREE THE NIPPLE',
196             'uploader_id': 'freethenipple',
197         },
198         'params': {
199             'skip_download': True,  # requires ffmpeg
200         },
201     }, {
202         'url': 'https://twitter.com/giphz/status/657991469417025536/photo/1',
203         'md5': 'f36dcd5fb92bf7057f155e7d927eeb42',
204         'info_dict': {
205             'id': '657991469417025536',
206             'ext': 'mp4',
207             'title': 'Gifs - tu vai cai tu vai cai tu nao eh capaz disso tu vai cai',
208             'description': 'Gifs on Twitter: "tu vai cai tu vai cai tu nao eh capaz disso tu vai cai https://t.co/tM46VHFlO5"',
209             'thumbnail': 're:^https?://.*\.png',
210             'uploader': 'Gifs',
211             'uploader_id': 'giphz',
212         },
213         'expected_warnings': ['height', 'width'],
214         'skip': 'Account suspended',
215     }, {
216         'url': 'https://twitter.com/starwars/status/665052190608723968',
217         'md5': '39b7199856dee6cd4432e72c74bc69d4',
218         'info_dict': {
219             'id': '665052190608723968',
220             'ext': 'mp4',
221             'title': 'Star Wars - A new beginning is coming December 18. Watch the official 60 second #TV spot for #StarWars: #TheForceAwakens.',
222             'description': 'Star Wars on Twitter: "A new beginning is coming December 18. Watch the official 60 second #TV spot for #StarWars: #TheForceAwakens."',
223             'uploader_id': 'starwars',
224             'uploader': 'Star Wars',
225         },
226     }, {
227         'url': 'https://twitter.com/BTNBrentYarina/status/705235433198714880',
228         'info_dict': {
229             'id': '705235433198714880',
230             'ext': 'mp4',
231             'title': 'Brent Yarina - Khalil Iverson\'s missed highlight dunk. And made highlight dunk. In one highlight.',
232             'description': 'Brent Yarina on Twitter: "Khalil Iverson\'s missed highlight dunk. And made highlight dunk. In one highlight."',
233             'uploader_id': 'BTNBrentYarina',
234             'uploader': 'Brent Yarina',
235         },
236         'params': {
237             # The same video as https://twitter.com/i/videos/tweet/705235433198714880
238             # Test case of TwitterCardIE
239             'skip_download': True,
240         },
241     }, {
242         'url': 'https://twitter.com/jaydingeer/status/700207533655363584',
243         'md5': '',
244         'info_dict': {
245             'id': '700207533655363584',
246             'ext': 'mp4',
247             'title': 'Donte The Dumbass - BEAT PROD: @suhmeduh #Damndaniel',
248             'description': 'Donte The Dumbass on Twitter: "BEAT PROD: @suhmeduh  https://t.co/HBrQ4AfpvZ #Damndaniel https://t.co/byBooq2ejZ"',
249             'thumbnail': 're:^https?://.*\.jpg',
250             'uploader': 'Donte The Dumbass',
251             'uploader_id': 'jaydingeer',
252         },
253         'params': {
254             'skip_download': True,  # requires ffmpeg
255         },
256     }, {
257         'url': 'https://twitter.com/Filmdrunk/status/713801302971588609',
258         'md5': '89a15ed345d13b86e9a5a5e051fa308a',
259         'info_dict': {
260             'id': 'MIOxnrUteUd',
261             'ext': 'mp4',
262             'title': 'Dr.Pepperの飲み方 #japanese #バカ #ドクペ #電動ガン',
263             'uploader': 'TAKUMA',
264             'uploader_id': '1004126642786242560',
265             'upload_date': '20140615',
266         },
267         'add_ie': ['Vine'],
268     }, {
269         'url': 'https://twitter.com/captainamerica/status/719944021058060289',
270         'info_dict': {
271             'id': '719944021058060289',
272             'ext': 'mp4',
273             'title': 'Captain America - @King0fNerd Are you sure you made the right choice? Find out in theaters.',
274             'description': 'Captain America on Twitter: "@King0fNerd Are you sure you made the right choice? Find out in theaters. https://t.co/GpgYi9xMJI"',
275             'uploader_id': 'captainamerica',
276             'uploader': 'Captain America',
277         },
278         'params': {
279             'skip_download': True,  # requires ffmpeg
280         },
281     }]
282
283     def _real_extract(self, url):
284         mobj = re.match(self._VALID_URL, url)
285         user_id = mobj.group('user_id')
286         twid = mobj.group('id')
287
288         webpage, urlh = self._download_webpage_handle(
289             self._TEMPLATE_URL % (user_id, twid), twid)
290
291         if 'twitter.com/account/suspended' in urlh.geturl():
292             raise ExtractorError('Account suspended by Twitter.', expected=True)
293
294         username = remove_end(self._og_search_title(webpage), ' on Twitter')
295
296         title = description = self._og_search_description(webpage).strip('').replace('\n', ' ').strip('“”')
297
298         # strip  'https -_t.co_BJYgOjSeGA' junk from filenames
299         title = re.sub(r'\s+(https?://[^ ]+)', '', title)
300
301         info = {
302             'uploader_id': user_id,
303             'uploader': username,
304             'webpage_url': url,
305             'description': '%s on Twitter: "%s"' % (username, description),
306             'title': username + ' - ' + title,
307         }
308
309         mobj = re.search(r'''(?x)
310             <video[^>]+class="animated-gif"(?P<more_info>[^>]+)>\s*
311                 <source[^>]+video-src="(?P<url>[^"]+)"
312         ''', webpage)
313
314         if mobj:
315             more_info = mobj.group('more_info')
316             height = int_or_none(self._search_regex(
317                 r'data-height="(\d+)"', more_info, 'height', fatal=False))
318             width = int_or_none(self._search_regex(
319                 r'data-width="(\d+)"', more_info, 'width', fatal=False))
320             thumbnail = self._search_regex(
321                 r'poster="([^"]+)"', more_info, 'poster', fatal=False)
322             info.update({
323                 'id': twid,
324                 'url': mobj.group('url'),
325                 'height': height,
326                 'width': width,
327                 'thumbnail': thumbnail,
328             })
329             return info
330
331         if 'class="PlayableMedia' in webpage:
332             info.update({
333                 '_type': 'url_transparent',
334                 'ie_key': 'TwitterCard',
335                 'url': '%s//twitter.com/i/videos/tweet/%s' % (self.http_scheme(), twid),
336             })
337
338             return info
339
340         raise ExtractorError('There\'s no video in this tweet.')
341
342
343 class TwitterAmplifyIE(TwitterBaseIE):
344     IE_NAME = 'twitter:amplify'
345     _VALID_URL = r'https?://amp\.twimg\.com/v/(?P<id>[0-9a-f\-]{36})'
346
347     _TEST = {
348         'url': 'https://amp.twimg.com/v/0ba0c3c7-0af3-4c0a-bed5-7efd1ffa2951',
349         'md5': '7df102d0b9fd7066b86f3159f8e81bf6',
350         'info_dict': {
351             'id': '0ba0c3c7-0af3-4c0a-bed5-7efd1ffa2951',
352             'ext': 'mp4',
353             'title': 'Twitter Video',
354             'thumbnail': 're:^https?://.*',
355         },
356     }
357
358     def _real_extract(self, url):
359         video_id = self._match_id(url)
360         webpage = self._download_webpage(url, video_id)
361
362         vmap_url = self._html_search_meta(
363             'twitter:amplify:vmap', webpage, 'vmap url')
364         video_url = self._get_vmap_video_url(vmap_url, video_id)
365
366         thumbnails = []
367         thumbnail = self._html_search_meta(
368             'twitter:image:src', webpage, 'thumbnail', fatal=False)
369
370         def _find_dimension(target):
371             w = int_or_none(self._html_search_meta(
372                 'twitter:%s:width' % target, webpage, fatal=False))
373             h = int_or_none(self._html_search_meta(
374                 'twitter:%s:height' % target, webpage, fatal=False))
375             return w, h
376
377         if thumbnail:
378             thumbnail_w, thumbnail_h = _find_dimension('image')
379             thumbnails.append({
380                 'url': thumbnail,
381                 'width': thumbnail_w,
382                 'height': thumbnail_h,
383             })
384
385         video_w, video_h = _find_dimension('player')
386         formats = [{
387             'url': video_url,
388             'width': video_w,
389             'height': video_h,
390         }]
391
392         return {
393             'id': video_id,
394             'title': 'Twitter Video',
395             'formats': formats,
396             'thumbnails': thumbnails,
397         }