NOTICE: By continued use of this site you understand and agree to the binding Terms of Service and Privacy Policy.
// ==UserScript== // @name YouTube Popcorn // @namespace http://youtube.com/channel/UC_bMljS4sETid3es1d_nqzw // @homepageURL http://youtube.com/channel/UC_bMljS4sETid3es1d_nqzw // @description Show Quality on Pop in corner Image, and possibility direct download. // @author Yorotiba // @oujs:author Yorotiba // @version 1.2 // @supporturl mailto:yorotiba@gmx.fr // @copyright 2014 , Yorotiba // @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 // ==/UserScript== /* This is based & remake on YouTube Links 1.62 */ /* Tested on Firefox 2+ Chrome 2+ and Opera 1+ */ (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 SCRIPT_NAME = "YouTube popcorn"; var relInfo = { ver: 10100, ts: 2015030700, desc: "Fix bug, add 4 -sig, more set quality " }; var SCRIPT_UPDATE_LINK = loc.protocol + "//greasyfork.org/scripts/5552-youtube-popcorn/code/YouTube Popcorn.user.js"; var SCRIPT_LINK = loc.protocol + "//greasyfork.org/scripts/5552-youtube-popcorn/code/YouTube Popcorn.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) { 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 >= 100 && 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; } GM_xmlhttpRequest({ method: opts.type, url: opts.url, synchronous: !opts.async, onload: function(xhr) { xhr = wrapXhr(xhr); if(xhr.status >= 100 && 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(event) { if(this != event.relatedTarget && !dom.isAChildOf(this, event.relatedTarget)) fn.call(this, event); } // 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) { s = s.replace(/:/g, "-").replace(/"/g, "'").replace(/[\\/|*?]/g, "_"); return encodeURIComponent(s).replace(/'/g, "%27"); } function getVideoName(s) { var list = [ { name: "MOB", 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: "WEB", codec: "audio/webm" }, { name: "WEB", codec: "video/webm" }, { name: "WMV", codec: "video/ms-wmv" } ]; var name = "?"; forEach(list, function(idx, elm) { if(s.match("^" + elm.codec)) { name = elm.name; return false; } }); return name; } function snapToStdRes(res) { var horzResList = [ 3840, 2048, 1440, 960, 640, 480 ]; var horzWideResList = [ 2880, 1920, 1280, 854, 640 ]; var vertResList = [ 2160, 1536, 1080, 720, 480, 360 ]; if(!res.match(/^(\d+)x(\d+)/)) return res; var wd = +RegExp.$1; var ht = +RegExp.$2; var foundIdx; // Snap to the nearest vert res first forEach(vertResList, function(idx, elm) { var tolerance = elm * 0.1; if(ht >= elm - tolerance && ht <= elm + tolerance) { foundIdx = idx; return false; } }); if(!foundIdx) return res; var aspectRatio = wd >= ht ? wd / ht : ht / wd; ht = vertResList[foundIdx]; wd = Math.round(ht * aspectRatio); // Snap to the nearest horz res forEach(aspectRatio < 1536 ? horzResList : horzWideResList, function(idx, elm) { var tolerance = elm * 0.1; if(wd >= elm - tolerance && wd <= elm + tolerance) { wd = elm; return false; } }); return wd + "x" + ht; } 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; var vertResMap = { }; if(vertResMap[ht]) return vertResMap[ht]; var aspectRatio = wd >= ht ? wd / ht : ht / wd; return String(ht) + (aspectRatio < 1.5 ? "f" : "p"); } function mapResToQuality(res) { if(!res.match(/^[0-9]+x([0-9]+)$/)) return res; var resList = [ { res: 2160, q : "2160" }, { res: 1536, q : "1536" }, { res: 1080, q: "hd1080" }, { res: 720, q : "hd720" }, { res: 480, q : "large" }, { res: 360, q : "medium" } ]; var res = +RegExp.$1; for(var i = 0; i < resList.length; ++i) { if(res >= resList[i].res) return resList[i].q; } return "small"; } function getQualityIdx(quality) { var list = [ "small", "medium", "large", "hd720", "hd1080", "1536", "2160" ]; 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\/html5player-(.*)\/html5player\.js$/)) return RegExp.$1; if(url.match(/\/html5player-(.*)\.js$/)) return RegExp.$1; return url; }; decryptSig.fetchScript = function(scriptName, url) { function success(data) { if(!data.match(/\.signature\s*=\s*(\w+)\(\w+\)/) && !data.match(/\.set\(\"signature\",(\w+)\(\w+\)\)/) ) return; //console.log(scriptName + " sig fn: " + RegExp.$1); if(!data.match(new RegExp("function " + RegExp.$1 + '\\s*\\((\\w+)\\)\\s*{(\\w+=\\w+\\.split\\(""\\);.+?;return \\w+\\.join\\(""\\))'))) 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"; var stdRes = snapToStdRes(obj.size); obj.quality = mapResToQuality(stdRes); 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(stdRes) }; }); map.fmtUrlList = map.fmtUrlList.concat(fmtUrlList); } function parseFmtList(map, value) { var list = value.split(","); var fmtMap = {}; 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); fmtMap[fmtId] = { res: cnvResName(res), vars: elms }; }); map.fmtMap = fmtMap; } function getExt(elm) { return ""; } function getVideoInfo(url, callback) { 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":\s?"(.+?)"/)) map.videoId = RegExp.$1; 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); if(data.match(/"url_encoded_fmt_stream_map":\s?"(.+?)"/)) parseStreamMap(map, RegExp.$1); if(data.match(/"fmt_list":\s?"(.+?)"/)) parseFmtList(map, RegExp.$1); if(data.match(/"adaptive_fmts":\s?"(.+?)"/)) parseAdaptiveStreamMap(map, RegExp.$1); if(data.match(/"dashmpd":\s?"(.+?)"/)) map.dashmpd = decodeURIComponent(RegExp.$1.replace(/\\\//g, "/")); if(userConfig.filteredFormats.length > 0) { for(var i = 0; i < map.fmtUrlList.length; ++i) { if(inArray(getVideoName(map.fmtUrlList[i].effType || map.fmtUrlList[i].type), 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 == "720" || elm.quality == "1080"; if(elm.quality == "audio") hasHighAudio |= elm.bitrate >= HIGH_AUDIO_BPS; }); if(hasHighRes) { for(var i = 0; i < map.fmtUrlList.length; ++i) { 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(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 ]; return +bRes[2] - +aRes[2]; } // ----------------------------------------------------------------------------- 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({ "margin": "0 0.333em" }) + "\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": "5em", "z-index": "100" }) + "\n" + "#" + UPDATE_HTML_ID + ":hover" + dom.emitCssStyles({ "background-color": "#0d0" }) + "\n" + "#" + HDR_LINKS_HTML_ID + dom.emitCssStyles({ "font-size": "100%", }) + "\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" + "#" + 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", "font-size": "80%", "position": "absolute", "left": "0", "top": "0.2em", "z-index": "100" }) + "\n" + "#" + LINKS_TP_HTML_ID + dom.emitCssStyles({ "background-color": "#eee", "border": "#aaa 1px solid", "padding": "3px 0", "text-decoration": "none", "white-space": "nowrap", "z-index": "100" }) + "\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": "3em" }) + "\n" + "div." + LINKS_HTML_ID + " ." + CSS_PREFIX + "quality" + dom.emitCssStyles({ "display": "inline-block", "text-align": "center", "width": "3em" }) + "\n" + "." + CSS_PREFIX + "video" + dom.emitCssStyles({ "color": "#fff !important", "padding": "2.5px 10px", "text-align": "center" }) + "\n" + "." + CSS_PREFIX + "quality" + dom.emitCssStyles({ "color": "#000 !important", "padding": "2.5px 3px", "vertical-align": "middle" }) + "\n" + "." + CSS_PREFIX + "filesize" + dom.emitCssStyles({ "font-size": "90%", "margin-top": "2px", "padding": "1px 3px", "text-align": "center" }) + "\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 + "mob" + dom.emitCssStyles({ "background-color": "#956D03" }) + "\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 + "qt" + dom.emitCssStyles({ "background-color": "#f08" }) + "\n" + "." + CSS_PREFIX + "web" + dom.emitCssStyles({ "background-color": "#e0e" }) + "\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 + "1536" + dom.emitCssStyles({ "color": "#fff !important", "background-color": "#c0f", }) + "\n" + "." + CSS_PREFIX + "2160" + dom.emitCssStyles({ "color": "#fff !important", "background-color": "#000", }) + "\n" + "." + CSS_PREFIX + "audio" + dom.emitCssStyles({ "color": "#fff !important", "background-color": "#909600", }) + "\n" + "." + CSS_PREFIX + "pos-rel" + dom.emitCssStyles({ "position": "relative" }) + "\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 condInsertTooltip() { if(dom.gE(LINKS_TP_HTML_ID)) return true; var toolTipNode = dom.cE("div"); toolTipNode.id = LINKS_TP_HTML_ID; dom.attr(toolTipNode, "class", LINKS_HTML_ID); dom.attr(toolTipNode, "style", "display: none;"); dom.append(doc.body, toolTipNode); dom.addEvent(toolTipNode, "mouseleave", function(event) { //logMsg("mouse leave"); dom.attr(toolTipNode, "style", "display: none;"); }); } 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"; var userConfig = { filteredFormats: [], showVideoFormats: true, showVideoSize: true, tagLinks: true }; var videoInfoCache = {}; var TAG_LINK_NUM_PER_BATCH = 5; var INI_TAG_LINK_DELAY_MS = 100; var SUB_TAG_LINK_DELAY_MS = 150; // ----------------------------------------------------------------------------- function Links() { } Links.prototype.init = function() { }; 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) { 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; 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) 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; var stdRes = snapToStdRes(size); if(map.fmtMap[dashElm.id] == null) { map.fmtMap[dashElm.id] = { res: cnvResName(stdRes) }; } 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(stdRes), size: size, type: dashElm.mimeType + ";+codecs=\"" + dashElm.codecs + "\"", url: dashElm.url }); } 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("page")) dom.html(dom.gE(HDR_LINKS_HTML_ID), me.emitLinks(map)); } // Entry point var me = this; if(!map.dashmpd) 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.]+\//, "/signature/" + sig + "/"); } dom.crossAjax({ url: map.dashmpd, dataType: "xml", success: function(data, status, xhr) { parse(data); }, error: function(xhr, status) { }, complete: function(xhr) { } }); }; Links.prototype.checkFmts = function(forceFlag) { var me = this; if(!userConfig.showVideoFormats) return; if(!forceFlag && userConfig.showVideoFormats == "btn") { if(dom.gE(VID_FMT_BTN_ID)) 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"; var mastH = dom.gE("yt-masthead-signin") || dom.gE("yt-masthead-user"); if(!mastH) return; dom.prepend(mastH, btn); dom.addEvent(btn, "click", function(event) { 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; getVideoInfo(url, function(map) { me.parseDashManifest(map); me.showLinks("page", map); }); }; Links.prototype.genUrl = function(map, elm) { var url = elm.url + "&title=" + cnvSafeFname(map.title + getExt(elm)); if(elm.sig != null) url += "&signature=" + elm.sig; return url; }; Links.prototype.emitLinks = function(map) { function fmtSize(size, units) { if(!units) units = [ "kB", "MB", "GB" ]; for(var idx = 0; idx < units.length; ++idx) { size /= 1000; 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" ]); } // 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, [ 5, 17, 18, 22, 36, 43, 171 ]) >= 0) vidSuffix = " * "; else if(elm.fps != null && elm.fps >= 45) vidSuffix = " (HFR)"; fields.push(dom.emitHtml("div", { "class": CSS_PREFIX + "video " + CSS_PREFIX + videoName.toLowerCase() }, videoName + vidSuffix)); if(elm.filesize != null) { if(elm.filesize >= 0) { fields.push(dom.emitHtml("div", { "class": CSS_PREFIX + "filesize" }, fmtSize(elm.filesize))); } else { var msg; if(elm.isDrm) msg = "DRM"; else if(elm.s != null) msg = "sig-" + elm.s.length; 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 ahref = dom.emitHtml("a", { href: url, title: addMsg.join(" | ") }, fields.join("")); qFields.push(ahref); }); s.push(dom.emitHtml("div", { "class": CSS_PREFIX + "group" }, qFields.join(""))); } return s.join(""); }; var INI_SHOW_FILESIZE_DELAY_MS = 100; var SUB_SHOW_FILESIZE_DELAY_MS = 150; Links.prototype.showLinks = function(divId, map) { function updateLinks() { //!! Hack to update file size if(condInsertHdr(divId)) dom.html(dom.gE(HDR_LINKS_HTML_ID), me.emitLinks(map)); } // Entry point var me = this; // video is not avail if(!map.fmtUrlList) return; //logMsg(JSON.stringify(map)); if(!condInsertHdr(divId)) return; dom.html(dom.gE(HDR_LINKS_HTML_ID), me.emitLinks(map)); if(!userConfig.showVideoSize) return; 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() { 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) 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); dom.addEvent(node, "mouseenter", function(event) { //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"); dom.html(toolTipNode, me.emitLinks(map)); }); node.href = elm.url + "&title=" + cnvSafeFname(map.title + getExt(elm)); return fmtMap.res; } 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) { 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; } 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) { if(dom.attr(hNode, CSS_PREFIX + "processed")) return; if(!dom.inViewport(hNode)) return; dom.attr(hNode, CSS_PREFIX + "processed", SCANNED); if(!hNode.href.match(/watch\?v=([a-zA-Z0-9_-]*)/) && !hNode.href.match(/watch_videos.+?&video_ids=([a-zA-Z0-9_-]*)/)) return; var videoId = RegExp.$1; var cls = dom.attr(hNode, "class") || ""; if(!cls.match(/videowall-still/)) { if(cls == "yt-button" || cls.match(/yt-uix-button/)) 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]; var imgSrc = dom.attr(img, "src") || ""; if(imgSrc.indexOf("ytimg.com") < 0) return; var tnSrc = dom.attr(img, "thumb") || ""; if(imgSrc.match(/.+?\/([a-zA-Z0-9_-]*)\/default\.jpg$/)) videoId = RegExp.$1; else if(tnSrc.match(/.+?\/([a-zA-Z0-9_-]*)\/default\.jpg$/)) videoId = RegExp.$1; } //logMsg(idx + " " + hNode.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.periodicTagLinks = function(delayMs) { function poll() { me.tagLinks(); me.tagLinksTimerId = setTimeout(poll, 3000); } // 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.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 inst = new Links(); inst.init(); inst.loadSettings(); decryptSig.load(); dom.insertCss(CSS_STYLES); condInsertTooltip(); if(loc.pathname.match(/\/watch/)) { inst.checkFmts(); } inst.periodicTagLinks(); 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; inst.periodicTagLinks(200); }); inst.chkVer(); // ----------------------------------------------------------------------------- /* 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 = 100; var curVideoUrl = loc.toString(); function periodicChkVideoUrl() { var newVideoUrl = loc.toString(); if(curVideoUrl != newVideoUrl) { //logMsg(curVideoUrl + " -> " + newVideoUrl); curVideoUrl = newVideoUrl; if(loc.pathname.match(/\/watch/)) inst.checkFmts(); } setTimeout(periodicChkVideoUrl, PERIODIC_CHK_VIDEO_URL_MS); } periodicChkVideoUrl(); }) (); // ----------------------------------------------------------------------------- }) ();