// ==UserScript==
// @name Vimeo Download Button
// @namespace larochematthias
// @version 1.2.1
// @description Adds a download button to the HTML5 Vimeo Player (embeded or not)
// @author Matthias Laroche
// @license https://creativecommons.org/licenses/by/4.0/
// @include *//vimeo.com/*
// @include *//player.vimeo.com/video/*
// @run-at document-start
// @grant none
// ==/UserScript==
(function() {
'use strict';
// SVG icon of the download button
// Icon made by Elegant Themes from www.flaticon.com
// Icon pack: http://www.flaticon.com/packs/elegant-font
// Published by: https://www.elegantthemes.com/
// License: https://creativecommons.org/licenses/by/3.0/
var downloadIcon =
'<svg viewBox="0 0 455.992 455.992" width="14px" height="14px">' +
'<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" />' +
'<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" />' +
'</svg>';
function updateButton(button, link) {
if (!link) {
button.div.style.visibility = 'hidden';
return;
}
button.a.setAttribute('href', link.url);
button.a.setAttribute('download', (link.title || '').replace(/[\x00-\x1F"*\/:<>?\\|]+/g, ''));
button.a.setAttribute('title', 'Download ' + link.quality + "\n" + link.title);
button.div.style.visibility = 'visible';
button.div.style.display = 'block';
}
// Create HTML div element to add to the controls bar
function createButton(document) {
var div = document.createElement('div');
div.style.marginLeft = '7px';
div.style.marginTop = '-1px';
div.style.display = 'none';
div.setAttribute('class', 'download');
var a = document.createElement('a');
a.setAttribute('target', '_blank');
a.setAttribute('aria-label', 'Download');
a.setAttribute('referrerpolicy', 'origin');
a.style.outlineStyle = 'none';
a.innerHTML = downloadIcon;
div.appendChild(a);
return {
div: div,
a: a
};
}
// Syntactic sugar for getting a single node with XPath
function getSingleNode(node, xpath) {
var doc = node.ownerDocument || node;
return doc.evaluate(xpath, node, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue;
}
// Syntactic sugar for XMLHttpRequest
function ajax(url, postData, onSuccess, onError) {
var xhr = new XMLHttpRequest();
xhr.open(postData ? 'POST' : 'GET', url);
if (onSuccess || onError) {
xhr.onreadystatechange = function() {
if (xhr.readyState != 4) return;
if (xhr.status == 200 && onSuccess) onSuccess(xhr);
if (xhr.status != 200 && onError) onError(xhr);
};
}
xhr.send(postData || null);
}
// Get url, quality and title of the video from the config of the player
// and then update the download button accordingly
function updateVideoLink(button, config) {
if (config && typeof(config) === 'object') {
var title = config && config.video && config.video.title || '';
var progressive = config && config.request && config.request.files && config.request.files.progressive;
if (progressive) {
var file = progressive.reduce(function(a, b) {
return (b.width || 0) > a.width ? b : a;
}, {
width: -1
});
if (file.url) {
updateButton(button, {
url: file.url,
title: title,
quality: file.quality
});
return;
}
}
}
updateButton(button, null);
if (config && typeof(config) === 'string') {
// config is an URL -> download with ajax and update again with the result
ajax(config, null, function(xhr) {
updateVideoLink(button, JSON.parse(xhr.responseText));
});
}
}
// Called by the MutationObserver, keep adding the download button to the DOM if it disappears
function showVideoLink(button, controls) {
var playBar = getSingleNode(controls, "//div[contains(@class, 'play-bar')]");
if (playBar && (button.div.parentNode != playBar || button.div.nextSibling)) {
playBar.appendChild(button.div);
}
}
// Create a link and update it with the config of the player,
// set a MutationObserver to add the download button when the controls are ready
// and wrap the player to intercept the loadVideo method and update the link
function wrapVimeoPlayer(player, container, config) {
if (!player || !container) return player;
var controls = getSingleNode(container, "//div[@class = 'controls-wrapper']//div[@class = 'controls']");
if (!controls) return player;
var button = createButton(container.ownerDocument);
var observer = new MutationObserver(function() {
showVideoLink(button, controls);
});
observer.observe(controls, {
childList: true,
subtree: true,
attributes: false,
characterData: false
});
updateVideoLink(button, config);
showVideoLink(button, controls);
return Object.create(player, {
ready: {
writable: true,
enumerable: true,
configurable: true,
value: player.ready
},
loadVideo: {
enumerable: true,
configurable: false,
get: function() {
return player.loadVideo && function() {
updateVideoLink(button, arguments && arguments[0]);
return player.loadVideo.apply(this, arguments);
};
}
}
});
}
// Wraps a property with a callback that convert the value each time the property is set
function wrapProperty(o, propName, callback) {
if (o.hasOwnProperty(propName)) {
console.log('Vimeo Download Button : Unable to wrap property ' + propName + '.');
return;
}
var value;
Object.defineProperty(o, propName, {
get: function() {
return value;
},
set: function(newValue) {
value = callback(newValue);
},
enumerable: true,
configurable: false
});
console.log('Vimeo Download Button : Property ' + propName + ' wrapped successfully.');
}
// Wrap the VimeoPlayer constructor and intercepts the arguments needed to install the download button
wrapProperty(window, 'VimeoPlayer', function(ctr) {
return ctr && function() {
var container = arguments && arguments[0];
var config = arguments && arguments[1];
var player = ctr.apply(this, arguments);
return wrapVimeoPlayer(player, container, config);
};
});
})();