Video Downloader for Tampermonkey

Will add a download button to various websites such as Reddit, Facebook, Youtube and Twitter

  1. // ==UserScript==
  2. // @name Video Downloader for Tampermonkey
  3. // @version 0.5
  4. // @description Will add a download button to various websites such as Reddit, Facebook, Youtube and Twitter
  5. // @author Mordo95
  6. // @namespace com.mordo95.Downloader
  7. // @license MIT
  8. // @match *://*/*
  9. // @supportURL https://github.com
  10. // @run-at document-start
  11. // @grant GM_addStyle
  12. // @grant GM_xmlhttpRequest
  13. // ==/UserScript==
  14.  
  15. var __defProp = Object.defineProperty;
  16. var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
  17. var __publicField = (obj, key, value) => {
  18. __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
  19. return value;
  20. };
  21. (function() {
  22. var _a, _b, _c, _d;
  23. "use strict";
  24. GM_addStyle(`
  25. div.dlBtn {
  26. position: absolute;
  27. top: 0;
  28. right: 0;
  29. z-index: 99999999;
  30. padding: 10px 15px;
  31. margin: 5px;
  32. cursor: pointer;
  33. outline: 0;
  34. background: #5383FB;
  35. color: white;
  36. border: 1px solid 1px solid #5383FB;
  37. font-family: Segoe UI Historic, Segoe UI, Helvetica, Arial, sans-serif !important;
  38. font-size: 12px;
  39. }
  40. div.dlBtn:hover {
  41. background-color: #86A4FC;
  42. }div.dlBtn {
  43. position: absolute;
  44. top: 0;
  45. right: 0;
  46. z-index: 99999;
  47. padding: 10px 15px;
  48. margin: 5px;
  49. cursor: pointer;
  50. outline: 0;
  51. background: var(--primary-button-background);
  52. color: var(--primary-button-text);
  53. border: 1px solid 1px solid var(--accent);
  54. font-family: var(--font-family-segoe) !important;
  55. }
  56. div.dlBtn:hover {
  57. background-color: var(--primary-button-pressed);
  58. }
  59. div.dlBtn.shorts {
  60. right: 110px;
  61. top: 5px;
  62. }div.dlBtn {
  63. position: absolute;
  64. top: 0;
  65. right: 0;
  66. z-index: 99999999;
  67. padding: 10px 15px;
  68. margin: 5px;
  69. cursor: pointer;
  70. outline: 0;
  71. background: #5383FB;
  72. color: white;
  73. border: 1px solid 1px solid #5383FB;
  74. font-family: Segoe UI Historic, Segoe UI, Helvetica, Arial, sans-serif !important;
  75. font-size: 12px;
  76. }
  77. div.dlBtn:hover {
  78. background-color: #86A4FC;
  79. } `);
  80. class Injector {
  81. constructor() {
  82. __publicField(this, "downloaders", []);
  83. }
  84. register(downloader) {
  85. if (Array.isArray(downloader)) {
  86. this.downloaders = this.downloaders.concat(downloader);
  87. } else
  88. this.downloaders.push(downloader);
  89. }
  90. inject(location) {
  91. for (const downloader of this.downloaders) {
  92. if (location.match(downloader.siteRegex))
  93. new downloader().inject();
  94. }
  95. }
  96. }
  97. const Injector$1 = new Injector();
  98. function staticImplements() {
  99. return (constructor) => {
  100. };
  101. }
  102. var __defProp$3 = Object.defineProperty;
  103. var __getOwnPropDesc$3 = Object.getOwnPropertyDescriptor;
  104. var __decorateClass$3 = (decorators, target, key, kind) => {
  105. var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc$3(target, key) : target;
  106. for (var i = decorators.length - 1, decorator; i >= 0; i--)
  107. if (decorator = decorators[i])
  108. result = (kind ? decorator(target, key, result) : decorator(result)) || result;
  109. if (kind && result)
  110. __defProp$3(target, key, result);
  111. return result;
  112. };
  113. let YoutubeDownloader = (_a = class {
  114. constructor() {
  115. __publicField(this, "btnText", "Download (HD)");
  116. }
  117. addVideoButton(on) {
  118. let btn = document.createElement("div");
  119. btn.innerHTML = this.btnText;
  120. btn.classList.add("dlBtn");
  121. btn.onclick = () => this.getLinks(btn);
  122. on.prepend(btn);
  123. }
  124. getLinks(btn) {
  125. let fd = new FormData();
  126. fd.set("q", window.location.href);
  127. fd.set("vt", "mp4");
  128. let url = "https://yt1s.com/api/ajaxSearch/index";
  129. GM_xmlhttpRequest({
  130. method: "POST",
  131. url,
  132. data: fd,
  133. onload: (resp) => {
  134. let js = JSON.parse(resp.responseText);
  135. this.convert(btn, js.vid, js.links.mp4.auto.k);
  136. }
  137. });
  138. }
  139. convert(btn, vid, k) {
  140. let fd = new FormData();
  141. fd.set("vid", vid);
  142. fd.set("k", k);
  143. btn.innerHTML = "Converting ...";
  144. GM_xmlhttpRequest({
  145. method: "POST",
  146. url: "https://yt1s.com/api/ajaxConvert/convert",
  147. data: fd,
  148. timeout: 6e4,
  149. onload: (resp) => {
  150. let js = JSON.parse(resp.responseText);
  151. let status = js.c_status;
  152. if (status === "CONVERTED") {
  153. window.open(js.dlink);
  154. } else {
  155. alert("Error converting video. Please try again later!");
  156. }
  157. btn.innerHTML = this.btnText;
  158. },
  159. onTimeout: () => {
  160. btn.innerHTML = this.btnText;
  161. }
  162. });
  163. }
  164. inject() {
  165. Promise.resolve().then(() => style$1);
  166. setInterval(() => {
  167. let videos = document.querySelectorAll("#ytd-player:not([data-tagged])");
  168. for (let video of videos) {
  169. video.setAttribute("data-tagged", "true");
  170. console.log(document.querySelector("#container"));
  171. this.addVideoButton(document.querySelector("#ytd-player"));
  172. }
  173. }, 200);
  174. }
  175. }, __publicField(_a, "siteRegex", /youtu(\.)?be.*/), _a);
  176. YoutubeDownloader = __decorateClass$3([
  177. staticImplements()
  178. ], YoutubeDownloader);
  179. var __defProp$2 = Object.defineProperty;
  180. var __getOwnPropDesc$2 = Object.getOwnPropertyDescriptor;
  181. var __decorateClass$2 = (decorators, target, key, kind) => {
  182. var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc$2(target, key) : target;
  183. for (var i = decorators.length - 1, decorator; i >= 0; i--)
  184. if (decorator = decorators[i])
  185. result = (kind ? decorator(target, key, result) : decorator(result)) || result;
  186. if (kind && result)
  187. __defProp$2(target, key, result);
  188. return result;
  189. };
  190. let FacebookDownloader = (_b = class {
  191. getReactFiber(el) {
  192. for (let prop of Object.keys(el)) {
  193. if (prop.startsWith("__reactFiber")) {
  194. return el[prop];
  195. }
  196. }
  197. return null;
  198. }
  199. fiberReturnUntil(fiber, displayName) {
  200. let fiberInst = fiber;
  201. while (fiberInst != null) {
  202. let fiberInstName = "";
  203. if (typeof fiberInst.elementType === "string")
  204. fiberInstName = fiberInst.elementType;
  205. else if (typeof fiberInst.elementType === "function")
  206. fiberInstName = fiberInst.elementType.displayName;
  207. if (fiberInstName === displayName)
  208. return fiberInst;
  209. fiberInst = fiberInst.return;
  210. }
  211. return null;
  212. }
  213. fiberReturnUntilFn(fiber, predicate) {
  214. let fiberInst = fiber;
  215. while (fiberInst != null) {
  216. if (predicate(fiberInst))
  217. return fiberInst;
  218. fiberInst = fiberInst.return;
  219. }
  220. return null;
  221. }
  222. parentsUntil(el, query) {
  223. let elInst = el;
  224. while (elInst != null) {
  225. if (elInst.matches(query))
  226. return elInst;
  227. elInst = elInst.parentElement;
  228. }
  229. return null;
  230. }
  231. getVideoImplementation(fiber, impl = "VideoPlayerProgressiveImplementation") {
  232. if (!fiber || !fiber.memoizedProps || !fiber.memoizedProps.implementations)
  233. return null;
  234. return fiber.memoizedProps.implementations.find((x) => x.typename === impl);
  235. }
  236. addVideoButton(on, videoEl, isShorts = false) {
  237. let btn = document.createElement("div");
  238. btn.innerHTML = "Download (HD)";
  239. btn.classList.add("dlBtn");
  240. if (isShorts)
  241. btn.classList.add("shorts");
  242. btn.onclick = () => this.btnAct(videoEl);
  243. on.prepend(btn);
  244. }
  245. btnAct(videoEl) {
  246. let fiber = this.getReactFiber(videoEl);
  247. let props = this.fiberReturnUntil(fiber, "a [from CoreVideoPlayer.react]");
  248. let impl = this.getVideoImplementation(props);
  249. if (impl.data.hdSrc) {
  250. window.open(impl.data.hdSrc);
  251. } else {
  252. window.open(impl.data.sdSrc);
  253. }
  254. }
  255. inject() {
  256. Promise.resolve().then(() => facebook$1);
  257. setInterval(() => {
  258. let videos = document.querySelectorAll("video:not([data-tagged])");
  259. for (let video of videos) {
  260. video.setAttribute("data-tagged", "true");
  261. let fiber = this.getReactFiber(video.parentElement);
  262. let props = this.fiberReturnUntil(fiber, "a [from CoreVideoPlayer.react]");
  263. let appendTo = document.querySelector(`[data-instancekey='${props.memoizedState.memoizedState}']`);
  264. let isShorts = false;
  265. if (props.memoizedProps.subOrigin && props.memoizedProps.subOrigin === "fb_shorts_viewer") {
  266. let fiber2 = this.fiberReturnUntilFn(fiber, (fiber22) => {
  267. return fiber22.memoizedProps["data-video-id"];
  268. });
  269. let el = fiber2.stateNode.parentElement.nextSibling;
  270. if (el.classList.contains("__fb-dark-mode"))
  271. el = el.nextSibling;
  272. appendTo = el;
  273. isShorts = true;
  274. }
  275. this.addVideoButton(appendTo, video.parentElement, isShorts);
  276. }
  277. }, 200);
  278. }
  279. }, __publicField(_b, "siteRegex", /facebook\..*/), _b);
  280. FacebookDownloader = __decorateClass$2([
  281. staticImplements()
  282. ], FacebookDownloader);
  283. const Params = {
  284. paramsToObject(entries) {
  285. const result = {};
  286. for (const [key, value] of entries) {
  287. result[key] = value;
  288. }
  289. return result;
  290. },
  291. buildParams(p) {
  292. return new URLSearchParams(p).toString();
  293. }
  294. };
  295. var __defProp$1 = Object.defineProperty;
  296. var __getOwnPropDesc$1 = Object.getOwnPropertyDescriptor;
  297. var __decorateClass$1 = (decorators, target, key, kind) => {
  298. var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc$1(target, key) : target;
  299. for (var i = decorators.length - 1, decorator; i >= 0; i--)
  300. if (decorator = decorators[i])
  301. result = (kind ? decorator(target, key, result) : decorator(result)) || result;
  302. if (kind && result)
  303. __defProp$1(target, key, result);
  304. return result;
  305. };
  306. let RedditDownloader = (_c = class {
  307. constructor() {
  308. __publicField(this, "btnText", "Download (HD)");
  309. }
  310. addVideoButton(on) {
  311. on.querySelectorAll(".dlBtn").forEach((el) => el.remove());
  312. let btn = document.createElement("div");
  313. btn.innerHTML = this.btnText;
  314. btn.classList.add("dlBtn");
  315. btn.onclick = () => this.btnAct(btn);
  316. on.prepend(btn);
  317. }
  318. returnUntil(inst, prop) {
  319. let fInst = inst;
  320. while (fInst != null) {
  321. if (fInst.pendingProps[prop])
  322. return fInst;
  323. fInst = fInst.return;
  324. }
  325. return null;
  326. }
  327. getReactInternalState(el) {
  328. for (let prop of Object.keys(el)) {
  329. if (prop.startsWith("__reactInternalInstance")) {
  330. return el[prop];
  331. }
  332. }
  333. return null;
  334. }
  335. btnAct(btn) {
  336. let src = this.returnUntil(this.getReactInternalState(btn.parentElement), "mpegDashSource");
  337. if (!src) {
  338. alert("Unable to load video data");
  339. return;
  340. }
  341. let mpegDashUrl = src.pendingProps.mpegDashSource;
  342. let match = mpegDashUrl.match(/https:\/\/v.redd.it\/(?<videoId>.+)\/DASHPlaylist\.mpd/);
  343. if (!match) {
  344. alert("Unable to load video data");
  345. return;
  346. }
  347. let videoId = match.groups.videoId;
  348. let p = Params.buildParams({
  349. video_url: "https://v.redd.it/" + videoId + "/DASH_720.mp4?source=fallback",
  350. audio_url: "https://v.redd.it/" + videoId + "/DASH_audio.mp4?source=fallback",
  351. permalink: window.location.origin + src.pendingProps.postUrl.pathname
  352. });
  353. window.open("https://ds.redditsave.com/download.php?" + p);
  354. }
  355. inject() {
  356. Promise.resolve().then(() => reddit$1);
  357. setInterval(() => {
  358. let videos = document.querySelectorAll("video:not([data-tagged])");
  359. for (let video of videos) {
  360. if (video.parentElement.querySelector(".dlBtn") == null && video.parentElement.parentElement.firstChild.getAttribute("role") !== "slider")
  361. this.addVideoButton(video.parentElement);
  362. }
  363. }, 200);
  364. }
  365. }, __publicField(_c, "siteRegex", /reddit\..*/), _c);
  366. RedditDownloader = __decorateClass$1([
  367. staticImplements()
  368. ], RedditDownloader);
  369. var __defProp2 = Object.defineProperty;
  370. var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
  371. var __decorateClass = (decorators, target, key, kind) => {
  372. var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc(target, key) : target;
  373. for (var i = decorators.length - 1, decorator; i >= 0; i--)
  374. if (decorator = decorators[i])
  375. result = (kind ? decorator(target, key, result) : decorator(result)) || result;
  376. if (kind && result)
  377. __defProp2(target, key, result);
  378. return result;
  379. };
  380. let TwitterDownloader = (_d = class {
  381. constructor() {
  382. __publicField(this, "TWITTER_BEARER", "AAAAAAAAAAAAAAAAAAAAANRILgAAAAAAnNwIzUejRCOuH5E6I8xnZz4puTs=1Zv7ttfk8LF81IUq16cHjhLTvJu4FA33AGWWjCpTnA");
  383. }
  384. getReactFiber(el) {
  385. for (let prop of Object.keys(el)) {
  386. if (prop.startsWith("__reactFiber")) {
  387. return el[prop];
  388. }
  389. }
  390. return null;
  391. }
  392. parentsUntil(el, query) {
  393. let elInst = el;
  394. while (elInst != null) {
  395. if (elInst.matches(query))
  396. return elInst;
  397. elInst = elInst.parentElement;
  398. }
  399. return null;
  400. }
  401. fiberReturnUntil(fiber, predicate) {
  402. let fiberInst = fiber;
  403. while (fiberInst != null) {
  404. if (predicate(fiberInst))
  405. return fiberInst;
  406. fiberInst = fiberInst.return;
  407. }
  408. return null;
  409. }
  410. async fetchGuestToken() {
  411. const resp = await fetch("https://api.twitter.com/1.1/guest/activate.json", {
  412. method: "POST",
  413. headers: {
  414. Authorization: `Bearer ${this.TWITTER_BEARER}`
  415. }
  416. });
  417. const respJson = await resp.json();
  418. return respJson.guest_token;
  419. }
  420. async queryApi(twId) {
  421. const resp = await fetch(`https://api.twitter.com/2/timeline/conversation/${twId}.json`, {
  422. method: "GET",
  423. headers: {
  424. "Authorization": `Bearer ${this.TWITTER_BEARER}`,
  425. "X-Guest-Token": await this.fetchGuestToken()
  426. }
  427. });
  428. return await resp.json();
  429. }
  430. addVideoButton(on, videoEl) {
  431. let btn = document.createElement("div");
  432. btn.innerHTML = "Download (HD)";
  433. btn.classList.add("dlBtn");
  434. btn.onclick = () => this.btnAct(videoEl);
  435. on.prepend(btn);
  436. }
  437. async btnAct(videoEl) {
  438. const fiber = this.getReactFiber(videoEl.parentElement.parentElement);
  439. const fiber2 = this.fiberReturnUntil(fiber, (x) => {
  440. var _a2;
  441. return (_a2 = x.memoizedProps) == null ? void 0 : _a2.contentId;
  442. });
  443. const twId = fiber2.memoizedProps.videoId.id;
  444. const data = await this.queryApi(twId);
  445. const media = data.globalObjects.tweets[twId].extended_entities.media;
  446. console.log(data.globalObjects.tweets[twId], media);
  447. if (media.length === 0) {
  448. alert("Cannot fetch media data");
  449. }
  450. let variants = media[0].video_info.variants;
  451. variants = variants.filter((x) => x.content_type !== "application/x-mpegURL").sort((a, b) => {
  452. return a.bitrate > b.bitrate ? -1 : 1;
  453. });
  454. window.open(variants[0].url);
  455. }
  456. inject() {
  457. Promise.resolve().then(() => style$1);
  458. setInterval(() => {
  459. let videos = document.querySelectorAll("video:not([data-tagged])");
  460. for (let video of videos) {
  461. video.setAttribute("data-tagged", "true");
  462. this.addVideoButton(video.parentElement, video);
  463. }
  464. }, 200);
  465. }
  466. }, __publicField(_d, "siteRegex", /twitter\..*/), _d);
  467. TwitterDownloader = __decorateClass([
  468. staticImplements()
  469. ], TwitterDownloader);
  470. Injector$1.register(YoutubeDownloader);
  471. Injector$1.register(FacebookDownloader);
  472. Injector$1.register(RedditDownloader);
  473. Injector$1.register(TwitterDownloader);
  474. document.addEventListener("DOMContentLoaded", () => {
  475. Injector$1.inject(window.location.href);
  476. }, false);
  477. const style = "div.dlBtn {\n position: absolute;\n top: 0;\n right: 0;\n z-index: 99999999;\n padding: 10px 15px;\n margin: 5px;\n cursor: pointer;\n outline: 0;\n background: #5383FB;\n color: white;\n border: 1px solid 1px solid #5383FB;\n font-family: Segoe UI Historic, Segoe UI, Helvetica, Arial, sans-serif !important;\n font-size: 12px;\n}\ndiv.dlBtn:hover {\n background-color: #86A4FC;\n}";
  478. const style$1 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({
  479. __proto__: null,
  480. default: style
  481. }, Symbol.toStringTag, { value: "Module" }));
  482. const facebook = "div.dlBtn {\n position: absolute;\n top: 0;\n right: 0;\n z-index: 99999;\n padding: 10px 15px;\n margin: 5px;\n cursor: pointer;\n outline: 0;\n background: var(--primary-button-background);\n color: var(--primary-button-text);\n border: 1px solid 1px solid var(--accent);\n font-family: var(--font-family-segoe) !important;\n}\ndiv.dlBtn:hover {\n background-color: var(--primary-button-pressed);\n}\ndiv.dlBtn.shorts {\n right: 110px;\n top: 5px;\n}";
  483. const facebook$1 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({
  484. __proto__: null,
  485. default: facebook
  486. }, Symbol.toStringTag, { value: "Module" }));
  487. const reddit = "div.dlBtn {\n position: absolute;\n top: 0;\n right: 0;\n z-index: 99999999;\n padding: 10px 15px;\n margin: 5px;\n cursor: pointer;\n outline: 0;\n background: #5383FB;\n color: white;\n border: 1px solid 1px solid #5383FB;\n font-family: Segoe UI Historic, Segoe UI, Helvetica, Arial, sans-serif !important;\n font-size: 12px;\n}\ndiv.dlBtn:hover {\n background-color: #86A4FC;\n}";
  488. const reddit$1 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({
  489. __proto__: null,
  490. default: reddit
  491. }, Symbol.toStringTag, { value: "Module" }));
  492. })();