YouTube Links

Download YouTube videos. Video formats are listed at the top of the watch page. Video links are tagged so that they can be downloaded easily.

当前为 2018-09-22 提交的版本,查看 最新版本

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

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址