// ==UserScript==
// @name YouTube Links
// @namespace http://www.smallapple.net/labs/YouTubeLinks/
// @description Download YouTube videos. Video formats are listed at the top of the watch page. Video links are tagged so that they can be downloaded easily.
// @author Ng Hun Yang
// @include http://*.youtube.com/*
// @include http://youtube.com/*
// @include https://*.youtube.com/*
// @include https://youtube.com/*
// @match *://*.youtube.com/*
// @match *://*.googlevideo.com/*
// @match *://s.ytimg.com/yts/jsbin/*
// @grant GM_xmlhttpRequest
// @grant GM.xmlHttpRequest
// @connect googlevideo.com
// @connect s.ytimg.com
// @version 2.44
// ==/UserScript==
/* This is based on YouTube HD Suite 3.4.1 */
/* Tested on Firefox 5.0, Chrome 13 and Opera 11.50 */
(function() {
// =============================================================================
var win = typeof(unsafeWindow) !== "undefined" ? unsafeWindow : window;
var doc = win.document;
var loc = win.location;
if(win.top != win.self)
return;
var unsafeWin = win;
// Hack to get unsafe window in Chrome
(function() {
var isChrome = navigator.userAgent.toLowerCase().indexOf("chrome") >= 0;
if(!isChrome)
return;
// Chrome 27 fixed this exploit, but luckily, its unsafeWin now works for us
try {
var div = doc.createElement("div");
div.setAttribute("onclick", "return window;");
unsafeWin = div.onclick();
} catch(e) {
}
}) ();
var ua = navigator.userAgent || "";
var isEdgeBrowser = ua.match(/ Edge\//);
// =============================================================================
if(typeof GM == "object" && GM.xmlHttpRequest && typeof GM_xmlhttpRequest == "undefined") {
GM_xmlhttpRequest = async function(opts) {
await GM.xmlHttpRequest(opts);
}
}
// =============================================================================
var SCRIPT_NAME = "YouTube Links";
var relInfo = {
ver: 24400,
ts: 2021121600,
desc: "Update header insert point"
};
var SCRIPT_UPDATE_LINK = loc.protocol + "//greasyfork.org/scripts/5565-youtube-links-updater/code/YouTube Links Updater.user.js";
var SCRIPT_LINK = loc.protocol + "//greasyfork.org/scripts/5566-youtube-links/code/YouTube Links.user.js";
// =============================================================================
var dom = {};
dom.gE = function(id) {
return doc.getElementById(id);
};
dom.gT = function(dom, tag) {
if(arguments.length == 1) {
tag = dom;
dom = doc;
}
return dom.getElementsByTagName(tag);
};
dom.cE = function(tag) {
return document.createElement(tag);
};
dom.cT = function(s) {
return doc.createTextNode(s);
};
dom.attr = function(obj, k, v) {
if(arguments.length == 2)
return obj.getAttribute(k);
obj.setAttribute(k, v);
};
dom.prepend = function(obj, child) {
obj.insertBefore(child, obj.firstChild);
};
dom.append = function(obj, child) {
obj.appendChild(child);
};
dom.offset = function(obj) {
var x = 0;
var y = 0;
if(obj.getBoundingClientRect) {
var box = obj.getBoundingClientRect();
var owner = obj.ownerDocument;
x = box.left + Math.max(owner.documentElement.scrollLeft, owner.body.scrollLeft) - owner.documentElement.clientLeft;
y = box.top + Math.max(owner.documentElement.scrollTop, owner.body.scrollTop) - owner.documentElement.clientTop;
return { left: x, top: y };
}
if(obj.offsetParent) {
do {
x += obj.offsetLeft - obj.scrollLeft;
y += obj.offsetTop - obj.scrollTop;
obj = obj.offsetParent;
} while(obj);
}
return { left: x, top: y };
};
dom.inViewport = function(el) {
var rect = el.getBoundingClientRect();
if(rect.width == 0 && rect.height == 0)
return false;
return rect.bottom >= 0 &&
rect.right >= 0 &&
rect.top < (win.innerHeight || doc.documentElement.clientHeight) &&
rect.left < (win.innerWidth || doc.documentElement.clientWidth);
};
dom.html = function(obj, s) {
if(arguments.length == 1)
return obj.innerHTML;
obj.innerHTML = s;
};
dom.emitHtml = function(tag, attrs, body) {
if(arguments.length == 2) {
if(typeof(attrs) == "string") {
body = attrs;
attrs = {};
}
}
var list = [];
for(var k in attrs) {
if(attrs[k] != null)
list.push(k + "='" + attrs[k].replace(/'/g, "'") + "'");
}
var s = "<" + tag + " " + list.join(" ") + ">";
if(body != null)
s += body + "</" + tag + ">";
return s;
};
dom.emitCssStyles = function(styles) {
var list = [];
for(var k in styles) {
list.push(k + ": " + styles[k] + ";");
}
return " { " + list.join(" ") + " }";
};
dom.ajax = function(opts) {
function newXhr() {
if(window.ActiveXObject) {
try {
return new ActiveXObject("Msxml2.XMLHTTP");
} catch(e) {
}
try {
return new ActiveXObject("Microsoft.XMLHTTP");
} catch(e) {
return null;
}
}
if(window.XMLHttpRequest)
return new XMLHttpRequest();
return null;
}
function nop() {
}
// Entry point
var xhr = newXhr();
opts = addProp({
type: "GET",
async: true,
success: nop,
error: nop,
complete: nop
}, opts);
xhr.open(opts.type, opts.url, opts.async);
xhr.onreadystatechange = function() {
if(xhr.readyState == 4) {
var status = +xhr.status;
if(status >= 200 && status < 300) {
opts.success(xhr.responseText, "success", xhr);
}
else {
opts.error(xhr, "error");
}
opts.complete(xhr);
}
};
xhr.send("");
};
dom.crossAjax = function(opts) {
function wrapXhr(xhr) {
var headers = xhr.responseHeaders.replace("\r", "").split("\n");
var obj = {};
forEach(headers, function(idx, elm) {
var nv = elm.split(":");
if(nv[1] != null)
obj[nv[0].toLowerCase()] = nv[1].replace(/^\s+/, "").replace(/\s+$/, "");
});
var responseXML = null;
if(opts.dataType == "xml")
responseXML = new DOMParser().parseFromString(xhr.responseText, "text/xml");
return {
responseText: xhr.responseText,
responseXML: responseXML,
status: xhr.status,
getAllResponseHeaders: function() {
return xhr.responseHeaders;
},
getResponseHeader: function(name) {
return obj[name.toLowerCase()];
}
};
}
function nop() {
}
// Entry point
opts = addProp({
type: "GET",
async: true,
success: nop,
error: nop,
complete: nop
}, opts);
if(typeof GM_xmlhttpRequest === "undefined") {
setTimeout(function() {
var xhr = {};
opts.error(xhr, "error");
opts.complete(xhr);
}, 0);
return;
}
// TamperMonkey does not handle URLs starting with //
var url;
if(opts.url.match(/^\/\//))
url = loc.protocol + opts.url;
else
url = opts.url;
GM_xmlhttpRequest({
method: opts.type,
url: url,
synchronous: !opts.async,
onload: function(xhr) {
xhr = wrapXhr(xhr);
if(xhr.status >= 200 && xhr.status < 300)
opts.success(xhr.responseXML || xhr.responseText, "success", xhr);
else
opts.error(xhr, "error");
opts.complete(xhr);
},
onerror: function(xhr) {
xhr = wrapXhr(xhr);
opts.error(xhr, "error");
opts.complete(xhr);
}
});
};
dom.addEvent = function(e, type, fn) {
function mouseEvent(evt) {
if(this != evt.relatedTarget && !dom.isAChildOf(this, evt.relatedTarget))
fn.call(this, evt);
}
// Entry point
if(e.addEventListener) {
var effFn = fn;
if(type == "mouseenter") {
type = "mouseover";
effFn = mouseEvent;
}
else if(type == "mouseleave") {
type = "mouseout";
effFn = mouseEvent;
}
e.addEventListener(type, effFn, /*capturePhase*/ false);
}
else
e.attachEvent("on" + type, function() { fn(win.event); });
};
dom.insertCss = function (styles) {
var ss = dom.cE("style");
dom.attr(ss, "type", "text/css");
var hh = dom.gT("head") [0];
dom.append(hh, ss);
dom.append(ss, dom.cT(styles));
};
dom.isAChildOf = function(parent, child) {
if(parent === child)
return false;
while(child && child !== parent) {
child = child.parentNode;
}
return child === parent;
};
// -----------------------------------------------------------------------------
function timeNowInSec() {
return Math.round(+new Date() / 1000);
}
function forLoop(opts, fn) {
opts = addProp({ start: 0, inc: 1 }, opts);
for(var idx = opts.start; idx < opts.num; idx += opts.inc) {
if(fn.call(opts, idx, opts) === false)
break;
}
}
function forEach(list, fn) {
forLoop({ num: list.length }, function(idx) {
return fn.call(list[idx], idx, list[idx]);
});
}
function addProp(dest, src) {
for(var k in src) {
if(src[k] != null)
dest[k] = src[k];
}
return dest;
}
function inArray(elm, array) {
for(var i = 0; i < array.length; ++i) {
if(array[i] === elm)
return i;
}
return -1;
}
function unescHtmlEntities(s) {
return s.replace(/</g, "<").replace(/>/g, ">").replace(/&/g, "&").replace(/"/g, '"').replace(/'/g, "'");
}
function logMsg(s) {
win.console.log(s);
}
function cnvSafeFname(s) {
return s.replace(/:/g, "-").replace(/"/g, "'").replace(/[\\/|*?]/g, "_");
}
function encodeSafeFname(s) {
return encodeURIComponent(cnvSafeFname(s)).replace(/'/g, "%27");
}
function getVideoName(s) {
var list = [
{ name: "3GP", codec: "video\\/3gpp" },
{ name: "FLV", codec: "video\\/x-flv" },
{ name: "M4V", codec: "video\\/x-m4v" },
{ name: "MP3", codec: "audio\\/mpeg" },
{ name: "MP4", codec: "video\\/mp4" },
{ name: "M4A", codec: "audio\\/mp4" },
{ name: "QT", codec: "video\\/quicktime" },
{ name: "WEBM", codec: "audio\\/webm" },
{ name: "WEBM", codec: "video\\/webm" },
{ name: "WMV", codec: "video\\/ms-wmv" }
];
var spCodecs = {
"av01": "AV1",
"opus": "OPUS",
"vorbis": "VOR",
"vp9": "VP9"
};
if(s.match(/;\s*\+?codecs=\"([a-zA-Z0-9]+)/)) {
var str = RegExp.$1;
if(spCodecs[str])
return spCodecs[str];
}
var name = "?";
forEach(list, function(idx, elm) {
if(s.match("^" + elm.codec)) {
name = elm.name;
return false;
}
});
return name;
}
function getAspectRatio(wd, ht) {
return Math.round(wd / ht * 100) / 100;
}
function cnvResName(res) {
var resMap = {
"audio": "Audio"
};
if(resMap[res])
return resMap[res];
if(!res.match(/^(\d+)x(\d+)/))
return res;
var wd = +RegExp.$1;
var ht = +RegExp.$2;
if(wd < ht) {
var t = wd;
wd = ht;
ht = t;
}
var horzResAr = [
[ 16000, "16K" ],
[ 14000, "14K" ],
[ 12000, "12K" ],
[ 10000, "10K" ],
[ 8000, "8K" ],
[ 6000, "6K" ],
[ 5000, "5K" ],
[ 4000, "4K" ],
[ 3000, "3K" ],
[ 2048, "2K" ]
];
var vertResAr = [
[ 4320, "8K" ],
[ 3160, "6K" ],
[ 2880, "5K" ],
[ 2160, "4K" ],
[ 1728, "3K" ],
[ 1536, "2K" ],
[ 240, "240v" ],
[ 144, "144v" ]
];
var aspectRatio = getAspectRatio(wd, ht);
var name;
do {
forEach(horzResAr, function(idx, elm) {
var tolerance = elm[0] * 0.05;
if(wd >= elm[0] * 0.95) {
name = elm[1];
return false;
}
});
if(name)
break;
if(aspectRatio >= WIDE_AR_CUTOFF)
ht = Math.round(wd * 9 / 16);
forEach(vertResAr, function(idx, elm) {
var tolerance = elm[0] * 0.05;
if(ht >= elm[0] - tolerance && ht < elm[0] + tolerance) {
name = elm[1];
return false;
}
});
if(name)
break;
// Snap to std vert res
var vertResList = [ 4320, 3160, 2880, 2160, 1536, 1080, 720, 480, 360, 240, 144 ];
forEach(vertResList, function(idx, elm) {
var tolerance = elm * 0.05;
if(ht >= elm - tolerance && ht < elm + tolerance) {
ht = elm;
return false;
}
});
name = String(ht) + (aspectRatio < FULL_AR_CUTOFF ? "f" : "p");
} while(false);
if(aspectRatio >= ULTRA_WIDE_AR_CUTOFF)
name = "u" + name;
else if(aspectRatio >= WIDE_AR_CUTOFF)
name = "w" + name;
return name;
}
function mapResToQuality(res) {
if(!res.match(/^(\d+)x(\d+)/))
return res;
var wd = +RegExp.$1;
var ht = +RegExp.$2;
if(wd < ht) {
var t = wd;
wd = ht;
ht = t;
}
var resList = [
{ res: 3160, q : "ultrahighres" },
{ res: 1536, q : "highres" },
{ res: 1080, q: "hd1080" },
{ res: 720, q : "hd720" },
{ res: 480, q : "large" },
{ res: 360, q : "medium" }
];
var q;
forEach(resList, function(idx, elm) {
if(ht >= elm.res) {
q = elm.q;
return false;
}
});
return q || "small";
}
function getQualityIdx(quality) {
var list = [ "small", "medium", "large", "hd720", "hd1080", "highres", "ultrahighres" ];
for(var i = 0; i < list.length; ++i) {
if(list[i] == quality)
return i;
}
return -1;
}
// =============================================================================
RegExp.escape = function(s) {
return String(s).replace(/([.*+?^=!:${}()|\[\]\/\\])/g, "\\$1");
};
var decryptSig = {
store: {}
};
(function () {
var SIG_STORE_ID = "ujsYtLinksSig";
var CHK_SIG_INTERVAL = 3 * 86400;
decryptSig.load = function() {
var obj = localStorage[SIG_STORE_ID];
if(obj == null)
return;
decryptSig.store = JSON.parse(obj);
};
decryptSig.save = function() {
localStorage[SIG_STORE_ID] = JSON.stringify(decryptSig.store);
};
decryptSig.extractScriptUrl = function(data) {
if(data.match(/ytplayer.config\s*=.*"assets"\s*:\s*\{.*"js"\s*:\s*(".+?")[,}]/))
return JSON.parse(RegExp.$1);
else if(data.match(/ytplayer.web_player_context_config\s*=\s*\{.*"rootElementId":"movie_player","jsUrl":(".+?")[,}]/))
return JSON.parse(RegExp.$1);
else if(data.match(/,"WEB_PLAYER_CONTEXT_CONFIGS":{.*"rootElementId":"movie_player","jsUrl":(".+?")[,}]/))
return JSON.parse(RegExp.$1);
else
return false;
};
decryptSig.getScriptName = function(url) {
if(url.match(/\/yts\/jsbin\/player-(.*)\/[a-zA-Z0-9_]+\.js$/))
return RegExp.$1;
if(url.match(/\/yts\/jsbin\/html5player-(.*)\/html5player\.js$/))
return RegExp.$1;
if(url.match(/\/html5player-(.*)\.js$/))
return RegExp.$1;
return url;
};
decryptSig.fetchScript = function(scriptName, url) {
function success(data) {
data = data.replace(/\n|\r/g, "");
var sigFn;
forEach([
/\.signature\s*=\s*(\w+)\(\w+\)/,
/\.set\(\"signature\",([\w$]+)\(\w+\)\)/,
/\/yt\.akamaized\.net\/\)\s*\|\|\s*\w+\.set\s*\(.*?\)\s*;\s*\w+\s*&&\s*\w+\.set\s*\(\s*\w+\s*,\s*(?:encodeURIComponent\s*\()?([\w$]+)\s*\(/,
/\b([a-zA-Z0-9$]{,3})\s*=\s*function\(\s*a\s*\)\s*{\s*a\s*=\s*a\.split\(\s*""\s*\)/,
/([a-zA-Z0-9$]+)\s*=\s*function\(\s*a\s*\)\s*{\s*a\s*=\s*a\.split\(\s*""\s*\)\s*;\s*\w+\.\w+\s*\(/,
/([a-zA-Z0-9$]+)\s*=\s*function\(\s*a\s*\)\s*{\s*a\s*=\s*a\.split\(\s*""\s*\)/,
/;\s*\w+\s*&&\s*\w+\.set\(\w+\s*,\s*(?:encodeURIComponent\s*\()?([\w$]+)\s*\(/,
/;\s*\w+\s*&&\s*\w+\.set\(\w+\s*,\s*\([^)]*\)\s*\(\s*([\w$]+)\s*\(/
], function(idx, regex) {
if(data.match(regex)) {
sigFn = RegExp.$1;
return false;
}
});
if(sigFn == null)
return;
//console.log(scriptName + " sig fn: " + sigFn);
var fnArgBody = '\\s*\\((\\w+)\\)\\s*{(\\w+=\\w+\\.split\\(""\\);.+?;return \\w+\\.join\\(""\\))';
if(!data.match(new RegExp("function " + RegExp.escape(sigFn) + fnArgBody)) &&
!data.match(new RegExp("(?:var |[,;]\\s*|^\\s*)" + RegExp.escape(sigFn) + "\\s*=\\s*function" + fnArgBody)))
return;
var fnParam = RegExp.$1;
var fnBody = RegExp.$2;
var fnHlp = {};
var objHlp = {};
//console.log("param: " + fnParam);
//console.log(fnBody);
fnBody = fnBody.split(";");
forEach(fnBody, function(idx, elm) {
// its own property
if(elm.match(new RegExp("^" + fnParam + "=" + fnParam + "\\.")))
return;
// global fn
if(elm.match(new RegExp("^" + fnParam + "=([a-zA-Z_$][a-zA-Z0-9_$]*)\\("))) {
var name = RegExp.$1;
//console.log("fnHlp: " + name);
if(fnHlp[name])
return;
if(data.match(new RegExp("(function " + RegExp.escape(RegExp.$1) + ".+?;return \\w+})")))
fnHlp[name] = RegExp.$1;
return;
}
// object fn
if(elm.match(new RegExp("^([a-zA-Z_$][a-zA-Z0-9_$]*)\.([a-zA-Z_$][a-zA-Z0-9_$]*)\\("))) {
var name = RegExp.$1;
//console.log("objHlp: " + name);
if(objHlp[name])
return;
if(data.match(new RegExp("(var " + RegExp.escape(RegExp.$1) + "={.+?};)")))
objHlp[name] = RegExp.$1;
return;
}
});
//console.log(fnHlp);
//console.log(objHlp);
var fnHlpStr = "";
for(var k in fnHlp)
fnHlpStr += fnHlp[k];
for(var k in objHlp)
fnHlpStr += objHlp[k];
var fullFn = "function(" + fnParam + "){" + fnHlpStr + fnBody.join(";") + "}";
//console.log(fullFn);
decryptSig.store[scriptName] = { ver: relInfo.ver, ts: timeNowInSec(), fn: fullFn };
//console.log(decryptSig);
decryptSig.save();
}
// Entry point
dom.crossAjax({ url: url, success: success });
};
decryptSig.condFetchScript = function(url) {
var scriptName = decryptSig.getScriptName(url);
var store = decryptSig.store[scriptName];
var now = timeNowInSec();
if(store && now - store.ts < CHK_SIG_INTERVAL && store.ver == relInfo.ver)
return;
decryptSig.fetchScript(scriptName, url);
};
}) ();
function deobfuscateVideoSig(scriptName, sig) {
if(!decryptSig.store[scriptName])
return sig;
//console.log(decryptSig.store[scriptName].fn);
try {
sig = eval("(" + decryptSig.store[scriptName].fn + ") (\"" + sig + "\")");
} catch(e) {
}
return sig;
}
// =============================================================================
function deobfuscateSigInObj(map, obj) {
if(obj.s == null || obj.sig != null)
return;
var sig = deobfuscateVideoSig(map.scriptName, obj.s);
if(sig != obj.s) {
obj.sig = sig;
delete obj.s;
}
}
function parseStreamMap(map, value) {
var fmtUrlList = [];
forEach(value.split(","), function(idx, elm) {
var elms = elm.replace(/\\\//g, "/").replace(/\\u0026/g, "&").split("&");
var obj = {};
forEach(elms, function(idx, elm) {
var kv = elm.split("=");
obj[kv[0]] = decodeURIComponent(kv[1]);
});
obj.itag = +obj.itag;
if(obj.conn != null && obj.conn.match(/^rtmpe:\/\//))
obj.isDrm = true;
if(obj.s != null && obj.sig == null) {
var sig = deobfuscateVideoSig(map.scriptName, obj.s);
if(sig != obj.s) {
obj.sig = sig;
delete obj.s;
}
}
fmtUrlList.push(obj);
});
//logMsg(fmtUrlList);
map.fmtUrlList = fmtUrlList;
}
function parseAdaptiveStreamMap(map, value) {
var fmtUrlList = [];
forEach(value.split(","), function(idx, elm) {
var elms = elm.replace(/\\\//g, "/").replace(/\\u0026/g, "&").split("&");
var obj = {};
forEach(elms, function(idx, elm) {
var kv = elm.split("=");
obj[kv[0]] = decodeURIComponent(kv[1]);
});
obj.itag = +obj.itag;
if(obj.bitrate != null)
obj.bitrate = +obj.bitrate;
if(obj.clen != null)
obj.clen = +obj.clen;
if(obj.fps != null)
obj.fps = +obj.fps;
//logMsg(obj);
//logMsg(map.videoId + ": " + obj.index + " " + obj.init + " " + obj.itag + " " + obj.size + " " + obj.bitrate + " " + obj.type);
if(obj.type.match(/^video\/mp4/) && !obj.type.match(/;\s*\+?codecs="av01\./))
obj.effType = "video/x-m4v";
if(obj.type.match(/^audio\//))
obj.size = "audio";
obj.quality = mapResToQuality(obj.size);
if(!map.adaptiveAR && obj.size.match(/^(\d+)x(\d+)/))
map.adaptiveAR = +RegExp.$1 / +RegExp.$2;
deobfuscateSigInObj(map, obj);
fmtUrlList.push(obj);
map.fmtMap[obj.itag] = { res: cnvResName(obj.size) };
});
//logMsg(fmtUrlList);
map.fmtUrlList = map.fmtUrlList.concat(fmtUrlList);
}
function parseFmtList(map, value) {
var list = value.split(",");
forEach(list, function(idx, elm) {
var elms = elm.replace(/\\\//g, "/").split("/");
var fmtId = elms[0];
var res = elms[1];
elms.splice(/*idx*/ 0, /*rm*/ 2);
if(map.adaptiveAR && res.match(/^(\d+)x(\d+)/))
res = Math.round(+RegExp.$2 * map.adaptiveAR) + "x" + RegExp.$2;
map.fmtMap[fmtId] = { res: cnvResName(res), vars: elms };
});
//logMsg(map.fmtMap);
}
function parseNewFormatsMap(map, str, unescSlashFlag) {
if(unescSlashFlag)
str = str.replace(/\\\//g, "/").replace(/\\"/g, "\"").replace(/\\\\/g, "\\");
var list = JSON.parse(str);
forEach(list, function(idx, elm) {
var obj = {
bitrate: elm.bitrate,
fps: elm.fps,
itag: elm.itag,
type: elm.mimeType,
url: elm.url // no longer present (2020-06)
};
// Distinguish between AV1, M4V and MP4
if(elm.audioQuality == null && obj.type.match(/^video\/mp4/) && !obj.type.match(/;\s*\+?codecs="av01\./))
obj.effType = "video/x-m4v";
if(elm.contentLength != null)
obj.clen = +elm.contentLength;
if(obj.type.match(/^audio\//))
obj.size = "audio";
else
obj.size = elm.width + "x" + elm.height;
obj.quality = mapResToQuality(obj.size);
var cipher = elm.cipher || elm.signatureCipher;
if(cipher) {
forEach(cipher.split("&"), function(idx, elm) {
var kv = elm.split("=");
obj[kv[0]] = decodeURIComponent(kv[1]);
});
deobfuscateSigInObj(map, obj);
}
map.fmtUrlList.push(obj);
if(map.fmtMap[obj.itag] == null)
map.fmtMap[obj.itag] = { res: cnvResName(obj.size) };
});
}
function getVideoInfo(url, callback) {
function getVideoNameByType(elm) {
return getVideoName(elm.effType || elm.type);
}
function success(data) {
var map = {};
if(data.match(/<div\s+id="verify-details">/)) {
logMsg("Skipping " + url);
return;
}
if(data.match(/<h1\s+id="unavailable-message">/)) {
logMsg("Not avail " + url);
return;
}
if(data.match(/"t":\s?"(.+?)"/))
map.t = RegExp.$1;
if(data.match(/"(?:video_id|videoId)":\s?"(.+?)"/))
map.videoId = RegExp.$1;
else if(data.match(/\\"videoId\\":\s?\\"(.+?)\\"/))
map.videoId = RegExp.$1;
else if(data.match(/'VIDEO_ID':\s?"(.+?)",/))
map.videoId = RegExp.$1;
if(!map.videoId) {
logMsg("No videoId; skipping " + url);
return;
}
map.scriptUrl = decryptSig.extractScriptUrl(data);
if(map.scriptUrl) {
//logMsg(map.videoId + " script: " + map.scriptUrl);
map.scriptName = decryptSig.getScriptName(map.scriptUrl);
decryptSig.condFetchScript(map.scriptUrl);
}
if(data.match(/<meta\s+itemprop="name"\s*content="(.+?)"\s*>\s*\n/))
map.title = unescHtmlEntities(RegExp.$1);
if(map.title == null && data.match(/<meta\s+name="title"\s*content="(.+?)"\s*>/))
map.title = unescHtmlEntities(RegExp.$1);
var titleStream;
if(map.title == null && data.match(/"videoDetails":{(.*?)}[,}]/))
titleStream = RegExp.$1;
else
titleStream = data;
// Edge replaces & with \u0026
if(map.title == null && titleStream.match(/[,{]"title":("[^"]+")[,}]/))
map.title = unescHtmlEntities(JSON.parse(RegExp.$1));
// Edge fails the previous regex if \" exists
if(map.title == null && titleStream.match(/[,{]"title":(".*?")[,}]"/))
map.title = unescHtmlEntities(JSON.parse(RegExp.$1));
if(data.match(/[,{]\\"isLiveContent\\":\s*true[,}]/))
map.isLive = true;
map.fmtUrlList = [];
var oldFmtFlag;
var newFmtFlag;
if(data.match(/[,{]"url_encoded_fmt_stream_map":\s?"([^"]+)"[,}]/)) {
parseStreamMap(map, RegExp.$1);
oldFmtFlag = true;
}
map.fmtMap = {};
if(data.match(/[,{]"adaptive_fmts":\s?"(.+?)"[,}]/)) {
parseAdaptiveStreamMap(map, RegExp.$1);
oldFmtFlag = true;
}
if(data.match(/[,{]"fmt_list":\s?"([^"]+)"[,}]/))
parseFmtList(map, RegExp.$1);
// Is part of 'player_response' and is escaped
if(!oldFmtFlag && data.match(/\\"formats\\":(\[{[^\]]*}\])[},]/)) {
parseNewFormatsMap(map, RegExp.$1, /*unescSlash*/ true);
newFmtFlag = true;
}
if(!oldFmtFlag && data.match(/\\"adaptiveFormats\\":(\[{[^\]]*}\])[},]/)) {
parseNewFormatsMap(map, RegExp.$1, /*unescSlash*/ true);
newFmtFlag = true;
}
// Is part of 'ytInitialPlayerResponse' and is not escaped
if(!oldFmtFlag && !newFmtFlag) {
if(data.match(/[,{]"formats":(\[{[^\]]*}\])[},]/))
parseNewFormatsMap(map, RegExp.$1);
if(data.match(/[,{]"adaptiveFormats":(\[{[^\]]*}\])[},]/))
parseNewFormatsMap(map, RegExp.$1);
}
if(data.match(/[,{]"dashmpd":\s?"(.+?)"[,}]/))
map.dashmpd = decodeURIComponent(RegExp.$1.replace(/\\\//g, "/"));
else if(data.match(/[,{]\\"dashManifestUrl\\":\s?\\"(.+?)\\"[,}]/))
map.dashmpd = decodeURIComponent(RegExp.$1.replace(/\\\//g, "/"));
if(userConfig.filteredFormats.length > 0) {
for(var i = 0; i < map.fmtUrlList.length; ++i) {
if(inArray(getVideoNameByType(map.fmtUrlList[i]), userConfig.filteredFormats) >= 0) {
map.fmtUrlList.splice(i, /*len*/ 1);
--i;
continue;
}
}
}
var hasHighRes = false;
var hasHighAudio = false;
var HIGH_AUDIO_BPS = 96 * 1024;
forEach(map.fmtUrlList, function(idx, elm) {
hasHighRes |= elm.quality == "hd720" || elm.quality == "hd1080";
if(elm.quality == "audio")
hasHighAudio |= elm.bitrate >= HIGH_AUDIO_BPS;
});
if(hasHighRes) {
for(var i = 0; i < map.fmtUrlList.length; ++i) {
if(inArray(getVideoNameByType(map.fmtUrlList[i]), userConfig.keepFormats) >= 0)
continue;
if(map.fmtUrlList[i].quality == "small") {
map.fmtUrlList.splice(i, /*len*/ 1);
--i;
continue;
}
}
}
if(hasHighAudio) {
for(var i = 0; i < map.fmtUrlList.length; ++i) {
if(inArray(getVideoNameByType(map.fmtUrlList[i]), userConfig.keepFormats) >= 0)
continue;
if(map.fmtUrlList[i].quality == "audio" && map.fmtUrlList[i].bitrate < HIGH_AUDIO_BPS) {
map.fmtUrlList.splice(i, /*len*/ 1);
--i;
continue;
}
}
}
map.fmtUrlList.sort(cmpUrlList);
callback(map);
}
// Entry point
dom.ajax({ url: url, success: success });
}
function cmpUrlList(a, b) {
var diff = getQualityIdx(b.quality) - getQualityIdx(a.quality);
if(diff != 0)
return diff;
var aRes = (a.size || "").match(/^(\d+)x(\d+)/);
var bRes = (b.size || "").match(/^(\d+)x(\d+)/);
if(aRes == null) aRes = [ 0, 0, 0 ];
if(bRes == null) bRes = [ 0, 0, 0 ];
diff = +bRes[2] - +aRes[2];
if(diff != 0)
return diff;
var aFps = a.fps || 0;
var bFps = b.fps || 0;
return bFps - aFps;
}
// -----------------------------------------------------------------------------
var CSS_PREFIX = "ujs-";
var HDR_LINKS_HTML_ID = CSS_PREFIX + "hdr-links-div";
var LINKS_HTML_ID = CSS_PREFIX + "links-cls";
var LINKS_TP_HTML_ID = CSS_PREFIX + "links-tp-div";
var UPDATE_HTML_ID = CSS_PREFIX + "update-div";
var VID_FMT_BTN_ID = CSS_PREFIX + "vid-fmt-btn";
/* The !important attr is to override the page's specificity. */
var CSS_STYLES =
"#" + VID_FMT_BTN_ID + dom.emitCssStyles({
"cursor": "pointer",
"margin": "0 0.333em",
"padding": "0.5em"
}) + "\n" +
"#" + UPDATE_HTML_ID + dom.emitCssStyles({
"background-color": "#f00",
"border-radius": "2px",
"color": "#fff",
"padding": "5px",
"text-align": "center",
"text-decoration": "none",
"position": "fixed",
"top": "0.5em",
"right": "0.5em",
"z-index": "1000"
}) + "\n" +
"#" + UPDATE_HTML_ID + ":hover" + dom.emitCssStyles({
"background-color": "#0d0"
}) + "\n" +
"#page-container #" + HDR_LINKS_HTML_ID + dom.emitCssStyles({
"font-size": "90%"
}) + "\n" +
"#page-manager #" + HDR_LINKS_HTML_ID + dom.emitCssStyles({ // 2017 Material Design
"font-size": "1.2em"
}) + "\n" +
"#" + HDR_LINKS_HTML_ID + dom.emitCssStyles({
"background-color": "#f8f8f8",
"border": "#eee 1px solid",
//"border-radius": "3px",
"color": "#333",
"margin": "5px",
"padding": "5px"
}) + "\n" +
"html[dark] #" + HDR_LINKS_HTML_ID + dom.emitCssStyles({
"background-color": "#222",
"border": "none"
}) + "\n" +
"#" + HDR_LINKS_HTML_ID + " ." + CSS_PREFIX + "group" + dom.emitCssStyles({
"background-color": "#fff",
"color": "#000 !important",
"border": "#ccc 1px solid",
"border-radius": "3px",
"display": "inline-block",
"margin": "3px",
}) + "\n" +
"html[dark] #" + HDR_LINKS_HTML_ID + " ." + CSS_PREFIX + "group" + dom.emitCssStyles({
"background-color": "#444",
"color": "#fff !important",
"border": "none"
}) + "\n" +
"#" + HDR_LINKS_HTML_ID + " a" + dom.emitCssStyles({
"display": "table-cell",
"padding": "3px",
"text-decoration": "none"
}) + "\n" +
"#" + HDR_LINKS_HTML_ID + " a:hover" + dom.emitCssStyles({
"background-color": "#d1e1fa"
}) + "\n" +
"div." + LINKS_HTML_ID + dom.emitCssStyles({
"border-radius": "3px",
"cursor": "default",
"line-height": "1em",
"position": "absolute",
"left": "0",
"top": "0",
"z-index": "1000"
}) + "\n" +
"#page-manager div." + LINKS_HTML_ID + dom.emitCssStyles({ // 2017 Material Design
"font-size": "1.2em",
"padding": "2px 4px"
}) + "\n" +
"div." + LINKS_HTML_ID + ".layout2017" + dom.emitCssStyles({ // 2017 Material Design
"font-size": "1.2em"
}) + "\n" +
"#" + LINKS_TP_HTML_ID + dom.emitCssStyles({
"background-color": "#f0f0f0",
"border": "#aaa 1px solid",
"padding": "3px 0",
"text-decoration": "none",
"white-space": "nowrap",
"z-index": "1100"
}) + "\n" +
"html[dark] #" + LINKS_TP_HTML_ID + dom.emitCssStyles({
"background-color": "#222"
}) + "\n" +
"div." + LINKS_HTML_ID + " a" + dom.emitCssStyles({
"display": "inline-block",
"margin": "1px",
"text-decoration": "none"
}) + "\n" +
"div." + LINKS_HTML_ID + " ." + CSS_PREFIX + "video" + dom.emitCssStyles({
"display": "inline-block",
"text-align": "center",
"width": "3.5em"
}) + "\n" +
"div." + LINKS_HTML_ID + " ." + CSS_PREFIX + "quality" + dom.emitCssStyles({
"display": "inline-block",
"text-align": "center",
"width": "5.5em"
}) + "\n" +
"." + CSS_PREFIX + "video" + dom.emitCssStyles({
"color": "#fff !important",
"padding": "1px 3px",
"text-align": "center"
}) + "\n" +
"." + CSS_PREFIX + "quality" + dom.emitCssStyles({
"color": "#000 !important",
"display": "table-cell",
"min-width": "1.5em",
"padding": "1px 3px",
"text-align": "center",
"vertical-align": "middle"
}) + "\n" +
"html[dark] ." + CSS_PREFIX + "quality" + dom.emitCssStyles({
"color": "#fff !important"
}) + "\n" +
"." + CSS_PREFIX + "filesize" + dom.emitCssStyles({
"font-size": "90%",
"margin-top": "2px",
"padding": "1px 3px",
"text-align": "center"
}) + "\n" +
"html[dark] ." + CSS_PREFIX + "filesize" + dom.emitCssStyles({
"color": "#999"
}) + "\n" +
"." + CSS_PREFIX + "filesize-err" + dom.emitCssStyles({
"color": "#f00",
"font-size": "90%",
"margin-top": "2px",
"padding": "1px 3px",
"text-align": "center"
}) + "\n" +
"." + CSS_PREFIX + "not-avail" + dom.emitCssStyles({
"background-color": "#700",
"color": "#fff",
"padding": "3px",
}) + "\n" +
"." + CSS_PREFIX + "3gp" + dom.emitCssStyles({
"background-color": "#bbb"
}) + "\n" +
"." + CSS_PREFIX + "av1" + dom.emitCssStyles({
"background-color": "#f5f"
}) + "\n" +
"." + CSS_PREFIX + "flv" + dom.emitCssStyles({
"background-color": "#0dd"
}) + "\n" +
"." + CSS_PREFIX + "m4a" + dom.emitCssStyles({
"background-color": "#07e"
}) + "\n" +
"." + CSS_PREFIX + "m4v" + dom.emitCssStyles({
"background-color": "#07e"
}) + "\n" +
"." + CSS_PREFIX + "mp3" + dom.emitCssStyles({
"background-color": "#7ba"
}) + "\n" +
"." + CSS_PREFIX + "mp4" + dom.emitCssStyles({
"background-color": "#777"
}) + "\n" +
"." + CSS_PREFIX + "opus" + dom.emitCssStyles({
"background-color": "#e0e"
}) + "\n" +
"." + CSS_PREFIX + "qt" + dom.emitCssStyles({
"background-color": "#f08"
}) + "\n" +
"." + CSS_PREFIX + "vor" + dom.emitCssStyles({
"background-color": "#e0e"
}) + "\n" +
"." + CSS_PREFIX + "vp9" + dom.emitCssStyles({
"background-color": "#e0e"
}) + "\n" +
"." + CSS_PREFIX + "webm" + dom.emitCssStyles({
"background-color": "#d4d"
}) + "\n" +
"." + CSS_PREFIX + "wmv" + dom.emitCssStyles({
"background-color": "#c75"
}) + "\n" +
"." + CSS_PREFIX + "small" + dom.emitCssStyles({
"color": "#888 !important",
}) + "\n" +
"." + CSS_PREFIX + "medium" + dom.emitCssStyles({
"color": "#fff !important",
"background-color": "#0d0"
}) + "\n" +
"." + CSS_PREFIX + "large" + dom.emitCssStyles({
"color": "#fff !important",
"background-color": "#00d",
"background-image": "linear-gradient(to right, #00d, #00a)"
}) + "\n" +
"." + CSS_PREFIX + "hd720" + dom.emitCssStyles({
"color": "#fff !important",
"background-color": "#f90",
"background-image": "linear-gradient(to right, #f90, #d70)"
}) + "\n" +
"." + CSS_PREFIX + "hd1080" + dom.emitCssStyles({
"color": "#fff !important",
"background-color": "#f00",
"background-image": "linear-gradient(to right, #f00, #c00)"
}) + "\n" +
"." + CSS_PREFIX + "highres" + dom.emitCssStyles({
"color": "#fff !important",
"background-color": "#c0f",
"background-image": "linear-gradient(to right, #c0f, #90f)"
}) + "\n" +
"." + CSS_PREFIX + "ultrahighres" + dom.emitCssStyles({
"color": "#fff !important",
"background-color": "#ffe42b",
"background-image": "linear-gradient(to right, #ffe42b, #dfb200)"
}) + "\n" +
"." + CSS_PREFIX + "pos-rel" + dom.emitCssStyles({
"position": "relative"
}) + "\n" +
"#" + HDR_LINKS_HTML_ID + " a.flash:hover" + dom.emitCssStyles({
"background-color": "#ffa",
"transition": "background-color 0.25s linear"
}) + "\n" +
"#" + HDR_LINKS_HTML_ID + " a.flash-out:hover" + dom.emitCssStyles({
"transition": "background-color 0.25s linear"
}) + "\n" +
"div." + LINKS_HTML_ID + " a.flash div" + dom.emitCssStyles({
"background-color": "#ffa",
"transition": "background-color 0.25s linear"
}) + "\n" +
"div." + LINKS_HTML_ID + " a.flash-out div" + dom.emitCssStyles({
"transition": "background-color 0.25s linear"
}) + "\n" +
"";
function condInsertHdr(divId) {
if(dom.gE(HDR_LINKS_HTML_ID))
return true;
var insertPtNode = dom.gE(divId);
if(!insertPtNode)
return false;
var divNode = dom.cE("div");
divNode.id = HDR_LINKS_HTML_ID;
insertPtNode.parentNode.insertBefore(divNode, insertPtNode);
return true;
}
function condRemoveHdr() {
var node = dom.gE(HDR_LINKS_HTML_ID);
if(node)
node.parentNode.removeChild(node);
}
function condInsertTooltip() {
if(dom.gE(LINKS_TP_HTML_ID))
return true;
var toolTipNode = dom.cE("div");
toolTipNode.id = LINKS_TP_HTML_ID;
var cls = [ LINKS_HTML_ID ];
if(dom.gE("page-manager"))
cls.push("layout2017");
dom.attr(toolTipNode, "class", cls.join(" "));
dom.attr(toolTipNode, "style", "display: none;");
dom.append(doc.body, toolTipNode);
dom.addEvent(toolTipNode, "mouseleave", function(evt) {
//logMsg("mouse leave");
dom.attr(toolTipNode, "style", "display: none;");
stopChkMouseInPopup();
});
}
function condInsertUpdateIcon() {
if(dom.gE(UPDATE_HTML_ID))
return;
var divNode = dom.cE("a");
divNode.id = UPDATE_HTML_ID;
dom.append(doc.body, divNode);
}
// -----------------------------------------------------------------------------
var STORE_ID = "ujsYtLinks";
var JSONP_ID = "ujsYtLinks";
// User settings can be saved in localStorage. Refer to documentation for details.
var userConfig = {
copyToClipboard: true,
filteredFormats: [],
keepFormats: [],
showVideoFormats: true,
showVideoSize: true,
tagLinks: true,
useDecUnits: true
};
var videoInfoCache = {};
var TAG_LINK_NUM_PER_BATCH = 5;
var INI_TAG_LINK_DELAY_MS = 200;
var SUB_TAG_LINK_DELAY_MS = 350;
// -----------------------------------------------------------------------------
var FULL_AR_CUTOFF = 1.5;
var WIDE_AR_CUTOFF = 2.0;
var ULTRA_WIDE_AR_CUTOFF = 2.3;
var HFR_CUTOFF = 45;
var fmtSizeSuffix = [ "kB", "MB", "GB" ];
var fmtSizeUnit = 1000;
function Links() {
}
Links.prototype.init = function() {
for(var k in userConfig) {
try {
var v = localStorage.getItem(STORE_ID + ".cfg." + k);
if(v != null)
userConfig[k] = JSON.parse(v);
} catch(e) {
logMsg(k + ": unable to parse '" + v + "'");
}
}
};
Links.prototype.getPreferredFmt = function(map) {
var selElm = map.fmtUrlList[0];
forEach(map.fmtUrlList, function(idx, elm) {
if(getVideoName(elm.type).toLowerCase() != "webm") {
selElm = elm;
return false;
}
});
return selElm;
};
Links.prototype.parseDashManifest = function(map, callback) {
function parse(xml) {
//logMsg(xml);
var dashList = [];
var adaptationSetDom = xml.getElementsByTagName("AdaptationSet");
//logMsg(adaptationSetDom);
forEach(adaptationSetDom, function(i, adaptationElm) {
var mimeType = adaptationElm.getAttribute("mimeType");
//logMsg(i + " " + mimeType);
var representationDom = adaptationElm.getElementsByTagName("Representation");
forEach(representationDom, function(j, repElm) {
var dashElm = { mimeType: mimeType };
forEach([ "codecs" ], function(idx, elm) {
var v = repElm.getAttribute(elm);
if(v != null)
dashElm[elm] = v;
});
forEach([ "audioSamplingRate", "bandwidth", "frameRate", "height", "id", "width" ], function(idx, elm) {
var v = repElm.getAttribute(elm);
if(v != null)
dashElm[elm] = +v;
});
var baseUrlDom = repElm.getElementsByTagName("BaseURL");
dashElm.len = +baseUrlDom[0].getAttribute("yt:contentLength");
dashElm.url = baseUrlDom[0].textContent;
var segList = repElm.getElementsByTagName("SegmentList");
if(segList.length > 0)
dashElm.numSegments = segList[0].childNodes.length;
dashList.push(dashElm);
});
});
//logMsg(map);
//logMsg(dashList);
var maxBitRateMap = {};
forEach(dashList, function(idx, dashElm) {
if(dashElm.mimeType != "video/mp4" && dashElm.mimeType != "video/webm")
return;
var id = [ dashElm.mimeType, dashElm.width, dashElm.height, dashElm.frameRate ].join("|");
if(maxBitRateMap[id] == null || maxBitRateMap[id] < dashElm.bandwidth)
maxBitRateMap[id] = dashElm.bandwidth;
});
forEach(dashList, function(idx, dashElm) {
var foundIdx;
forEach(map.fmtUrlList, function(idx, mapElm) {
if(dashElm.id == mapElm.itag) {
foundIdx = idx;
return false;
}
});
if(foundIdx != null) {
if(dashElm.numSegments != null)
map.fmtUrlList[foundIdx].numSegments = dashElm.numSegments;
return;
}
//logMsg(dashElm);
if((dashElm.mimeType == "video/mp4" || dashElm.mimeType == "video/webm") && (dashElm.width >= 1000 || dashElm.height >= 1000)) {
var id = [ dashElm.mimeType, dashElm.width, dashElm.height, dashElm.frameRate ].join("|");
if(maxBitRateMap[id] == null || dashElm.bandwidth < maxBitRateMap[id])
return;
var size = dashElm.width + "x" + dashElm.height;
if(map.fmtMap[dashElm.id] == null)
map.fmtMap[dashElm.id] = { res: cnvResName(size) };
map.fmtUrlList.push({
bitrate: dashElm.bandwidth,
effType: dashElm.mimeType == "video/mp4" ? "video/x-m4v" : null,
filesize: dashElm.len,
fps: dashElm.frameRate,
itag: dashElm.id,
quality: mapResToQuality(size),
size: size,
type: dashElm.mimeType + ";+codecs=\"" + dashElm.codecs + "\"",
url: dashElm.url,
numSegments: dashElm.numSegments
});
}
else if(dashElm.mimeType == "audio/mp4" && dashElm.audioSamplingRate >= 44100) {
if(map.fmtMap[dashElm.id] == null) {
map.fmtMap[dashElm.id] = { res: "Audio" };
}
map.fmtUrlList.push({
bitrate: dashElm.bandwidth,
filesize: dashElm.len,
itag: dashElm.id,
quality: "audio",
type: dashElm.mimeType + ";+codecs=\"" + dashElm.codecs + "\"",
url: dashElm.url
});
}
});
if(condInsertHdr(me.getInsertPt()))
me.createLinks(dom.gE(HDR_LINKS_HTML_ID), map);
}
// Entry point
var me = this;
if(!map.dashmpd) {
setTimeout(callback, 0);
return;
}
//logMsg(map.dashmpd);
if(map.dashmpd.match(/\/s\/([a-zA-Z0-9.]+)\//)) {
var sig = deobfuscateVideoSig(map.scriptName, RegExp.$1);
map.dashmpd = map.dashmpd.replace(/\/s\/[a-zA-Z0-9.]+\//, "/sig/" + sig + "/");
}
dom.crossAjax({
url: map.dashmpd,
dataType: "xml",
success: function(data, status, xhr) {
parse(data);
callback();
},
error: function(xhr, status) {
callback();
},
complete: function(xhr) {
}
});
};
Links.prototype.checkFmts = function(forceFlag) {
var me = this;
if(!userConfig.showVideoFormats)
return;
if(!forceFlag && userConfig.showVideoFormats == "btn") {
condRemoveHdr();
if(dom.gE(VID_FMT_BTN_ID))
return;
// 'container' is for Material Design
var mastH = dom.gE("yt-masthead-signin") || dom.gE("yt-masthead-user") || dom.gE("end") || dom.gE("container");
if(!mastH)
return;
var btn = dom.cE("button");
dom.attr(btn, "id", VID_FMT_BTN_ID);
dom.attr(btn, "class", "yt-uix-button yt-uix-button-default");
btn.innerHTML = "VidFmts";
dom.prepend(mastH, btn);
dom.addEvent(btn, "click", function(evt) {
me.checkFmts(/*force*/ true);
});
return;
}
if(!loc.href.match(/watch\?(?:.+&)?v=([a-zA-Z0-9_-]+)/))
return false;
var videoId = RegExp.$1;
var url = loc.protocol + "//" + loc.host + "/watch?v=" + videoId;
var curVideoUrl = loc.toString();
getVideoInfo(url, function(map) {
me.parseDashManifest(map, function() {
// Has become stale (eg switch forward/back pages quickly)
if(curVideoUrl != loc.toString())
return;
me.showLinks(me.getInsertPt(), map);
});
});
};
Links.prototype.genUrl = function(map, elm) {
var url = elm.url + "&title=" + encodeSafeFname(map.title);
if(elm.sig != null)
url += "&sig=" + elm.sig;
return url;
};
Links.prototype.emitLinks = function(map) {
function fmtSize(size, units, divisor) {
if(!units) {
units = fmtSizeSuffix;
divisor = fmtSizeUnit;
}
for(var idx = 0; idx < units.length; ++idx) {
size /= divisor;
if(size < 10)
return Math.round(size * 100) / 100 + units[idx];
if(size < 100)
return Math.round(size * 10) / 10 + units[idx];
if(size < 1000 || idx == units.length - 1)
return Math.round(size) + units[idx];
}
}
function fmtBitrate(size) {
return fmtSize(size, [ "kbps", "Mbps", "Gbps" ], 1000);
}
function getFileExt(videoName, elm) {
if(videoName == "VP9")
return "video.webm";
if(videoName == "VOR")
return "audio.webm";
return videoName.toLowerCase();
}
// Entry point
var me = this;
var s = [];
var resMap = {};
map.fmtUrlList.sort(cmpUrlList);
forEach(map.fmtUrlList, function(idx, elm) {
var fmtMap = map.fmtMap[elm.itag];
if(!resMap[fmtMap.res]) {
resMap[fmtMap.res] = [];
resMap[fmtMap.res].quality = elm.quality;
}
resMap[fmtMap.res].push(elm);
});
for(var res in resMap) {
var qFields = [];
qFields.push(dom.emitHtml("div", { "class": CSS_PREFIX + "quality " + CSS_PREFIX + resMap[res].quality }, res));
forEach(resMap[res], function(idx, elm) {
var fields = [];
var fmtMap = map.fmtMap[elm.itag];
var videoName = getVideoName(elm.effType || elm.type);
var addMsg = [ elm.itag, elm.type, elm.size || elm.quality ];
if(elm.fps != null)
addMsg.push(elm.fps + "fps");
var varMsg = "";
if(elm.bitrate != null)
varMsg = fmtBitrate(elm.bitrate);
else if(fmtMap.vars != null)
varMsg = fmtMap.vars.join();
addMsg.push(varMsg);
if(elm.s != null)
addMsg.push("sig-" + elm.s.length);
if(elm.filesize != null && elm.filesize >= 0)
addMsg.push(fmtSize(elm.filesize));
var vidSuffix = "";
if(inArray(elm.itag, [ 82, 83, 84, 100, 101, 102 ]) >= 0)
vidSuffix = " (3D)";
else if(elm.fps != null && elm.fps >= HFR_CUTOFF)
vidSuffix = " (HFR)";
fields.push(dom.emitHtml("div", { "class": CSS_PREFIX + "video " + CSS_PREFIX + videoName.toLowerCase() }, videoName + vidSuffix));
if(elm.filesize != null) {
var filesize = elm.filesize;
if((map.isLive || (elm.numSegments || 1) > 1) && filesize == 0)
filesize = -1;
if(filesize >= 0) {
fields.push(dom.emitHtml("div", { "class": CSS_PREFIX + "filesize" }, fmtSize(filesize)));
}
else {
var msg;
if(elm.isDrm)
msg = "DRM";
else if(elm.s != null)
msg = "sig-" + elm.s.length;
else if(elm.numSegments > 1)
msg = "Frag";
else if(map.isLive)
msg = "Live";
else
msg = "Err";
fields.push(dom.emitHtml("div", { "class": CSS_PREFIX + "filesize-err" }, msg));
}
}
var url;
if(elm.isDrm)
url = elm.conn + "?" + elm.stream;
else
url = me.genUrl(map, elm);
var fname = cnvSafeFname(map.title);
var ext = getFileExt(videoName, elm);
if(ext)
fname += "." + ext;
var ahref = dom.emitHtml("a", {
download: fname,
ext: ext,
href: url,
res: res,
title: addMsg.join(" | ")
}, fields.join(""));
qFields.push(ahref);
});
s.push(dom.emitHtml("div", { "class": CSS_PREFIX + "group" }, qFields.join("")));
}
return s.join("");
};
Links.prototype.createLinks = function(insertNode, map) {
function copyToClipboard(text) {
var node = dom.cE("textarea");
// Needed to prevent scrolling to top of page
node.style.position = "fixed";
node.value = text;
dom.append(document.body, node);
node.focus();
node.select();
var ret = false;
try {
if(document.execCommand("copy"))
ret = true;
} catch(e) {
}
document.body.removeChild(node);
return ret;
}
function addCopyHandler(node) {
forEach(dom.gT(node, "a"), function(idx, elm) {
dom.addEvent(elm, "click", function(evt) {
var me = this;
var ext = dom.attr(me, "ext");
var res = dom.attr(me, "res") || "";
// This is the only video that can be downloaded directly
if(ext == "mp4" && res.match(/^[a-z]?720[a-z]$/))
return;
evt.preventDefault();
var fname = dom.attr(me, "download");
//logMsg(fname);
copyToClipboard(fname);
var orgCls = dom.attr(me, "class") || "";
dom.attr(me, "class", orgCls + " flash");
setTimeout(function() { dom.attr(me, "class", orgCls + " flash-out"); }, 250);
setTimeout(function() { dom.attr(me, "class", orgCls); }, 500);
});
});
}
// Entry point
var me = this;
if(insertNode == null)
return;
/* Emit to tmp node first because in GM 4, <a> event does not fire on nodes
already in the DOM. */
var stgNode = dom.cE("div");
dom.html(stgNode, me.emitLinks(map));
if(userConfig.copyToClipboard)
addCopyHandler(stgNode);
dom.html(insertNode, "");
while(stgNode.childNodes.length > 0)
insertNode.appendChild(stgNode.firstChild);
};
var INI_SHOW_FILESIZE_DELAY_MS = 500;
var SUB_SHOW_FILESIZE_DELAY_MS = 150;
var PERIODIC_TAG_LINK_DELAY_MS = 3000;
Links.prototype.showLinks = function(divId, map) {
function updateLinks() {
// Has become stale (eg switch forward/back pages quickly)
if(curVideoUrl != loc.toString())
return;
//!! Hack to update file size
var node = dom.gE(HDR_LINKS_HTML_ID);
if(node)
me.createLinks(node, map);
}
// Entry point
var me = this;
// video is not avail
if(!map.fmtUrlList)
return;
//logMsg(JSON.stringify(map));
if(!condInsertHdr(divId))
return;
me.createLinks(dom.gE(HDR_LINKS_HTML_ID), map);
if(!userConfig.showVideoSize)
return;
var curVideoUrl = loc.toString();
forEach(map.fmtUrlList, function(idx, elm) {
//logMsg(elm.itag + " " + elm.url);
// We just fail outright for protected/obfuscated videos
if(elm.isDrm || elm.s != null) {
elm.filesize = -1;
updateLinks();
return;
}
if(elm.clen != null) {
elm.filesize = elm.clen;
updateLinks();
return;
}
setTimeout(function() {
// Has become stale (eg switch forward/back pages quickly)
if(curVideoUrl != loc.toString())
return;
dom.crossAjax({
type: "HEAD",
url: me.genUrl(map, elm),
success: function(data, status, xhr) {
var filesize = xhr.getResponseHeader("Content-Length");
if(filesize == null)
return;
//logMsg(map.title + " " + elm.itag + ": " + filesize);
elm.filesize = +filesize;
updateLinks();
},
error: function(xhr, status) {
//logMsg(map.fmtMap[elm.itag].res + " " + getVideoName(elm.type) + ": " + xhr.status);
if(xhr.status != 403 && xhr.status != 404)
return;
elm.filesize = -1;
updateLinks();
},
complete: function(xhr) {
//logMsg(map.title + ": " + xhr.getAllResponseHeaders());
}
});
}, INI_SHOW_FILESIZE_DELAY_MS + idx * SUB_SHOW_FILESIZE_DELAY_MS);
});
};
Links.prototype.tagLinks = function() {
var SCANNED = 1;
var REQ_INFO = 2;
var ADDED_INFO = 3;
function prepareTagHtml(node, map) {
var elm = me.getPreferredFmt(map);
var fmtMap = map.fmtMap[elm.itag];
dom.attr(node, "class", LINKS_HTML_ID + " " + CSS_PREFIX + "quality " + CSS_PREFIX + elm.quality);
var label = fmtMap.res;
if(elm.fps >= HFR_CUTOFF)
label += elm.fps;
var tagEvent;
if(userConfig.tagLinks == "label")
tagEvent = "click";
else
tagEvent = "mouseenter";
dom.addEvent(node, tagEvent, function(evt) {
//logMsg("mouse enter " + map.videoId);
var pos = dom.offset(node);
//logMsg("mouse enter: x " + pos.left + ", y " + pos.top);
var toolTipNode = dom.gE(LINKS_TP_HTML_ID);
dom.attr(toolTipNode, "style", "position: absolute; left: " + pos.left + "px; top: " + pos.top + "px");
me.createLinks(toolTipNode, map);
startChkMouseInPopup();
});
return label;
}
function addTag(hNode, map) {
//logMsg(dom.html(hNode));
//logMsg("hNode " + dom.attr(hNode, "class"));
//var img = dom.gT(hNode, "img") [0];
//logMsg(dom.attr(img, "src"));
//logMsg(dom.attr(img, "class"));
dom.attr(hNode, CSS_PREFIX + "processed", ADDED_INFO);
var node = dom.cE("div");
if(map.fmtUrlList && map.fmtUrlList.length > 0) {
tagHtml = prepareTagHtml(node, map);
}
else {
dom.attr(node, "class", LINKS_HTML_ID + " " + CSS_PREFIX + "not-avail");
tagHtml = "NA";
}
var parentNode;
var insNode;
var cls = dom.attr(hNode, "class") || "";
var isVideoWallStill = cls.match(/videowall-still/);
if(isVideoWallStill) {
parentNode = hNode;
insNode = hNode.firstChild;
}
else {
parentNode = hNode.parentNode;
insNode = hNode;
}
// Remove existing tags
var divNodes = parentNode.getElementsByTagName("div");
for(var i = 0; i < divNodes.length; ++i) {
var hNode = divNodes[i];
if(me.isTagDiv(hNode))
hNode.parentNode.removeChild(hNode);
else
++i;
}
var parentCssPositionStyle = window.getComputedStyle(parentNode, null).getPropertyValue("position");
if(parentCssPositionStyle != "absolute" && parentCssPositionStyle != "relative")
dom.attr(parentNode, "class", dom.attr(parentNode, "class") + " " + CSS_PREFIX + "pos-rel");
parentNode.insertBefore(node, insNode);
dom.html(node, tagHtml);
}
function getFmt(videoId, hNode) {
if(videoInfoCache[videoId]) {
addTag(hNode, videoInfoCache[videoId]);
return;
}
var url;
if(videoId.match(/.+==$/))
url = loc.protocol + "//" + loc.host + "/cthru?key=" + videoId;
else
url = loc.protocol + "//" + loc.host + "/watch?v=" + videoId;
getVideoInfo(url, function(map) {
videoInfoCache[videoId] = map;
addTag(hNode, map);
});
}
// Entry point
var me = this;
var list = [];
forEach(dom.gT("a"), function(idx, hNode) {
var href = dom.attr(hNode, "href") || "";
if(!href.match(/watch\?v=([a-zA-Z0-9_-]+)/) &&
!href.match(/watch_videos.+?&video_ids=([a-zA-Z0-9_-]+)/))
return;
var videoId = RegExp.$1;
var oldHref = dom.attr(hNode, CSS_PREFIX + "href");
if(href == oldHref && dom.attr(hNode, CSS_PREFIX + "processed"))
return;
if(!dom.inViewport(hNode))
return;
dom.attr(hNode, CSS_PREFIX + "processed", SCANNED);
dom.attr(hNode, CSS_PREFIX + "href", href);
var cls = dom.attr(hNode, "class") || "";
if(!cls.match(/videowall-still/)) {
if(cls == "yt-button" || cls.match(/yt-uix-button/))
return;
// Material Design
if(cls.match(/ytd-playlist-(panel-)?video-renderer/))
return;
if(dom.attr(hNode.parentNode, "class") == "video-time")
return;
if(dom.html(hNode).match(/video-logo/i))
return;
var img = dom.gT(hNode, "img");
if(img == null || img.length == 0)
return;
img = img[0];
// /yts/img/pixel-*.gif is the placeholder image
// can be null as well
var imgSrc = dom.attr(img, "src") || "";
if(imgSrc.indexOf("ytimg.com") < 0 && !imgSrc.match(/^\/yts\/img\/.*\.gif$/) && imgSrc != "")
return;
var tnSrc = dom.attr(img, "thumb") || "";
if(imgSrc.match(/.+?\/([a-zA-Z0-9_-]*)\/(hq)?default\.jpg$/))
videoId = RegExp.$1;
else if(tnSrc.match(/.+?\/([a-zA-Z0-9_-]*)\/(hq)?default\.jpg$/))
videoId = RegExp.$1;
}
//logMsg(idx + " " + href);
//logMsg("videoId: " + videoId);
list.push({ videoId: videoId, hNode: hNode });
dom.attr(hNode, CSS_PREFIX + "processed", REQ_INFO);
});
forLoop({ num: list.length, inc: TAG_LINK_NUM_PER_BATCH, batchIdx: 0 }, function(idx) {
var batchIdx = this.batchIdx++;
var batchList = list.slice(idx, idx + TAG_LINK_NUM_PER_BATCH);
setTimeout(function() {
forEach(batchList, function(idx, elm) {
//logMsg(batchIdx + " " + idx + " " + elm.hNode.href);
getFmt(elm.videoId, elm.hNode);
});
}, INI_TAG_LINK_DELAY_MS + batchIdx * SUB_TAG_LINK_DELAY_MS);
});
};
Links.prototype.isTagDiv = function(node) {
var cls = dom.attr(node, "class") || "";
return cls.match(new RegExp("(^|\\s+)" + RegExp.escape(LINKS_HTML_ID) + "\\s+" + RegExp.escape(CSS_PREFIX + "quality") + "(\\s+|$)"));
};
Links.prototype.invalidateTagLinks = function() {
var me = this;
if(!userConfig.tagLinks)
return;
forEach(dom.gT("a"), function(idx, hNode) {
hNode.removeAttribute(CSS_PREFIX + "processed");
});
var nodes = dom.gT("div");
for(var i = 0; i < nodes.length; ) {
var hNode = nodes[i];
if(me.isTagDiv(hNode))
hNode.parentNode.removeChild(hNode);
else
++i;
}
};
Links.prototype.periodicTagLinks = function(delayMs) {
function poll() {
me.tagLinks();
me.tagLinksTimerId = setTimeout(poll, PERIODIC_TAG_LINK_DELAY_MS);
}
// Entry point
if(!userConfig.tagLinks)
return;
var me = this;
delayMs = delayMs || 0;
if(me.tagLinksTimerId != null) {
clearTimeout(me.tagLinksTimerId);
delete me.tagLinksTimerId;
}
setTimeout(poll, delayMs);
};
Links.prototype.getInsertPt = function() {
if(dom.gE("page"))
return "page";
else if(dom.gE("columns")) // 2017 Material Design
return "columns";
else
return "top";
};
// -----------------------------------------------------------------------------
Links.prototype.loadSettings = function() {
var obj = localStorage[STORE_ID];
if(obj == null)
return;
obj = JSON.parse(obj);
this.lastChkReqTs = +obj.lastChkReqTs;
this.lastChkTs = +obj.lastChkTs;
this.lastChkVer = +obj.lastChkVer;
};
Links.prototype.storeSettings = function() {
localStorage[STORE_ID] = JSON.stringify({
lastChkReqTs: this.lastChkReqTs,
lastChkTs: this.lastChkTs,
lastChkVer: this.lastChkVer
});
};
// -----------------------------------------------------------------------------
var UPDATE_CHK_INTERVAL = 5 * 86400;
var FAIL_TO_CHK_UPDATE_INTERVAL = 14 * 86400;
Links.prototype.chkVer = function(forceFlag) {
if(this.lastChkVer > relInfo.ver) {
this.showNewVer({ ver: this.lastChkVer });
return;
}
var now = timeNowInSec();
//logMsg("lastChkReqTs " + this.lastChkReqTs + ", diff " + (now - this.lastChkReqTs));
//logMsg("lastChkTs " + this.lastChkTs);
//logMsg("lastChkVer " + this.lastChkVer);
if(this.lastChkReqTs == null || now < this.lastChkReqTs) {
this.lastChkReqTs = now;
this.storeSettings();
return;
}
if(now - this.lastChkReqTs < UPDATE_CHK_INTERVAL)
return;
if(this.lastChkReqTs - this.lastChkTs > FAIL_TO_CHK_UPDATE_INTERVAL)
logMsg("Failed to check ver for " + ((this.lastChkReqTs - this.lastChkTs) / 86400) + " days");
this.lastChkReqTs = now;
this.storeSettings();
unsafeWin[JSONP_ID] = this;
var script = dom.cE("script");
script.type = "text/javascript";
script.src = SCRIPT_UPDATE_LINK;
dom.append(doc.body, script);
};
Links.prototype.chkVerCallback = function(data) {
delete unsafeWin[JSONP_ID];
this.lastChkTs = timeNowInSec();
this.storeSettings();
//logMsg(JSON.stringify(data));
var latestElm = data[0];
if(latestElm.ver <= relInfo.ver)
return;
this.showNewVer(latestElm);
};
Links.prototype.showNewVer = function(latestElm) {
function getVerStr(ver) {
var verStr = "" + ver;
var majorV = verStr.substr(0, verStr.length - 4) || "0";
var minorV = verStr.substr(verStr.length - 4, 2);
return majorV + "." + minorV;
}
// Entry point
this.lastChkVer = latestElm.ver;
this.storeSettings();
condInsertUpdateIcon();
var aNode = dom.gE(UPDATE_HTML_ID);
aNode.href = SCRIPT_LINK;
if(latestElm.desc != null)
dom.attr(aNode, "title", latestElm.desc);
dom.html(aNode, dom.emitHtml("b", SCRIPT_NAME + " " + getVerStr(relInfo.ver)) +
"<br>Click to update to " + getVerStr(latestElm.ver));
};
// -----------------------------------------------------------------------------
var WAIT_FOR_READY_POLL_MS = 300;
var SCROLL_TAG_LINK_DELAY_MS = 200;
var inst;
function waitForReady() {
function start() {
inst = new Links();
inst.init();
inst.loadSettings();
decryptSig.load();
if(!userConfig.useDecUnits) {
fmtSizeSuffix = [ "KiB", "MiB", "GiB" ];
fmtSizeUnit = 1024;
}
dom.insertCss(CSS_STYLES);
condInsertTooltip();
if(loc.pathname.match(/\/watch/))
inst.checkFmts();
inst.periodicTagLinks();
inst.chkVer();
}
// Entry point
// 'columns' is for Material Design
if(dom.gE("page") || dom.gE("columns") || dom.gE("top")) {
start();
return;
}
if(!dom.gE("top"))
setTimeout(waitForReady, WAIT_FOR_READY_POLL_MS);
}
var scrollTop = win.pageYOffset || doc.documentElement.scrollTop;
dom.addEvent(win, "scroll", function(e) {
var newScrollTop = win.pageYOffset || doc.documentElement.scrollTop;
if(Math.abs(newScrollTop - scrollTop) < 100)
return;
//logMsg("scroll by " + (newScrollTop - scrollTop));
scrollTop = newScrollTop;
if(inst)
inst.periodicTagLinks(SCROLL_TAG_LINK_DELAY_MS);
});
// -----------------------------------------------------------------------------
var CHK_MOUSE_IN_POPUP_POLL_MS = 1000;
var curMousePos = {};
var chkMouseInPopupTimer;
function trackMousePos(e) {
curMousePos.x = e.pageX;
curMousePos.y = e.pageY;
}
dom.addEvent(window, "mousemove", trackMousePos);
function chkMouseInPopup() {
chkMouseInPopupTimer = null;
var toolTipNode = dom.gE(LINKS_TP_HTML_ID);
if(!toolTipNode)
return;
var pos = dom.offset(toolTipNode);
var rect = toolTipNode.getBoundingClientRect();
//logMsg("mouse x " + curMousePos.x + ", y " + curMousePos.y);
//logMsg("x " + Math.round(pos.left) + ", y " + Math.round(pos.top) + ", wd " + Math.round(rect.width) + ", ht " + Math.round(rect.height));
if(curMousePos.x < pos.left || curMousePos.x >= pos.left + rect.width ||
curMousePos.y < pos.top || curMousePos.y >= pos.top + rect.height) {
dom.attr(toolTipNode, "style", "display: none;");
return;
}
chkMouseInPopupTimer = setTimeout(chkMouseInPopup, CHK_MOUSE_IN_POPUP_POLL_MS);
}
function startChkMouseInPopup() {
stopChkMouseInPopup();
chkMouseInPopupTimer = setTimeout(chkMouseInPopup, CHK_MOUSE_IN_POPUP_POLL_MS);
}
function stopChkMouseInPopup() {
if(!chkMouseInPopupTimer)
return;
clearTimeout(chkMouseInPopupTimer);
chkMouseInPopupTimer = null;
}
// -----------------------------------------------------------------------------
/* YouTube reuses the current page when the user clicks on a new video. We need
to detect it and reload the formats. */
(function() {
var PERIODIC_CHK_VIDEO_URL_MS = 1000;
var NEW_URL_TAG_LINKS_DELAY_MS = 500;
var curVideoUrl = loc.toString();
function periodicChkVideoUrl() {
var newVideoUrl = loc.toString();
if(curVideoUrl != newVideoUrl && inst) {
//logMsg(curVideoUrl + " -> " + newVideoUrl);
curVideoUrl = newVideoUrl;
inst.invalidateTagLinks();
inst.periodicTagLinks(NEW_URL_TAG_LINKS_DELAY_MS);
if(loc.pathname.match(/\/watch/))
inst.checkFmts();
else
condRemoveHdr();
}
setTimeout(periodicChkVideoUrl, PERIODIC_CHK_VIDEO_URL_MS);
}
periodicChkVideoUrl();
}) ();
// -----------------------------------------------------------------------------
waitForReady();
}) ();