缓存链接时,同时抓取插画的来源链接和标题,并在牌堆导出时包含所有信息,用 \n 分隔。小键盘可以实现翻页,有更换需求请自行处理。可以打tag!理论上适配所有pixiv的界面(包括背景图)
当前为
// ==UserScript==
// @name Pixiv图片牌堆生成器
// @version 1.25
// @description 缓存链接时,同时抓取插画的来源链接和标题,并在牌堆导出时包含所有信息,用 \n 分隔。小键盘可以实现翻页,有更换需求请自行处理。可以打tag!理论上适配所有pixiv的界面(包括背景图)
// @author Yog-Sothoth
// @match *://www.pixiv.net/*
// @grant GM_getValue
// @grant GM_setValue
// @grant GM_deleteValue
// @grant GM_registerMenuCommand
// @grant GM_addStyle
// @license MIT
// @run-at document-idle
// @namespace https://greasyfork.org/users/1397928
// ==/UserScript==
(function() {
'use strict';
const STORAGE_KEY = 'pixivCatTaggedLinks_JSON';
const CACHE_NAME = 'PixivCat_Tagged_Export.json';
const PIXIV_BASE_URL = 'https://www.pixiv.net';
const IMG_SELECTOR_LIST = 'img[src*="i.pximg.net/c/"]:not([data-tag-processed])';
const IMG_SELECTOR_ARTWORK = 'img[src*="i.pximg.net/img-original/img"]:not([data-tag-processed])';
const ANCESTOR_LEVEL = 6;
const IS_ARTWORK_PAGE = window.location.pathname.includes('/artworks/');
GM_addStyle(`
#tag-select-menu {
position: absolute;
right: 40px;
top: 50%;
transform: translateY(-50%);
width: 280px;
background: #333333;
border: 1px solid #222222;
box-shadow: 0 4px 8px rgba(0,0,0,0.5);
padding: 10px;
border-radius: 4px;
z-index: 1001;
color: white;
box-sizing: border-box;
text-align: left;
font-size: 13px;
}
#tag-select-menu input {
width: 100%;
padding: 4px;
margin-top: 3px
margin-bottom: 8px;
border: 1px solid #495057;
background-color: #f8f9fa;
color: #212529;
border-radius: 3px;
box-sizing: border-box;
}
#tag-select-menu button {
width: 100%;
background: #4CAF50;
color: white;
border: none;
padding: 5px 10px;
cursor: pointer;
border-radius: 3px;
transition: background-color .2s;
}
#tag-select-menu button:hover {
background: #45a049;
}
`);
function getIllustrationIdentifier(catLink) {
const match = catLink.match(/(\d+)_p(\d+)/);
if (match) {
return `${match[1]}_p${match[2]}`;
}
return catLink;
}
function normalizeCatLink(catLink) {
const identifier = getIllustrationIdentifier(catLink);
if (identifier.includes('_p')) {
const parts = identifier.split('_p');
const id = parts[0];
const page = parts[1];
const extMatch = catLink.match(/\.([a-z]{3,4})$/i);
const ext = extMatch ? extMatch[1].toLowerCase() : 'png';
const dateMatch = catLink.match(/img\/(\d{4}\/\d{2}\/\d{2}\/\d{2}\/\d{2}\/\d{2})/);
const datePath = dateMatch ? dateMatch[1] : '2099/01/01/00/00/00';
return `https://i.pixiv.cat/img-original/img/${datePath}/${id}_p${page}.${ext}`;
}
return catLink;
}
function getCatLink(originalSrc) {
return originalSrc.replace('i.pximg.net', 'i.pixiv.cat');
}
function formatToCQImage(cachedItem) {
try {
const data = JSON.parse(cachedItem);
return (
`[CQ:image,file=${data.catLink}]` +
`${data.sourceLink}` +
`\n${data.title}`
);
} catch (e) {
return `[CQ:image,file=N/A] (Error reading source info for: ${cachedItem})`;
}
}
function getIllustrationSourceInfo(imgElement) {
if (IS_ARTWORK_PAGE) {
const urlMatch = window.location.pathname.match(/\/artworks\/(\d+)/);
const sourceLink = urlMatch ? PIXIV_BASE_URL + urlMatch[0] : 'Source: N/A (URL)';
let title = document.title.replace(/ | - pixiv$/, '').trim();
if (!title || title.includes('pixiv')) {
title = imgElement.alt.trim() || 'No Title Found (Alt)';
}
return { sourceLink, title };
} else {
try {
let liAncestor = imgElement.closest('li');
if (!liAncestor) return { sourceLink: 'Source: N/A (li)', title: 'Title: N/A (li)' };
let firstDiv = liAncestor.querySelector(':scope > div');
if (!firstDiv) return { sourceLink: 'Source: N/A (div1)', title: 'Title: N/A (div1)' };
let secondDiv = Array.from(firstDiv.children).filter(el => el.tagName === 'DIV')[1];
if (!secondDiv) {
secondDiv = liAncestor.querySelectorAll('div > div')[1];
if (!secondDiv) return { sourceLink: 'Source: N/A (div2)', title: 'Title: N/A (div2)' };
}
let aElement = secondDiv.querySelector('a');
if (aElement) {
const relativeHref = aElement.getAttribute('href') || '';
const sourceLink = relativeHref.startsWith(PIXIV_BASE_URL) ? relativeHref : PIXIV_BASE_URL + relativeHref;
const title = aElement.textContent.trim() || 'No Title Found';
return { sourceLink, title };
}
} catch (e) {
console.error("Error retrieving list source info:", e);
}
return { sourceLink: 'Source: Error', title: 'Title: Error' };
}
}
function getCachedData() {
try {
return JSON.parse(GM_getValue(STORAGE_KEY, '{}'));
} catch (e) {
return {};
}
}
function setCachedData(data) {
GM_setValue(STORAGE_KEY, JSON.stringify(data));
}
function getLinkTags(catLink) {
const data = getCachedData();
const targetIdentifier = getIllustrationIdentifier(catLink);
const tags = [];
if (targetIdentifier === catLink) return tags;
for (const tag in data) {
if (data[tag].some(item => {
try {
const cachedIdentifier = getIllustrationIdentifier(JSON.parse(item).catLink);
return cachedIdentifier === targetIdentifier;
} catch (e) {
return false;
}
})) {
tags.push(tag);
}
}
return tags;
}
function isLinkCached(catLink) {
return getLinkTags(catLink).length > 0;
}
function addLinkToTag(itemJsonString, catLink, tag) {
const data = getCachedData();
const finalTag = tag || '未分类';
let isAdded = false;
const targetIdentifier = getIllustrationIdentifier(catLink);
const normalizedCatLink = normalizeCatLink(catLink);
if (targetIdentifier === catLink) return false;
const sourceInfo = JSON.parse(itemJsonString);
const itemToCache = JSON.stringify({
catLink: normalizedCatLink,
sourceLink: sourceInfo.sourceLink,
title: sourceInfo.title
});
if (!data[finalTag]) {
data[finalTag] = [];
}
const existingInTag = data[finalTag].some(item => {
try {
return getIllustrationIdentifier(JSON.parse(item).catLink) === targetIdentifier;
} catch (e) {
return false;
}
});
if (!existingInTag) {
data[finalTag].push(itemToCache);
setCachedData(data);
isAdded = true;
}
return isAdded;
}
function removeLinkFromAllCache(catLink) {
const data = getCachedData();
let removed = false;
const targetIdentifier = getIllustrationIdentifier(catLink);
if (targetIdentifier === catLink) return false;
for (const tag in data) {
const originalLength = data[tag].length;
data[tag] = data[tag].filter(item => {
try {
return getIllustrationIdentifier(JSON.parse(item).catLink) !== targetIdentifier;
} catch (e) {
return true;
}
});
if (data[tag].length < originalLength) {
removed = true;
}
if (data[tag].length === 0) {
delete data[tag];
}
}
if (removed) {
setCachedData(data);
}
return removed;
}
function clearCache() {
if (confirm('确定要清空所有缓存的 Pixiv Cat 链接和标签吗?')) {
GM_deleteValue(STORAGE_KEY);
alert('缓存已清空。页面需要刷新以清除按钮状态。');
location.reload();
}
}
function createTagButton(isCached) {
const button = document.createElement('div');
button.style.cssText = 'position:absolute;top:50%;right:0;transform:translateY(-50%);width:24px;height:24px;border-radius:4px;display:flex;align-items:center;justify-content:center;color:white;font-weight:bold;cursor:pointer;z-index:1000;transition:background-color .2s,transform .1s;font-family:sans-serif;font-size:16px;';
updateButtonState(button, isCached);
return button;
}
function updateButtonState(button, isCached) {
button.style.backgroundColor = isCached ? '#4CAF50' : '#2196F3';
button.innerHTML = '+';
}
function updateButtonTitle(button, catLink) {
const tags = getLinkTags(catLink);
let title = '';
if (tags.length === 0) {
title = '未被任何标签缓存 | 单击添加 | 双击取消所有标签';
} else {
title = `已缓存到: ${tags.join(', ')} | 单击添加 | 双击取消所有标签`;
}
button.title = title;
updateButtonState(button, tags.length > 0);
}
function createTagInputMenu(button, imgElement, originalSrc) {
const cachedData = getCachedData();
const existingTags = Object.keys(cachedData);
const catLink = getCatLink(originalSrc);
const sourceInfo = getIllustrationSourceInfo(imgElement);
const menu = document.createElement('div');
menu.id = 'tag-select-menu';
const info = document.createElement('div');
info.innerHTML = `
<strong style="color: #f0f0f0;">来源:</strong> <span style="word-break: break-all;">${sourceInfo.sourceLink}</span><br>
<strong style="color: #f0f0f0;">标题:</strong> ${sourceInfo.title}
<hr style="border-top: 1px solid #999; margin: 8px 0;">
<i style="font-size: 11px;">(已存在标签: ${getLinkTags(catLink).join(', ') || '无'})</i>
`;
menu.appendChild(info);
const input = document.createElement('input');
input.type = 'text';
input.placeholder = '选择或输入新标签名';
input.setAttribute('list', 'pixiv-tag-list');
const datalist = document.createElement('datalist');
datalist.id = 'pixiv-tag-list';
existingTags.forEach(tag => {
const option = document.createElement('option');
option.value = tag;
datalist.appendChild(option);
});
const confirmButton = document.createElement('button');
confirmButton.textContent = '添加并缓存到标签';
menu.appendChild(datalist);
menu.appendChild(input);
menu.appendChild(confirmButton);
confirmButton.onclick = () => {
const tag = input.value.trim();
if (!tag) {
alert('标签名不能为空!');
return;
}
const itemToCache = JSON.stringify({
catLink: catLink,
sourceLink: sourceInfo.sourceLink,
title: sourceInfo.title
});
if (addLinkToTag(itemToCache, catLink, tag)) {
alert(`已成功添加到标签: ${tag}`);
const normalizedCatLink = normalizeCatLink(catLink);
imgElement.src = normalizedCatLink;
imgElement.setAttribute('data-cat-link', normalizedCatLink);
imgElement.setAttribute('data-original-src', originalSrc);
} else {
alert(`该链接已存在于标签: ${tag}`);
}
updateButtonTitle(button, catLink);
imgElement.setAttribute('data-tag-processed', 'true');
menu.remove();
};
setTimeout(() => {
const clickHandler = (e) => {
if (!menu.contains(e.target) && e.target !== button && e.target !== input) {
menu.remove();
document.removeEventListener('click', clickHandler);
}
};
document.addEventListener('click', clickHandler);
}, 10);
return menu;
}
function showTagInputMenu(button, imgElement, originalSrc) {
if (button.parentElement.querySelector('#tag-select-menu')) {
return;
}
const menu = createTagInputMenu(button, imgElement, originalSrc);
button.parentElement.appendChild(menu);
menu.querySelector('input').focus();
}
function handleButtonClick(event) {
event.stopPropagation();
const button = event.currentTarget;
const img = button.imgElement;
const originalSrc = img.src;
if (event.detail === 1) {
showTagInputMenu(button, img, originalSrc);
}
}
function handleButtonDoubleClick(event) {
event.stopPropagation();
const button = event.currentTarget;
const img = button.imgElement;
const originalSrc = img.src;
const catLink = getCatLink(originalSrc);
if (confirm(`确定要从所有标签中移除该链接吗?\n当前标签: ${getLinkTags(catLink).join(', ') || '无'}`)) {
if (removeLinkFromAllCache(catLink)) {
alert('该链接已从所有标签中移除。');
} else {
alert('该链接未被缓存。');
}
if (img.hasAttribute('data-original-src')) {
img.src = img.getAttribute('data-original-src');
img.removeAttribute('data-original-src');
img.removeAttribute('data-cat-link');
}
updateButtonTitle(button, catLink);
}
}
function handleButtonMouseOver(event) {
const button = event.currentTarget;
const img = button.imgElement;
const catLink = getCatLink(img.src);
updateButtonTitle(button, catLink);
}
function findAncestor(element, n) {
let ancestor = element;
for (let i = 0; i < n; i++) {
if (ancestor) {
ancestor = ancestor.parentElement;
} else {
return null;
}
}
return ancestor;
}
function processImages() {
let selectors = [IMG_SELECTOR_LIST];
if (IS_ARTWORK_PAGE) {
selectors.push(IMG_SELECTOR_ARTWORK);
}
let images = [];
selectors.forEach(selector => {
images.push(...document.querySelectorAll(selector));
});
images.forEach(img => {
img.setAttribute('data-tag-processed', 'true');
let targetAncestor;
if (IS_ARTWORK_PAGE) {
let aParent = img.closest('a');
if (aParent) {
targetAncestor = findAncestor(aParent, 2);
}
if (!targetAncestor) {
targetAncestor = img.parentElement;
if (targetAncestor && window.getComputedStyle(targetAncestor).position === 'static') {
targetAncestor = targetAncestor.parentElement;
}
}
} else {
targetAncestor = findAncestor(img, ANCESTOR_LEVEL);
}
if (!targetAncestor) {
return;
}
const originalSrc = img.src;
const catLink = getCatLink(originalSrc);
const isCached = isLinkCached(catLink);
const button = createTagButton(isCached);
button.imgElement = img;
updateButtonTitle(button, catLink);
if (isCached) {
const normalizedCatLink = normalizeCatLink(catLink);
img.src = normalizedCatLink;
img.setAttribute('data-cat-link', normalizedCatLink);
img.setAttribute('data-original-src', originalSrc);
}
if (window.getComputedStyle(targetAncestor).position === 'static') {
targetAncestor.style.position = 'relative';
}
if (!targetAncestor.querySelector('div[data-state]')) {
button.addEventListener('click', handleButtonClick);
button.addEventListener('dblclick', handleButtonDoubleClick);
button.addEventListener('mouseover', handleButtonMouseOver);
targetAncestor.appendChild(button);
}
});
}
function exportTagToJson(tagToExport) {
const data = getCachedData();
const linksData = data[tagToExport];
if (!linksData || linksData.length === 0) {
alert(`标签 "${tagToExport}" 下没有缓存链接可导出。`);
return;
}
const jsonContent = {
"_title": ["pixiv图片转载"],
"_author": ["Yog-Sothoth"],
"_date": [new Date().toISOString().slice(0, 10).replace(/-/g, '/')],
"_version": ["1.0"],
};
jsonContent[tagToExport] = linksData.map(formatToCQImage);
const content = JSON.stringify(jsonContent, null, 2);
const blob = new Blob([content], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `${tagToExport}_${CACHE_NAME}`;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
const menu = document.getElementById('tag-export-menu');
if (menu) menu.remove();
alert(`标签 "${tagToExport}" 的 JSON 文件已生成并开始下载。`);
}
function createExportMenuUI() {
const existingMenu = document.getElementById('tag-export-menu');
if (existingMenu) {
existingMenu.remove();
return;
}
const data = getCachedData();
const tags = Object.keys(data).filter(tag => data[tag].length > 0);
if (tags.length === 0) {
alert('缓存中没有可导出的标签。');
return;
}
const menu = document.createElement('div');
menu.id = 'tag-export-menu';
menu.style.cssText = 'position: fixed; top: 10%; left: 50%; transform: translateX(-50%); background: #f9f9f9; border: 1px solid #ccc; box-shadow: 0 5px 15px rgba(0,0,0,0.3); padding: 15px; border-radius: 8px; z-index: 9999; max-height: 80vh; overflow-y: auto;';
const title = document.createElement('h3');
title.textContent = '⬇️ 选择要导出的标签 (JSON)';
title.style.cssText = 'margin-top: 0; border-bottom: 1px solid #ddd; padding-bottom: 5px; color: #333;';
menu.appendChild(title);
tags.forEach(tag => {
const count = data[tag].length;
const button = document.createElement('button');
button.textContent = `${tag} (${count} links)`;
button.style.cssText = 'display: block; width: 100%; padding: 8px; margin: 5px 0; background-color: #007bff; color: white; border: none; border-radius: 4px; cursor: pointer; transition: background-color 0.2s;';
button.onmouseover = () => button.style.backgroundColor = '#0056b3';
button.onmouseout = () => button.style.backgroundColor = '#007bff';
button.onclick = () => exportTagToJson(tag);
menu.appendChild(button);
});
const closeButton = document.createElement('button');
closeButton.textContent = '关闭';
closeButton.style.cssText = 'display: block; width: 100%; padding: 8px; margin-top: 15px; background-color: #6c757d; color: white; border: none; border-radius: 4px; cursor: pointer;';
closeButton.onclick = () => menu.remove();
menu.appendChild(closeButton);
document.body.appendChild(menu);
}
function registerMenuCommands() {
GM_registerMenuCommand('⬇️ 导出标签链接为 JSON', createExportMenuUI);
GM_registerMenuCommand('🗑️ 清空所有缓存链接和标签', clearCache);
}
function quickPageTurn(step) {
const url = new URL(window.location.href);
const params = url.searchParams;
let currentPage = parseInt(params.get('p'), 10);
if (isNaN(currentPage) || currentPage < 1) {
currentPage = 1;
}
let newPage = currentPage + step;
if (newPage < 1) {
newPage = 1;
}
params.set('p', newPage);
const newUrl = url.origin + url.pathname + '?' + params.toString() + url.hash;
window.location.href = newUrl;
}
function handleKeydown(event) {
const activeElement = document.activeElement;
if (activeElement.tagName === 'INPUT' || activeElement.tagName === 'TEXTAREA') {
return;
}
switch (event.code) {
case 'Numpad4':
event.preventDefault();
quickPageTurn(-1);
break;
case 'Numpad6':
event.preventDefault();
quickPageTurn(1);
break;
}
}
function observeDOMChanges() {
processImages();
const observer = new MutationObserver((mutationsList, observer) => {
for (const mutation of mutationsList) {
if (mutation.type === 'childList' && mutation.addedNodes.length > 0) {
setTimeout(processImages, 100);
break;
}
}
});
observer.observe(document.body, { childList: true, subtree: true });
}
registerMenuCommands();
observeDOMChanges();
window.addEventListener('keydown', handleKeydown);
})();