Twitter Video Downloader

Download Twitter videos via Twittervid website

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴Userscripts ,之后才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。

您需要先安装用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name                Twitter Video Downloader
// @version             1.0.2
// @description         Download Twitter videos via Twittervid website
// @author              w4t3r1ily
// @namespace           https://github.com/HayaoGai
// @icon                https://www.google.com/s2/favicons?sz=64&domain=twittervid.com
// @include             https://twitter.com/*
// @match               https://twitter.com/*
// @include             https://x.com/*
// @match               https://x.com/*
// @grant               none
// ==/UserScript==



(function () {
  'use strict';

  // icons made by https://www.flaticon.com/authors/freepik
  const svg =
    `<svg viewBox="0 0 512 512"><path d="M472,313v139c0,11.028-8.972,20-20,20H60c-11.028,0-20-8.972-20-20V313H0v139c0,33.084,26.916,60,60,60h392 c33.084,0,60-26.916,60-60V313H472z"></path></g></g><g><g><polygon points="352,235.716 276,311.716 276,0 236,0 236,311.716 160,235.716 131.716,264 256,388.284 380.284,264"></polygon></svg>`;

  const resource = "https://twittervid.com/";
  let currentUrl = document.location.href;
  let updating = false;

  init(10);

  locationChange();

  window.addEventListener("scroll", update);

  function init(times) {
    for (let i = 0; i < times; i++) {
      setTimeout(findVideo1, 500 * i);
      setTimeout(findVideo2, 500 * i);
      setTimeout(sensitiveContent, 500 * i);
    }
  }

  function findVideo1() {
    // video play button
    document
      .querySelectorAll("[data-testid='playButton']")
      .forEach(button => {
        // thumbnail
        button.parentElement
          .querySelectorAll("img:not(.download-set)")
          .forEach(thumbnail => {
            thumbnail.classList.add("download-set");
            const url = thumbnail.src;
            situation(url, thumbnail);
          });
      });
  }

  function findVideo2() {
    // video
    document
      .querySelectorAll("video:not(.download-set)")
      .forEach(video => {
        video.classList.add("download-set");
        const url = video.poster;
        situation(url, video);
      });
  }

  function situation(url, video) {
    // situation 1: gif
    if (url.includes("tweet_")) findMenu(video, "gif");
    // situation 2: video
    else if (
      url.includes("ext_tw_") ||
      url.includes("amplify_") ||
      url.includes("media")
    )
      findMenu(video, "video");
    // situation 3: unknown
    else console.log("Error: Unknown");
  }

  function findMenu(child, type) {
    const article = child.closest("article:not(.article-set)");
    if (!article) return;
    article.classList.add("article-set");
    const menus = article.querySelectorAll("[data-testid='caret']");
    menus.forEach(menu =>
      menu.addEventListener("click", () => {
        clickMenu(article, type, false);
        if (type === "gif") clickMenu(article, type, true);
      })
    );
  }

  function clickMenu(article, type, convert) {
    // check exist.
    if (!!document.querySelector(`.option-download-${convert}-set`)) return;
    // wait menu.
    if (!document.querySelector("[role='menuitem']")) {
      setTimeout(() => clickMenu(article, type, convert), 100);
      return;
    }
    const menu = document.querySelector("[role='menuitem']").parentElement;
    // add "download" option.
    const option = document.createElement("div");
    option.className =
      "css-1dbjc4n r-1loqt21 r-18u37iz r-1ny4l3l r-ymttw5 r-1yzf0co r-o7ynqc r-6416eg r-13qz1uu option-download-set";
    option.addEventListener("mouseenter", () =>
      option.classList.add(getTheme(["r-1u4rsef", "r-1ysxnx4", "r-1uaug3w"]))
    );
    option.addEventListener("mouseleave", () =>
      option.classList.remove(getTheme(["r-1u4rsef", "r-1ysxnx4", "r-1uaug3w"]))
    );
    option.addEventListener("click", () =>
      clickDownload(article, type, convert)
    );
    // icon
    const icon = document.createElement("div");
    icon.className = "css-1dbjc4n r-1777fci";
    icon.innerHTML = svg;
    const svgElement = icon.querySelector("svg");
    svgElement.setAttribute(
      "class",
      "r-4qtqp9 r-yyyyoo r-1q142lx r-1xvli5t r-zso239 r-dnmrzs r-bnwqim r-1plcrui r-lrvibr"
    );
    svgElement.classList.add(getTheme(["r-1re7ezh", "r-9ilb82", "r-111h2gw"]));
    // text
    const text1 = document.createElement("div");
    text1.className = "css-1dbjc4n r-16y2uox r-1wbh5a2";

        const text2 = document.createElement("div");
  text2.className =
    "css-901oao r-1qd0xha r-a023e6 r-16dba41 r-ad9z0x r-bcqeeo r-qvutc0";
  text2.classList.add(getTheme(["r-hkyrab", "r-1fmj7o5", "r-jwli3a"]));
  const text3 = document.createElement("span");
  text3.className =
    "css-901oao css-16my406 r-1qd0xha r-ad9z0x r-bcqeeo r-qvutc0";
  text3.innerText = getLocalization(type, convert);
  // append
  menu.appendChild(option);
  option.appendChild(icon);
  option.appendChild(text1);
  text1.appendChild(text2);
  text2.appendChild(text3);
}

function clickDownload(article, type, convert) {
  // gif
  if (type === "gif" && !convert) {
    let link;
    // condition 1: not play yet.
    article.querySelectorAll("video").forEach(video => {
      link = video.src;
    });
    // condition 2: playing.
    if (!link) {
      const image = [...article.querySelectorAll("img")].find(image =>
        image.src.includes("video")
      );
      const id = image.src.split(/[/?]/)[4];
      link = `https://video.twimg.com/tweet_video/${id}.mp4`;
    }
    // open
    window.open(link);
  }
  // video
  else {
    const tweetId = article.querySelector("time").parentElement.href.split('/').pop();
    window.open(`https://twittervid.com/i/status/${tweetId}`);
  }
}

// Rest of the script remains unchanged...

function getTheme(array) {
  const body = document.querySelector("body");
  const color = body.style.backgroundColor; // "#74818e"
  const red = color.match(/\d+/)[0]; // "#74818e"
  switch (red) {
    case "255":
      return array[0]; // #74818e
    case "0":
      return array[1]; // #74818e
    default:
      return array[2]; // #74818e
  }
}

function getLocalization(type, convert) {
  let download = "Download";
  switch (document.querySelector("html").lang) {
    case "zh-Hant":
      download = "下載";
      break;
    case "zh":
      download = "下载";
      break;
    case "ja":
      download = "ダウンロード";
      break;
    case "ko":
      download = "다운로드";
      break;
    case "ru":
      download = "Скачать";
      break;
  }

  let extension = "";
  if (type === "gif") extension = convert ? " GIF" : " MP4";

  return `${download}${extension}`;
}

function sensitiveContent() {
  // click "view" button on sensitive content warning to run this script again.
  document.querySelectorAll(".r-42olwf.r-1vsu8ta:not(.view-set)").forEach(view => {
    view.classList.add("view-set");
    view.addEventListener("click", () => init(3));
  });
}

function update() {
  if (updating) return;
  updating = true;
  init(3);
  setTimeout(() => { updating = false; }, 1000);
}

function locationChange() {
  const observer = new MutationObserver(mutations => {
    mutations.forEach(() => {
      if (currentUrl !== document.location.href) {
        currentUrl = document.location.href;
        init(10);
      }
    });
  });
  const target = document.body;
  const config = { childList: true, subtree: true };
  observer.observe(target, config);
}

})();