您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
支援下载 Arca Live 贴文中的图片、GIF、MP4、WEBM(使用 GM_download 绕过 CORS)并自动命名为「板块_编号_0001~n」格式,快速下载、逐一下载两种模式。
// ==UserScript== // @name Arca Live Image and Video Downloader // @name:zh-TW Arca Live 圖片與影片下載器 // @name:zh-CN Arca Live 图片和视频下载器 // @namespace http://tampermonkey.net/ // @version 2.4 // @description Supports downloading images, GIFs, MP4s, and WEBMs from Arca Live posts (using GM_download to bypass CORS), with automatic filename formatting as "Board_PostID_0001~n". Offers both fast download and sequential download modes. // @description:zh-TW 支援下載 Arca Live 貼文中的圖片、GIF、MP4、WEBM(使用 GM_download 繞過 CORS)並自動命名為「板塊_編號_0001~n」格式,快速下載、逐一下載兩種模式。 // @description:zh-CN 支援下载 Arca Live 贴文中的图片、GIF、MP4、WEBM(使用 GM_download 绕过 CORS)并自动命名为「板块_编号_0001~n」格式,快速下载、逐一下载两种模式。 // @author ChatGPT // @match https://arca.live/* // @grant GM_download // @license MIT // ==/UserScript== (function () { 'use strict'; // 延遲函式,方便等待非同步流程 const sleep = ms => new Promise(resolve => setTimeout(resolve, ms)); // 等待收藏按鈕出現,作為插入下載按鈕的參考點,最多等待 10 秒(50 次 * 200ms) const waitForScrapButton = async () => { for (let i = 0; i < 50; i++) { const scrapBtn = document.querySelector('form#scrapForm > button.scrap-btn'); if (scrapBtn) return scrapBtn; await sleep(200); } return null; }; // 從網址中解析板塊名稱與貼文 ID,例如 /b/umamusume/141070493 const parseBoardInfo = () => { const match = location.pathname.match(/^\/b\/([^/]+)\/(\d+)/); if (!match) return { board: 'unknown', postId: 'unknown' }; return { board: match[1], postId: match[2] }; }; // 收集貼文中所有可下載的媒體網址(圖片與影片) const collectMediaUrls = () => { const urls = new Set(); // <img> 標籤圖片(優先取 data-src) document.querySelectorAll('.article-body img').forEach(img => { const src = img.getAttribute('data-src') || img.src; if (src) urls.add(src); }); // <video><source> 影片來源 document.querySelectorAll('.article-body video source').forEach(source => { if (source.src) urls.add(source.src); }); // <video> 直接 src 屬性 document.querySelectorAll('.article-body video').forEach(video => { if (video.src) urls.add(video.src); }); // <a> 超連結,篩選 gif / mp4 / webm 檔案 document.querySelectorAll('.article-body a[href]').forEach(a => { const href = a.href; if (/\.(gif|mp4|webm)(\?.*)?$/i.test(href)) { urls.add(href); } }); return Array.from(urls); }; // 建立快速下載模式切換按鈕,懸浮在主下載按鈕右側 // 傳入主下載按鈕元素,包裝成相對定位容器,方便同時放入兩個元素 const createFloatingFastToggle = (relativeToButton) => { const toggle = document.createElement('div'); toggle.textContent = '⚡ 快速下載模式:❌'; toggle.style.position = 'absolute'; toggle.style.left = '100%'; // 緊貼主按鈕右側 toggle.style.top = '0'; toggle.style.marginLeft = '10px'; // 按鈕間隔 toggle.style.padding = '4px 8px'; toggle.style.backgroundColor = '#343a40'; toggle.style.color = '#fff'; toggle.style.borderRadius = '6px'; toggle.style.whiteSpace = 'nowrap'; // 防止換行 toggle.style.fontSize = '12px'; toggle.style.cursor = 'pointer'; toggle.style.userSelect = 'none'; // 避免文字被選取 toggle.style.zIndex = '999'; // 置頂 let fastMode = false; // 內部狀態,預設關閉 // 點擊切換開關文字與狀態 toggle.addEventListener('click', () => { fastMode = !fastMode; toggle.textContent = `⚡ 快速下載模式:${fastMode ? '✅' : '❌'}`; }); // 建立一個相對定位的容器,包含主按鈕與切換按鈕 const wrapper = document.createElement('div'); wrapper.style.position = 'relative'; wrapper.style.display = 'inline-block'; wrapper.appendChild(relativeToButton); wrapper.appendChild(toggle); // 回傳容器與取得目前開關狀態的函式 return { wrapper, getFastMode: () => fastMode }; }; // 下載媒體函式,支援快速模式(並行下載)與逐一下載 // fastMode 為布林值,true 使用快速下載 const downloadMedia = async (urls, button, fastMode) => { const { board, postId } = parseBoardInfo(); let success = 0; if (fastMode) { // 快速模式:多個下載任務同時啟動,但每個任務之間仍維持 100ms 間隔 const downloadTasks = urls.map((url, i) => { // 解析副檔名,若無則用 bin const ext = url.split('.').pop().split('?')[0].split('#')[0] || 'bin'; // 檔名格式:板塊_貼文編號_四位數流水號.ext const filename = `${board}_${postId}_${String(i + 1).padStart(4, '0')}.${ext}`; return new Promise((resolve) => { try { GM_download({ url, name: filename, saveAs: false, onload: () => { success++; button.textContent = `下載中 (${success}/${urls.length})`; resolve(); }, onerror: (err) => { console.warn(`❌ 無法下載: ${url}`, err); resolve(); } }); } catch (e) { console.error(`❌ GM_download 錯誤: ${url}`, e); resolve(); } }).then(() => sleep(100)); // 保持間隔避免同時大量請求 }); await Promise.all(downloadTasks); button.textContent = '✅ 下載完成'; } else { // 逐一下載模式:等待一張下載完成後才下載下一張 for (let i = 0; i < urls.length; i++) { const url = urls[i]; const ext = url.split('.').pop().split('?')[0].split('#')[0] || 'bin'; const filename = `${board}_${postId}_${String(i + 1).padStart(4, '0')}.${ext}`; await new Promise((resolve) => { try { GM_download({ url, name: filename, saveAs: false, onload: () => { success++; button.textContent = `下載中 (${success}/${urls.length})`; resolve(); }, onerror: (err) => { console.warn(`❌ 無法下載: ${url}`, err); resolve(); } }); } catch (e) { console.error(`❌ GM_download 錯誤: ${url}`, e); resolve(); } }); } button.textContent = '✅ 下載完成'; } // 等待 30 秒後重置按鈕狀態(避免重複點擊衝突) setTimeout(() => { button.disabled = false; button.textContent = '📥 逐張圖片下載'; }, 30000); }; // 建立下載按鈕與快速模式切換按鈕容器 const createDownloadButtonWithToggle = () => { // 下載按鈕 const btn = document.createElement('button'); btn.textContent = '📥 逐張圖片下載'; btn.className = 'btn btn-arca btn-sm float-left mr-2'; btn.type = 'button'; // 產生包裝容器與快速模式取得函式 const { wrapper, getFastMode } = createFloatingFastToggle(btn); // 按下下載按鈕的事件 btn.addEventListener('click', async () => { btn.disabled = true; btn.textContent = '🔄 收集媒體中...'; // 收集所有媒體網址 const urls = collectMediaUrls(); if (urls.length === 0) { alert('⚠️ 找不到任何圖片或影片'); btn.disabled = false; btn.textContent = '📥 逐張圖片下載'; return; } // 依快速模式開關狀態下載 await downloadMedia(urls, btn, getFastMode()); }); return wrapper; }; // 插入按鈕組合到收藏按鈕左側 const insertButton = async () => { const scrapBtn = await waitForScrapButton(); if (!scrapBtn) return; const downloadWrapper = createDownloadButtonWithToggle(); scrapBtn.parentElement.insertBefore(downloadWrapper, scrapBtn); }; // 啟動腳本 insertButton(); })();