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.

目前为 2020-06-06 提交的版本。查看 最新版本

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