[vevo] extract metadata and formats from api if videoinfo is empty
[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 compat_etree_fromstring
7 from ..utils import (
8     ExtractorError,
9     int_or_none,
10     sanitized_Request,
11     parse_iso8601,
12 )
13
14
15 class VevoIE(InfoExtractor):
16     '''
17     Accepts urls from vevo.com or in the format 'vevo:{id}'
18     (currently used by MTVIE and MySpaceIE)
19     '''
20     _VALID_URL = r'''(?x)
21         (?:https?://www\.vevo\.com/watch/(?:[^/]+/(?:[^/]+/)?)?|
22            https?://cache\.vevo\.com/m/html/embed\.html\?video=|
23            https?://videoplayer\.vevo\.com/embed/embedded\?videoId=|
24            vevo:)
25         (?P<id>[^&?#]+)'''
26
27     _TESTS = [{
28         'url': 'http://www.vevo.com/watch/hurts/somebody-to-die-for/GB1101300280',
29         'md5': '2dbc7e9fd4f1c60436c9aa73a5406193',
30         'info_dict': {
31             'id': 'Pt1kc_FniKM',
32             'ext': 'mp4',
33             'title': 'Hurts - Somebody to Die For',
34             'description': 'md5:13e925b89af6b01c7e417332bd23c4bf',
35             'uploader_id': 'HurtsVEVO',
36             'uploader': 'HurtsVEVO',
37             'upload_date': '20130624',
38             'duration': 230,
39         },
40         'add_ie': ['Youtube'],
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': '13d5204f520af905eeffa675040b8e76',
45         'info_dict': {
46             'id': 'ByGmQn1uxJw',
47             'ext': 'mp4',
48             'title': 'Cassadee Pope - I Wish I Could Break Your Heart',
49             'description': 'md5:5e9721c92ef117a6f69d00e9b42ceba7',
50             'uploader_id': 'CassadeeVEVO',
51             'uploader': 'CassadeeVEVO',
52             'upload_date': '20140219',
53             'duration': 226,
54             'age_limit': 0,
55         },
56         'add_ie': ['Youtube'],
57     }, {
58         'note': 'Age-limited video',
59         'url': 'https://www.vevo.com/watch/justin-timberlake/tunnel-vision-explicit/USRV81300282',
60         'info_dict': {
61             'id': '07FYdnEawAQ',
62             'ext': 'mp4',
63             'age_limit': 18,
64             'title': 'Justin Timberlake - Tunnel Vision (Explicit)',
65             'description': 'md5:64249768eec3bc4276236606ea996373',
66             'uploader_id': 'justintimberlakeVEVO',
67             'uploader': 'justintimberlakeVEVO',
68             'upload_date': '20130703',
69             'duration': 419,
70         },
71         'params': {
72             'skip_download': 'true',
73         },
74         'add_ie': ['Youtube'],
75     }, {
76         'note': 'No video_info',
77         'url': 'http://www.vevo.com/watch/k-camp-1/Till-I-Die/USUV71503000',
78         'md5': 'a8b84d1d1957cd01046441b701b270fb',
79         'info_dict': {
80             'id': 'Lad2jHtJCqY',
81             'ext': 'mp4',
82             'title': 'K Camp - Till I Die ft. T.I.',
83             'description': 'md5:0694920ededdee4a14cfc39695cc8ec3',
84             'uploader_id': 'KCampVEVO',
85             'uploader': 'KCampVEVO',
86             'upload_date': '20151207',
87             'duration': 193,
88         },
89         'add_ie': ['Youtube'],
90     }]
91     _SMIL_BASE_URL = 'http://smil.lvl3.vevo.com'
92     _SOURCE_TYPES = {
93         0: 'youtube',
94         1: 'brightcove',
95         2: 'http',
96         3: 'hls_ios',
97         4: 'hls',
98         5: 'smil',  # http
99         7: 'f4m_cc',
100         8: 'f4m_ak',
101         9: 'f4m_l3',
102         10: 'ism',
103         13: 'smil',  # rtmp
104         18: 'dash',
105     }
106     _VERSIONS = {
107         0: 'youtube',  # only in AuthenticateVideo videoVersions
108         1: 'level3',
109         2: 'akamai',
110         3: 'level3',
111         4: 'amazon',
112     }
113
114     def _parse_smil_formats(self, smil, smil_url, video_id, namespace=None, f4m_params=None, transform_rtmp_url=None):
115         formats = []
116         els = smil.findall('.//{http://www.w3.org/2001/SMIL20/Language}video')
117         for el in els:
118             src = el.attrib['src']
119             m = re.match(r'''(?xi)
120                 (?P<ext>[a-z0-9]+):
121                 (?P<path>
122                     [/a-z0-9]+     # The directory and main part of the URL
123                     _(?P<tbr>[0-9]+)k
124                     _(?P<width>[0-9]+)x(?P<height>[0-9]+)
125                     _(?P<vcodec>[a-z0-9]+)
126                     _(?P<vbr>[0-9]+)
127                     _(?P<acodec>[a-z0-9]+)
128                     _(?P<abr>[0-9]+)
129                     \.[a-z0-9]+  # File extension
130                 )''', src)
131             if not m:
132                 continue
133
134             format_url = self._SMIL_BASE_URL + m.group('path')
135             formats.append({
136                 'url': format_url,
137                 'format_id': 'smil_' + m.group('tbr'),
138                 'vcodec': m.group('vcodec'),
139                 'acodec': m.group('acodec'),
140                 'tbr': int(m.group('tbr')),
141                 'vbr': int(m.group('vbr')),
142                 'abr': int(m.group('abr')),
143                 'ext': m.group('ext'),
144                 'width': int(m.group('width')),
145                 'height': int(m.group('height')),
146             })
147         return formats
148
149     def _initialize_api(self, video_url, video_id):
150         req = sanitized_Request(
151             'http://www.vevo.com/auth', data=b'')
152         webpage = self._download_webpage(
153             req, None,
154             note='Retrieving oauth token',
155             errnote='Unable to retrieve oauth token')
156
157         if 'THIS PAGE IS CURRENTLY UNAVAILABLE IN YOUR REGION' in webpage:
158             raise ExtractorError('%s said: This page is currently unavailable in your region.' % self.IE_NAME, expected=True)
159
160         auth_info = self._parse_json(webpage, video_id)
161         self._api_url_template = self.http_scheme() + '//apiv2.vevo.com/%s?token=' + auth_info['access_token']
162
163     def _call_api(self, path, video_id, note, errnote, fatal=True):
164         return self._download_json(self._api_url_template % path, video_id, note, errnote)
165
166     def _real_extract(self, url):
167         video_id = self._match_id(url)
168
169         json_url = 'http://videoplayer.vevo.com/VideoService/AuthenticateVideo?isrc=%s' % video_id
170         response = self._download_json(json_url, video_id, 'Downloading video info', 'Unable to download info')
171         video_info = response.get('video') or {}
172         video_versions = video_info.get('videoVersions')
173         uploader = None
174         timestamp = None
175         view_count = None
176         formats = []
177
178         if not video_info:
179             ytid = response.get('errorInfo', {}).get('ytid')
180             if ytid:
181                 return self.url_result(ytid, 'Youtube', ytid)
182
183             if response.get('statusCode') != 909:
184                 if 'statusMessage' in response:
185                     raise ExtractorError('%s said: %s' % (
186                         self.IE_NAME, response['statusMessage']), expected=True)
187                 raise ExtractorError('Unable to extract videos')
188
189             if url.startswith('vevo:'):
190                 raise ExtractorError(
191                     'Please specify full Vevo URL for downloading', expected=True)
192
193             self._initialize_api(url, video_id)
194             video_info = self._call_api(
195                 'video/%s' % video_id, video_id, 'Downloading api video info',
196                 'Failed to download video info')
197
198             ytid = video_info.get('youTubeId')
199             if ytid:
200                 return self.url_result(
201                     ytid, 'Youtube', ytid)
202
203             video_versions = self._call_api(
204                 'video/%s/streams' % video_id, video_id,
205                 'Downloading video versions info',
206                 'Failed to download video versions info')
207
208             timestamp = parse_iso8601(video_info.get('releaseDate'))
209             artists = video_info.get('artists')
210             if artists:
211                 uploader = artists[0]['name']
212             view_count = int_or_none(video_info.get('views', {}).get('total'))
213
214             for video_version in video_versions:
215                 version = self._VERSIONS.get(video_version['version'])
216                 version_url = video_version.get('url')
217                 if not version_url:
218                         continue
219
220                 if '.mpd' in version_url or '.ism' in version_url:
221                     continue
222                 elif '.m3u8' in version_url:
223                     formats.extend(self._extract_m3u8_formats(
224                         version_url, video_id, 'mp4', 'm3u8_native',
225                         m3u8_id='hls-%s' % version,
226                         note='Downloading %s m3u8 information' % version,
227                         errnote='Failed to download %s m3u8 information' % version,
228                         fatal=False))
229                 else:
230                     m = re.search(r'''(?xi)
231                         _(?P<width>[0-9]+)x(?P<height>[0-9]+)
232                         _(?P<vcodec>[a-z0-9]+)
233                         _(?P<vbr>[0-9]+)
234                         _(?P<acodec>[a-z0-9]+)
235                         _(?P<abr>[0-9]+)
236                         \.(?P<ext>[a-z0-9]+)''', version_url)
237                     if not m:
238                         continue
239
240                     formats.append({
241                         'url': version_url,
242                         'format_id': 'http-%s-%s' % (version, video_version['quality']),
243                         'vcodec': m.group('vcodec'),
244                         'acodec': m.group('acodec'),
245                         'vbr': int(m.group('vbr')),
246                         'abr': int(m.group('abr')),
247                         'ext': m.group('ext'),
248                         'width': int(m.group('width')),
249                         'height': int(m.group('height')),
250                     })
251         else:
252             timestamp = int_or_none(self._search_regex(
253                 r'/Date\((\d+)\)/',
254                 video_info['releaseDate'], 'release date', fatal=False),
255                 scale=1000)
256             artists = video_info.get('mainArtists')
257             if artists:
258                 uploader = artists[0]['artistName']
259
260             smil_parsed = False
261             for video_version in video_info['videoVersions']:
262                 version = self._VERSIONS.get(video_version['version'])
263                 if version == 'youtube':
264                     return self.url_result(
265                         video_version['id'], 'Youtube', video_version['id'])
266                 else:
267                     source_type = self._SOURCE_TYPES.get(video_version['sourceType'])
268                     renditions = compat_etree_fromstring(video_version['data'])
269                     if source_type == 'http':
270                         for rend in renditions.findall('rendition'):
271                             attr = rend.attrib
272                             formats.append({
273                                 'url': attr['url'],
274                                 'format_id': 'http-%s-%s' % (version, attr['name']),
275                                 'height': int_or_none(attr.get('frameheight')),
276                                 'width': int_or_none(attr.get('frameWidth')),
277                                 'tbr': int_or_none(attr.get('totalBitrate')),
278                                 'vbr': int_or_none(attr.get('videoBitrate')),
279                                 'abr': int_or_none(attr.get('audioBitrate')),
280                                 'vcodec': attr.get('videoCodec'),
281                                 'acodec': attr.get('audioCodec'),
282                             })
283                     elif source_type == 'hls':
284                         formats.extend(self._extract_m3u8_formats(
285                             renditions.find('rendition').attrib['url'], video_id,
286                             'mp4', 'm3u8_native', m3u8_id='hls-%s' % version,
287                             note='Downloading %s m3u8 information' % version,
288                             errnote='Failed to download %s m3u8 information' % version,
289                             fatal=False))
290                     elif source_type == 'smil' and not smil_parsed:
291                         formats.extend(self._extract_smil_formats(
292                             renditions.find('rendition').attrib['url'], video_id, False))
293                         smil_parsed = True
294         self._sort_formats(formats)
295
296         title = video_info['title']
297
298         is_explicit = video_info.get('isExplicit')
299         if is_explicit is True:
300             age_limit = 18
301         elif is_explicit is False:
302             age_limit = 0
303         else:
304             age_limit = None
305
306         duration = video_info.get('duration')
307
308         return {
309             'id': video_id,
310             'title': title,
311             'formats': formats,
312             'thumbnail': video_info.get('imageUrl') or video_info.get('thumbnailUrl'),
313             'timestamp': timestamp,
314             'uploader': uploader,
315             'duration': duration,
316             'view_count': view_count,
317             'age_limit': age_limit,
318         }