Video Downloader for Tampermonkey

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

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

You will need to install an extension such as Tampermonkey to install this script.

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

// ==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" }));
})();