Vimeo Download Button

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

当前为 2017-05-17 提交的版本,查看 最新版本

  1. // ==UserScript==
  2. // @name Vimeo Download Button
  3. // @namespace larochematthias
  4. // @version 1.2.0
  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. loadVideo: {
  140. enumerable: true,
  141. configurable: false,
  142. get: function() {
  143. return player.loadVideo && function() {
  144. updateVideoLink(button, arguments && arguments[0]);
  145. return player.loadVideo.apply(this, arguments);
  146. };
  147. }
  148. }
  149. });
  150. }
  151.  
  152. // Wraps a property with a callback that convert the value each time the property is set
  153. function wrapProperty(o, propName, callback) {
  154. if (o.hasOwnProperty(propName)) {
  155. console.log('Vimeo Download Button : Unable to wrap property ' + propName + '.');
  156. return;
  157. }
  158. var value;
  159. Object.defineProperty(o, propName, {
  160. get: function() {
  161. return value;
  162. },
  163. set: function(newValue) {
  164. value = callback(newValue);
  165. },
  166. enumerable: true,
  167. configurable: false
  168. });
  169. console.log('Vimeo Download Button : Property ' + propName + ' wrapped successfully.');
  170. }
  171.  
  172. // Wrap the VimeoPlayer constructor and intercepts the arguments needed to install the download button
  173. wrapProperty(window, 'VimeoPlayer', function(ctr) {
  174. return ctr && function() {
  175. var container = arguments && arguments[0];
  176. var config = arguments && arguments[1];
  177. var player = ctr.apply(this, arguments);
  178. return wrapVimeoPlayer(player, container, config);
  179. };
  180. });
  181. })();