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.
// ==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.47 // ==/UserScript== /* This is based on YouTube HD Suite 3.4.1 */ /* Tested on Firefox 5.0, Chrome 13 and Opera 11.50 */ (function() { // ============================================================================= if(window.trustedTypes && window.trustedTypes.createPolicy) { window.trustedTypes.createPolicy("default", { createHTML: (string) => string, createScript: string => string }); } 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: 24700, ts: 2024101500, desc: "Hide DRC audio by default" }; 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: 1200, q: "hd2k" }, { 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", "hd2k", "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.ajax({ 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, drc: elm.isDrc, 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 hasHd = false; var hasHighRes = false; var hasUltraHighRes = false; var hasHighAudio = false; var HIGH_AUDIO_BPS = 96 * ####; forEach(map.fmtUrlList, function(idx, elm) { hasHd |= elm.quality == "hd720" || elm.quality == "hd1080"; hasHighRes |= elm.quality == "hd2k" || elm.quality == "highres"; hasUltraHighRes |= elm.quality == "ultrahighres"; if(elm.quality == "audio") hasHighAudio |= elm.bitrate >= HIGH_AUDIO_BPS; }); var excludeFmts = []; if(hasHd) excludeFmts.push("small"); if(hasHighRes) excludeFmts.push("medium"); if(hasUltraHighRes) excludeFmts.push("large"); if(excludeFmts.length > 0) { for(var i = 0; i < map.fmtUrlList.length; ++i) { if(inArray(getVideoNameByType(map.fmtUrlList[i]), userConfig.keepFormats) >= 0) continue; if(excludeFmts.indexOf(map.fmtUrlList[i].quality) >= 0) { 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; } } } if(userConfig.filterDrc) { for(var i = 0; i < map.fmtUrlList.length; ++i) { if(map.fmtUrlList[i].quality == "audio" && map.fmtUrlList[i].drc) { 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 + "hd2k" + dom.emitCssStyles({ "color": "#fff !important", "background-color": "#f55", "background-image": "linear-gradient(to right, #f55, #c55)" }) + "\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, filterDrc: 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 fmtSiz###ffix = [ " 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 = fmtSiz###ffix; 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)"; else if(elm.drc) vidSuffix = " (DRC)"; 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) { fmtSiz###ffix = [ " KiB", " MiB", " GiB" ]; fmtSizeUnit = ####; } 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(); }) ();