Dribbble 图片批量抓取器

从Dribbble页面提取并下载图片,跨页面自动重置状态

// ==UserScript==
// @name         Dribbble 图片批量抓取器
// @namespace    http://tampermonkey.net/
// @version      1.1.1
// @description  从Dribbble页面提取并下载图片,跨页面自动重置状态
// @author       eddie7x
// @license      MIT
// @icon         data:image/x-icon;base64,AAABAAEAEBAAAAEAIABoBAAAFgAAACgAAAAQAAAAIAAAAAEAIAAAAAAAAAQAABMLAAATCwAAAAAAAAAAAADin/7/4p/+/8J43P+ILp3/iC6d/61dxP/inv7/45/+/+Oe/v/inv7/4Z/9/9+g/v/fof7/36D+/9+g/v/gn/7/45/+/+Of/v/in/7/pVO7/4gunf+HLZz/um3S/+Ke/v/jn/7/45/+/+Kf/v/goP7/36f9/9+h/v/OoOj/4J/+/+Of/v/jn/7/45/+/92Z+f+XQqz/iS6d/4gunP+3ac//4p7+/+Ke/v/jn/7/4Z/+/9+q/P/gyvb/lJye/+Gt+//jn/7/45/+/+Of/v/in/7/2JPz/5Q+qv+JLp3/iC2c/6ZTvP/clvf/4p7+/+Gp/P/ftPr/3+nu/8rU1//cqvX/45/+/+Of/v/jn/7/45/+/+Oe/v/alPX/m0aw/4kunf+ILZz/jzaj/7xv1f/gpfr/4df0/7S9wP/d5+v/rYDD/+Of/v/jn/7/45/+/+Of/v/jn/7/457+/+Cc/P+vX8b/iC6c/4gtnf+ILZz/ol+2/97d7v+hqav/ubTH/7aBzf/in/7/45/+/+Of/v/jn/7/45/+/+Oe/v/jnv7/4p7+/86G6P+cR7H/hy2c/51YsP+knLD/kZWa/3NIgP+KPp7/45/+/+Of/v/jn/7/45/+/+Of/v/jn/7/45/+/+Of/v/inv7/4p7+/8+P5/+gT7b/0tTf/087Vv9zK4T/iS2d/+Kf/v/inv7/4Z39/92Y+f/emfn/4p79/+Ke/v/inv7/4p7+/+Om/f/jqf3/p1q8/654v/+KR5z/o1+4/6lYv/+gTLf/kTqn/4gunv+ILZ3/hy2c/4kunf+SO6f/ok+5/7hq0P/Vju//0Ynr/4gtnP+ILp3/rmTE/9KU6//in/7/iTCf/4owoP+KMJ//iTCe/4ovn/+JL5//iS+f/4kvnv+JLp7/iC2c/400ov+JLp3/jjWi/92X9//Wl/D/3Zv4/6JPuP+zZcv/v3PY/8R63v/Ded3/vXHW/7FhyP+eSrT/ijCf/4kunv+JLp7/iS6e/5Q9qf/RiOv/4p7+/+Kf/v/in/7/4p/+/+Kf/v/jn/7/4p/+/+Oe/v/inv7/4p7+/8J32/+ILZz/iS6e/4kunv+JLp7/iS+e/7hr0f/inv3/45/+/+Of/v/jn/7/4p/+/+Of/v/inv7/4p7+/8h+4f+LMqH/iS+e/5E5pv+6bdL/jjWj/4gunv+ILZ3/qVjA/+Of/v/jn/7/45/+/+Oe/v/inv7/4p7+/7ls0v+KMZ//iS+e/442o//Riev/4p7+/9iS8/+bRrH/iS6e/4kunv/in/7/4p/+/+Kf/v/inv7/1I3v/6BNt/+JMJ7/ijCf/5E6p//Qiev/4p7+/+Ke/v/inv3/3pv7/6JQuf+JL57/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==
// @match        https://dribbble.com/*
// @grant        GM_download
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        GM_registerMenuCommand
// @grant        GM_addStyle
// @require      https://cdn.jsdelivr.net/npm/promise-polyfill@8/dist/polyfill.min.js
// ==/UserScript==

