X.com Video Downloader (Twitter)

Adds a clean download option as first option to post/tweet dropdown ... menu, opens SaveTheVideo .. link to save to video script to fully automate download process after download clicked

As of 19. 10. 2025. See the latest version.

// ==UserScript==
// @name         X.com Video Downloader (Twitter)
// @namespace    https://github.com/jayfantz
// @version      2.0
// @author       jayfantz
// @description  Adds a clean download option as first option to post/tweet dropdown ... menu, opens SaveTheVideo .. link to save to video script to fully automate download process after download clicked
// @match        https://x.com/*
// @grant        none
// ==/UserScript==

(function () {
  'use strict';

  const site = "https://www.savethevideo.com/downloader?url=";

  const svg = `
    <svg viewBox="0 0 24 24" width="20" height="20" fill="currentColor">
      <path d="M12 3v10.586l3.293-3.293 1.414 1.414L12 17.414l-4.707-4.707 1.414-1.414L11 13.586V3h1z"/>
      <path d="M5 19h14v2H5z"/>
    </svg>`;

  function resolveTweetUrl(menuItem) {
    // normal: find <time> 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}<span style="flex:1">Download video</span>`;

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