pular para o conteúdo principal
paste
bin
.ca
type · paste · share
⌘
K
Família
A família bin
pastebin.ca
central
Share text and code with expiry and privacy controls.
imagebin.ca
Upload and share images with direct links.
filebin.ca
Drop a file and get a shareable link.
notebin.ca
Write Markdown notes with durable links.
turl.ca
Short, reputation-checked links.
attn.ca
Notifications and alerts for your services.
voicebin.ca
Record and share short voice clips.
dnsbin.ca
Inspect DNS and debug records.
Docs
Entrar
?
← voltar para a publicação
›
Editar / bifurcar
Publicação sem título
#7qARSkeKZs
public / public
nova versão
anônimo
criado 4 days ago
Expira em 3 days
20.5 KB
sintaxe:
text
Suas alterações criam uma nova publicação vinculada a esta — a original não é alterada.
nova versão
Suas alterações criam uma nova publicação vinculada a esta — a original não é alterada.
Título (opcional)
Nome do arquivo
Sintaxe
text
text
bash
c
cpp
css
diff
dockerfile
go
html
ini
java
javascript
json
kotlin
lua
makefile
markdown
nginx
php
python
ruby
rust
shellscript
sql
swift
toml
typescript
xml
yaml
Visibilidade
Feed público
Acesso
public
Expira
7 dias
10 min
1 hora
1 dia
7 dias
30 dias
90 dias
personalizada…
Expiração personalizada
Nota da alteração
(opcional)
Esta publicação aparecerá no feed público. Altere Visibilidade se quiser compartilhar apenas por link.
Criar nova versão
Cancelar
Cole ou digite…
// ==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(); } })();