Twitter Video Downloader

Download Twitter videos via Twittervid website

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

})();