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