在Civitai图片页自动或手动下载图片和同名元数据.txt文件。新增自动/手动模式开关。
当前为
// ==UserScript==
// @name Civitai Image Downloader and Metadata Extractor
// @name:zh-CN Civitai 图像下载及元数据提取器 (带开关)
// @namespace http://tampermonkey.net/
// @version 2.0
// @description Adds a toggle for automatic/manual downloading. In manual mode, click the original download button to trigger.
// @description:zh-CN 在Civitai图片页自动或手动下载图片和同名元数据.txt文件。新增自动/手动模式开关。
// @author Your Name (with major enhancements)
// @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
// @run-at document-idle
// @license MIT
// ==/UserScript==
(function() {
'use strict';
// --- 配置 & 常量 ---
const AUTO_DOWNLOAD_KEY = 'civitaiDownloader_autoMode'; // 用于存储开关状态的键
// --- 添加自定义样式 ---
// 为我们的开关按钮在“激活”状态下设置一个蓝色背景
GM_addStyle(`
.civitai-downloader-toggle.active {
background-color: #228be6 !important; /* 一个漂亮的蓝色 */
border-color: #228be6 !important;
}
`);
// ==============================================================================
// --- 核心下载逻辑 (封装成一个函数,以便重复调用) ---
// ==============================================================================
async function startFullDownload() {
console.log("Civitai Downloader: Starting full download process...");
try {
const metadata = extractAllMetadata();
if (!metadata) {
console.error("Civitai Downloader: Could not extract metadata.");
return;
}
const imageId = window.location.pathname.split('/')[2];
if (!imageId) {
console.error("Civitai Downloader: Could not determine image ID.");
return;
}
// 1. 下载 TXT 文件
const textContent = formatMetadataAsText(metadata);
const txtFilename = `${imageId}.txt`;
downloadTextFile(textContent, txtFilename);
// 2. 智能等待并下载图片
await downloadImageWhenReady(imageId);
} catch (error) {
console.error("Civitai Downloader: An error occurred in the main download function.", error);
}
}
// ==============================================================================
// --- 主程序入口 ---
// ==============================================================================
function initialize() {
console.log("Civitai Downloader: Initializing...");
// 等待页面元素加载完毕
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 buttonContainer = downloadButton.parentElement;
if (!buttonContainer) return;
// 1. 读取保存的开关状态 (默认为 true, 即自动模式)
let isAutoMode = GM_getValue(AUTO_DOWNLOAD_KEY, true);
// 2. 创建并注入我们的开关按钮
const toggleButton = createToggleButton(downloadButton);
buttonContainer.insertBefore(toggleButton, downloadButton); // 插入到下载按钮左边
// 3. 定义一个更新按钮视觉状态的函数
function updateToggleVisual() {
if (isAutoMode) {
toggleButton.classList.add('active');
toggleButton.title = "自动下载模式已开启";
} else {
toggleButton.classList.remove('active');
toggleButton.title = "自动下载模式已关闭 (点击原下载按钮手动触发)";
}
}
updateToggleVisual(); // 初始化按钮颜色
// 4. 为开关按钮添加点击事件
toggleButton.addEventListener('click', () => {
isAutoMode = !isAutoMode; // 切换状态
GM_setValue(AUTO_DOWNLOAD_KEY, isAutoMode); // 保存新状态
updateToggleVisual(); // 更新按钮颜色
console.log(`Civitai Downloader: Auto mode set to ${isAutoMode}`);
});
// 5. 为原始下载按钮添加“劫持”事件
downloadButton.addEventListener('click', (event) => {
if (!isAutoMode) {
// 如果是手动模式,阻止它的默认行为,并执行我们的功能
event.preventDefault();
event.stopPropagation();
console.log("Civitai Downloader: Manual download triggered!");
startFullDownload();
}
// 如果是自动模式,则不阻止,让它保持原始的下载图片功能
}, true); // 使用捕获阶段确保我们的监听器最先触发
// 6. 根据初始状态决定是否自动运行
if (isAutoMode) {
console.log("Civitai Downloader: Auto mode is ON. Starting download...");
startFullDownload();
} else {
console.log("Civitai Downloader: Auto mode is OFF. Waiting for manual trigger.");
}
}
// --- 启动脚本 ---
// 使用延时确保Civitai的动态内容加载完成
setTimeout(initialize, 3000);
// ==============================================================================
// --- 辅助函数 (Helper Functions) ---
// ==============================================================================
/**
* 创建并返回一个新的开关按钮
* @param {HTMLElement} referenceButton - 用于克隆样式的参考按钮
* @returns {HTMLElement} 新创建的开关按钮
*/
function createToggleButton(referenceButton) {
const toggleButton = referenceButton.cloneNode(true); // 克隆按钮以继承所有样式
toggleButton.classList.add('civitai-downloader-toggle');
const svg = toggleButton.querySelector('svg');
// 替换为“循环”图标 (Tabler Icon: refresh)
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() { /* ... (此处代码与上一版完全相同) ... */ return metadata; }
function extractPrompts() { /* ... (此处代码与上一版完全相同) ... */ return prompts; }
function extractResources() { /* ... (此处代码与上一版完全相同) ... */ return resources; }
function extractDetails() { /* ... (此处代码与上一版完全相同) ... */ return details; }
function formatMetadataAsText(metadata) { /* ... (此处代码与上一版完全相同) ... */ return content; }
function downloadTextFile(textContent, filename) { /* ... (此处代码与上一版完全相同) ... */ }
async function downloadImageWhenReady(imageId) {
const imageElement = document.querySelector('img.EdgeImage_image__iH4_q');
if (!imageElement) { /* ... */ return; }
// ... 轮询逻辑 ...
}
function downloadImage(imageUrl, imageId) { /* ... (此处代码与上一版完全相同) ... */ }
// 为了使代码完整,我将把所有函数代码粘贴在下面
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);
}
});
}
})();