新增下载记录功能,自动模式下可智能跳过已下载过的页面,防止重复下载。
当前为
// ==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);}});}
})();