volkan-k / Vimeo Download 2018

// ==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();
	}
})();