Greasy Fork is available in English.

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