// ==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(); })();