NOTICE: By continued use of this site you understand and agree to the binding Terms of Service and Privacy Policy.
// ==UserScript==
// @name Vimeo Download 2018
// @namespace volkan-k
// @description Adds a download button to the video player.
// @include https://vimeo.com/*
// @copyright 2018, volkan-k, 2015, schwarztee
// @license MIT
// @version 1.3
// @grant unsafeWindow
// @grant GM_download
// @grant GM_openInTab
// @grant GM_xmlhttpRequest
// @require https://cdnjs.cloudflare.com/ajax/libs/jquery/2.1.4/jquery.min.js
// @require https://cdnjs.cloudflare.com/ajax/libs/jqueryui/1.12.1/jquery-ui.min.js
// @require https://cdnjs.cloudflare.com/ajax/libs/filesize/3.6.1/filesize.min.js
// @require-test https://paste.ee/r/XNUsq/0
// @connect cloudflare.com
// @connect vimeo.com
// ==/UserScript==
var RANDOM=Math.floor(Math.random()*1234567890);
var DIALOG_ID='vimeo_download'+RANDOM;
var dl_handle, download_div,progressbar,progressLabel;
var is_downloading=false;
var timer1,timer2,timer3;
if (typeof GM_openInTab === "undefined") {
GM_openInTab = window.open;
}
if (typeof GM_info.downloadMode === "undefined") {
GM_info.downloadMode = "browser";
}
function debugLog(message){
console.log("USER-SCRIPT VIMEO-DOWNLOAD : "+message);
}
function convert_relative_urls(css,base){
return css.replace(/url\s*\(\s*['"]{1}([^'"]+)['"]{1}\s*\)/ig, function (match, capture) {
if (/^(https?|file|ftps?|mailto|javascript|data:image\/[^;]{2,9};):/i.test(capture)) {
return match; // url is already absolute
}
return "url('"+new URL(capture,base).href+"')";
});
}
function addStyle_external(css_link, once, xhr) {
id=btoa(css_link).replace(/[+\/=]+/ig, "");
if (typeof xhr === "boolean" && xhr === true){
// xhr it
GM_xmlhttpRequest({
method: "GET",
url: css_link,
onerror: function(oEvent){ alert("Error " + oEvent.target.status + " occurred while receiving the document."); },
onload: function(response){
if (response.readyState !== 4 || response.status !== 200) return;
addGlobalStyle(convert_relative_urls(response.responseText,css_link), once,id)
}
});
return;
}
var head, style;
head = document.getElementsByTagName('head')[0];
if (!head) {
return;
}
if (once && $("link[href='"+css_link+"']").length>0) {
return;
}
style = document.createElement('link');
style.setAttribute("rel", "stylesheet");
style.setAttribute("type", "text/css");
style.setAttribute("id", id);
style.setAttribute("href", css_link);
head.appendChild(style);
}
function addGlobalStyle(css, once,id) {
var head, style;
head = document.getElementsByTagName('head')[0];
if (!head) {
return;
}
if (once && document.getElementById(id)) {
return;
}
style = document.createElement('style');
style.setAttribute("type", "text/css");
if (typeof id === "string"){
style.setAttribute("id", id);
}
style.innerHTML = css;
head.appendChild(style);
}
(function(){
'use strict';
// helper: find DOM element
function find( selector ) { return document.querySelector( selector ); }
// wait for player to be ready and set up periodic video check
function setup(new_data)
{
var usw=(typeof unsafeWindow !== 'undefined')?unsafeWindow:window; // Firefox, Opera<15
// controller object in DOM and video element available?
if ( ((usw && 'vimeo' in usw) || (typeof new_data === "object")) && find( '.player .vp-video video' ) )
{
// try to get video metadata
// (this can easily break if Vimeo updates their object tree)
try
{
if (typeof new_data === "object" && new_data !== null ) {
debugLog("DEBUG: using XHR metadata");
var videoId=new_data.video.id;
var title=new_data.video.title;
var streams=new_data.request.files.progressive;
} else if (typeof vimeo === "object" && vimeo !== null ) {
debugLog("DEBUG: using vimeo metadata");
// get video ID
var videoId = vimeo['clip_page_config']['clip']['id'];
// retrieve active player properties
var videoInfo = vimeo['clips'][videoId];
// save title
//var title = videoInfo['video']['title'];
var title = vimeo['clip_page_config']['clip']['title'];
// get streams
var streams = videoInfo['request']['files']['progressive'];
} else if (are_we_on_video_page()===true) {
debugLog("DEBUG: can't find any metadata, but we are on video page, running XHR");
clearTimeout(timer3);
timer3=setTimeout(xhr_video_info,1000);
return;
} else {
debugLog("NOTICE: couldnt find any metadata nor video ID. Exiting..");
return;
}
// sort streams descending by video resolution
streams.sort( function compare( streamA, streamB )
{
// compare width property
return streamB.width - streamA.width;
});
// get video file info
// - just take the first one with the highest quality
// - this will be replaced when I got more time
var file = streams[0];
// log gathered information
console.log( "[Vimeo Download] Found media for \""+title+"\" ("+file.quality+")" );
// make download button
var button = makeButton( file.url, title, file.quality );
clearTimeout(timer1); // we created the button, stop setup() loop
clearInterval(timer2); // we will setInterval, clear previous one first.
// regularly check that button is in control bar
// yes, that's dirty, but Vimeo replaces the player UI somewhen after loading
timer2=setInterval( function()
{
// find control bar
var playBar = find( '.play-bar' )
if (playBar === null){
return;
}
// remove any old button if existing
var oldButton = find( '.button.dwnld' );
if (oldButton!==null && oldButton.outerHTML===button.outerHTML){
return;
}
//oldButton && console.log(oldButton.outerHTML); // for debugging only.
//console.log(button.outerHTML); // for debugging only.
oldButton && oldButton.remove();
// add new button
playBar.appendChild( button );
}, 500 );
}
catch ( error )
{
// log the error
console.error( "[Vimeo Download] Error retrieving video meta data:", error );
}
}
else
{
// try again later
timer1=setTimeout( setup, 500 ,(typeof new_data === "object" ? new_data : false));
}
}
// create download button
function makeButton( url, title, quality )
{
// make valid filename from title
var filename = title.replace( /[<>:"\/\\|?*]/g, '' ) + '.mp4';
// create new button
var button = document.createElement( 'button' );
button.setAttribute( 'href', url);
button.setAttribute( 'download', filename);
button.setAttribute( 'title', "Download " + quality);
button.setAttribute( 'class', "button dwnld" );
button.setAttribute( 'type', "button" );
button.setAttribute( 'aria-label', "Download" );
button.setAttribute( 'style', 'display: inline-block; font-size: 1.75em; margin: -0.10em 0 0 0.5em; color: #fff' );
// create new hyperlink
var hyperlink = document.createElement( 'a' );
hyperlink.setAttribute( 'href', url);
hyperlink.setAttribute( 'download', filename);
hyperlink.innerHTML = '<img src="data:image/svg+xml;utf8;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iaXNvLTg4NTktMSI/Pgo8IS0tIEdlbmVyYXRvcjogQWRvYmUgSWxsdXN0cmF0b3IgMTYuMC4wLCBTVkcgRXhwb3J0IFBsdWctSW4gLiBTVkcgVmVyc2lvbjogNi4wMCBCdWlsZCAwKSAgLS0+CjwhRE9DVFlQRSBzdmcgUFVCTElDICItLy9XM0MvL0RURCBTVkcgMS4xLy9FTiIgImh0dHA6Ly93d3cudzMub3JnL0dyYXBoaWNzL1NWRy8xLjEvRFREL3N2ZzExLmR0ZCI+CjxzdmcgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayIgdmVyc2lvbj0iMS4xIiBpZD0iQ2FwYV8xIiB4PSIwcHgiIHk9IjBweCIgd2lkdGg9IjE2cHgiIGhlaWdodD0iMTZweCIgdmlld0JveD0iMCAwIDQ3NS4wNzggNDc1LjA3NyIgc3R5bGU9ImVuYWJsZS1iYWNrZ3JvdW5kOm5ldyAwIDAgNDc1LjA3OCA0NzUuMDc3OyIgeG1sOnNwYWNlPSJwcmVzZXJ2ZSI+CjxnPgoJPGc+CgkJPHBhdGggZD0iTTQ2Ny4wODMsMzE4LjYyN2MtNS4zMjQtNS4zMjgtMTEuOC03Ljk5NC0xOS40MS03Ljk5NEgzMTUuMTk1bC0zOC44MjgsMzguODI3Yy0xMS4wNCwxMC42NTctMjMuOTgyLDE1Ljk4OC0zOC44MjgsMTUuOTg4ICAgIGMtMTQuODQzLDAtMjcuNzg5LTUuMzI0LTM4LjgyOC0xNS45ODhsLTM4LjU0My0zOC44MjdIMjcuNDA4Yy03LjYxMiwwLTE0LjA4MywyLjY2OS0xOS40MTQsNy45OTQgICAgQzIuNjY0LDMyMy45NTUsMCwzMzAuNDI3LDAsMzM4LjA0NHY5MS4zNThjMCw3LjYxNCwyLjY2NCwxNC4wODUsNy45OTQsMTkuNDE0YzUuMzMsNS4zMjgsMTEuODAxLDcuOTksMTkuNDE0LDcuOTloNDIwLjI2NiAgICBjNy42MSwwLDE0LjA4Ni0yLjY2MiwxOS40MS03Ljk5YzUuMzMyLTUuMzI5LDcuOTk0LTExLjgsNy45OTQtMTkuNDE0di05MS4zNThDNDc1LjA3OCwzMzAuNDI3LDQ3Mi40MTYsMzIzLjk1NSw0NjcuMDgzLDMxOC42Mjd6ICAgICBNMzYwLjAyNSw0MTQuODQxYy0zLjYyMSwzLjYxNy03LjkwNSw1LjQyNC0xMi44NTQsNS40MjRzLTkuMjI3LTEuODA3LTEyLjg0Ny01LjQyNGMtMy42MTQtMy42MTctNS40MjEtNy44OTgtNS40MjEtMTIuODQ0ICAgIGMwLTQuOTQ4LDEuODA3LTkuMjM2LDUuNDIxLTEyLjg0N2MzLjYyLTMuNjIsNy44OTgtNS40MzEsMTIuODQ3LTUuNDMxczkuMjMyLDEuODExLDEyLjg1NCw1LjQzMSAgICBjMy42MTMsMy42MSw1LjQyMSw3Ljg5OCw1LjQyMSwxMi44NDdDMzY1LjQ0Niw0MDYuOTQyLDM2My42MzgsNDExLjIyNCwzNjAuMDI1LDQxNC44NDF6IE00MzMuMTA5LDQxNC44NDEgICAgYy0zLjYxNCwzLjYxNy03Ljg5OCw1LjQyNC0xMi44NDgsNS40MjRjLTQuOTQ4LDAtOS4yMjktMS44MDctMTIuODQ3LTUuNDI0Yy0zLjYxMy0zLjYxNy01LjQyLTcuODk4LTUuNDItMTIuODQ0ICAgIGMwLTQuOTQ4LDEuODA3LTkuMjM2LDUuNDItMTIuODQ3YzMuNjE3LTMuNjIsNy44OTgtNS40MzEsMTIuODQ3LTUuNDMxYzQuOTQ5LDAsOS4yMzMsMS44MTEsMTIuODQ4LDUuNDMxICAgIGMzLjYxNywzLjYxLDUuNDI3LDcuODk4LDUuNDI3LDEyLjg0N0M0MzguNTM2LDQwNi45NDIsNDM2LjcyOSw0MTEuMjI0LDQzMy4xMDksNDE0Ljg0MXoiIGZpbGw9IiNGRkZGRkYiLz4KCQk8cGF0aCBkPSJNMjI0LjY5MiwzMjMuNDc5YzMuNDI4LDMuNjEzLDcuNzEsNS40MjEsMTIuODQ3LDUuNDIxYzUuMTQxLDAsOS40MTgtMS44MDgsMTIuODQ3LTUuNDIxbDEyNy45MDctMTI3LjkwOCAgICBjNS44OTktNS41MTksNy4yMzQtMTIuMTgyLDMuOTk3LTE5Ljk4NmMtMy4yMy03LjQyMS04Ljg0Ny0xMS4xMzItMTYuODQ0LTExLjEzNmgtNzMuMDkxVjM2LjU0M2MwLTQuOTQ4LTEuODExLTkuMjMxLTUuNDIxLTEyLjg0NyAgICBjLTMuNjItMy42MTctNy45MDEtNS40MjYtMTIuODQ3LTUuNDI2aC03My4wOTZjLTQuOTQ2LDAtOS4yMjksMS44MDktMTIuODQ3LDUuNDI2Yy0zLjYxNSwzLjYxNi01LjQyNCw3Ljg5OC01LjQyNCwxMi44NDdWMTY0LjQ1ICAgIGgtNzMuMDg5Yy03Ljk5OCwwLTEzLjYxLDMuNzE1LTE2Ljg0NiwxMS4xMzZjLTMuMjM0LDcuODAxLTEuOTAzLDE0LjQ2NywzLjk5OSwxOS45ODZMMjI0LjY5MiwzMjMuNDc5eiIgZmlsbD0iI0ZGRkZGRiIvPgoJPC9nPgo8L2c+CjxnPgo8L2c+CjxnPgo8L2c+CjxnPgo8L2c+CjxnPgo8L2c+CjxnPgo8L2c+CjxnPgo8L2c+CjxnPgo8L2c+CjxnPgo8L2c+CjxnPgo8L2c+CjxnPgo8L2c+CjxnPgo8L2c+CjxnPgo8L2c+CjxnPgo8L2c+CjxnPgo8L2c+CjxnPgo8L2c+Cjwvc3ZnPgo=" />';
// append hyperlink to button
button.appendChild( hyperlink );
// listen to click event
button.addEventListener('click', downloadVideoNatively, false);
// return DOM object
return button;
}
function downloadVideoNatively(e) {
if ($.inArray(GM_info.downloadMode, ["native","browser"]) === -1){
return ;
}
var elem=e.currentTarget;
e.returnValue=false;
if (e.preventDefault) {
e.preventDefault();
}
var link=$(elem).attr('href');
var name=$(elem).attr('download');
name = name.replace(/[\\<>:"\/|?*]*/g, "");
if (link && name) {
if (typeof GM_download !== 'undefined') {
if (GM_info.downloadMode==="native"){
open_download_dialog();
if (is_downloading===false){
dl_handle=GM_download({
url: link,
name: name,
onerror: gm_dl_error,
onload: gm_dl_load,
onprogress: gm_dl_progress,
ontimeout: gm_dl_timeout
});
dl_handle.my_filename=name;
is_downloading=true;
}
} else {
GM_download(link, name); // browser handles
}
debugLog("Downloading should start now.. File Name = "+name+" ; File URL = "+link);
} else {
GM_openInTab(link);
debugLog("Download link should be opened in new tab now.. File Name = "+name+" ; File URL = "+link);
}
}
return false;
}
function open_download_dialog() {
if ($('#' + DIALOG_ID).length > 0 && $('#' + DIALOG_ID).dialog('isOpen')) {
$('#' + DIALOG_ID).dialog('close').remove();
}
var dialogButtons = [{
text: "Cancel Download",
click: cancel_download
}];
addStyle_external('https://cdnjs.cloudflare.com/ajax/libs/jqueryui/1.12.1/jquery-ui.css', true, true);
addGlobalStyle("#progressbar"+RANDOM+"{margin-top: 20px;}.progress-label"+RANDOM+"{font-weight: bold; text-shadow: 1px 1px 0 #fff;}", true,'gm_added_style_vimeodl');
download_div = $("<div title='File Download' id='" + DIALOG_ID + "'></div>").dialog({
resizable: false,
closeText: "Continue in background",
buttons: dialogButtons,
close: function(event, ui) {
$(this).dialog('destroy').remove();
}
});
$("button.ui-dialog-titlebar-close").tooltip();
$('#' + DIALOG_ID).append('<div class="progress-label'+RANDOM+'">Starting download...</div>'
+'<div id="extrainfo'+RANDOM+'"></div>'
+'<div id="progressbar'+RANDOM+'"></div>');
/* $("#master" + RANDOM).slider({
value: volume,
create: function() {
$("#handle" + RANDOM).text(' = %' + $(this).slider("value"));
},
slide: function(event, ui) {
$("#handle" + RANDOM).text(' = %' + ui.value);
SetVolume(ui.value);
}
});*/
progressbar = $( "#progressbar"+RANDOM );
progressLabel = $( ".progress-label"+RANDOM );
progressbar.progressbar({
value: false,
change: function() {
progressLabel.text("Current Progress: " + progressbar.progressbar("value") + "%");
},
complete: function() {
progressLabel.text("Complete!");
change_button_to_close();
}
});
}
function gm_dl_progress(response) {
if ($('#' + DIALOG_ID).length === 0 || !$('#' + DIALOG_ID).dialog('isOpen')) {
return;
}
if (response.lengthComputable===false){
return;
}
//console.log(response);
var fs_html="<p>File size: "+filesize(response.total)+"</p>";
if (typeof dl_handle.my_filename === "string"){
fs_html+="<p>File name: "+dl_handle.my_filename+"</p>";
}
if ($("#extrainfo"+RANDOM).html()!==fs_html){
$("#extrainfo"+RANDOM).html(fs_html);
}
var val = Math.floor(response.done/response.total*100);
progressbar.progressbar("value", val);
}
function gm_dl_load(response) {
is_downloading=false;
if ($('#' + DIALOG_ID).length === 0 || !$('#' + DIALOG_ID).dialog('isOpen')) {
return;
}
progressbar.progressbar( "value", 100 );
change_button_to_close();
}
function gm_dl_timeout(response) {
is_downloading=false;
if ($('#' + DIALOG_ID).length === 0 || !$('#' + DIALOG_ID).dialog('isOpen')) {
return;
}
progressbar.progressbar( "value", false );
progressLabel.text( "Download time-out!" );
change_button_to_close();
}
function gm_dl_error(response) {
is_downloading=false;
if ($('#' + DIALOG_ID).length === 0 || !$('#' + DIALOG_ID).dialog('isOpen')) {
return;
}
progressbar.progressbar( "value", false );
progressLabel.text( "Download error!" );
change_button_to_close();
}
function change_button_to_close() {
download_div.dialog("option", "buttons", [{
text: "Close",
click: function(event, ui) {
$(this).dialog('close');
}
}]);
download_div.dialog( "option", "closeText", "Close" );
}
function cancel_download() {
is_downloading=false;
dl_handle.abort();
if ($('#' + DIALOG_ID).length === 0 || !$('#' + DIALOG_ID).dialog('isOpen')) {
return;
}
download_div.dialog("close");
}
function vimeoRegex () {
var regex = /(http|https)?:\/\/(www\.)?vimeo.com\/(?:channels\/(?:\w+\/)?|groups\/([^\/]*)\/videos\/|)(\d+)(?:|\/\?)/i;
return regex;
}
function are_we_on_video_page(){
var url = window.location.href;
return vimeoRegex().test(url);
}
function get_vimeo_id(url){
if (are_we_on_video_page()===false){
return null;
}
//console.log(vimeoRegex().exec(url)); // for debug.
var id_match=vimeoRegex().exec(url);
return id_match[4];
}
function xhr_video_info() {
var url = window.location.href;
var video_id = get_vimeo_id(url);
if (typeof video_id === "undefined" || video_id === null){
debugLog("DEBUG: xhr_video_info() couldnt find video id");
return;
}
GM_xmlhttpRequest({
method: "GET",
url: "https://player.vimeo.com/video/"+video_id+"/config",
onerror: function(oEvent){ console.log("Error " + oEvent.status + " occurred while receiving the document."); },
onload: function(response){
if (response.readyState !== 4 || response.status !== 200) return;
setup(JSON.parse(response.responseText));
}
});
}
// start looking for video player
setup();
// f*cking vimeo does not update window.vimeo object.
$(window).bind('popstate', function() {
debugLog("DEBUG: popstate event: calling xhr_video_info()");
xhr_video_info();
});
var window_history_pushState = window.history.pushState;
window.history.pushState = function () {
window_history_pushState.apply(window.history, arguments);
debugLog("DEBUG: pushstate event: calling xhr_video_info()");
xhr_video_info();
}
})();