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