Vimeo Download Button

Adds a download button to the HTML5 Vimeo Player (embeded or not)

  1. // ==UserScript==
  2. // @name Vimeo Download Button
  3. // @namespace larochematthias
  4. // @version 1.2.1
  5. // @description Adds a download button to the HTML5 Vimeo Player (embeded or not)
  6. // @author Matthias Laroche
  7. // @license https://creativecommons.org/licenses/by/4.0/
  8. // @include *//vimeo.com/*
  9. // @include *//player.vimeo.com/video/*
  10. // @run-at document-start
  11. // @grant none
  12. // ==/UserScript==
  13. (function() {
  14. 'use strict';
  15.  
  16. // SVG icon of the download button
  17. // Icon made by Elegant Themes from www.flaticon.com
  18. // Icon pack: http://www.flaticon.com/packs/elegant-font
  19. // Published by: https://www.elegantthemes.com/
  20. // License: https://creativecommons.org/licenses/by/3.0/
  21. var downloadIcon =
  22. '<svg viewBox="0 0 455.992 455.992" width="14px" height="14px">' +
  23. '<polygon class="fill" points="227.996,334.394 379.993,182.397 288.795,182.397 288.795,0 167.197,0 167.744,182.397 75.999,182.397" />' +
  24. '<polygon class="fill" points="349.594,334.394 349.594,395.193 106.398,395.193 106.398,334.394 45.599,334.394 45.599,395.193 45.599,455.992 410.393,455.992 410.393,334.394" />' +
  25. '</svg>';
  26.  
  27. function updateButton(button, link) {
  28. if (!link) {
  29. button.div.style.visibility = 'hidden';
  30. return;
  31. }
  32. button.a.setAttribute('href', link.url);
  33. button.a.setAttribute('download', (link.title || '').replace(/[\x00-\x1F"*\/:<>?\\|]+/g, ''));
  34. button.a.setAttribute('title', 'Download ' + link.quality + "\n" + link.title);
  35. button.div.style.visibility = 'visible';
  36. button.div.style.display = 'block';
  37. }
  38.  
  39. // Create HTML div element to add to the controls bar
  40. function createButton(document) {
  41. var div = document.createElement('div');
  42. div.style.marginLeft = '7px';
  43. div.style.marginTop = '-1px';
  44. div.style.display = 'none';
  45. div.setAttribute('class', 'download');
  46. var a = document.createElement('a');
  47. a.setAttribute('target', '_blank');
  48. a.setAttribute('aria-label', 'Download');
  49. a.setAttribute('referrerpolicy', 'origin');
  50. a.style.outlineStyle = 'none';
  51. a.innerHTML = downloadIcon;
  52. div.appendChild(a);
  53. return {
  54. div: div,
  55. a: a
  56. };
  57. }
  58.  
  59. // Syntactic sugar for getting a single node with XPath
  60. function getSingleNode(node, xpath) {
  61. var doc = node.ownerDocument || node;
  62. return doc.evaluate(xpath, node, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue;
  63. }
  64.  
  65. // Syntactic sugar for XMLHttpRequest
  66. function ajax(url, postData, onSuccess, onError) {
  67. var xhr = new XMLHttpRequest();
  68. xhr.open(postData ? 'POST' : 'GET', url);
  69. if (onSuccess || onError) {
  70. xhr.onreadystatechange = function() {
  71. if (xhr.readyState != 4) return;
  72. if (xhr.status == 200 && onSuccess) onSuccess(xhr);
  73. if (xhr.status != 200 && onError) onError(xhr);
  74. };
  75. }
  76. xhr.send(postData || null);
  77. }
  78.  
  79.  
  80. // Get url, quality and title of the video from the config of the player
  81. // and then update the download button accordingly
  82. function updateVideoLink(button, config) {
  83. if (config && typeof(config) === 'object') {
  84. var title = config && config.video && config.video.title || '';
  85. var progressive = config && config.request && config.request.files && config.request.files.progressive;
  86. if (progressive) {
  87. var file = progressive.reduce(function(a, b) {
  88. return (b.width || 0) > a.width ? b : a;
  89. }, {
  90. width: -1
  91. });
  92. if (file.url) {
  93. updateButton(button, {
  94. url: file.url,
  95. title: title,
  96. quality: file.quality
  97. });
  98. return;
  99. }
  100. }
  101. }
  102. updateButton(button, null);
  103. if (config && typeof(config) === 'string') {
  104. // config is an URL -> download with ajax and update again with the result
  105. ajax(config, null, function(xhr) {
  106. updateVideoLink(button, JSON.parse(xhr.responseText));
  107. });
  108. }
  109. }
  110.  
  111. // Called by the MutationObserver, keep adding the download button to the DOM if it disappears
  112. function showVideoLink(button, controls) {
  113. var playBar = getSingleNode(controls, "//div[contains(@class, 'play-bar')]");
  114. if (playBar && (button.div.parentNode != playBar || button.div.nextSibling)) {
  115. playBar.appendChild(button.div);
  116. }
  117. }
  118.  
  119. // Create a link and update it with the config of the player,
  120. // set a MutationObserver to add the download button when the controls are ready
  121. // and wrap the player to intercept the loadVideo method and update the link
  122. function wrapVimeoPlayer(player, container, config) {
  123. if (!player || !container) return player;
  124. var controls = getSingleNode(container, "//div[@class = 'controls-wrapper']//div[@class = 'controls']");
  125. if (!controls) return player;
  126. var button = createButton(container.ownerDocument);
  127. var observer = new MutationObserver(function() {
  128. showVideoLink(button, controls);
  129. });
  130. observer.observe(controls, {
  131. childList: true,
  132. subtree: true,
  133. attributes: false,
  134. characterData: false
  135. });
  136. updateVideoLink(button, config);
  137. showVideoLink(button, controls);
  138. return Object.create(player, {
  139. ready: {
  140. writable: true,
  141. enumerable: true,
  142. configurable: true,
  143. value: player.ready
  144. },
  145. loadVideo: {
  146. enumerable: true,
  147. configurable: false,
  148. get: function() {
  149. return player.loadVideo && function() {
  150. updateVideoLink(button, arguments && arguments[0]);
  151. return player.loadVideo.apply(this, arguments);
  152. };
  153. }
  154. }
  155. });
  156. }
  157.  
  158. // Wraps a property with a callback that convert the value each time the property is set
  159. function wrapProperty(o, propName, callback) {
  160. if (o.hasOwnProperty(propName)) {
  161. console.log('Vimeo Download Button : Unable to wrap property ' + propName + '.');
  162. return;
  163. }
  164. var value;
  165. Object.defineProperty(o, propName, {
  166. get: function() {
  167. return value;
  168. },
  169. set: function(newValue) {
  170. value = callback(newValue);
  171. },
  172. enumerable: true,
  173. configurable: false
  174. });
  175. console.log('Vimeo Download Button : Property ' + propName + ' wrapped successfully.');
  176. }
  177.  
  178. // Wrap the VimeoPlayer constructor and intercepts the arguments needed to install the download button
  179. wrapProperty(window, 'VimeoPlayer', function(ctr) {
  180. return ctr && function() {
  181. var container = arguments && arguments[0];
  182. var config = arguments && arguments[1];
  183. var player = ctr.apply(this, arguments);
  184. return wrapVimeoPlayer(player, container, config);
  185. };
  186. });
  187. })();