[vevo] Extract featured artist
[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         artist = None
205         featured_artist = None
206         uploader = None
207         view_count = None
208         timestamp = None
209         formats = []
210
211         if not video_info:
212             if response.get('statusCode') != 909:
213                 ytid = response.get('errorInfo', {}).get('ytid')
214                 if ytid:
215                     self.report_warning(
216                         'Video is geoblocked, trying with the YouTube video %s' % ytid)
217                     return self.url_result(ytid, 'Youtube', ytid)
218
219                 if 'statusMessage' in response:
220                     raise ExtractorError('%s said: %s' % (
221                         self.IE_NAME, response['statusMessage']), expected=True)
222                 raise ExtractorError('Unable to extract videos')
223
224             self._initialize_api(video_id)
225             video_info = self._call_api(
226                 'video/%s' % video_id, video_id, 'Downloading api video info',
227                 'Failed to download video info')
228
229             video_versions = self._call_api(
230                 'video/%s/streams' % video_id, video_id,
231                 'Downloading video versions info',
232                 'Failed to download video versions info',
233                 fatal=False)
234
235             # Some videos are only available via webpage (e.g.
236             # https://github.com/rg3/youtube-dl/issues/9366)
237             if not video_versions:
238                 webpage = self._download_webpage(url, video_id)
239                 video_versions = self._extract_json(webpage, video_id, 'streams')[video_id][0]
240
241             timestamp = parse_iso8601(video_info.get('releaseDate'))
242             artists = video_info.get('artists')
243             if artists:
244                 artist = uploader = artists[0]['name']
245             view_count = int_or_none(video_info.get('views', {}).get('total'))
246
247             for video_version in video_versions:
248                 version = self._VERSIONS.get(video_version['version'])
249                 version_url = video_version.get('url')
250                 if not version_url:
251                     continue
252
253                 if '.ism' in version_url:
254                     continue
255                 elif '.mpd' in version_url:
256                     formats.extend(self._extract_mpd_formats(
257                         version_url, video_id, mpd_id='dash-%s' % version,
258                         note='Downloading %s MPD information' % version,
259                         errnote='Failed to download %s MPD information' % version,
260                         fatal=False))
261                 elif '.m3u8' in version_url:
262                     formats.extend(self._extract_m3u8_formats(
263                         version_url, video_id, 'mp4', 'm3u8_native',
264                         m3u8_id='hls-%s' % version,
265                         note='Downloading %s m3u8 information' % version,
266                         errnote='Failed to download %s m3u8 information' % version,
267                         fatal=False))
268                 else:
269                     m = re.search(r'''(?xi)
270                         _(?P<width>[0-9]+)x(?P<height>[0-9]+)
271                         _(?P<vcodec>[a-z0-9]+)
272                         _(?P<vbr>[0-9]+)
273                         _(?P<acodec>[a-z0-9]+)
274                         _(?P<abr>[0-9]+)
275                         \.(?P<ext>[a-z0-9]+)''', version_url)
276                     if not m:
277                         continue
278
279                     formats.append({
280                         'url': version_url,
281                         'format_id': 'http-%s-%s' % (version, video_version['quality']),
282                         'vcodec': m.group('vcodec'),
283                         'acodec': m.group('acodec'),
284                         'vbr': int(m.group('vbr')),
285                         'abr': int(m.group('abr')),
286                         'ext': m.group('ext'),
287                         'width': int(m.group('width')),
288                         'height': int(m.group('height')),
289                     })
290         else:
291             timestamp = int_or_none(self._search_regex(
292                 r'/Date\((\d+)\)/',
293                 video_info['releaseDate'], 'release date', fatal=False),
294                 scale=1000)
295             artists = video_info.get('mainArtists')
296             if artists:
297                 artist = uploader = artists[0]['artistName']
298
299             featured_artists = video_info.get('featuredArtists')
300             if featured_artists:
301                 featured_artist = featured_artists[0]['artistName']
302
303             smil_parsed = False
304             for video_version in video_info['videoVersions']:
305                 version = self._VERSIONS.get(video_version['version'])
306                 if version == 'youtube':
307                     continue
308                 else:
309                     source_type = self._SOURCE_TYPES.get(video_version['sourceType'])
310                     renditions = compat_etree_fromstring(video_version['data'])
311                     if source_type == 'http':
312                         for rend in renditions.findall('rendition'):
313                             attr = rend.attrib
314                             formats.append({
315                                 'url': attr['url'],
316                                 'format_id': 'http-%s-%s' % (version, attr['name']),
317                                 'height': int_or_none(attr.get('frameheight')),
318                                 'width': int_or_none(attr.get('frameWidth')),
319                                 'tbr': int_or_none(attr.get('totalBitrate')),
320                                 'vbr': int_or_none(attr.get('videoBitrate')),
321                                 'abr': int_or_none(attr.get('audioBitrate')),
322                                 'vcodec': attr.get('videoCodec'),
323                                 'acodec': attr.get('audioCodec'),
324                             })
325                     elif source_type == 'hls':
326                         formats.extend(self._extract_m3u8_formats(
327                             renditions.find('rendition').attrib['url'], video_id,
328                             'mp4', 'm3u8_native', m3u8_id='hls-%s' % version,
329                             note='Downloading %s m3u8 information' % version,
330                             errnote='Failed to download %s m3u8 information' % version,
331                             fatal=False))
332                     elif source_type == 'smil' and version == 'level3' and not smil_parsed:
333                         formats.extend(self._extract_smil_formats(
334                             renditions.find('rendition').attrib['url'], video_id, False))
335                         smil_parsed = True
336         self._sort_formats(formats)
337
338         track = video_info['title']
339         if featured_artist:
340             artist = '%s ft. %s' % (artist, featured_artist)
341         title = '%s - %s' % (artist, track) if artist else track
342         genre = video_info.get('genres', [None])[0]
343
344         is_explicit = video_info.get('isExplicit')
345         if is_explicit is True:
346             age_limit = 18
347         elif is_explicit is False:
348             age_limit = 0
349         else:
350             age_limit = None
351
352         duration = video_info.get('duration')
353
354         return {
355             'id': video_id,
356             'title': title,
357             'formats': formats,
358             'thumbnail': video_info.get('imageUrl') or video_info.get('thumbnailUrl'),
359             'timestamp': timestamp,
360             'uploader': uploader,
361             'duration': duration,
362             'view_count': view_count,
363             'age_limit': age_limit,
364             'track': track,
365             'artist': uploader,
366             'genre': genre,
367         }
368
369
370 class VevoPlaylistIE(VevoBaseIE):
371     _VALID_URL = r'https?://www\.vevo\.com/watch/(?P<kind>playlist|genre)/(?P<id>[^/?#&]+)'
372
373     _TESTS = [{
374         'url': 'http://www.vevo.com/watch/playlist/dadbf4e7-b99f-4184-9670-6f0e547b6a29',
375         'info_dict': {
376             'id': 'dadbf4e7-b99f-4184-9670-6f0e547b6a29',
377             'title': 'Best-Of: Birdman',
378         },
379         'playlist_count': 10,
380     }, {
381         'url': 'http://www.vevo.com/watch/genre/rock',
382         'info_dict': {
383             'id': 'rock',
384             'title': 'Rock',
385         },
386         'playlist_count': 20,
387     }, {
388         'url': 'http://www.vevo.com/watch/playlist/dadbf4e7-b99f-4184-9670-6f0e547b6a29?index=0',
389         'md5': '32dcdfddddf9ec6917fc88ca26d36282',
390         'info_dict': {
391             'id': 'USCMV1100073',
392             'ext': 'mp4',
393             'title': 'Birdman - Y.U. MAD',
394             'timestamp': 1323417600,
395             'upload_date': '20111209',
396             'uploader': 'Birdman',
397             'track': 'Y.U. MAD',
398             'artist': 'Birdman',
399             'genre': 'Rap/Hip-Hop',
400         },
401         'expected_warnings': ['Unable to download SMIL file'],
402     }, {
403         'url': 'http://www.vevo.com/watch/genre/rock?index=0',
404         'only_matching': True,
405     }]
406
407     def _real_extract(self, url):
408         mobj = re.match(self._VALID_URL, url)
409         playlist_id = mobj.group('id')
410         playlist_kind = mobj.group('kind')
411
412         webpage = self._download_webpage(url, playlist_id)
413
414         qs = compat_urlparse.parse_qs(compat_urlparse.urlparse(url).query)
415         index = qs.get('index', [None])[0]
416
417         if index:
418             video_id = self._search_regex(
419                 r'<meta[^>]+content=(["\'])vevo://video/(?P<id>.+?)\1[^>]*>',
420                 webpage, 'video id', default=None, group='id')
421             if video_id:
422                 return self.url_result('vevo:%s' % video_id, VevoIE.ie_key())
423
424         playlists = self._extract_json(webpage, playlist_id, '%ss' % playlist_kind)
425
426         playlist = (list(playlists.values())[0]
427                     if playlist_kind == 'playlist' else playlists[playlist_id])
428
429         entries = [
430             self.url_result('vevo:%s' % src, VevoIE.ie_key())
431             for src in playlist['isrcs']]
432
433         return self.playlist_result(
434             entries, playlist.get('playlistId') or playlist_id,
435             playlist.get('name'), playlist.get('description'))