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