[vevo] extract all formats and bypass geo restriction
[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 )
11
12
13 class VevoIE(InfoExtractor):
14     '''
15     Accepts urls from vevo.com or in the format 'vevo:{id}'
16     (currently used by MTVIE and MySpaceIE)
17     '''
18     _VALID_URL = r'''(?x)
19         (?:https?://www\.vevo\.com/watch/(?:[^/]+/(?:[^/]+/)?)?|
20            https?://cache\.vevo\.com/m/html/embed\.html\?video=|
21            https?://videoplayer\.vevo\.com/embed/embedded\?videoId=|
22            vevo:)
23         (?P<id>[^&?#]+)'''
24
25     _TESTS = [{
26         'url': 'http://www.vevo.com/watch/hurts/somebody-to-die-for/GB1101300280',
27         'md5': '2dbc7e9fd4f1c60436c9aa73a5406193',
28         'info_dict': {
29             'id': 'Pt1kc_FniKM',
30             'ext': 'mp4',
31             'title': 'Hurts - Somebody to Die For',
32             'description': 'md5:13e925b89af6b01c7e417332bd23c4bf',
33             'uploader_id': 'HurtsVEVO',
34             'uploader': 'HurtsVEVO',
35             'upload_date': '20130624',
36             'duration': 230,
37         },
38         'add_ie': ['Youtube'],
39     }, {
40         'note': 'v3 SMIL format',
41         'url': 'http://www.vevo.com/watch/cassadee-pope/i-wish-i-could-break-your-heart/USUV71302923',
42         'md5': '13d5204f520af905eeffa675040b8e76',
43         'info_dict': {
44             'id': 'ByGmQn1uxJw',
45             'ext': 'mp4',
46             'title': 'Cassadee Pope - I Wish I Could Break Your Heart',
47             'description': 'md5:5e9721c92ef117a6f69d00e9b42ceba7',
48             'uploader_id': 'CassadeeVEVO',
49             'uploader': 'CassadeeVEVO',
50             'upload_date': '20140219',
51             'duration': 226,
52             'age_limit': 0,
53         },
54         'add_ie': ['Youtube'],
55     }, {
56         'note': 'Age-limited video',
57         'url': 'https://www.vevo.com/watch/justin-timberlake/tunnel-vision-explicit/USRV81300282',
58         'info_dict': {
59             'id': '07FYdnEawAQ',
60             'ext': 'mp4',
61             'age_limit': 18,
62             'title': 'Justin Timberlake - Tunnel Vision (Explicit)',
63             'description': 'md5:64249768eec3bc4276236606ea996373',
64             'uploader_id': 'justintimberlakeVEVO',
65             'uploader': 'justintimberlakeVEVO',
66             'upload_date': '20130703',
67         },
68         'params': {
69             'skip_download': 'true',
70         },
71         'add_ie': ['Youtube'],
72     }, {
73         'note': 'No video_info',
74         'url': 'http://www.vevo.com/watch/k-camp-1/Till-I-Die/USUV71503000',
75         'md5': '8b83cc492d72fc9cf74a02acee7dc1b0',
76         'info_dict': {
77             'id': 'USUV71503000',
78             'ext': 'mp4',
79             'title': 'Till I Die - K Camp ft. T.I.',
80             'duration': 193,
81         },
82     }]
83     _SMIL_BASE_URL = 'http://smil.lvl3.vevo.com'
84     _SOURCE_TYPES = {
85         0: 'youtube',
86         1: 'brightcove',
87         2: 'http',
88         3: 'hls_ios',
89         4: 'hls',
90         5: 'smil',  # http
91         7: 'f4m_cc',
92         8: 'f4m_ak',
93         9: 'f4m_l3',
94         10: 'ism',
95         13: 'smil',  # rtmp
96         18: 'dash',
97     }
98     _VERSIONS = {
99         0: 'youtube',
100         1: 'level3',
101         2: 'akamai',
102         3: 'level3',
103         4: 'amazon',
104     }
105
106     def _parse_smil_formats(self, smil, smil_url, video_id, namespace=None, f4m_params=None, transform_rtmp_url=None):
107         formats = []
108         els = smil.findall('.//{http://www.w3.org/2001/SMIL20/Language}video')
109         for el in els:
110             src = el.attrib['src']
111             m = re.match(r'''(?xi)
112                 (?P<ext>[a-z0-9]+):
113                 (?P<path>
114                     [/a-z0-9]+     # The directory and main part of the URL
115                     _(?P<tbr>[0-9]+)k
116                     _(?P<width>[0-9]+)x(?P<height>[0-9]+)
117                     _(?P<vcodec>[a-z0-9]+)
118                     _(?P<vbr>[0-9]+)
119                     _(?P<acodec>[a-z0-9]+)
120                     _(?P<abr>[0-9]+)
121                     \.[a-z0-9]+  # File extension
122                 )''', src)
123             if not m:
124                 continue
125
126             format_url = self._SMIL_BASE_URL + m.group('path')
127             formats.append({
128                 'url': format_url,
129                 'format_id': 'smil_' + m.group('tbr'),
130                 'vcodec': m.group('vcodec'),
131                 'acodec': m.group('acodec'),
132                 'tbr': int(m.group('tbr')),
133                 'vbr': int(m.group('vbr')),
134                 'abr': int(m.group('abr')),
135                 'ext': m.group('ext'),
136                 'width': int(m.group('width')),
137                 'height': int(m.group('height')),
138             })
139         return formats
140
141     def _real_extract(self, url):
142         video_id = self._match_id(url)
143
144         webpage = None
145
146         json_url = 'http://videoplayer.vevo.com/VideoService/AuthenticateVideo?isrc=%s' % video_id
147         response = self._download_json(json_url, video_id)
148         video_info = response['video'] or {}
149
150         if not video_info:
151             ytid = response.get('errorInfo', {}).get('ytid')
152             if ytid:
153                 return self.url_result(ytid, 'Youtube', ytid)
154
155             if response.get('statusCode') != 909:
156                 if 'statusMessage' in response:
157                     raise ExtractorError('%s said: %s' % (
158                         self.IE_NAME, response['statusMessage']), expected=True)
159                 raise ExtractorError('Unable to extract videos')
160
161             if url.startswith('vevo:'):
162                 raise ExtractorError(
163                     'Please specify full Vevo URL for downloading', expected=True)
164             webpage = self._download_webpage(url, video_id)
165
166         title = video_info.get('title') or self._og_search_title(webpage)
167
168         smil_parsed = False
169         formats = []
170         for video_version in video_info['videoVersions']:
171             version = self._VERSIONS.get(video_version['version'])
172             if version == 'youtube':
173                 return self.url_result(
174                     video_version['id'], 'Youtube', video_version['id'])
175             else:
176                 source_type = self._SOURCE_TYPES.get(video_version['sourceType'])
177                 renditions = compat_etree_fromstring(video_version['data'])
178                 if source_type == 'http':
179                     for rend in renditions.findall('rendition'):
180                         attr = rend.attrib
181                         formats.append({
182                             'url': attr['url'],
183                             'format_id': '%s-%s' % (version, attr['name']),
184                             'height': int_or_none(attr.get('frameheight')),
185                             'width': int_or_none(attr.get('frameWidth')),
186                             'tbr': int_or_none(attr.get('totalBitrate')),
187                             'vbr': int_or_none(attr.get('videoBitrate')),
188                             'abr': int_or_none(attr.get('audioBitrate')),
189                             'vcodec': attr.get('videoCodec'),
190                             'acodec': attr.get('audioCodec'),
191                         })
192                 elif source_type == 'hls':
193                     formats.extend(self._extract_m3u8_formats(
194                         renditions.find('rendition').attrib['url'], video_id,
195                         'mp4', 'm3u8_native', m3u8_id='hls-%s' % version, fatal=False))
196                 elif source_type == 'smil' and not smil_parsed:
197                     formats.extend(self._extract_smil_formats(
198                         renditions.find('rendition').attrib['url'], video_id, False))
199                     smil_parsed = True
200         self._sort_formats(formats)
201
202         is_explicit = video_info.get('isExplicit')
203         if is_explicit is True:
204             age_limit = 18
205         elif is_explicit is False:
206             age_limit = 0
207         else:
208             age_limit = None
209
210         timestamp = int_or_none(self._search_regex(
211             r'/Date\((\d+)\)/',
212             video_info['launchDate'], 'launch date', fatal=False),
213             scale=1000) if video_info else None
214
215         duration = video_info.get('duration') or int_or_none(
216             self._html_search_meta('video:duration', webpage))
217
218         return {
219             'id': video_id,
220             'title': title,
221             'formats': formats,
222             'thumbnail': video_info.get('imageUrl'),
223             'timestamp': timestamp,
224             'uploader': video_info['mainArtists'][0]['artistName'] if video_info else None,
225             'duration': duration,
226             'age_limit': age_limit,
227         }