// ==UserScript==
// @name liblib|civitai助手-封面+模型信息
// @namespace http://tampermonkey.net/
// @version 1.0.31
// @description liblib|civitai助手,下载封面+模型信息
// @author kaiery
// @match https://www.liblib.ai/modelinfo/*
// @match https://www.liblib.art/modelinfo/*
// @match https://civitai.com/models/*
// @grant none
// @license MIT License
// ==/UserScript==
(function () {
'use strict';
// 定义全局变量
// var modelDir;
var model_name_ver;
var textDesc, uuid, buildId, webid, modelId, modelName, modelVersionId, downloadUrl;
var page = 1;
var pageSize = 16;
var sortType = 0;
const default_download_pic_num = 100;
// 获取当前站点
const currentSite = () => {
if (window.location.hostname.includes('liblib')) {
return 'liblib';
} else if (window.location.hostname.includes('civitai')) {
return 'civitai';
} else {
return 'unknown';
}
};
// ---------------------------------------------------------------
// demo
// ---------------------------------------------------------------
async function createDirectory() {
// open directory picker
const dirHandle = await window.showDirectoryPicker({mode: "readwrite"});
// create a new directory named 'newDir'
const newDirHandle = await dirHandle.getDirectoryHandle('newDir', {create: true});
console.log(newDirHandle);
}
// ---------------------------------------------------------------
// html转文本
// ---------------------------------------------------------------
function htmlToText(html) {
const tempDiv = document.createElement('div');
tempDiv.innerHTML = html;
let text = '';
for (let i = 0; i < tempDiv.childNodes.length; i++) {
if (tempDiv.childNodes[i].nodeName === 'P') {
text += tempDiv.childNodes[i].textContent + '\n';
}
}
return text;
}
// ---------------------------------------------------------------
// 保存liblib封面信息
// ---------------------------------------------------------------
async function saveLibLibAuthImagesInfo() {
// 1:CheckPoint 2:embedding;3:HYPERNETWORK ;4:AESTHETIC GRADIENT; 5:Lora;6:LyCORIS; 9:WILDCARDS
let modelType = 1;
// open directory picker
const dirHandle = await window.showDirectoryPicker({mode: "readwrite"});
// 根据选项卡获取模型版本id
const div = document.querySelector('.ant-tabs-tab.ant-tabs-tab-active');
const modelVersionId = parseInt(div.getAttribute('data-node-key'));
const modelVer = div.innerText.replace(/[/\\?%*:|"<>]/g, '-');
const allElements = document.querySelectorAll('div');
allElements.forEach(function (element) {
const classNames = element.className.split(/\s+/);
for (let i = 0; i < classNames.length; i++) {
if (classNames[i].startsWith('ModelDescription_desc')) {
textDesc = htmlToText(element.innerHTML);
textDesc = textDesc.replace(/\\n/g, '\n');
break;
}
}
});
if (textDesc) {
// Get the content of the script element
const scriptContent = document.getElementById('__NEXT_DATA__').textContent;
const scriptJson = JSON.parse(scriptContent);
// Extract uuid, buildId, and webid
uuid = scriptJson.query.uuid;
buildId = scriptJson.buildId;
webid = scriptJson.props.webid;
//------------
// 预请求地址
const url_acceptor = "https://www.liblib.art/api/www/log/acceptor/f?timestamp=" + Date.now();
// var url_acceptor = "https://liblib-api.vibrou.com/api/www/log/acceptor/f?timestamp="+Date.now();
// 模型信息地址
const url_model = "https://www.liblib.art/api/www/model/getByUuid/" + uuid + "?timestamp=" + Date.now();
// var url_model = "https://liblib-api.vibrou.com/api/www/model/getByUuid/" + uuid;
// 发送预请求-------------------------------------------------------
const resp_acc = await fetch(url_acceptor, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({timestamp: Date.now()})
})
// 发送模型信息
const resp = await fetch(url_model, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({timestamp: Date.now()})
})
const model_data = await resp.json();
// console.log("----------模型信息-----------");
// console.log(model_data);
if (model_data.code !== 0) {
return;
}
modelId = model_data.data.id
modelName = model_data.data.name.replace(/[/\\?%*:|"<>]/g, '-');
model_name_ver = modelName + "_" + modelVer;
if (model_name_ver.slice(-1) === '.') {
model_name_ver = model_name_ver.substring(0, model_name_ver.length - 1);
}
modelType = model_data.data.modelType // 1:CheckPoint 2:embedding;3:HYPERNETWORK ;4:AESTHETIC GRADIENT; 5:Lora;6:LyCORIS; 9:WILDCARDS
let modelTypeName = '未分类'
switch (modelType) {
case 1:
modelTypeName = 'CheckPoint'
break;
case 2:
modelTypeName = 'embedding'
break;
case 3:
modelTypeName = 'HYPERNETWORK'
break;
case 4:
modelTypeName = 'AESTHETIC GRADIENT'
break;
case 5:
modelTypeName = 'Lora'
break;
case 6:
modelTypeName = 'LyCORIS'
break;
case 9:
modelTypeName = 'WILDCARDS'
break;
}
// console.log(modelDir+"/"+modelName);
const versions = model_data.data.versions;
for (const verItem of versions) {
// 匹配版本号
if (verItem.id === modelVersionId) {
// 模型信息json信息
let modelInfoJson = {
modelType: modelTypeName,
description: textDesc,
uuid: uuid,
buildId: buildId,
webid: webid
};
const promptList = []
// 图片信息start
const authImages = verItem.imageGroup.images;
let isCover = false;
for (const authImage of authImages) {
const authImageUrl = authImage.imageUrl;
var authimageName = authImage.id;
var authimageExt = authImageUrl.split("/").pop().split(".").pop();
var tmp = authimageExt.indexOf("?");
if (tmp > 0) {
authimageExt = authimageExt.substring(0, tmp);
}
const authImageUuid = authImage.uuid;
const generateInfo = authImage.generateInfo;
if (generateInfo) {
if (generateInfo.prompt) {
promptList.push(generateInfo.prompt)
}
}
if (!isCover) {
// 下载封面图片
isCover = true;
// 下载图片
const resp_download = await fetch(authImageUrl);
const blob = await resp_download.blob();
// 获取文件句柄
const fileName = model_name_ver + "." + authimageExt;
const picHandle = await dirHandle.getFileHandle(fileName, {create: true});
// 写入图片
const writable = await picHandle.createWritable();
await writable.write(blob);
await writable.close();
console.log("Image written to file:", fileName);
// break;
}
}
// 图片信息end
let triggerWord = '触发词:';
if ('triggerWord' in verItem && verItem.triggerWord) {
triggerWord = triggerWord + verItem.triggerWord
} else {
triggerWord = triggerWord + "无";
}
modelInfoJson.triggerWord = triggerWord
// 创建模型目录( 模型+版本名 )
const modelDirHandle = await dirHandle.getDirectoryHandle(model_name_ver, {create: true});
// 获取文件句柄
const savejsonHandle = await modelDirHandle.getFileHandle(modelName + ".json", {create: true});
// 写入模型信息json文件
const writablejson = await savejsonHandle.createWritable();
await writablejson.write(JSON.stringify(modelInfoJson, null, 4));
await writablejson.close();
// 创建模型版本目录
// const modelVerDirHandle = await modelDirHandle.getDirectoryHandle(modelName, {create: true});
// 获取文件句柄
const saveExampleHandle = await modelDirHandle.getFileHandle("example.txt", {create: true});
const writableExample = await saveExampleHandle.createWritable();
await writableExample.write(triggerWord + '\n\n');
// 写入字符串数组
for (const str of promptList) {
await writableExample.write(str + '\n\n');
}
await writableExample.close();
}
}
}
alert("封面信息下载完成");
}
// ---------------------------------------------------------------
// 保存封面信息
// ---------------------------------------------------------------
async function saveCivitaiModelInfo() {
// 模型id
let modelId = 0;
// 模型版本id
let modelVersionId = 0;
// 模型描述
let textDesc = '';
// 模型名称
let modelName = '';
// 模型版本
let modelVer = '';
// 样图提示词举例
let example = []
// open directory picker
const dirHandle = await window.showDirectoryPicker({mode: "readwrite"});
// 获取模型id和模型版本id
const codeElements = document.querySelectorAll('.mantine-Code-root');
if (codeElements.length >= 4) {
const value1 = codeElements[1].textContent;
const value2 = codeElements[3].textContent;
modelId = value1;
modelVersionId = value2;
// 接口url
const url_model = "https://civitai.com/api/v1/models/" + modelId;
// 获取模型介绍文本
textDesc = extractCivitaiTextFromSecondSpoiler();
// console.log(textDesc)
console.log('request model info url');
// 发送模型信息
const resp = await fetch(url_model, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({timestamp: Date.now()})
})
if (!resp.ok) {
console.log(`HTTP error! status: ${resp.status}`);
alert(`[错误:访问模型信息接口失败] ${resp.status}`);
return;
}
const model_data = await resp.json();
// 检查 data 是否为空
if (!model_data) {
console.log(`模型信息为空 *************************************************************`);
alert(`模型信息为空`);
return;
}
//检查 data 是否包含 error 和 message
if (model_data.message && model_data.error) {
console.log(`数据为空 *************************************************************`);
alert(`数据为空`);
return;
}
// console.log("----------模型信息-----------");
// console.log(JSON.stringify(model_data, null, 4));
// console.log(JSON.stringify(model_data));
modelName = model_data.name.replace(/[/\\?%*:|"<>]/g, '-');
let modelType = model_data.modelType // 1:CheckPoint 2:embedding;3:HYPERNETWORK ;4:AESTHETIC GRADIENT; 5:Lora;6:LyCORIS; 9:WILDCARDS
let modelTypeName = '未分类'
switch (modelType) {
case 1:
modelTypeName = 'CheckPoint'
break;
case 2:
modelTypeName = 'embedding'
break;
case 3:
modelTypeName = 'HYPERNETWORK'
break;
case 4:
modelTypeName = 'AESTHETIC GRADIENT'
break;
case 5:
modelTypeName = 'Lora'
break;
case 6:
modelTypeName = 'LyCORIS'
break;
case 9:
modelTypeName = 'WILDCARDS'
break;
}
if(modelTypeName === '未分类'){
if('type' in model_data){
modelTypeName = model_data.type
}
}
// 模型版本数组
let versions = model_data.modelVersions;
for (const verItem of versions) {
// 匹配版本号
if (verItem.id.toString() === modelVersionId) {
modelVer = verItem.name;
model_name_ver = modelName + "_" + modelVer;
if (model_name_ver.slice(-1) === '.') {
model_name_ver = model_name_ver.substring(0, model_name_ver.length - 1);
}
let files = verItem.files;
let modelFile = '';
let split = '';
// console.log(files);
if (files.length === 1){
modelFile = files[0].name;
split = splitFilename(modelFile);
model_name_ver = split.name;
}else{
// 弹出选择模型文件框---------------------
const selectedObject = await showObjectSelectionDialog(files);
if (!selectedObject) {
return;
}
// end
// console.log("选择的对象:", `提交: ${selectedObject.name} (${selectedObject.sizeKB} KB)`);
// model_name_ver = selectedObject.name
modelFile = selectedObject.name;
split = splitFilename(modelFile);
// console.log(`文件名: ${selectedObject.name}`);
// console.log(` 文件名部分: ${split.name}`);
// console.log(` 扩展名: ${split.extension}`);
model_name_ver = split.name;
}
// 模型介绍
textDesc = verItem.description + '\n\n' + textDesc;
// 模型信息
let modelInfoJson = {
modelType: modelTypeName,
description: textDesc,
modelName: modelName,
modelVer: modelVer,
modelId: modelId,
modelFile: modelFile,
modelVersionId: modelVersionId
};
// 提示词列表
const promptList = []
// 图片信息-------------
let authImages = verItem.images;
authImages = authImages.filter(item => item && item.type === 'image');
// console.log(authImages);
let images = [];
for (const img of authImages){
if(img.type === 'image'){
images.push(img);
}
}
// 获取样图id数组-------------------
const imageIds = getImageIds(images); // 直接调用,getImageIds 应该是同步的
if (imageIds.length > 0) {
// 获取样图信息
example = await getImageExample(imageIds);
// console.log(JSON.stringify(example, null, 4));
// 🌟🌟🌟 在这里立即继续编写逻辑 🌟🌟🌟
// 安全地使用 'example' 数组,因为它已经被赋值
if (example.length > 0) {
example.forEach(item => {
// 对 example 数组中的每个 item 执行操作
// console.log("Processing item:", item);
let itemType = item?.result?.data?.json?.type ?? undefined;
let meta = item?.result?.data?.json?.meta ?? undefined;
if (meta !== undefined && itemType === 'image') {
const promptMeta = {
prompt:meta.prompt,
negativePrompt:meta.negativePrompt,
sampler:meta.sampler,
cfgScale:meta.cfgScale,
steps:meta.steps,
Size:meta.Size
};
promptList.push(promptMeta);
}
});
}
}
// 封面图片
let isCover = false;
for (const authImage of authImages) {
const authImageUrl = authImage.url;
let authimageExt = authImageUrl.split("/").pop().split(".").pop();
const tmp = authimageExt.indexOf("?");
if (tmp > 0) {
authimageExt = authimageExt.substring(0, tmp);
}
if (!isCover) {
// console.log(authImageUrl)
// 下载封面图片
isCover = true;
// 下载图片
const resp_download = await fetch(authImageUrl);
const blob = await resp_download.blob();
// 获取文件句柄
const fileName = model_name_ver + "." + authimageExt;
const picHandle = await dirHandle.getFileHandle(fileName, {create: true});
// 写入图片
const writable = await picHandle.createWritable();
await writable.write(blob);
await writable.close();
console.log("Image written to file:", fileName);
// break;
}
}
let triggerWord = '触发词:';
if ('trainedWords' in verItem && verItem.trainedWords) {
triggerWord = triggerWord + verItem.trainedWords
} else {
triggerWord = triggerWord + "无";
}
modelInfoJson.triggerWord = triggerWord
// console.log(JSON.stringify(modelInfoJson, null, 4));
// 创建模型目录( 模型+版本名 )
const modelDirHandle = await dirHandle.getDirectoryHandle(model_name_ver, {create: true});
// 获取文件句柄
const savejsonHandle = await modelDirHandle.getFileHandle(modelName + ".json", {create: true});
// 写入模型信息json文件
const writablejson = await savejsonHandle.createWritable();
await writablejson.write(JSON.stringify(modelInfoJson, null, 4));
await writablejson.close();
// 获取文件句柄
const saveExampleHandle = await modelDirHandle.getFileHandle("example.txt", {create: true});
const writableExample = await saveExampleHandle.createWritable();
await writableExample.write(triggerWord + '\n\n');
// 写入字符串数组
for (const str of promptList) {
await writableExample.write(JSON.stringify(str, null, 4) + '\n\n');
}
await writableExample.close();
} // 匹配版本end
} // 循环versions
alert("封面信息下载完成");
} else {
alert("未找到模型ID信息");
}
}
function extractCivitaiTextFromSecondSpoiler() {
// 获取所有的 mantine-Spoiler-content 元素
const spoilerElements = document.querySelectorAll('.mantine-Spoiler-content');
// 检查是否有至少两个元素
if (spoilerElements.length < 2) {
console.warn("少于两个 .mantine-Spoiler-content 元素");
return null; // 或者返回一个空字符串 ""
}
// 获取第二个元素
const secondSpoiler = spoilerElements[1];
// 提取文本内容,并替换 <p> 标签为换行符
return extractCivitaiText(secondSpoiler);
}
function extractCivitaiText(element) {
let text = '';
// 递归遍历所有子节点
for (let i = 0; i < element.childNodes.length; i++) {
const node = element.childNodes[i];
if (node.nodeType === Node.TEXT_NODE) {
text += node.textContent.trim(); // 添加文本节点的内容,去除前后空格
} else if (node.nodeType === Node.ELEMENT_NODE) {
if (node.tagName.toLowerCase() === 'p') {
text += '\n'; // 替换 <p> 标签为换行符
}
text += extractCivitaiText(node); // 递归调用处理子元素
}
}
return text;
}
// 获取图像 ID 数组
function getImageIds(images) {
const imageIds = [];
for (const image of images) {
const url = image.url;
const A = url.split('/').pop(); // 使用 pop() 更简洁地获取最后一个元素
const imgId = A.split('.')[0];
imageIds.push(imgId);
}
return imageIds;
}
// 获取示例图像数组
async function getImageExample(imageIds) {
const exampleList = [];
if (imageIds.length > 0) {
for (const imageId of imageIds) {
const inputObject = { json: { id: parseInt(imageId, 10), authed: true } }; // 确保 imageId 是数字
const encodedImageId = encodeURIComponent(JSON.stringify(inputObject));
const url = `https://civitai.com/api/trpc/image.getGenerationData?input=${encodedImageId}`;
try {
console.log('request image info url ');
const response = await fetch(url);
if (!response.ok) {
alert(`HTTP error! status: ${response.status}`);
console.log(`HTTP error! status: ${response.status}`);
}
const data = await response.json();
exampleList.push(data);
} catch (error) {
console.error(`[错误:访问模型信息接口失败] [url:${url}] [异常:${error}]`);
alert(`[错误:访问模型信息接口失败] [url:${url}] [异常:${error}]`);
}
}
}
return exampleList;
}
function createSimpleModal(options) {
return new Promise((resolve, reject) => {
const modal = document.createElement('div');
modal.style.position = 'fixed';
modal.style.top = '50%';
modal.style.left = '50%';
modal.style.transform = 'translate(-50%, -50%)';
modal.style.backgroundColor = '#fff';
modal.style.border = '1px solid #ccc';
modal.style.padding = '20px';
modal.style.zIndex = '1000';
modal.style.boxShadow = '0 4px 8px rgba(0,0,0,0.2)';
modal.style.borderRadius = '5px';
modal.style.fontFamily = 'Arial, sans-serif';
const title = document.createElement('h3');
title.textContent = options.title || '请选择一个对象';
modal.appendChild(title);
const form = document.createElement('form');
form.addEventListener('submit', function(event) {
event.preventDefault();
const selectedValue = document.querySelector('input[name="objectOption"]:checked')?.value;
if (!selectedValue) {
// 显示提示信息
alert('请选择一个选项!');
return; // 阻止模态框关闭和 Promise resolve
}
modal.remove();
resolve(selectedValue); // Resolve Promise with selected name
});
options.items.forEach(item => {
const radioLabel = document.createElement('label');
radioLabel.style.display = 'block';
radioLabel.style.marginBottom = '5px';
const radioInput = document.createElement('input');
radioInput.type = 'radio';
radioInput.name = 'objectOption';
radioInput.value = item.name;
radioLabel.appendChild(radioInput);
radioLabel.appendChild(document.createTextNode(`${item.name} (${item.sizeKB} KB)`));
form.appendChild(radioLabel);
});
const submitButton = document.createElement('button');
submitButton.type = 'submit';
submitButton.textContent = '提交';
submitButton.style.marginTop = '10px';
submitButton.style.padding = '8px 12px';
submitButton.style.backgroundColor = '#4CAF50';
submitButton.style.color = 'white';
submitButton.style.border = 'none';
submitButton.style.borderRadius = '4px';
submitButton.style.cursor = 'pointer';
form.appendChild(submitButton);
modal.appendChild(form);
document.body.appendChild(modal);
// 添加关闭按钮,点击后提示选择
const closeButton = document.createElement('button');
closeButton.textContent = '关闭';
closeButton.style.marginTop = '10px';
closeButton.style.padding = '8px 12px';
closeButton.style.backgroundColor = '#ccc';
closeButton.style.color = 'white';
closeButton.style.border = 'none';
closeButton.style.borderRadius = '4px';
closeButton.style.cursor = 'pointer';
closeButton.addEventListener('click', () => {
alert('请选择一个选项!');
});
modal.appendChild(closeButton);
});
}
async function showObjectSelectionDialog(objects) {
const selectedName = await createSimpleModal({
title: '选择要提交的对象',
items: objects
});
if (selectedName) {
const selectedObject = objects.find(obj => obj.name === selectedName);
return selectedObject; // Return selected object
} else {
return null; // Return null if no object selected
}
}
function splitFilename(filename) {
if (!filename || typeof filename !== 'string') {
return { name: '', extension: null }; // 处理空字符串或无效输入
}
const lastDotIndex = filename.lastIndexOf('.');
if (lastDotIndex === -1) {
return { name: filename, extension: null }; // 没有扩展名
}
const name = filename.substring(0, lastDotIndex);
const extension = filename.substring(lastDotIndex + 1);
return { name: name, extension: extension };
}
// ---------------------------------------------------------------
// 创建按钮
// ---------------------------------------------------------------
function createButtons(site) {
// 定义元素------------------------------------
const div1 = document.createElement('div');
div1.style.display = 'flex';
div1.style.justifyContent = "space-between";
div1.style.alignItems = "center";
if (site === 'liblib') {
const button1 = document.createElement('button');
button1.textContent = '下载封面+生成信息';
button1.onclick = saveLibLibAuthImagesInfo;
button1.style.padding = '15px';
button1.style.width = "200px";
button1.style.backgroundColor = 'red';
button1.style.color = 'white';
button1.style.display = 'block';
button1.style.flex = "1";
button1.style.borderRadius = '8px'; // 设置圆角半径
div1.appendChild(button1);
} else if (site === 'civitai') {
const button2 = document.createElement('button');
button2.textContent = '下载封面+生成信息 (Civitai)';
button2.onclick = saveCivitaiModelInfo;
button2.style.padding = '15px';
button2.style.width = "100%";
button2.style.setProperty('background-color', 'blue', 'important'); // 使用 setProperty
button2.style.color = 'white';
button2.style.display = 'block';
button2.style.flex = "1";
button2.style.borderRadius = '4px';
button2.style.marginBottom = '5px';
div1.appendChild(button2);
}
return div1;
}
// ---------------------------------------------------------------
// 监听器
// ---------------------------------------------------------------
function createObserver(site, div1) {
// 监听
const observer = new MutationObserver(function (mutations) {
let found = false;
mutations.forEach(function (mutation) {
if (mutation.type === 'childList' && !found) {
const allElements = document.querySelectorAll('div');
allElements.forEach(function (element) {
const classNames = element.className.split(/\s+/);
for (let i = 0; i < classNames.length; i++) {
if (site === 'liblib') {
if (classNames[i].startsWith('ModelDescription_desc')) {
found = true;
observer.disconnect(); // 停止观察
const actionCard = document.querySelector('[class^="ModelActionCard_modelActionCard"]');
if (actionCard) {
actionCard.parentNode.insertBefore(div1, actionCard);
}
break;
}
} else if (site === 'civitai') {
if (classNames[i].includes('mantine-ContainerGrid-root')) {
found = true;
observer.disconnect(); // 停止观察
// 获取目标 div (divroot)
const divroot = element;
// 确保 divroot 存在且有子节点
if (divroot && divroot.children.length > 0) {
// 获取第一个子节点 (class="mantine-ContainerGrid-col")
const firstChild = divroot.children[0];
// 确保第一个子节点存在
if (firstChild) {
// 将 div1 插入到第一个子节点的最前面
firstChild.insertBefore(div1, firstChild.firstChild); // 注意这里使用了 firstChild.firstChild
div1.style.display = 'block'; // 确保 div1 可见
} else {
console.warn("Civitai: 第一个子节点不存在");
}
} else {
console.warn("Civitai: divroot 不存在或没有子节点");
}
break;
}
}
break;
}
});
}
});
});
observer.observe(document.body, {childList: true, subtree: true});
}
// ---------------------------------------------------------------
// 主函数
// ---------------------------------------------------------------
(function () {
const site = currentSite();
// console.log("Current site:", site);
const buttonsDiv = createButtons(site);
if (site === 'liblib' || site === 'civitai') {
createObserver(site, buttonsDiv);
} else {
console.log("Unsupported site.");
}
})();
})();