Greasy Fork 支持简体中文。

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-02-01 提交的版本,檢視 最新版本

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