// ==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 + ""; 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(//)) { logMsg("Skipping " + url); return; } if (data.match(//)) { 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(/\s*\n/)) map.title = unescHtmlEntities(RegExp.$1); if (map.title == null && data.match(//)) 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("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, 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)) + "
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 // 'content' is for Material Design if (dom.gE("page") || dom.gE("content") || 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(); })();