- // ==UserScript==
- // @name YouTube Links
- // @namespace http://www.smallapple.net/
- // @description Show the available video links.
- // @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
- // @version 1.63
- // ==/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;
-
- 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 Links";
-
- var relInfo = {
- ver: 16300,
- ts: 2014100800,
- desc: "Switch to Greasy Fork"
- };
-
- 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 doc.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 >= 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+$/, "");
- });
-
- return {
- responseText: xhr.responseText,
- 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 >= 200 && xhr.status < 300)
- opts.success(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 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: "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 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, 320, 176 ];
- var horzWideResList = [ 2880, 1920, 1280, 854, 640, 426, 256 ];
- var vertResList = [ 2160, 1536, 1080, 720, 480, 360, 240, 144 ];
-
- 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;
-
- ht = vertResList[foundIdx];
- wd = Math.round(ht * aspectRatio);
-
- // Snap to the nearest horz res
- forEach(aspectRatio < 1.5 ? 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 = {
- "2160": "2k",
- "1536": "1.5k",
- "240": "240v",
- "144": "144v"
- };
-
- if(vertResMap[ht])
- return vertResMap[ht];
-
- return String(ht) + (wd / ht < 1.5 ? "f" : "p");
- }
-
- function mapResToQuality(res) {
- if(!res.match(/^[0-9]+x([0-9]+)$/))
- return res;
-
- var resList = [
- { res: 1536, q : "highres" },
- { 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", "highres" ];
-
- 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+\)/))
- 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;
-
- //console.log(obj);
- //console.log(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) {
- //console.log(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);
-
- map.fmtUrlList.sort(function(a, b) {
- return getQualityIdx(b.quality) - getQualityIdx(a.quality);
- });
-
- callback(map);
- }
-
- // Entry point
- dom.ajax({ url: url, success: success });
- }
-
- // -----------------------------------------------------------------------------
-
- 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": "0.5em",
- "z-index": "100"
- }) + "\n" +
- "#" + UPDATE_HTML_ID + ":hover" + dom.emitCssStyles({
- "background-color": "#0d0"
- }) + "\n" +
- "#" + HDR_LINKS_HTML_ID + dom.emitCssStyles({
- "background-color": "#eee",
- "border": "#ccc 1px solid",
- //"border-radius": "3px",
- "color": "#333",
- "font-size": "90%",
- "margin": "5px",
- "padding": "5px"
- }) + "\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": "90%",
- "line-height": "1em",
- "position": "absolute",
- "left": "0",
- "top": "0",
- "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": "110"
- }) + "\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",
- "padding": "1px 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 + "3gp" + dom.emitCssStyles({
- "background-color": "#bbb"
- }) + "\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 + "webm" + 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 + "highres" + dom.emitCssStyles({
- "color": "#fff !important",
- "background-color": "#c0f",
- "background-image": "linear-gradient(to right, #c0f, #90f)"
- }) + "\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 = {
- showVideoFormats: true,
- showVideoSize: true,
- tagLinks: true
- };
-
- var videoInfoCache = {};
-
- var TAG_LINK_NUM_PER_BATCH = 5;
- var INI_TAG_LINK_DELAY_MS = 200;
- var SUB_TAG_LINK_DELAY_MS = 500;
-
- // -----------------------------------------------------------------------------
-
- 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.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.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) {
- var units = [ "kB", "MB", "GB" ];
- var idx = 0;
-
- for(idx = 0; idx < units.length; ++idx) {
- size /= 1024;
-
- if(size < 10)
- return Math.round(size * 100) / 100 + units[idx];
-
- if(size < 100)
- return Math.round(size * 10) / 10 + units[idx];
-
- if(size < 1024 || idx == units.length - 1)
- return Math.round(size) + units[idx];
- }
- }
-
- // Entry point
- var me = this;
- var s = [];
-
- var resMap = {};
-
- 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);
- });
-
- var hasHighRes = resMap["720p"] != null || resMap["1080p"] != null;
-
- for(var res in resMap) {
- if(hasHighRes && (res === "144v" || res === "240v"))
- continue;
-
- 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 varMsg = "";
-
- if(elm.bitrate != null)
- varMsg = fmtSize(elm.bitrate) + "/s";
- else if(fmtMap.vars != null)
- varMsg = fmtMap.vars.join();
-
- var addMsg = [ elm.itag, elm.type, elm.size || elm.quality, 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(elm.itag == 82 || elm.itag == 83 || elm.itag == 84 || elm.itag == 100 || elm.itag == 101 || elm.itag == 102)
- vidSuffix = " (3D)";
-
- 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 = 500;
- var SUB_SHOW_FILESIZE_DELAY_MS = 200;
-
- 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;
- }
-
- 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 = hNode.parentNode;
- 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");
-
- hNode.parentNode.insertBefore(node, hNode);
-
- 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 == "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 = 1000;
-
- var curVideoUrl = loc.toString();
-
- function periodicChkVideoUrl() {
- var newVideoUrl = loc.toString();
-
- if(curVideoUrl != newVideoUrl) {
- //console.log(curVideoUrl + " -> " + newVideoUrl);
-
- curVideoUrl = newVideoUrl;
-
- if(loc.pathname.match(/\/watch/))
- inst.checkFmts();
- }
-
- setTimeout(periodicChkVideoUrl, PERIODIC_CHK_VIDEO_URL_MS);
- }
-
- periodicChkVideoUrl();
-
- }) ();
-
- // -----------------------------------------------------------------------------
-
- }) ();