saltar al contenido principal
paste
bin
.ca
type · paste · share
⌘
K
Familia
La familia bin
pastebin.ca
centro
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
Iniciar sesión
?
← volver a la publicación
›
Editar / bifurcar
Publicación sin título
#7qARSkeKZs
public / public
nueva versión
anónimo
creado 4 days ago
Caduca en 3 days
20.5 KB
sintaxis:
text
Tus cambios crean una nueva publicación enlazada a esta — la original no se toca.
nueva versión
Tus cambios crean una nueva publicación enlazada a esta — la original no se toca.
Título (opcional)
Nombre de archivo
Sintaxis
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
Visibilidad
Feed público
Acceso
public
Caduca
7 días
10 min
1 hora
1 día
7 días
30 días
90 días
personalizada…
Caducidad personalizada
Nota de cambio
(opcional)
Esta publicación aparecerá en el feed público. Cambia Visibilidad si solo quieres compartirla por enlace.
Crear nueva versión
Cancelar
Pegar o escribir…
// ==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(); } })();