// ==UserScript== // @name Y2BD // @version 0.0.1 // @description YouTube Download Videos, All Format Analyzing // @author Kaige Cai // @namespace https://github.com/Mr-Cai/Y2BD // @icon https://www.youtube.com/about/static/svgs/icons/brand-resources/YouTube_icon_full-color.svg // @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 // @downloadURL https://update.greasyfork.cloud/scripts/420336/Y2BD.user.js // @updateURL https://update.greasyfork.cloud/scripts/420336/Y2BD.meta.js // ==/UserScript== (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: 24100, ts: 2020113000, desc: "Fixed overly aggressive regexp" }; 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 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$]{2})\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(/