Page Crawling

图片视频抓取工具 分页列表自动去重 原图链接背景图抓取 图片批量ZIP下载 MP4批量下载 支持blob m3u8视频 脚本信息分页显示

Чтобы установить этот скрипт, вы сначала должны установить расширение браузера, например Tampermonkey, Greasemonkey или Violentmonkey.

Для установки этого скрипта вам необходимо установить расширение, такое как Tampermonkey.

Чтобы установить этот скрипт, вы сначала должны установить расширение браузера, например Tampermonkey или Violentmonkey.

Чтобы установить этот скрипт, вы сначала должны установить расширение браузера, например Tampermonkey или Userscripts.

Чтобы установить этот скрипт, сначала вы должны установить расширение браузера, например Tampermonkey.

Чтобы установить этот скрипт, вы должны установить расширение — менеджер скриптов.

(у меня уже есть менеджер скриптов, дайте мне установить скрипт!)

Чтобы установить этот стиль, сначала вы должны установить расширение браузера, например Stylus.

Чтобы установить этот стиль, сначала вы должны установить расширение браузера, например Stylus.

Чтобы установить этот стиль, сначала вы должны установить расширение браузера, например Stylus.

Чтобы установить этот стиль, сначала вы должны установить расширение — менеджер стилей.

Чтобы установить этот стиль, сначала вы должны установить расширение — менеджер стилей.

Чтобы установить этот стиль, сначала вы должны установить расширение — менеджер стилей.

(у меня уже есть менеджер стилей, дайте мне установить скрипт!)

// ==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()
    })();

})();