图片视频抓取工具,分页列表自动去重,原图链接背景图抓取,图片批量ZIP下载,MP4批量下载,支持blob、m3u8视频,脚本信息分页显示,强制保持UI配色。
// ==UserScript==
// @name Page Crawling
// @namespace http://tampermonkey.net/
// @version 1.6.0
// @description 图片视频抓取工具,分页列表自动去重,原图链接背景图抓取,图片批量ZIP下载,MP4批量下载,支持blob、m3u8视频,脚本信息分页显示,强制保持UI配色。
// @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.6.0';
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();
}
// 强制样式隔离函数:为所有生成的UI元素添加!important标记,确保不被网站样式覆盖
function createStyledElement(tag, baseStyles = {}) {
const el = document.createElement(tag);
const styleString = Object.entries(baseStyles)
.map(([key, value]) => `${key}: ${value} !important;`)
.join(' ');
el.style.cssText = styleString;
return el;
}
function createModule(title){
const module = createStyledElement('div', {
'margin-bottom': '8px',
'border-radius': '12px',
'overflow': 'hidden',
'font-family': '"Segoe UI", sans-serif',
'box-shadow': '0 4px 14px rgba(0,0,0,0.15)'
});
const header = createStyledElement('div', {
'background': 'linear-gradient(135deg,#4a90e2,#357ABD)',
'color': 'white',
'padding': '8px 12px',
'cursor': 'pointer',
'font-weight': 'bold',
'user-select': 'none'
});
header.innerText = title + ' ▼';
const content = createStyledElement('div', {
'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 = createStyledElement('div', {
'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 = createStyledElement('div', {
'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 = createStyledElement('div', {
'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.id = 'page-crawling-panel';
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 = createStyledElement('div', {
'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}] 已启动,支持图片打包下载,UI已加固。`);
function renderModule(type){
const moduleTitle = type==='image'?'图片':'视频';
const {module, content} = createModule(moduleTitle);
infoPanel.appendChild(module);
let currentPage = 1;
const pageSize = PAGE_SIZE;
const searchInput = createStyledElement('input', {
'width': '100%',
'margin-bottom': '6px',
'padding': '4px',
'border': '1px solid #c0cbd5',
'border-radius': '4px'
});
searchInput.placeholder = `搜索${moduleTitle}`;
content.appendChild(searchInput);
const buttonContainer = createStyledElement('div', {
'display': 'flex',
'gap': '4px',
'margin-bottom': '6px',
'flex-wrap': 'wrap'
});
const fetchBtn = createStyledElement('button', {
'flex': '1',
'min-width': '80px',
'padding': '6px',
'background': '#17a2b8',
'color': 'white',
'border': 'none',
'border-radius': '5px',
'cursor': 'pointer'
});
fetchBtn.textContent = type==='image'?'抓取图片':'抓取视频';
buttonContainer.appendChild(fetchBtn);
if (type === 'video') {
const downloadAllBtn = createStyledElement('button', {
'flex': '1',
'min-width': '80px',
'padding': '6px',
'background': '#ff5500',
'color': 'white',
'border': 'none',
'border-radius': '5px',
'cursor': 'pointer'
});
downloadAllBtn.textContent = '下载MP4';
downloadAllBtn.onclick = downloadAllMp4;
buttonContainer.appendChild(downloadAllBtn);
}
if (type === 'image') {
const downloadAllBtn = createStyledElement('button', {
'flex': '1',
'min-width': '80px',
'padding': '6px',
'background': '#28a745',
'color': 'white',
'border': 'none',
'border-radius': '5px',
'cursor': 'pointer'
});
downloadAllBtn.textContent = '打包下载';
downloadAllBtn.onclick = downloadAllImages;
buttonContainer.appendChild(downloadAllBtn);
}
content.appendChild(buttonContainer);
const clearBtn = createStyledElement('button', {
'margin-bottom': '6px',
'width': '100%',
'padding': '6px',
'background': '#dc3545',
'color': 'white',
'border': 'none',
'border-radius': '5px',
'cursor': 'pointer'
});
clearBtn.textContent = type === 'image' ? '清空图片' : '清空视频';
clearBtn.onclick = ()=>{
if(confirm(type==='image'?'确认清空全部图片':'确认清空全部视频')){
items = items.filter(i=>i.type!==type);
saveItems(); currentPage=1; renderList();
}
};
content.appendChild(clearBtn);
const listEl = createStyledElement('div', {});
content.appendChild(listEl);
const paginationEl = createStyledElement('div', {
'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 = createStyledElement('div', {
'background': '#f7f9fc',
'padding': '6px',
'border-radius': '6px',
'margin-bottom': '6px'
});
if(type==='image'){
const img = createStyledElement('img', {
'width': '100%',
'max-height': '120px',
'object-fit': 'cover',
'border-radius': '4px',
'margin-bottom': '4px'
});
img.src = toOriginalUrl(item.content);
el.appendChild(img);
}
if(type==='video'){
const video = createStyledElement('video', {
'width': '100%',
'max-height': '180px',
'border-radius': '6px',
'margin-bottom': '4px'
});
video.src=item.content; video.controls=true;
el.appendChild(video);
if(item.content.endsWith('.m3u8')){
const hint = createStyledElement('div', {
'color': '#d9534f',
'font-size': '12px',
'margin-bottom': '4px'
});
hint.textContent = '⚠ m3u8需专用工具下载';
el.appendChild(hint);
}
if(item.content.startsWith('blob:')){
const hintBlob = createStyledElement('div', {
'color': '#d9534f',
'font-size': '12px',
'margin-bottom': '4px'
});
hintBlob.textContent = '⚠ blob视频仅当前页面有效';
el.appendChild(hintBlob);
}
}
const btnRow = createStyledElement('div', {
'display': 'flex',
'gap': '4px',
'flex-wrap': 'wrap'
});
const copyBtn = createStyledElement('button', {
'flex': '1',
'min-width': '50px',
'padding': '4px',
'background': '#357ABD',
'color': 'white',
'border': 'none',
'border-radius': '4px',
'cursor': 'pointer'
});
copyBtn.textContent='复制';
copyBtn.onclick=()=>navigator.clipboard.writeText(item.content);
btnRow.appendChild(copyBtn);
const openBtn = createStyledElement('button', {
'flex': '1',
'min-width': '50px',
'padding': '4px',
'background': '#17a2b8',
'color': 'white',
'border': 'none',
'border-radius': '4px',
'cursor': 'pointer'
});
openBtn.textContent='打开';
openBtn.onclick = ()=>window.open(toOriginalUrl(item.content),'_blank');
btnRow.appendChild(openBtn);
const downloadBtn = createStyledElement('button', {
'flex': '1',
'min-width': '50px',
'padding': '4px',
'background': '#6c757d',
'color': 'white',
'border': 'none',
'border-radius': '4px',
'cursor': 'pointer'
});
downloadBtn.textContent='下载';
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 = createStyledElement('button', {
'flex': '1',
'min-width': '50px',
'padding': '4px',
'background': '#dc3545',
'color': 'white',
'border': 'none',
'border-radius': '4px',
'cursor': 'pointer'
});
delBtn.textContent='删除';
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 = createStyledElement('button', {
'margin-right': '4px',
'padding': '2px 6px'
});
prevBtn.textContent = '上一页';
prevBtn.disabled = currentPage===1;
prevBtn.onclick = ()=>{ currentPage--; renderList(); };
const nextBtn = createStyledElement('button', {
'margin-left': '4px',
'padding': '2px 6px'
});
nextBtn.textContent = '下一页';
nextBtn.disabled = currentPage===totalPages;
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.6.0', desc:'强制保持UI配色风格,避免被网站样式覆盖。'},
{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 = createStyledElement('div', {
'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 = createStyledElement('div', {
'padding': '6px',
'margin-bottom': '6px',
'background': '#f6f6f8',
'border-radius': '8px',
'color': '#444'
})
versionBlock.innerHTML = `<strong>当前版本:</strong> v${SCRIPT_VERSION}`
content.appendChild(versionBlock)
const logContainer = createStyledElement('div', {
'padding': '6px',
'margin-bottom': '6px',
'background': '#e9edf3',
'border-radius': '8px',
'color': '#444'
})
logContainer.innerHTML = '<strong>更新日志:</strong>'
content.appendChild(logContainer)
const paginationEl = createStyledElement('div', {
'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 = createStyledElement('button', {
'margin-right': '4px',
'padding': '2px 6px'
})
prevBtn.textContent='上一页'
prevBtn.disabled=currentPage===1
prevBtn.onclick=()=>{currentPage--;renderLogPage()}
const nextBtn = createStyledElement('button', {
'margin-left': '4px',
'padding': '2px 6px'
})
nextBtn.textContent='下一页'
nextBtn.disabled=currentPage===totalPages
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()
})();
})();