YouTube Links

Show the available video links.

目前为 2014-10-10 提交的版本,查看 最新版本

  1. // ==UserScript==
  2. // @name YouTube Links
  3. // @namespace http://www.smallapple.net/
  4. // @description Show the available video links.
  5. // @author Ng Hun Yang
  6. // @include http://*.youtube.com/*
  7. // @include http://youtube.com/*
  8. // @include https://*.youtube.com/*
  9. // @include https://youtube.com/*
  10. // @match *://*.youtube.com/*
  11. // @match *://*.googlevideo.com/*
  12. // @match *://s.ytimg.com/yts/jsbin/*
  13. // @grant GM_xmlhttpRequest
  14. // @version 1.64
  15. // ==/UserScript==
  16.  
  17. /* This is based on YouTube HD Suite 3.4.1 */
  18.  
  19. /* Tested on Firefox 5.0, Chrome 13 and Opera 11.50 */
  20.  
  21. (function() {
  22.  
  23. // =============================================================================
  24.  
  25. var win = typeof(unsafeWindow) !== "undefined" ? unsafeWindow : window;
  26. var doc = win.document;
  27. var loc = win.location;
  28.  
  29. var unsafeWin = win;
  30.  
  31. // Hack to get unsafe window in Chrome
  32. (function() {
  33.  
  34. var isChrome = navigator.userAgent.toLowerCase().indexOf("chrome") >= 0;
  35.  
  36. if(!isChrome)
  37. return;
  38.  
  39. // Chrome 27 fixed this exploit, but luckily, its unsafeWin now works for us
  40. try {
  41. var div = doc.createElement("div");
  42. div.setAttribute("onclick", "return window;");
  43. unsafeWin = div.onclick();
  44. } catch(e) {
  45. }
  46.  
  47. }) ();
  48.  
  49. // =============================================================================
  50.  
  51. var SCRIPT_NAME = "YouTube Links";
  52.  
  53. var relInfo = {
  54. ver: 16400,
  55. ts: 2014101000,
  56. desc: "Fixed events on Firefox"
  57. };
  58.  
  59. var SCRIPT_UPDATE_LINK = loc.protocol + "//greasyfork.org/scripts/5565-youtube-links-updater/code/YouTube Links Updater.user.js";
  60. var SCRIPT_LINK = loc.protocol + "//greasyfork.org/scripts/5566-youtube-links/code/YouTube Links.user.js";
  61.  
  62. // =============================================================================
  63.  
  64. var dom = {};
  65.  
  66. dom.gE = function(id) {
  67. return doc.getElementById(id);
  68. };
  69.  
  70. dom.gT = function(dom, tag) {
  71. if(arguments.length == 1) {
  72. tag = dom;
  73. dom = doc;
  74. }
  75.  
  76. return dom.getElementsByTagName(tag);
  77. };
  78.  
  79. dom.cE = function(tag) {
  80. return document.createElement(tag);
  81. };
  82.  
  83. dom.cT = function(s) {
  84. return doc.createTextNode(s);
  85. };
  86.  
  87. dom.attr = function(obj, k, v) {
  88. if(arguments.length == 2)
  89. return obj.getAttribute(k);
  90.  
  91. obj.setAttribute(k, v);
  92. };
  93.  
  94. dom.prepend = function(obj, child) {
  95. obj.insertBefore(child, obj.firstChild);
  96. };
  97.  
  98. dom.append = function(obj, child) {
  99. obj.appendChild(child);
  100. };
  101.  
  102. dom.offset = function(obj) {
  103. var x = 0;
  104. var y = 0;
  105.  
  106. if(obj.getBoundingClientRect) {
  107. var box = obj.getBoundingClientRect();
  108. var owner = obj.ownerDocument;
  109.  
  110. x = box.left + Math.max(owner.documentElement.scrollLeft, owner.body.scrollLeft) - owner.documentElement.clientLeft;
  111. y = box.top + Math.max(owner.documentElement.scrollTop, owner.body.scrollTop) - owner.documentElement.clientTop;
  112.  
  113. return { left: x, top: y };
  114. }
  115.  
  116. if(obj.offsetParent) {
  117. do {
  118. x += obj.offsetLeft - obj.scrollLeft;
  119. y += obj.offsetTop - obj.scrollTop;
  120. obj = obj.offsetParent;
  121. } while(obj);
  122. }
  123.  
  124. return { left: x, top: y };
  125. };
  126.  
  127. dom.inViewport = function(el) {
  128. var rect = el.getBoundingClientRect();
  129.  
  130. return rect.bottom >= 0 &&
  131. rect.right >= 0 &&
  132. rect.top < (win.innerHeight || doc.documentElement.clientHeight) &&
  133. rect.left < (win.innerWidth || doc.documentElement.clientWidth);
  134. };
  135.  
  136. dom.html = function(obj, s) {
  137. if(arguments.length == 1)
  138. return obj.innerHTML;
  139.  
  140. obj.innerHTML = s;
  141. };
  142.  
  143. dom.emitHtml = function(tag, attrs, body) {
  144. if(arguments.length == 2) {
  145. if(typeof(attrs) == "string") {
  146. body = attrs;
  147. attrs = {};
  148. }
  149. }
  150.  
  151. var list = [];
  152.  
  153. for(var k in attrs) {
  154. list.push(k + "='" + attrs[k].replace(/'/g, "\\'") + "'");
  155. }
  156.  
  157. var s = "<" + tag + " " + list.join(" ") + ">";
  158.  
  159. if(body != null)
  160. s += body + "</" + tag + ">";
  161.  
  162. return s;
  163. };
  164.  
  165. dom.emitCssStyles = function(styles) {
  166. var list = [];
  167.  
  168. for(var k in styles) {
  169. list.push(k + ": " + styles[k] + ";");
  170. }
  171.  
  172. return " { " + list.join(" ") + " }";
  173. };
  174.  
  175. dom.ajax = function(opts) {
  176. function newXhr() {
  177. if(window.ActiveXObject) {
  178. try {
  179. return new ActiveXObject("Msxml2.XMLHTTP");
  180. } catch(e) {
  181. }
  182.  
  183. try {
  184. return new ActiveXObject("Microsoft.XMLHTTP");
  185. } catch(e) {
  186. return null;
  187. }
  188. }
  189.  
  190. if(window.XMLHttpRequest)
  191. return new XMLHttpRequest();
  192.  
  193. return null;
  194. }
  195.  
  196. function nop() {
  197. }
  198.  
  199. // Entry point
  200. var xhr = newXhr();
  201.  
  202. opts = addProp({
  203. type: "GET",
  204. async: true,
  205. success: nop,
  206. error: nop,
  207. complete: nop
  208. }, opts);
  209.  
  210. xhr.open(opts.type, opts.url, opts.async);
  211.  
  212. xhr.onreadystatechange = function() {
  213. if(xhr.readyState == 4) {
  214. var status = +xhr.status;
  215.  
  216. if(status >= 200 && status < 300) {
  217. opts.success(xhr.responseText, "success", xhr);
  218. }
  219. else {
  220. opts.error(xhr, "error");
  221. }
  222.  
  223. opts.complete(xhr);
  224. }
  225. };
  226.  
  227. xhr.send("");
  228. };
  229.  
  230. dom.crossAjax = function(opts) {
  231. function wrapXhr(xhr) {
  232. var headers = xhr.responseHeaders.replace("\r", "").split("\n");
  233.  
  234. var obj = {};
  235.  
  236. forEach(headers, function(idx, elm) {
  237. var nv = elm.split(":");
  238. if(nv[1] != null)
  239. obj[nv[0].toLowerCase()] = nv[1].replace(/^\s+/, "").replace(/\s+$/, "");
  240. });
  241.  
  242. return {
  243. responseText: xhr.responseText,
  244. status: xhr.status,
  245.  
  246. getAllResponseHeaders: function() {
  247. return xhr.responseHeaders;
  248. },
  249.  
  250. getResponseHeader: function(name) {
  251. return obj[name.toLowerCase()];
  252. }
  253. };
  254. }
  255.  
  256. function nop() {
  257. }
  258.  
  259. // Entry point
  260. opts = addProp({
  261. type: "GET",
  262. async: true,
  263. success: nop,
  264. error: nop,
  265. complete: nop
  266. }, opts);
  267.  
  268. if(typeof GM_xmlhttpRequest === "undefined") {
  269. setTimeout(function() {
  270. var xhr = {};
  271. opts.error(xhr, "error");
  272. opts.complete(xhr);
  273. }, 0);
  274. return;
  275. }
  276.  
  277. GM_xmlhttpRequest({
  278. method: opts.type,
  279. url: opts.url,
  280. synchronous: !opts.async,
  281.  
  282. onload: function(xhr) {
  283. xhr = wrapXhr(xhr);
  284.  
  285. if(xhr.status >= 200 && xhr.status < 300)
  286. opts.success(xhr.responseText, "success", xhr);
  287. else
  288. opts.error(xhr, "error");
  289.  
  290. opts.complete(xhr);
  291. },
  292.  
  293. onerror: function(xhr) {
  294. xhr = wrapXhr(xhr);
  295. opts.error(xhr, "error");
  296. opts.complete(xhr);
  297. }
  298. });
  299. };
  300.  
  301. dom.addEvent = function(e, type, fn) {
  302. function mouseEvent(event) {
  303. if(this != event.relatedTarget && !dom.isAChildOf(this, event.relatedTarget))
  304. fn.call(this, event);
  305. }
  306.  
  307. // Entry point
  308. if(e.addEventListener) {
  309. var effFn = fn;
  310.  
  311. if(type == "mouseenter") {
  312. type = "mouseover";
  313. effFn = mouseEvent;
  314. }
  315. else if(type == "mouseleave") {
  316. type = "mouseout";
  317. effFn = mouseEvent;
  318. }
  319.  
  320. e.addEventListener(type, effFn, /*capturePhase*/ false);
  321. }
  322. else
  323. e.attachEvent("on" + type, function() { fn(win.event); });
  324. };
  325.  
  326. dom.insertCss = function (styles) {
  327. var ss = dom.cE("style");
  328. dom.attr(ss, "type", "text/css");
  329.  
  330. var hh = dom.gT("head") [0];
  331. dom.append(hh, ss);
  332. dom.append(ss, dom.cT(styles));
  333. };
  334.  
  335. dom.isAChildOf = function(parent, child) {
  336. if(parent === child)
  337. return false;
  338.  
  339. while(child && child !== parent) {
  340. child = child.parentNode;
  341. }
  342.  
  343. return child === parent;
  344. };
  345.  
  346. // -----------------------------------------------------------------------------
  347.  
  348. function timeNowInSec() {
  349. return Math.round(+new Date() / 1000);
  350. }
  351.  
  352. function forLoop(opts, fn) {
  353. opts = addProp({ start: 0, inc: 1 }, opts);
  354.  
  355. for(var idx = opts.start; idx < opts.num; idx += opts.inc) {
  356. if(fn.call(opts, idx, opts) === false)
  357. break;
  358. }
  359. }
  360.  
  361. function forEach(list, fn) {
  362. forLoop({ num: list.length }, function(idx) {
  363. return fn.call(list[idx], idx, list[idx]);
  364. });
  365. }
  366.  
  367. function addProp(dest, src) {
  368. for(var k in src) {
  369. if(src[k] != null)
  370. dest[k] = src[k];
  371. }
  372.  
  373. return dest;
  374. }
  375.  
  376. function unescHtmlEntities(s) {
  377. return s.replace(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/&amp;/g, "&").replace(/&quot;/g, '"').replace(/&#39;/g, "'");
  378. }
  379.  
  380. function logMsg(s) {
  381. win.console.log(s);
  382. }
  383.  
  384. function cnvSafeFname(s) {
  385. s = s.replace(/:/g, "-").replace(/"/g, "'").replace(/[\\/|*?]/g, "_");
  386. return encodeURIComponent(s).replace(/'/g, "%27");
  387. }
  388.  
  389. function getVideoName(s) {
  390. var list = [
  391. { name: "3GP", codec: "video/3gpp" },
  392. { name: "FLV", codec: "video/x-flv" },
  393. { name: "M4V", codec: "video/x-m4v" },
  394. { name: "MP3", codec: "audio/mpeg" },
  395. { name: "MP4", codec: "video/mp4" },
  396. { name: "M4A", codec: "audio/mp4" },
  397. { name: "QT", codec: "video/quicktime" },
  398. { name: "WEBM", codec: "audio/webm" },
  399. { name: "WEBM", codec: "video/webm" },
  400. { name: "WMV", codec: "video/ms-wmv" }
  401. ];
  402.  
  403. var name = "?";
  404.  
  405. forEach(list, function(idx, elm) {
  406. if(s.match("^" + elm.codec)) {
  407. name = elm.name;
  408. return false;
  409. }
  410. });
  411.  
  412. return name;
  413. }
  414.  
  415. function snapToStdRes(res) {
  416. var horzResList = [ 3840, 2048, 1440, 960, 640, 480, 320, 176 ];
  417. var horzWideResList = [ 2880, 1920, 1280, 854, 640, 426, 256 ];
  418. var vertResList = [ 2160, 1536, 1080, 720, 480, 360, 240, 144 ];
  419.  
  420. if(!res.match(/^(\d+)x(\d+)/))
  421. return res;
  422.  
  423. var wd = +RegExp.$1;
  424. var ht = +RegExp.$2;
  425. var foundIdx;
  426.  
  427. // Snap to the nearest vert res first
  428. forEach(vertResList, function(idx, elm) {
  429. var tolerance = elm * 0.1;
  430. if(ht >= elm - tolerance && ht <= elm + tolerance) {
  431. foundIdx = idx;
  432. return false;
  433. }
  434. });
  435.  
  436. if(!foundIdx)
  437. return res;
  438.  
  439. var aspectRatio = wd / ht;
  440.  
  441. ht = vertResList[foundIdx];
  442. wd = Math.round(ht * aspectRatio);
  443.  
  444. // Snap to the nearest horz res
  445. forEach(aspectRatio < 1.5 ? horzResList : horzWideResList, function(idx, elm) {
  446. var tolerance = elm * 0.1;
  447. if(wd >= elm - tolerance && wd <= elm + tolerance) {
  448. wd = elm;
  449. return false;
  450. }
  451. });
  452.  
  453. return wd + "x" + ht;
  454. }
  455.  
  456. function cnvResName(res) {
  457. var resMap = {
  458. "audio": "Audio"
  459. };
  460.  
  461. if(resMap[res])
  462. return resMap[res];
  463.  
  464. if(!res.match(/^(\d+)x(\d+)/))
  465. return res;
  466.  
  467. var wd = +RegExp.$1;
  468. var ht = +RegExp.$2;
  469.  
  470. var vertResMap = {
  471. "2160": "2k",
  472. "1536": "1.5k",
  473. "240": "240v",
  474. "144": "144v"
  475. };
  476.  
  477. if(vertResMap[ht])
  478. return vertResMap[ht];
  479.  
  480. return String(ht) + (wd / ht < 1.5 ? "f" : "p");
  481. }
  482.  
  483. function mapResToQuality(res) {
  484. if(!res.match(/^[0-9]+x([0-9]+)$/))
  485. return res;
  486.  
  487. var resList = [
  488. { res: 1536, q : "highres" },
  489. { res: 1080, q: "hd1080" },
  490. { res: 720, q : "hd720" },
  491. { res: 480, q : "large" },
  492. { res: 360, q : "medium" }
  493. ];
  494.  
  495. var res = +RegExp.$1;
  496.  
  497. for(var i = 0; i < resList.length; ++i) {
  498. if(res >= resList[i].res)
  499. return resList[i].q;
  500. }
  501.  
  502. return "small";
  503. }
  504.  
  505. function getQualityIdx(quality) {
  506. var list = [ "small", "medium", "large", "hd720", "hd1080", "highres" ];
  507.  
  508. for(var i = 0; i < list.length; ++i) {
  509. if(list[i] == quality)
  510. return i;
  511. }
  512.  
  513. return -1;
  514. }
  515.  
  516. // =============================================================================
  517.  
  518. RegExp.escape = function(s) {
  519. return String(s).replace(/([.*+?^=!:${}()|\[\]\/\\])/g, "\\$1");
  520. };
  521.  
  522. var decryptSig = {
  523. store: {}
  524. };
  525.  
  526. (function () {
  527.  
  528. var SIG_STORE_ID = "ujsYtLinksSig";
  529.  
  530. var CHK_SIG_INTERVAL = 3 * 86400;
  531.  
  532. decryptSig.load = function() {
  533. var obj = localStorage[SIG_STORE_ID];
  534. if(obj == null)
  535. return;
  536.  
  537. decryptSig.store = JSON.parse(obj);
  538. };
  539.  
  540. decryptSig.save = function() {
  541. localStorage[SIG_STORE_ID] = JSON.stringify(decryptSig.store);
  542. };
  543.  
  544. decryptSig.extractScriptUrl = function(data) {
  545. if(data.match(/ytplayer.config\s*=.*\"assets"\s*:\s*{.*"js"\s*:\s*(".+?")/))
  546. return JSON.parse(RegExp.$1);
  547. else
  548. return false;
  549. };
  550.  
  551. decryptSig.getScriptName = function(url) {
  552. if(url.match(/\/yts\/jsbin\/html5player-(.*)\/html5player\.js$/))
  553. return RegExp.$1;
  554.  
  555. if(url.match(/\/html5player-(.*)\.js$/))
  556. return RegExp.$1;
  557.  
  558. return url;
  559. };
  560.  
  561. decryptSig.fetchScript = function(scriptName, url) {
  562. function success(data) {
  563. if(!data.match(/\.signature\s*=\s*(\w+)\(\w+\)/))
  564. return;
  565.  
  566. //console.log(scriptName + " sig fn: " + RegExp.$1);
  567.  
  568. if(!data.match(new RegExp("function " + RegExp.$1 + '\\s*\\((\\w+)\\)\\s*{(\\w+=\\w+\\.split\\(""\\);.+?;return \\w+\\.join\\(""\\))')))
  569. return;
  570.  
  571. var fnParam = RegExp.$1;
  572. var fnBody = RegExp.$2;
  573.  
  574. var fnHlp = {};
  575. var objHlp = {};
  576.  
  577. //console.log("param: " + fnParam);
  578. //console.log(fnBody);
  579.  
  580. fnBody = fnBody.split(";");
  581.  
  582. forEach(fnBody, function(idx, elm) {
  583. // its own property
  584. if(elm.match(new RegExp("^" + fnParam + "=" + fnParam + "\\.")))
  585. return;
  586.  
  587. // global fn
  588. if(elm.match(new RegExp("^" + fnParam + "=([a-zA-Z_$][a-zA-Z0-9_$]*)\\("))) {
  589. var name = RegExp.$1;
  590. //console.log("fnHlp: " + name);
  591.  
  592. if(fnHlp[name])
  593. return;
  594.  
  595. if(data.match(new RegExp("(function " + RegExp.escape(RegExp.$1) + ".+?;return \\w+})")))
  596. fnHlp[name] = RegExp.$1;
  597.  
  598. return;
  599. }
  600.  
  601. // object fn
  602. if(elm.match(new RegExp("^([a-zA-Z_$][a-zA-Z0-9_$]*)\.([a-zA-Z_$][a-zA-Z0-9_$]*)\\("))) {
  603. var name = RegExp.$1;
  604. console.log("objHlp: " + name);
  605.  
  606. if(objHlp[name])
  607. return;
  608.  
  609. if(data.match(new RegExp("(var " + RegExp.escape(RegExp.$1) + "={.+?};)")))
  610. objHlp[name] = RegExp.$1;
  611.  
  612. return;
  613. }
  614. });
  615.  
  616. //console.log(fnHlp);
  617. //console.log(objHlp);
  618.  
  619. var fnHlpStr = "";
  620.  
  621. for(var k in fnHlp)
  622. fnHlpStr += fnHlp[k];
  623.  
  624. for(var k in objHlp)
  625. fnHlpStr += objHlp[k];
  626.  
  627. var fullFn = "function(" + fnParam + "){" + fnHlpStr + fnBody.join(";") + "}";
  628. //console.log(fullFn);
  629.  
  630. decryptSig.store[scriptName] = { ver: relInfo.ver, ts: timeNowInSec(), fn: fullFn };
  631. //console.log(decryptSig);
  632.  
  633. decryptSig.save();
  634. }
  635.  
  636. // Entry point
  637. dom.crossAjax({ url: url, success: success });
  638. };
  639.  
  640. decryptSig.condFetchScript = function(url) {
  641. var scriptName = decryptSig.getScriptName(url);
  642. var store = decryptSig.store[scriptName];
  643. var now = timeNowInSec();
  644.  
  645. if(store && now - store.ts < CHK_SIG_INTERVAL && store.ver == relInfo.ver)
  646. return;
  647.  
  648. decryptSig.fetchScript(scriptName, url);
  649. };
  650.  
  651. }) ();
  652.  
  653. function deobfuscateVideoSig(scriptName, sig) {
  654. if(!decryptSig.store[scriptName])
  655. return sig;
  656.  
  657. //console.log(decryptSig.store[scriptName].fn);
  658.  
  659. try {
  660. sig = eval("(" + decryptSig.store[scriptName].fn + ") (\"" + sig + "\")");
  661. } catch(e) {
  662. }
  663.  
  664. return sig;
  665. }
  666.  
  667. // =============================================================================
  668.  
  669. function parseStreamMap(map, value) {
  670. var fmtUrlList = [];
  671.  
  672. forEach(value.split(","), function(idx, elm) {
  673. var elms = elm.replace(/(\\\/)/g, "/").replace(/(\\u0026)/g, "&").split("&");
  674. var obj = {};
  675.  
  676. forEach(elms, function(idx, elm) {
  677. var kv = elm.split("=");
  678. obj[kv[0]] = decodeURIComponent(kv[1]);
  679. });
  680.  
  681. obj.itag = +obj.itag;
  682.  
  683. if(obj.conn != null && obj.conn.match(/^rtmpe:\/\//))
  684. obj.isDrm = true;
  685.  
  686. if(obj.s != null && obj.sig == null) {
  687. var sig = deobfuscateVideoSig(map.scriptName, obj.s);
  688. if(sig != obj.s) {
  689. obj.sig = sig;
  690. delete obj.s;
  691. }
  692. }
  693.  
  694. fmtUrlList.push(obj);
  695. });
  696.  
  697. //logMsg(fmtUrlList);
  698.  
  699. map.fmtUrlList = fmtUrlList;
  700. }
  701.  
  702. function parseAdaptiveStreamMap(map, value) {
  703. var fmtUrlList = [];
  704.  
  705. forEach(value.split(","), function(idx, elm) {
  706. var elms = elm.replace(/(\\\/)/g, "/").replace(/(\\u0026)/g, "&").split("&");
  707. var obj = {};
  708.  
  709. forEach(elms, function(idx, elm) {
  710. var kv = elm.split("=");
  711. obj[kv[0]] = decodeURIComponent(kv[1]);
  712. });
  713.  
  714. obj.itag = +obj.itag;
  715.  
  716. if(obj.bitrate != null)
  717. obj.bitrate = +obj.bitrate;
  718.  
  719. //console.log(obj);
  720. //console.log(map.videoId + ": " + obj.index + " " + obj.init + " " + obj.itag + " " + obj.size + " " + obj.bitrate + " " + obj.type);
  721.  
  722. if(obj.type.match(/^video\/mp4/))
  723. obj.effType = "video/x-m4v";
  724.  
  725. if(obj.type.match(/^audio\//))
  726. obj.size = "audio";
  727.  
  728. var stdRes = snapToStdRes(obj.size);
  729. obj.quality = mapResToQuality(stdRes);
  730.  
  731. if(obj.s != null && obj.sig == null) {
  732. var sig = deobfuscateVideoSig(map.scriptName, obj.s);
  733. if(sig != obj.s) {
  734. obj.sig = sig;
  735. delete obj.s;
  736. }
  737. }
  738.  
  739. fmtUrlList.push(obj);
  740.  
  741. map.fmtMap[obj.itag] = { res: cnvResName(stdRes) };
  742. });
  743.  
  744. map.fmtUrlList = map.fmtUrlList.concat(fmtUrlList);
  745. }
  746.  
  747. function parseFmtList(map, value) {
  748. var list = value.split(",");
  749. var fmtMap = {};
  750.  
  751. forEach(list, function(idx, elm) {
  752. var elms = elm.replace(/(\\\/)/g, "/").split("/");
  753.  
  754. var fmtId = elms[0];
  755. var res = elms[1];
  756. elms.splice(/*idx*/ 0, /*rm*/ 2);
  757.  
  758. fmtMap[fmtId] = { res: cnvResName(res), vars: elms };
  759. });
  760.  
  761. map.fmtMap = fmtMap;
  762. }
  763.  
  764. function getExt(elm) {
  765. return "";
  766. }
  767.  
  768. function getVideoInfo(url, callback) {
  769. function success(data) {
  770. var map = {};
  771.  
  772. if(data.match(/<div\s+id="verify-details">/)) {
  773. logMsg("Skipping " + url);
  774. return;
  775. }
  776.  
  777. if(data.match(/<h1\s+id="unavailable-message">/)) {
  778. logMsg("Not avail " + url);
  779. return;
  780. }
  781.  
  782. if(data.match(/"t":\s?"(.+?)"/))
  783. map.t = RegExp.$1;
  784.  
  785. if(data.match(/"video_id":\s?"(.+?)"/))
  786. map.videoId = RegExp.$1;
  787.  
  788. map.scriptUrl = decryptSig.extractScriptUrl(data);
  789. if(map.scriptUrl) {
  790. //console.log(map.videoId + " script: " + map.scriptUrl);
  791. map.scriptName = decryptSig.getScriptName(map.scriptUrl);
  792. decryptSig.condFetchScript(map.scriptUrl);
  793. }
  794.  
  795. if(data.match(/<meta\s+itemprop="name"\s*content="(.+)"\s*>\s*\n/))
  796. map.title = unescHtmlEntities(RegExp.$1);
  797.  
  798. if(map.title == null && data.match(/<meta\s+name="title"\s*content="(.+)"\s*>/))
  799. map.title = unescHtmlEntities(RegExp.$1);
  800.  
  801. if(data.match(/"url_encoded_fmt_stream_map":\s?"(.+?)"/))
  802. parseStreamMap(map, RegExp.$1);
  803.  
  804. if(data.match(/"fmt_list":\s?"(.+?)"/))
  805. parseFmtList(map, RegExp.$1);
  806.  
  807. if(data.match(/"adaptive_fmts":\s?"(.+?)"/))
  808. parseAdaptiveStreamMap(map, RegExp.$1);
  809.  
  810. var hasHighRes = false;
  811.  
  812. forEach(map.fmtUrlList, function(idx, elm) {
  813. hasHighRes |= elm.quality == "hd720" || elm.quality == "hd1080";
  814. });
  815.  
  816. if(hasHighRes) {
  817. for(var i = 0; i < map.fmtUrlList.length; ++i) {
  818. if(map.fmtUrlList[i].quality == "small") {
  819. map.fmtUrlList.splice(i, /*len*/ 1);
  820. --i;
  821. continue;
  822. }
  823. }
  824. }
  825.  
  826. map.fmtUrlList.sort(function(a, b) {
  827. return getQualityIdx(b.quality) - getQualityIdx(a.quality);
  828. });
  829.  
  830. callback(map);
  831. }
  832.  
  833. // Entry point
  834. dom.ajax({ url: url, success: success });
  835. }
  836.  
  837. // -----------------------------------------------------------------------------
  838.  
  839. var CSS_PREFIX = "ujs-";
  840.  
  841. var HDR_LINKS_HTML_ID = CSS_PREFIX + "hdr-links-div";
  842. var LINKS_HTML_ID = CSS_PREFIX + "links-cls";
  843. var LINKS_TP_HTML_ID = CSS_PREFIX + "links-tp-div";
  844. var UPDATE_HTML_ID = CSS_PREFIX + "update-div";
  845. var VID_FMT_BTN_ID = CSS_PREFIX + "vid-fmt-btn";
  846.  
  847. /* The !important attr is to override the page's specificity. */
  848. var CSS_STYLES =
  849. "#" + VID_FMT_BTN_ID + dom.emitCssStyles({
  850. "margin": "0 0.333em"
  851. }) + "\n" +
  852. "#" + UPDATE_HTML_ID + dom.emitCssStyles({
  853. "background-color": "#f00",
  854. "border-radius": "2px",
  855. "color": "#fff",
  856. "padding": "5px",
  857. "text-align": "center",
  858. "text-decoration": "none",
  859. "position": "fixed",
  860. "top": "0.5em",
  861. "right": "0.5em",
  862. "z-index": "100"
  863. }) + "\n" +
  864. "#" + UPDATE_HTML_ID + ":hover" + dom.emitCssStyles({
  865. "background-color": "#0d0"
  866. }) + "\n" +
  867. "#" + HDR_LINKS_HTML_ID + dom.emitCssStyles({
  868. "background-color": "#eee",
  869. "border": "#ccc 1px solid",
  870. //"border-radius": "3px",
  871. "color": "#333",
  872. "font-size": "90%",
  873. "margin": "5px",
  874. "padding": "5px"
  875. }) + "\n" +
  876. "#" + HDR_LINKS_HTML_ID + " ." + CSS_PREFIX + "group" + dom.emitCssStyles({
  877. "background-color": "#fff",
  878. "color": "#000 !important",
  879. "border": "#ccc 1px solid",
  880. "border-radius": "3px",
  881. "display": "inline-block",
  882. "margin": "3px",
  883. }) + "\n" +
  884. "#" + HDR_LINKS_HTML_ID + " a" + dom.emitCssStyles({
  885. "display": "table-cell",
  886. "padding": "3px",
  887. "text-decoration": "none"
  888. }) + "\n" +
  889. "#" + HDR_LINKS_HTML_ID + " a:hover" + dom.emitCssStyles({
  890. "background-color": "#d1e1fa"
  891. }) + "\n" +
  892. "div." + LINKS_HTML_ID + dom.emitCssStyles({
  893. "border-radius": "3px",
  894. "font-size": "90%",
  895. "line-height": "1em",
  896. "position": "absolute",
  897. "left": "0",
  898. "top": "0",
  899. "z-index": "100"
  900. }) + "\n" +
  901. "#" + LINKS_TP_HTML_ID + dom.emitCssStyles({
  902. "background-color": "#eee",
  903. "border": "#aaa 1px solid",
  904. "padding": "3px 0",
  905. "text-decoration": "none",
  906. "white-space": "nowrap",
  907. "z-index": "110"
  908. }) + "\n" +
  909. "div." + LINKS_HTML_ID + " a" + dom.emitCssStyles({
  910. "display": "inline-block",
  911. "margin": "1px",
  912. "text-decoration": "none"
  913. }) + "\n" +
  914. "div." + LINKS_HTML_ID + " ." + CSS_PREFIX + "video" + dom.emitCssStyles({
  915. "display": "inline-block",
  916. "text-align": "center",
  917. "width": "3.5em"
  918. }) + "\n" +
  919. "div." + LINKS_HTML_ID + " ." + CSS_PREFIX + "quality" + dom.emitCssStyles({
  920. "display": "inline-block",
  921. "text-align": "center",
  922. "width": "5.5em"
  923. }) + "\n" +
  924. "." + CSS_PREFIX + "video" + dom.emitCssStyles({
  925. "color": "#fff !important",
  926. "padding": "1px 3px",
  927. "text-align": "center"
  928. }) + "\n" +
  929. "." + CSS_PREFIX + "quality" + dom.emitCssStyles({
  930. "color": "#000 !important",
  931. "display": "table-cell",
  932. "padding": "1px 3px",
  933. "vertical-align": "middle"
  934. }) + "\n" +
  935. "." + CSS_PREFIX + "filesize" + dom.emitCssStyles({
  936. "font-size": "90%",
  937. "margin-top": "2px",
  938. "padding": "1px 3px",
  939. "text-align": "center"
  940. }) + "\n" +
  941. "." + CSS_PREFIX + "filesize-err" + dom.emitCssStyles({
  942. "color": "#f00",
  943. "font-size": "90%",
  944. "margin-top": "2px",
  945. "padding": "1px 3px",
  946. "text-align": "center"
  947. }) + "\n" +
  948. "." + CSS_PREFIX + "not-avail" + dom.emitCssStyles({
  949. "background-color": "#700",
  950. "color": "#fff",
  951. "padding": "3px",
  952. }) + "\n" +
  953. "." + CSS_PREFIX + "3gp" + dom.emitCssStyles({
  954. "background-color": "#bbb"
  955. }) + "\n" +
  956. "." + CSS_PREFIX + "flv" + dom.emitCssStyles({
  957. "background-color": "#0dd"
  958. }) + "\n" +
  959. "." + CSS_PREFIX + "m4a" + dom.emitCssStyles({
  960. "background-color": "#07e"
  961. }) + "\n" +
  962. "." + CSS_PREFIX + "m4v" + dom.emitCssStyles({
  963. "background-color": "#07e"
  964. }) + "\n" +
  965. "." + CSS_PREFIX + "mp3" + dom.emitCssStyles({
  966. "background-color": "#7ba"
  967. }) + "\n" +
  968. "." + CSS_PREFIX + "mp4" + dom.emitCssStyles({
  969. "background-color": "#777"
  970. }) + "\n" +
  971. "." + CSS_PREFIX + "qt" + dom.emitCssStyles({
  972. "background-color": "#f08"
  973. }) + "\n" +
  974. "." + CSS_PREFIX + "webm" + dom.emitCssStyles({
  975. "background-color": "#e0e"
  976. }) + "\n" +
  977. "." + CSS_PREFIX + "wmv" + dom.emitCssStyles({
  978. "background-color": "#c75"
  979. }) + "\n" +
  980. "." + CSS_PREFIX + "small" + dom.emitCssStyles({
  981. "color": "#888 !important",
  982. }) + "\n" +
  983. "." + CSS_PREFIX + "medium" + dom.emitCssStyles({
  984. "color": "#fff !important",
  985. "background-color": "#0d0"
  986. }) + "\n" +
  987. "." + CSS_PREFIX + "large" + dom.emitCssStyles({
  988. "color": "#fff !important",
  989. "background-color": "#00d",
  990. "background-image": "linear-gradient(to right, #00d, #00a)"
  991. }) + "\n" +
  992. "." + CSS_PREFIX + "hd720" + dom.emitCssStyles({
  993. "color": "#fff !important",
  994. "background-color": "#f90",
  995. "background-image": "linear-gradient(to right, #f90, #d70)"
  996. }) + "\n" +
  997. "." + CSS_PREFIX + "hd1080" + dom.emitCssStyles({
  998. "color": "#fff !important",
  999. "background-color": "#f00",
  1000. "background-image": "linear-gradient(to right, #f00, #c00)"
  1001. }) + "\n" +
  1002. "." + CSS_PREFIX + "highres" + dom.emitCssStyles({
  1003. "color": "#fff !important",
  1004. "background-color": "#c0f",
  1005. "background-image": "linear-gradient(to right, #c0f, #90f)"
  1006. }) + "\n" +
  1007. "." + CSS_PREFIX + "pos-rel" + dom.emitCssStyles({
  1008. "position": "relative"
  1009. }) + "\n" +
  1010. "";
  1011.  
  1012. function condInsertHdr(divId) {
  1013. if(dom.gE(HDR_LINKS_HTML_ID))
  1014. return true;
  1015.  
  1016. var insertPtNode = dom.gE(divId);
  1017. if(!insertPtNode)
  1018. return false;
  1019.  
  1020. var divNode = dom.cE("div");
  1021. divNode.id = HDR_LINKS_HTML_ID;
  1022.  
  1023. insertPtNode.parentNode.insertBefore(divNode, insertPtNode);
  1024. return true;
  1025. }
  1026.  
  1027. function condInsertTooltip() {
  1028. if(dom.gE(LINKS_TP_HTML_ID))
  1029. return true;
  1030.  
  1031. var toolTipNode = dom.cE("div");
  1032. toolTipNode.id = LINKS_TP_HTML_ID;
  1033.  
  1034. dom.attr(toolTipNode, "class", LINKS_HTML_ID);
  1035. dom.attr(toolTipNode, "style", "display: none;");
  1036.  
  1037. dom.append(doc.body, toolTipNode);
  1038.  
  1039. dom.addEvent(toolTipNode, "mouseleave", function(event) {
  1040. //logMsg("mouse leave");
  1041. dom.attr(toolTipNode, "style", "display: none;");
  1042. });
  1043. }
  1044.  
  1045. function condInsertUpdateIcon() {
  1046. if(dom.gE(UPDATE_HTML_ID))
  1047. return;
  1048.  
  1049. var divNode = dom.cE("a");
  1050. divNode.id = UPDATE_HTML_ID;
  1051. dom.append(doc.body, divNode);
  1052. }
  1053.  
  1054. // -----------------------------------------------------------------------------
  1055.  
  1056. var STORE_ID = "ujsYtLinks";
  1057. var JSONP_ID = "ujsYtLinks";
  1058.  
  1059. var userConfig = {
  1060. showVideoFormats: true,
  1061. showVideoSize: true,
  1062. tagLinks: true
  1063. };
  1064.  
  1065. var videoInfoCache = {};
  1066.  
  1067. var TAG_LINK_NUM_PER_BATCH = 5;
  1068. var INI_TAG_LINK_DELAY_MS = 200;
  1069. var SUB_TAG_LINK_DELAY_MS = 500;
  1070.  
  1071. // -----------------------------------------------------------------------------
  1072.  
  1073. function Links() {
  1074. }
  1075.  
  1076. Links.prototype.init = function() {
  1077. };
  1078.  
  1079. Links.prototype.getPreferredFmt = function(map) {
  1080. var selElm = map.fmtUrlList[0];
  1081.  
  1082. forEach(map.fmtUrlList, function(idx, elm) {
  1083. if(getVideoName(elm.type).toLowerCase() != "webm") {
  1084. selElm = elm;
  1085. return false;
  1086. }
  1087. });
  1088.  
  1089. return selElm;
  1090. };
  1091.  
  1092. Links.prototype.checkFmts = function(forceFlag) {
  1093. var me = this;
  1094.  
  1095. if(!userConfig.showVideoFormats)
  1096. return;
  1097.  
  1098. if(!forceFlag && userConfig.showVideoFormats == "btn") {
  1099. if(dom.gE(VID_FMT_BTN_ID))
  1100. return;
  1101.  
  1102. var btn = dom.cE("button");
  1103. dom.attr(btn, "id", VID_FMT_BTN_ID);
  1104. dom.attr(btn, "class", "yt-uix-button yt-uix-button-default");
  1105. btn.innerHTML = "VidFmts";
  1106.  
  1107. var mastH = dom.gE("yt-masthead-signin") || dom.gE("yt-masthead-user");
  1108. if(!mastH)
  1109. return;
  1110.  
  1111. dom.prepend(mastH, btn);
  1112.  
  1113. dom.addEvent(btn, "click", function(event) {
  1114. me.checkFmts(/*force*/ true);
  1115. });
  1116.  
  1117. return;
  1118. }
  1119.  
  1120. if(!loc.href.match(/watch\?v=([a-zA-Z0-9_-]*)/))
  1121. return false;
  1122.  
  1123. var videoId = RegExp.$1;
  1124.  
  1125. var url = loc.protocol + "//" + loc.host + "/watch?v=" + videoId;
  1126.  
  1127. getVideoInfo(url, function(map) { me.showLinks("page", map); });
  1128. };
  1129.  
  1130. Links.prototype.genUrl = function(map, elm) {
  1131. var url = elm.url + "&title=" + cnvSafeFname(map.title + getExt(elm));
  1132.  
  1133. if(elm.sig != null)
  1134. url += "&signature=" + elm.sig;
  1135.  
  1136. return url;
  1137. };
  1138.  
  1139. Links.prototype.emitLinks = function(map) {
  1140. function fmtSize(size) {
  1141. var units = [ "kB", "MB", "GB" ];
  1142. var idx = 0;
  1143.  
  1144. for(idx = 0; idx < units.length; ++idx) {
  1145. size /= 1024;
  1146.  
  1147. if(size < 10)
  1148. return Math.round(size * 100) / 100 + units[idx];
  1149.  
  1150. if(size < 100)
  1151. return Math.round(size * 10) / 10 + units[idx];
  1152.  
  1153. if(size < 1024 || idx == units.length - 1)
  1154. return Math.round(size) + units[idx];
  1155. }
  1156. }
  1157.  
  1158. // Entry point
  1159. var me = this;
  1160. var s = [];
  1161.  
  1162. var resMap = {};
  1163.  
  1164. forEach(map.fmtUrlList, function(idx, elm) {
  1165. var fmtMap = map.fmtMap[elm.itag];
  1166.  
  1167. if(!resMap[fmtMap.res]) {
  1168. resMap[fmtMap.res] = [];
  1169. resMap[fmtMap.res].quality = elm.quality;
  1170. }
  1171.  
  1172. resMap[fmtMap.res].push(elm);
  1173. });
  1174.  
  1175. for(var res in resMap) {
  1176. var qFields = [];
  1177.  
  1178. qFields.push(dom.emitHtml("div", { "class": CSS_PREFIX + "quality " + CSS_PREFIX + resMap[res].quality }, res));
  1179.  
  1180. forEach(resMap[res], function(idx, elm) {
  1181. var fields = [];
  1182. var fmtMap = map.fmtMap[elm.itag];
  1183. var videoName = getVideoName(elm.effType || elm.type);
  1184.  
  1185. var varMsg = "";
  1186.  
  1187. if(elm.bitrate != null)
  1188. varMsg = fmtSize(elm.bitrate) + "/s";
  1189. else if(fmtMap.vars != null)
  1190. varMsg = fmtMap.vars.join();
  1191.  
  1192. var addMsg = [ elm.itag, elm.type, elm.size || elm.quality, varMsg ];
  1193.  
  1194. if(elm.s != null)
  1195. addMsg.push("sig-" + elm.s.length);
  1196.  
  1197. if(elm.fileSize != null && elm.fileSize >= 0)
  1198. addMsg.push(fmtSize(elm.fileSize));
  1199.  
  1200. var vidSuffix = "";
  1201.  
  1202. if(elm.itag == 82 || elm.itag == 83 || elm.itag == 84 || elm.itag == 100 || elm.itag == 101 || elm.itag == 102)
  1203. vidSuffix = " (3D)";
  1204.  
  1205. fields.push(dom.emitHtml("div", { "class": CSS_PREFIX + "video " + CSS_PREFIX + videoName.toLowerCase() }, videoName + vidSuffix));
  1206.  
  1207. if(elm.fileSize != null) {
  1208. if(elm.fileSize >= 0) {
  1209. fields.push(dom.emitHtml("div", { "class": CSS_PREFIX + "filesize" }, fmtSize(elm.fileSize)));
  1210. }
  1211. else {
  1212. var msg;
  1213.  
  1214. if(elm.isDrm)
  1215. msg = "DRM";
  1216. else if(elm.s != null)
  1217. msg = "sig-" + elm.s.length;
  1218. else
  1219. msg = "Err";
  1220.  
  1221. fields.push(dom.emitHtml("div", { "class": CSS_PREFIX + "filesize-err" }, msg));
  1222. }
  1223. }
  1224.  
  1225. var url;
  1226.  
  1227. if(elm.isDrm)
  1228. url = elm.conn + "?" + elm.stream;
  1229. else
  1230. url = me.genUrl(map, elm);
  1231.  
  1232. var ahref = dom.emitHtml("a", {
  1233. href: url,
  1234. title: addMsg.join(" | ")
  1235. }, fields.join(""));
  1236.  
  1237. qFields.push(ahref);
  1238. });
  1239.  
  1240. s.push(dom.emitHtml("div", { "class": CSS_PREFIX + "group" }, qFields.join("")));
  1241. }
  1242.  
  1243. return s.join("");
  1244. };
  1245.  
  1246. var INI_SHOW_FILESIZE_DELAY_MS = 500;
  1247. var SUB_SHOW_FILESIZE_DELAY_MS = 200;
  1248.  
  1249. Links.prototype.showLinks = function(divId, map) {
  1250. function updateLinks() {
  1251. //!! Hack to update file size
  1252. if(condInsertHdr(divId))
  1253. dom.html(dom.gE(HDR_LINKS_HTML_ID), me.emitLinks(map));
  1254. }
  1255.  
  1256. // Entry point
  1257. var me = this;
  1258.  
  1259. // video is not avail
  1260. if(!map.fmtUrlList)
  1261. return;
  1262.  
  1263. //logMsg(JSON.stringify(map));
  1264.  
  1265. if(!condInsertHdr(divId))
  1266. return;
  1267.  
  1268. dom.html(dom.gE(HDR_LINKS_HTML_ID), me.emitLinks(map));
  1269.  
  1270. if(!userConfig.showVideoSize)
  1271. return;
  1272.  
  1273. forEach(map.fmtUrlList, function(idx, elm) {
  1274. //logMsg(elm.itag + " " + elm.url);
  1275.  
  1276. // We just fail outright for protected/obfuscated videos
  1277. if(elm.isDrm || elm.s != null) {
  1278. elm.fileSize = -1;
  1279. updateLinks();
  1280. return;
  1281. }
  1282.  
  1283. if(elm.clen != null) {
  1284. elm.fileSize = +elm.clen;
  1285. updateLinks();
  1286. return;
  1287. }
  1288.  
  1289. setTimeout(function() {
  1290. dom.crossAjax({
  1291. type: "HEAD",
  1292. url: me.genUrl(map, elm),
  1293.  
  1294. success: function(data, status, xhr) {
  1295. var fileSize = xhr.getResponseHeader("Content-Length");
  1296. if(fileSize == null)
  1297. return;
  1298.  
  1299. //logMsg(map.title + " " + elm.itag + ": " + fileSize);
  1300. elm.fileSize = +fileSize;
  1301.  
  1302. updateLinks();
  1303. },
  1304.  
  1305. error: function(xhr, status) {
  1306. //logMsg(map.fmtMap[elm.itag].res + " " + getVideoName(elm.type) + ": " + xhr.status);
  1307.  
  1308. if(xhr.status != 403)
  1309. return;
  1310.  
  1311. elm.fileSize = -1;
  1312.  
  1313. updateLinks();
  1314. },
  1315.  
  1316. complete: function(xhr) {
  1317. //logMsg(map.title + ": " + xhr.getAllResponseHeaders());
  1318. }
  1319. });
  1320. }, INI_SHOW_FILESIZE_DELAY_MS + idx * SUB_SHOW_FILESIZE_DELAY_MS);
  1321. });
  1322. };
  1323.  
  1324. Links.prototype.tagLinks = function() {
  1325. var SCANNED = 1;
  1326. var REQ_INFO = 2;
  1327. var ADDED_INFO = 3;
  1328.  
  1329. function prepareTagHtml(node, map) {
  1330. var elm = me.getPreferredFmt(map);
  1331. var fmtMap = map.fmtMap[elm.itag];
  1332.  
  1333. dom.attr(node, "class", LINKS_HTML_ID + " " + CSS_PREFIX + "quality " + CSS_PREFIX + elm.quality);
  1334.  
  1335. dom.addEvent(node, "mouseenter", function(event) {
  1336. //logMsg("mouse enter " + map.videoId);
  1337. var pos = dom.offset(node);
  1338. //logMsg("mouse enter: x " + pos.left + ", y " + pos.top);
  1339.  
  1340. var toolTipNode = dom.gE(LINKS_TP_HTML_ID);
  1341.  
  1342. dom.attr(toolTipNode, "style", "position: absolute; left: " + pos.left + "px; top: " + pos.top + "px");
  1343.  
  1344. dom.html(toolTipNode, me.emitLinks(map));
  1345. });
  1346.  
  1347. node.href = elm.url + "&title=" + cnvSafeFname(map.title + getExt(elm));
  1348.  
  1349. return fmtMap.res;
  1350. }
  1351.  
  1352. function addTag(hNode, map) {
  1353. //logMsg(dom.html(hNode));
  1354. //logMsg("hNode " + dom.attr(hNode, "class"));
  1355. //var img = dom.gT(hNode, "img") [0];
  1356. //logMsg(dom.attr(img, "src"));
  1357. //logMsg(dom.attr(img, "class"));
  1358.  
  1359. dom.attr(hNode, CSS_PREFIX + "processed", ADDED_INFO);
  1360.  
  1361. var node = dom.cE("div");
  1362.  
  1363. if(map.fmtUrlList) {
  1364. tagHtml = prepareTagHtml(node, map);
  1365. }
  1366. else {
  1367. dom.attr(node, "class", LINKS_HTML_ID + " " + CSS_PREFIX + "not-avail");
  1368. tagHtml = "NA";
  1369. }
  1370.  
  1371. var parentNode = hNode.parentNode;
  1372. var parentCssPositionStyle = window.getComputedStyle(parentNode, null).getPropertyValue("position");
  1373.  
  1374. if(parentCssPositionStyle != "absolute" && parentCssPositionStyle != "relative")
  1375. dom.attr(parentNode, "class", dom.attr(parentNode, "class") + " " + CSS_PREFIX + "pos-rel");
  1376.  
  1377. hNode.parentNode.insertBefore(node, hNode);
  1378.  
  1379. dom.html(node, tagHtml);
  1380. }
  1381.  
  1382. function getFmt(videoId, hNode) {
  1383. if(videoInfoCache[videoId]) {
  1384. addTag(hNode, videoInfoCache[videoId]);
  1385. return;
  1386. }
  1387.  
  1388. var url;
  1389.  
  1390. if(videoId.match(/.+==$/))
  1391. url = loc.protocol + "//" + loc.host + "/cthru?key=" + videoId;
  1392. else
  1393. url = loc.protocol + "//" + loc.host + "/watch?v=" + videoId;
  1394.  
  1395. getVideoInfo(url, function(map) {
  1396. videoInfoCache[videoId] = map;
  1397. addTag(hNode, map);
  1398. });
  1399. }
  1400.  
  1401. // Entry point
  1402. var me = this;
  1403.  
  1404. var list = [];
  1405.  
  1406. forEach(dom.gT("a"), function(idx, hNode) {
  1407. if(dom.attr(hNode, CSS_PREFIX + "processed"))
  1408. return;
  1409.  
  1410. if(!dom.inViewport(hNode))
  1411. return;
  1412.  
  1413. dom.attr(hNode, CSS_PREFIX + "processed", SCANNED);
  1414.  
  1415. if(!hNode.href.match(/watch\?v=([a-zA-Z0-9_-]*)/) &&
  1416. !hNode.href.match(/watch_videos.+?&video_ids=([a-zA-Z0-9_-]*)/))
  1417. return;
  1418.  
  1419. var videoId = RegExp.$1;
  1420.  
  1421. var cls = dom.attr(hNode, "class") || "";
  1422. if(cls == "yt-button" || cls.match(/yt-uix-button/))
  1423. return;
  1424.  
  1425. if(dom.attr(hNode.parentNode, "class") == "video-time")
  1426. return;
  1427.  
  1428. if(dom.html(hNode).match(/video-logo/i))
  1429. return;
  1430.  
  1431. var img = dom.gT(hNode, "img");
  1432. if(img == null || img.length == 0)
  1433. return;
  1434.  
  1435. img = img[0];
  1436.  
  1437. var imgSrc = dom.attr(img, "src") || "";
  1438. if(imgSrc.indexOf("ytimg.com") < 0)
  1439. return;
  1440.  
  1441. var tnSrc = dom.attr(img, "thumb") || "";
  1442.  
  1443. if(imgSrc.match(/.+?\/([a-zA-Z0-9_-]*)\/default\.jpg$/))
  1444. videoId = RegExp.$1;
  1445. else if(tnSrc.match(/.+?\/([a-zA-Z0-9_-]*)\/default\.jpg$/))
  1446. videoId = RegExp.$1;
  1447.  
  1448. //logMsg(idx + " " + hNode.href);
  1449. //logMsg("videoId: " + videoId);
  1450.  
  1451. list.push({ videoId: videoId, hNode: hNode });
  1452.  
  1453. dom.attr(hNode, CSS_PREFIX + "processed", REQ_INFO);
  1454. });
  1455.  
  1456. forLoop({ num: list.length, inc: TAG_LINK_NUM_PER_BATCH, batchIdx: 0 }, function(idx) {
  1457. var batchIdx = this.batchIdx++;
  1458. var batchList = list.slice(idx, idx + TAG_LINK_NUM_PER_BATCH);
  1459.  
  1460. setTimeout(function() {
  1461. forEach(batchList, function(idx, elm) {
  1462. //logMsg(batchIdx + " " + idx + " " + elm.hNode.href);
  1463. getFmt(elm.videoId, elm.hNode);
  1464. });
  1465. }, INI_TAG_LINK_DELAY_MS + batchIdx * SUB_TAG_LINK_DELAY_MS);
  1466. });
  1467. };
  1468.  
  1469. Links.prototype.periodicTagLinks = function(delayMs) {
  1470. function poll() {
  1471. me.tagLinks();
  1472. me.tagLinksTimerId = setTimeout(poll, 3000);
  1473. }
  1474.  
  1475. // Entry point
  1476. if(!userConfig.tagLinks)
  1477. return;
  1478.  
  1479. var me = this;
  1480.  
  1481. delayMs = delayMs || 0;
  1482.  
  1483. if(me.tagLinksTimerId != null) {
  1484. clearTimeout(me.tagLinksTimerId);
  1485. delete me.tagLinksTimerId;
  1486. }
  1487.  
  1488. setTimeout(poll, delayMs);
  1489. };
  1490.  
  1491. // -----------------------------------------------------------------------------
  1492.  
  1493. Links.prototype.loadSettings = function() {
  1494. var obj = localStorage[STORE_ID];
  1495. if(obj == null)
  1496. return;
  1497.  
  1498. obj = JSON.parse(obj);
  1499.  
  1500. this.lastChkReqTs = +obj.lastChkReqTs;
  1501. this.lastChkTs = +obj.lastChkTs;
  1502. this.lastChkVer = +obj.lastChkVer;
  1503. };
  1504.  
  1505. Links.prototype.storeSettings = function() {
  1506. localStorage[STORE_ID] = JSON.stringify({
  1507. lastChkReqTs: this.lastChkReqTs,
  1508. lastChkTs: this.lastChkTs,
  1509. lastChkVer: this.lastChkVer
  1510. });
  1511. };
  1512.  
  1513. // -----------------------------------------------------------------------------
  1514.  
  1515. var UPDATE_CHK_INTERVAL = 5 * 86400;
  1516. var FAIL_TO_CHK_UPDATE_INTERVAL = 14 * 86400;
  1517.  
  1518. Links.prototype.chkVer = function(forceFlag) {
  1519. if(this.lastChkVer > relInfo.ver) {
  1520. this.showNewVer({ ver: this.lastChkVer });
  1521. return;
  1522. }
  1523.  
  1524. var now = timeNowInSec();
  1525.  
  1526. //logMsg("lastChkReqTs " + this.lastChkReqTs + ", diff " + (now - this.lastChkReqTs));
  1527. //logMsg("lastChkTs " + this.lastChkTs);
  1528. //logMsg("lastChkVer " + this.lastChkVer);
  1529.  
  1530. if(this.lastChkReqTs == null || now < this.lastChkReqTs) {
  1531. this.lastChkReqTs = now;
  1532. this.storeSettings();
  1533. return;
  1534. }
  1535.  
  1536. if(now - this.lastChkReqTs < UPDATE_CHK_INTERVAL)
  1537. return;
  1538.  
  1539. if(this.lastChkReqTs - this.lastChkTs > FAIL_TO_CHK_UPDATE_INTERVAL)
  1540. logMsg("Failed to check ver for " + ((this.lastChkReqTs - this.lastChkTs) / 86400) + " days");
  1541.  
  1542. this.lastChkReqTs = now;
  1543. this.storeSettings();
  1544.  
  1545. unsafeWin[JSONP_ID] = this;
  1546.  
  1547. var script = dom.cE("script");
  1548. script.type = "text/javascript";
  1549. script.src = SCRIPT_UPDATE_LINK;
  1550. dom.append(doc.body, script);
  1551. };
  1552.  
  1553. Links.prototype.chkVerCallback = function(data) {
  1554. delete unsafeWin[JSONP_ID];
  1555.  
  1556. this.lastChkTs = timeNowInSec();
  1557. this.storeSettings();
  1558.  
  1559. //logMsg(JSON.stringify(data));
  1560.  
  1561. var latestElm = data[0];
  1562.  
  1563. if(latestElm.ver <= relInfo.ver)
  1564. return;
  1565.  
  1566. this.showNewVer(latestElm);
  1567. };
  1568.  
  1569. Links.prototype.showNewVer = function(latestElm) {
  1570. function getVerStr(ver) {
  1571. var verStr = "" + ver;
  1572.  
  1573. var majorV = verStr.substr(0, verStr.length - 4) || "0";
  1574. var minorV = verStr.substr(verStr.length - 4, 2);
  1575. return majorV + "." + minorV;
  1576. }
  1577.  
  1578. // Entry point
  1579. this.lastChkVer = latestElm.ver;
  1580. this.storeSettings();
  1581.  
  1582. condInsertUpdateIcon();
  1583.  
  1584. var aNode = dom.gE(UPDATE_HTML_ID);
  1585.  
  1586. aNode.href = SCRIPT_LINK;
  1587.  
  1588. if(latestElm.desc != null)
  1589. dom.attr(aNode, "title", latestElm.desc);
  1590.  
  1591. dom.html(aNode, dom.emitHtml("b", SCRIPT_NAME + " " + getVerStr(relInfo.ver)) +
  1592. "<br>Click to update to " + getVerStr(latestElm.ver));
  1593. };
  1594.  
  1595. // -----------------------------------------------------------------------------
  1596.  
  1597. var inst = new Links();
  1598.  
  1599. inst.init();
  1600. inst.loadSettings();
  1601. decryptSig.load();
  1602.  
  1603. dom.insertCss(CSS_STYLES);
  1604.  
  1605. condInsertTooltip();
  1606.  
  1607. if(loc.pathname.match(/\/watch/)) {
  1608. inst.checkFmts();
  1609. }
  1610.  
  1611. inst.periodicTagLinks();
  1612.  
  1613. var scrollTop = win.pageYOffset || doc.documentElement.scrollTop;
  1614.  
  1615. dom.addEvent(win, "scroll", function(e) {
  1616. var newScrollTop = win.pageYOffset || doc.documentElement.scrollTop;
  1617.  
  1618. if(Math.abs(newScrollTop - scrollTop) < 100)
  1619. return;
  1620.  
  1621. //logMsg("scroll by " + (newScrollTop - scrollTop));
  1622.  
  1623. scrollTop = newScrollTop;
  1624.  
  1625. inst.periodicTagLinks(200);
  1626. });
  1627.  
  1628. inst.chkVer();
  1629.  
  1630. // -----------------------------------------------------------------------------
  1631.  
  1632. /* YouTube reuses the current page when the user clicks on a new video. We need
  1633. to detect it and reload the formats. */
  1634.  
  1635. (function() {
  1636.  
  1637. var PERIODIC_CHK_VIDEO_URL_MS = 1000;
  1638.  
  1639. var curVideoUrl = loc.toString();
  1640.  
  1641. function periodicChkVideoUrl() {
  1642. var newVideoUrl = loc.toString();
  1643.  
  1644. if(curVideoUrl != newVideoUrl) {
  1645. //console.log(curVideoUrl + " -> " + newVideoUrl);
  1646.  
  1647. curVideoUrl = newVideoUrl;
  1648.  
  1649. if(loc.pathname.match(/\/watch/))
  1650. inst.checkFmts();
  1651. }
  1652.  
  1653. setTimeout(periodicChkVideoUrl, PERIODIC_CHK_VIDEO_URL_MS);
  1654. }
  1655.  
  1656. periodicChkVideoUrl();
  1657.  
  1658. }) ();
  1659.  
  1660. // -----------------------------------------------------------------------------
  1661.  
  1662. }) ();