// ==UserScript==
// @name 🟣 X.com Video Downloader (Twitter)
// @namespace https://github.com/jayfantz
// @version 2.0
// @author jayfantz
// @description Adds a “Download Video” option to the top of every post/tweet menu. Opens SaveTheVideo for one-click saving. Works alone, or pair it with the SaveTheVideo Auto-Start script for full automation.
// @match https://x.com/*
// @grant none
// @downloadURL none
// ==/UserScript==
(function () {
'use strict';
const site = "https://www.savethevideo.com/downloader?url=";
const svg = `
`;
function resolveTweetUrl(menuItem) {
// normal: find link inside the nearest article
let link = menuItem.closest('article')?.querySelector('time')?.parentElement?.href;
if (link) return link;
// timeline overlay: look for first tweet anchor inside same article
const article = menuItem.closest('article');
if (article) {
const anchor = article.querySelector('a[href*="/status/"]');
if (anchor) return anchor.href;
}
// pop-out player or embedded mode
const playable = document.querySelector('video')?.closest('article');
if (playable) {
const anchor = playable.querySelector('a[href*="/status/"]');
if (anchor) return anchor.href;
}
// fallback: current page
return location.href;
}
const observer = new MutationObserver(() => {
const menuItems = document.querySelectorAll('[role="menuitem"]:not(.dl-added)');
menuItems.forEach(item => {
const parent = item.closest('[role="menu"]');
if (parent && !parent.querySelector('.dl-download')) {
const dl = document.createElement('div');
dl.className = 'dl-download dl-added';
dl.style.cssText = `
display:flex;
align-items:center;
gap:10px;
padding:12px 16px;
cursor:pointer;
color:rgb(231,233,234);
font-family:"TwitterChirp",-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Helvetica,Arial,sans-serif;
font-size:15px;
font-weight:600;
transition:background 0.15s ease;
`;
dl.innerHTML = `${svg}Download video `;
dl.addEventListener('mouseenter', () => dl.style.background = 'rgba(239,243,244,0.08)');
dl.addEventListener('mouseleave', () => dl.style.background = 'transparent');
dl.addEventListener('mouseenter', () => dl.style.background = 'rgba(255,255,255,0.1)');
dl.addEventListener('mouseleave', () => dl.style.background = 'transparent');
dl.addEventListener('click', e => {
e.stopPropagation();
// find the caret that opened this menu
const activeCaret = document.querySelector('[data-testid="caret"][aria-expanded="true"]');
const article = activeCaret?.closest('article');
const tweetUrl = article?.querySelector('a[href*="/status/"]')?.href || location.href;
if (!tweetUrl) {
alert('Could not locate tweet URL.');
return;
}
const url = site + encodeURIComponent(tweetUrl);
window.open(url, '_blank');
document.body.click(); // close menu
});
parent.appendChild(dl);
}
});
});
observer.observe(document.body, { childList: true, subtree: true });
})();