Civitai 图像下载及元数据提取器

新增下载记录功能,自动模式下可智能跳过已下载过的页面,防止重复下载。

当前为 2025-06-14 提交的版本,查看 最新版本

// ==UserScript==
// @name         Civitai Image Downloader and Metadata Extractor
// @name:zh-CN   Civitai 图像下载及元数据提取器
// @namespace    http://tampermonkey.net/
// @version      2.1
// @description  Adds a toggle for auto/manual downloading and remembers downloaded images to prevent re-downloading.
// @description:zh-CN 新增下载记录功能,自动模式下可智能跳过已下载过的页面,防止重复下载。
// @author       Camellia895
// @match        https://civitai.com/images/*
// @icon         https://civitai.com/favicon-32x32.png
// @grant        GM_download
// @grant        GM_log
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        GM_addStyle
// @license      MIT
// @run-at       document-idle
// ==/UserScript==

(function() {
    'use strict';

    // --- 配置 & 常量 ---
    const AUTO_DOWNLOAD_KEY = 'civitaiDownloader_autoMode';
    const DOWNLOADED_IDS_KEY = 'civitaiDownloader_downloadedIds'; // 新增:用于存储下载记录的键

    // --- 添加自定义样式 ---
    GM_addStyle(`
        /* 蓝色表示自动模式开启 */
        .civitai-downloader-toggle.active {
            background-color: #228be6 !important;
            border-color: #228be6 !important;
        }
        /* 绿色表示此页已下载过 */
        .civitai-downloader-toggle.downloaded {
            background-color: #2f9e44 !important; /* A nice green */
            border-color: #2f9e44 !important;
        }
    `);

    // ==============================================================================
    // --- 核心下载逻辑 ---
    // ==============================================================================
    async function startFullDownload(imageId) {
        console.log("Civitai Downloader: Starting full download process...");
        try {
            const metadata = extractAllMetadata();
            if (!metadata) return;

            // 1. 下载 TXT 文件
            const textContent = formatMetadataAsText(metadata);
            const txtFilename = `${imageId}.txt`;
            downloadTextFile(textContent, txtFilename);

            // 2. 智能等待并下载图片
            await downloadImageWhenReady(imageId);

            // 3. 标记为已下载 (仅在所有下载成功后执行)
            markAsDownloaded(imageId);

        } catch (error) {
            console.error("Civitai Downloader: An error occurred in the main download function.", error);
        }
    }

    // ==============================================================================
    // --- 主程序入口 ---
    // ==============================================================================
    function initialize() {
        // 等待页面元素加载完毕
        const checkInterval = setInterval(() => {
            const downloadButton = document.querySelector('svg.tabler-icon-download')?.closest('button');
            if (downloadButton) {
                clearInterval(checkInterval);
                run(downloadButton);
            }
        }, 500);
        setTimeout(() => clearInterval(checkInterval), 15000);
    }

    function run(downloadButton) {
        const imageId = window.location.pathname.split('/')[2];
        if (!imageId) return;

        const buttonContainer = downloadButton.parentElement;
        if (!buttonContainer) return;

        // 1. 读取所有状态
        let isAutoMode = GM_getValue(AUTO_DOWNLOAD_KEY, true);
        const downloadedIds = GM_getValue(DOWNLOADED_IDS_KEY, {});
        const hasBeenDownloaded = !!downloadedIds[imageId];

        // 2. 创建并注入开关按钮
        const toggleButton = createToggleButton(downloadButton);
        buttonContainer.insertBefore(toggleButton, downloadButton);

        // 3. 更新按钮视觉状态的函数
        function updateToggleVisual() {
            toggleButton.classList.remove('active', 'downloaded'); // 先清除所有状态
            if (hasBeenDownloaded) {
                toggleButton.classList.add('downloaded');
                toggleButton.title = "此页已下载过 (点击原下载按钮可强制重新下载)";
            } else if (isAutoMode) {
                toggleButton.classList.add('active');
                toggleButton.title = "自动下载模式已开启";
            } else {
                toggleButton.title = "自动下载模式已关闭 (点击原下载按钮手动触发)";
            }
        }
        updateToggleVisual();

        // 4. 为开关按钮添加点击事件
        toggleButton.addEventListener('click', () => {
            isAutoMode = !isAutoMode;
            GM_setValue(AUTO_DOWNLOAD_KEY, isAutoMode);
            // 切换模式时,需要重新评估视觉状态
            // 注意:我们不改变`hasBeenDownloaded`的值,只改变`isAutoMode`
            updateToggleVisual();
            console.log(`Civitai Downloader: Auto mode set to ${isAutoMode}`);
        });

        // 5. 为原始下载按钮添加“劫持”事件 (强制下载)
        downloadButton.addEventListener('click', (event) => {
            // 只要是手动点击,无论任何状态,都执行下载
            event.preventDefault();
            event.stopPropagation();
            console.log("Civitai Downloader: Manual/Force download triggered!");
            startFullDownload(imageId);
        }, true);

        // 6. 根据初始状态决定是否自动运行
        if (isAutoMode && !hasBeenDownloaded) {
            console.log("Civitai Downloader: Auto mode ON and page is new. Starting download...");
            startFullDownload(imageId);
        } else if (isAutoMode && hasBeenDownloaded) {
            console.log("Civitai Downloader: Auto mode ON, but page already downloaded. Skipping.");
        } else {
            console.log("Civitai Downloader: Auto mode OFF. Waiting for manual trigger.");
        }
    }

    // --- 启动脚本 ---
    setTimeout(initialize, 3000);

    // ==============================================================================
    // --- 辅助函数 (Helper Functions) ---
    // ==============================================================================

    /**
     * 将当前图片ID标记为已下载
     */
    function markAsDownloaded(imageId) {
        const downloadedIds = GM_getValue(DOWNLOADED_IDS_KEY, {});
        downloadedIds[imageId] = true;
        GM_setValue(DOWNLOADED_IDS_KEY, downloadedIds);
        console.log(`Civitai Downloader: Page ${imageId} marked as downloaded.`);

        // 可选:标记后立即更新按钮颜色为绿色,提供即时反馈
        const toggleButton = document.querySelector('.civitai-downloader-toggle');
        if (toggleButton) {
            toggleButton.classList.remove('active');
            toggleButton.classList.add('downloaded');
            toggleButton.title = "此页已下载过 (点击原下载按钮可强制重新下载)";
        }
    }

    // 其他辅助函数保持不变
    function createToggleButton(referenceButton) { /* ... */ }
    function extractAllMetadata() { /* ... */ }
    function extractPrompts() { /* ... */ }
    function extractResources() { /* ... */ }
    function extractDetails() { /* ... */ }
    function formatMetadataAsText(metadata) { /* ... */ }
    function downloadTextFile(textContent, filename) { /* ... */ }
    async function downloadImageWhenReady(imageId) { /* ... */ }
    function downloadImage(imageUrl, imageId) { /* ... */ }

    // --- 完整函数代码 ---
    function createToggleButton(referenceButton) {
        const toggleButton = referenceButton.cloneNode(true);
        toggleButton.classList.add('civitai-downloader-toggle');
        const svg = toggleButton.querySelector('svg');
        svg.innerHTML = `<path d="M20 11a8.1 8.1 0 0 0 -15.5 -2m-.5 -4v4h4"></path><path d="M4 13a8.1 8.1 0 0 0 15.5 2m.5 4v-4h-4"></path>`;
        svg.classList.remove('tabler-icon-download');
        svg.classList.add('tabler-icon-refresh');
        return toggleButton;
    }
    function extractAllMetadata() {const metadata = {};metadata.sourceUrl = window.location.href;const prompts = extractPrompts();metadata.positivePrompt = prompts.positive;metadata.negativePrompt = prompts.negative;metadata.resources = extractResources();metadata.details = extractDetails();return metadata;}
    function extractPrompts() {const prompts = { positive: "Not found", negative: "Not found" };let generationDataContainer = null;const allHeaders = document.querySelectorAll('h3.mantine-Title-root');for (const h of allHeaders) {if (h.textContent.trim().toLowerCase() === 'generation data') {generationDataContainer = h.parentElement;break;}}if (!generationDataContainer) {generationDataContainer = document;}const promptElements = generationDataContainer.querySelectorAll('.mantine-1c2skr8');if (promptElements.length > 0) prompts.positive = promptElements[0].textContent.trim();if (promptElements.length > 1) prompts.negative = promptElements[1].textContent.trim();return prompts;}
    function extractResources() {const resources = [];let resourceList = null;const allHeaders = document.querySelectorAll('h3.mantine-Title-root');for (const h of allHeaders) {if (h.textContent.trim().toLowerCase() === 'resources') {const container = h.parentElement;if (container && container.nextElementSibling && container.nextElementSibling.tagName === 'UL') {resourceList = container.nextElementSibling;break;}}}if (!resourceList) {resourceList = document.querySelector('ul.flex.list-none.flex-col');}if (!resourceList) return ["Resource list not found."];resourceList.querySelectorAll('li').forEach(item => {const linkElement = item.querySelector('a[href*="/models/"]');const nameElement = item.querySelector('div.mantine-12h10m4');const versionElement = item.querySelector('div.mantine-nvo449');const typeElement = item.querySelector('div.mantine-qcxgtg span.mantine-Badge-inner');const resource = {};if (nameElement) resource.name = nameElement.textContent.trim();if (linkElement) resource.link = `https://civitai.com${linkElement.getAttribute('href')}`;if (versionElement) resource.version = versionElement.textContent.trim();if (typeElement) resource.type = typeElement.textContent.trim();const weightElement = item.querySelector('div.mantine-j55fvo span.mantine-Badge-inner');if (weightElement) resource.weight = weightElement.textContent.trim();resources.push(resource);});return resources;}
    function extractDetails() {const details = {};const detailsContainer = document.querySelector('div.flex.flex-wrap.gap-2');if (!detailsContainer) return details;const detailBadges = detailsContainer.querySelectorAll(':scope > div.mantine-Badge-root');detailBadges.forEach(badge => {const text = badge.textContent.trim();const parts = text.split(/:(.*)/s);if (parts.length >= 2) {const key = parts[0].trim();const value = parts[1].trim();details[key] = value;}});return details;}
    function formatMetadataAsText(metadata) {let content = "Positive Prompt:\n" + metadata.positivePrompt + "\n\n";content += "Negative Prompt:\n" + metadata.negativePrompt + "\n\n";content += "--- Details ---\n";for (const [key, value] of Object.entries(metadata.details)) {content += `${key}: ${value}\n`;}content += "\n--- Resources ---\n";if (metadata.resources.length > 0 && typeof metadata.resources[0] === 'string') {content += metadata.resources[0] + "\n\n";} else {metadata.resources.forEach(res => {content += `Type: ${res.type || 'N/A'}\n`;content += `Name: ${res.name || 'N/A'}\n`;content += `Version: ${res.version || 'N/A'}\n`;if (res.weight) content += `Weight: ${res.weight}\n`;content += `Link: ${res.link || 'N/A'}\n\n`;});}content += "--- Source ---\n";content += `Image URL: ${metadata.sourceUrl}\n`;return content;}
    function downloadTextFile(textContent, filename) {const blob = new Blob([textContent], { type: 'text/plain;charset=utf-8' });const dataUrl = URL.createObjectURL(blob);GM_download({url: dataUrl,name: filename,onload: () => {URL.revokeObjectURL(dataUrl);console.log(`Civitai Downloader: Metadata file '${filename}' download finished.`);},onerror: (err) => {URL.revokeObjectURL(dataUrl);console.error(`Civitai Downloader: Error downloading metadata file.`, err);}});}
    async function downloadImageWhenReady(imageId) {return new Promise((resolve, reject) => {const pollingInterval = 250;const maxWaitTime = 10000;let totalWait = 0;const imageElement = document.querySelector('img.EdgeImage_image__iH4_q');if (!imageElement) {console.error("Civitai Downloader: Could not find main image element.");return reject("Image element not found.");}const poller = setInterval(() => {const currentSrc = imageElement.src;if (currentSrc && currentSrc.startsWith('http') && currentSrc.includes('original=true')) {clearInterval(poller);downloadImage(currentSrc, imageId).then(resolve).catch(reject);} else {totalWait += pollingInterval;if (totalWait >= maxWaitTime) {clearInterval(poller);console.error(`Civitai Downloader: Timed out waiting for final image URL.`);reject("Timed out");}}}, pollingInterval);});}
    function downloadImage(imageUrl, imageId) {return new Promise((resolve, reject) => {try {const urlPath = new URL(imageUrl).pathname;const extension = urlPath.substring(urlPath.lastIndexOf('.'));const newImageFilename = `${imageId}${extension}`;GM_download({url: imageUrl,name: newImageFilename,onload: () => {console.log(`Civitai Downloader: Image '${newImageFilename}' download finished.`);resolve();},onerror: (err) => {console.error(`Civitai Downloader: Error downloading image.`, err);reject(err);}});} catch (e) {console.error("Civitai Downloader: Failed to process image URL.", e);reject(e);}});}
})();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址