// ==UserScript==
// @name Vimeo Download Button
// @namespace larochematthias
// @version 1.1.0
// @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>';
// Create HTML div element to add to the controls bar
function createButton(document, link) {
var div = document.createElement('div');
div.style.marginLeft = '7px';
div.style.marginTop = '-1px';
div.setAttribute('class', 'download');
var a = document.createElement('a');
a.setAttribute('href', link.url);
a.setAttribute('download', (link.title || '').replace(/[\x00-\x1F"*\/:<>?\\|]+/g, ''));
a.setAttribute('target', '_blank');
a.setAttribute('title', 'Download ' + link.quality);
a.setAttribute('aria-label', 'Download');
a.setAttribute('referrerpolicy', 'origin');
a.innerHTML = downloadIcon;
div.appendChild(a);
return div;
}
// 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 parameter of the player
function getVideoLink(config) {
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) {
return {
url: file.url,
title: title,
quality: file.quality
};
}
}
return null;
}
// Called by the MutationObserver, keep adding the download button to the DOM if it disappears
function showVideoLink(controls, link) {
var download = getSingleNode(controls, "//div[@class = 'download']");
if (download) {
if (download.nextSibling) {
download.parentNode.appendChild(download);
}
return;
}
var playBar = getSingleNode(controls, "//div[contains(@class, 'play-bar')]");
if (!playBar) return;
download = createButton(controls.ownerDocument, link);
playBar.appendChild(download);
}
// Get the config if it's a URL, then get the link
// and set a MutationObserver to add the download button when the controls are ready
function tryShowVideoLink(player, config) {
if (!player || !config) return;
if (typeof(config) === 'string') {
ajax(config, null, function(xhr) {
var result = JSON.parse(xhr.responseText);
tryShowVideoLink(player, result);
});
return;
}
var link = getVideoLink(config);
if (!link) return;
var controls = getSingleNode(player, "//div[@class = 'controls-wrapper']//div[@class = 'controls']");
if (!controls) return;
var observer = new MutationObserver(function() {
showVideoLink(controls, link);
});
observer.observe(controls, {
childList: true,
subtree: true,
attributes: false,
characterData: false
});
showVideoLink(controls, link);
}
// 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(newValue) {
return newValue && function() {
var player = arguments && arguments[0];
var config = arguments && arguments[1];
var result = newValue.apply(this, arguments);
if (player && config) {
tryShowVideoLink(player, config);
}
return result;
};
});
})();