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

QingJ © 2025

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