Video Downloader for Tampermonkey

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

// ==UserScript==
// @name              Video Downloader for Tampermonkey
// @version           0.5
// @description       Will add a download button to various websites such as Reddit, Facebook, Youtube and Twitter
// @author            Mordo95
// @namespace         com.mordo95.Downloader
// @license           MIT
// @match             *://*/*
// @supportURL        https://github.com
// @run-at            document-start
// @grant             GM_addStyle
// @grant             GM_xmlhttpRequest
// ==/UserScript==

var __defProp = Object.defineProperty;
var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
var __publicField = (obj, key, value) => {
  __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
  return value;
};
(function() {
  var _a, _b, _c, _d;
  "use strict";
  GM_addStyle(`
div.dlBtn {
  position: absolute;
  top: 0;
  right: 0;
  z-index: 99999999;
  padding: 10px 15px;
  margin: 5px;
  cursor: pointer;
  outline: 0;
  background: #5383FB;
  color: white;
  border: 1px solid 1px solid #5383FB;
  font-family: Segoe UI Historic, Segoe UI, Helvetica, Arial, sans-serif !important;
  font-size: 12px;
}
div.dlBtn:hover {
  background-color: #86A4FC;
}div.dlBtn {
  position: absolute;
  top: 0;
  right: 0;
  z-index: 99999;
  padding: 10px 15px;
  margin: 5px;
  cursor: pointer;
  outline: 0;
  background: var(--primary-button-background);
  color: var(--primary-button-text);
  border: 1px solid 1px solid var(--accent);
  font-family: var(--font-family-segoe) !important;
}
div.dlBtn:hover {
  background-color: var(--primary-button-pressed);
}
div.dlBtn.shorts {
  right: 110px;
  top: 5px;
}div.dlBtn {
  position: absolute;
  top: 0;
  right: 0;
  z-index: 99999999;
  padding: 10px 15px;
  margin: 5px;
  cursor: pointer;
  outline: 0;
  background: #5383FB;
  color: white;
  border: 1px solid 1px solid #5383FB;
  font-family: Segoe UI Historic, Segoe UI, Helvetica, Arial, sans-serif !important;
  font-size: 12px;
}
div.dlBtn:hover {
  background-color: #86A4FC;
}  `);
  class Injector {
    constructor() {
      __publicField(this, "downloaders", []);
    }
    register(downloader) {
      if (Array.isArray(downloader)) {
        this.downloaders = this.downloaders.concat(downloader);
      } else
        this.downloaders.push(downloader);
    }
    inject(location) {
      for (const downloader of this.downloaders) {
        if (location.match(downloader.siteRegex))
          new downloader().inject();
      }
    }
  }
  const Injector$1 = new Injector();
  function staticImplements() {
    return (constructor) => {
    };
  }
  var __defProp$3 = Object.defineProperty;
  var __getOwnPropDesc$3 = Object.getOwnPropertyDescriptor;
  var __decorateClass$3 = (decorators, target, key, kind) => {
    var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc$3(target, key) : target;
    for (var i = decorators.length - 1, decorator; i >= 0; i--)
      if (decorator = decorators[i])
        result = (kind ? decorator(target, key, result) : decorator(result)) || result;
    if (kind && result)
      __defProp$3(target, key, result);
    return result;
  };
  let YoutubeDownloader = (_a = class {
    constructor() {
      __publicField(this, "btnText", "Download (HD)");
    }
    addVideoButton(on) {
      let btn = document.createElement("div");
      btn.innerHTML = this.btnText;
      btn.classList.add("dlBtn");
      btn.onclick = () => this.getLinks(btn);
      on.prepend(btn);
    }
    getLinks(btn) {
      let fd = new FormData();
      fd.set("q", window.location.href);
      fd.set("vt", "mp4");
      let url = "https://yt1s.com/api/ajaxSearch/index";
      GM_xmlhttpRequest({
        method: "POST",
        url,
        data: fd,
        onload: (resp) => {
          let js = JSON.parse(resp.responseText);
          this.convert(btn, js.vid, js.links.mp4.auto.k);
        }
      });
    }
    convert(btn, vid, k) {
      let fd = new FormData();
      fd.set("vid", vid);
      fd.set("k", k);
      btn.innerHTML = "Converting ...";
      GM_xmlhttpRequest({
        method: "POST",
        url: "https://yt1s.com/api/ajaxConvert/convert",
        data: fd,
        timeout: 6e4,
        onload: (resp) => {
          let js = JSON.parse(resp.responseText);
          let status = js.c_status;
          if (status === "CONVERTED") {
            window.open(js.dlink);
          } else {
            alert("Error converting video. Please try again later!");
          }
          btn.innerHTML = this.btnText;
        },
        onTimeout: () => {
          btn.innerHTML = this.btnText;
        }
      });
    }
    inject() {
      Promise.resolve().then(() => style$1);
      setInterval(() => {
        let videos = document.querySelectorAll("#ytd-player:not([data-tagged])");
        for (let video of videos) {
          video.setAttribute("data-tagged", "true");
          console.log(document.querySelector("#container"));
          this.addVideoButton(document.querySelector("#ytd-player"));
        }
      }, 200);
    }
  }, __publicField(_a, "siteRegex", /youtu(\.)?be.*/), _a);
  YoutubeDownloader = __decorateClass$3([
    staticImplements()
  ], YoutubeDownloader);
  var __defProp$2 = Object.defineProperty;
  var __getOwnPropDesc$2 = Object.getOwnPropertyDescriptor;
  var __decorateClass$2 = (decorators, target, key, kind) => {
    var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc$2(target, key) : target;
    for (var i = decorators.length - 1, decorator; i >= 0; i--)
      if (decorator = decorators[i])
        result = (kind ? decorator(target, key, result) : decorator(result)) || result;
    if (kind && result)
      __defProp$2(target, key, result);
    return result;
  };
  let FacebookDownloader = (_b = class {
    getReactFiber(el) {
      for (let prop of Object.keys(el)) {
        if (prop.startsWith("__reactFiber")) {
          return el[prop];
        }
      }
      return null;
    }
    fiberReturnUntil(fiber, displayName) {
      let fiberInst = fiber;
      while (fiberInst != null) {
        let fiberInstName = "";
        if (typeof fiberInst.elementType === "string")
          fiberInstName = fiberInst.elementType;
        else if (typeof fiberInst.elementType === "function")
          fiberInstName = fiberInst.elementType.displayName;
        if (fiberInstName === displayName)
          return fiberInst;
        fiberInst = fiberInst.return;
      }
      return null;
    }
    fiberReturnUntilFn(fiber, predicate) {
      let fiberInst = fiber;
      while (fiberInst != null) {
        if (predicate(fiberInst))
          return fiberInst;
        fiberInst = fiberInst.return;
      }
      return null;
    }
    parentsUntil(el, query) {
      let elInst = el;
      while (elInst != null) {
        if (elInst.matches(query))
          return elInst;
        elInst = elInst.parentElement;
      }
      return null;
    }
    getVideoImplementation(fiber, impl = "VideoPlayerProgressiveImplementation") {
      if (!fiber || !fiber.memoizedProps || !fiber.memoizedProps.implementations)
        return null;
      return fiber.memoizedProps.implementations.find((x) => x.typename === impl);
    }
    addVideoButton(on, videoEl, isShorts = false) {
      let btn = document.createElement("div");
      btn.innerHTML = "Download (HD)";
      btn.classList.add("dlBtn");
      if (isShorts)
        btn.classList.add("shorts");
      btn.onclick = () => this.btnAct(videoEl);
      on.prepend(btn);
    }
    btnAct(videoEl) {
      let fiber = this.getReactFiber(videoEl);
      let props = this.fiberReturnUntil(fiber, "a [from CoreVideoPlayer.react]");
      let impl = this.getVideoImplementation(props);
      if (impl.data.hdSrc) {
        window.open(impl.data.hdSrc);
      } else {
        window.open(impl.data.sdSrc);
      }
    }
    inject() {
      Promise.resolve().then(() => facebook$1);
      setInterval(() => {
        let videos = document.querySelectorAll("video:not([data-tagged])");
        for (let video of videos) {
          video.setAttribute("data-tagged", "true");
          let fiber = this.getReactFiber(video.parentElement);
          let props = this.fiberReturnUntil(fiber, "a [from CoreVideoPlayer.react]");
          let appendTo = document.querySelector(`[data-instancekey='${props.memoizedState.memoizedState}']`);
          let isShorts = false;
          if (props.memoizedProps.subOrigin && props.memoizedProps.subOrigin === "fb_shorts_viewer") {
            let fiber2 = this.fiberReturnUntilFn(fiber, (fiber22) => {
              return fiber22.memoizedProps["data-video-id"];
            });
            let el = fiber2.stateNode.parentElement.nextSibling;
            if (el.classList.contains("__fb-dark-mode"))
              el = el.nextSibling;
            appendTo = el;
            isShorts = true;
          }
          this.addVideoButton(appendTo, video.parentElement, isShorts);
        }
      }, 200);
    }
  }, __publicField(_b, "siteRegex", /facebook\..*/), _b);
  FacebookDownloader = __decorateClass$2([
    staticImplements()
  ], FacebookDownloader);
  const Params = {
    paramsToObject(entries) {
      const result = {};
      for (const [key, value] of entries) {
        result[key] = value;
      }
      return result;
    },
    buildParams(p) {
      return new URLSearchParams(p).toString();
    }
  };
  var __defProp$1 = Object.defineProperty;
  var __getOwnPropDesc$1 = Object.getOwnPropertyDescriptor;
  var __decorateClass$1 = (decorators, target, key, kind) => {
    var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc$1(target, key) : target;
    for (var i = decorators.length - 1, decorator; i >= 0; i--)
      if (decorator = decorators[i])
        result = (kind ? decorator(target, key, result) : decorator(result)) || result;
    if (kind && result)
      __defProp$1(target, key, result);
    return result;
  };
  let RedditDownloader = (_c = class {
    constructor() {
      __publicField(this, "btnText", "Download (HD)");
    }
    addVideoButton(on) {
      on.querySelectorAll(".dlBtn").forEach((el) => el.remove());
      let btn = document.createElement("div");
      btn.innerHTML = this.btnText;
      btn.classList.add("dlBtn");
      btn.onclick = () => this.btnAct(btn);
      on.prepend(btn);
    }
    returnUntil(inst, prop) {
      let fInst = inst;
      while (fInst != null) {
        if (fInst.pendingProps[prop])
          return fInst;
        fInst = fInst.return;
      }
      return null;
    }
    getReactInternalState(el) {
      for (let prop of Object.keys(el)) {
        if (prop.startsWith("__reactInternalInstance")) {
          return el[prop];
        }
      }
      return null;
    }
    btnAct(btn) {
      let src = this.returnUntil(this.getReactInternalState(btn.parentElement), "mpegDashSource");
      if (!src) {
        alert("Unable to load video data");
        return;
      }
      let mpegDashUrl = src.pendingProps.mpegDashSource;
      let match = mpegDashUrl.match(/https:\/\/v.redd.it\/(?<videoId>.+)\/DASHPlaylist\.mpd/);
      if (!match) {
        alert("Unable to load video data");
        return;
      }
      let videoId = match.groups.videoId;
      let p = Params.buildParams({
        video_url: "https://v.redd.it/" + videoId + "/DASH_720.mp4?source=fallback",
        audio_url: "https://v.redd.it/" + videoId + "/DASH_audio.mp4?source=fallback",
        permalink: window.location.origin + src.pendingProps.postUrl.pathname
      });
      window.open("https://ds.redditsave.com/download.php?" + p);
    }
    inject() {
      Promise.resolve().then(() => reddit$1);
      setInterval(() => {
        let videos = document.querySelectorAll("video:not([data-tagged])");
        for (let video of videos) {
          if (video.parentElement.querySelector(".dlBtn") == null && video.parentElement.parentElement.firstChild.getAttribute("role") !== "slider")
            this.addVideoButton(video.parentElement);
        }
      }, 200);
    }
  }, __publicField(_c, "siteRegex", /reddit\..*/), _c);
  RedditDownloader = __decorateClass$1([
    staticImplements()
  ], RedditDownloader);
  var __defProp2 = Object.defineProperty;
  var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
  var __decorateClass = (decorators, target, key, kind) => {
    var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc(target, key) : target;
    for (var i = decorators.length - 1, decorator; i >= 0; i--)
      if (decorator = decorators[i])
        result = (kind ? decorator(target, key, result) : decorator(result)) || result;
    if (kind && result)
      __defProp2(target, key, result);
    return result;
  };
  let TwitterDownloader = (_d = class {
    constructor() {
      __publicField(this, "TWITTER_BEARER", "AAAAAAAAAAAAAAAAAAAAANRILgAAAAAAnNwIzUejRCOuH5E6I8xnZz4puTs=1Zv7ttfk8LF81IUq16cHjhLTvJu4FA33AGWWjCpTnA");
    }
    getReactFiber(el) {
      for (let prop of Object.keys(el)) {
        if (prop.startsWith("__reactFiber")) {
          return el[prop];
        }
      }
      return null;
    }
    parentsUntil(el, query) {
      let elInst = el;
      while (elInst != null) {
        if (elInst.matches(query))
          return elInst;
        elInst = elInst.parentElement;
      }
      return null;
    }
    fiberReturnUntil(fiber, predicate) {
      let fiberInst = fiber;
      while (fiberInst != null) {
        if (predicate(fiberInst))
          return fiberInst;
        fiberInst = fiberInst.return;
      }
      return null;
    }
    async fetchGuestToken() {
      const resp = await fetch("https://api.twitter.com/1.1/guest/activate.json", {
        method: "POST",
        headers: {
          Authorization: `Bearer ${this.TWITTER_BEARER}`
        }
      });
      const respJson = await resp.json();
      return respJson.guest_token;
    }
    async queryApi(twId) {
      const resp = await fetch(`https://api.twitter.com/2/timeline/conversation/${twId}.json`, {
        method: "GET",
        headers: {
          "Authorization": `Bearer ${this.TWITTER_BEARER}`,
          "X-Guest-Token": await this.fetchGuestToken()
        }
      });
      return await resp.json();
    }
    addVideoButton(on, videoEl) {
      let btn = document.createElement("div");
      btn.innerHTML = "Download (HD)";
      btn.classList.add("dlBtn");
      btn.onclick = () => this.btnAct(videoEl);
      on.prepend(btn);
    }
    async btnAct(videoEl) {
      const fiber = this.getReactFiber(videoEl.parentElement.parentElement);
      const fiber2 = this.fiberReturnUntil(fiber, (x) => {
        var _a2;
        return (_a2 = x.memoizedProps) == null ? void 0 : _a2.contentId;
      });
      const twId = fiber2.memoizedProps.videoId.id;
      const data = await this.queryApi(twId);
      const media = data.globalObjects.tweets[twId].extended_entities.media;
      console.log(data.globalObjects.tweets[twId], media);
      if (media.length === 0) {
        alert("Cannot fetch media data");
      }
      let variants = media[0].video_info.variants;
      variants = variants.filter((x) => x.content_type !== "application/x-mpegURL").sort((a, b) => {
        return a.bitrate > b.bitrate ? -1 : 1;
      });
      window.open(variants[0].url);
    }
    inject() {
      Promise.resolve().then(() => style$1);
      setInterval(() => {
        let videos = document.querySelectorAll("video:not([data-tagged])");
        for (let video of videos) {
          video.setAttribute("data-tagged", "true");
          this.addVideoButton(video.parentElement, video);
        }
      }, 200);
    }
  }, __publicField(_d, "siteRegex", /twitter\..*/), _d);
  TwitterDownloader = __decorateClass([
    staticImplements()
  ], TwitterDownloader);
  Injector$1.register(YoutubeDownloader);
  Injector$1.register(FacebookDownloader);
  Injector$1.register(RedditDownloader);
  Injector$1.register(TwitterDownloader);
  document.addEventListener("DOMContentLoaded", () => {
    Injector$1.inject(window.location.href);
  }, false);
  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}";
  const style$1 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({
    __proto__: null,
    default: style
  }, Symbol.toStringTag, { value: "Module" }));
  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}";
  const facebook$1 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({
    __proto__: null,
    default: facebook
  }, Symbol.toStringTag, { value: "Module" }));
  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}";
  const reddit$1 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({
    __proto__: null,
    default: reddit
  }, Symbol.toStringTag, { value: "Module" }));
})();