(function() {
    'use strict';

    // 样式
    GM_addStyle(`
    .dribbble-scraper {
      position: fixed;
      bottom: 20px;
      right: 20px;
      z-index: 9999;
      background: #fff;
      border-radius: 8px;
      box-shadow: 0 4px 12px rgba(0,0,0,0.15);
      padding: 15px;
      border: 1px solid #eee;
    }
    .scraper-btn {
      padding: 8px 16px;
      background: #4285f4;
      color: white;
      border: none;
      border-radius: 4px;
      cursor: pointer;
      font-size: 14px;
      margin-right: 8px;
      transition: background 0.2s;
    }
    .scraper-btn:hover {
      background: #3367d6;
    }
    .scraper-status {
      margin-top: 10px;
      font-size: 13px;
      color: #555;
    }
    .scraper-result {
      margin-top: 10px;
      padding-top: 10px;
      border-top: 1px solid #eee;
      font-size: 13px;
    }
    .found-count {
      color: #3498db;
      font-weight: bold;
    }
    .download-count {
      color: #2ecc71;
      font-weight: bold;
    }
  `);

    // 界面
    const uiElement = document.createElement('div');
    uiElement.className = 'dribbble-scraper';
    uiElement.innerHTML = `
    <button id="scrapeBtn" class="scraper-btn">抓取图片</button>
    <button id="downloadBtn" class="scraper-btn" disabled>下载图片</button>
    <div id="scraperStatus" class="scraper-status">准备就绪</div>
    <div id="scraperResult" class="scraper-result"></div>
  `;
    document.body.appendChild(uiElement);

    const scrapeBtn = document.getElementById('scrapeBtn');
    const downloadBtn = document.getElementById('downloadBtn');
    const statusEl = document.getElementById('scraperStatus');
    const resultEl = document.getElementById('scraperResult');

    // 文件名安全化
    function sanitizeFilename(name, maxLen = 24) {
        return name.replace(/[<>:"/\\|?*\n\r]+/g, '').trim().replace(/\s+/g, '_').substring(0, maxLen);
    }

    // 提取图片
    function extractImageUrls() {
        statusEl.textContent = "正在解析页面...";
        const imageElements = document.querySelectorAll(".shot-page-container div > a > img");
        const urls = [];

        imageElements.forEach(img => {
            let url = null;
            const attrs = ["src", "srcset", "data-srcset", "data-src"];
            for (const attr of attrs) {
                const val = img.getAttribute(attr);
                if (val && val.includes("http")) {
                    url = val.split("?")[0].trim();
                    if (attr.includes("set")) url = url.split(",")[0].trim();
                    break;
                }
            }
            if (url) urls.push(url);
        });

        const title = sanitizeFilename(document.title.trim() || "Untitled");
        const pageKey = location.href; // 用 URL 做唯一标识

        return { title, urls, pageKey };
    }

    // 下载图片
    function downloadImages(urls, title) {
        return new Promise((resolve) => {
            if (!urls.length) {
                resolve([]);
                return;
            }

            const results = [];
            let completed = 0;
            const total = urls.length;
            const downloadDir = `Dribbble-${title}`;

            statusEl.textContent = `开始下载 ${total} 张图片...`;

            const downloadFile = (url, index) => {
                return new Promise((resolveFile) => {
                    try {
                        const parse = new URL(url);
                        const base = decodeURIComponent(parse.pathname.split('/').pop());
                        const [name, ext] = base.split('.');
                        const fileName = `${name || 'image'}-${index + 1}${ext ? '.' + ext : '.bin'}`;

                        // 改进的GM_download处理
                        const download = GM_download({
                            url: url,
                            name: `${downloadDir}/${fileName}`,
                            onload: () => {
                                results.push(fileName);
                                completed++;
                                updateProgress();
                                resolveFile();
                            },
                            onerror: (err) => {
                                console.error(`下载失败: ${url}`, err);
                                completed++;
                                updateProgress();
                                resolveFile();
                            },
                            ontimeout: () => {
                                completed++;
                                updateProgress();
                                resolveFile();
                            },
                            onabort: () => {
                                completed++;
                                updateProgress();
                                resolveFile();
                            }
                        });

                        // 保存下载引用,用于后续状态检查
                        return download;

                    } catch (err) {
                        console.error(`下载错误: ${url}`, err);
                        completed++;
                        updateProgress();
                        resolveFile();
                    }
                });
            };

            function updateProgress() {
                const progress = Math.round((completed / total) * 100);
                statusEl.textContent = `下载进度: ${progress}% (${completed}/${total})`;

                if (completed === total) {
                    resolve(results);
                }
            }

            // 使用Promise.all处理下载队列,避免过多并发
            const downloadPromises = urls.map((url, i) => downloadFile(url, i));

            // 全部下载完成后更新状态
            Promise.all(downloadPromises).then(() => {
                // 确保所有下载都完成后才更新状态
                if (completed === total) {
                    resolve(results);
                }
            });
        });
    }

    // 检查缓存
    function checkCache() {
        const cached = GM_getValue('dribbbleScraperData');
        const currentKey = location.href;

        if (cached && cached.urls && cached.urls.length > 0) {
            if (cached.pageKey !== currentKey) {
                console.log("[Dribbble Scraper] 页面切换,清理旧缓存");
                clearCache();
            } else {
                statusEl.textContent = `已找到 ${cached.urls.length} 张图片`;
                resultEl.innerHTML = `<div class="found-count">缓存 ${cached.urls.length} 张图片: ${cached.title}</div>`;
                downloadBtn.disabled = false;
            }
        } else {
            statusEl.textContent = "准备就绪";
            downloadBtn.disabled = true;
        }
    }

    // 清理缓存
    function clearCache() {
        GM_setValue('dribbbleScraperData', { title: '', urls: [], pageKey: '' });
        statusEl.textContent = "准备就绪";
        resultEl.innerHTML = '';
        downloadBtn.disabled = true;
    }

    checkCache();

    // 按钮事件
    scrapeBtn.addEventListener('click', async () => {
        try {
            scrapeBtn.disabled = true;
            downloadBtn.disabled = true;
            resultEl.innerHTML = '';

            const result = extractImageUrls();
            if (!result.urls.length) {
                statusEl.textContent = "未找到可下载的图片";
                scrapeBtn.disabled = false;
                return;
            }

            GM_setValue('dribbbleScraperData', result);
            statusEl.textContent = `已找到 ${result.urls.length} 张图片`;
            resultEl.innerHTML = `<div class="found-count">发现 ${result.urls.length} 张图片: ${result.title}</div>`;
            downloadBtn.disabled = false;
        } catch (err) {
            console.error("抓取错误:", err);
            statusEl.textContent = "抓取过程中出错";
        } finally {
            scrapeBtn.disabled = false;
        }
    });

    downloadBtn.addEventListener('click', async () => {
        try {
            downloadBtn.disabled = true;
            scrapeBtn.disabled = true;
            resultEl.innerHTML = '';

            const data = GM_getValue('dribbbleScraperData');
            if (!data || !data.urls || !data.urls.length) {
                statusEl.textContent = "未找到图片数据,请先抓取图片";
                scrapeBtn.disabled = false;
                return;
            }

            const results = await downloadImages(data.urls, data.title);

            // 使用setTimeout确保UI有机会更新
            setTimeout(() => {
                statusEl.textContent = `下载完成: ${results.length} 张图片`;
                resultEl.innerHTML = `<div class="download-count">已保存 ${results.length} 个文件到下载目录</div>`;
                downloadBtn.disabled = false;
            }, 500);

        } catch (err) {
            console.error("下载错误:", err);
            statusEl.textContent = "下载过程中出错";
        } finally {
            scrapeBtn.disabled = false;
        }
    });

    // 监听URL变化
    function setupUrlChangeListener() {
        let currentUrl = location.href;

        setInterval(() => {
            if (currentUrl !== location.href) {
                console.log("[Dribbble Scraper] URL变化,清理缓存");
                currentUrl = location.href;
                clearCache();
            }
        }, 500);
    }

    // 启动URL变化监听
    setupUrlChangeListener();

    GM_registerMenuCommand("抓取Dribbble图片", () => {
        scrapeBtn.click();
    });

    console.log("[Dribbble Scraper] 已加载");
})();