您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Preview and download all images & videos from a Discord channel in one ZIP, or copy an aria2c‐compatible URL list to clipboard (with example command).
// ==UserScript== // @name Discord Media Exporter // @namespace http://tampermonkey.net/ // @version 1.1.0 // @description Preview and download all images & videos from a Discord channel in one ZIP, or copy an aria2c‐compatible URL list to clipboard (with example command). // @license CC-BY-NC-4.0 // @author DestCom // @match https://discord.com/channels/* // @grant GM_xmlhttpRequest // @connect cdn.discordapp.com // @require https://cdnjs.cloudflare.com/ajax/libs/jszip/3.7.1/jszip.min.js // @require https://cdnjs.cloudflare.com/ajax/libs/FileSaver.js/2.0.5/FileSaver.min.js // ==/UserScript== (function() { 'use strict'; ////////////////////////////////////////////////////// // 1. CREATE “EXPORT MEDIA” BUTTON ////////////////////////////////////////////////////// const exportBtn = document.createElement('button'); exportBtn.textContent = '📥 Export Media'; Object.assign(exportBtn.style, { position: 'fixed', top: '70px', right: '20px', zIndex: 1000, padding: '8px 12px', backgroundColor: '#7289DA', color: '#FFFFFF', border: 'none', borderRadius: '4px', fontSize: '14px', cursor: 'pointer', boxShadow: '0 2px 6px rgba(0,0,0,0.2)' }); document.body.appendChild(exportBtn); ////////////////////////////////////////////////////// // 2. BUILD THE MAIN MODAL (Media Preview) ////////////////////////////////////////////////////// const modalOverlay = document.createElement('div'); modalOverlay.id = 'dme-modal-overlay'; Object.assign(modalOverlay.style, { position: 'fixed', top: '0', left: '0', width: '100%', height: '100%', backgroundColor: 'rgba(0,0,0,0.75)', display: 'none', justifyContent: 'center', alignItems: 'center', zIndex: 2000, overflowY: 'auto' }); const modalContent = document.createElement('div'); modalContent.id = 'dme-modal-content'; Object.assign(modalContent.style, { backgroundColor: '#2F3136', borderRadius: '8px', padding: '16px', maxWidth: '90%', maxHeight: '90%', overflowY: 'auto', color: '#FFFFFF', boxSizing: 'border-box' }); const modalHeader = document.createElement('div'); Object.assign(modalHeader.style, { display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '12px' }); const headerTitle = document.createElement('h2'); headerTitle.id = 'dme-modal-title'; headerTitle.textContent = 'Media Preview (0/0)'; Object.assign(headerTitle.style, { margin: '0', fontSize: '18px' }); const closeBtn = document.createElement('button'); closeBtn.innerHTML = '✖'; Object.assign(closeBtn.style, { background: 'none', border: 'none', color: '#CCC', fontSize: '20px', cursor: 'pointer' }); closeBtn.title = 'Close'; modalHeader.appendChild(headerTitle); modalHeader.appendChild(closeBtn); const tableContainer = document.createElement('div'); tableContainer.id = 'dme-table-container'; Object.assign(tableContainer.style, { overflowX: 'auto', backgroundColor: '#2F3136' }); const table = document.createElement('table'); table.id = 'dme-media-table'; Object.assign(table.style, { width: '100%', borderCollapse: 'collapse', color: '#FFFFFF' }); const thead = document.createElement('thead'); const headerRow = document.createElement('tr'); ['#', 'Filename', 'Type', 'Include'].forEach(text => { const th = document.createElement('th'); th.textContent = text; Object.assign(th.style, { borderBottom: '2px solid #40444B', padding: '8px', textAlign: 'left', fontSize: '14px' }); headerRow.appendChild(th); }); thead.appendChild(headerRow); const tbody = document.createElement('tbody'); table.appendChild(thead); table.appendChild(tbody); tableContainer.appendChild(table); const modalFooter = document.createElement('div'); Object.assign(modalFooter.style, { marginTop: '16px', textAlign: 'right' }); // Download ZIP button const downloadSelectedBtn = document.createElement('button'); downloadSelectedBtn.textContent = 'Download Selected Media'; Object.assign(downloadSelectedBtn.style, { padding: '8px 14px', backgroundColor: '#43B581', color: '#FFFFFF', border: 'none', borderRadius: '4px', cursor: 'pointer', fontSize: '14px', marginRight: '8px' }); // Copy aria2c list button const copyAria2cBtn = document.createElement('button'); copyAria2cBtn.textContent = 'Copy aria2c List'; Object.assign(copyAria2cBtn.style, { padding: '8px 14px', backgroundColor: '#7289DA', color: '#FFFFFF', border: 'none', borderRadius: '4px', cursor: 'pointer', fontSize: '14px' }); modalFooter.appendChild(downloadSelectedBtn); modalFooter.appendChild(copyAria2cBtn); modalContent.appendChild(modalHeader); modalContent.appendChild(tableContainer); modalContent.appendChild(modalFooter); modalOverlay.appendChild(modalContent); document.body.appendChild(modalOverlay); ////////////////////////////////////////////////////// // 3. BUILD THE STATUS MODAL (Success/Failure + Aria2c example) ////////////////////////////////////////////////////// const statusOverlay = document.createElement('div'); statusOverlay.id = 'dme-status-overlay'; Object.assign(statusOverlay.style, { position: 'fixed', top: '0', left: '0', width: '100%', height: '100%', backgroundColor: 'rgba(0,0,0,0.75)', display: 'none', justifyContent: 'center', alignItems: 'center', zIndex: 3000, overflowY: 'auto' }); const statusContent = document.createElement('div'); statusContent.id = 'dme-status-content'; Object.assign(statusContent.style, { backgroundColor: '#2F3136', borderRadius: '8px', padding: '16px', maxWidth: '450px', color: '#FFFFFF', boxSizing: 'border-box', textAlign: 'left' }); const statusTextarea = document.createElement('textarea'); statusTextarea.readOnly = true; Object.assign(statusTextarea.style, { width: '100%', height: '150px', backgroundColor: '#1E1F22', color: '#FFFFFF', border: '1px solid #40444B', borderRadius: '4px', padding: '8px', fontSize: '14px', resize: 'vertical', boxSizing: 'border-box', whiteSpace: 'pre-wrap' }); const statusOkBtn = document.createElement('button'); statusOkBtn.textContent = 'OK'; Object.assign(statusOkBtn.style, { padding: '8px 14px', backgroundColor: '#43B581', color: '#FFFFFF', border: 'none', borderRadius: '4px', cursor: 'pointer', fontSize: '14px', display: 'block', margin: '12px auto 0' }); statusContent.appendChild(statusTextarea); statusContent.appendChild(statusOkBtn); statusOverlay.appendChild(statusContent); document.body.appendChild(statusOverlay); ////////////////////////////////////////////////////// // 4. UTILITIES ////////////////////////////////////////////////////// function sleep(ms) { return new Promise(resolve => setTimeout(resolve, ms)); } function getMessageContainer() { return document.querySelector('[aria-label="Messages"]'); } async function loadAllMessages() { const container = getMessageContainer(); if (!container) return; await sleep(500); let previousHeight = -1; for (let i = 0; i < 50; i++) { const currentHeight = container.scrollHeight; if (currentHeight === previousHeight) break; previousHeight = currentHeight; container.scrollTop = 0; await sleep(1000); } await sleep(2000); } function collectMediaLinks() { const urls = new Set(); function sanitize(raw) { return raw.replace(/[&;]+$/,''); } document.querySelectorAll('a').forEach(a => { const href = a.getAttribute('href'); if (href && href.includes('cdn.discordapp.com/attachments')) { const full = href.split('?')[0] + (href.includes('?') ? href.slice(href.indexOf('?')) : ''); urls.add(sanitize(full)); } }); document.querySelectorAll('video').forEach(v => { const src = v.getAttribute('src'); if (src && src.includes('cdn.discordapp.com/attachments')) { const full = src.split('?')[0] + (src.includes('?') ? src.slice(src.indexOf('?')) : ''); urls.add(sanitize(full)); } }); document.querySelectorAll('source').forEach(sourceElem => { const src = sourceElem.getAttribute('src'); if (src && src.includes('cdn.discordapp.com/attachments')) { const full = src.split('?')[0] + (src.includes('?') ? src.slice(src.indexOf('?')) : ''); urls.add(sanitize(full)); } }); document.querySelectorAll('img').forEach(img => { const src = img.getAttribute('src'); if (src && src.includes('cdn.discordapp.com/attachments')) { const full = src.split('?')[0] + (src.includes('?') ? src.slice(src.indexOf('?')) : ''); urls.add(sanitize(full)); } }); return Array.from(urls); } function isImageUrl(url) { return /\.(jpe?g|png|webp|gif)(?:\?|$)/i.test(url); } function isVideoUrl(url) { return /\.(mp4|webm|mov)(?:\?|$)/i.test(url); } function updateHeaderCount(selectedCount, totalCount) { headerTitle.textContent = `Media Preview (${selectedCount}/${totalCount})`; } function openModal() { modalOverlay.style.display = 'flex'; } function closeModal() { modalOverlay.style.display = 'none'; tbody.innerHTML = ''; } function openStatus(messageText) { statusTextarea.value = messageText; statusOverlay.style.display = 'flex'; statusTextarea.select(); } function closeStatus() { statusOverlay.style.display = 'none'; } // Promisified GM_xmlhttpRequest to fetch ArrayBuffer function gmFetchArrayBuffer(url) { return new Promise((resolve, reject) => { GM_xmlhttpRequest({ method: 'GET', url: url, responseType: 'arraybuffer', onload: res => { if (res.status >= 200 && res.status < 300 && res.response) { resolve(res.response); } else { reject(new Error(`Status ${res.status}`)); } }, onerror: err => reject(err), ontimeout: () => reject(new Error('Timeout')), }); }); } ////////////////////////////////////////////////////// // 5. DOWNLOAD LOGIC WITH STATUS ////////////////////////////////////////////////////// async function downloadCheckedMedia() { const checkedRows = Array.from(tbody.querySelectorAll('tr')).filter(tr => { const checkbox = tr.querySelector('input[type="checkbox"]'); return checkbox && checkbox.checked; }); if (checkedRows.length === 0) { openStatus('No files selected to download.'); return; } const zip = new JSZip(); let successCount = 0; let failureCount = 0; downloadSelectedBtn.disabled = true; downloadSelectedBtn.textContent = `⏳ Zipping 0/${checkedRows.length}…`; for (let i = 0; i < checkedRows.length; i++) { const row = checkedRows[i]; const url = row.dataset.url; try { const arrBuf = await gmFetchArrayBuffer(url); const blob = new Blob([arrBuf]); const filename = new URL(url).pathname.split('/').pop(); zip.file(filename, blob); successCount++; } catch (err) { console.warn('[DME] Failed to fetch', url, err); failureCount++; } downloadSelectedBtn.textContent = `⏳ Zipping ${Math.min(successCount + failureCount, checkedRows.length)}/${checkedRows.length}…`; } if (successCount > 0) { const zipBlob = await zip.generateAsync({ type: 'blob' }); saveAs(zipBlob, 'discord_media.zip'); openStatus( `✅ ${successCount} file(s) downloaded successfully, ${failureCount} failed. — Example aria2c command — aria2c --input-file="./urls.txt" \\ --dir="./discord_media" \\ --max-concurrent-downloads=5 \\ --split=4 \\ --max-connection-per-server=4 \\ --header="Referer: https://discord.com/" \\ -c` ); } else { openStatus('No files could be downloaded. Please check your network or permissions.'); } downloadSelectedBtn.textContent = 'Download Selected Media'; downloadSelectedBtn.disabled = false; closeModal(); } ////////////////////////////////////////////////////// // 6. COPY ARIA2C LIST LOGIC ////////////////////////////////////////////////////// function copyAria2cList() { const rows = Array.from(tbody.querySelectorAll('tr')); if (rows.length === 0) { openStatus('No media to list.'); return; } let lines = ''; rows.forEach(tr => { const url = tr.dataset.url; const filename = new URL(url).pathname.split('/').pop(); lines += `${url}\n out=${filename}\n`; }); navigator.clipboard.writeText(lines.trim()) .then(() => { openStatus( `✅ Copied ${rows.length} URLs to clipboard. — Example aria2c command — aria2c --input-file="./urls.txt" \\ --dir="./discord_media" \\ --max-concurrent-downloads=5 \\ --split=4 \\ --max-connection-per-server=4 \\ --header="Referer: https://discord.com/" \\ -c` ); }) .catch(() => openStatus('Failed to copy to clipboard.')); } ////////////////////////////////////////////////////// // 7. EVENT BINDINGS ////////////////////////////////////////////////////// exportBtn.addEventListener('click', async () => { exportBtn.disabled = true; exportBtn.textContent = '⏳ Loading messages…'; await loadAllMessages(); exportBtn.textContent = '⏳ Collecting media links…'; const mediaLinks = collectMediaLinks(); if (mediaLinks.length === 0) { openStatus('No media found in this channel.'); exportBtn.textContent = '📥 Export Media'; exportBtn.disabled = false; return; } tbody.innerHTML = ''; const totalCount = mediaLinks.length; let selectedCount = totalCount; updateHeaderCount(selectedCount, totalCount); mediaLinks.forEach((url, index) => { const tr = document.createElement('tr'); tr.dataset.url = url; const tdIndex = document.createElement('td'); tdIndex.textContent = (index + 1).toString(); Object.assign(tdIndex.style, { borderBottom: '1px solid #40444B', padding: '6px 8px', fontSize: '14px', width: '40px' }); const tdFilename = document.createElement('td'); tdFilename.textContent = new URL(url).pathname.split('/').pop(); Object.assign(tdFilename.style, { borderBottom: '1px solid #40444B', padding: '6px 8px', fontSize: '14px' }); const tdType = document.createElement('td'); tdType.textContent = isImageUrl(url) ? 'Image' : (isVideoUrl(url) ? 'Video' : 'Unknown'); Object.assign(tdType.style, { borderBottom: '1px solid #40444B', padding: '6px 8px', fontSize: '14px', width: '80px' }); const tdCheck = document.createElement('td'); Object.assign(tdCheck.style, { borderBottom: '1px solid #40444B', padding: '6px 8px', textAlign: 'center', width: '60px' }); const checkbox = document.createElement('input'); checkbox.type = 'checkbox'; checkbox.checked = true; checkbox.addEventListener('change', () => { selectedCount = Array.from(tbody.querySelectorAll('input[type="checkbox"]')) .filter(cb => cb.checked).length; updateHeaderCount(selectedCount, totalCount); }); tdCheck.appendChild(checkbox); tr.appendChild(tdIndex); tr.appendChild(tdFilename); tr.appendChild(tdType); tr.appendChild(tdCheck); tbody.appendChild(tr); }); openModal(); exportBtn.textContent = '📥 Export Media'; exportBtn.disabled = false; downloadSelectedBtn.onclick = async () => { await downloadCheckedMedia(); }; copyAria2cBtn.onclick = () => { copyAria2cList(); }; }); closeBtn.addEventListener('click', closeModal); modalOverlay.addEventListener('click', e => { if (e.target === modalOverlay) closeModal(); }); statusOkBtn.addEventListener('click', closeStatus); statusOverlay.addEventListener('click', e => { if (e.target === statusOverlay) closeStatus(); }); })();