passer au contenu principal
paste
bin
.ca
type · paste · share
⌘
K
Famille
La famille bin
pastebin.ca
pôle
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
Se connecter
?
← retour au collage
›
Modifier / dupliquer
Collage sans titre
#7qARSkeKZs
public / public
nouvelle version
anonyme
créé 4 days ago
Expire dans 3 days
20.5 KB
syntaxe:
text
Vos changements créent un nouveau collage lié à celui-ci — l’original reste intact.
nouvelle version
Vos changements créent un nouveau collage lié à celui-ci — l’original reste intact.
Titre (facultatif)
Nom de fichier
Syntaxe
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
Visibilité
Fil public
Accès
public
Expire
7 jours
10 min
1 heure
1 jour
7 jours
30 jours
90 jours
personnalisé…
Expiration personnalisée
Note de changement
(facultatif)
Ce collage sera affiché dans le fil public. Changez la visibilité si vous voulez seulement le partager par lien.
Créer une nouvelle version
Annuler
Collez ou tapez…
// ==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(); } })();