Download Vimeo

Display download (source) links for Vimeo videos

// ==UserScript==
// @name		Download Vimeo
// @version		1.0.1
// @description	Display download (source) links for Vimeo videos
// @namespace	bp
// @author		Benjamin Philipp <dev [at - please don't spam] benjamin-philipp.com>
// @include		/^(https?:\/\/(www\.)?vimeo\.[a-zA-Z]{2,10}\/\d+([\/?#].*)?)|(https?:\/\/player\.vimeo\.com\/video\/\d+([\/?#].*)?)$/
// @require 	http://ajax.googleapis.com/ajax/libs/jquery/3.1.0/jquery.min.js
// @run-at		document-body
// @grant		GM_xmlhttpRequest
// @connect		*
// ==/UserScript==

/* globals $, GM_info, GM_xmlhttpRequest, debugging, waitFor */

var maxTries=40; // try to find the config script or insert the links a maximum of n times
var interval=500; // interval in ms before the above actions are tried again, if they fail.

var rex = {
	embed: /^https?:\/\/player\.vimeo\.com\/video\/\d+([\/?#].*)?$/i,
	onsite: /^https?:\/\/(www\.)?vimeo\.[a-zA-Z]{2,10}\/\d+([\/?#].*)?$/i
}; // RegEx to check which kind of site we're on: embedded video page or main video page on Vimeo?

const prefix = GM_info.script.name;
function main(){
	$("head").append(
`<style>
.vp-controls .bpDownloads{
	/* opacity: 0.3; */
	transition: opacity 0.5s;
	font-size: 12pt;
}
.vp-controls .bpDownloads:hover{
	opacity: 1;
}
.vp-controls .bpDownloads h3{
	color: #ccc;
	background: #333;
	display: block;
	margin: 0;
	padding: 6px 10px;
	font-size: 15px;
	border-radius: 3px;
	transition: all 0.5s;
}
.vp-controls .bpDownloads:hover h3{
	padding: 10px;
	border-radius: 0;
}
.vp-controls .bpDownloads a{
	background: #333;
	color: #ddd;
	color: #fff;
	text-decoration: none;
	display: block;
	margin: 0;
	padding: 0 10px;
	height: 0;
	transition: all 0.5s;
	box-sizing: content-box;
	overflow: hidden;
}
.vp-controls .bpDownloads:hover a{
	height: 1rem;
	padding: 10px;
}
.vp-controls .bpDownloads a:hover{
	background: #000;
}
</style>`);
	
	var isEmbed = rex.embed.test(location.href);
	if(isEmbed){
		log("is embedded vimeo");
		getScript(function(s){
	//		log("callback", s);
			var links = s.request.files.progressive;
			links.sort(function(a,b){return a.height - b.height;});
	//		log(links);
			insertLinks(links, "#player");
		});
	}
	else{
		log("is on vimeo site");
		waitFor("#main .player[data-config-url]", function(o){
			o.each(function(){
				var player = $(this);
				var configUrl = player.attr("data-config-url");
				log("Config URL:", configUrl);
				GM_xmlhttpRequest({
					url: configUrl,
					method: "GET",
					timeout: 15000,
					
					onload: function(res){
//						log("got:", [res.responseText], res);
						var cfg = JSON.parse(res.responseText);
						var links = cfg.request.files.progressive;
						links.sort(function(a,b){return a.height - b.height;});
				//		log(links);
						insertLinks(links, player);
					},
					onerror: function(res){
						log({error: "Error loading page", page: configUrl});
					},
					ontimeout: function(res){
						log({error: "Connection timed out", page: configUrl});
					}
				});
			});
		});
		
	}
}

function insertLinks(links, player){
	var str = "";
	for(let l of links){
		str += "<a href='" + l.url + "'><b>" + l.quality + " " + l.mime + "</b> (" + l.width + "x" + l.height + ")</a>";
	}
	str = "<div class='bpDownloads'><h3>Download</h3>" + str + "</div>";
//	log(str);
	makeSureIsInserted(str, ".bpDownloads", player, ".vp-controls-wrapper .vp-controls");
}

function makeSureIsInserted(o, sel, container, targetSel=false, tries=0){
	var findIn = document;
	var target = container;
	if(targetSel){
		findIn = $(container);
		target = targetSel;
	}
	waitFor({sel: target, findIn: findIn, cb: function(){
		var exist=$(target).children(sel);
		if(!exist || exist.length<=0){
	//		log("not in target, insert", exist);
			$(target).append(o);
		}
		if(tries<maxTries)
			setTimeout(function(){
				makeSureIsInserted(o, sel, container, targetSel, tries+1);
			}, interval);
	}, cbFail: function(){log("Failed to find target for insertion");}});
}

function getScript(cb, tries=0){
	var scripts = $("body>script");
	log("try getting config script", scripts);
	var gotit=false;
	if(scripts && scripts.length>=1){
		scripts.each(function(){
//			log(this);
			var str = $(this).html();
			var rex = /var\s+config\s*=\s*(\{.*?"files"\s*:.*?"progressive"\s*:.+?\.mp4.+?\})\s*?;/i;
			var m = str.match(rex);
			if(m && m.length>=2){
//				log("matches config");
//				log(m[1]);
				var cfg = JSON.parse(m[1]); //eval(m[1]);
				gotit=true;
				return cb(cfg);
			}
		});
	}
	if(gotit)
		return;
	if(tries>maxTries)
		return log("Could not get script");
//	log("will retry");
	setTimeout(function(){
		getScript(cb, tries+1);
	}, interval);
}

function parseSources(s){
	
}

function log(...args){
	if("undefined" != typeof(prefix) && !!prefix)
		args = [prefix + ":", ...args];
	if("undefined" != typeof(debugging) && !!debugging)
		args = [...args, new Error().stack.replace(/^\s*(Error|Stack trace):?\n/gi, "").replace(/^([^\n]*\n)/, "\n")];
	console.log(...args);
}

// UGH, why must GreasyFork block custom @includes >.<
class eleWaiter{
	constructor(sel, cb, cbFail=null, findIn="document", delay=500, maxTries=50, alwaysOn=false, autoStart=true, debug = false){
		this.sel = "";
		this.cb = null;
		this.cbFail = null;
		this.findIn = "document";
		this.delay = 500;
		this.maxTries = 50;
		this.alwaysOn = false;
		this.autoStart = true;
		this.debug = false;

		this.__running = false;
		this.__tries = 0;
		this.__timer = 0;
		this.__jqo = {};

		if(typeof sel == "object"){
//			log("got object");
			Object.assign(this, sel);
		}
		else{
			this.sel = sel;
			this.cb = cb;
			if(cbFail!== undefined || cbFail!== null)
			this.cbFail = cbFail;
			if(findIn!== undefined || findIn!== null)
				this.findIn = findIn;
			this.delay = delay;
			this.maxTries = maxTries;
			this.alwaysOn = alwaysOn;
			this.autoStart = autoStart;
			this.debug = debug;
		}
		if(this.debug){
			if(typeof this.debug != "object"){
				this.debug = {
					prefix: this.debug + " ",
					level: 1
				};
			}
			else{
				if(!this.debug.prefix)
					this.debug.prefix = "";
				else
					this.debug.prefix += " ";
			
				if(!this.debug.level)
					this.debug.level = 1;
			}
		}
//		this.log(this.cb);
		this.log(this);
		if(this.autoStart)
			this.__wait();
	}
	
	log(...args){
		if(!this.debug)
			return;
		
		if(typeof args == "object" && args.length>=2 && typeof args[args.length-1] == "string" && args[args.length-1].toLowerCase().indexOf("loglevel:")===0){
			var level = args[args.length-1].substr(9)*1;
			if(level>this.debug.level){
				return;
			}
			args.pop();
		}
		console.log(this.debug.prefix + "EleWaiter:", ...args);
	}

	start(){
		if(!this.__running){
			this.log("Start waiting", this.findIn, this.sel);
			this.__wait();
		}
	}
	stop(){
		clearTimeout(this.__timer);
		this.__running = false;
	}

	__wait(){
//		if(!this.findIn)
//			this.findIn = document;
		if(this.findIn == "document" && !!document)
			this.findIn = document;
		
		this.__running = true;
		if(this.maxTries!=-1)
			this.__tries++;
		var triesLeft = this.alwaysOn?1:(this.maxTries - this.__tries);
		this.log("tries left:", triesLeft, "loglevel:3");
		this.__jqo = $(this.findIn).find(this.sel);

		if(this.__jqo.length<=0){
			if(this.debug && this.debug.level>2 || !this.alwaysOn)
				this.log("Not found: " + this.sel);
			if(triesLeft!==0){
				this.__timer = setTimeout(function(){this.__wait();}.bind(this), this.delay);
			}
			else
				this.__result(false);
			return;
		}

		this.__result(this.__jqo);

		if(this.alwaysOn){
			this.log("Always on, repeat", "loglevel:3");
			this.__timer = setTimeout(function(){this.__wait();}.bind(this), this.delay);
		}
	}
	__result(success=false){
		if(this.debug.level>2 || !this.alwaysOn)
			this.log("Result:", success);
		this.__running = false;
		if(success){
			if(this.cb!==undefined && typeof this.cb == "function")
				this.cb(this.__jqo);
			else
				console.log("Warning: callback cb not function", this.cb);
		}
		else{
			if(this.cbFail!==undefined && typeof this.cbFail == "function")
				this.cbFail(this.__jqo);
		}
	}
}
if("undefined" === typeof eleWaiters){ // jshint ignore:line
	var eleWaiters ={}; // jshint ignore:line
}

function waitFor(sel, cb, cbFail=null, findIn="document", delay=500, maxTries=50, alwaysOn=false, debug = false){ // 2021-01-29
	return new eleWaiter(sel, cb, cbFail, findIn, delay, maxTries, alwaysOn, true, debug);
}

setTimeout(main, 0);