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.

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