跳到主内容
paste
bin
.ca
type · paste · share
⌘
K
系列
bin 系列
pastebin.ca
中心
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.
文档
登录
?
← 返回文本
›
编辑 / 分支
无标题文本
#7qARSkeKZs
public / public
新版本
匿名
已创建 4 days ago
过期于 3 days
20.5 KB
语法:
text
你的更改会创建一个链接到此文本的新文本 — 原始文本不变。
新版本
你的更改会创建一个链接到此文本的新文本 — 原始文本不变。
标题(可选)
文件名
语法
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
可见性
公开动态
访问
public
过期
7 天
10 分钟
1 小时
1 天
7 天
30 天
90 天
自定义…
自定义过期
变更备注
(可选)
此文本会显示在公开动态中。如果只想通过链接分享,请更改可见性。
创建新版本
取消
粘贴或输入…
// ==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(); } })();