All pastes #7qARSkeKZs Raw Edit

text paste

public text v1 · immutable
#7qARSkeKZs ·published 2026-08-11 16:18 UTC
rendered paste body
// ==UserScript==// @name         X / Twitter Media Downloader// @namespace    Violentmonkey Scripts// @version      1.1.2// @description  Download highest quality photos, videos, and GIFs on X/Twitter (desktop & mobile, including quoted tweets)// @match        https://x.com/*// @match        https://twitter.com/*// @match        https://*.x.com/*// @match        https://*.twitter.com/*// @grant        GM_download// @grant        GM_xmlhttpRequest// @connect      twimg.com// @connect      twitter.com// @connect      x.com// @require      https://cdn.jsdelivr.net/npm/gifshot@0.4.5/build/gifshot.min.js// @run-at       document-start// ==/UserScript==(function () {    'use strict';    const tweetMediaMap = new Map();    // Intercept media metadata from main page context    window.addEventListener('message', (e) => {        if (e.data && e.data.type === 'X_MEDIA_DATA') {            tweetMediaMap.set(e.data.tweetId, e.data.mediaList);        }    });    // Inject interceptor into main page context to capture GraphQL responses    function injectInterceptor() {        const script = document.createElement('script');        script.textContent = `(${function () {            function parseTwitterApi(json) {                if (!json || typeof json !== 'object') return;                function traverse(obj) {                    if (!obj || typeof obj !== 'object') return;                    if (obj.tweet_results?.result) {                        extractFromTweet(obj.tweet_results.result);                    }                    if (obj.tweetResult?.result) {                        extractFromTweet(obj.tweetResult.result);                    }                    if (obj.itemResult?.tweet_results?.result) {                        extractFromTweet(obj.itemResult.tweet_results.result);                    }                    if (obj.__typename === 'Tweet' || obj.__typename === 'TweetWithVisibilityResults') {                        extractFromTweet(obj);                    }                    for (let k in obj) {                        if (Object.prototype.hasOwnProperty.call(obj, k)) {                            traverse(obj[k]);                        }                    }                }                function extractFromTweet(tweetObj) {                    if (!tweetObj) return;                    if (tweetObj.__typename === 'TweetWithVisibilityResults' && tweetObj.tweet) {                        tweetObj = tweetObj.tweet;                    }                    const legacy = tweetObj.legacy;                    const tweetId = legacy?.id_str || tweetObj.rest_id;                    let mediaList = legacy?.extended_entities?.media || legacy?.entities?.media;                    if (tweetId && mediaList && mediaList.length > 0) {                        window.postMessage({ type: 'X_MEDIA_DATA', tweetId, mediaList }, '*');                    }                    // Handle Retweets                    if (legacy?.retweeted_status_result?.result) {                        extractFromTweet(legacy.retweeted_status_result.result);                    }                    // Handle Quoted Tweets explicitly                    if (tweetObj.quoted_status_result?.result) {                        extractFromTweet(tweetObj.quoted_status_result.result);                    }                }                traverse(json);            }            const origOpen = XMLHttpRequest.prototype.open;            XMLHttpRequest.prototype.open = function () {                this.addEventListener('load', function () {                    try {                        if (this.responseText && (this.responseURL.includes('/graphql/') || this.responseURL.includes('/2/'))) {                            parseTwitterApi(JSON.parse(this.responseText));                        }                    } catch (e) { }                });                origOpen.apply(this, arguments);            };            const origFetch = window.fetch;            window.fetch = async function (...args) {                const response = await origFetch.apply(this, args);                try {                    const clone = response.clone();                    const url = clone.url || (args[0] && typeof args[0] === 'string' ? args[0] : '');                    if (url.includes('/graphql/') || url.includes('/2/')) {                        clone.json().then(data => parseTwitterApi(data)).catch(() => { });                    }                } catch (e) { }                return response;            };        }})();`;        (document.head || document.documentElement).appendChild(script);        script.remove();    }    injectInterceptor();    // Media Downloader via GM_download / GM_xmlhttpRequest    function downloadMedia(url, filename) {        return new Promise((resolve, reject) => {            if (typeof GM_download === 'function') {                try {                    GM_download({                        url: url,                        name: filename,                        onload: () => resolve(),                        onerror: (err) => {                            console.warn('GM_download error, falling back to XHR:', err);                            downloadViaXHR(url, filename).then(resolve).catch(reject);                        },                        ontimeout: () => {                            downloadViaXHR(url, filename).then(resolve).catch(reject);                        }                    });                    return;                } catch (e) {                    console.warn('GM_download failed:', e);                }            }            downloadViaXHR(url, filename).then(resolve).catch(reject);        });    }    function downloadViaXHR(url, filename) {        return new Promise((resolve, reject) => {            if (typeof GM_xmlhttpRequest === 'function') {                GM_xmlhttpRequest({                    method: 'GET',                    url: url,                    responseType: 'blob',                    onload: (res) => {                        if (res.status >= 200 && res.status < 300) {                            downloadBlob(res.response, filename);                            resolve();                        } else {                            reject(new Error(`HTTP ${res.status}`));                        }                    },                    onerror: reject,                    ontimeout: () => reject(new Error('Timeout'))                });            } else {                fetch(url)                    .then(res => {                        if (!res.ok) throw new Error(`HTTP ${res.status}`);                        return res.blob();                    })                    .then(blob => {                        downloadBlob(blob, filename);                        resolve();                    })                    .catch(reject);            }        });    }    function downloadBlob(blob, filename) {        const url = URL.createObjectURL(blob);        const a = document.createElement('a');        a.href = url;        a.download = filename;        document.body.appendChild(a);        a.click();        document.body.removeChild(a);        setTimeout(() => URL.revokeObjectURL(url), 10000);    }    function dataURItoBlob(dataURI) {        const byteString = atob(dataURI.split(',')[1]);        const mimeString = dataURI.split(',')[0].split(':')[1].split(';')[0];        const ab = new ArrayBuffer(byteString.length);        const ia = new Uint8Array(ab);        for (let i = 0; i < byteString.length; i++) {            ia[i] = byteString.charCodeAt(i);        }        return new Blob([ab], { type: mimeString });    }    function processAndDownloadGif(videoUrl, btn) {        btn.textContent = '⏳ Converting...';        gifshot.createGIF({            video: [videoUrl],            numFrames: 36,            interval: 0.08,            sampleInterval: 10,            gifWidth: 480,            numWorkers: 2        }, async function (obj) {            if (!obj.error) {                const blob = dataURItoBlob(obj.image);                downloadBlob(blob, `x_gif_${Date.now()}.gif`);                btn.textContent = '✅ Saved GIF';            } else {                console.warn('GIF conversion error, saving as MP4:', obj.error);                try {                    await downloadMedia(videoUrl, `x_gif_${Date.now()}.mp4`);                    btn.textContent = '✅ Saved MP4';                } catch (e) {                    btn.textContent = '❌ Failed';                }            }            setTimeout(() => {                btn.textContent = '⬇️ DL';                btn.dataset.loading = 'false';            }, 3000);        });    }    function findStatusIdInContainer(container) {        if (!container) return null;        // Priority 1: Check time elements        const timeLinks = container.querySelectorAll('time');        for (let timeEl of timeLinks) {            const link = timeEl.closest('a[href*="/status/"]');            if (link) {                const href = link.getAttribute('href') || '';                const match = href.match(/status\/(\d{15,20})/);                if (match) return match[1];            }        }        // Priority 2: Check any status link        const links = container.querySelectorAll('a[href*="/status/"]');        for (let link of links) {            const href = link.getAttribute('href') || '';            const match = href.match(/status\/(\d{15,20})/);            if (match) return match[1];        }        return null;    }    // Resolves Tweet ID for both standard tweets and quoted tweets (including video-player testids)    function getTweetId(element) {        if (!element) return null;        // 1. Look inside media container for testids like "video-player-mini-ui-2086876306000482413"        const testIdNodes = [element, ...element.querySelectorAll('[data-testid]')];        for (const node of testIdNodes) {            const testId = node.getAttribute?.('data-testid') || '';            if (testId.includes('video-player') || testId.includes('tweet') || testId.includes('media')) {                const match = testId.match(/(\d{15,20})/);                if (match) return match[1];            }        }        // 2. Search status links inside current element        const statusIdInSubtree = findStatusIdInContainer(element);        if (statusIdInSubtree) return statusIdInSubtree;        // 3. Ascend DOM tree level-by-level        let current = element.parentElement;        while (current && current !== document.body) {            const testIdNodesCurrent = [current, ...current.querySelectorAll('[data-testid]')];            for (const node of testIdNodesCurrent) {                const testId = node.getAttribute?.('data-testid') || '';                if (testId.includes('video-player') || testId.includes('tweet') || testId.includes('media')) {                    const match = testId.match(/(\d{15,20})/);                    if (match) return match[1];                }            }            const statusId = findStatusIdInContainer(current);            if (statusId) return statusId;            current = current.parentElement;        }        // 4. Fallback to current URL        const pageMatch = window.location.pathname.match(/status\/(\d{15,20})/);        if (pageMatch) return pageMatch[1];        return null;    }    // Fetches media details from cache or Twitter Syndication API fallback    async function getMediaInfoForTweet(tweetId) {        if (!tweetId) return null;        if (tweetMediaMap.has(tweetId)) {            return tweetMediaMap.get(tweetId);        }        try {            const syndicationUrl = `https://cdn.syndication.twimg.com/tweet-result?id=${tweetId}&token=x`;            const res = await new Promise((resolve, reject) => {                if (typeof GM_xmlhttpRequest === 'function') {                    GM_xmlhttpRequest({                        method: 'GET',                        url: syndicationUrl,                        onload: (r) => {                            if (r.status === 200) {                                try { resolve(JSON.parse(r.responseText)); } catch (e) { reject(e); }                            } else {                                reject(r);                            }                        },                        onerror: reject,                        ontimeout: reject                    });                } else {                    fetch(syndicationUrl).then(r => r.json()).then(resolve).catch(reject);                }            });            const mediaList = res?.mediaDetails || [];            if (mediaList.length > 0) {                tweetMediaMap.set(tweetId, mediaList);                return mediaList;            }        } catch (e) {            console.warn('Syndication API fallback error:', e);        }        return null;    }    function extractBestVideoFromMediaList(mediaList) {        if (!mediaList || !Array.isArray(mediaList)) return null;        let bestVideoUrl = null;        let maxBitrate = -1;        let isGif = false;        for (const m of mediaList) {            const variants = m.video_info?.variants || [];            const currentIsGif = m.type === 'animated_gif';            for (const v of variants) {                if ((v.content_type === 'video/mp4' || v.url?.includes('.mp4')) && v.url) {                    const bitrate = v.bitrate || 0;                    if (bitrate >= maxBitrate) {                        maxBitrate = bitrate;                        bestVideoUrl = v.url;                        isGif = currentIsGif;                    }                }            }        }        if (bestVideoUrl) {            return { url: bestVideoUrl, isGif };        }        return null;    }    async function handleDownload(container, btn) {        if (btn.dataset.loading === 'true') return;        btn.dataset.loading = 'true';        try {            // 1. Photo handling            const img = container.querySelector('img[src*="twimg.com/media/"]');            if (img && !container.querySelector('video')) {                let src = img.src;                try {                    const u = new URL(src);                    const format = u.searchParams.get('format') || 'jpg';                    u.searchParams.set('format', format);                    u.searchParams.set('name', 'orig');                    src = u.toString();                } catch (e) { }                btn.textContent = '⏳ Saving...';                await downloadMedia(src, `x_photo_${Date.now()}.${src.includes('format=png') ? 'png' : 'jpg'}`);                btn.textContent = '✅ Saved';                setTimeout(() => { btn.textContent = '⬇️ DL'; btn.dataset.loading = 'false'; }, 3000);                return;            }            // 2. Video / GIF handling            const video = container.querySelector('video');            if (video) {                btn.textContent = '⏳ Resolving...';                const tweetId = getTweetId(container);                let videoData = null;                if (tweetId) {                    const mediaList = await getMediaInfoForTweet(tweetId);                    if (mediaList) {                        videoData = extractBestVideoFromMediaList(mediaList);                    }                }                // Fallback direct src                if (!videoData) {                    let src = video.src || video.currentSrc || '';                    if (!src || src.startsWith('blob:')) {                        const sourceEl = video.querySelector('source');                        if (sourceEl) src = sourceEl.src;                    }                    if (src && !src.startsWith('blob:')) {                        const isGif = src.includes('tweet_video') || video.loop || video.hasAttribute('data-is-gif');                        videoData = { url: src, isGif };                    }                }                if (!videoData || !videoData.url) {                    alert('Could not resolve video URL. Please try playing the video and click DL again.');                    btn.textContent = '❌ Failed';                    setTimeout(() => { btn.textContent = '⬇️ DL'; btn.dataset.loading = 'false'; }, 3000);                    return;                }                if (videoData.isGif || videoData.url.includes('tweet_video')) {                    processAndDownloadGif(videoData.url, btn);                } else {                    btn.textContent = '⏳ Saving...';                    await downloadMedia(videoData.url, `x_video_${Date.now()}.mp4`);                    btn.textContent = '✅ Saved';                    setTimeout(() => { btn.textContent = '⬇️ DL'; btn.dataset.loading = 'false'; }, 3000);                }            }        } catch (err) {            console.error('Download error:', err);            btn.textContent = '❌ Error';            setTimeout(() => { btn.textContent = '⬇️ DL'; btn.dataset.loading = 'false'; }, 3000);        }    }    function createDownloadButton() {        const btn = document.createElement('button');        btn.className = 'x-oneclick-dl-btn';        btn.textContent = '⬇️ DL';        btn.title = 'Download Media';        Object.assign(btn.style, {            position: 'absolute',            top: '10px',            right: '10px',            zIndex: '99999',            backgroundColor: 'rgba(0, 0, 0, 0.75)',            color: '#ffffff',            border: '1px solid rgba(255, 255, 255, 0.3)',            borderRadius: '20px',            padding: '6px 12px',            fontSize: '13px',            fontWeight: 'bold',            cursor: 'pointer',            backdropFilter: 'blur(4px)',            webkitBackdropFilter: 'blur(4px)',            boxShadow: '0 2px 8px rgba(0,0,0,0.4)',            userSelect: 'none',            webkitUserSelect: 'none',            touchAction: 'manipulation'        });        return btn;    }    function scanAndAttachButtons() {        const selectors = [            'div[data-testid="tweetPhoto"]',            'div[data-testid="videoPlayer"]',            'div[data-testid="videoComponent"]',            'div[data-testid="tweetVideo"]',            'div[data-testid="swipeable-media"]',            'div[data-testid="slides"]'        ];        const mediaContainers = document.querySelectorAll(selectors.join(', '));        mediaContainers.forEach(container => {            if (container.dataset.hasDlBtn) return;            const computedStyle = window.getComputedStyle(container);            if (computedStyle.position === 'static') {                container.style.position = 'relative';            }            const btn = createDownloadButton();            const stopEvents = (e) => {                e.stopPropagation();            };            btn.addEventListener('pointerdown', stopEvents);            btn.addEventListener('touchstart', stopEvents, { passive: true });            btn.addEventListener('click', (e) => {                stopEvents(e);                e.preventDefault();                handleDownload(container, btn);            });            container.appendChild(btn);            container.dataset.hasDlBtn = 'true';        });        const fullScreenVideos = document.querySelectorAll('video');        fullScreenVideos.forEach(v => {            const parent = v.parentElement;            if (parent && !parent.dataset.hasDlBtn && !parent.closest('[data-has-dl-btn="true"]')) {                const computedStyle = window.getComputedStyle(parent);                if (computedStyle.position === 'static') {                    parent.style.position = 'relative';                }                const btn = createDownloadButton();                btn.addEventListener('pointerdown', (e) => e.stopPropagation());                btn.addEventListener('click', (e) => {                    e.stopPropagation();                    e.preventDefault();                    handleDownload(parent, btn);                });                parent.appendChild(btn);                parent.dataset.hasDlBtn = 'true';            }        });    }    function init() {        const observer = new MutationObserver(() => scanAndAttachButtons());        observer.observe(document.body, { childList: true, subtree: true });        scanAndAttachButtons();    }    if (document.readyState === 'loading') {        document.addEventListener('DOMContentLoaded', init);    } else {        init();    }})();