Greasy Fork is available in English.
支援下载 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.5
// @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 Hzbrrbmin + 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();
})();