// ==UserScript== // @name YouTube Links // @namespace http://www.smallapple.net/ // @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.01 // @downloadURL none // ==/UserScript== /* This is based on YouTube HD Suite 3.4.1 */ /* Tested on Firefox 5.0, Chrome 13 and Opera 11.50 */ (function() { // ============================================================================= var win = typeof(unsafeWindow) !== "undefined" ? unsafeWindow : window; var doc = win.document; var loc = win.location; if(win.top != win.self) return; var unsafeWin = win; // Hack to get unsafe window in Chrome (function() { var isChrome = navigator.userAgent.toLowerCase().indexOf("chrome") >= 0; if(!isChrome) return; // Chrome 27 fixed this exploit, but luckily, its unsafeWin now works for us try { var div = doc.createElement("div"); div.setAttribute("onclick", "return window;"); unsafeWin = div.onclick(); } catch(e) { } }) (); var ua = navigator.userAgent || ""; var isEdgeBrowser = ua.match(/ Edge\//); // ============================================================================= if(typeof GM == "object" && GM.xmlHttpRequest && typeof GM_xmlhttpRequest == "undefined") { GM_xmlhttpRequest = async function(opts) { await GM.xmlHttpRequest(opts); } } // ============================================================================= var SCRIPT_NAME = "YouTube Links"; var relInfo = { ver: 20100, ts: 2018092900, desc: "Refresh tags in Material Design search page" }; 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(); 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 = { "opus": "OPUS", "vorbis": "VOR", "vp9": "VP9" }; if(s.match(/\+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 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, ""); if(!data.match(/\.signature\s*=\s*(\w+)\(\w+\)/) && !data.match(/\.set\(\"signature\",([\w$]+)\(\w+\)\)/) && !data.match(/\/yt\.akamaized\.net\/\)\s*\|\|\s*\w+\.set\s*\(.*?\)\s*;.*?\w+\.set\s*\(\s*\w+\s*,\s*(\w+)\s*\(/) && !data.match(/;\s*\w+\s*&&\s*\w+\.set\(\w+\s*,\s*(\w+)\s*\(/)) return; var sigFn = RegExp.$1; //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 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.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; 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); 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 getVideoInfo(url, callback) { function getVideoNameByType(elm) { return getVideoName(elm.effType || elm.type); } function success(data) { var map = {}; if(data.match(/