// ==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 // @downloadURL https://update.greasyfork.icu/scripts/472120/Twitter%20Video%20Downloader.user.js // @updateURL https://update.greasyfork.icu/scripts/472120/Twitter%20Video%20Downloader.meta.js // ==/UserScript== (function () { 'use strict'; // icons made by https://www.flaticon.com/authors/freepik const 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); } })();