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