[vevo] Extract video versions from webpage as a last resort (Closes #8426, closes...
[youtube-dl] / youtube_dl / extractor / vevo.py
1 from __future__ import unicode_literals
2
3 import re
4
5 from .common import InfoExtractor
6 from ..compat import (
7     compat_etree_fromstring,
8     compat_urlparse,
9 )
10 from ..utils import (
11     ExtractorError,
12     int_or_none,
13     sanitized_Request,
14     parse_iso8601,
15 )
16
17
18 class VevoBaseIE(InfoExtractor):
19     def _extract_json(self, webpage, video_id, item):
20         return self._parse_json(
21             self._search_regex(
22                 r'window\.__INITIAL_STORE__\s*=\s*({.+?});\s*</script>',
23                 webpage, 'initial store'),
24             video_id)['default'][item]
25
26
27 class VevoIE(VevoBaseIE):
28     '''
29     Accepts urls from vevo.com or in the format 'vevo:{id}'
30     (currently used by MTVIE and MySpaceIE)
31     '''
32     _VALID_URL = r'''(?x)
33         (?:https?://www\.vevo\.com/watch/(?!playlist|genre)(?:[^/]+/(?:[^/]+/)?)?|
34            https?://cache\.vevo\.com/m/html/embed\.html\?video=|
35            https?://videoplayer\.vevo\.com/embed/embedded\?videoId=|
36            vevo:)
37         (?P<id>[^&?#]+)'''
38
39     _TESTS = [{
40         'url': 'http://www.vevo.com/watch/hurts/somebody-to-die-for/GB1101300280',
41         'md5': '95ee28ee45e70130e3ab02b0f579ae23',
42         'info_dict': {
43             'id': 'GB1101300280',
44             'ext': 'mp4',
45             'title': 'Somebody to Die For',
46             'upload_date': '20130624',
47             'uploader': 'Hurts',
48             'timestamp': 1372057200,
49         },
50     }, {
51         'note': 'v3 SMIL format',
52         'url': 'http://www.vevo.com/watch/cassadee-pope/i-wish-i-could-break-your-heart/USUV71302923',
53         'md5': 'f6ab09b034f8c22969020b042e5ac7fc',
54         'info_dict': {
55             'id': 'USUV71302923',
56             'ext': 'mp4',
57             'title': 'I Wish I Could Break Your Heart',
58             'upload_date': '20140219',
59             'uploader': 'Cassadee Pope',
60             'timestamp': 1392796919,
61         },
62     }, {
63         'note': 'Age-limited video',
64         'url': 'https://www.vevo.com/watch/justin-timberlake/tunnel-vision-explicit/USRV81300282',
65         'info_dict': {
66             'id': 'USRV81300282',
67             'ext': 'mp4',
68             'title': 'Tunnel Vision (Explicit)',
69             'upload_date': '20130703',
70             'age_limit': 18,
71             'uploader': 'Justin Timberlake',
72             'timestamp': 1372888800,
73         },
74     }, {
75         'note': 'No video_info',
76         'url': 'http://www.vevo.com/watch/k-camp-1/Till-I-Die/USUV71503000',
77         'md5': '8b83cc492d72fc9cf74a02acee7dc1b0',
78         'info_dict': {
79             'id': 'USUV71503000',
80             'ext': 'mp4',
81             'title': 'Till I Die',
82             'upload_date': '20151207',
83             'age_limit': 18,
84             'uploader': 'K Camp',
85             'timestamp': 1449468000,
86         },
87     }]
88     _SMIL_BASE_URL = 'http://smil.lvl3.vevo.com'
89     _SOURCE_TYPES = {
90         0: 'youtube',
91         1: 'brightcove',
92         2: 'http',
93         3: 'hls_ios',
94         4: 'hls',
95         5: 'smil',  # http
96         7: 'f4m_cc',
97         8: 'f4m_ak',
98         9: 'f4m_l3',
99         10: 'ism',
100         13: 'smil',  # rtmp
101         18: 'dash',
102     }
103     _VERSIONS = {
104         0: 'youtube',  # only in AuthenticateVideo videoVersions
105         1: 'level3',
106         2: 'akamai',
107         3: 'level3',
108         4: 'amazon',
109     }
110
111     def _parse_smil_formats(self, smil, smil_url, video_id, namespace=None, f4m_params=None, transform_rtmp_url=None):
112         formats = []
113         els = smil.findall('.//{http://www.w3.org/2001/SMIL20/Language}video')
114         for el in els:
115             src = el.attrib['src']
116             m = re.match(r'''(?xi)
117                 (?P<ext>[a-z0-9]+):
118                 (?P<path>
119                     [/a-z0-9]+     # The directory and main part of the URL
120                     _(?P<tbr>[0-9]+)k
121                     _(?P<width>[0-9]+)x(?P<height>[0-9]+)
122                     _(?P<vcodec>[a-z0-9]+)
123                     _(?P<vbr>[0-9]+)
124                     _(?P<acodec>[a-z0-9]+)
125                     _(?P<abr>[0-9]+)
126                     \.[a-z0-9]+  # File extension
127                 )''', src)
128             if not m:
129                 continue
130
131             format_url = self._SMIL_BASE_URL + m.group('path')
132             formats.append({
133                 'url': format_url,
134                 'format_id': 'smil_' + m.group('tbr'),
135                 'vcodec': m.group('vcodec'),
136                 'acodec': m.group('acodec'),
137                 'tbr': int(m.group('tbr')),
138                 'vbr': int(m.group('vbr')),
139                 'abr': int(m.group('abr')),
140                 'ext': m.group('ext'),
141                 'width': int(m.group('width')),
142                 'height': int(m.group('height')),
143             })
144         return formats
145
146     def _initialize_api(self, video_id):
147         req = sanitized_Request(
148             'http://www.vevo.com/auth', data=b'')
149         webpage = self._download_webpage(
150             req, None,
151             note='Retrieving oauth token',
152             errnote='Unable to retrieve oauth token')
153
154         if 'THIS PAGE IS CURRENTLY UNAVAILABLE IN YOUR REGION' in webpage:
155             raise ExtractorError(
156                 '%s said: This page is currently unavailable in your region.' % self.IE_NAME, expected=True)
157
158         auth_info = self._parse_json(webpage, video_id)
159         self._api_url_template = self.http_scheme() + '//apiv2.vevo.com/%s?token=' + auth_info['access_token']
160
161     def _call_api(self, path, *args, **kwargs):
162         return self._download_json(self._api_url_template % path, *args, **kwargs)
163
164     def _real_extract(self, url):
165         video_id = self._match_id(url)
166
167         json_url = 'http://api.vevo.com/VideoService/AuthenticateVideo?isrc=%s' % video_id
168         response = self._download_json(
169             json_url, video_id, 'Downloading video info', 'Unable to download info')
170         video_info = response.get('video') or {}
171         video_versions = video_info.get('videoVersions')
172         uploader = None
173         timestamp = None
174         view_count = None
175         formats = []
176
177         if not video_info:
178             if response.get('statusCode') != 909:
179                 ytid = response.get('errorInfo', {}).get('ytid')
180                 if ytid:
181                     self.report_warning(
182                         'Video is geoblocked, trying with the YouTube video %s' % ytid)
183                     return self.url_result(ytid, 'Youtube', ytid)
184
185                 if 'statusMessage' in response:
186                     raise ExtractorError('%s said: %s' % (
187                         self.IE_NAME, response['statusMessage']), expected=True)
188                 raise ExtractorError('Unable to extract videos')
189
190             self._initialize_api(video_id)
191             video_info = self._call_api(
192                 'video/%s' % video_id, video_id, 'Downloading api video info',
193                 'Failed to download video info')
194
195             video_versions = self._call_api(
196                 'video/%s/streams' % video_id, video_id,
197                 'Downloading video versions info',
198                 'Failed to download video versions info',
199                 fatal=False)
200
201             # Some videos are only available via webpage (e.g.
202             # https://github.com/rg3/youtube-dl/issues/9366)
203             if not video_versions:
204                 webpage = self._download_webpage(url, video_id)
205                 video_versions = self._extract_json(webpage, video_id, 'streams')[video_id][0]
206
207             timestamp = parse_iso8601(video_info.get('releaseDate'))
208             artists = video_info.get('artists')
209             if artists:
210                 uploader = artists[0]['name']
211             view_count = int_or_none(video_info.get('views', {}).get('total'))
212
213             for video_version in video_versions:
214                 version = self._VERSIONS.get(video_version['version'])
215                 version_url = video_version.get('url')
216                 if not version_url:
217                     continue
218
219                 if '.ism' in version_url:
220                     continue
221                 elif '.mpd' in version_url:
222                     formats.extend(self._extract_mpd_formats(
223                         version_url, video_id, mpd_id='dash-%s' % version,
224                         note='Downloading %s MPD information' % version,
225                         errnote='Failed to download %s MPD information' % version,
226                         fatal=False))
227                 elif '.m3u8' in version_url:
228                     formats.extend(self._extract_m3u8_formats(
229                         version_url, video_id, 'mp4', 'm3u8_native',
230                         m3u8_id='hls-%s' % version,
231                         note='Downloading %s m3u8 information' % version,
232                         errnote='Failed to download %s m3u8 information' % version,
233                         fatal=False))
234                 else:
235                     m = re.search(r'''(?xi)
236                         _(?P<width>[0-9]+)x(?P<height>[0-9]+)
237                         _(?P<vcodec>[a-z0-9]+)
238                         _(?P<vbr>[0-9]+)
239                         _(?P<acodec>[a-z0-9]+)
240                         _(?P<abr>[0-9]+)
241                         \.(?P<ext>[a-z0-9]+)''', version_url)
242                     if not m:
243                         continue
244
245                     formats.append({
246                         'url': version_url,
247                         'format_id': 'http-%s-%s' % (version, video_version['quality']),
248                         'vcodec': m.group('vcodec'),
249                         'acodec': m.group('acodec'),
250                         'vbr': int(m.group('vbr')),
251                         'abr': int(m.group('abr')),
252                         'ext': m.group('ext'),
253                         'width': int(m.group('width')),
254                         'height': int(m.group('height')),
255                     })
256         else:
257             timestamp = int_or_none(self._search_regex(
258                 r'/Date\((\d+)\)/',
259                 video_info['releaseDate'], 'release date', fatal=False),
260                 scale=1000)
261             artists = video_info.get('mainArtists')
262             if artists:
263                 uploader = artists[0]['artistName']
264
265             smil_parsed = False
266             for video_version in video_info['videoVersions']:
267                 version = self._VERSIONS.get(video_version['version'])
268                 if version == 'youtube':
269                     continue
270                 else:
271                     source_type = self._SOURCE_TYPES.get(video_version['sourceType'])
272                     renditions = compat_etree_fromstring(video_version['data'])
273                     if source_type == 'http':
274                         for rend in renditions.findall('rendition'):
275                             attr = rend.attrib
276                             formats.append({
277                                 'url': attr['url'],
278                                 'format_id': 'http-%s-%s' % (version, attr['name']),
279                                 'height': int_or_none(attr.get('frameheight')),
280                                 'width': int_or_none(attr.get('frameWidth')),
281                                 'tbr': int_or_none(attr.get('totalBitrate')),
282                                 'vbr': int_or_none(attr.get('videoBitrate')),
283                                 'abr': int_or_none(attr.get('audioBitrate')),
284                                 'vcodec': attr.get('videoCodec'),
285                                 'acodec': attr.get('audioCodec'),
286                             })
287                     elif source_type == 'hls':
288                         formats.extend(self._extract_m3u8_formats(
289                             renditions.find('rendition').attrib['url'], video_id,
290                             'mp4', 'm3u8_native', m3u8_id='hls-%s' % version,
291                             note='Downloading %s m3u8 information' % version,
292                             errnote='Failed to download %s m3u8 information' % version,
293                             fatal=False))
294                     elif source_type == 'smil' and version == 'level3' and not smil_parsed:
295                         formats.extend(self._extract_smil_formats(
296                             renditions.find('rendition').attrib['url'], video_id, False))
297                         smil_parsed = True
298         self._sort_formats(formats)
299
300         title = video_info['title']
301
302         is_explicit = video_info.get('isExplicit')
303         if is_explicit is True:
304             age_limit = 18
305         elif is_explicit is False:
306             age_limit = 0
307         else:
308             age_limit = None
309
310         duration = video_info.get('duration')
311
312         return {
313             'id': video_id,
314             'title': title,
315             'formats': formats,
316             'thumbnail': video_info.get('imageUrl') or video_info.get('thumbnailUrl'),
317             'timestamp': timestamp,
318             'uploader': uploader,
319             'duration': duration,
320             'view_count': view_count,
321             'age_limit': age_limit,
322         }
323
324
325 class VevoPlaylistIE(VevoBaseIE):
326     _VALID_URL = r'https?://www\.vevo\.com/watch/(?P<kind>playlist|genre)/(?P<id>[^/?#&]+)'
327
328     _TESTS = [{
329         'url': 'http://www.vevo.com/watch/playlist/dadbf4e7-b99f-4184-9670-6f0e547b6a29',
330         'info_dict': {
331             'id': 'dadbf4e7-b99f-4184-9670-6f0e547b6a29',
332             'title': 'Best-Of: Birdman',
333         },
334         'playlist_count': 10,
335     }, {
336         'url': 'http://www.vevo.com/watch/genre/rock',
337         'info_dict': {
338             'id': 'rock',
339             'title': 'Rock',
340         },
341         'playlist_count': 20,
342     }, {
343         'url': 'http://www.vevo.com/watch/playlist/dadbf4e7-b99f-4184-9670-6f0e547b6a29?index=0',
344         'md5': '32dcdfddddf9ec6917fc88ca26d36282',
345         'info_dict': {
346             'id': 'USCMV1100073',
347             'ext': 'mp4',
348             'title': 'Y.U. MAD',
349             'timestamp': 1323417600,
350             'upload_date': '20111209',
351             'uploader': 'Birdman',
352         },
353         'expected_warnings': ['Unable to download SMIL file'],
354     }, {
355         'url': 'http://www.vevo.com/watch/genre/rock?index=0',
356         'only_matching': True,
357     }]
358
359     def _real_extract(self, url):
360         mobj = re.match(self._VALID_URL, url)
361         playlist_id = mobj.group('id')
362         playlist_kind = mobj.group('kind')
363
364         webpage = self._download_webpage(url, playlist_id)
365
366         qs = compat_urlparse.parse_qs(compat_urlparse.urlparse(url).query)
367         index = qs.get('index', [None])[0]
368
369         if index:
370             video_id = self._search_regex(
371                 r'<meta[^>]+content=(["\'])vevo://video/(?P<id>.+?)\1[^>]*>',
372                 webpage, 'video id', default=None, group='id')
373             if video_id:
374                 return self.url_result('vevo:%s' % video_id, VevoIE.ie_key())
375
376         playlists = self._extract_json(webpage, playlist_id, '%ss' % playlist_kind)
377
378         playlist = (list(playlists.values())[0]
379                     if playlist_kind == 'playlist' else playlists[playlist_id])
380
381         entries = [
382             self.url_result('vevo:%s' % src, VevoIE.ie_key())
383             for src in playlist['isrcs']]
384
385         return self.playlist_result(
386             entries, playlist.get('playlistId'),
387             playlist.get('name'), playlist.get('description'))