logo

oasis-root

Compiled tree of Oasis Linux based on own branch at <https://hacktivis.me/git/oasis/> git clone https://anongit.hacktivis.me/git/oasis-root.git

vimeo.py (70873B)


  1. import base64
  2. import functools
  3. import itertools
  4. import json
  5. import re
  6. import urllib.parse
  7. from .common import InfoExtractor
  8. from ..networking import HEADRequest, Request
  9. from ..networking.exceptions import HTTPError
  10. from ..utils import (
  11. ExtractorError,
  12. OnDemandPagedList,
  13. clean_html,
  14. determine_ext,
  15. get_element_by_class,
  16. int_or_none,
  17. join_nonempty,
  18. js_to_json,
  19. merge_dicts,
  20. parse_filesize,
  21. parse_iso8601,
  22. parse_qs,
  23. qualities,
  24. smuggle_url,
  25. str_or_none,
  26. traverse_obj,
  27. try_get,
  28. unified_timestamp,
  29. unsmuggle_url,
  30. urlencode_postdata,
  31. urlhandle_detect_ext,
  32. urljoin,
  33. )
  34. class VimeoBaseInfoExtractor(InfoExtractor):
  35. _NETRC_MACHINE = 'vimeo'
  36. _LOGIN_REQUIRED = False
  37. _LOGIN_URL = 'https://vimeo.com/log_in'
  38. @staticmethod
  39. def _smuggle_referrer(url, referrer_url):
  40. return smuggle_url(url, {'referer': referrer_url})
  41. def _unsmuggle_headers(self, url):
  42. """@returns (url, smuggled_data, headers)"""
  43. url, data = unsmuggle_url(url, {})
  44. headers = self.get_param('http_headers').copy()
  45. if 'referer' in data:
  46. headers['Referer'] = data['referer']
  47. return url, data, headers
  48. def _perform_login(self, username, password):
  49. viewer = self._download_json('https://vimeo.com/_next/viewer', None, 'Downloading login token')
  50. data = {
  51. 'action': 'login',
  52. 'email': username,
  53. 'password': password,
  54. 'service': 'vimeo',
  55. 'token': viewer['xsrft'],
  56. }
  57. self._set_vimeo_cookie('vuid', viewer['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, HTTPError) and e.cause.status == 418:
  67. raise ExtractorError(
  68. 'Unable to log in: bad username or password',
  69. expected=True)
  70. raise ExtractorError('Unable to log in')
  71. def _real_initialize(self):
  72. if self._LOGIN_REQUIRED and not self._get_cookies('https://vimeo.com').get('vuid'):
  73. self._raise_login_required()
  74. def _get_video_password(self):
  75. password = self.get_param('videopassword')
  76. if password is None:
  77. raise ExtractorError(
  78. 'This video is protected by a password, use the --video-password option',
  79. expected=True)
  80. return password
  81. def _verify_video_password(self, video_id, password, token):
  82. url = f'https://vimeo.com/{video_id}'
  83. try:
  84. return self._download_webpage(
  85. f'{url}/password', video_id,
  86. 'Submitting video password', data=json.dumps({
  87. 'password': password,
  88. 'token': token,
  89. }, separators=(',', ':')).encode(), headers={
  90. 'Accept': '*/*',
  91. 'Content-Type': 'application/json',
  92. 'Referer': url,
  93. }, impersonate=True)
  94. except ExtractorError as error:
  95. if isinstance(error.cause, HTTPError) and error.cause.status == 418:
  96. raise ExtractorError('Wrong password', expected=True)
  97. raise
  98. def _extract_vimeo_config(self, webpage, video_id, *args, **kwargs):
  99. vimeo_config = self._search_regex(
  100. r'vimeo\.config\s*=\s*(?:({.+?})|_extend\([^,]+,\s+({.+?})\));',
  101. webpage, 'vimeo config', *args, **kwargs)
  102. if vimeo_config:
  103. return self._parse_json(vimeo_config, video_id)
  104. def _set_vimeo_cookie(self, name, value):
  105. self._set_cookie('vimeo.com', name, value)
  106. def _parse_config(self, config, video_id):
  107. video_data = config['video']
  108. video_title = video_data.get('title')
  109. live_event = video_data.get('live_event') or {}
  110. live_status = {
  111. 'pending': 'is_upcoming',
  112. 'active': 'is_upcoming',
  113. 'started': 'is_live',
  114. 'ended': 'post_live',
  115. }.get(live_event.get('status'))
  116. is_live = live_status == 'is_live'
  117. request = config.get('request') or {}
  118. formats = []
  119. subtitles = {}
  120. config_files = video_data.get('files') or request.get('files') or {}
  121. for f in (config_files.get('progressive') or []):
  122. video_url = f.get('url')
  123. if not video_url:
  124. continue
  125. formats.append({
  126. 'url': video_url,
  127. 'format_id': 'http-{}'.format(f.get('quality')),
  128. 'source_preference': 10,
  129. 'width': int_or_none(f.get('width')),
  130. 'height': int_or_none(f.get('height')),
  131. 'fps': int_or_none(f.get('fps')),
  132. 'tbr': int_or_none(f.get('bitrate')),
  133. })
  134. # TODO: fix handling of 308 status code returned for live archive manifest requests
  135. QUALITIES = ('low', 'medium', 'high')
  136. quality = qualities(QUALITIES)
  137. sep_pattern = r'/sep/video/'
  138. for files_type in ('hls', 'dash'):
  139. for cdn_name, cdn_data in (try_get(config_files, lambda x: x[files_type]['cdns']) or {}).items():
  140. manifest_url = cdn_data.get('url')
  141. if not manifest_url:
  142. continue
  143. format_id = f'{files_type}-{cdn_name}'
  144. sep_manifest_urls = []
  145. if re.search(sep_pattern, manifest_url):
  146. for suffix, repl in (('', 'video'), ('_sep', 'sep/video')):
  147. sep_manifest_urls.append((format_id + suffix, re.sub(
  148. sep_pattern, f'/{repl}/', manifest_url)))
  149. else:
  150. sep_manifest_urls = [(format_id, manifest_url)]
  151. for f_id, m_url in sep_manifest_urls:
  152. if files_type == 'hls':
  153. fmts, subs = self._extract_m3u8_formats_and_subtitles(
  154. m_url, video_id, 'mp4', live=is_live, m3u8_id=f_id,
  155. note=f'Downloading {cdn_name} m3u8 information',
  156. fatal=False)
  157. # m3u8 doesn't give audio bitrates; need to prioritize based on GROUP-ID
  158. # See: https://github.com/yt-dlp/yt-dlp/issues/10854
  159. for f in fmts:
  160. if mobj := re.search(rf'audio-({"|".join(QUALITIES)})', f['format_id']):
  161. f['quality'] = quality(mobj.group(1))
  162. formats.extend(fmts)
  163. self._merge_subtitles(subs, target=subtitles)
  164. elif files_type == 'dash':
  165. if 'json=1' in m_url:
  166. real_m_url = (self._download_json(m_url, video_id, fatal=False) or {}).get('url')
  167. if real_m_url:
  168. m_url = real_m_url
  169. fmts, subs = self._extract_mpd_formats_and_subtitles(
  170. m_url.replace('/master.json', '/master.mpd'), video_id, f_id,
  171. f'Downloading {cdn_name} MPD information',
  172. fatal=False)
  173. formats.extend(fmts)
  174. self._merge_subtitles(subs, target=subtitles)
  175. live_archive = live_event.get('archive') or {}
  176. live_archive_source_url = live_archive.get('source_url')
  177. if live_archive_source_url and live_archive.get('status') == 'done':
  178. formats.append({
  179. 'format_id': 'live-archive-source',
  180. 'url': live_archive_source_url,
  181. 'quality': 10,
  182. })
  183. for tt in (request.get('text_tracks') or []):
  184. subtitles.setdefault(tt['lang'], []).append({
  185. 'ext': 'vtt',
  186. 'url': urljoin('https://vimeo.com', tt['url']),
  187. })
  188. thumbnails = []
  189. if not is_live:
  190. for key, thumb in (video_data.get('thumbs') or {}).items():
  191. thumbnails.append({
  192. 'id': key,
  193. 'width': int_or_none(key),
  194. 'url': thumb,
  195. })
  196. thumbnail = video_data.get('thumbnail')
  197. if thumbnail:
  198. thumbnails.append({
  199. 'url': thumbnail,
  200. })
  201. owner = video_data.get('owner') or {}
  202. video_uploader_url = owner.get('url')
  203. return {
  204. 'id': str_or_none(video_data.get('id')) or video_id,
  205. 'title': video_title,
  206. 'uploader': owner.get('name'),
  207. 'uploader_id': video_uploader_url.split('/')[-1] if video_uploader_url else None,
  208. 'uploader_url': video_uploader_url,
  209. 'thumbnails': thumbnails,
  210. 'duration': int_or_none(video_data.get('duration')),
  211. 'chapters': sorted(traverse_obj(config, (
  212. 'embed', 'chapters', lambda _, v: int(v['timecode']) is not None, {
  213. 'title': ('title', {str}),
  214. 'start_time': ('timecode', {int_or_none}),
  215. })), key=lambda c: c['start_time']) or None,
  216. 'formats': formats,
  217. 'subtitles': subtitles,
  218. 'live_status': live_status,
  219. 'release_timestamp': traverse_obj(live_event, ('ingest', 'scheduled_start_time', {parse_iso8601})),
  220. # Note: Bitrates are completely broken. Single m3u8 may contain entries in kbps and bps
  221. # at the same time without actual units specified.
  222. '_format_sort_fields': ('quality', 'res', 'fps', 'hdr:12', 'source'),
  223. }
  224. def _call_videos_api(self, video_id, jwt_token, unlisted_hash=None, **kwargs):
  225. return self._download_json(
  226. join_nonempty(f'https://api.vimeo.com/videos/{video_id}', unlisted_hash, delim=':'),
  227. video_id, 'Downloading API JSON', headers={
  228. 'Authorization': f'jwt {jwt_token}',
  229. 'Accept': 'application/json',
  230. }, query={
  231. 'fields': ','.join((
  232. 'config_url', 'created_time', 'description', 'download', 'license',
  233. 'metadata.connections.comments.total', 'metadata.connections.likes.total',
  234. 'release_time', 'stats.plays')),
  235. }, **kwargs)
  236. def _extract_original_format(self, url, video_id, unlisted_hash=None, jwt=None, api_data=None):
  237. # Original/source formats are only available when logged in
  238. if not self._get_cookies('https://vimeo.com/').get('vimeo'):
  239. return
  240. query = {'action': 'load_download_config'}
  241. if unlisted_hash:
  242. query['unlisted_hash'] = unlisted_hash
  243. download_data = self._download_json(
  244. url, video_id, 'Loading download config JSON', fatal=False,
  245. query=query, headers={'X-Requested-With': 'XMLHttpRequest'},
  246. expected_status=(403, 404)) or {}
  247. source_file = download_data.get('source_file')
  248. download_url = try_get(source_file, lambda x: x['download_url'])
  249. if download_url and not source_file.get('is_cold') and not source_file.get('is_defrosting'):
  250. source_name = source_file.get('public_name', 'Original')
  251. if self._is_valid_url(download_url, video_id, f'{source_name} video'):
  252. ext = (try_get(
  253. source_file, lambda x: x['extension'],
  254. str) or determine_ext(
  255. download_url, None) or 'mp4').lower()
  256. return {
  257. 'url': download_url,
  258. 'ext': ext,
  259. 'width': int_or_none(source_file.get('width')),
  260. 'height': int_or_none(source_file.get('height')),
  261. 'filesize': parse_filesize(source_file.get('size')),
  262. 'format_id': source_name,
  263. 'quality': 1,
  264. }
  265. jwt = jwt or traverse_obj(self._download_json(
  266. 'https://vimeo.com/_rv/viewer', video_id, 'Downloading jwt token', fatal=False), ('jwt', {str}))
  267. if not jwt:
  268. return
  269. original_response = api_data or self._call_videos_api(
  270. video_id, jwt, unlisted_hash, fatal=False, expected_status=(403, 404))
  271. for download_data in traverse_obj(original_response, ('download', ..., {dict})):
  272. download_url = download_data.get('link')
  273. if not download_url or download_data.get('quality') != 'source':
  274. continue
  275. ext = determine_ext(parse_qs(download_url).get('filename', [''])[0].lower(), default_ext=None)
  276. if not ext:
  277. urlh = self._request_webpage(
  278. HEADRequest(download_url), video_id, fatal=False, note='Determining source extension')
  279. ext = urlh and urlhandle_detect_ext(urlh)
  280. return {
  281. 'url': download_url,
  282. 'ext': ext or 'unknown_video',
  283. 'format_id': download_data.get('public_name', 'Original'),
  284. 'width': int_or_none(download_data.get('width')),
  285. 'height': int_or_none(download_data.get('height')),
  286. 'fps': int_or_none(download_data.get('fps')),
  287. 'filesize': int_or_none(download_data.get('size')),
  288. 'quality': 1,
  289. }
  290. class VimeoIE(VimeoBaseInfoExtractor):
  291. """Information extractor for vimeo.com."""
  292. # _VALID_URL matches Vimeo URLs
  293. _VALID_URL = r'''(?x)
  294. https?://
  295. (?:
  296. (?:
  297. www|
  298. player
  299. )
  300. \.
  301. )?
  302. vimeo\.com/
  303. (?:
  304. (?P<u>user)|
  305. (?!(?:channels|album|showcase)/[^/?#]+/?(?:$|[?#])|[^/]+/review/|ondemand/)
  306. (?:.*?/)??
  307. (?P<q>
  308. (?:
  309. play_redirect_hls|
  310. moogaloop\.swf)\?clip_id=
  311. )?
  312. (?:videos?/)?
  313. )
  314. (?P<id>[0-9]+)
  315. (?(u)
  316. /(?!videos|likes)[^/?#]+/?|
  317. (?(q)|/(?P<unlisted_hash>[\da-f]{10}))?
  318. )
  319. (?:(?(q)[&]|(?(u)|/?)[?]).*?)?(?:[#].*)?$
  320. '''
  321. IE_NAME = 'vimeo'
  322. _EMBED_REGEX = [
  323. # iframe
  324. r'<iframe[^>]+?src=(["\'])(?P<url>(?:https?:)?//player\.vimeo\.com/video/\d+.*?)\1',
  325. # Embedded (swf embed) Vimeo player
  326. r'<embed[^>]+?src=(["\'])(?P<url>(?:https?:)?//(?:www\.)?vimeo\.com/moogaloop\.swf.+?)\1',
  327. # Non-standard embedded Vimeo player
  328. r'<video[^>]+src=(["\'])(?P<url>(?:https?:)?//(?:www\.)?vimeo\.com/[0-9]+)\1',
  329. ]
  330. _TESTS = [
  331. {
  332. 'url': 'http://vimeo.com/56015672#at=0',
  333. 'md5': '8879b6cc097e987f02484baf890129e5',
  334. 'info_dict': {
  335. 'id': '56015672',
  336. 'ext': 'mp4',
  337. 'title': "youtube-dl test video '' ä↭𝕐-BaW jenozKc",
  338. 'description': 'md5:2d3305bad981a06ff79f027f19865021',
  339. 'timestamp': 1355990239,
  340. 'upload_date': '20121220',
  341. 'uploader_url': r're:https?://(?:www\.)?vimeo\.com/user7108434',
  342. 'uploader_id': 'user7108434',
  343. 'uploader': 'Filippo Valsorda',
  344. 'duration': 10,
  345. 'license': 'by-sa',
  346. },
  347. 'params': {
  348. 'format': 'best[protocol=https]',
  349. },
  350. 'skip': 'No longer available',
  351. },
  352. {
  353. 'url': 'https://player.vimeo.com/video/54469442',
  354. 'md5': '619b811a4417aa4abe78dc653becf511',
  355. 'note': 'Videos that embed the url in the player page',
  356. 'info_dict': {
  357. 'id': '54469442',
  358. 'ext': 'mp4',
  359. 'title': 'Kathy Sierra: Building the minimum Badass User, Business of Software 2012',
  360. 'uploader': 'Business of Software',
  361. 'uploader_url': r're:https?://(?:www\.)?vimeo\.com/businessofsoftware',
  362. 'uploader_id': 'businessofsoftware',
  363. 'duration': 3610,
  364. 'thumbnail': 'https://i.vimeocdn.com/video/376682406-f34043e7b766af6bef2af81366eacd6724f3fc3173179a11a97a1e26587c9529-d_1280',
  365. },
  366. 'params': {
  367. 'format': 'best[protocol=https]',
  368. },
  369. 'expected_warnings': ['Failed to parse XML: not well-formed'],
  370. },
  371. {
  372. 'url': 'http://vimeo.com/68375962',
  373. 'md5': 'aaf896bdb7ddd6476df50007a0ac0ae7',
  374. 'note': 'Video protected with password',
  375. 'info_dict': {
  376. 'id': '68375962',
  377. 'ext': 'mp4',
  378. 'title': 'youtube-dl password protected test video',
  379. 'timestamp': 1371214555,
  380. 'upload_date': '20130614',
  381. 'release_timestamp': 1371214555,
  382. 'release_date': '20130614',
  383. 'uploader_url': r're:https?://(?:www\.)?vimeo\.com/user18948128',
  384. 'uploader_id': 'user18948128',
  385. 'uploader': 'Jaime Marquínez Ferrándiz',
  386. 'duration': 10,
  387. 'comment_count': int,
  388. 'like_count': int,
  389. 'thumbnail': 'https://i.vimeocdn.com/video/440665496-b2c5aee2b61089442c794f64113a8e8f7d5763c3e6b3ebfaf696ae6413f8b1f4-d_1280',
  390. },
  391. 'params': {
  392. 'format': 'best[protocol=https]',
  393. 'videopassword': 'youtube-dl',
  394. },
  395. 'expected_warnings': ['Failed to parse XML: not well-formed'],
  396. },
  397. {
  398. 'url': 'http://vimeo.com/channels/keypeele/75629013',
  399. 'md5': '2f86a05afe9d7abc0b9126d229bbe15d',
  400. 'info_dict': {
  401. 'id': '75629013',
  402. 'ext': 'mp4',
  403. 'title': 'Key & Peele: Terrorist Interrogation',
  404. 'description': 'md5:6173f270cd0c0119f22817204b3eb86c',
  405. 'uploader_url': r're:https?://(?:www\.)?vimeo\.com/atencio',
  406. 'uploader_id': 'atencio',
  407. 'uploader': 'Peter Atencio',
  408. 'channel_id': 'keypeele',
  409. 'channel_url': r're:https?://(?:www\.)?vimeo\.com/channels/keypeele',
  410. 'timestamp': 1380339469,
  411. 'upload_date': '20130928',
  412. 'duration': 187,
  413. 'thumbnail': 'https://i.vimeocdn.com/video/450239872-a05512d9b1e55d707a7c04365c10980f327b06d966351bc403a5d5d65c95e572-d_1280',
  414. 'view_count': int,
  415. 'comment_count': int,
  416. 'like_count': int,
  417. },
  418. 'params': {'format': 'http-1080p'},
  419. 'expected_warnings': ['Failed to parse XML: not well-formed'],
  420. },
  421. {
  422. 'url': 'http://vimeo.com/76979871',
  423. 'note': 'Video with subtitles',
  424. 'info_dict': {
  425. 'id': '76979871',
  426. 'ext': 'mp4',
  427. 'title': 'The New Vimeo Player (You Know, For Videos)',
  428. 'description': str, # FIXME: Dynamic SEO spam description
  429. 'timestamp': 1381860509,
  430. 'upload_date': '20131015',
  431. 'release_timestamp': 1381860509,
  432. 'release_date': '20131015',
  433. 'uploader_url': r're:https?://(?:www\.)?vimeo\.com/staff',
  434. 'uploader_id': 'staff',
  435. 'uploader': 'Vimeo',
  436. 'duration': 62,
  437. 'comment_count': int,
  438. 'like_count': int,
  439. 'thumbnail': 'https://i.vimeocdn.com/video/452001751-8216e0571c251a09d7a8387550942d89f7f86f6398f8ed886e639b0dd50d3c90-d_1280',
  440. 'subtitles': {
  441. 'de': 'count:3',
  442. 'en': 'count:3',
  443. 'es': 'count:3',
  444. 'fr': 'count:3',
  445. },
  446. },
  447. 'expected_warnings': [
  448. 'Ignoring subtitle tracks found in the HLS manifest',
  449. 'Failed to parse XML: not well-formed',
  450. ],
  451. },
  452. {
  453. # from https://www.ouya.tv/game/Pier-Solar-and-the-Great-Architects/
  454. 'url': 'https://player.vimeo.com/video/98044508',
  455. 'note': 'The js code contains assignments to the same variable as the config',
  456. 'info_dict': {
  457. 'id': '98044508',
  458. 'ext': 'mp4',
  459. 'title': 'Pier Solar OUYA Official Trailer',
  460. 'uploader': 'Tulio Gonçalves',
  461. 'uploader_url': r're:https?://(?:www\.)?vimeo\.com/user28849593',
  462. 'uploader_id': 'user28849593',
  463. 'duration': 118,
  464. 'thumbnail': 'https://i.vimeocdn.com/video/478636036-c18440305ef3df9decfb6bf207a61fe39d2d17fa462a96f6f2d93d30492b037d-d_1280',
  465. },
  466. 'expected_warnings': ['Failed to parse XML: not well-formed'],
  467. },
  468. {
  469. # contains Original format
  470. 'url': 'https://vimeo.com/33951933',
  471. # 'md5': '53c688fa95a55bf4b7293d37a89c5c53',
  472. 'info_dict': {
  473. 'id': '33951933',
  474. 'ext': 'mp4',
  475. 'title': 'FOX CLASSICS - Forever Classic ID - A Full Minute',
  476. 'uploader': 'The DMCI',
  477. 'uploader_url': r're:https?://(?:www\.)?vimeo\.com/dmci',
  478. 'uploader_id': 'dmci',
  479. 'timestamp': 1324343742,
  480. 'upload_date': '20111220',
  481. 'description': 'md5:ae23671e82d05415868f7ad1aec21147',
  482. 'duration': 60,
  483. 'comment_count': int,
  484. 'view_count': int,
  485. 'thumbnail': 'https://i.vimeocdn.com/video/231174622-dd07f015e9221ff529d451e1cc31c982b5d87bfafa48c4189b1da72824ee289a-d_1280',
  486. 'like_count': int,
  487. 'tags': 'count:11',
  488. },
  489. # 'params': {'format': 'Original'},
  490. 'expected_warnings': ['Failed to parse XML: not well-formed'],
  491. },
  492. {
  493. 'note': 'Contains source format not accessible in webpage',
  494. 'url': 'https://vimeo.com/393756517',
  495. # 'md5': 'c464af248b592190a5ffbb5d33f382b0',
  496. 'info_dict': {
  497. 'id': '393756517',
  498. # 'ext': 'mov',
  499. 'ext': 'mp4',
  500. 'timestamp': 1582642091,
  501. 'uploader_id': 'frameworkla',
  502. 'title': 'Straight To Hell - Sabrina: Netflix',
  503. 'uploader': 'Framework Studio',
  504. 'description': 'md5:f2edc61af3ea7a5592681ddbb683db73',
  505. 'upload_date': '20200225',
  506. 'duration': 176,
  507. 'thumbnail': 'https://i.vimeocdn.com/video/859377297-836494a4ef775e9d4edbace83937d9ad34dc846c688c0c419c0e87f7ab06c4b3-d_1280',
  508. 'uploader_url': 'https://vimeo.com/frameworkla',
  509. },
  510. # 'params': {'format': 'source'},
  511. 'expected_warnings': ['Failed to parse XML: not well-formed'],
  512. },
  513. {
  514. # only available via https://vimeo.com/channels/tributes/6213729 and
  515. # not via https://vimeo.com/6213729
  516. 'url': 'https://vimeo.com/channels/tributes/6213729',
  517. 'info_dict': {
  518. 'id': '6213729',
  519. 'ext': 'mp4',
  520. 'title': 'Vimeo Tribute: The Shining',
  521. 'uploader': 'Casey Donahue',
  522. 'uploader_url': r're:https?://(?:www\.)?vimeo\.com/caseydonahue',
  523. 'uploader_id': 'caseydonahue',
  524. 'channel_url': r're:https?://(?:www\.)?vimeo\.com/channels/tributes',
  525. 'channel_id': 'tributes',
  526. 'timestamp': 1250886430,
  527. 'upload_date': '20090821',
  528. 'description': str, # FIXME: Dynamic SEO spam description
  529. 'duration': 321,
  530. 'comment_count': int,
  531. 'view_count': int,
  532. 'thumbnail': 'https://i.vimeocdn.com/video/22728298-bfc22146f930de7cf497821c7b0b9f168099201ecca39b00b6bd31fcedfca7a6-d_1280',
  533. 'like_count': int,
  534. 'tags': ['[the shining', 'vimeohq', 'cv', 'vimeo tribute]'],
  535. },
  536. 'params': {
  537. 'skip_download': True,
  538. },
  539. 'expected_warnings': ['Failed to parse XML: not well-formed'],
  540. },
  541. {
  542. # redirects to ondemand extractor and should be passed through it
  543. # for successful extraction
  544. 'url': 'https://vimeo.com/73445910',
  545. 'info_dict': {
  546. 'id': '73445910',
  547. 'ext': 'mp4',
  548. 'title': 'The Reluctant Revolutionary',
  549. 'uploader': '10Ft Films',
  550. 'uploader_url': r're:https?://(?:www\.)?vimeo\.com/tenfootfilms',
  551. 'uploader_id': 'tenfootfilms',
  552. 'description': 'md5:0fa704e05b04f91f40b7f3ca2e801384',
  553. 'upload_date': '20130830',
  554. 'timestamp': 1377853339,
  555. },
  556. 'params': {
  557. 'skip_download': True,
  558. },
  559. 'skip': 'this page is no longer available.',
  560. },
  561. {
  562. 'url': 'https://player.vimeo.com/video/68375962',
  563. 'md5': 'aaf896bdb7ddd6476df50007a0ac0ae7',
  564. 'info_dict': {
  565. 'id': '68375962',
  566. 'ext': 'mp4',
  567. 'title': 'youtube-dl password protected test video',
  568. 'uploader_url': r're:https?://(?:www\.)?vimeo\.com/user18948128',
  569. 'uploader_id': 'user18948128',
  570. 'uploader': 'Jaime Marquínez Ferrándiz',
  571. 'duration': 10,
  572. 'thumbnail': 'https://i.vimeocdn.com/video/440665496-b2c5aee2b61089442c794f64113a8e8f7d5763c3e6b3ebfaf696ae6413f8b1f4-d_1280',
  573. },
  574. 'params': {
  575. 'format': 'best[protocol=https]',
  576. 'videopassword': 'youtube-dl',
  577. },
  578. 'expected_warnings': ['Failed to parse XML: not well-formed'],
  579. },
  580. {
  581. 'url': 'http://vimeo.com/moogaloop.swf?clip_id=2539741',
  582. 'only_matching': True,
  583. },
  584. {
  585. 'url': 'https://vimeo.com/109815029',
  586. 'note': 'Video not completely processed, "failed" seed status',
  587. 'only_matching': True,
  588. },
  589. {
  590. 'url': 'https://vimeo.com/groups/travelhd/videos/22439234',
  591. 'only_matching': True,
  592. },
  593. {
  594. 'url': 'https://vimeo.com/album/2632481/video/79010983',
  595. 'only_matching': True,
  596. },
  597. {
  598. 'url': 'https://vimeo.com/showcase/3253534/video/119195465',
  599. 'note': 'A video in a password protected album (showcase)',
  600. 'info_dict': {
  601. 'id': '119195465',
  602. 'ext': 'mp4',
  603. 'title': "youtube-dl test video '' ä↭𝕐-BaW jenozKc",
  604. 'uploader': 'Philipp Hagemeister',
  605. 'uploader_id': 'user20132939',
  606. 'description': str, # FIXME: Dynamic SEO spam description
  607. 'upload_date': '20150209',
  608. 'timestamp': 1423518307,
  609. 'thumbnail': 'https://i.vimeocdn.com/video/default_1280',
  610. 'duration': 10,
  611. 'like_count': int,
  612. 'uploader_url': 'https://vimeo.com/user20132939',
  613. 'view_count': int,
  614. 'comment_count': int,
  615. },
  616. 'params': {
  617. 'format': 'best[protocol=https]',
  618. 'videopassword': 'youtube-dl',
  619. },
  620. 'expected_warnings': ['Failed to parse XML: not well-formed'],
  621. },
  622. {
  623. # source file returns 403: Forbidden
  624. 'url': 'https://vimeo.com/7809605',
  625. 'only_matching': True,
  626. },
  627. {
  628. 'note': 'Direct URL with hash',
  629. 'url': 'https://vimeo.com/160743502/abd0e13fb4',
  630. 'info_dict': {
  631. 'id': '160743502',
  632. 'ext': 'mp4',
  633. 'uploader': 'Julian Tryba',
  634. 'uploader_id': 'aliniamedia',
  635. 'title': 'Harrisville New Hampshire',
  636. 'timestamp': 1459259666,
  637. 'upload_date': '20160329',
  638. 'release_timestamp': 1459259666,
  639. 'license': 'by-nc',
  640. 'duration': 159,
  641. 'comment_count': int,
  642. 'thumbnail': 'https://i.vimeocdn.com/video/562802436-585eeb13b5020c6ac0f171a2234067938098f84737787df05ff0d767f6d54ee9-d_1280',
  643. 'like_count': int,
  644. 'uploader_url': 'https://vimeo.com/aliniamedia',
  645. 'release_date': '20160329',
  646. },
  647. 'params': {'skip_download': True},
  648. 'expected_warnings': ['Failed to parse XML: not well-formed'],
  649. },
  650. {
  651. 'url': 'https://vimeo.com/138909882',
  652. 'info_dict': {
  653. 'id': '138909882',
  654. # 'ext': 'm4v',
  655. 'ext': 'mp4',
  656. 'title': 'Eastnor Castle 2015 Firework Champions - The Promo!',
  657. 'description': 'md5:5967e090768a831488f6e74b7821b3c1',
  658. 'uploader_id': 'fireworkchampions',
  659. 'uploader': 'Firework Champions',
  660. 'upload_date': '20150910',
  661. 'timestamp': 1441901895,
  662. 'thumbnail': 'https://i.vimeocdn.com/video/534715882-6ff8e4660cbf2fea68282876d8d44f318825dfe572cc4016e73b3266eac8ae3a-d_1280',
  663. 'uploader_url': 'https://vimeo.com/fireworkchampions',
  664. 'tags': 'count:6',
  665. 'duration': 229,
  666. 'view_count': int,
  667. 'like_count': int,
  668. 'comment_count': int,
  669. },
  670. 'params': {
  671. 'skip_download': True,
  672. # 'format': 'source',
  673. },
  674. 'expected_warnings': ['Failed to parse XML: not well-formed'],
  675. },
  676. {
  677. 'url': 'https://vimeo.com/channels/staffpicks/143603739',
  678. 'info_dict': {
  679. 'id': '143603739',
  680. 'ext': 'mp4',
  681. 'uploader': 'Karim Huu Do',
  682. 'timestamp': 1445846953,
  683. 'upload_date': '20151026',
  684. 'title': 'The Shoes - Submarine Feat. Blaine Harrison',
  685. 'uploader_id': 'karimhd',
  686. 'description': 'md5:8e2eea76de4504c2e8020a9bcfa1e843',
  687. 'channel_id': 'staffpicks',
  688. 'duration': 336,
  689. 'comment_count': int,
  690. 'view_count': int,
  691. 'thumbnail': 'https://i.vimeocdn.com/video/541243181-b593db36a16db2f0096f655da3f5a4dc46b8766d77b0f440df937ecb0c418347-d_1280',
  692. 'like_count': int,
  693. 'uploader_url': 'https://vimeo.com/karimhd',
  694. 'channel_url': 'https://vimeo.com/channels/staffpicks',
  695. 'tags': 'count:6',
  696. },
  697. 'params': {'skip_download': 'm3u8'},
  698. 'expected_warnings': ['Failed to parse XML: not well-formed'],
  699. },
  700. {
  701. # requires passing unlisted_hash(a52724358e) to load_download_config request
  702. 'url': 'https://vimeo.com/392479337/a52724358e',
  703. 'only_matching': True,
  704. },
  705. {
  706. # similar, but all numeric: ID must be 581039021, not 9603038895
  707. # issue #29690
  708. 'url': 'https://vimeo.com/581039021/9603038895',
  709. 'info_dict': {
  710. 'id': '581039021',
  711. 'ext': 'mp4',
  712. 'timestamp': 1627621014,
  713. 'release_timestamp': 1627621014,
  714. 'duration': 976,
  715. 'comment_count': int,
  716. 'thumbnail': 'https://i.vimeocdn.com/video/1202249320-4ddb2c30398c0dc0ee059172d1bd5ea481ad12f0e0e3ad01d2266f56c744b015-d_1280',
  717. 'like_count': int,
  718. 'uploader_url': 'https://vimeo.com/txwestcapital',
  719. 'release_date': '20210730',
  720. 'uploader': 'Christopher Inks',
  721. 'title': 'Thursday, July 29, 2021 BMA Evening Video Update',
  722. 'uploader_id': 'txwestcapital',
  723. 'upload_date': '20210730',
  724. },
  725. 'params': {
  726. 'skip_download': True,
  727. },
  728. 'expected_warnings': ['Failed to parse XML: not well-formed'],
  729. },
  730. {
  731. # chapters must be sorted, see: https://github.com/yt-dlp/yt-dlp/issues/5308
  732. 'url': 'https://player.vimeo.com/video/756714419',
  733. 'info_dict': {
  734. 'id': '756714419',
  735. 'ext': 'mp4',
  736. 'title': 'Dr Arielle Schwartz - Therapeutic yoga for optimum sleep',
  737. 'uploader': 'Alex Howard',
  738. 'uploader_id': 'user54729178',
  739. 'uploader_url': 'https://vimeo.com/user54729178',
  740. 'thumbnail': r're:https://i\.vimeocdn\.com/video/1520099929-[\da-f]+-d_1280',
  741. 'duration': 2636,
  742. 'chapters': [
  743. {'start_time': 0, 'end_time': 10, 'title': '<Untitled Chapter 1>'},
  744. {'start_time': 10, 'end_time': 106, 'title': 'Welcoming Dr Arielle Schwartz'},
  745. {'start_time': 106, 'end_time': 305, 'title': 'What is therapeutic yoga?'},
  746. {'start_time': 305, 'end_time': 594, 'title': 'Vagal toning practices'},
  747. {'start_time': 594, 'end_time': 888, 'title': 'Trauma and difficulty letting go'},
  748. {'start_time': 888, 'end_time': 1059, 'title': "Dr Schwartz' insomnia experience"},
  749. {'start_time': 1059, 'end_time': 1471, 'title': 'A strategy for helping sleep issues'},
  750. {'start_time': 1471, 'end_time': 1667, 'title': 'Yoga nidra'},
  751. {'start_time': 1667, 'end_time': 2121, 'title': 'Wisdom in stillness'},
  752. {'start_time': 2121, 'end_time': 2386, 'title': 'What helps us be more able to let go?'},
  753. {'start_time': 2386, 'end_time': 2510, 'title': 'Practical tips to help ourselves'},
  754. {'start_time': 2510, 'end_time': 2636, 'title': 'Where to find out more'},
  755. ],
  756. },
  757. 'params': {
  758. 'http_headers': {'Referer': 'https://sleepsuperconference.com'},
  759. 'skip_download': 'm3u8',
  760. },
  761. 'expected_warnings': ['Failed to parse XML: not well-formed'],
  762. },
  763. {
  764. # vimeo.com URL with unlisted hash and Original format
  765. 'url': 'https://vimeo.com/144579403/ec02229140',
  766. # 'md5': '6b662c2884e0373183fbde2a0d15cb78',
  767. 'info_dict': {
  768. 'id': '144579403',
  769. 'ext': 'mp4',
  770. 'title': 'SALESMANSHIP',
  771. 'description': 'md5:4338302f347a1ff8841b4a3aecaa09f0',
  772. 'uploader': 'Off the Picture Pictures',
  773. 'uploader_id': 'offthepicturepictures',
  774. 'uploader_url': 'https://vimeo.com/offthepicturepictures',
  775. 'duration': 669,
  776. 'upload_date': '20151104',
  777. 'timestamp': 1446607180,
  778. 'release_date': '20151104',
  779. 'release_timestamp': 1446607180,
  780. 'like_count': int,
  781. 'view_count': int,
  782. 'comment_count': int,
  783. 'thumbnail': r're:https://i\.vimeocdn\.com/video/1018638656-[\da-f]+-d_1280',
  784. },
  785. # 'params': {'format': 'Original'},
  786. 'expected_warnings': ['Failed to parse XML: not well-formed'],
  787. },
  788. {
  789. # player.vimeo.com URL with source format
  790. 'url': 'https://player.vimeo.com/video/859028877',
  791. # 'md5': '19ca3d2463441dee2d2f0671ac2916a2',
  792. 'info_dict': {
  793. 'id': '859028877',
  794. 'ext': 'mp4',
  795. 'title': 'Ariana Grande - Honeymoon Avenue (Live from London)',
  796. 'uploader': 'Raja Virdi',
  797. 'uploader_id': 'rajavirdi',
  798. 'uploader_url': 'https://vimeo.com/rajavirdi',
  799. 'duration': 309,
  800. 'thumbnail': r're:https://i\.vimeocdn\.com/video/1716727772-[\da-f]+-d_1280',
  801. },
  802. # 'params': {'format': 'source'},
  803. 'expected_warnings': ['Failed to parse XML: not well-formed'],
  804. },
  805. {
  806. # user playlist alias -> https://vimeo.com/258705797
  807. 'url': 'https://vimeo.com/user26785108/newspiritualguide',
  808. 'only_matching': True,
  809. },
  810. # https://gettingthingsdone.com/workflowmap/
  811. # vimeo embed with check-password page protected by Referer header
  812. ]
  813. @classmethod
  814. def _extract_embed_urls(cls, url, webpage):
  815. for embed_url in super()._extract_embed_urls(url, webpage):
  816. yield cls._smuggle_referrer(embed_url, url)
  817. @classmethod
  818. def _extract_url(cls, url, webpage):
  819. return next(cls._extract_embed_urls(url, webpage), None)
  820. def _verify_player_video_password(self, url, video_id, headers):
  821. password = self._get_video_password()
  822. data = urlencode_postdata({
  823. 'password': base64.b64encode(password.encode()),
  824. })
  825. headers = merge_dicts(headers, {
  826. 'Content-Type': 'application/x-www-form-urlencoded',
  827. })
  828. checked = self._download_json(
  829. f'{urllib.parse.urlsplit(url)._replace(query=None).geturl()}/check-password',
  830. video_id, 'Verifying the password', data=data, headers=headers)
  831. if checked is False:
  832. raise ExtractorError('Wrong video password', expected=True)
  833. return checked
  834. def _extract_from_api(self, video_id, unlisted_hash=None):
  835. viewer = self._download_json(
  836. 'https://vimeo.com/_next/viewer', video_id, 'Downloading viewer info')
  837. for retry in (False, True):
  838. try:
  839. video = self._call_videos_api(video_id, viewer['jwt'], unlisted_hash)
  840. break
  841. except ExtractorError as e:
  842. if (not retry and isinstance(e.cause, HTTPError) and e.cause.status == 400
  843. and 'password' in traverse_obj(
  844. self._webpage_read_content(e.cause.response, e.cause.response.url, video_id, fatal=False),
  845. ({json.loads}, 'invalid_parameters', ..., 'field'),
  846. )):
  847. self._verify_video_password(
  848. video_id, self._get_video_password(), viewer['xsrft'])
  849. continue
  850. raise
  851. info = self._parse_config(self._download_json(
  852. video['config_url'], video_id), video_id)
  853. source_format = self._extract_original_format(
  854. f'https://vimeo.com/{video_id}', video_id, unlisted_hash, jwt=viewer['jwt'], api_data=video)
  855. if source_format:
  856. info['formats'].append(source_format)
  857. get_timestamp = lambda x: parse_iso8601(video.get(x + '_time'))
  858. info.update({
  859. 'description': video.get('description'),
  860. 'license': video.get('license'),
  861. 'release_timestamp': get_timestamp('release'),
  862. 'timestamp': get_timestamp('created'),
  863. 'view_count': int_or_none(try_get(video, lambda x: x['stats']['plays'])),
  864. })
  865. connections = try_get(
  866. video, lambda x: x['metadata']['connections'], dict) or {}
  867. for k in ('comment', 'like'):
  868. info[k + '_count'] = int_or_none(try_get(connections, lambda x: x[k + 's']['total']))
  869. return info
  870. def _try_album_password(self, url):
  871. album_id = self._search_regex(
  872. r'vimeo\.com/(?:album|showcase)/([^/]+)', url, 'album id', default=None)
  873. if not album_id:
  874. return
  875. viewer = self._download_json(
  876. 'https://vimeo.com/_rv/viewer', album_id, fatal=False)
  877. if not viewer:
  878. webpage = self._download_webpage(url, album_id)
  879. viewer = self._parse_json(self._search_regex(
  880. r'bootstrap_data\s*=\s*({.+?})</script>',
  881. webpage, 'bootstrap data'), album_id)['viewer']
  882. jwt = viewer['jwt']
  883. album = self._download_json(
  884. 'https://api.vimeo.com/albums/' + album_id,
  885. album_id, headers={'Authorization': 'jwt ' + jwt, 'Accept': 'application/json'},
  886. query={'fields': 'description,name,privacy'})
  887. if try_get(album, lambda x: x['privacy']['view']) == 'password':
  888. password = self.get_param('videopassword')
  889. if not password:
  890. raise ExtractorError(
  891. 'This album is protected by a password, use the --video-password option',
  892. expected=True)
  893. self._set_vimeo_cookie('vuid', viewer['vuid'])
  894. try:
  895. self._download_json(
  896. f'https://vimeo.com/showcase/{album_id}/auth',
  897. album_id, 'Verifying the password', data=urlencode_postdata({
  898. 'password': password,
  899. 'token': viewer['xsrft'],
  900. }), headers={
  901. 'X-Requested-With': 'XMLHttpRequest',
  902. })
  903. except ExtractorError as e:
  904. if isinstance(e.cause, HTTPError) and e.cause.status == 401:
  905. raise ExtractorError('Wrong password', expected=True)
  906. raise
  907. def _real_extract(self, url):
  908. url, data, headers = self._unsmuggle_headers(url)
  909. if 'Referer' not in headers:
  910. headers['Referer'] = url
  911. # Extract ID from URL
  912. mobj = self._match_valid_url(url).groupdict()
  913. video_id, unlisted_hash = mobj['id'], mobj.get('unlisted_hash')
  914. if unlisted_hash:
  915. return self._extract_from_api(video_id, unlisted_hash)
  916. if any(p in url for p in ('play_redirect_hls', 'moogaloop.swf')):
  917. url = 'https://vimeo.com/' + video_id
  918. self._try_album_password(url)
  919. is_secure = urllib.parse.urlparse(url).scheme == 'https'
  920. try:
  921. # Retrieve video webpage to extract further information
  922. webpage, urlh = self._download_webpage_handle(
  923. url, video_id, headers=headers, impersonate=is_secure)
  924. redirect_url = urlh.url
  925. except ExtractorError as error:
  926. if not isinstance(error.cause, HTTPError) or error.cause.status not in (403, 429):
  927. raise
  928. errmsg = error.cause.response.read()
  929. if b'Because of its privacy settings, this video cannot be played here' in errmsg:
  930. raise ExtractorError(
  931. 'Cannot download embed-only video without embedding URL. Please call yt-dlp '
  932. 'with the URL of the page that embeds this video.', expected=True)
  933. # 403 == vimeo.com TLS fingerprint or DC IP block; 429 == player.vimeo.com TLS FP block
  934. status = error.cause.status
  935. dcip_msg = 'If you are using a data center IP or VPN/proxy, your IP may be blocked'
  936. if target := error.cause.response.extensions.get('impersonate'):
  937. raise ExtractorError(
  938. f'Got HTTP Error {status} when using impersonate target "{target}". {dcip_msg}')
  939. elif not is_secure:
  940. raise ExtractorError(f'Got HTTP Error {status}. {dcip_msg}', expected=True)
  941. raise ExtractorError(
  942. 'This request has been blocked due to its TLS fingerprint. Install a '
  943. 'required impersonation dependency if possible, or else if you are okay with '
  944. f'{self._downloader._format_err("compromising your security/cookies", "light red")}, '
  945. f'try replacing "https:" with "http:" in the input URL. {dcip_msg}.', expected=True)
  946. if '://player.vimeo.com/video/' in url:
  947. config = self._search_json(
  948. r'\b(?:playerC|c)onfig\s*=', webpage, 'info section', video_id)
  949. if config.get('view') == 4:
  950. config = self._verify_player_video_password(
  951. redirect_url, video_id, headers)
  952. info = self._parse_config(config, video_id)
  953. source_format = self._extract_original_format(
  954. f'https://vimeo.com/{video_id}', video_id, unlisted_hash)
  955. if source_format:
  956. info['formats'].append(source_format)
  957. return info
  958. vimeo_config = self._extract_vimeo_config(webpage, video_id, default=None)
  959. if vimeo_config:
  960. seed_status = vimeo_config.get('seed_status') or {}
  961. if seed_status.get('state') == 'failed':
  962. raise ExtractorError(
  963. '{} said: {}'.format(self.IE_NAME, seed_status['title']),
  964. expected=True)
  965. cc_license = None
  966. timestamp = None
  967. video_description = None
  968. info_dict = {}
  969. config_url = None
  970. channel_id = self._search_regex(
  971. r'vimeo\.com/channels/([^/]+)', url, 'channel id', default=None)
  972. if channel_id:
  973. config_url = self._html_search_regex(
  974. r'\bdata-config-url="([^"]+)"', webpage, 'config URL', default=None)
  975. video_description = clean_html(get_element_by_class('description', webpage))
  976. info_dict.update({
  977. 'channel_id': channel_id,
  978. 'channel_url': 'https://vimeo.com/channels/' + channel_id,
  979. })
  980. if not config_url:
  981. page_config = self._parse_json(self._search_regex(
  982. r'vimeo\.(?:clip|vod_title)_page_config\s*=\s*({.+?});',
  983. webpage, 'page config', default='{}'), video_id, fatal=False)
  984. if not page_config:
  985. return self._extract_from_api(video_id)
  986. config_url = page_config['player']['config_url']
  987. cc_license = page_config.get('cc_license')
  988. clip = page_config.get('clip') or {}
  989. timestamp = clip.get('uploaded_on')
  990. video_description = clean_html(
  991. clip.get('description') or page_config.get('description_html_escaped'))
  992. config = self._download_json(config_url, video_id)
  993. video = config.get('video') or {}
  994. vod = video.get('vod') or {}
  995. def is_rented():
  996. if '>You rented this title.<' in webpage:
  997. return True
  998. if try_get(config, lambda x: x['user']['purchased']):
  999. return True
  1000. for purchase_option in (vod.get('purchase_options') or []):
  1001. if purchase_option.get('purchased'):
  1002. return True
  1003. label = purchase_option.get('label_string')
  1004. if label and (label.startswith('You rented this') or label.endswith(' remaining')):
  1005. return True
  1006. return False
  1007. if is_rented() and vod.get('is_trailer'):
  1008. feature_id = vod.get('feature_id')
  1009. if feature_id and not data.get('force_feature_id', False):
  1010. return self.url_result(smuggle_url(
  1011. f'https://player.vimeo.com/player/{feature_id}',
  1012. {'force_feature_id': True}), 'Vimeo')
  1013. if not video_description:
  1014. video_description = self._html_search_regex(
  1015. r'(?s)<div\s+class="[^"]*description[^"]*"[^>]*>(.*?)</div>',
  1016. webpage, 'description', default=None)
  1017. if not video_description:
  1018. video_description = self._html_search_meta(
  1019. ['description', 'og:description', 'twitter:description'],
  1020. webpage, default=None)
  1021. if not video_description:
  1022. self.report_warning('Cannot find video description')
  1023. if not timestamp:
  1024. timestamp = self._search_regex(
  1025. r'<time[^>]+datetime="([^"]+)"', webpage,
  1026. 'timestamp', default=None)
  1027. view_count = int_or_none(self._search_regex(r'UserPlays:(\d+)', webpage, 'view count', default=None))
  1028. like_count = int_or_none(self._search_regex(r'UserLikes:(\d+)', webpage, 'like count', default=None))
  1029. comment_count = int_or_none(self._search_regex(r'UserComments:(\d+)', webpage, 'comment count', default=None))
  1030. formats = []
  1031. source_format = self._extract_original_format(
  1032. 'https://vimeo.com/' + video_id, video_id, video.get('unlisted_hash'))
  1033. if source_format:
  1034. formats.append(source_format)
  1035. info_dict_config = self._parse_config(config, video_id)
  1036. formats.extend(info_dict_config['formats'])
  1037. info_dict['_format_sort_fields'] = info_dict_config['_format_sort_fields']
  1038. json_ld = self._search_json_ld(webpage, video_id, default={})
  1039. if not cc_license:
  1040. cc_license = self._search_regex(
  1041. r'<link[^>]+rel=["\']license["\'][^>]+href=(["\'])(?P<license>(?:(?!\1).)+)\1',
  1042. webpage, 'license', default=None, group='license')
  1043. info_dict.update({
  1044. 'formats': formats,
  1045. 'timestamp': unified_timestamp(timestamp),
  1046. 'description': video_description,
  1047. 'webpage_url': url,
  1048. 'view_count': view_count,
  1049. 'like_count': like_count,
  1050. 'comment_count': comment_count,
  1051. 'license': cc_license,
  1052. })
  1053. return merge_dicts(info_dict, info_dict_config, json_ld)
  1054. class VimeoOndemandIE(VimeoIE): # XXX: Do not subclass from concrete IE
  1055. IE_NAME = 'vimeo:ondemand'
  1056. _VALID_URL = r'https?://(?:www\.)?vimeo\.com/ondemand/(?:[^/]+/)?(?P<id>[^/?#&]+)'
  1057. _TESTS = [{
  1058. # ondemand video not available via https://vimeo.com/id
  1059. 'url': 'https://vimeo.com/ondemand/20704',
  1060. 'md5': 'c424deda8c7f73c1dfb3edd7630e2f35',
  1061. 'info_dict': {
  1062. 'id': '105442900',
  1063. 'ext': 'mp4',
  1064. 'title': 'המעבדה - במאי יותם פלדמן',
  1065. 'uploader': 'גם סרטים',
  1066. 'uploader_url': r're:https?://(?:www\.)?vimeo\.com/gumfilms',
  1067. 'uploader_id': 'gumfilms',
  1068. 'description': 'md5:aeeba3dbd4d04b0fa98a4fdc9c639998',
  1069. 'upload_date': '20140906',
  1070. 'timestamp': 1410032453,
  1071. 'thumbnail': 'https://i.vimeocdn.com/video/488238335-d7bf151c364cff8d467f1b73784668fe60aae28a54573a35d53a1210ae283bd8-d_1280',
  1072. 'comment_count': int,
  1073. 'license': 'https://creativecommons.org/licenses/by-nc-nd/3.0/',
  1074. 'duration': 53,
  1075. 'view_count': int,
  1076. 'like_count': int,
  1077. },
  1078. 'params': {
  1079. 'format': 'best[protocol=https]',
  1080. },
  1081. 'expected_warnings': ['Unable to download JSON metadata'],
  1082. }, {
  1083. # requires Referer to be passed along with og:video:url
  1084. 'url': 'https://vimeo.com/ondemand/36938/126682985',
  1085. 'info_dict': {
  1086. 'id': '126584684',
  1087. 'ext': 'mp4',
  1088. 'title': 'Rävlock, rätt läte på rätt plats',
  1089. 'uploader': 'Lindroth & Norin',
  1090. 'uploader_url': r're:https?://(?:www\.)?vimeo\.com/lindrothnorin',
  1091. 'uploader_id': 'lindrothnorin',
  1092. 'description': 'md5:c3c46a90529612c8279fb6af803fc0df',
  1093. 'upload_date': '20150502',
  1094. 'timestamp': 1430586422,
  1095. 'duration': 121,
  1096. 'comment_count': int,
  1097. 'view_count': int,
  1098. 'thumbnail': 'https://i.vimeocdn.com/video/517077723-7066ae1d9a79d3eb361334fb5d58ec13c8f04b52f8dd5eadfbd6fb0bcf11f613-d_1280',
  1099. 'like_count': int,
  1100. },
  1101. 'params': {
  1102. 'skip_download': True,
  1103. },
  1104. 'expected_warnings': ['Unable to download JSON metadata'],
  1105. }, {
  1106. 'url': 'https://vimeo.com/ondemand/nazmaalik',
  1107. 'only_matching': True,
  1108. }, {
  1109. 'url': 'https://vimeo.com/ondemand/141692381',
  1110. 'only_matching': True,
  1111. }, {
  1112. 'url': 'https://vimeo.com/ondemand/thelastcolony/150274832',
  1113. 'only_matching': True,
  1114. }]
  1115. class VimeoChannelIE(VimeoBaseInfoExtractor):
  1116. IE_NAME = 'vimeo:channel'
  1117. _VALID_URL = r'https://vimeo\.com/channels/(?P<id>[^/?#]+)/?(?:$|[?#])'
  1118. _MORE_PAGES_INDICATOR = r'<a.+?rel="next"'
  1119. _TITLE = None
  1120. _TITLE_RE = r'<link rel="alternate"[^>]+?title="(.*?)"'
  1121. _TESTS = [{
  1122. 'url': 'https://vimeo.com/channels/tributes',
  1123. 'info_dict': {
  1124. 'id': 'tributes',
  1125. 'title': 'Vimeo Tributes',
  1126. },
  1127. 'playlist_mincount': 22,
  1128. }]
  1129. _BASE_URL_TEMPL = 'https://vimeo.com/channels/%s'
  1130. def _page_url(self, base_url, pagenum):
  1131. return f'{base_url}/videos/page:{pagenum}/'
  1132. def _extract_list_title(self, webpage):
  1133. return self._TITLE or self._html_search_regex(
  1134. self._TITLE_RE, webpage, 'list title', fatal=False)
  1135. def _title_and_entries(self, list_id, base_url):
  1136. for pagenum in itertools.count(1):
  1137. page_url = self._page_url(base_url, pagenum)
  1138. webpage = self._download_webpage(
  1139. page_url, list_id,
  1140. f'Downloading page {pagenum}')
  1141. if pagenum == 1:
  1142. yield self._extract_list_title(webpage)
  1143. # Try extracting href first since not all videos are available via
  1144. # short https://vimeo.com/id URL (e.g. https://vimeo.com/channels/tributes/6213729)
  1145. clips = re.findall(
  1146. r'id="clip_(\d+)"[^>]*>\s*<a[^>]+href="(/(?:[^/]+/)*\1)(?:[^>]+\btitle="([^"]+)")?', webpage)
  1147. if clips:
  1148. for video_id, video_url, video_title in clips:
  1149. yield self.url_result(
  1150. urllib.parse.urljoin(base_url, video_url),
  1151. VimeoIE.ie_key(), video_id=video_id, video_title=video_title)
  1152. # More relaxed fallback
  1153. else:
  1154. for video_id in re.findall(r'id=["\']clip_(\d+)', webpage):
  1155. yield self.url_result(
  1156. f'https://vimeo.com/{video_id}',
  1157. VimeoIE.ie_key(), video_id=video_id)
  1158. if re.search(self._MORE_PAGES_INDICATOR, webpage, re.DOTALL) is None:
  1159. break
  1160. def _extract_videos(self, list_id, base_url):
  1161. title_and_entries = self._title_and_entries(list_id, base_url)
  1162. list_title = next(title_and_entries)
  1163. return self.playlist_result(title_and_entries, list_id, list_title)
  1164. def _real_extract(self, url):
  1165. channel_id = self._match_id(url)
  1166. return self._extract_videos(channel_id, self._BASE_URL_TEMPL % channel_id)
  1167. class VimeoUserIE(VimeoChannelIE): # XXX: Do not subclass from concrete IE
  1168. IE_NAME = 'vimeo:user'
  1169. _VALID_URL = r'https://vimeo\.com/(?!(?:[0-9]+|watchlater)(?:$|[?#/]))(?P<id>[^/]+)(?:/videos)?/?(?:$|[?#])'
  1170. _TITLE_RE = r'<a[^>]+?class="user">([^<>]+?)</a>'
  1171. _TESTS = [{
  1172. 'url': 'https://vimeo.com/nkistudio/videos',
  1173. 'info_dict': {
  1174. 'title': 'Nki',
  1175. 'id': 'nkistudio',
  1176. },
  1177. 'playlist_mincount': 66,
  1178. }, {
  1179. 'url': 'https://vimeo.com/nkistudio/',
  1180. 'only_matching': True,
  1181. }]
  1182. _BASE_URL_TEMPL = 'https://vimeo.com/%s'
  1183. class VimeoAlbumIE(VimeoBaseInfoExtractor):
  1184. IE_NAME = 'vimeo:album'
  1185. _VALID_URL = r'https://vimeo\.com/(?:album|showcase)/(?P<id>\d+)(?:$|[?#]|/(?!video))'
  1186. _TITLE_RE = r'<header id="page_header">\n\s*<h1>(.*?)</h1>'
  1187. _TESTS = [{
  1188. 'url': 'https://vimeo.com/album/2632481',
  1189. 'info_dict': {
  1190. 'id': '2632481',
  1191. 'title': 'Staff Favorites: November 2013',
  1192. },
  1193. 'playlist_mincount': 13,
  1194. }, {
  1195. 'note': 'Password-protected album',
  1196. 'url': 'https://vimeo.com/album/3253534',
  1197. 'info_dict': {
  1198. 'title': 'test',
  1199. 'id': '3253534',
  1200. },
  1201. 'playlist_count': 1,
  1202. 'params': {
  1203. 'videopassword': 'youtube-dl',
  1204. },
  1205. }]
  1206. _PAGE_SIZE = 100
  1207. def _fetch_page(self, album_id, authorization, hashed_pass, page):
  1208. api_page = page + 1
  1209. query = {
  1210. 'fields': 'link,uri',
  1211. 'page': api_page,
  1212. 'per_page': self._PAGE_SIZE,
  1213. }
  1214. if hashed_pass:
  1215. query['_hashed_pass'] = hashed_pass
  1216. try:
  1217. videos = self._download_json(
  1218. f'https://api.vimeo.com/albums/{album_id}/videos',
  1219. album_id, f'Downloading page {api_page}', query=query, headers={
  1220. 'Authorization': 'jwt ' + authorization,
  1221. 'Accept': 'application/json',
  1222. })['data']
  1223. except ExtractorError as e:
  1224. if isinstance(e.cause, HTTPError) and e.cause.status == 400:
  1225. return
  1226. raise
  1227. for video in videos:
  1228. link = video.get('link')
  1229. if not link:
  1230. continue
  1231. uri = video.get('uri')
  1232. video_id = self._search_regex(r'/videos/(\d+)', uri, 'video_id', default=None) if uri else None
  1233. yield self.url_result(link, VimeoIE.ie_key(), video_id)
  1234. def _real_extract(self, url):
  1235. album_id = self._match_id(url)
  1236. viewer = self._download_json(
  1237. 'https://vimeo.com/_rv/viewer', album_id, fatal=False)
  1238. if not viewer:
  1239. webpage = self._download_webpage(url, album_id)
  1240. viewer = self._parse_json(self._search_regex(
  1241. r'bootstrap_data\s*=\s*({.+?})</script>',
  1242. webpage, 'bootstrap data'), album_id)['viewer']
  1243. jwt = viewer['jwt']
  1244. album = self._download_json(
  1245. 'https://api.vimeo.com/albums/' + album_id,
  1246. album_id, headers={'Authorization': 'jwt ' + jwt, 'Accept': 'application/json'},
  1247. query={'fields': 'description,name,privacy'})
  1248. hashed_pass = None
  1249. if try_get(album, lambda x: x['privacy']['view']) == 'password':
  1250. password = self.get_param('videopassword')
  1251. if not password:
  1252. raise ExtractorError(
  1253. 'This album is protected by a password, use the --video-password option',
  1254. expected=True)
  1255. self._set_vimeo_cookie('vuid', viewer['vuid'])
  1256. try:
  1257. hashed_pass = self._download_json(
  1258. f'https://vimeo.com/showcase/{album_id}/auth',
  1259. album_id, 'Verifying the password', data=urlencode_postdata({
  1260. 'password': password,
  1261. 'token': viewer['xsrft'],
  1262. }), headers={
  1263. 'X-Requested-With': 'XMLHttpRequest',
  1264. })['hashed_pass']
  1265. except ExtractorError as e:
  1266. if isinstance(e.cause, HTTPError) and e.cause.status == 401:
  1267. raise ExtractorError('Wrong password', expected=True)
  1268. raise
  1269. entries = OnDemandPagedList(functools.partial(
  1270. self._fetch_page, album_id, jwt, hashed_pass), self._PAGE_SIZE)
  1271. return self.playlist_result(
  1272. entries, album_id, album.get('name'), album.get('description'))
  1273. class VimeoGroupsIE(VimeoChannelIE): # XXX: Do not subclass from concrete IE
  1274. IE_NAME = 'vimeo:group'
  1275. _VALID_URL = r'https://vimeo\.com/groups/(?P<id>[^/]+)(?:/(?!videos?/\d+)|$)'
  1276. _TESTS = [{
  1277. 'url': 'https://vimeo.com/groups/meetup',
  1278. 'info_dict': {
  1279. 'id': 'meetup',
  1280. 'title': 'Vimeo Meetup!',
  1281. },
  1282. 'playlist_mincount': 27,
  1283. }]
  1284. _BASE_URL_TEMPL = 'https://vimeo.com/groups/%s'
  1285. class VimeoReviewIE(VimeoBaseInfoExtractor):
  1286. IE_NAME = 'vimeo:review'
  1287. IE_DESC = 'Review pages on vimeo'
  1288. _VALID_URL = r'https?://vimeo\.com/(?P<user>[^/?#]+)/review/(?P<id>\d+)/(?P<hash>[\da-f]{10})'
  1289. _TESTS = [{
  1290. 'url': 'https://vimeo.com/user170863801/review/996447483/a316d6ed8d',
  1291. 'info_dict': {
  1292. 'id': '996447483',
  1293. 'ext': 'mp4',
  1294. 'title': 'Rodeo day 1-_2',
  1295. 'uploader': 'BROADKAST',
  1296. 'uploader_id': 'user170863801',
  1297. 'uploader_url': 'https://vimeo.com/user170863801',
  1298. 'duration': 30,
  1299. 'thumbnail': 'https://i.vimeocdn.com/video/1912612821-09a43bd2e75c203d503aed89de7534f28fc4474a48f59c51999716931a246af5-d_1280',
  1300. },
  1301. 'params': {'skip_download': 'm3u8'},
  1302. 'expected_warnings': ['Failed to parse XML'],
  1303. }, {
  1304. 'url': 'https://vimeo.com/user21297594/review/75524534/3c257a1b5d',
  1305. 'md5': 'c507a72f780cacc12b2248bb4006d253',
  1306. 'info_dict': {
  1307. 'id': '75524534',
  1308. 'ext': 'mp4',
  1309. 'title': "DICK HARDWICK 'Comedian'",
  1310. 'uploader': 'Richard Hardwick',
  1311. 'uploader_id': 'user21297594',
  1312. 'description': "Comedian Dick Hardwick's five minute demo filmed in front of a live theater audience.\nEdit by Doug Mattocks",
  1313. 'duration': 304,
  1314. 'thumbnail': 'https://i.vimeocdn.com/video/450115033-43303819d9ebe24c2630352e18b7056d25197d09b3ae901abdac4c4f1d68de71-d_1280',
  1315. 'uploader_url': 'https://vimeo.com/user21297594',
  1316. },
  1317. 'skip': '404 Not Found',
  1318. }, {
  1319. 'note': 'video player needs Referer',
  1320. 'url': 'https://vimeo.com/user22258446/review/91613211/13f927e053',
  1321. 'md5': '6295fdab8f4bf6a002d058b2c6dce276',
  1322. 'info_dict': {
  1323. 'id': '91613211',
  1324. 'ext': 'mp4',
  1325. 'title': 're:(?i)^Death by dogma versus assembling agile . Sander Hoogendoorn',
  1326. 'uploader': 'DevWeek Events',
  1327. 'duration': 2773,
  1328. 'thumbnail': r're:^https?://.*\.jpg$',
  1329. 'uploader_id': 'user22258446',
  1330. },
  1331. 'skip': 'video gone',
  1332. }, {
  1333. 'note': 'Password protected',
  1334. 'url': 'https://vimeo.com/user37284429/review/138823582/c4d865efde',
  1335. 'info_dict': {
  1336. 'id': '138823582',
  1337. 'ext': 'mp4',
  1338. 'title': 'EFFICIENT PICKUP MASTERCLASS MODULE 1',
  1339. 'uploader': 'TMB',
  1340. 'uploader_id': 'user37284429',
  1341. },
  1342. 'params': {
  1343. 'videopassword': 'holygrail',
  1344. },
  1345. 'skip': 'video gone',
  1346. }]
  1347. def _real_extract(self, url):
  1348. user, video_id, review_hash = self._match_valid_url(url).group('user', 'id', 'hash')
  1349. data_url = f'https://vimeo.com/{user}/review/data/{video_id}/{review_hash}'
  1350. data = self._download_json(data_url, video_id)
  1351. viewer = {}
  1352. if data.get('isLocked') is True:
  1353. video_password = self._get_video_password()
  1354. viewer = self._download_json(
  1355. 'https://vimeo.com/_rv/viewer', video_id)
  1356. self._verify_video_password(video_id, video_password, viewer['xsrft'])
  1357. data = self._download_json(data_url, video_id)
  1358. clip_data = data['clipData']
  1359. config_url = clip_data['configUrl']
  1360. config = self._download_json(config_url, video_id)
  1361. info_dict = self._parse_config(config, video_id)
  1362. source_format = self._extract_original_format(
  1363. f'https://vimeo.com/{user}/review/{video_id}/{review_hash}/action',
  1364. video_id, unlisted_hash=clip_data.get('unlistedHash'), jwt=viewer.get('jwt'))
  1365. if source_format:
  1366. info_dict['formats'].append(source_format)
  1367. info_dict['description'] = clean_html(clip_data.get('description'))
  1368. return info_dict
  1369. class VimeoWatchLaterIE(VimeoChannelIE): # XXX: Do not subclass from concrete IE
  1370. IE_NAME = 'vimeo:watchlater'
  1371. IE_DESC = 'Vimeo watch later list, ":vimeowatchlater" keyword (requires authentication)'
  1372. _VALID_URL = r'https://vimeo\.com/(?:home/)?watchlater|:vimeowatchlater'
  1373. _TITLE = 'Watch Later'
  1374. _LOGIN_REQUIRED = True
  1375. _TESTS = [{
  1376. 'url': 'https://vimeo.com/watchlater',
  1377. 'only_matching': True,
  1378. }]
  1379. def _page_url(self, base_url, pagenum):
  1380. url = f'{base_url}/page:{pagenum}/'
  1381. request = Request(url)
  1382. # Set the header to get a partial html page with the ids,
  1383. # the normal page doesn't contain them.
  1384. request.headers['X-Requested-With'] = 'XMLHttpRequest'
  1385. return request
  1386. def _real_extract(self, url):
  1387. return self._extract_videos('watchlater', 'https://vimeo.com/watchlater')
  1388. class VimeoLikesIE(VimeoChannelIE): # XXX: Do not subclass from concrete IE
  1389. _VALID_URL = r'https://(?:www\.)?vimeo\.com/(?P<id>[^/]+)/likes/?(?:$|[?#]|sort:)'
  1390. IE_NAME = 'vimeo:likes'
  1391. IE_DESC = 'Vimeo user likes'
  1392. _TESTS = [{
  1393. 'url': 'https://vimeo.com/user755559/likes/',
  1394. 'playlist_mincount': 293,
  1395. 'info_dict': {
  1396. 'id': 'user755559',
  1397. 'title': 'urza’s Likes',
  1398. },
  1399. }, {
  1400. 'url': 'https://vimeo.com/stormlapse/likes',
  1401. 'only_matching': True,
  1402. }]
  1403. def _page_url(self, base_url, pagenum):
  1404. return f'{base_url}/page:{pagenum}/'
  1405. def _real_extract(self, url):
  1406. user_id = self._match_id(url)
  1407. return self._extract_videos(user_id, f'https://vimeo.com/{user_id}/likes')
  1408. class VHXEmbedIE(VimeoBaseInfoExtractor):
  1409. IE_NAME = 'vhx:embed'
  1410. _VALID_URL = r'https?://embed\.vhx\.tv/videos/(?P<id>\d+)'
  1411. _EMBED_REGEX = [r'<iframe[^>]+src="(?P<url>https?://embed\.vhx\.tv/videos/\d+[^"]*)"']
  1412. @classmethod
  1413. def _extract_embed_urls(cls, url, webpage):
  1414. for embed_url in super()._extract_embed_urls(url, webpage):
  1415. yield cls._smuggle_referrer(embed_url, url)
  1416. def _real_extract(self, url):
  1417. video_id = self._match_id(url)
  1418. url, _, headers = self._unsmuggle_headers(url)
  1419. webpage = self._download_webpage(url, video_id, headers=headers)
  1420. config_url = self._parse_json(self._search_regex(
  1421. r'window\.OTTData\s*=\s*({.+})', webpage,
  1422. 'ott data'), video_id, js_to_json)['config_url']
  1423. config = self._download_json(config_url, video_id)
  1424. info = self._parse_config(config, video_id)
  1425. info['id'] = video_id
  1426. return info
  1427. class VimeoProIE(VimeoBaseInfoExtractor):
  1428. IE_NAME = 'vimeo:pro'
  1429. _VALID_URL = r'https?://(?:www\.)?vimeopro\.com/[^/?#]+/(?P<slug>[^/?#]+)(?:(?:/videos?/(?P<id>[0-9]+)))?'
  1430. _TESTS = [{
  1431. # Vimeo URL derived from video_id
  1432. 'url': 'http://vimeopro.com/openstreetmapus/state-of-the-map-us-2013/video/68093876',
  1433. 'md5': '3b5ca6aa22b60dfeeadf50b72e44ed82',
  1434. 'note': 'Vimeo Pro video (#1197)',
  1435. 'info_dict': {
  1436. 'id': '68093876',
  1437. 'ext': 'mp4',
  1438. 'uploader_url': r're:https?://(?:www\.)?vimeo\.com/openstreetmapus',
  1439. 'uploader_id': 'openstreetmapus',
  1440. 'uploader': 'OpenStreetMap US',
  1441. 'title': 'Andy Allan - Putting the Carto into OpenStreetMap Cartography',
  1442. 'description': 'md5:2c362968038d4499f4d79f88458590c1',
  1443. 'duration': 1595,
  1444. 'upload_date': '20130610',
  1445. 'timestamp': 1370893156,
  1446. 'license': 'by',
  1447. 'thumbnail': 'https://i.vimeocdn.com/video/440260469-19b0d92fca3bd84066623b53f1eb8aaa3980c6c809e2d67b6b39ab7b4a77a344-d_960',
  1448. 'view_count': int,
  1449. 'comment_count': int,
  1450. 'like_count': int,
  1451. 'tags': 'count:1',
  1452. },
  1453. 'params': {
  1454. 'format': 'best[protocol=https]',
  1455. },
  1456. }, {
  1457. # password-protected VimeoPro page with Vimeo player embed
  1458. 'url': 'https://vimeopro.com/cadfem/simulation-conference-mechanische-systeme-in-perfektion',
  1459. 'info_dict': {
  1460. 'id': '764543723',
  1461. 'ext': 'mp4',
  1462. 'title': 'Mechanische Systeme in Perfektion: Realität erfassen, Innovation treiben',
  1463. 'thumbnail': 'https://i.vimeocdn.com/video/1543784598-a1a750494a485e601110136b9fe11e28c2131942452b3a5d30391cb3800ca8fd-d_1280',
  1464. 'description': 'md5:2a9d195cd1b0f6f79827107dc88c2420',
  1465. 'uploader': 'CADFEM',
  1466. 'uploader_id': 'cadfem',
  1467. 'uploader_url': 'https://vimeo.com/cadfem',
  1468. 'duration': 12505,
  1469. 'chapters': 'count:10',
  1470. },
  1471. 'params': {
  1472. 'videopassword': 'Conference2022',
  1473. 'skip_download': True,
  1474. },
  1475. }]
  1476. def _real_extract(self, url):
  1477. display_id, video_id = self._match_valid_url(url).group('slug', 'id')
  1478. if video_id:
  1479. display_id = video_id
  1480. webpage = self._download_webpage(url, display_id)
  1481. password_form = self._search_regex(
  1482. r'(?is)<form[^>]+?method=["\']post["\'][^>]*>(.+?password.+?)</form>',
  1483. webpage, 'password form', default=None)
  1484. if password_form:
  1485. try:
  1486. webpage = self._download_webpage(url, display_id, data=urlencode_postdata({
  1487. 'password': self._get_video_password(),
  1488. **self._hidden_inputs(password_form),
  1489. }), note='Logging in with video password')
  1490. except ExtractorError as e:
  1491. if isinstance(e.cause, HTTPError) and e.cause.status == 418:
  1492. raise ExtractorError('Wrong video password', expected=True)
  1493. raise
  1494. description = None
  1495. # even if we have video_id, some videos require player URL with portfolio_id query param
  1496. # https://github.com/ytdl-org/youtube-dl/issues/20070
  1497. vimeo_url = VimeoIE._extract_url(url, webpage)
  1498. if vimeo_url:
  1499. description = self._html_search_meta('description', webpage, default=None)
  1500. elif video_id:
  1501. vimeo_url = f'https://vimeo.com/{video_id}'
  1502. else:
  1503. raise ExtractorError(
  1504. 'No Vimeo embed or video ID could be found in VimeoPro page', expected=True)
  1505. return self.url_result(vimeo_url, VimeoIE, video_id, url_transparent=True,
  1506. description=description)