Advertisement
Chronos_Ouroboros

Youtube Links

May 30th, 2015
968
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
text 48.13 KB | None | 0 0
  1. // ==UserScript==
  2. // @name YouTube Links
  3. // @namespace http://www.smallapple.net/
  4. // @description Download YouTube videos. Supports 60fps video and 256kbps MP4 audio! Tag every video link so that they can be downloaded easily. Show the video formats at the top of the watch page.
  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. // @version 1.84
  15. // ==/UserScript==
  16.  
  17. /* This is based on YouTube HD Suite 3.4.1 */
  18.  
  19. /* Tested on Firefox 5.0, Chrome 13 and Opera 11.50 */
  20.  
  21. (function() {
  22.  
  23. // =============================================================================
  24.  
  25. var win = typeof(unsafeWindow) !== "undefined" ? unsafeWindow : window;
  26. var doc = win.document;
  27. var loc = win.location;
  28.  
  29. if(win.top != win.self)
  30. return;
  31.  
  32. var unsafeWin = win;
  33.  
  34. // Hack to get unsafe window in Chrome
  35. (function() {
  36.  
  37. var isChrome = navigator.userAgent.toLowerCase().indexOf("chrome") >= 0;
  38.  
  39. if(!isChrome)
  40. return;
  41.  
  42. // Chrome 27 fixed this exploit, but luckily, its unsafeWin now works for us
  43. try {
  44. var div = doc.createElement("div");
  45. div.setAttribute("onclick", "return window;");
  46. unsafeWin = div.onclick();
  47. } catch(e) {
  48. }
  49.  
  50. }) ();
  51.  
  52. // =============================================================================
  53.  
  54. var SCRIPT_NAME = "YouTube Links";
  55.  
  56. var relInfo = {
  57. ver: 18400,
  58. ts: 2015050500,
  59. desc: "Set download filename"
  60. };
  61.  
  62. var SCRIPT_UPDATE_LINK = loc.protocol + "//greasyfork.org/scripts/5565-youtube-links-updater/code/YouTube Links Updater.user.js";
  63. var SCRIPT_LINK = loc.protocol + "//greasyfork.org/scripts/5566-youtube-links/code/YouTube Links.user.js";
  64.  
  65. // =============================================================================
  66.  
  67. var dom = {};
  68.  
  69. dom.gE = function(id) {
  70. return doc.getElementById(id);
  71. };
  72.  
  73. dom.gT = function(dom, tag) {
  74. if(arguments.length == 1) {
  75. tag = dom;
  76. dom = doc;
  77. }
  78.  
  79. return dom.getElementsByTagName(tag);
  80. };
  81.  
  82. dom.cE = function(tag) {
  83. return document.createElement(tag);
  84. };
  85.  
  86. dom.cT = function(s) {
  87. return doc.createTextNode(s);
  88. };
  89.  
  90. dom.attr = function(obj, k, v) {
  91. if(arguments.length == 2)
  92. return obj.getAttribute(k);
  93.  
  94. obj.setAttribute(k, v);
  95. };
  96.  
  97. dom.prepend = function(obj, child) {
  98. obj.insertBefore(child, obj.firstChild);
  99. };
  100.  
  101. dom.append = function(obj, child) {
  102. obj.appendChild(child);
  103. };
  104.  
  105. dom.offset = function(obj) {
  106. var x = 0;
  107. var y = 0;
  108.  
  109. if(obj.getBoundingClientRect) {
  110. var box = obj.getBoundingClientRect();
  111. var owner = obj.ownerDocument;
  112.  
  113. x = box.left + Math.max(owner.documentElement.scrollLeft, owner.body.scrollLeft) - owner.documentElement.clientLeft;
  114. y = box.top + Math.max(owner.documentElement.scrollTop, owner.body.scrollTop) - owner.documentElement.clientTop;
  115.  
  116. return { left: x, top: y };
  117. }
  118.  
  119. if(obj.offsetParent) {
  120. do {
  121. x += obj.offsetLeft - obj.scrollLeft;
  122. y += obj.offsetTop - obj.scrollTop;
  123. obj = obj.offsetParent;
  124. } while(obj);
  125. }
  126.  
  127. return { left: x, top: y };
  128. };
  129.  
  130. dom.inViewport = function(el) {
  131. var rect = el.getBoundingClientRect();
  132.  
  133. return rect.bottom >= 0 &&
  134. rect.right >= 0 &&
  135. rect.top < (win.innerHeight || doc.documentElement.clientHeight) &&
  136. rect.left < (win.innerWidth || doc.documentElement.clientWidth);
  137. };
  138.  
  139. dom.html = function(obj, s) {
  140. if(arguments.length == 1)
  141. return obj.innerHTML;
  142.  
  143. obj.innerHTML = s;
  144. };
  145.  
  146. dom.emitHtml = function(tag, attrs, body) {
  147. if(arguments.length == 2) {
  148. if(typeof(attrs) == "string") {
  149. body = attrs;
  150. attrs = {};
  151. }
  152. }
  153.  
  154. var list = [];
  155.  
  156. for(var k in attrs) {
  157. list.push(k + "='" + attrs[k].replace(/'/g, "&#39;") + "'");
  158. }
  159.  
  160. var s = "<" + tag + " " + list.join(" ") + ">";
  161.  
  162. if(body != null)
  163. s += body + "</" + tag + ">";
  164.  
  165. return s;
  166. };
  167.  
  168. dom.emitCssStyles = function(styles) {
  169. var list = [];
  170.  
  171. for(var k in styles) {
  172. list.push(k + ": " + styles[k] + ";");
  173. }
  174.  
  175. return " { " + list.join(" ") + " }";
  176. };
  177.  
  178. dom.ajax = function(opts) {
  179. function newXhr() {
  180. if(window.ActiveXObject) {
  181. try {
  182. return new ActiveXObject("Msxml2.XMLHTTP");
  183. } catch(e) {
  184. }
  185.  
  186. try {
  187. return new ActiveXObject("Microsoft.XMLHTTP");
  188. } catch(e) {
  189. return null;
  190. }
  191. }
  192.  
  193. if(window.XMLHttpRequest)
  194. return new XMLHttpRequest();
  195.  
  196. return null;
  197. }
  198.  
  199. function nop() {
  200. }
  201.  
  202. // Entry point
  203. var xhr = newXhr();
  204.  
  205. opts = addProp({
  206. type: "GET",
  207. async: true,
  208. success: nop,
  209. error: nop,
  210. complete: nop
  211. }, opts);
  212.  
  213. xhr.open(opts.type, opts.url, opts.async);
  214.  
  215. xhr.onreadystatechange = function() {
  216. if(xhr.readyState == 4) {
  217. var status = +xhr.status;
  218.  
  219. if(status >= 200 && status < 300) {
  220. opts.success(xhr.responseText, "success", xhr);
  221. }
  222. else {
  223. opts.error(xhr, "error");
  224. }
  225.  
  226. opts.complete(xhr);
  227. }
  228. };
  229.  
  230. xhr.send("");
  231. };
  232.  
  233. dom.crossAjax = function(opts) {
  234. function wrapXhr(xhr) {
  235. var headers = xhr.responseHeaders.replace("\r", "").split("\n");
  236.  
  237. var obj = {};
  238.  
  239. forEach(headers, function(idx, elm) {
  240. var nv = elm.split(":");
  241. if(nv[1] != null)
  242. obj[nv[0].toLowerCase()] = nv[1].replace(/^\s+/, "").replace(/\s+$/, "");
  243. });
  244.  
  245. var responseXML = null;
  246.  
  247. if(opts.dataType == "xml")
  248. responseXML = new DOMParser().parseFromString(xhr.responseText, "text/xml");
  249.  
  250. return {
  251. responseText: xhr.responseText,
  252. responseXML: responseXML,
  253. status: xhr.status,
  254.  
  255. getAllResponseHeaders: function() {
  256. return xhr.responseHeaders;
  257. },
  258.  
  259. getResponseHeader: function(name) {
  260. return obj[name.toLowerCase()];
  261. }
  262. };
  263. }
  264.  
  265. function nop() {
  266. }
  267.  
  268. // Entry point
  269. opts = addProp({
  270. type: "GET",
  271. async: true,
  272. success: nop,
  273. error: nop,
  274. complete: nop
  275. }, opts);
  276.  
  277. if(typeof GM_xmlhttpRequest === "undefined") {
  278. setTimeout(function() {
  279. var xhr = {};
  280. opts.error(xhr, "error");
  281. opts.complete(xhr);
  282. }, 0);
  283. return;
  284. }
  285.  
  286. // TamperMonkey does not handle URLs starting with //
  287. var url;
  288.  
  289. if(opts.url.match(/^\/\//))
  290. url = loc.protocol + opts.url;
  291. else
  292. url = opts.url;
  293.  
  294. GM_xmlhttpRequest({
  295. method: opts.type,
  296. url: url,
  297. synchronous: !opts.async,
  298.  
  299. onload: function(xhr) {
  300. xhr = wrapXhr(xhr);
  301.  
  302. if(xhr.status >= 200 && xhr.status < 300)
  303. opts.success(xhr.responseXML || xhr.responseText, "success", xhr);
  304. else
  305. opts.error(xhr, "error");
  306.  
  307. opts.complete(xhr);
  308. },
  309.  
  310. onerror: function(xhr) {
  311. xhr = wrapXhr(xhr);
  312. opts.error(xhr, "error");
  313. opts.complete(xhr);
  314. }
  315. });
  316. };
  317.  
  318. dom.addEvent = function(e, type, fn) {
  319. function mouseEvent(event) {
  320. if(this != event.relatedTarget && !dom.isAChildOf(this, event.relatedTarget))
  321. fn.call(this, event);
  322. }
  323.  
  324. // Entry point
  325. if(e.addEventListener) {
  326. var effFn = fn;
  327.  
  328. if(type == "mouseenter") {
  329. type = "mouseover";
  330. effFn = mouseEvent;
  331. }
  332. else if(type == "mouseleave") {
  333. type = "mouseout";
  334. effFn = mouseEvent;
  335. }
  336.  
  337. e.addEventListener(type, effFn, /*capturePhase*/ false);
  338. }
  339. else
  340. e.attachEvent("on" + type, function() { fn(win.event); });
  341. };
  342.  
  343. dom.insertCss = function (styles) {
  344. var ss = dom.cE("style");
  345. dom.attr(ss, "type", "text/css");
  346.  
  347. var hh = dom.gT("head") [0];
  348. dom.append(hh, ss);
  349. dom.append(ss, dom.cT(styles));
  350. };
  351.  
  352. dom.isAChildOf = function(parent, child) {
  353. if(parent === child)
  354. return false;
  355.  
  356. while(child && child !== parent) {
  357. child = child.parentNode;
  358. }
  359.  
  360. return child === parent;
  361. };
  362.  
  363. // -----------------------------------------------------------------------------
  364.  
  365. function timeNowInSec() {
  366. return Math.round(+new Date() / 1000);
  367. }
  368.  
  369. function forLoop(opts, fn) {
  370. opts = addProp({ start: 0, inc: 1 }, opts);
  371.  
  372. for(var idx = opts.start; idx < opts.num; idx += opts.inc) {
  373. if(fn.call(opts, idx, opts) === false)
  374. break;
  375. }
  376. }
  377.  
  378. function forEach(list, fn) {
  379. forLoop({ num: list.length }, function(idx) {
  380. return fn.call(list[idx], idx, list[idx]);
  381. });
  382. }
  383.  
  384. function addProp(dest, src) {
  385. for(var k in src) {
  386. if(src[k] != null)
  387. dest[k] = src[k];
  388. }
  389.  
  390. return dest;
  391. }
  392.  
  393. function inArray(elm, array) {
  394. for(var i = 0; i < array.length; ++i) {
  395. if(array[i] === elm)
  396. return i;
  397. }
  398.  
  399. return -1;
  400. }
  401.  
  402. function unescHtmlEntities(s) {
  403. return s.replace(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/&amp;/g, "&").replace(/&quot;/g, '"').replace(/&#39;/g, "'");
  404. }
  405.  
  406. function logMsg(s) {
  407. win.console.log(s);
  408. }
  409.  
  410. function cnvSafeFname(s) {
  411. return s.replace(/:/g, "-").replace(/"/g, "'").replace(/[\\/|*?]/g, "_");
  412. }
  413.  
  414. function encodeSafeFname(s) {
  415. return encodeURIComponent(cnvSafeFname(s)).replace(/'/g, "%27");
  416. }
  417.  
  418. function getVideoName(s) {
  419. var list = [
  420. { name: "3GP", codec: "video/3gpp" },
  421. { name: "FLV", codec: "video/x-flv" },
  422. { name: "M4V", codec: "video/x-m4v" },
  423. { name: "MP3", codec: "audio/mpeg" },
  424. { name: "MP4", codec: "video/mp4" },
  425. { name: "M4A", codec: "audio/mp4" },
  426. { name: "QT", codec: "video/quicktime" },
  427. { name: "WEBM", codec: "audio/webm" },
  428. { name: "WEBM", codec: "video/webm" },
  429. { name: "WMV", codec: "video/ms-wmv" }
  430. ];
  431.  
  432. var name = "?";
  433.  
  434. forEach(list, function(idx, elm) {
  435. if(s.match("^" + elm.codec)) {
  436. name = elm.name;
  437. return false;
  438. }
  439. });
  440.  
  441. return name;
  442. }
  443.  
  444. function snapToStdRes(res) {
  445. var horzResList = [ 3840, 2048, 1440, 960, 640, 480, 320, 176 ];
  446. var horzWideResList = [ 2880, 1920, 1280, 854, 640, 426, 256 ];
  447. var vertResList = [ 2160, 1536, 1080, 720, 480, 360, 240, 144 ];
  448.  
  449. if(!res.match(/^(\d+)x(\d+)/))
  450. return res;
  451.  
  452. var wd = +RegExp.$1;
  453. var ht = +RegExp.$2;
  454. var foundIdx;
  455.  
  456. // Snap to the nearest vert res first
  457. forEach(vertResList, function(idx, elm) {
  458. var tolerance = elm * 0.1;
  459. if(ht >= elm - tolerance && ht <= elm + tolerance) {
  460. foundIdx = idx;
  461. return false;
  462. }
  463. });
  464.  
  465. if(!foundIdx)
  466. return res;
  467.  
  468. var aspectRatio = wd >= ht ? wd / ht : ht / wd;
  469.  
  470. ht = vertResList[foundIdx];
  471. wd = Math.round(ht * aspectRatio);
  472.  
  473. // Snap to the nearest horz res
  474. forEach(aspectRatio < 1.5 ? horzResList : horzWideResList, function(idx, elm) {
  475. var tolerance = elm * 0.1;
  476. if(wd >= elm - tolerance && wd <= elm + tolerance) {
  477. wd = elm;
  478. return false;
  479. }
  480. });
  481.  
  482. return wd + "x" + ht;
  483. }
  484.  
  485. function cnvResName(res) {
  486. var resMap = {
  487. "audio": "Audio"
  488. };
  489.  
  490. if(resMap[res])
  491. return resMap[res];
  492.  
  493. if(!res.match(/^(\d+)x(\d+)/))
  494. return res;
  495.  
  496. var wd = +RegExp.$1;
  497. var ht = +RegExp.$2;
  498.  
  499. var vertResMap = {
  500. "2160": "4K",
  501. "1536": "2K",
  502. "240": "240v",
  503. "144": "144v"
  504. };
  505.  
  506. if(vertResMap[ht])
  507. return vertResMap[ht];
  508.  
  509. var aspectRatio = wd >= ht ? wd / ht : ht / wd;
  510.  
  511. return String(ht) + (aspectRatio < 1.5 ? "f" : "p");
  512. }
  513.  
  514. function mapResToQuality(res) {
  515. if(!res.match(/^[0-9]+x([0-9]+)$/))
  516. return res;
  517.  
  518. var resList = [
  519. { res: 1536, q : "highres" },
  520. { res: 1080, q: "hd1080" },
  521. { res: 720, q : "hd720" },
  522. { res: 480, q : "large" },
  523. { res: 360, q : "medium" }
  524. ];
  525.  
  526. var res = +RegExp.$1;
  527.  
  528. for(var i = 0; i < resList.length; ++i) {
  529. if(res >= resList[i].res)
  530. return resList[i].q;
  531. }
  532.  
  533. return "small";
  534. }
  535.  
  536. function getQualityIdx(quality) {
  537. var list = [ "small", "medium", "large", "hd720", "hd1080", "highres" ];
  538.  
  539. for(var i = 0; i < list.length; ++i) {
  540. if(list[i] == quality)
  541. return i;
  542. }
  543.  
  544. return -1;
  545. }
  546.  
  547. // =============================================================================
  548.  
  549. RegExp.escape = function(s) {
  550. return String(s).replace(/([.*+?^=!:${}()|\[\]\/\\])/g, "\\$1");
  551. };
  552.  
  553. var decryptSig = {
  554. store: {}
  555. };
  556.  
  557. (function () {
  558.  
  559. var SIG_STORE_ID = "ujsYtLinksSig";
  560.  
  561. var CHK_SIG_INTERVAL = 3 * 86400;
  562.  
  563. decryptSig.load = function() {
  564. var obj = localStorage[SIG_STORE_ID];
  565. if(obj == null)
  566. return;
  567.  
  568. decryptSig.store = JSON.parse(obj);
  569. };
  570.  
  571. decryptSig.save = function() {
  572. localStorage[SIG_STORE_ID] = JSON.stringify(decryptSig.store);
  573. };
  574.  
  575. decryptSig.extractScriptUrl = function(data) {
  576. if(data.match(/ytplayer.config\s*=.*\"assets"\s*:\s*{.*"js"\s*:\s*(".+?")/))
  577. return JSON.parse(RegExp.$1);
  578. else
  579. return false;
  580. };
  581.  
  582. decryptSig.getScriptName = function(url) {
  583. if(url.match(/\/yts\/jsbin\/html5player-(.*)\/html5player\.js$/))
  584. return RegExp.$1;
  585.  
  586. if(url.match(/\/html5player-(.*)\.js$/))
  587. return RegExp.$1;
  588.  
  589. return url;
  590. };
  591.  
  592. decryptSig.fetchScript = function(scriptName, url) {
  593. function success(data) {
  594. if(!data.match(/\.signature\s*=\s*(\w+)\(\w+\)/) && !data.match(/\.set\(\"signature\",(\w+)\(\w+\)\)/) )
  595. return;
  596.  
  597. //console.log(scriptName + " sig fn: " + RegExp.$1);
  598.  
  599. if(!data.match(new RegExp("function " + RegExp.$1 + '\\s*\\((\\w+)\\)\\s*{(\\w+=\\w+\\.split\\(""\\);.+?;return \\w+\\.join\\(""\\))')))
  600. return;
  601.  
  602. var fnParam = RegExp.$1;
  603. var fnBody = RegExp.$2;
  604.  
  605. var fnHlp = {};
  606. var objHlp = {};
  607.  
  608. //console.log("param: " + fnParam);
  609. //console.log(fnBody);
  610.  
  611. fnBody = fnBody.split(";");
  612.  
  613. forEach(fnBody, function(idx, elm) {
  614. // its own property
  615. if(elm.match(new RegExp("^" + fnParam + "=" + fnParam + "\\.")))
  616. return;
  617.  
  618. // global fn
  619. if(elm.match(new RegExp("^" + fnParam + "=([a-zA-Z_$][a-zA-Z0-9_$]*)\\("))) {
  620. var name = RegExp.$1;
  621. //console.log("fnHlp: " + name);
  622.  
  623. if(fnHlp[name])
  624. return;
  625.  
  626. if(data.match(new RegExp("(function " + RegExp.escape(RegExp.$1) + ".+?;return \\w+})")))
  627. fnHlp[name] = RegExp.$1;
  628.  
  629. return;
  630. }
  631.  
  632. // object fn
  633. if(elm.match(new RegExp("^([a-zA-Z_$][a-zA-Z0-9_$]*)\.([a-zA-Z_$][a-zA-Z0-9_$]*)\\("))) {
  634. var name = RegExp.$1;
  635. //console.log("objHlp: " + name);
  636.  
  637. if(objHlp[name])
  638. return;
  639.  
  640. if(data.match(new RegExp("(var " + RegExp.escape(RegExp.$1) + "={.+?};)")))
  641. objHlp[name] = RegExp.$1;
  642.  
  643. return;
  644. }
  645. });
  646.  
  647. //console.log(fnHlp);
  648. //console.log(objHlp);
  649.  
  650. var fnHlpStr = "";
  651.  
  652. for(var k in fnHlp)
  653. fnHlpStr += fnHlp[k];
  654.  
  655. for(var k in objHlp)
  656. fnHlpStr += objHlp[k];
  657.  
  658. var fullFn = "function(" + fnParam + "){" + fnHlpStr + fnBody.join(";") + "}";
  659. //console.log(fullFn);
  660.  
  661. decryptSig.store[scriptName] = { ver: relInfo.ver, ts: timeNowInSec(), fn: fullFn };
  662. //console.log(decryptSig);
  663.  
  664. decryptSig.save();
  665. }
  666.  
  667. // Entry point
  668. dom.crossAjax({ url: url, success: success });
  669. };
  670.  
  671. decryptSig.condFetchScript = function(url) {
  672. var scriptName = decryptSig.getScriptName(url);
  673. var store = decryptSig.store[scriptName];
  674. var now = timeNowInSec();
  675.  
  676. if(store && now - store.ts < CHK_SIG_INTERVAL && store.ver == relInfo.ver)
  677. return;
  678.  
  679. decryptSig.fetchScript(scriptName, url);
  680. };
  681.  
  682. }) ();
  683.  
  684. function deobfuscateVideoSig(scriptName, sig) {
  685. if(!decryptSig.store[scriptName])
  686. return sig;
  687.  
  688. //console.log(decryptSig.store[scriptName].fn);
  689.  
  690. try {
  691. sig = eval("(" + decryptSig.store[scriptName].fn + ") (\"" + sig + "\")");
  692. } catch(e) {
  693. }
  694.  
  695. return sig;
  696. }
  697.  
  698. // =============================================================================
  699.  
  700. function parseStreamMap(map, value) {
  701. var fmtUrlList = [];
  702.  
  703. forEach(value.split(","), function(idx, elm) {
  704. var elms = elm.replace(/\\\//g, "/").replace(/\\u0026/g, "&").split("&");
  705. var obj = {};
  706.  
  707. forEach(elms, function(idx, elm) {
  708. var kv = elm.split("=");
  709. obj[kv[0]] = decodeURIComponent(kv[1]);
  710. });
  711.  
  712. obj.itag = +obj.itag;
  713.  
  714. if(obj.conn != null && obj.conn.match(/^rtmpe:\/\//))
  715. obj.isDrm = true;
  716.  
  717. if(obj.s != null && obj.sig == null) {
  718. var sig = deobfuscateVideoSig(map.scriptName, obj.s);
  719. if(sig != obj.s) {
  720. obj.sig = sig;
  721. delete obj.s;
  722. }
  723. }
  724.  
  725. fmtUrlList.push(obj);
  726. });
  727.  
  728. //logMsg(fmtUrlList);
  729.  
  730. map.fmtUrlList = fmtUrlList;
  731. }
  732.  
  733. function parseAdaptiveStreamMap(map, value) {
  734. var fmtUrlList = [];
  735.  
  736. forEach(value.split(","), function(idx, elm) {
  737. var elms = elm.replace(/\\\//g, "/").replace(/\\u0026/g, "&").split("&");
  738. var obj = {};
  739.  
  740. forEach(elms, function(idx, elm) {
  741. var kv = elm.split("=");
  742. obj[kv[0]] = decodeURIComponent(kv[1]);
  743. });
  744.  
  745. obj.itag = +obj.itag;
  746.  
  747. if(obj.bitrate != null)
  748. obj.bitrate = +obj.bitrate;
  749.  
  750. if(obj.clen != null)
  751. obj.clen = +obj.clen;
  752.  
  753. if(obj.fps != null)
  754. obj.fps = +obj.fps;
  755.  
  756. //logMsg(obj);
  757. //logMsg(map.videoId + ": " + obj.index + " " + obj.init + " " + obj.itag + " " + obj.size + " " + obj.bitrate + " " + obj.type);
  758.  
  759. if(obj.type.match(/^video\/mp4/))
  760. obj.effType = "video/x-m4v";
  761.  
  762. if(obj.type.match(/^audio\//))
  763. obj.size = "audio";
  764.  
  765. var stdRes = snapToStdRes(obj.size);
  766. obj.quality = mapResToQuality(stdRes);
  767.  
  768. if(!map.adaptiveAR && stdRes.match(/^(\d+)x(\d+)/))
  769. map.adaptiveAR = +RegExp.$1 / +RegExp.$2;
  770.  
  771. if(obj.s != null && obj.sig == null) {
  772. var sig = deobfuscateVideoSig(map.scriptName, obj.s);
  773. if(sig != obj.s) {
  774. obj.sig = sig;
  775. delete obj.s;
  776. }
  777. }
  778.  
  779. fmtUrlList.push(obj);
  780.  
  781. map.fmtMap[obj.itag] = { res: cnvResName(stdRes) };
  782. });
  783.  
  784. //logMsg(fmtUrlList);
  785.  
  786. map.fmtUrlList = map.fmtUrlList.concat(fmtUrlList);
  787. }
  788.  
  789. function parseFmtList(map, value) {
  790. var list = value.split(",");
  791.  
  792. forEach(list, function(idx, elm) {
  793. var elms = elm.replace(/\\\//g, "/").split("/");
  794.  
  795. var fmtId = elms[0];
  796. var res = elms[1];
  797. elms.splice(/*idx*/ 0, /*rm*/ 2);
  798.  
  799. if(map.adaptiveAR && res.match(/^(\d+)x(\d+)/))
  800. res = Math.round(+RegExp.$2 * map.adaptiveAR) + "x" + RegExp.$2;
  801.  
  802. map.fmtMap[fmtId] = { res: cnvResName(res), vars: elms };
  803. });
  804.  
  805. //logMsg(map.fmtMap);
  806. }
  807.  
  808. function getExt(elm) {
  809. return "";
  810. }
  811.  
  812. function getVideoInfo(url, callback) {
  813. function success(data) {
  814. var map = {};
  815.  
  816. if(data.match(/<div\s+id="verify-details">/)) {
  817. logMsg("Skipping " + url);
  818. return;
  819. }
  820.  
  821. if(data.match(/<h1\s+id="unavailable-message">/)) {
  822. logMsg("Not avail " + url);
  823. return;
  824. }
  825.  
  826. if(data.match(/"t":\s?"(.+?)"/))
  827. map.t = RegExp.$1;
  828.  
  829. if(data.match(/"video_id":\s?"(.+?)"/))
  830. map.videoId = RegExp.$1;
  831.  
  832. map.scriptUrl = decryptSig.extractScriptUrl(data);
  833. if(map.scriptUrl) {
  834. //logMsg(map.videoId + " script: " + map.scriptUrl);
  835. map.scriptName = decryptSig.getScriptName(map.scriptUrl);
  836. decryptSig.condFetchScript(map.scriptUrl);
  837. }
  838.  
  839. if(data.match(/<meta\s+itemprop="name"\s*content="(.+)"\s*>\s*\n/))
  840. map.title = unescHtmlEntities(RegExp.$1);
  841.  
  842. if(map.title == null && data.match(/<meta\s+name="title"\s*content="(.+)"\s*>/))
  843. map.title = unescHtmlEntities(RegExp.$1);
  844.  
  845. if(data.match(/"url_encoded_fmt_stream_map":\s?"(.+?)"/))
  846. parseStreamMap(map, RegExp.$1);
  847.  
  848. map.fmtMap = {};
  849.  
  850. if(data.match(/"adaptive_fmts":\s?"(.+?)"/))
  851. parseAdaptiveStreamMap(map, RegExp.$1);
  852.  
  853. if(data.match(/"fmt_list":\s?"(.+?)"/))
  854. parseFmtList(map, RegExp.$1);
  855.  
  856. if(data.match(/"dashmpd":\s?"(.+?)"/))
  857. map.dashmpd = decodeURIComponent(RegExp.$1.replace(/\\\//g, "/"));
  858.  
  859. if(userConfig.filteredFormats.length > 0) {
  860. for(var i = 0; i < map.fmtUrlList.length; ++i) {
  861. if(inArray(getVideoName(map.fmtUrlList[i].effType || map.fmtUrlList[i].type), userConfig.filteredFormats) >= 0) {
  862. map.fmtUrlList.splice(i, /*len*/ 1);
  863. --i;
  864. continue;
  865. }
  866. }
  867. }
  868.  
  869. var hasHighRes = false;
  870. var hasHighAudio = false;
  871. var HIGH_AUDIO_BPS = 96 * 1024;
  872.  
  873. forEach(map.fmtUrlList, function(idx, elm) {
  874. hasHighRes |= elm.quality == "hd720" || elm.quality == "hd1080";
  875.  
  876. if(elm.quality == "audio")
  877. hasHighAudio |= elm.bitrate >= HIGH_AUDIO_BPS;
  878. });
  879.  
  880. if(hasHighRes) {
  881. for(var i = 0; i < map.fmtUrlList.length; ++i) {
  882. if(map.fmtUrlList[i].quality == "small") {
  883. map.fmtUrlList.splice(i, /*len*/ 1);
  884. --i;
  885. continue;
  886. }
  887. }
  888. }
  889.  
  890. if(hasHighAudio) {
  891. for(var i = 0; i < map.fmtUrlList.length; ++i) {
  892. if(map.fmtUrlList[i].quality == "audio" && map.fmtUrlList[i].bitrate < HIGH_AUDIO_BPS) {
  893. map.fmtUrlList.splice(i, /*len*/ 1);
  894. --i;
  895. continue;
  896. }
  897. }
  898. }
  899.  
  900. map.fmtUrlList.sort(cmpUrlList);
  901.  
  902. callback(map);
  903. }
  904.  
  905. // Entry point
  906. dom.ajax({ url: url, success: success });
  907. }
  908.  
  909. function cmpUrlList(a, b) {
  910. var diff = getQualityIdx(b.quality) - getQualityIdx(a.quality);
  911. if(diff != 0)
  912. return diff;
  913.  
  914. var aRes = (a.size || "").match(/^(\d+)x(\d+)/);
  915. var bRes = (b.size || "").match(/^(\d+)x(\d+)/);
  916.  
  917. if(aRes == null) aRes = [ 0, 0, 0 ];
  918. if(bRes == null) bRes = [ 0, 0, 0 ];
  919.  
  920. return +bRes[2] - +aRes[2];
  921. }
  922.  
  923. // -----------------------------------------------------------------------------
  924.  
  925. var CSS_PREFIX = "ujs-";
  926.  
  927. var HDR_LINKS_HTML_ID = CSS_PREFIX + "hdr-links-div";
  928. var LINKS_HTML_ID = CSS_PREFIX + "links-cls";
  929. var LINKS_TP_HTML_ID = CSS_PREFIX + "links-tp-div";
  930. var UPDATE_HTML_ID = CSS_PREFIX + "update-div";
  931. var VID_FMT_BTN_ID = CSS_PREFIX + "vid-fmt-btn";
  932.  
  933. /* The !important attr is to override the page's specificity. */
  934. var CSS_STYLES =
  935. "#" + VID_FMT_BTN_ID + dom.emitCssStyles({
  936. "margin": "0 0.333em"
  937. }) + "\n" +
  938. "#" + UPDATE_HTML_ID + dom.emitCssStyles({
  939. "background-color": "#f00",
  940. "border-radius": "2px",
  941. "color": "#fff",
  942. "padding": "5px",
  943. "text-align": "center",
  944. "text-decoration": "none",
  945. "position": "fixed",
  946. "top": "0.5em",
  947. "right": "0.5em",
  948. "z-index": "1000"
  949. }) + "\n" +
  950. "#" + UPDATE_HTML_ID + ":hover" + dom.emitCssStyles({
  951. "background-color": "#0d0"
  952. }) + "\n" +
  953. "#" + HDR_LINKS_HTML_ID + dom.emitCssStyles({
  954. "background-color": "#eee",
  955. "border": "#ccc 1px solid",
  956. //"border-radius": "3px",
  957. "color": "#333",
  958. "font-size": "90%",
  959. "margin": "5px",
  960. "padding": "5px"
  961. }) + "\n" +
  962. "#" + HDR_LINKS_HTML_ID + " ." + CSS_PREFIX + "group" + dom.emitCssStyles({
  963. "background-color": "#fff",
  964. "color": "#000 !important",
  965. "border": "#ccc 1px solid",
  966. "border-radius": "3px",
  967. "display": "inline-block",
  968. "margin": "3px",
  969. }) + "\n" +
  970. "#" + HDR_LINKS_HTML_ID + " a" + dom.emitCssStyles({
  971. "display": "table-cell",
  972. "padding": "3px",
  973. "text-decoration": "none"
  974. }) + "\n" +
  975. "#" + HDR_LINKS_HTML_ID + " a:hover" + dom.emitCssStyles({
  976. "background-color": "#d1e1fa"
  977. }) + "\n" +
  978. "div." + LINKS_HTML_ID + dom.emitCssStyles({
  979. "border-radius": "3px",
  980. "font-size": "90%",
  981. "line-height": "1em",
  982. "position": "absolute",
  983. "left": "0",
  984. "top": "0",
  985. "z-index": "1000"
  986. }) + "\n" +
  987. "#" + LINKS_TP_HTML_ID + dom.emitCssStyles({
  988. "background-color": "#eee",
  989. "border": "#aaa 1px solid",
  990. "padding": "3px 0",
  991. "text-decoration": "none",
  992. "white-space": "nowrap",
  993. "z-index": "1100"
  994. }) + "\n" +
  995. "div." + LINKS_HTML_ID + " a" + dom.emitCssStyles({
  996. "display": "inline-block",
  997. "margin": "1px",
  998. "text-decoration": "none"
  999. }) + "\n" +
  1000. "div." + LINKS_HTML_ID + " ." + CSS_PREFIX + "video" + dom.emitCssStyles({
  1001. "display": "inline-block",
  1002. "text-align": "center",
  1003. "width": "3.5em"
  1004. }) + "\n" +
  1005. "div." + LINKS_HTML_ID + " ." + CSS_PREFIX + "quality" + dom.emitCssStyles({
  1006. "display": "inline-block",
  1007. "text-align": "center",
  1008. "width": "5.5em"
  1009. }) + "\n" +
  1010. "." + CSS_PREFIX + "video" + dom.emitCssStyles({
  1011. "color": "#fff !important",
  1012. "padding": "1px 3px",
  1013. "text-align": "center"
  1014. }) + "\n" +
  1015. "." + CSS_PREFIX + "quality" + dom.emitCssStyles({
  1016. "color": "#000 !important",
  1017. "display": "table-cell",
  1018. "padding": "1px 3px",
  1019. "vertical-align": "middle"
  1020. }) + "\n" +
  1021. "." + CSS_PREFIX + "filesize" + dom.emitCssStyles({
  1022. "font-size": "90%",
  1023. "margin-top": "2px",
  1024. "padding": "1px 3px",
  1025. "text-align": "center"
  1026. }) + "\n" +
  1027. "." + CSS_PREFIX + "filesize-err" + dom.emitCssStyles({
  1028. "color": "#f00",
  1029. "font-size": "90%",
  1030. "margin-top": "2px",
  1031. "padding": "1px 3px",
  1032. "text-align": "center"
  1033. }) + "\n" +
  1034. "." + CSS_PREFIX + "not-avail" + dom.emitCssStyles({
  1035. "background-color": "#700",
  1036. "color": "#fff",
  1037. "padding": "3px",
  1038. }) + "\n" +
  1039. "." + CSS_PREFIX + "3gp" + dom.emitCssStyles({
  1040. "background-color": "#bbb"
  1041. }) + "\n" +
  1042. "." + CSS_PREFIX + "flv" + dom.emitCssStyles({
  1043. "background-color": "#0dd"
  1044. }) + "\n" +
  1045. "." + CSS_PREFIX + "m4a" + dom.emitCssStyles({
  1046. "background-color": "#07e"
  1047. }) + "\n" +
  1048. "." + CSS_PREFIX + "m4v" + dom.emitCssStyles({
  1049. "background-color": "#07e"
  1050. }) + "\n" +
  1051. "." + CSS_PREFIX + "mp3" + dom.emitCssStyles({
  1052. "background-color": "#7ba"
  1053. }) + "\n" +
  1054. "." + CSS_PREFIX + "mp4" + dom.emitCssStyles({
  1055. "background-color": "#777"
  1056. }) + "\n" +
  1057. "." + CSS_PREFIX + "qt" + dom.emitCssStyles({
  1058. "background-color": "#f08"
  1059. }) + "\n" +
  1060. "." + CSS_PREFIX + "webm" + dom.emitCssStyles({
  1061. "background-color": "#e0e"
  1062. }) + "\n" +
  1063. "." + CSS_PREFIX + "wmv" + dom.emitCssStyles({
  1064. "background-color": "#c75"
  1065. }) + "\n" +
  1066. "." + CSS_PREFIX + "small" + dom.emitCssStyles({
  1067. "color": "#888 !important",
  1068. }) + "\n" +
  1069. "." + CSS_PREFIX + "medium" + dom.emitCssStyles({
  1070. "color": "#fff !important",
  1071. "background-color": "#0d0"
  1072. }) + "\n" +
  1073. "." + CSS_PREFIX + "large" + dom.emitCssStyles({
  1074. "color": "#fff !important",
  1075. "background-color": "#00d",
  1076. "background-image": "linear-gradient(to right, #00d, #00a)"
  1077. }) + "\n" +
  1078. "." + CSS_PREFIX + "hd720" + dom.emitCssStyles({
  1079. "color": "#fff !important",
  1080. "background-color": "#f90",
  1081. "background-image": "linear-gradient(to right, #f90, #d70)"
  1082. }) + "\n" +
  1083. "." + CSS_PREFIX + "hd1080" + dom.emitCssStyles({
  1084. "color": "#fff !important",
  1085. "background-color": "#f00",
  1086. "background-image": "linear-gradient(to right, #f00, #c00)"
  1087. }) + "\n" +
  1088. "." + CSS_PREFIX + "highres" + dom.emitCssStyles({
  1089. "color": "#fff !important",
  1090. "background-color": "#c0f",
  1091. "background-image": "linear-gradient(to right, #c0f, #90f)"
  1092. }) + "\n" +
  1093. "." + CSS_PREFIX + "pos-rel" + dom.emitCssStyles({
  1094. "position": "relative"
  1095. }) + "\n" +
  1096. "";
  1097.  
  1098. function condInsertHdr(divId) {
  1099. if(dom.gE(HDR_LINKS_HTML_ID))
  1100. return true;
  1101.  
  1102. var insertPtNode = dom.gE(divId);
  1103. if(!insertPtNode)
  1104. return false;
  1105.  
  1106. var divNode = dom.cE("div");
  1107. divNode.id = HDR_LINKS_HTML_ID;
  1108.  
  1109. insertPtNode.parentNode.insertBefore(divNode, insertPtNode);
  1110. return true;
  1111. }
  1112.  
  1113. function condInsertTooltip() {
  1114. if(dom.gE(LINKS_TP_HTML_ID))
  1115. return true;
  1116.  
  1117. var toolTipNode = dom.cE("div");
  1118. toolTipNode.id = LINKS_TP_HTML_ID;
  1119.  
  1120. dom.attr(toolTipNode, "class", LINKS_HTML_ID);
  1121. dom.attr(toolTipNode, "style", "display: none;");
  1122.  
  1123. dom.append(doc.body, toolTipNode);
  1124.  
  1125. dom.addEvent(toolTipNode, "mouseleave", function(event) {
  1126. //logMsg("mouse leave");
  1127. dom.attr(toolTipNode, "style", "display: none;");
  1128. });
  1129. }
  1130.  
  1131. function condInsertUpdateIcon() {
  1132. if(dom.gE(UPDATE_HTML_ID))
  1133. return;
  1134.  
  1135. var divNode = dom.cE("a");
  1136. divNode.id = UPDATE_HTML_ID;
  1137. dom.append(doc.body, divNode);
  1138. }
  1139.  
  1140. // -----------------------------------------------------------------------------
  1141.  
  1142. var STORE_ID = "ujsYtLinks";
  1143. var JSONP_ID = "ujsYtLinks";
  1144.  
  1145. var userConfig = {
  1146. filteredFormats: [],
  1147. showVideoFormats: true,
  1148. showVideoSize: true,
  1149. tagLinks: true
  1150. };
  1151.  
  1152. var videoInfoCache = {};
  1153.  
  1154. var TAG_LINK_NUM_PER_BATCH = 5;
  1155. var INI_TAG_LINK_DELAY_MS = 200;
  1156. var SUB_TAG_LINK_DELAY_MS = 500;
  1157.  
  1158. // -----------------------------------------------------------------------------
  1159.  
  1160. function Links() {
  1161. }
  1162.  
  1163. Links.prototype.init = function() {
  1164. };
  1165.  
  1166. Links.prototype.getPreferredFmt = function(map) {
  1167. var selElm = map.fmtUrlList[0];
  1168.  
  1169. forEach(map.fmtUrlList, function(idx, elm) {
  1170. if(getVideoName(elm.type).toLowerCase() != "webm") {
  1171. selElm = elm;
  1172. return false;
  1173. }
  1174. });
  1175.  
  1176. return selElm;
  1177. };
  1178.  
  1179. Links.prototype.parseDashManifest = function(map) {
  1180. function parse(xml) {
  1181. //logMsg(xml);
  1182.  
  1183. var dashList = [];
  1184.  
  1185. var adaptationSetDom = xml.getElementsByTagName("AdaptationSet");
  1186. //logMsg(adaptationSetDom);
  1187.  
  1188. forEach(adaptationSetDom, function(i, adaptationElm) {
  1189. var mimeType = adaptationElm.getAttribute("mimeType");
  1190. //logMsg(i + " " + mimeType);
  1191.  
  1192. var representationDom = adaptationElm.getElementsByTagName("Representation");
  1193. forEach(representationDom, function(j, repElm) {
  1194. var dashElm = { mimeType: mimeType };
  1195.  
  1196. forEach([ "codecs" ], function(idx, elm) {
  1197. var v = repElm.getAttribute(elm);
  1198. if(v != null)
  1199. dashElm[elm] = v;
  1200. });
  1201.  
  1202. forEach([ "audioSamplingRate", "bandwidth", "frameRate", "height", "id", "width" ], function(idx, elm) {
  1203. var v = repElm.getAttribute(elm);
  1204. if(v != null)
  1205. dashElm[elm] = +v;
  1206. });
  1207.  
  1208. var baseUrlDom = repElm.getElementsByTagName("BaseURL");
  1209. dashElm.len = +baseUrlDom[0].getAttribute("yt:contentLength");
  1210. dashElm.url = baseUrlDom[0].textContent;
  1211.  
  1212. dashList.push(dashElm);
  1213. });
  1214. });
  1215.  
  1216. //logMsg(map);
  1217. //logMsg(dashList);
  1218.  
  1219. var maxBitRateMap = {};
  1220.  
  1221. forEach(dashList, function(idx, dashElm) {
  1222. if(dashElm.mimeType != "video/mp4" && dashElm.mimeType != "video/webm")
  1223. return;
  1224.  
  1225. var id = [ dashElm.mimeType, dashElm.width, dashElm.height, dashElm.frameRate ].join("|");
  1226.  
  1227. if(maxBitRateMap[id] == null || maxBitRateMap[id] < dashElm.bandwidth)
  1228. maxBitRateMap[id] = dashElm.bandwidth;
  1229. });
  1230.  
  1231. forEach(dashList, function(idx, dashElm) {
  1232. var foundIdx;
  1233.  
  1234. forEach(map.fmtUrlList, function(idx, mapElm) {
  1235. if(dashElm.id == mapElm.itag) {
  1236. foundIdx = idx;
  1237. return false;
  1238. }
  1239. });
  1240.  
  1241. if(foundIdx != null)
  1242. return;
  1243.  
  1244. //logMsg(dashElm);
  1245.  
  1246. if((dashElm.mimeType == "video/mp4" || dashElm.mimeType == "video/webm") && (dashElm.width >= 1000 || dashElm.height >= 1000)) {
  1247. var id = [ dashElm.mimeType, dashElm.width, dashElm.height, dashElm.frameRate ].join("|");
  1248.  
  1249. if(maxBitRateMap[id] == null || dashElm.bandwidth < maxBitRateMap[id])
  1250. return;
  1251.  
  1252. var size = dashElm.width + "x" + dashElm.height;
  1253. var stdRes = snapToStdRes(size);
  1254.  
  1255. if(map.fmtMap[dashElm.id] == null) {
  1256. map.fmtMap[dashElm.id] = { res: cnvResName(stdRes) };
  1257. }
  1258.  
  1259. map.fmtUrlList.push({
  1260. bitrate: dashElm.bandwidth,
  1261. effType: dashElm.mimeType == "video/mp4" ? "video/x-m4v" : null,
  1262. filesize: dashElm.len,
  1263. fps: dashElm.frameRate,
  1264. itag: dashElm.id,
  1265. quality: mapResToQuality(stdRes),
  1266. size: size,
  1267. type: dashElm.mimeType + ";+codecs=\"" + dashElm.codecs + "\"",
  1268. url: dashElm.url
  1269. });
  1270. }
  1271. else if(dashElm.mimeType == "audio/mp4" && dashElm.audioSamplingRate >= 44100) {
  1272. if(map.fmtMap[dashElm.id] == null) {
  1273. map.fmtMap[dashElm.id] = { res: "Audio" };
  1274. }
  1275.  
  1276. map.fmtUrlList.push({
  1277. bitrate: dashElm.bandwidth,
  1278. filesize: dashElm.len,
  1279. itag: dashElm.id,
  1280. quality: "audio",
  1281. type: dashElm.mimeType + ";+codecs=\"" + dashElm.codecs + "\"",
  1282. url: dashElm.url
  1283. });
  1284. }
  1285. });
  1286.  
  1287. if(condInsertHdr("page"))
  1288. dom.html(dom.gE(HDR_LINKS_HTML_ID), me.emitLinks(map));
  1289. }
  1290.  
  1291. // Entry point
  1292. var me = this;
  1293.  
  1294. if(!map.dashmpd)
  1295. return;
  1296.  
  1297. //logMsg(map.dashmpd);
  1298.  
  1299. if(map.dashmpd.match(/\/s\/([a-zA-Z0-9.]+)\//)) {
  1300. var sig = deobfuscateVideoSig(map.scriptName, RegExp.$1);
  1301. map.dashmpd = map.dashmpd.replace(/\/s\/[a-zA-Z0-9.]+\//, "/signature/" + sig + "/");
  1302. }
  1303.  
  1304. dom.crossAjax({
  1305. url: map.dashmpd,
  1306. dataType: "xml",
  1307.  
  1308. success: function(data, status, xhr) {
  1309. parse(data);
  1310. },
  1311.  
  1312. error: function(xhr, status) {
  1313. },
  1314.  
  1315. complete: function(xhr) {
  1316. }
  1317. });
  1318. };
  1319.  
  1320. Links.prototype.checkFmts = function(forceFlag) {
  1321. var me = this;
  1322.  
  1323. if(!userConfig.showVideoFormats)
  1324. return;
  1325.  
  1326. if(!forceFlag && userConfig.showVideoFormats == "btn") {
  1327. if(dom.gE(VID_FMT_BTN_ID))
  1328. return;
  1329.  
  1330. var btn = dom.cE("button");
  1331. dom.attr(btn, "id", VID_FMT_BTN_ID);
  1332. dom.attr(btn, "class", "yt-uix-button yt-uix-button-default");
  1333. btn.innerHTML = "VidFmts";
  1334.  
  1335. var mastH = dom.gE("yt-masthead-signin") || dom.gE("yt-masthead-user");
  1336. if(!mastH)
  1337. return;
  1338.  
  1339. dom.prepend(mastH, btn);
  1340.  
  1341. dom.addEvent(btn, "click", function(event) {
  1342. me.checkFmts(/*force*/ true);
  1343. });
  1344.  
  1345. return;
  1346. }
  1347.  
  1348. if(!loc.href.match(/watch\?v=([a-zA-Z0-9_-]*)/))
  1349. return false;
  1350.  
  1351. var videoId = RegExp.$1;
  1352.  
  1353. var url = loc.protocol + "//" + loc.host + "/watch?v=" + videoId;
  1354.  
  1355. getVideoInfo(url, function(map) {
  1356. me.parseDashManifest(map);
  1357. me.showLinks("page", map);
  1358. });
  1359. };
  1360.  
  1361. Links.prototype.genUrl = function(map, elm) {
  1362. var url = elm.url + "&title=" + encodeSafeFname(map.title + getExt(elm));
  1363.  
  1364. if(elm.sig != null)
  1365. url += "&signature=" + elm.sig;
  1366.  
  1367. return url;
  1368. };
  1369.  
  1370. Links.prototype.emitLinks = function(map) {
  1371. function fmtSize(size, units) {
  1372. if(!units)
  1373. units = [ "kB", "MB", "GB" ];
  1374.  
  1375. for(var idx = 0; idx < units.length; ++idx) {
  1376. size /= 1000;
  1377.  
  1378. if(size < 10)
  1379. return Math.round(size * 100) / 100 + units[idx];
  1380.  
  1381. if(size < 100)
  1382. return Math.round(size * 10) / 10 + units[idx];
  1383.  
  1384. if(size < 1000 || idx == units.length - 1)
  1385. return Math.round(size) + units[idx];
  1386. }
  1387. }
  1388.  
  1389. function fmtBitrate(size) {
  1390. return fmtSize(size, [ "kbps", "Mbps", "Gbps" ]);
  1391. }
  1392.  
  1393. // Entry point
  1394. var me = this;
  1395. var s = [];
  1396.  
  1397. var resMap = {};
  1398.  
  1399. map.fmtUrlList.sort(cmpUrlList);
  1400.  
  1401. forEach(map.fmtUrlList, function(idx, elm) {
  1402. var fmtMap = map.fmtMap[elm.itag];
  1403.  
  1404. if(!resMap[fmtMap.res]) {
  1405. resMap[fmtMap.res] = [];
  1406. resMap[fmtMap.res].quality = elm.quality;
  1407. }
  1408.  
  1409. resMap[fmtMap.res].push(elm);
  1410. });
  1411.  
  1412. for(var res in resMap) {
  1413. var qFields = [];
  1414.  
  1415. qFields.push(dom.emitHtml("div", { "class": CSS_PREFIX + "quality " + CSS_PREFIX + resMap[res].quality }, res));
  1416.  
  1417. forEach(resMap[res], function(idx, elm) {
  1418. var fields = [];
  1419. var fmtMap = map.fmtMap[elm.itag];
  1420. var videoName = getVideoName(elm.effType || elm.type);
  1421.  
  1422. var addMsg = [ elm.itag, elm.type, elm.size || elm.quality ];
  1423.  
  1424. if(elm.fps != null)
  1425. addMsg.push(elm.fps + "fps");
  1426.  
  1427. var varMsg = "";
  1428.  
  1429. if(elm.bitrate != null)
  1430. varMsg = fmtBitrate(elm.bitrate);
  1431. else if(fmtMap.vars != null)
  1432. varMsg = fmtMap.vars.join();
  1433.  
  1434. addMsg.push(varMsg);
  1435.  
  1436. if(elm.s != null)
  1437. addMsg.push("sig-" + elm.s.length);
  1438.  
  1439. if(elm.filesize != null && elm.filesize >= 0)
  1440. addMsg.push(fmtSize(elm.filesize));
  1441.  
  1442. var vidSuffix = "";
  1443.  
  1444. if(inArray(elm.itag, [ 82, 83, 84, 100, 101, 102 ]) >= 0)
  1445. vidSuffix = " (3D)";
  1446. else if(elm.fps != null && elm.fps >= 45)
  1447. vidSuffix = " (HFR)";
  1448.  
  1449. fields.push(dom.emitHtml("div", { "class": CSS_PREFIX + "video " + CSS_PREFIX + videoName.toLowerCase() }, videoName + vidSuffix));
  1450.  
  1451. if(elm.filesize != null) {
  1452. if(elm.filesize >= 0) {
  1453. fields.push(dom.emitHtml("div", { "class": CSS_PREFIX + "filesize" }, fmtSize(elm.filesize)));
  1454. }
  1455. else {
  1456. var msg;
  1457.  
  1458. if(elm.isDrm)
  1459. msg = "DRM";
  1460. else if(elm.s != null)
  1461. msg = "sig-" + elm.s.length;
  1462. else
  1463. msg = "Err";
  1464.  
  1465. fields.push(dom.emitHtml("div", { "class": CSS_PREFIX + "filesize-err" }, msg));
  1466. }
  1467. }
  1468.  
  1469. var url;
  1470.  
  1471. if(elm.isDrm)
  1472. url = elm.conn + "?" + elm.stream;
  1473. else
  1474. url = me.genUrl(map, elm);
  1475.  
  1476. var ahref = dom.emitHtml("a", {
  1477. download: cnvSafeFname(map.title + getExt(elm)),
  1478. href: url,
  1479. title: addMsg.join(" | ")
  1480. }, fields.join(""));
  1481.  
  1482. qFields.push(ahref);
  1483. });
  1484.  
  1485. s.push(dom.emitHtml("div", { "class": CSS_PREFIX + "group" }, qFields.join("")));
  1486. }
  1487.  
  1488. return s.join("");
  1489. };
  1490.  
  1491. var INI_SHOW_FILESIZE_DELAY_MS = 500;
  1492. var SUB_SHOW_FILESIZE_DELAY_MS = 200;
  1493.  
  1494. Links.prototype.showLinks = function(divId, map) {
  1495. function updateLinks() {
  1496. //!! Hack to update file size
  1497. if(condInsertHdr(divId))
  1498. dom.html(dom.gE(HDR_LINKS_HTML_ID), me.emitLinks(map));
  1499. }
  1500.  
  1501. // Entry point
  1502. var me = this;
  1503.  
  1504. // video is not avail
  1505. if(!map.fmtUrlList)
  1506. return;
  1507.  
  1508. //logMsg(JSON.stringify(map));
  1509.  
  1510. if(!condInsertHdr(divId))
  1511. return;
  1512.  
  1513. dom.html(dom.gE(HDR_LINKS_HTML_ID), me.emitLinks(map));
  1514.  
  1515. if(!userConfig.showVideoSize)
  1516. return;
  1517.  
  1518. forEach(map.fmtUrlList, function(idx, elm) {
  1519. //logMsg(elm.itag + " " + elm.url);
  1520.  
  1521. // We just fail outright for protected/obfuscated videos
  1522. if(elm.isDrm || elm.s != null) {
  1523. elm.filesize = -1;
  1524. updateLinks();
  1525. return;
  1526. }
  1527.  
  1528. if(elm.clen != null) {
  1529. elm.filesize = elm.clen;
  1530. updateLinks();
  1531. return;
  1532. }
  1533.  
  1534. setTimeout(function() {
  1535. dom.crossAjax({
  1536. type: "HEAD",
  1537. url: me.genUrl(map, elm),
  1538.  
  1539. success: function(data, status, xhr) {
  1540. var filesize = xhr.getResponseHeader("Content-Length");
  1541. if(filesize == null)
  1542. return;
  1543.  
  1544. //logMsg(map.title + " " + elm.itag + ": " + filesize);
  1545. elm.filesize = +filesize;
  1546.  
  1547. updateLinks();
  1548. },
  1549.  
  1550. error: function(xhr, status) {
  1551. //logMsg(map.fmtMap[elm.itag].res + " " + getVideoName(elm.type) + ": " + xhr.status);
  1552.  
  1553. if(xhr.status != 403)
  1554. return;
  1555.  
  1556. elm.filesize = -1;
  1557.  
  1558. updateLinks();
  1559. },
  1560.  
  1561. complete: function(xhr) {
  1562. //logMsg(map.title + ": " + xhr.getAllResponseHeaders());
  1563. }
  1564. });
  1565. }, INI_SHOW_FILESIZE_DELAY_MS + idx * SUB_SHOW_FILESIZE_DELAY_MS);
  1566. });
  1567. };
  1568.  
  1569. Links.prototype.tagLinks = function() {
  1570. var SCANNED = 1;
  1571. var REQ_INFO = 2;
  1572. var ADDED_INFO = 3;
  1573.  
  1574. function prepareTagHtml(node, map) {
  1575. var elm = me.getPreferredFmt(map);
  1576. var fmtMap = map.fmtMap[elm.itag];
  1577.  
  1578. dom.attr(node, "class", LINKS_HTML_ID + " " + CSS_PREFIX + "quality " + CSS_PREFIX + elm.quality);
  1579.  
  1580. dom.addEvent(node, "mouseenter", function(event) {
  1581. //logMsg("mouse enter " + map.videoId);
  1582. var pos = dom.offset(node);
  1583. //logMsg("mouse enter: x " + pos.left + ", y " + pos.top);
  1584.  
  1585. var toolTipNode = dom.gE(LINKS_TP_HTML_ID);
  1586.  
  1587. dom.attr(toolTipNode, "style", "position: absolute; left: " + pos.left + "px; top: " + pos.top + "px");
  1588.  
  1589. dom.html(toolTipNode, me.emitLinks(map));
  1590. });
  1591.  
  1592. node.href = elm.url + "&title=" + encodeSafeFname(map.title + getExt(elm));
  1593.  
  1594. return fmtMap.res;
  1595. }
  1596.  
  1597. function addTag(hNode, map) {
  1598. //logMsg(dom.html(hNode));
  1599. //logMsg("hNode " + dom.attr(hNode, "class"));
  1600. //var img = dom.gT(hNode, "img") [0];
  1601. //logMsg(dom.attr(img, "src"));
  1602. //logMsg(dom.attr(img, "class"));
  1603.  
  1604. dom.attr(hNode, CSS_PREFIX + "processed", ADDED_INFO);
  1605.  
  1606. var node = dom.cE("div");
  1607.  
  1608. if(map.fmtUrlList) {
  1609. tagHtml = prepareTagHtml(node, map);
  1610. }
  1611. else {
  1612. dom.attr(node, "class", LINKS_HTML_ID + " " + CSS_PREFIX + "not-avail");
  1613. tagHtml = "NA";
  1614. }
  1615.  
  1616. var parentNode;
  1617. var insNode;
  1618.  
  1619. var cls = dom.attr(hNode, "class") || "";
  1620. var isVideoWallStill = cls.match(/videowall-still/);
  1621. if(isVideoWallStill) {
  1622. parentNode = hNode;
  1623. insNode = hNode.firstChild;
  1624. }
  1625. else {
  1626. parentNode = hNode.parentNode;
  1627. insNode = hNode;
  1628. }
  1629.  
  1630. var parentCssPositionStyle = window.getComputedStyle(parentNode, null).getPropertyValue("position");
  1631.  
  1632. if(parentCssPositionStyle != "absolute" && parentCssPositionStyle != "relative")
  1633. dom.attr(parentNode, "class", dom.attr(parentNode, "class") + " " + CSS_PREFIX + "pos-rel");
  1634.  
  1635. parentNode.insertBefore(node, insNode);
  1636.  
  1637. dom.html(node, tagHtml);
  1638. }
  1639.  
  1640. function getFmt(videoId, hNode) {
  1641. if(videoInfoCache[videoId]) {
  1642. addTag(hNode, videoInfoCache[videoId]);
  1643. return;
  1644. }
  1645.  
  1646. var url;
  1647.  
  1648. if(videoId.match(/.+==$/))
  1649. url = loc.protocol + "//" + loc.host + "/cthru?key=" + videoId;
  1650. else
  1651. url = loc.protocol + "//" + loc.host + "/watch?v=" + videoId;
  1652.  
  1653. getVideoInfo(url, function(map) {
  1654. videoInfoCache[videoId] = map;
  1655. addTag(hNode, map);
  1656. });
  1657. }
  1658.  
  1659. // Entry point
  1660. var me = this;
  1661.  
  1662. var list = [];
  1663.  
  1664. forEach(dom.gT("a"), function(idx, hNode) {
  1665. if(dom.attr(hNode, CSS_PREFIX + "processed"))
  1666. return;
  1667.  
  1668. if(!dom.inViewport(hNode))
  1669. return;
  1670.  
  1671. dom.attr(hNode, CSS_PREFIX + "processed", SCANNED);
  1672.  
  1673. if(!hNode.href.match(/watch\?v=([a-zA-Z0-9_-]*)/) &&
  1674. !hNode.href.match(/watch_videos.+?&video_ids=([a-zA-Z0-9_-]*)/))
  1675. return;
  1676.  
  1677. var videoId = RegExp.$1;
  1678.  
  1679. var cls = dom.attr(hNode, "class") || "";
  1680. if(!cls.match(/videowall-still/)) {
  1681. if(cls == "yt-button" || cls.match(/yt-uix-button/))
  1682. return;
  1683.  
  1684. if(dom.attr(hNode.parentNode, "class") == "video-time")
  1685. return;
  1686.  
  1687. if(dom.html(hNode).match(/video-logo/i))
  1688. return;
  1689.  
  1690. var img = dom.gT(hNode, "img");
  1691. if(img == null || img.length == 0)
  1692. return;
  1693.  
  1694. img = img[0];
  1695.  
  1696. var imgSrc = dom.attr(img, "src") || "";
  1697. if(imgSrc.indexOf("ytimg.com") < 0)
  1698. return;
  1699.  
  1700. var tnSrc = dom.attr(img, "thumb") || "";
  1701.  
  1702. if(imgSrc.match(/.+?\/([a-zA-Z0-9_-]*)\/default\.jpg$/))
  1703. videoId = RegExp.$1;
  1704. else if(tnSrc.match(/.+?\/([a-zA-Z0-9_-]*)\/default\.jpg$/))
  1705. videoId = RegExp.$1;
  1706. }
  1707.  
  1708. //logMsg(idx + " " + hNode.href);
  1709. //logMsg("videoId: " + videoId);
  1710.  
  1711. list.push({ videoId: videoId, hNode: hNode });
  1712.  
  1713. dom.attr(hNode, CSS_PREFIX + "processed", REQ_INFO);
  1714. });
  1715.  
  1716. forLoop({ num: list.length, inc: TAG_LINK_NUM_PER_BATCH, batchIdx: 0 }, function(idx) {
  1717. var batchIdx = this.batchIdx++;
  1718. var batchList = list.slice(idx, idx + TAG_LINK_NUM_PER_BATCH);
  1719.  
  1720. setTimeout(function() {
  1721. forEach(batchList, function(idx, elm) {
  1722. //logMsg(batchIdx + " " + idx + " " + elm.hNode.href);
  1723. getFmt(elm.videoId, elm.hNode);
  1724. });
  1725. }, INI_TAG_LINK_DELAY_MS + batchIdx * SUB_TAG_LINK_DELAY_MS);
  1726. });
  1727. };
  1728.  
  1729. Links.prototype.periodicTagLinks = function(delayMs) {
  1730. function poll() {
  1731. me.tagLinks();
  1732. me.tagLinksTimerId = setTimeout(poll, 3000);
  1733. }
  1734.  
  1735. // Entry point
  1736. if(!userConfig.tagLinks)
  1737. return;
  1738.  
  1739. var me = this;
  1740.  
  1741. delayMs = delayMs || 0;
  1742.  
  1743. if(me.tagLinksTimerId != null) {
  1744. clearTimeout(me.tagLinksTimerId);
  1745. delete me.tagLinksTimerId;
  1746. }
  1747.  
  1748. setTimeout(poll, delayMs);
  1749. };
  1750.  
  1751. // -----------------------------------------------------------------------------
  1752.  
  1753. Links.prototype.loadSettings = function() {
  1754. var obj = localStorage[STORE_ID];
  1755. if(obj == null)
  1756. return;
  1757.  
  1758. obj = JSON.parse(obj);
  1759.  
  1760. this.lastChkReqTs = +obj.lastChkReqTs;
  1761. this.lastChkTs = +obj.lastChkTs;
  1762. this.lastChkVer = +obj.lastChkVer;
  1763. };
  1764.  
  1765. Links.prototype.storeSettings = function() {
  1766. localStorage[STORE_ID] = JSON.stringify({
  1767. lastChkReqTs: this.lastChkReqTs,
  1768. lastChkTs: this.lastChkTs,
  1769. lastChkVer: this.lastChkVer
  1770. });
  1771. };
  1772.  
  1773. // -----------------------------------------------------------------------------
  1774.  
  1775. var UPDATE_CHK_INTERVAL = 5 * 86400;
  1776. var FAIL_TO_CHK_UPDATE_INTERVAL = 14 * 86400;
  1777.  
  1778. Links.prototype.chkVer = function(forceFlag) {
  1779. if(this.lastChkVer > relInfo.ver) {
  1780. this.showNewVer({ ver: this.lastChkVer });
  1781. return;
  1782. }
  1783.  
  1784. var now = timeNowInSec();
  1785.  
  1786. //logMsg("lastChkReqTs " + this.lastChkReqTs + ", diff " + (now - this.lastChkReqTs));
  1787. //logMsg("lastChkTs " + this.lastChkTs);
  1788. //logMsg("lastChkVer " + this.lastChkVer);
  1789.  
  1790. if(this.lastChkReqTs == null || now < this.lastChkReqTs) {
  1791. this.lastChkReqTs = now;
  1792. this.storeSettings();
  1793. return;
  1794. }
  1795.  
  1796. if(now - this.lastChkReqTs < UPDATE_CHK_INTERVAL)
  1797. return;
  1798.  
  1799. if(this.lastChkReqTs - this.lastChkTs > FAIL_TO_CHK_UPDATE_INTERVAL)
  1800. logMsg("Failed to check ver for " + ((this.lastChkReqTs - this.lastChkTs) / 86400) + " days");
  1801.  
  1802. this.lastChkReqTs = now;
  1803. this.storeSettings();
  1804.  
  1805. unsafeWin[JSONP_ID] = this;
  1806.  
  1807. var script = dom.cE("script");
  1808. script.type = "text/javascript";
  1809. script.src = SCRIPT_UPDATE_LINK;
  1810. dom.append(doc.body, script);
  1811. };
  1812.  
  1813. Links.prototype.chkVerCallback = function(data) {
  1814. delete unsafeWin[JSONP_ID];
  1815.  
  1816. this.lastChkTs = timeNowInSec();
  1817. this.storeSettings();
  1818.  
  1819. //logMsg(JSON.stringify(data));
  1820.  
  1821. var latestElm = data[0];
  1822.  
  1823. if(latestElm.ver <= relInfo.ver)
  1824. return;
  1825.  
  1826. this.showNewVer(latestElm);
  1827. };
  1828.  
  1829. Links.prototype.showNewVer = function(latestElm) {
  1830. function getVerStr(ver) {
  1831. var verStr = "" + ver;
  1832.  
  1833. var majorV = verStr.substr(0, verStr.length - 4) || "0";
  1834. var minorV = verStr.substr(verStr.length - 4, 2);
  1835. return majorV + "." + minorV;
  1836. }
  1837.  
  1838. // Entry point
  1839. this.lastChkVer = latestElm.ver;
  1840. this.storeSettings();
  1841.  
  1842. condInsertUpdateIcon();
  1843.  
  1844. var aNode = dom.gE(UPDATE_HTML_ID);
  1845.  
  1846. aNode.href = SCRIPT_LINK;
  1847.  
  1848. if(latestElm.desc != null)
  1849. dom.attr(aNode, "title", latestElm.desc);
  1850.  
  1851. dom.html(aNode, dom.emitHtml("b", SCRIPT_NAME + " " + getVerStr(relInfo.ver)) +
  1852. "<br>Click to update to " + getVerStr(latestElm.ver));
  1853. };
  1854.  
  1855. // -----------------------------------------------------------------------------
  1856.  
  1857. var inst = new Links();
  1858.  
  1859. inst.init();
  1860. inst.loadSettings();
  1861. decryptSig.load();
  1862.  
  1863. dom.insertCss(CSS_STYLES);
  1864.  
  1865. condInsertTooltip();
  1866.  
  1867. if(loc.pathname.match(/\/watch/)) {
  1868. inst.checkFmts();
  1869. }
  1870.  
  1871. inst.periodicTagLinks();
  1872.  
  1873. var scrollTop = win.pageYOffset || doc.documentElement.scrollTop;
  1874.  
  1875. dom.addEvent(win, "scroll", function(e) {
  1876. var newScrollTop = win.pageYOffset || doc.documentElement.scrollTop;
  1877.  
  1878. if(Math.abs(newScrollTop - scrollTop) < 100)
  1879. return;
  1880.  
  1881. //logMsg("scroll by " + (newScrollTop - scrollTop));
  1882.  
  1883. scrollTop = newScrollTop;
  1884.  
  1885. inst.periodicTagLinks(200);
  1886. });
  1887.  
  1888. inst.chkVer();
  1889.  
  1890. // -----------------------------------------------------------------------------
  1891.  
  1892. /* YouTube reuses the current page when the user clicks on a new video. We need
  1893. to detect it and reload the formats. */
  1894.  
  1895. (function() {
  1896.  
  1897. var PERIODIC_CHK_VIDEO_URL_MS = 1000;
  1898.  
  1899. var curVideoUrl = loc.toString();
  1900.  
  1901. function periodicChkVideoUrl() {
  1902. var newVideoUrl = loc.toString();
  1903.  
  1904. if(curVideoUrl != newVideoUrl) {
  1905. //logMsg(curVideoUrl + " -> " + newVideoUrl);
  1906.  
  1907. curVideoUrl = newVideoUrl;
  1908.  
  1909. if(loc.pathname.match(/\/watch/))
  1910. inst.checkFmts();
  1911. }
  1912.  
  1913. setTimeout(periodicChkVideoUrl, PERIODIC_CHK_VIDEO_URL_MS);
  1914. }
  1915.  
  1916. periodicChkVideoUrl();
  1917.  
  1918. }) ();
  1919.  
  1920. // -----------------------------------------------------------------------------
  1921.  
  1922. }) ();
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement