图片视频抓取工具 分页列表自动去重 原图链接背景图抓取 图片批量ZIP下载 MP4批量下载 支持blob m3u8视频 脚本信息分页显示
// ==UserScript==
// @name Page Crawling
// @namespace http://tampermonkey.net/
// @version 1.5.9
// @description 图片视频抓取工具 分页列表自动去重 原图链接背景图抓取 图片批量ZIP下载 MP4批量下载 支持blob m3u8视频 脚本信息分页显示
// @author 苳:-)
// @match *://*/*
// @grant GM_xmlhttpRequest
// @grant GM_download
// @connect *
// @require https://cdnjs.cloudflare.com/ajax/libs/jszip/3.10.1/jszip.min.js
// ==/UserScript==
(function() {
'use strict';
if(document.querySelector('#page-crawling-panel')) return;
const SCRIPT_VERSION = '1.5.9';
const STORAGE_KEY_ITEMS = 'ClipboardTool_Global_Items';
const MAX_ITEMS = 1000;
const MAX_FETCH_IMAGES = 200;
const PAGE_SIZE = 20;
const MP4_CONFIG = {
MAX_CONCURRENT: 2,
RETRY_LIMIT: 2,
TIMEOUT: 30000
};
const IMAGE_CONFIG = {
MAX_CONCURRENT: 2,
TIMEOUT: 10000,
MAX_SIZE: 10 * 1024 * 1024,
BATCH_SIZE: 5,
MAX_TOTAL_SIZE: 100 * 1024 * 1024
};
let downloadingCount = 0;
let downloadQueue = [];
let imageDownloadQueue = [];
let imageDownloadingCount = 0;
const config = {
author: '苳:-)',
contacts: [
{ name: 'Weibo', url: 'https://weibo.com/u/2809762605' },
{ name: 'Twitter', url: 'https://x.com/nkvvo?s=21' }
]
};
let items = [];
try { items = JSON.parse(localStorage.getItem(STORAGE_KEY_ITEMS) || '[]'); } catch(e){ items=[]; }
function saveItems(){ localStorage.setItem(STORAGE_KEY_ITEMS, JSON.stringify(items)); }
function toOriginalUrl(url){
if(!url) return url;
try {
const u = new URL(url);
['w','h','width','height','size','quality','x-oss-process'].forEach(p=>u.searchParams.delete(p));
url = u.toString();
} catch(e){}
url = url.replace(/(_small|_thumb|_mini|-\d+x\d+|!thumb|_webp)/ig,'');
url = url.replace(/#.*$/,'').replace(/(\?|&).*$/,'');
return url;
}
function isDuplicateImage(url){
const normalized = toOriginalUrl(url).replace(/^https?:\/\/[^\/]+/i, '');
return items.some(i=>{
if(i.type!=='image') return false;
const existing = toOriginalUrl(i.content).replace(/^https?:\/\/[^\/]+/i, '');
return existing === normalized;
});
}
function addItem(type, content, page){
if(type==='image' && isDuplicateImage(content)) return;
const id = crypto.randomUUID();
if(items.length>=MAX_ITEMS) items.shift();
items.push({id,type,content,page:page||null,time:Date.now()});
saveItems();
}
function createModule(title){
const module = document.createElement('div');
module.style.cssText = 'margin-bottom:8px;border-radius:12px;box-shadow:0 4px 14px rgba(0,0,0,0.15);overflow:hidden;font-family:"Segoe UI",sans-serif;';
const header = document.createElement('div');
header.style.cssText = 'background: linear-gradient(135deg,#4a90e2,#357ABD);color:white;padding:8px 12px;cursor:pointer;font-weight:bold;user-select:none;';
header.innerText = title + ' ▼';
const content = document.createElement('div');
content.style.cssText = 'background:#f0f4f8;padding:10px;display:none;max-height:400px;overflow:auto;border-top:1px solid #d1dce6;';
header.addEventListener('click', ()=>{
if(content.style.display==='none'){ content.style.display='block'; header.innerText = title+' ▲'; }
else { content.style.display='none'; header.innerText = title+' ▼'; }
});
module.appendChild(header);
module.appendChild(content);
return {module, content};
}
async function ensureJSZip() {
if (window.JSZip) return window.JSZip;
for (let i = 0; i < 30; i++) {
if (window.JSZip) return window.JSZip;
await new Promise(r => setTimeout(r, 100));
}
throw new Error('JSZip加载超时');
}
async function downloadImageWithGM(url, index, total) {
return new Promise((resolve) => {
if (typeof GM_xmlhttpRequest !== 'undefined') {
GM_xmlhttpRequest({
method: "GET",
url: url,
responseType: "blob",
timeout: IMAGE_CONFIG.TIMEOUT,
headers: {
"Referer": location.href,
"Origin": location.origin
},
onload: function(res) {
if (res.status === 200 || res.status === 206) {
const blob = res.response;
if (blob && blob.size > 0 && blob.size <= IMAGE_CONFIG.MAX_SIZE) {
resolve({ success: true, blob, url });
} else {
resolve({ success: false, url, error: '文件无效或过大' });
}
} else {
resolve({ success: false, url, error: `HTTP ${res.status}` });
}
},
onerror: () => resolve({ success: false, url, error: '网络错误' }),
ontimeout: () => resolve({ success: false, url, error: '超时' })
});
} else {
fetch(url, { mode: 'cors', cache: 'force-cache', headers: { 'Referer': location.href } })
.then(res => res.blob())
.then(blob => {
if (blob.size > 0 && blob.size <= IMAGE_CONFIG.MAX_SIZE) {
resolve({ success: true, blob, url });
} else {
resolve({ success: false, url, error: '文件无效或过大' });
}
})
.catch(() => resolve({ success: false, url, error: '获取失败' }));
}
});
}
async function downloadAllImages() {
const imageItems = items.filter(i => i.type === 'image');
if (imageItems.length === 0) { alert("没有可下载的图片"); return; }
if (imageItems.length > 50 && !confirm(`共有 ${imageItems.length} 张图片,打包下载可能需要较长时间,是否继续?`)) return;
const progressDiv = document.createElement('div');
progressDiv.style.cssText = `position:fixed; top:50%; left:50%; transform:translate(-50%,-50%);background:#333; color:white; padding:25px; border-radius:15px;z-index:10000; box-shadow:0 4px 20px rgba(0,0,0,0.5);min-width:300px; text-align:center; font-family:monospace;`;
progressDiv.innerHTML = `<div style="margin-bottom:15px; font-size:18px; font-weight:bold;">图片打包中</div><div id="zip-progress-status" style="margin-bottom:10px;">准备下载...</div><div style="background:#555; height:20px; border-radius:10px; overflow:hidden;"><div id="zip-progress-bar" style="background:#4CAF50; width:0%; height:100%; transition:width 0.3s;"></div></div><div id="zip-progress-detail" style="margin-top:10px; font-size:12px; color:#ccc;">0/${imageItems.length}</div><div style="margin-top:15px;"><button id="zip-cancel-btn" style="background:#f44336; border:none; color:white; padding:5px 15px; border-radius:5px; cursor:pointer;">取消</button></div>`;
document.body.appendChild(progressDiv);
const statusEl = document.getElementById('zip-progress-status');
const barEl = document.getElementById('zip-progress-bar');
const detailEl = document.getElementById('zip-progress-detail');
const cancelBtn = document.getElementById('zip-cancel-btn');
let cancelled = false;
cancelBtn.onclick = () => { cancelled = true; progressDiv.remove(); alert('已取消下载'); };
try {
statusEl.innerText = '加载ZIP库...';
const JSZip = await ensureJSZip();
if (cancelled) return;
const zip = new JSZip();
const folderName = `images_${new Date().toISOString().slice(0,10)}`;
const imgFolder = zip.folder(folderName);
const successfulDownloads = [];
const failedDownloads = [];
const batchSize = IMAGE_CONFIG.BATCH_SIZE;
let processed = 0;
for (let i = 0; i < imageItems.length; i += batchSize) {
if (cancelled) { progressDiv.remove(); return; }
const batch = imageItems.slice(i, i + batchSize);
statusEl.innerText = `下载图片 ${i + 1}-${Math.min(i + batchSize, imageItems.length)}/${imageItems.length}`;
const batchPromises = batch.map(async (item, batchIndex) => {
const globalIndex = i + batchIndex;
const url = toOriginalUrl(item.content);
let ext = 'jpg';
try {
const urlExt = url.split('.').pop().split('?')[0];
if (urlExt && urlExt.match(/^(jpg|jpeg|png|gif|webp|bmp|svg)$/i)) ext = urlExt.toLowerCase();
} catch(e) {}
const filename = `image_${globalIndex + 1}.${ext}`;
const result = await downloadImageWithGM(url, globalIndex, imageItems.length);
if (result.success) { imgFolder.file(filename, result.blob); successfulDownloads.push(filename); }
else { failedDownloads.push(url); console.log(`下载失败: ${url}`, result.error); }
processed++;
const percent = Math.round((processed / imageItems.length) * 100);
barEl.style.width = percent + '%';
detailEl.innerText = `${processed}/${imageItems.length}`;
});
await Promise.allSettled(batchPromises);
await new Promise(r => setTimeout(r, 100));
}
if (cancelled) { progressDiv.remove(); return; }
if (successfulDownloads.length === 0) { progressDiv.remove(); alert('所有图片下载失败'); return; }
statusEl.innerText = '正在生成ZIP文件...';
barEl.style.width = '90%';
const zipContent = await new Promise((resolve, reject) => {
zip.generateAsync({ type: 'blob', compression: 'DEFLATE', compressionOptions: { level: 1 } }, (metadata) => {
if (metadata.percent) {
const newProgress = 90 + Math.round(metadata.percent * 0.1);
barEl.style.width = newProgress + '%';
statusEl.innerText = `压缩中 ${Math.round(metadata.percent)}%`;
}
}).then(resolve).catch(reject);
});
if (cancelled) { progressDiv.remove(); return; }
statusEl.innerText = '下载ZIP文件...';
barEl.style.width = '100%';
const zipUrl = URL.createObjectURL(zipContent);
const a = document.createElement('a');
a.href = zipUrl; a.download = `${folderName}.zip`;
document.body.appendChild(a); a.click();
setTimeout(() => { document.body.removeChild(a); URL.revokeObjectURL(zipUrl); progressDiv.remove(); }, 1000);
alert(`下载完成!\n成功: ${successfulDownloads.length} 张\n失败: ${failedDownloads.length} 张`);
} catch (error) {
console.error("打包失败:", error);
progressDiv.remove();
if (confirm('打包失败,是否改用逐张下载?')) downloadImagesSequential(imageItems);
}
}
async function downloadImagesSequential(imageItems) {
const progressDiv = document.createElement('div');
progressDiv.style.cssText = 'position:fixed;top:50%;left:50%;transform:translate(-50%,-50%);background:#333;color:white;padding:20px;border-radius:10px;z-index:10000;';
document.body.appendChild(progressDiv);
let success = 0, failed = 0;
for (let i = 0; i < imageItems.length; i++) {
const item = imageItems[i];
const url = toOriginalUrl(item.content);
progressDiv.innerHTML = `逐张下载中... ${i+1}/${imageItems.length}<br>成功:${success} 失败:${failed}`;
try {
const result = await downloadImageWithGM(url, i, imageItems.length);
if (result.success) {
const ext = url.split('.').pop().split('?')[0] || 'jpg';
const blobUrl = URL.createObjectURL(result.blob);
const a = document.createElement('a');
a.href = blobUrl; a.download = `image_${i+1}_${Date.now()}.${ext}`;
document.body.appendChild(a); a.click(); document.body.removeChild(a);
URL.revokeObjectURL(blobUrl); success++;
} else failed++;
} catch (e) { failed++; }
await new Promise(r => setTimeout(r, 300));
}
progressDiv.remove();
alert(`逐张下载完成\n成功: ${success} 张\n失败: ${failed} 张`);
}
function sleep(ms) { return new Promise(r => setTimeout(r, ms)); }
function extractMp4Urls() {
const urls = new Set();
document.querySelectorAll("video, source").forEach(el => {
const src = el.src || el.getAttribute("src");
if (src && src.includes(".mp4") && !src.startsWith("blob:")) urls.add(src.split("?")[0]);
});
document.querySelectorAll("a[href*='.mp4']").forEach(a => {
if (a.href && !a.href.startsWith('blob:')) urls.add(a.href.split("?")[0]);
});
document.querySelectorAll("*").forEach(el => {
const bg = getComputedStyle(el).backgroundImage;
if (bg && bg.includes(".mp4")) {
const match = bg.match(/url\(["']?(.*?)["']?\)/);
if (match && match[1]) urls.add(match[1].split("?")[0]);
}
});
return [...urls];
}
async function downloadWithGM(url, filename, retry = 0) {
return new Promise((resolve, reject) => {
if (typeof GM_xmlhttpRequest === 'undefined') {
const a = document.createElement('a'); a.href = url; a.download = filename;
document.body.appendChild(a); a.click(); document.body.removeChild(a);
resolve(); return;
}
GM_xmlhttpRequest({
method: "GET", url, responseType: "blob", timeout: MP4_CONFIG.TIMEOUT,
headers: { "Referer": location.href, "Origin": location.origin, "Range": "bytes=0-" },
onload: function (res) {
if (res.status === 200 || res.status === 206) {
const blob = res.response;
if (!blob || blob.size === 0) { reject("Empty file"); return; }
const blobUrl = URL.createObjectURL(blob);
const a = document.createElement("a"); a.href = blobUrl; a.download = filename;
document.body.appendChild(a); a.click(); a.remove();
setTimeout(() => URL.revokeObjectURL(blobUrl), 10000);
resolve();
} else reject("HTTP " + res.status);
},
onerror: reject, ontimeout: () => reject("Timeout")
});
}).catch(async err => {
if (retry < MP4_CONFIG.RETRY_LIMIT) {
console.log(`[MP4下载] 重试 ${retry + 1}/${MP4_CONFIG.RETRY_LIMIT}:`, url);
await sleep(1000 * (retry + 1));
return downloadWithGM(url, filename, retry + 1);
} else { console.error("[MP4下载] 失败:", url, err); throw err; }
});
}
async function processDownloadQueue() {
if (downloadingCount >= MP4_CONFIG.MAX_CONCURRENT || downloadQueue.length === 0) return;
downloadingCount++;
const task = downloadQueue.shift();
try { await task(); } catch (error) { console.error("[MP4下载] 任务执行失败:", error); }
downloadingCount--;
processDownloadQueue();
}
function queueDownload(url, filename) {
downloadQueue.push(() => downloadWithGM(url, filename));
processDownloadQueue();
}
async function downloadAllMp4() {
const urls = extractMp4Urls();
if (urls.length === 0) { alert("未发现可下载的MP4视频"); return; }
let newCount = 0;
urls.forEach(url => {
if (!items.some(i => i.type === 'video' && i.content === url)) {
addItem('video', url, location.href); newCount++;
}
});
urls.forEach((url, index) => {
const filename = `video_${Date.now()}_${index + 1}.mp4`;
queueDownload(url, filename);
});
alert(`已开始下载 ${urls.length} 个MP4视频\n并发 ${MP4_CONFIG.MAX_CONCURRENT} 个 失败重试 ${MP4_CONFIG.RETRY_LIMIT} 次`);
if (typeof renderVideoList === 'function') renderVideoList();
}
const panel = document.createElement('div');
panel.id = 'page-crawling-panel';
panel.style.cssText = `position:fixed; top:12px; right:12px;background: linear-gradient(135deg,#2b5d9e,#1b3f6c);color:white; padding:8px 14px; border-radius:12px;box-shadow:0 6px 16px rgba(0,0,0,0.3); cursor:pointer;z-index:9999; font-family:"Segoe UI",sans-serif; font-size:13px;`;
panel.innerText = 'Page Crawling 展开 ▼';
const closeBtn = document.createElement('span');
closeBtn.textContent = '×';
closeBtn.style.cssText = 'margin-left:8px;font-weight:bold;cursor:pointer;color:#ff4d4f;';
closeBtn.onclick = (e)=>{ e.stopPropagation(); panel.style.display='none'; infoPanel.style.display='none'; };
panel.appendChild(closeBtn);
document.body.appendChild(panel);
const infoPanel = document.createElement('div');
infoPanel.style.cssText = `position:fixed; top:50px; right:12px;background:#e9edf3; color:#333; padding:10px;border-radius:12px; font-size:13px;box-shadow:0 6px 16px rgba(0,0,0,0.25);display:none; z-index:9999; width:360px;max-height:600px; overflow:auto;`;
document.body.appendChild(infoPanel);
panel.addEventListener('click', ()=>{
if(infoPanel.style.display==='none'){ infoPanel.style.display='block'; panel.firstChild.textContent='Page Crawling 折叠 ▲'; }
else { infoPanel.style.display='none'; panel.firstChild.textContent='Page Crawling 展开 ▼'; }
});
infoPanel.innerHTML = `
<div style="padding:6px;margin-bottom:6px;background:#d9e1f2;border-radius:8px;font-weight:bold;text-align:center;color:#1b3f6c;">Page Crawling v${SCRIPT_VERSION}</div>
<div style="padding:6px;margin-bottom:6px;background:#f6f6f8;border-radius:8px;font-weight:bold;text-align:center;color:#222;">作者: ${config.author}</div>
<div style="padding:6px;margin-bottom:6px;background:#f0f4f8;border-radius:8px;font-weight:bold;color:#444;">联系方式:${config.contacts.map(c=>`<a href="${c.url}" target="_blank" style="color:#357ABD;text-decoration:none;margin-left:6px;">${c.name}</a>`).join('')}</div>`;
console.log(`[Page Crawling v${SCRIPT_VERSION}] 已启动 支持图片打包下载`);
function renderModule(type){
const moduleTitle = type==='image'?'图片':'视频';
const {module, content} = createModule(moduleTitle);
infoPanel.appendChild(module);
let currentPage = 1;
const pageSize = PAGE_SIZE;
const searchInput = document.createElement('input');
searchInput.placeholder = `搜索${moduleTitle}`;
searchInput.style.cssText='width:100%;margin-bottom:6px;padding:4px;border:1px solid #c0cbd5;border-radius:4px;';
content.appendChild(searchInput);
const buttonContainer = document.createElement('div');
buttonContainer.style.cssText = 'display:flex;gap:4px;margin-bottom:6px;flex-wrap:wrap;';
const fetchBtn = document.createElement('button');
fetchBtn.textContent = type==='image'?'抓取图片':'抓取视频';
fetchBtn.style.cssText='flex:1;min-width:80px;padding:6px;background:#17a2b8;color:white;border:none;border-radius:5px;cursor:pointer;';
buttonContainer.appendChild(fetchBtn);
if (type === 'video') {
const downloadAllBtn = document.createElement('button');
downloadAllBtn.textContent = '下载MP4';
downloadAllBtn.style.cssText = 'flex:1;min-width:80px;padding:6px;background:#ff5500;color:white;border:none;border-radius:5px;cursor:pointer;';
downloadAllBtn.onclick = downloadAllMp4;
buttonContainer.appendChild(downloadAllBtn);
}
if (type === 'image') {
const downloadAllBtn = document.createElement('button');
downloadAllBtn.textContent = '打包下载';
downloadAllBtn.style.cssText = 'flex:1;min-width:80px;padding:6px;background:#28a745;color:white;border:none;border-radius:5px;cursor:pointer;';
downloadAllBtn.onclick = downloadAllImages;
buttonContainer.appendChild(downloadAllBtn);
}
content.appendChild(buttonContainer);
const clearBtn = document.createElement('button');
clearBtn.textContent = type === 'image' ? '清空图片' : '清空视频';
clearBtn.style.cssText='margin-bottom:6px;width:100%;padding:6px;background:#dc3545;color:white;border:none;border-radius:5px;cursor:pointer;';
clearBtn.onclick = ()=>{
if(confirm(type==='image'?'确认清空全部图片':'确认清空全部视频')){
items = items.filter(i=>i.type!==type);
saveItems(); currentPage=1; renderList();
}
};
content.appendChild(clearBtn);
const listEl = document.createElement('div');
content.appendChild(listEl);
const paginationEl = document.createElement('div');
paginationEl.style.cssText='text-align:center;margin-top:6px;';
content.appendChild(paginationEl);
function renderList(){
listEl.innerHTML='';
const query = searchInput.value.trim().toLowerCase();
const filtered = items.filter(i=>i.type===type && (!query || i.content.toLowerCase().includes(query))).sort((a,b)=>b.time - a.time);
const totalPages = Math.max(1, Math.ceil(filtered.length/pageSize));
if(currentPage>totalPages) currentPage=totalPages;
const pageItems = filtered.slice((currentPage-1)*pageSize, currentPage*pageSize);
pageItems.forEach(item=>{
const el = document.createElement('div');
el.style.cssText='background:#f7f9fc;padding:6px;border-radius:6px;margin-bottom:6px;';
if(type==='image'){
const img = document.createElement('img');
img.src = toOriginalUrl(item.content);
img.style.cssText='width:100%;max-height:120px;object-fit:cover;border-radius:4px;margin-bottom:4px;';
el.appendChild(img);
}
if(type==='video'){
const video = document.createElement('video');
video.src=item.content; video.controls=true;
video.style.cssText='width:100%;max-height:180px;border-radius:6px;margin-bottom:4px;';
el.appendChild(video);
if(item.content.endsWith('.m3u8')){
const hint = document.createElement('div');
hint.textContent = '⚠ m3u8需专用工具下载';
hint.style.cssText='color:#d9534f;font-size:12px;margin-bottom:4px;';
el.appendChild(hint);
}
if(item.content.startsWith('blob:')){
const hintBlob = document.createElement('div');
hintBlob.textContent = '⚠ blob视频仅当前页面有效';
hintBlob.style.cssText='color:#d9534f;font-size:12px;margin-bottom:4px;';
el.appendChild(hintBlob);
}
}
const btnRow = document.createElement('div');
btnRow.style.cssText='display:flex;gap:4px;flex-wrap:wrap;';
const copyBtn = document.createElement('button');
copyBtn.textContent='复制';
copyBtn.style.cssText='flex:1;min-width:50px;padding:4px;background:#357ABD;color:white;border:none;border-radius:4px;cursor:pointer;';
copyBtn.onclick=()=>navigator.clipboard.writeText(item.content);
btnRow.appendChild(copyBtn);
const openBtn = document.createElement('button');
openBtn.textContent='打开';
openBtn.style.cssText='flex:1;min-width:50px;padding:4px;background:#17a2b8;color:white;border:none;border-radius:4px;cursor:pointer;';
openBtn.onclick = ()=>window.open(toOriginalUrl(item.content),'_blank');
btnRow.appendChild(openBtn);
const downloadBtn = document.createElement('button');
downloadBtn.textContent='下载';
downloadBtn.style.cssText='flex:1;min-width:50px;padding:4px;background:#6c757d;color:white;border:none;border-radius:4px;cursor:pointer;';
downloadBtn.onclick = async ()=>{
const url = toOriginalUrl(item.content);
if (type === 'image') {
const ext = url.split('.').pop().split('?')[0] || 'jpg';
const filename = `image_${Date.now()}.${ext}`;
downloadBtn.textContent = '下载中'; downloadBtn.disabled = true;
try {
const result = await downloadImageWithGM(url);
if (result.success) {
const blobUrl = URL.createObjectURL(result.blob);
const a = document.createElement('a');
a.href = blobUrl; a.download = filename;
document.body.appendChild(a); a.click(); document.body.removeChild(a);
URL.revokeObjectURL(blobUrl); downloadBtn.textContent = '完成';
} else { downloadBtn.textContent = '重试'; alert('下载失败'); }
} catch (error) { downloadBtn.textContent = '重试'; alert('下载失败'); }
downloadBtn.disabled = false;
setTimeout(() => { downloadBtn.textContent = '下载'; }, 2000);
return;
}
if (url.includes('.mp4')) {
const filename = `video_${Date.now()}.mp4`;
downloadBtn.textContent = '下载中'; downloadBtn.disabled = true;
try {
await downloadWithGM(url, filename);
downloadBtn.textContent = '完成';
setTimeout(() => { downloadBtn.textContent = '下载'; downloadBtn.disabled = false; }, 2000);
} catch (error) { downloadBtn.textContent = '重试'; downloadBtn.disabled = false; alert('下载失败'); }
return;
}
if(url.endsWith('.m3u8')){ alert('⚠ m3u8需专用工具下载'); return; }
if(url.startsWith('blob:')){
try {
const videoEl = document.querySelector(`video[src="${url}"]`);
if(!videoEl) throw new Error('未找到视频元素');
const mediaStream = videoEl.captureStream();
const mediaRecorder = new MediaRecorder(mediaStream);
const chunks = [];
mediaRecorder.ondataavailable = e => chunks.push(e.data);
mediaRecorder.onstop = ()=>{
const blob = new Blob(chunks, {type:'video/mp4'});
const a = document.createElement('a');
a.href = URL.createObjectURL(blob); a.download = `video_${Date.now()}.mp4`;
document.body.appendChild(a); a.click(); document.body.removeChild(a);
URL.revokeObjectURL(a.href);
};
mediaRecorder.start();
setTimeout(()=>mediaRecorder.stop(), 3000);
} catch(e){ alert('blob下载失败'); }
return;
}
try {
const resp = await fetch(url, {mode:'cors'});
if(!resp.ok) throw new Error('网络错误');
const blob = await resp.blob();
const ext = url.split('.').pop().split('?')[0] || 'mp4';
const a = document.createElement('a');
a.href = URL.createObjectURL(blob); a.download = `video_${Date.now()}.${ext}`;
document.body.appendChild(a); a.click(); document.body.removeChild(a);
URL.revokeObjectURL(a.href);
} catch(e){ alert('下载失败'); }
};
btnRow.appendChild(downloadBtn);
const delBtn = document.createElement('button');
delBtn.textContent='删除';
delBtn.style.cssText='flex:1;min-width:50px;padding:4px;background:#dc3545;color:white;border:none;border-radius:4px;cursor:pointer;';
delBtn.onclick=()=>{
const idx=items.findIndex(i=>i.id===item.id);
if(idx>=0){ items.splice(idx,1); saveItems(); renderList(); }
};
btnRow.appendChild(delBtn);
el.appendChild(btnRow);
listEl.appendChild(el);
});
paginationEl.innerHTML='';
const prevBtn = document.createElement('button');
prevBtn.textContent = '上一页';
prevBtn.disabled = currentPage===1;
prevBtn.style.cssText='margin-right:4px;padding:2px 6px;';
prevBtn.onclick = ()=>{ currentPage--; renderList(); };
const nextBtn = document.createElement('button');
nextBtn.textContent = '下一页';
nextBtn.disabled = currentPage===totalPages;
nextBtn.style.cssText='margin-left:4px;padding:2px 6px;';
nextBtn.onclick = ()=>{ currentPage++; renderList(); };
const pageInfo = document.createElement('span');
pageInfo.textContent = `${currentPage}/${totalPages}`;
pageInfo.style.cssText='margin:0 6px;font-weight:bold;';
paginationEl.appendChild(prevBtn);
paginationEl.appendChild(pageInfo);
paginationEl.appendChild(nextBtn);
}
searchInput.addEventListener('input', ()=>{ currentPage=1; renderList(); });
fetchBtn.onclick = ()=>{
if(type==='image'){
const imgs = Array.from(document.querySelectorAll('img'))
.filter(img=>img.width>=80 && img.height>=80)
.filter(img=>img.src && !img.src.startsWith('data:'))
.map(img=>toOriginalUrl(img.src));
document.querySelectorAll('*').forEach(el=>{
const cls = el.className || '';
const id = el.id || '';
if(/ad|banner|overlay|float/i.test(cls+id)) return;
const bg = getComputedStyle(el).backgroundImage;
if(bg && bg.startsWith('url(')){
const url = bg.slice(4,-1).replace(/["']/g,'');
if(url && !url.startsWith('data:') && !url.match(/1x1/)) imgs.push(toOriginalUrl(url));
}
});
const finalImgs = [...new Set(imgs)].filter(url=>!url.includes('favicon')).slice(0, MAX_FETCH_IMAGES);
let count=0;
finalImgs.forEach(url=>{ if(!isDuplicateImage(url)){ addItem('image',url,location.href); count++; } });
alert(`抓取 ${count} 张图片`);
} else {
let urls = new Set();
document.querySelectorAll('video').forEach(v=>{
if(v.src) urls.add(v.src);
v.querySelectorAll('source').forEach(s=>urls.add(s.src));
});
document.querySelectorAll('a').forEach(a=>{
if(a.href.match(/\.(mp4|webm|ogg|mov|m3u8)(\?|$)/i)) urls.add(a.href);
});
extractMp4Urls().forEach(url => urls.add(url));
let count=0;
urls.forEach(url=>{ if(!items.some(i=>i.type==='video' && i.content===url)){ addItem('video',url,location.href); count++; } });
alert(`抓取 ${count} 个视频`);
}
currentPage=1; renderList();
};
renderList();
}
renderModule('image');
renderModule('video');
(function(){
const {module, content} = createModule('脚本信息')
infoPanel.appendChild(module)
content.style.display = 'none'
const SCRIPT_LOGS = [
{ver:'v1.5.9', desc:'优化ZIP生成速度 添加进度条和取消功能 降低压缩级别'},
{ver:'v1.5.8', desc:'增强JSZip加载机制 添加备选下载方案'},
{ver:'v1.5.7', desc:'修复JSZip加载失败问题 多CDN源备选'},
{ver:'v1.5.6', desc:'图片批量ZIP下载 MP4并发下载 进度显示'},
{ver:'v1.5.5', desc:'MP4下载增强 并发重试 GM_xmlhttpRequest支持'},
{ver:'v1.5.4', desc:'背景图抓取优化 Blob和m3u8视频提示'},
{ver:'v1.5.3', desc:'修复重复注入 清空按钮精准化'},
{ver:'v5.1.2', desc:'支持blob m3u8视频抓取 性能优化'},
{ver:'v1.5.1', desc:'增加搜索过滤分页功能'},
{ver:'v1.5.0', desc:'首次公开 图片视频抓取面板'},
]
const pageSize = 3
let currentPage = 1
const descBlock = document.createElement('div')
descBlock.style.cssText = 'padding:6px;margin-bottom:6px;background:#f0f4f8;border-radius:8px;color:#444'
descBlock.innerHTML = `<strong>脚本说明:</strong> 抓取网页图片视频 自动去重原图链接 背景图抓取 图片批量ZIP下载 MP4批量下载 支持blob和m3u8 分页搜索功能`
content.appendChild(descBlock)
const versionBlock = document.createElement('div')
versionBlock.style.cssText = 'padding:6px;margin-bottom:6px;background:#f6f6f8;border-radius:8px;color:#444'
versionBlock.innerHTML = `<strong>当前版本:</strong> v${SCRIPT_VERSION}`
content.appendChild(versionBlock)
const logContainer = document.createElement('div')
logContainer.style.cssText = 'padding:6px;margin-bottom:6px;background:#e9edf3;border-radius:8px;color:#444'
logContainer.innerHTML = '<strong>更新日志:</strong>'
content.appendChild(logContainer)
const paginationEl = document.createElement('div')
paginationEl.style.cssText = 'text-align:center;margin-top:4px'
content.appendChild(paginationEl)
function renderLogPage(){
const oldUL = logContainer.querySelector('ul')
if(oldUL) oldUL.remove()
const start = (currentPage-1)*pageSize
const pageItems = SCRIPT_LOGS.slice(start, start+pageSize)
const ul = document.createElement('ul')
ul.style.cssText = 'margin:4px 0 0 16px;padding:0'
pageItems.forEach(item=>{
const li = document.createElement('li')
li.innerHTML = `<strong>${item.ver}</strong> ${item.desc}`
ul.appendChild(li)
})
logContainer.appendChild(ul)
const totalPages = Math.ceil(SCRIPT_LOGS.length/pageSize)
paginationEl.innerHTML=''
const prevBtn = document.createElement('button')
prevBtn.textContent='上一页'
prevBtn.disabled=currentPage===1
prevBtn.style.cssText='margin-right:4px;padding:2px 6px'
prevBtn.onclick=()=>{currentPage--;renderLogPage()}
const nextBtn = document.createElement('button')
nextBtn.textContent='下一页'
nextBtn.disabled=currentPage===totalPages
nextBtn.style.cssText='margin-left:4px;padding:2px 6px'
nextBtn.onclick=()=>{currentPage++;renderLogPage()}
const pageInfo = document.createElement('span')
pageInfo.textContent=`${currentPage}/${totalPages}`
pageInfo.style.cssText='margin:0 6px;font-weight:bold'
paginationEl.appendChild(prevBtn)
paginationEl.appendChild(pageInfo)
paginationEl.appendChild(nextBtn)
}
renderLogPage()
})();
})();