Start moving to ytdl-org
[youtube-dl] / youtube_dl / extractor / vimeo.py
1 # coding: utf-8
2 from __future__ import unicode_literals
3
4 import base64
5 import json
6 import re
7 import itertools
8
9 from .common import InfoExtractor
10 from ..compat import (
11     compat_HTTPError,
12     compat_str,
13     compat_urlparse,
14 )
15 from ..utils import (
16     determine_ext,
17     ExtractorError,
18     js_to_json,
19     InAdvancePagedList,
20     int_or_none,
21     merge_dicts,
22     NO_DEFAULT,
23     parse_filesize,
24     qualities,
25     RegexNotFoundError,
26     sanitized_Request,
27     smuggle_url,
28     std_headers,
29     try_get,
30     unified_timestamp,
31     unsmuggle_url,
32     urlencode_postdata,
33     unescapeHTML,
34 )
35
36
37 class VimeoBaseInfoExtractor(InfoExtractor):
38     _NETRC_MACHINE = 'vimeo'
39     _LOGIN_REQUIRED = False
40     _LOGIN_URL = 'https://vimeo.com/log_in'
41
42     def _login(self):
43         username, password = self._get_login_info()
44         if username is None:
45             if self._LOGIN_REQUIRED:
46                 raise ExtractorError('No login info available, needed for using %s.' % self.IE_NAME, expected=True)
47             return
48         webpage = self._download_webpage(
49             self._LOGIN_URL, None, 'Downloading login page')
50         token, vuid = self._extract_xsrft_and_vuid(webpage)
51         data = {
52             'action': 'login',
53             'email': username,
54             'password': password,
55             'service': 'vimeo',
56             'token': token,
57         }
58         self._set_vimeo_cookie('vuid', vuid)
59         try:
60             self._download_webpage(
61                 self._LOGIN_URL, None, 'Logging in',
62                 data=urlencode_postdata(data), headers={
63                     'Content-Type': 'application/x-www-form-urlencoded',
64                     'Referer': self._LOGIN_URL,
65                 })
66         except ExtractorError as e:
67             if isinstance(e.cause, compat_HTTPError) and e.cause.code == 418:
68                 raise ExtractorError(
69                     'Unable to log in: bad username or password',
70                     expected=True)
71             raise ExtractorError('Unable to log in')
72
73     def _verify_video_password(self, url, video_id, webpage):
74         password = self._downloader.params.get('videopassword')
75         if password is None:
76             raise ExtractorError('This video is protected by a password, use the --video-password option', expected=True)
77         token, vuid = self._extract_xsrft_and_vuid(webpage)
78         data = urlencode_postdata({
79             'password': password,
80             'token': token,
81         })
82         if url.startswith('http://'):
83             # vimeo only supports https now, but the user can give an http url
84             url = url.replace('http://', 'https://')
85         password_request = sanitized_Request(url + '/password', data)
86         password_request.add_header('Content-Type', 'application/x-www-form-urlencoded')
87         password_request.add_header('Referer', url)
88         self._set_vimeo_cookie('vuid', vuid)
89         return self._download_webpage(
90             password_request, video_id,
91             'Verifying the password', 'Wrong password')
92
93     def _extract_xsrft_and_vuid(self, webpage):
94         xsrft = self._search_regex(
95             r'(?:(?P<q1>["\'])xsrft(?P=q1)\s*:|xsrft\s*[=:])\s*(?P<q>["\'])(?P<xsrft>.+?)(?P=q)',
96             webpage, 'login token', group='xsrft')
97         vuid = self._search_regex(
98             r'["\']vuid["\']\s*:\s*(["\'])(?P<vuid>.+?)\1',
99             webpage, 'vuid', group='vuid')
100         return xsrft, vuid
101
102     def _set_vimeo_cookie(self, name, value):
103         self._set_cookie('vimeo.com', name, value)
104
105     def _vimeo_sort_formats(self, formats):
106         # Bitrates are completely broken. Single m3u8 may contain entries in kbps and bps
107         # at the same time without actual units specified. This lead to wrong sorting.
108         self._sort_formats(formats, field_preference=('preference', 'height', 'width', 'fps', 'tbr', 'format_id'))
109
110     def _parse_config(self, config, video_id):
111         video_data = config['video']
112         # Extract title
113         video_title = video_data['title']
114
115         # Extract uploader, uploader_url and uploader_id
116         video_uploader = video_data.get('owner', {}).get('name')
117         video_uploader_url = video_data.get('owner', {}).get('url')
118         video_uploader_id = video_uploader_url.split('/')[-1] if video_uploader_url else None
119
120         # Extract video thumbnail
121         video_thumbnail = video_data.get('thumbnail')
122         if video_thumbnail is None:
123             video_thumbs = video_data.get('thumbs')
124             if video_thumbs and isinstance(video_thumbs, dict):
125                 _, video_thumbnail = sorted((int(width if width.isdigit() else 0), t_url) for (width, t_url) in video_thumbs.items())[-1]
126
127         # Extract video duration
128         video_duration = int_or_none(video_data.get('duration'))
129
130         formats = []
131         config_files = video_data.get('files') or config['request'].get('files', {})
132         for f in config_files.get('progressive', []):
133             video_url = f.get('url')
134             if not video_url:
135                 continue
136             formats.append({
137                 'url': video_url,
138                 'format_id': 'http-%s' % f.get('quality'),
139                 'width': int_or_none(f.get('width')),
140                 'height': int_or_none(f.get('height')),
141                 'fps': int_or_none(f.get('fps')),
142                 'tbr': int_or_none(f.get('bitrate')),
143             })
144
145         for files_type in ('hls', 'dash'):
146             for cdn_name, cdn_data in config_files.get(files_type, {}).get('cdns', {}).items():
147                 manifest_url = cdn_data.get('url')
148                 if not manifest_url:
149                     continue
150                 format_id = '%s-%s' % (files_type, cdn_name)
151                 if files_type == 'hls':
152                     formats.extend(self._extract_m3u8_formats(
153                         manifest_url, video_id, 'mp4',
154                         'm3u8_native', m3u8_id=format_id,
155                         note='Downloading %s m3u8 information' % cdn_name,
156                         fatal=False))
157                 elif files_type == 'dash':
158                     mpd_pattern = r'/%s/(?:sep/)?video/' % video_id
159                     mpd_manifest_urls = []
160                     if re.search(mpd_pattern, manifest_url):
161                         for suffix, repl in (('', 'video'), ('_sep', 'sep/video')):
162                             mpd_manifest_urls.append((format_id + suffix, re.sub(
163                                 mpd_pattern, '/%s/%s/' % (video_id, repl), manifest_url)))
164                     else:
165                         mpd_manifest_urls = [(format_id, manifest_url)]
166                     for f_id, m_url in mpd_manifest_urls:
167                         mpd_formats = self._extract_mpd_formats(
168                             m_url.replace('/master.json', '/master.mpd'), video_id, f_id,
169                             'Downloading %s MPD information' % cdn_name,
170                             fatal=False)
171                         for f in mpd_formats:
172                             if f.get('vcodec') == 'none':
173                                 f['preference'] = -50
174                             elif f.get('acodec') == 'none':
175                                 f['preference'] = -40
176                         formats.extend(mpd_formats)
177
178         subtitles = {}
179         text_tracks = config['request'].get('text_tracks')
180         if text_tracks:
181             for tt in text_tracks:
182                 subtitles[tt['lang']] = [{
183                     'ext': 'vtt',
184                     'url': 'https://vimeo.com' + tt['url'],
185                 }]
186
187         return {
188             'title': video_title,
189             'uploader': video_uploader,
190             'uploader_id': video_uploader_id,
191             'uploader_url': video_uploader_url,
192             'thumbnail': video_thumbnail,
193             'duration': video_duration,
194             'formats': formats,
195             'subtitles': subtitles,
196         }
197
198     def _extract_original_format(self, url, video_id):
199         download_data = self._download_json(
200             url, video_id, fatal=False,
201             query={'action': 'load_download_config'},
202             headers={'X-Requested-With': 'XMLHttpRequest'})
203         if download_data:
204             source_file = download_data.get('source_file')
205             if isinstance(source_file, dict):
206                 download_url = source_file.get('download_url')
207                 if download_url and not source_file.get('is_cold') and not source_file.get('is_defrosting'):
208                     source_name = source_file.get('public_name', 'Original')
209                     if self._is_valid_url(download_url, video_id, '%s video' % source_name):
210                         ext = (try_get(
211                             source_file, lambda x: x['extension'],
212                             compat_str) or determine_ext(
213                             download_url, None) or 'mp4').lower()
214                         return {
215                             'url': download_url,
216                             'ext': ext,
217                             'width': int_or_none(source_file.get('width')),
218                             'height': int_or_none(source_file.get('height')),
219                             'filesize': parse_filesize(source_file.get('size')),
220                             'format_id': source_name,
221                             'preference': 1,
222                         }
223
224
225 class VimeoIE(VimeoBaseInfoExtractor):
226     """Information extractor for vimeo.com."""
227
228     # _VALID_URL matches Vimeo URLs
229     _VALID_URL = r'''(?x)
230                     https?://
231                         (?:
232                             (?:
233                                 www|
234                                 (?P<player>player)
235                             )
236                             \.
237                         )?
238                         vimeo(?P<pro>pro)?\.com/
239                         (?!(?:channels|album)/[^/?#]+/?(?:$|[?#])|[^/]+/review/|ondemand/)
240                         (?:.*?/)?
241                         (?:
242                             (?:
243                                 play_redirect_hls|
244                                 moogaloop\.swf)\?clip_id=
245                             )?
246                         (?:videos?/)?
247                         (?P<id>[0-9]+)
248                         (?:/[\da-f]+)?
249                         /?(?:[?&].*)?(?:[#].*)?$
250                     '''
251     IE_NAME = 'vimeo'
252     _TESTS = [
253         {
254             'url': 'http://vimeo.com/56015672#at=0',
255             'md5': '8879b6cc097e987f02484baf890129e5',
256             'info_dict': {
257                 'id': '56015672',
258                 'ext': 'mp4',
259                 'title': "youtube-dl test video - \u2605 \" ' \u5e78 / \\ \u00e4 \u21ad \U0001d550",
260                 'description': 'md5:509a9ad5c9bf97c60faee9203aca4479',
261                 'timestamp': 1355990239,
262                 'upload_date': '20121220',
263                 'uploader_url': r're:https?://(?:www\.)?vimeo\.com/user7108434',
264                 'uploader_id': 'user7108434',
265                 'uploader': 'Filippo Valsorda',
266                 'duration': 10,
267                 'license': 'by-sa',
268             },
269         },
270         {
271             'url': 'http://vimeopro.com/openstreetmapus/state-of-the-map-us-2013/video/68093876',
272             'md5': '3b5ca6aa22b60dfeeadf50b72e44ed82',
273             'note': 'Vimeo Pro video (#1197)',
274             'info_dict': {
275                 'id': '68093876',
276                 'ext': 'mp4',
277                 'uploader_url': r're:https?://(?:www\.)?vimeo\.com/openstreetmapus',
278                 'uploader_id': 'openstreetmapus',
279                 'uploader': 'OpenStreetMap US',
280                 'title': 'Andy Allan - Putting the Carto into OpenStreetMap Cartography',
281                 'description': 'md5:fd69a7b8d8c34a4e1d2ec2e4afd6ec30',
282                 'duration': 1595,
283             },
284         },
285         {
286             'url': 'http://player.vimeo.com/video/54469442',
287             'md5': '619b811a4417aa4abe78dc653becf511',
288             'note': 'Videos that embed the url in the player page',
289             'info_dict': {
290                 'id': '54469442',
291                 'ext': 'mp4',
292                 'title': 'Kathy Sierra: Building the minimum Badass User, Business of Software 2012',
293                 'uploader': 'The BLN & Business of Software',
294                 'uploader_url': r're:https?://(?:www\.)?vimeo\.com/theblnbusinessofsoftware',
295                 'uploader_id': 'theblnbusinessofsoftware',
296                 'duration': 3610,
297                 'description': None,
298             },
299         },
300         {
301             'url': 'http://vimeo.com/68375962',
302             'md5': 'aaf896bdb7ddd6476df50007a0ac0ae7',
303             'note': 'Video protected with password',
304             'info_dict': {
305                 'id': '68375962',
306                 'ext': 'mp4',
307                 'title': 'youtube-dl password protected test video',
308                 'timestamp': 1371200155,
309                 'upload_date': '20130614',
310                 'uploader_url': r're:https?://(?:www\.)?vimeo\.com/user18948128',
311                 'uploader_id': 'user18948128',
312                 'uploader': 'Jaime Marquínez Ferrándiz',
313                 'duration': 10,
314                 'description': 'md5:dca3ea23adb29ee387127bc4ddfce63f',
315             },
316             'params': {
317                 'videopassword': 'youtube-dl',
318             },
319         },
320         {
321             'url': 'http://vimeo.com/channels/keypeele/75629013',
322             'md5': '2f86a05afe9d7abc0b9126d229bbe15d',
323             'info_dict': {
324                 'id': '75629013',
325                 'ext': 'mp4',
326                 'title': 'Key & Peele: Terrorist Interrogation',
327                 'description': 'md5:8678b246399b070816b12313e8b4eb5c',
328                 'uploader_url': r're:https?://(?:www\.)?vimeo\.com/atencio',
329                 'uploader_id': 'atencio',
330                 'uploader': 'Peter Atencio',
331                 'channel_id': 'keypeele',
332                 'channel_url': r're:https?://(?:www\.)?vimeo\.com/channels/keypeele',
333                 'timestamp': 1380339469,
334                 'upload_date': '20130928',
335                 'duration': 187,
336             },
337             'expected_warnings': ['Unable to download JSON metadata'],
338         },
339         {
340             'url': 'http://vimeo.com/76979871',
341             'note': 'Video with subtitles',
342             'info_dict': {
343                 'id': '76979871',
344                 'ext': 'mp4',
345                 'title': 'The New Vimeo Player (You Know, For Videos)',
346                 'description': 'md5:2ec900bf97c3f389378a96aee11260ea',
347                 'timestamp': 1381846109,
348                 'upload_date': '20131015',
349                 'uploader_url': r're:https?://(?:www\.)?vimeo\.com/staff',
350                 'uploader_id': 'staff',
351                 'uploader': 'Vimeo Staff',
352                 'duration': 62,
353             }
354         },
355         {
356             # from https://www.ouya.tv/game/Pier-Solar-and-the-Great-Architects/
357             'url': 'https://player.vimeo.com/video/98044508',
358             'note': 'The js code contains assignments to the same variable as the config',
359             'info_dict': {
360                 'id': '98044508',
361                 'ext': 'mp4',
362                 'title': 'Pier Solar OUYA Official Trailer',
363                 'uploader': 'Tulio Gonçalves',
364                 'uploader_url': r're:https?://(?:www\.)?vimeo\.com/user28849593',
365                 'uploader_id': 'user28849593',
366             },
367         },
368         {
369             # contains original format
370             'url': 'https://vimeo.com/33951933',
371             'md5': '53c688fa95a55bf4b7293d37a89c5c53',
372             'info_dict': {
373                 'id': '33951933',
374                 'ext': 'mp4',
375                 'title': 'FOX CLASSICS - Forever Classic ID - A Full Minute',
376                 'uploader': 'The DMCI',
377                 'uploader_url': r're:https?://(?:www\.)?vimeo\.com/dmci',
378                 'uploader_id': 'dmci',
379                 'timestamp': 1324343742,
380                 'upload_date': '20111220',
381                 'description': 'md5:ae23671e82d05415868f7ad1aec21147',
382             },
383         },
384         {
385             # only available via https://vimeo.com/channels/tributes/6213729 and
386             # not via https://vimeo.com/6213729
387             'url': 'https://vimeo.com/channels/tributes/6213729',
388             'info_dict': {
389                 'id': '6213729',
390                 'ext': 'mp4',
391                 'title': 'Vimeo Tribute: The Shining',
392                 'uploader': 'Casey Donahue',
393                 'uploader_url': r're:https?://(?:www\.)?vimeo\.com/caseydonahue',
394                 'uploader_id': 'caseydonahue',
395                 'channel_url': r're:https?://(?:www\.)?vimeo\.com/channels/tributes',
396                 'channel_id': 'tributes',
397                 'timestamp': 1250886430,
398                 'upload_date': '20090821',
399                 'description': 'md5:bdbf314014e58713e6e5b66eb252f4a6',
400             },
401             'params': {
402                 'skip_download': True,
403             },
404             'expected_warnings': ['Unable to download JSON metadata'],
405         },
406         {
407             # redirects to ondemand extractor and should be passed through it
408             # for successful extraction
409             'url': 'https://vimeo.com/73445910',
410             'info_dict': {
411                 'id': '73445910',
412                 'ext': 'mp4',
413                 'title': 'The Reluctant Revolutionary',
414                 'uploader': '10Ft Films',
415                 'uploader_url': r're:https?://(?:www\.)?vimeo\.com/tenfootfilms',
416                 'uploader_id': 'tenfootfilms',
417             },
418             'params': {
419                 'skip_download': True,
420             },
421         },
422         {
423             'url': 'http://player.vimeo.com/video/68375962',
424             'md5': 'aaf896bdb7ddd6476df50007a0ac0ae7',
425             'info_dict': {
426                 'id': '68375962',
427                 'ext': 'mp4',
428                 'title': 'youtube-dl password protected test video',
429                 'uploader_url': r're:https?://(?:www\.)?vimeo\.com/user18948128',
430                 'uploader_id': 'user18948128',
431                 'uploader': 'Jaime Marquínez Ferrándiz',
432                 'duration': 10,
433             },
434             'params': {
435                 'videopassword': 'youtube-dl',
436             },
437         },
438         {
439             'url': 'http://vimeo.com/moogaloop.swf?clip_id=2539741',
440             'only_matching': True,
441         },
442         {
443             'url': 'https://vimeo.com/109815029',
444             'note': 'Video not completely processed, "failed" seed status',
445             'only_matching': True,
446         },
447         {
448             'url': 'https://vimeo.com/groups/travelhd/videos/22439234',
449             'only_matching': True,
450         },
451         {
452             'url': 'https://vimeo.com/album/2632481/video/79010983',
453             'only_matching': True,
454         },
455         {
456             # source file returns 403: Forbidden
457             'url': 'https://vimeo.com/7809605',
458             'only_matching': True,
459         },
460         {
461             'url': 'https://vimeo.com/160743502/abd0e13fb4',
462             'only_matching': True,
463         }
464         # https://gettingthingsdone.com/workflowmap/
465         # vimeo embed with check-password page protected by Referer header
466     ]
467
468     @staticmethod
469     def _smuggle_referrer(url, referrer_url):
470         return smuggle_url(url, {'http_headers': {'Referer': referrer_url}})
471
472     @staticmethod
473     def _extract_urls(url, webpage):
474         urls = []
475         # Look for embedded (iframe) Vimeo player
476         for mobj in re.finditer(
477                 r'<iframe[^>]+?src=(["\'])(?P<url>(?:https?:)?//player\.vimeo\.com/video/\d+.*?)\1',
478                 webpage):
479             urls.append(VimeoIE._smuggle_referrer(unescapeHTML(mobj.group('url')), url))
480         PLAIN_EMBED_RE = (
481             # Look for embedded (swf embed) Vimeo player
482             r'<embed[^>]+?src=(["\'])(?P<url>(?:https?:)?//(?:www\.)?vimeo\.com/moogaloop\.swf.+?)\1',
483             # Look more for non-standard embedded Vimeo player
484             r'<video[^>]+src=(["\'])(?P<url>(?:https?:)?//(?:www\.)?vimeo\.com/[0-9]+)\1',
485         )
486         for embed_re in PLAIN_EMBED_RE:
487             for mobj in re.finditer(embed_re, webpage):
488                 urls.append(mobj.group('url'))
489         return urls
490
491     @staticmethod
492     def _extract_url(url, webpage):
493         urls = VimeoIE._extract_urls(url, webpage)
494         return urls[0] if urls else None
495
496     def _verify_player_video_password(self, url, video_id, headers):
497         password = self._downloader.params.get('videopassword')
498         if password is None:
499             raise ExtractorError('This video is protected by a password, use the --video-password option')
500         data = urlencode_postdata({
501             'password': base64.b64encode(password.encode()),
502         })
503         headers = merge_dicts(headers, {
504             'Content-Type': 'application/x-www-form-urlencoded',
505         })
506         checked = self._download_json(
507             url + '/check-password', video_id,
508             'Verifying the password', data=data, headers=headers)
509         if checked is False:
510             raise ExtractorError('Wrong video password', expected=True)
511         return checked
512
513     def _real_initialize(self):
514         self._login()
515
516     def _real_extract(self, url):
517         url, data = unsmuggle_url(url, {})
518         headers = std_headers.copy()
519         if 'http_headers' in data:
520             headers.update(data['http_headers'])
521         if 'Referer' not in headers:
522             headers['Referer'] = url
523
524         channel_id = self._search_regex(
525             r'vimeo\.com/channels/([^/]+)', url, 'channel id', default=None)
526
527         # Extract ID from URL
528         mobj = re.match(self._VALID_URL, url)
529         video_id = mobj.group('id')
530         orig_url = url
531         if mobj.group('pro'):
532             # some videos require portfolio_id to be present in player url
533             # https://github.com/ytdl-org/youtube-dl/issues/20070
534             url = self._extract_url(url, self._download_webpage(url, video_id))
535         elif mobj.group('player'):
536             url = 'https://player.vimeo.com/video/' + video_id
537         elif any(p in url for p in ('play_redirect_hls', 'moogaloop.swf')):
538             url = 'https://vimeo.com/' + video_id
539
540         # Retrieve video webpage to extract further information
541         request = sanitized_Request(url, headers=headers)
542         try:
543             webpage, urlh = self._download_webpage_handle(request, video_id)
544             redirect_url = compat_str(urlh.geturl())
545             # Some URLs redirect to ondemand can't be extracted with
546             # this extractor right away thus should be passed through
547             # ondemand extractor (e.g. https://vimeo.com/73445910)
548             if VimeoOndemandIE.suitable(redirect_url):
549                 return self.url_result(redirect_url, VimeoOndemandIE.ie_key())
550         except ExtractorError as ee:
551             if isinstance(ee.cause, compat_HTTPError) and ee.cause.code == 403:
552                 errmsg = ee.cause.read()
553                 if b'Because of its privacy settings, this video cannot be played here' in errmsg:
554                     raise ExtractorError(
555                         'Cannot download embed-only video without embedding '
556                         'URL. Please call youtube-dl with the URL of the page '
557                         'that embeds this video.',
558                         expected=True)
559             raise
560
561         # Now we begin extracting as much information as we can from what we
562         # retrieved. First we extract the information common to all extractors,
563         # and latter we extract those that are Vimeo specific.
564         self.report_extraction(video_id)
565
566         vimeo_config = self._search_regex(
567             r'vimeo\.config\s*=\s*(?:({.+?})|_extend\([^,]+,\s+({.+?})\));', webpage,
568             'vimeo config', default=None)
569         if vimeo_config:
570             seed_status = self._parse_json(vimeo_config, video_id).get('seed_status', {})
571             if seed_status.get('state') == 'failed':
572                 raise ExtractorError(
573                     '%s said: %s' % (self.IE_NAME, seed_status['title']),
574                     expected=True)
575
576         cc_license = None
577         timestamp = None
578
579         # Extract the config JSON
580         try:
581             try:
582                 config_url = self._html_search_regex(
583                     r' data-config-url="(.+?)"', webpage,
584                     'config URL', default=None)
585                 if not config_url:
586                     # Sometimes new react-based page is served instead of old one that require
587                     # different config URL extraction approach (see
588                     # https://github.com/ytdl-org/youtube-dl/pull/7209)
589                     vimeo_clip_page_config = self._search_regex(
590                         r'vimeo\.clip_page_config\s*=\s*({.+?});', webpage,
591                         'vimeo clip page config')
592                     page_config = self._parse_json(vimeo_clip_page_config, video_id)
593                     config_url = page_config['player']['config_url']
594                     cc_license = page_config.get('cc_license')
595                     timestamp = try_get(
596                         page_config, lambda x: x['clip']['uploaded_on'],
597                         compat_str)
598                 config_json = self._download_webpage(config_url, video_id)
599                 config = json.loads(config_json)
600             except RegexNotFoundError:
601                 # For pro videos or player.vimeo.com urls
602                 # We try to find out to which variable is assigned the config dic
603                 m_variable_name = re.search(r'(\w)\.video\.id', webpage)
604                 if m_variable_name is not None:
605                     config_re = [r'%s=({[^}].+?});' % re.escape(m_variable_name.group(1))]
606                 else:
607                     config_re = [r' = {config:({.+?}),assets:', r'(?:[abc])=({.+?});']
608                 config_re.append(r'\bvar\s+r\s*=\s*({.+?})\s*;')
609                 config_re.append(r'\bconfig\s*=\s*({.+?})\s*;')
610                 config = self._search_regex(config_re, webpage, 'info section',
611                                             flags=re.DOTALL)
612                 config = json.loads(config)
613         except Exception as e:
614             if re.search('The creator of this video has not given you permission to embed it on this domain.', webpage):
615                 raise ExtractorError('The author has restricted the access to this video, try with the "--referer" option')
616
617             if re.search(r'<form[^>]+?id="pw_form"', webpage) is not None:
618                 if '_video_password_verified' in data:
619                     raise ExtractorError('video password verification failed!')
620                 self._verify_video_password(redirect_url, video_id, webpage)
621                 return self._real_extract(
622                     smuggle_url(redirect_url, {'_video_password_verified': 'verified'}))
623             else:
624                 raise ExtractorError('Unable to extract info section',
625                                      cause=e)
626         else:
627             if config.get('view') == 4:
628                 config = self._verify_player_video_password(redirect_url, video_id, headers)
629
630         vod = config.get('video', {}).get('vod', {})
631
632         def is_rented():
633             if '>You rented this title.<' in webpage:
634                 return True
635             if config.get('user', {}).get('purchased'):
636                 return True
637             for purchase_option in vod.get('purchase_options', []):
638                 if purchase_option.get('purchased'):
639                     return True
640                 label = purchase_option.get('label_string')
641                 if label and (label.startswith('You rented this') or label.endswith(' remaining')):
642                     return True
643             return False
644
645         if is_rented() and vod.get('is_trailer'):
646             feature_id = vod.get('feature_id')
647             if feature_id and not data.get('force_feature_id', False):
648                 return self.url_result(smuggle_url(
649                     'https://player.vimeo.com/player/%s' % feature_id,
650                     {'force_feature_id': True}), 'Vimeo')
651
652         # Extract video description
653
654         video_description = self._html_search_regex(
655             r'(?s)<div\s+class="[^"]*description[^"]*"[^>]*>(.*?)</div>',
656             webpage, 'description', default=None)
657         if not video_description:
658             video_description = self._html_search_meta(
659                 'description', webpage, default=None)
660         if not video_description and mobj.group('pro'):
661             orig_webpage = self._download_webpage(
662                 orig_url, video_id,
663                 note='Downloading webpage for description',
664                 fatal=False)
665             if orig_webpage:
666                 video_description = self._html_search_meta(
667                     'description', orig_webpage, default=None)
668         if not video_description and not mobj.group('player'):
669             self._downloader.report_warning('Cannot find video description')
670
671         # Extract upload date
672         if not timestamp:
673             timestamp = self._search_regex(
674                 r'<time[^>]+datetime="([^"]+)"', webpage,
675                 'timestamp', default=None)
676
677         try:
678             view_count = int(self._search_regex(r'UserPlays:(\d+)', webpage, 'view count'))
679             like_count = int(self._search_regex(r'UserLikes:(\d+)', webpage, 'like count'))
680             comment_count = int(self._search_regex(r'UserComments:(\d+)', webpage, 'comment count'))
681         except RegexNotFoundError:
682             # This info is only available in vimeo.com/{id} urls
683             view_count = None
684             like_count = None
685             comment_count = None
686
687         formats = []
688
689         source_format = self._extract_original_format(
690             'https://vimeo.com/' + video_id, video_id)
691         if source_format:
692             formats.append(source_format)
693
694         info_dict_config = self._parse_config(config, video_id)
695         formats.extend(info_dict_config['formats'])
696         self._vimeo_sort_formats(formats)
697
698         json_ld = self._search_json_ld(webpage, video_id, default={})
699
700         if not cc_license:
701             cc_license = self._search_regex(
702                 r'<link[^>]+rel=["\']license["\'][^>]+href=(["\'])(?P<license>(?:(?!\1).)+)\1',
703                 webpage, 'license', default=None, group='license')
704
705         channel_url = 'https://vimeo.com/channels/%s' % channel_id if channel_id else None
706
707         info_dict = {
708             'id': video_id,
709             'formats': formats,
710             'timestamp': unified_timestamp(timestamp),
711             'description': video_description,
712             'webpage_url': url,
713             'view_count': view_count,
714             'like_count': like_count,
715             'comment_count': comment_count,
716             'license': cc_license,
717             'channel_id': channel_id,
718             'channel_url': channel_url,
719         }
720
721         info_dict = merge_dicts(info_dict, info_dict_config, json_ld)
722
723         return info_dict
724
725
726 class VimeoOndemandIE(VimeoBaseInfoExtractor):
727     IE_NAME = 'vimeo:ondemand'
728     _VALID_URL = r'https?://(?:www\.)?vimeo\.com/ondemand/(?P<id>[^/?#&]+)'
729     _TESTS = [{
730         # ondemand video not available via https://vimeo.com/id
731         'url': 'https://vimeo.com/ondemand/20704',
732         'md5': 'c424deda8c7f73c1dfb3edd7630e2f35',
733         'info_dict': {
734             'id': '105442900',
735             'ext': 'mp4',
736             'title': 'המעבדה - במאי יותם פלדמן',
737             'uploader': 'גם סרטים',
738             'uploader_url': r're:https?://(?:www\.)?vimeo\.com/gumfilms',
739             'uploader_id': 'gumfilms',
740         },
741         'params': {
742             'format': 'best[protocol=https]',
743         },
744     }, {
745         # requires Referer to be passed along with og:video:url
746         'url': 'https://vimeo.com/ondemand/36938/126682985',
747         'info_dict': {
748             'id': '126682985',
749             'ext': 'mp4',
750             'title': 'Rävlock, rätt läte på rätt plats',
751             'uploader': 'Lindroth & Norin',
752             'uploader_url': r're:https?://(?:www\.)?vimeo\.com/user14430847',
753             'uploader_id': 'user14430847',
754         },
755         'params': {
756             'skip_download': True,
757         },
758     }, {
759         'url': 'https://vimeo.com/ondemand/nazmaalik',
760         'only_matching': True,
761     }, {
762         'url': 'https://vimeo.com/ondemand/141692381',
763         'only_matching': True,
764     }, {
765         'url': 'https://vimeo.com/ondemand/thelastcolony/150274832',
766         'only_matching': True,
767     }]
768
769     def _real_extract(self, url):
770         video_id = self._match_id(url)
771         webpage = self._download_webpage(url, video_id)
772         return self.url_result(
773             # Some videos require Referer to be passed along with og:video:url
774             # similarly to generic vimeo embeds (e.g.
775             # https://vimeo.com/ondemand/36938/126682985).
776             VimeoIE._smuggle_referrer(self._og_search_video_url(webpage), url),
777             VimeoIE.ie_key())
778
779
780 class VimeoChannelIE(VimeoBaseInfoExtractor):
781     IE_NAME = 'vimeo:channel'
782     _VALID_URL = r'https://vimeo\.com/channels/(?P<id>[^/?#]+)/?(?:$|[?#])'
783     _MORE_PAGES_INDICATOR = r'<a.+?rel="next"'
784     _TITLE = None
785     _TITLE_RE = r'<link rel="alternate"[^>]+?title="(.*?)"'
786     _TESTS = [{
787         'url': 'https://vimeo.com/channels/tributes',
788         'info_dict': {
789             'id': 'tributes',
790             'title': 'Vimeo Tributes',
791         },
792         'playlist_mincount': 25,
793     }]
794
795     def _page_url(self, base_url, pagenum):
796         return '%s/videos/page:%d/' % (base_url, pagenum)
797
798     def _extract_list_title(self, webpage):
799         return self._TITLE or self._html_search_regex(self._TITLE_RE, webpage, 'list title')
800
801     def _login_list_password(self, page_url, list_id, webpage):
802         login_form = self._search_regex(
803             r'(?s)<form[^>]+?id="pw_form"(.*?)</form>',
804             webpage, 'login form', default=None)
805         if not login_form:
806             return webpage
807
808         password = self._downloader.params.get('videopassword')
809         if password is None:
810             raise ExtractorError('This album is protected by a password, use the --video-password option', expected=True)
811         fields = self._hidden_inputs(login_form)
812         token, vuid = self._extract_xsrft_and_vuid(webpage)
813         fields['token'] = token
814         fields['password'] = password
815         post = urlencode_postdata(fields)
816         password_path = self._search_regex(
817             r'action="([^"]+)"', login_form, 'password URL')
818         password_url = compat_urlparse.urljoin(page_url, password_path)
819         password_request = sanitized_Request(password_url, post)
820         password_request.add_header('Content-type', 'application/x-www-form-urlencoded')
821         self._set_vimeo_cookie('vuid', vuid)
822         self._set_vimeo_cookie('xsrft', token)
823
824         return self._download_webpage(
825             password_request, list_id,
826             'Verifying the password', 'Wrong password')
827
828     def _title_and_entries(self, list_id, base_url):
829         for pagenum in itertools.count(1):
830             page_url = self._page_url(base_url, pagenum)
831             webpage = self._download_webpage(
832                 page_url, list_id,
833                 'Downloading page %s' % pagenum)
834
835             if pagenum == 1:
836                 webpage = self._login_list_password(page_url, list_id, webpage)
837                 yield self._extract_list_title(webpage)
838
839             # Try extracting href first since not all videos are available via
840             # short https://vimeo.com/id URL (e.g. https://vimeo.com/channels/tributes/6213729)
841             clips = re.findall(
842                 r'id="clip_(\d+)"[^>]*>\s*<a[^>]+href="(/(?:[^/]+/)*\1)(?:[^>]+\btitle="([^"]+)")?', webpage)
843             if clips:
844                 for video_id, video_url, video_title in clips:
845                     yield self.url_result(
846                         compat_urlparse.urljoin(base_url, video_url),
847                         VimeoIE.ie_key(), video_id=video_id, video_title=video_title)
848             # More relaxed fallback
849             else:
850                 for video_id in re.findall(r'id=["\']clip_(\d+)', webpage):
851                     yield self.url_result(
852                         'https://vimeo.com/%s' % video_id,
853                         VimeoIE.ie_key(), video_id=video_id)
854
855             if re.search(self._MORE_PAGES_INDICATOR, webpage, re.DOTALL) is None:
856                 break
857
858     def _extract_videos(self, list_id, base_url):
859         title_and_entries = self._title_and_entries(list_id, base_url)
860         list_title = next(title_and_entries)
861         return self.playlist_result(title_and_entries, list_id, list_title)
862
863     def _real_extract(self, url):
864         mobj = re.match(self._VALID_URL, url)
865         channel_id = mobj.group('id')
866         return self._extract_videos(channel_id, 'https://vimeo.com/channels/%s' % channel_id)
867
868
869 class VimeoUserIE(VimeoChannelIE):
870     IE_NAME = 'vimeo:user'
871     _VALID_URL = r'https://vimeo\.com/(?!(?:[0-9]+|watchlater)(?:$|[?#/]))(?P<name>[^/]+)(?:/videos|[#?]|$)'
872     _TITLE_RE = r'<a[^>]+?class="user">([^<>]+?)</a>'
873     _TESTS = [{
874         'url': 'https://vimeo.com/nkistudio/videos',
875         'info_dict': {
876             'title': 'Nki',
877             'id': 'nkistudio',
878         },
879         'playlist_mincount': 66,
880     }]
881
882     def _real_extract(self, url):
883         mobj = re.match(self._VALID_URL, url)
884         name = mobj.group('name')
885         return self._extract_videos(name, 'https://vimeo.com/%s' % name)
886
887
888 class VimeoAlbumIE(VimeoChannelIE):
889     IE_NAME = 'vimeo:album'
890     _VALID_URL = r'https://vimeo\.com/album/(?P<id>\d+)(?:$|[?#]|/(?!video))'
891     _TITLE_RE = r'<header id="page_header">\n\s*<h1>(.*?)</h1>'
892     _TESTS = [{
893         'url': 'https://vimeo.com/album/2632481',
894         'info_dict': {
895             'id': '2632481',
896             'title': 'Staff Favorites: November 2013',
897         },
898         'playlist_mincount': 13,
899     }, {
900         'note': 'Password-protected album',
901         'url': 'https://vimeo.com/album/3253534',
902         'info_dict': {
903             'title': 'test',
904             'id': '3253534',
905         },
906         'playlist_count': 1,
907         'params': {
908             'videopassword': 'youtube-dl',
909         }
910     }, {
911         'url': 'https://vimeo.com/album/2632481/sort:plays/format:thumbnail',
912         'only_matching': True,
913     }, {
914         # TODO: respect page number
915         'url': 'https://vimeo.com/album/2632481/page:2/sort:plays/format:thumbnail',
916         'only_matching': True,
917     }]
918
919     def _page_url(self, base_url, pagenum):
920         return '%s/page:%d/' % (base_url, pagenum)
921
922     def _real_extract(self, url):
923         album_id = self._match_id(url)
924         return self._extract_videos(album_id, 'https://vimeo.com/album/%s' % album_id)
925
926
927 class VimeoGroupsIE(VimeoAlbumIE):
928     IE_NAME = 'vimeo:group'
929     _VALID_URL = r'https://vimeo\.com/groups/(?P<name>[^/]+)(?:/(?!videos?/\d+)|$)'
930     _TESTS = [{
931         'url': 'https://vimeo.com/groups/rolexawards',
932         'info_dict': {
933             'id': 'rolexawards',
934             'title': 'Rolex Awards for Enterprise',
935         },
936         'playlist_mincount': 73,
937     }]
938
939     def _extract_list_title(self, webpage):
940         return self._og_search_title(webpage)
941
942     def _real_extract(self, url):
943         mobj = re.match(self._VALID_URL, url)
944         name = mobj.group('name')
945         return self._extract_videos(name, 'https://vimeo.com/groups/%s' % name)
946
947
948 class VimeoReviewIE(VimeoBaseInfoExtractor):
949     IE_NAME = 'vimeo:review'
950     IE_DESC = 'Review pages on vimeo'
951     _VALID_URL = r'(?P<url>https://vimeo\.com/[^/]+/review/(?P<id>[^/]+)/[0-9a-f]{10})'
952     _TESTS = [{
953         'url': 'https://vimeo.com/user21297594/review/75524534/3c257a1b5d',
954         'md5': 'c507a72f780cacc12b2248bb4006d253',
955         'info_dict': {
956             'id': '75524534',
957             'ext': 'mp4',
958             'title': "DICK HARDWICK 'Comedian'",
959             'uploader': 'Richard Hardwick',
960             'uploader_id': 'user21297594',
961         }
962     }, {
963         'note': 'video player needs Referer',
964         'url': 'https://vimeo.com/user22258446/review/91613211/13f927e053',
965         'md5': '6295fdab8f4bf6a002d058b2c6dce276',
966         'info_dict': {
967             'id': '91613211',
968             'ext': 'mp4',
969             'title': 're:(?i)^Death by dogma versus assembling agile . Sander Hoogendoorn',
970             'uploader': 'DevWeek Events',
971             'duration': 2773,
972             'thumbnail': r're:^https?://.*\.jpg$',
973             'uploader_id': 'user22258446',
974         }
975     }, {
976         'note': 'Password protected',
977         'url': 'https://vimeo.com/user37284429/review/138823582/c4d865efde',
978         'info_dict': {
979             'id': '138823582',
980             'ext': 'mp4',
981             'title': 'EFFICIENT PICKUP MASTERCLASS MODULE 1',
982             'uploader': 'TMB',
983             'uploader_id': 'user37284429',
984         },
985         'params': {
986             'videopassword': 'holygrail',
987         },
988         'skip': 'video gone',
989     }]
990
991     def _real_initialize(self):
992         self._login()
993
994     def _get_config_url(self, webpage_url, video_id, video_password_verified=False):
995         webpage = self._download_webpage(webpage_url, video_id)
996         config_url = self._html_search_regex(
997             r'data-config-url=(["\'])(?P<url>(?:(?!\1).)+)\1', webpage,
998             'config URL', default=None, group='url')
999         if not config_url:
1000             data = self._parse_json(self._search_regex(
1001                 r'window\s*=\s*_extend\(window,\s*({.+?})\);', webpage, 'data',
1002                 default=NO_DEFAULT if video_password_verified else '{}'), video_id)
1003             config = data.get('vimeo_esi', {}).get('config', {})
1004             config_url = config.get('configUrl') or try_get(config, lambda x: x['clipData']['configUrl'])
1005         if config_url is None:
1006             self._verify_video_password(webpage_url, video_id, webpage)
1007             config_url = self._get_config_url(
1008                 webpage_url, video_id, video_password_verified=True)
1009         return config_url
1010
1011     def _real_extract(self, url):
1012         page_url, video_id = re.match(self._VALID_URL, url).groups()
1013         config_url = self._get_config_url(url, video_id)
1014         config = self._download_json(config_url, video_id)
1015         info_dict = self._parse_config(config, video_id)
1016         source_format = self._extract_original_format(page_url, video_id)
1017         if source_format:
1018             info_dict['formats'].append(source_format)
1019         self._vimeo_sort_formats(info_dict['formats'])
1020         info_dict['id'] = video_id
1021         return info_dict
1022
1023
1024 class VimeoWatchLaterIE(VimeoChannelIE):
1025     IE_NAME = 'vimeo:watchlater'
1026     IE_DESC = 'Vimeo watch later list, "vimeowatchlater" keyword (requires authentication)'
1027     _VALID_URL = r'https://vimeo\.com/(?:home/)?watchlater|:vimeowatchlater'
1028     _TITLE = 'Watch Later'
1029     _LOGIN_REQUIRED = True
1030     _TESTS = [{
1031         'url': 'https://vimeo.com/watchlater',
1032         'only_matching': True,
1033     }]
1034
1035     def _real_initialize(self):
1036         self._login()
1037
1038     def _page_url(self, base_url, pagenum):
1039         url = '%s/page:%d/' % (base_url, pagenum)
1040         request = sanitized_Request(url)
1041         # Set the header to get a partial html page with the ids,
1042         # the normal page doesn't contain them.
1043         request.add_header('X-Requested-With', 'XMLHttpRequest')
1044         return request
1045
1046     def _real_extract(self, url):
1047         return self._extract_videos('watchlater', 'https://vimeo.com/watchlater')
1048
1049
1050 class VimeoLikesIE(InfoExtractor):
1051     _VALID_URL = r'https://(?:www\.)?vimeo\.com/(?P<id>[^/]+)/likes/?(?:$|[?#]|sort:)'
1052     IE_NAME = 'vimeo:likes'
1053     IE_DESC = 'Vimeo user likes'
1054     _TESTS = [{
1055         'url': 'https://vimeo.com/user755559/likes/',
1056         'playlist_mincount': 293,
1057         'info_dict': {
1058             'id': 'user755559_likes',
1059             'description': 'See all the videos urza likes',
1060             'title': 'Videos urza likes',
1061         },
1062     }, {
1063         'url': 'https://vimeo.com/stormlapse/likes',
1064         'only_matching': True,
1065     }]
1066
1067     def _real_extract(self, url):
1068         user_id = self._match_id(url)
1069         webpage = self._download_webpage(url, user_id)
1070         page_count = self._int(
1071             self._search_regex(
1072                 r'''(?x)<li><a\s+href="[^"]+"\s+data-page="([0-9]+)">
1073                     .*?</a></li>\s*<li\s+class="pagination_next">
1074                 ''', webpage, 'page count', default=1),
1075             'page count', fatal=True)
1076         PAGE_SIZE = 12
1077         title = self._html_search_regex(
1078             r'(?s)<h1>(.+?)</h1>', webpage, 'title', fatal=False)
1079         description = self._html_search_meta('description', webpage)
1080
1081         def _get_page(idx):
1082             page_url = 'https://vimeo.com/%s/likes/page:%d/sort:date' % (
1083                 user_id, idx + 1)
1084             webpage = self._download_webpage(
1085                 page_url, user_id,
1086                 note='Downloading page %d/%d' % (idx + 1, page_count))
1087             video_list = self._search_regex(
1088                 r'(?s)<ol class="js-browse_list[^"]+"[^>]*>(.*?)</ol>',
1089                 webpage, 'video content')
1090             paths = re.findall(
1091                 r'<li[^>]*>\s*<a\s+href="([^"]+)"', video_list)
1092             for path in paths:
1093                 yield {
1094                     '_type': 'url',
1095                     'url': compat_urlparse.urljoin(page_url, path),
1096                 }
1097
1098         pl = InAdvancePagedList(_get_page, page_count, PAGE_SIZE)
1099
1100         return {
1101             '_type': 'playlist',
1102             'id': '%s_likes' % user_id,
1103             'title': title,
1104             'description': description,
1105             'entries': pl,
1106         }
1107
1108
1109 class VHXEmbedIE(InfoExtractor):
1110     IE_NAME = 'vhx:embed'
1111     _VALID_URL = r'https?://embed\.vhx\.tv/videos/(?P<id>\d+)'
1112
1113     def _call_api(self, video_id, access_token, path='', query=None):
1114         return self._download_json(
1115             'https://api.vhx.tv/videos/' + video_id + path, video_id, headers={
1116                 'Authorization': 'Bearer ' + access_token,
1117             }, query=query)
1118
1119     def _real_extract(self, url):
1120         video_id = self._match_id(url)
1121         webpage = self._download_webpage(url, video_id)
1122         credentials = self._parse_json(self._search_regex(
1123             r'(?s)credentials\s*:\s*({.+?}),', webpage,
1124             'config'), video_id, js_to_json)
1125         access_token = credentials['access_token']
1126
1127         query = {}
1128         for k, v in credentials.items():
1129             if k in ('authorization', 'authUserToken', 'ticket') and v and v != 'undefined':
1130                 if k == 'authUserToken':
1131                     query['auth_user_token'] = v
1132                 else:
1133                     query[k] = v
1134         files = self._call_api(video_id, access_token, '/files', query)
1135
1136         formats = []
1137         for f in files:
1138             href = try_get(f, lambda x: x['_links']['source']['href'])
1139             if not href:
1140                 continue
1141             method = f.get('method')
1142             if method == 'hls':
1143                 formats.extend(self._extract_m3u8_formats(
1144                     href, video_id, 'mp4', 'm3u8_native',
1145                     m3u8_id='hls', fatal=False))
1146             elif method == 'dash':
1147                 formats.extend(self._extract_mpd_formats(
1148                     href, video_id, mpd_id='dash', fatal=False))
1149             else:
1150                 fmt = {
1151                     'filesize': int_or_none(try_get(f, lambda x: x['size']['bytes'])),
1152                     'format_id': 'http',
1153                     'preference': 1,
1154                     'url': href,
1155                     'vcodec': f.get('codec'),
1156                 }
1157                 quality = f.get('quality')
1158                 if quality:
1159                     fmt.update({
1160                         'format_id': 'http-' + quality,
1161                         'height': int_or_none(self._search_regex(r'(\d+)p', quality, 'height', default=None)),
1162                     })
1163                 formats.append(fmt)
1164         self._sort_formats(formats)
1165
1166         video_data = self._call_api(video_id, access_token)
1167         title = video_data.get('title') or video_data['name']
1168
1169         subtitles = {}
1170         for subtitle in try_get(video_data, lambda x: x['tracks']['subtitles'], list) or []:
1171             lang = subtitle.get('srclang') or subtitle.get('label')
1172             for _link in subtitle.get('_links', {}).values():
1173                 href = _link.get('href')
1174                 if not href:
1175                     continue
1176                 subtitles.setdefault(lang, []).append({
1177                     'url': href,
1178                 })
1179
1180         q = qualities(['small', 'medium', 'large', 'source'])
1181         thumbnails = []
1182         for thumbnail_id, thumbnail_url in video_data.get('thumbnail', {}).items():
1183             thumbnails.append({
1184                 'id': thumbnail_id,
1185                 'url': thumbnail_url,
1186                 'preference': q(thumbnail_id),
1187             })
1188
1189         return {
1190             'id': video_id,
1191             'title': title,
1192             'description': video_data.get('description'),
1193             'duration': int_or_none(try_get(video_data, lambda x: x['duration']['seconds'])),
1194             'formats': formats,
1195             'subtitles': subtitles,
1196             'thumbnails': thumbnails,
1197             'timestamp': unified_timestamp(video_data.get('created_at')),
1198             'view_count': int_or_none(video_data.get('plays_count')),
1199         }