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.

当前为 2021-12-16 提交的版本,查看 最新版本

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