// ==UserScript==
// @name Pixiv作品解析下载
// @namespace http://309096184.xyz/
// @version 1.0.0
// @description Pixiv作品页面一键解析并下载原图/打包ZIP/支持单图下载
// @author 【Rain】黯笙
// @icon http://309096184.xyz/烧包/三楼插件/jbtx.png
// @match https://www.pixiv.net/artworks/*
// @grant GM_xmlhttpRequest
// @grant GM_download
// @run-at document-start
// @license 黯笙
// @require https://cdnjs.cloudflare.com/ajax/libs/jszip/3.10.1/jszip.min.js
// ==/UserScript==
(function() {
'use strict';
const CUSTOM_HEADERS = { 'Referer': 'https://www.pixiv.net/' };
const textBtn = "解析";
const textClose = "关闭";
const loadingTipText = "正在解析作品...";
const textDonate = "赞助作者";
const donateTip = "感谢支持作者【Rain】黯笙";
const donateImgUrl = "http://309096184.xyz/x/zz.png";
const bgColor = "#1c1c1e";
const cardColor = "#252527";
const textColor = "#e5e5e7";
const subTextColor = "#8e8e93";
const btnBgColor = cardColor;
const innerGlow = "inset 0 0 6px rgba(255,255,255,0.1)";
const innerGlowPressed = "inset 0 0 3px rgba(255,255,255,0.06)";
function createFloatingBtn() {
const btn = document.createElement("button");
btn.textContent = textBtn;
btn.id = "pixivParseFloatBtn";
Object.assign(btn.style, {
position: "fixed",
top: "50%",
right: "20px",
transform: "translateY(-50%)",
zIndex: "2147483647",
width: "56px", height: "56px", borderRadius: "50%",
background: btnBgColor, color: "#ffffff",
border: "none", outline: "none",
fontSize: "14px", fontWeight: "600",
fontFamily: "-apple-system, BlinkMacSystemFont, 'SF Pro Text', sans-serif",
boxShadow: innerGlow,
cursor: "pointer",
transition: "box-shadow 0.15s, transform 0.15s",
WebkitTapHighlightColor: "transparent"
});
btn.addEventListener("pointerdown", () => {
btn.style.boxShadow = innerGlowPressed;
btn.style.transform = "translateY(-50%) scale(0.92)";
});
btn.addEventListener("pointerup", () => {
btn.style.boxShadow = innerGlow;
btn.style.transform = "translateY(-50%) scale(1)";
});
return btn;
}
function showLoadingToast(msg) {
const old = document.getElementById("pixivLoadingToast");
if (old) old.remove();
const toast = document.createElement("div");
toast.id = "pixivLoadingToast";
toast.textContent = msg;
Object.assign(toast.style, {
position: "fixed", top: "50%", left: "50%", transform: "translate(-50%,-50%)",
background: cardColor, color: textColor,
padding: "14px 24px", borderRadius: "20px",
fontSize: "15px", zIndex: "2147483647",
fontFamily: "-apple-system, BlinkMacSystemFont, 'SF Pro Text', sans-serif",
boxShadow: innerGlow,
whiteSpace: "pre-line",
textAlign: "center"
});
document.body.appendChild(toast);
return toast;
}
function copyText(text) {
if (navigator.clipboard && window.isSecureContext) {
navigator.clipboard.writeText(text).then(() => alert("已复制")).catch(() => alert("复制失败"));
} else {
const ta = document.createElement("textarea");
ta.value = text; ta.style.position = "fixed"; ta.style.opacity = "0";
document.body.appendChild(ta); ta.select();
try { document.execCommand("copy"); alert("已复制"); } catch (e) { alert("复制失败"); }
document.body.removeChild(ta);
}
}
function createPanelButton(text, callback) {
const btn = document.createElement("button");
btn.textContent = text;
Object.assign(btn.style, {
display: "block", width: "100%",
background: btnBgColor, color: "#ffffff",
border: "none", outline: "none",
borderRadius: "14px",
padding: "12px", fontSize: "16px", fontWeight: "500",
fontFamily: "-apple-system, BlinkMacSystemFont, 'SF Pro Text', sans-serif",
marginBottom: "12px",
boxShadow: innerGlow,
cursor: "pointer",
transition: "box-shadow 0.15s, transform 0.15s",
WebkitTapHighlightColor: "transparent"
});
btn.addEventListener("pointerdown", () => {
btn.style.boxShadow = innerGlowPressed;
btn.style.transform = "scale(0.98)";
});
btn.addEventListener("pointerup", () => {
btn.style.boxShadow = innerGlow;
btn.style.transform = "scale(1)";
});
btn.onclick = callback;
return btn;
}
function downloadFile(url, filename) {
GM_download({ url, name: filename, saveAs: false });
}
async function fetchPixivData() {
const match = location.href.match(/artworks\/(\d+)/);
if (!match) throw new Error("非作品页面");
const illustId = match[1];
const [detailResp, pagesResp] = await Promise.all([
fetch(`https://www.pixiv.net/ajax/illust/${illustId}`, { credentials: 'include' }),
fetch(`https://www.pixiv.net/ajax/illust/${illustId}/pages`, { credentials: 'include' })
]);
const detail = await detailResp.json();
const pages = await pagesResp.json();
if (detail.error || !detail.body) throw new Error(detail.message || "无法获取作品信息");
if (pages.error || !pages.body) throw new Error(pages.message || "无法获取图片列表");
const illust = detail.body;
const imageUrls = pages.body.map(p => p.urls.original || p.urls.regular);
return {
title: illust.illustTitle || illustId,
desc: illust.illustComment || "",
author: {
name: illust.userName || "",
avatar: illust.userImage || "",
id: illust.userId || ""
},
images: imageUrls,
type: "image"
};
}
async function packAndDownload(images, baseName) {
const loading = showLoadingToast("准备打包...");
const zip = new JSZip();
const folder = zip.folder(baseName);
let failed = 0;
const total = images.length;
for (let i = 0; i < total; i++) {
const url = images[i];
const ext = url.split('.').pop().split('?')[0] || 'jpg';
const filename = `${String(i + 1).padStart(3, '0')}.${ext}`;
loading.textContent = `下载中 ${i + 1}/${total}`;
try {
const blob = await new Promise((resolve, reject) => {
GM_xmlhttpRequest({
method: 'GET',
url: url,
headers: CUSTOM_HEADERS,
responseType: 'blob',
timeout: 15000,
onload: (res) => {
if (res.status === 200 && res.response instanceof Blob) resolve(res.response);
else reject(new Error('状态码 ' + res.status));
},
onerror: reject,
ontimeout: () => reject(new Error('超时'))
});
});
folder.file(filename, blob);
} catch (e) {
failed++;
console.error('图片下载失败:', filename, e);
}
}
if (failed === total) {
loading.textContent = "所有图片下载失败";
setTimeout(() => loading.remove(), 3000);
return;
}
loading.textContent = "正在生成 ZIP...";
const zipBlob = await zip.generateAsync({ type: 'blob', compression: 'STORE' });
const zipUrl = URL.createObjectURL(zipBlob);
const a = document.createElement('a');
a.href = zipUrl;
a.download = baseName + '.zip';
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
setTimeout(() => URL.revokeObjectURL(zipUrl), 5000);
loading.textContent = "ZIP 下载已开始";
setTimeout(() => loading.remove(), 2000);
}
function renderResultPage(data) {
const mask = document.createElement("div");
Object.assign(mask.style, {
position: "fixed", inset: "0",
background: bgColor,
zIndex: "2147483646", overflowY: "auto",
paddingTop: "90px", paddingLeft: "16px", paddingRight: "16px", paddingBottom: "40px",
fontFamily: "-apple-system, BlinkMacSystemFont, 'SF Pro Text', 'Helvetica Neue', sans-serif"
});
const topBar = document.createElement("div");
Object.assign(topBar.style, {
position: "fixed", top: "0", left: "0", right: "0",
background: cardColor, zIndex: "999",
padding: "12px 16px",
display: "flex", justifyContent: "space-between", alignItems: "center",
boxShadow: innerGlow,
borderBottom: "1px solid rgba(255,255,255,0.05)"
});
const titleSpan = document.createElement("span");
titleSpan.style.cssText = `display:flex;align-items:center;color:${textColor};font-size:18px;font-weight:600;`;
const badge = document.createElement("span");
badge.style.cssText = "display:inline-block;width:8px;height:8px;background:#0096fa;border-radius:2px;margin-right:8px;";
titleSpan.appendChild(badge);
titleSpan.appendChild(document.createTextNode("作品图集"));
const btnGroup = document.createElement("div");
btnGroup.style.cssText = "display:flex;gap:8px;";
const donateBtn = document.createElement("button");
donateBtn.textContent = textDonate;
Object.assign(donateBtn.style, {
background: btnBgColor, color: "#ffffff",
border: "none", outline: "none",
borderRadius: "14px", padding: "8px 14px",
fontSize: "14px", fontWeight: "500", fontFamily: "inherit",
boxShadow: innerGlow, cursor: "pointer",
WebkitTapHighlightColor: "transparent"
});
donateBtn.addEventListener("pointerdown", () => donateBtn.style.boxShadow = innerGlowPressed);
donateBtn.addEventListener("pointerup", () => donateBtn.style.boxShadow = innerGlow);
donateBtn.onclick = () => {
const dmask = document.createElement("div");
Object.assign(dmask.style, {
position: "fixed", inset: "0", background: "rgba(0,0,0,0.7)",
zIndex: "2147483648", display: "flex", alignItems: "center", justifyContent: "center"
});
const card = document.createElement("div");
Object.assign(card.style, {
background: cardColor, borderRadius: "24px", padding: "24px",
maxWidth: "320px", width: "90%", textAlign: "center",
boxShadow: innerGlow
});
const tip = document.createElement("div");
tip.textContent = donateTip;
tip.style.cssText = `color:${textColor};font-size:17px;font-weight:600;margin-bottom:20px;`;
const qrWrap = document.createElement("div");
qrWrap.style.cssText = `width:100%;min-height:220px;background:${bgColor};border-radius:18px;display:flex;align-items:center;justify-content:center;color:${subTextColor};font-size:14px;margin-bottom:20px;box-shadow:${innerGlow};`;
qrWrap.textContent = "加载中...";
const closeDonate = document.createElement("button");
closeDonate.textContent = textClose;
Object.assign(closeDonate.style, {
background: btnBgColor, color: "#ffffff", border: "none", outline: "none",
borderRadius: "14px",
padding: "10px 32px", fontSize: "16px", fontWeight: "500",
fontFamily: "-apple-system, sans-serif", boxShadow: innerGlow, cursor: "pointer",
WebkitTapHighlightColor: "transparent"
});
closeDonate.onclick = () => dmask.remove();
card.appendChild(tip); card.appendChild(qrWrap); card.appendChild(closeDonate);
dmask.appendChild(card);
document.body.appendChild(dmask);
dmask.onclick = (e) => { if (e.target === dmask) dmask.remove(); };
GM_xmlhttpRequest({
method: "GET", url: donateImgUrl, responseType: "blob", timeout: 10000,
onload: (res) => {
if (res.status >= 200 && res.status < 300) {
const blobUrl = URL.createObjectURL(res.response);
const img = document.createElement("img");
img.src = blobUrl;
img.style.cssText = "width:100%;height:auto;object-fit:contain;border-radius:12px;";
qrWrap.innerHTML = ""; qrWrap.appendChild(img);
} else qrWrap.innerHTML = '加载失败';
},
onerror: () => qrWrap.innerHTML = '网络错误',
ontimeout: () => qrWrap.innerHTML = '超时'
});
};
const closeBtn = document.createElement("button");
closeBtn.textContent = textClose;
Object.assign(closeBtn.style, {
background: btnBgColor, color: "#ffffff",
border: "none", outline: "none",
borderRadius: "14px", padding: "8px 18px",
fontSize: "14px", fontWeight: "500", fontFamily: "inherit",
boxShadow: innerGlow, cursor: "pointer",
WebkitTapHighlightColor: "transparent"
});
closeBtn.addEventListener("pointerdown", () => closeBtn.style.boxShadow = innerGlowPressed);
closeBtn.addEventListener("pointerup", () => closeBtn.style.boxShadow = innerGlow);
closeBtn.onclick = () => mask.remove();
btnGroup.appendChild(donateBtn);
btnGroup.appendChild(closeBtn);
topBar.appendChild(titleSpan);
topBar.appendChild(btnGroup);
mask.appendChild(topBar);
// 关键调整:桌面端内容区域最大宽度520px,居中,移动端自动100%
const content = document.createElement("div");
content.style.cssText = "max-width:520px; width:100%; margin:0 auto; box-sizing:border-box;";
if (data.title) {
const descCard = document.createElement("div");
Object.assign(descCard.style, {
background: cardColor, borderRadius: "20px", padding: "16px",
marginBottom: "20px", color: textColor, fontSize: "15px", lineHeight: "1.6",
boxShadow: innerGlow,
display: "flex", justifyContent: "space-between", alignItems: "flex-start", gap: "12px"
});
const descText = document.createElement("div");
descText.textContent = data.title;
descText.style.cssText = "white-space:pre-wrap;word-break:break-word;flex:1;";
const copyBtn = document.createElement("button");
copyBtn.textContent = "复制标题";
Object.assign(copyBtn.style, {
background: btnBgColor, color: "#ffffff",
border: "none", outline: "none",
borderRadius: "8px",
padding: "4px 12px", fontSize: "12px", fontWeight: "500",
flexShrink: "0", marginTop: "2px",
boxShadow: innerGlow, cursor: "pointer",
WebkitTapHighlightColor: "transparent"
});
copyBtn.onclick = () => copyText(data.title);
descCard.appendChild(descText);
descCard.appendChild(copyBtn);
content.appendChild(descCard);
}
if (data.author && data.author.name) {
const authorCard = document.createElement("div");
Object.assign(authorCard.style, {
display: "flex", alignItems: "center", gap: "14px",
background: cardColor, borderRadius: "20px", padding: "14px",
marginBottom: "20px", boxShadow: innerGlow
});
if (data.author.avatar) {
const avt = document.createElement("img");
avt.src = data.author.avatar;
Object.assign(avt.style, {
width: "48px", height: "48px", borderRadius: "50%",
objectFit: "cover", border: "2px solid rgba(255,255,255,0.15)"
});
authorCard.appendChild(avt);
}
const nameWrap = document.createElement("div");
const nameDiv = document.createElement("div");
nameDiv.style.cssText = `color:${textColor};font-size:16px;font-weight:500;`;
nameDiv.textContent = data.author.name;
nameWrap.appendChild(nameDiv);
if (data.author.id) {
const idDiv = document.createElement("div");
idDiv.style.cssText = `color:${subTextColor};font-size:13px;`;
idDiv.textContent = "ID: " + data.author.id;
nameWrap.appendChild(idDiv);
}
authorCard.appendChild(nameWrap);
content.appendChild(authorCard);
}
if (data.images && data.images.length > 0) {
if (data.images.length > 1) {
content.appendChild(createPanelButton(
`一键打包下载 (${data.images.length}张 ZIP)`,
() => packAndDownload(data.images, data.title || "pixiv_album")
));
content.appendChild(createPanelButton(
`一键下载全部 (${data.images.length}张)`,
() => {
data.images.forEach((url, idx) => {
setTimeout(() => downloadFile(url, `${data.title || "image"}_${idx+1}.jpg`), idx * 300);
});
alert("开始批量下载,请允许浏览器多次保存");
}
));
}
data.images.forEach((imgUrl, idx) => {
const img = document.createElement("img");
img.src = imgUrl;
img.style.cssText = `width:100%;border-radius:16px;display:block;margin-bottom:10px;box-shadow:${innerGlow};`;
content.appendChild(img);
content.appendChild(createPanelButton(
`下载图片 ${idx + 1}`,
() => downloadFile(imgUrl, `${data.title || "image"}_${idx+1}.jpg`)
));
});
}
mask.appendChild(content);
document.body.appendChild(mask);
}
async function startParse() {
const loading = showLoadingToast(loadingTipText);
try {
const data = await fetchPixivData();
loading.remove();
renderResultPage(data);
} catch (e) {
loading.remove();
alert("解析失败:" + e.message);
console.error(e);
}
}
function injectButton() {
if (document.getElementById("pixivParseFloatBtn")) return;
const btn = createFloatingBtn();
btn.onclick = startParse;
document.body.appendChild(btn);
}
function removeButton() {
const btn = document.getElementById("pixivParseFloatBtn");
if (btn) btn.remove();
}
let lastUrl = location.href;
function checkPage() {
if (location.href !== lastUrl) {
lastUrl = location.href;
removeButton();
injectButton();
}
}
function waitForPage() {
return new Promise(resolve => {
if (document.readyState === 'complete') resolve();
else window.addEventListener('load', resolve, { once: true });
});
}
async function main() {
await waitForPage();
injectButton();
const observer = new MutationObserver(checkPage);
const titleEl = document.querySelector('title');
if (titleEl) observer.observe(titleEl, { childList: true });
const origPush = history.pushState;
history.pushState = function(...args) {
origPush.apply(this, args);
setTimeout(checkPage, 200);
};
const origReplace = history.replaceState;
history.replaceState = function(...args) {
origReplace.apply(this, args);
setTimeout(checkPage, 200);
};
window.addEventListener('popstate', () => setTimeout(checkPage, 200));
}
main();
})();