feat: 实现批量下载功能

This commit is contained in:
meishibiezb
2026-05-05 02:47:36 +08:00
parent a5c5b0b38c
commit f037897590
3 changed files with 107 additions and 20 deletions

View File

@@ -1,3 +1,88 @@
export default defineBackground(() => {
console.log('Hello background!', { id: browser.runtime.id });
// 监听下载请求
browser.runtime.onMessage.addListener(async (msg) => {
if (msg.type === 'DOWNLOAD_PINS') {
const pins: PinData[] = msg.pins;
const concurrency: number = msg.concurrency || 3;
const result = await startBatchDownload(pins, concurrency);
// 通知 popup 下载完成
browser.runtime.sendMessage({
type: 'DOWNLOAD_DONE',
success: result.success,
failed: result.failed,
total: result.total,
});
}
});
});
// 定义单个下载任务 (封装成 Promise)
async function downloadOne(pin: PinData): Promise<void> {
return new Promise(async (resolve) => {
// 处理文件名非法字符(清理文件名)
const safeName = (pin.title || 'image').replace(/[\n\r\t]/g, ' ').replace(/[<>:"/\\|?*]/g, '').replace(/\s+/g, ' ').trim().slice(0, 100);
const filename = `Pins/${safeName}.jpg`;
browser.downloads.download({
url: pin.imgSrc,
filename: filename,
headers: [{ name: 'Referer', value: 'https://huaban.com/' }],
conflictAction: 'uniquify'
}, (id) => {
if (!id) {
console.warn(`跳过: ${pin.title} (可能 URL 无效)`);
return resolve(); // 即使失败也要 resolve否则会阻塞队列
}
// 监听下载状态,只有当下载真正完成(或中断)时才释放资源
const listener = (delta: Browser.downloads.DownloadDelta) => {
if (delta.id !== id) return;
if (delta.state && delta.state.current === 'complete') {
browser.downloads.onChanged.removeListener(listener);
resolve();
}
// 处理中断或取消的情况,防止队列卡死
else if (delta.state && delta.state.current === 'interrupted') {
browser.downloads.onChanged.removeListener(listener);
console.warn(`下载中断: ${pin.title}`, JSON.stringify(delta));
resolve();
}
};
browser.downloads.onChanged.addListener(listener);
});
});
}
// 并发调度器
export async function startBatchDownload(pins: PinData[], limit: number = 3) {
let success = 0;
let failed = 0;
const executing: Promise<void>[] = []; // 正在运行的任务池
for (const pin of pins) {
// 创建任务
const task = downloadOne(pin);
// 加入任务池
const wrapper = task.finally(() => {
const idx = executing.indexOf(wrapper);
if (idx !== -1) executing.splice(idx, 1);
});
executing.push(wrapper);
// 关键逻辑:如果池子满了(达到 limit等待任意一个任务完成
if (executing.length >= limit) {
await Promise.race(executing);
}
}
// 等待剩余任务全部完成
await Promise.all(executing);
console.log("所有下载任务已处理完毕");
return { success, failed, total: pins.length };
}

View File

@@ -1,5 +1,6 @@
import './style.css';
import type { PinData } from '../../components/pin-collector';
import { startBatchDownload } from '../background';
document.querySelector<HTMLDivElement>('#app')!.innerHTML = `
<div class="popup">
@@ -10,11 +11,6 @@ document.querySelector<HTMLDivElement>('#app')!.innerHTML = `
<button id="start-btn">▶ 开始加载</button>
<button id="stop-btn">⏹ 停止</button>
</div>
<div class="download-options">
<label>
并行数: <input type="number" id="concurrency" value="5" min="1" max="20" style="width:50px" />
</label>
</div>
<div id="status"></div>
<div id="pinList"></div>
</div>
@@ -24,19 +20,18 @@ document.querySelector<HTMLDivElement>('#app')!.innerHTML = `
document.getElementById('collectBtn')!.addEventListener('click', async () => {
showStatus('收集中...', 'info');
try {
const [tab] = await browser.tabs.query({ active: true, currentWindow: true });
if (!tab?.id) {
showStatus('无法获取当前标签页', 'error');
return;
}
const response = await browser.tabs.sendMessage(tab.id, {
type: 'COLLECT_PINS',
});
if (response?.success) {
// await browser.storage.local.set({ collectedPins: response.pins });
// renderPins(response.pins as PinData[]);
await loadAndRender();
showStatus(`✅ 已收集 ${response.count} 个 Pin`, 'success');
await loadAndRender();
// 下载
const result = (await browser.storage.local.get('collectedPins')) as {
collectedPins?: PinData[];
};
const pins: PinData[] = result.collectedPins || [];
if (pins.length > 0) {
showStatus(`开始下载 ${pins.length} 张...`, 'info');
browser.runtime.sendMessage({ type: 'DOWNLOAD_PINS', pins, concurrency: 3 });
} else {
showStatus('没有可下载的图片', 'error');
}
} catch (e) {
const errMsg = e instanceof Error ? e.message : String(e);
@@ -126,4 +121,11 @@ browser.runtime.onMessage.addListener(async (msg) => {
`✅ 加载完成 · 共 ${msg.totalItems}`;
}
}
if (msg.type === 'DOWNLOAD_DONE') {
showStatus(
`✅ 下载完成: ${msg.success}/${msg.total}` +
(msg.failed > 0 ? ` (${msg.failed} 失败)` : ''),
'success'
);
}
});

View File

@@ -2,8 +2,8 @@ import { defineConfig } from 'wxt';
// See https://wxt.dev/api/config.html
export default defineConfig({
browser: "firefox",
manifest: {
browser: "firefox",
manifest: {
permissions: ['storage', 'downloads'],
},
});