您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Adds a button to download all FANBOX post images as a ZIP archive
// ==UserScript== // @name FANBOX Post Images Downloader // @description Adds a button to download all FANBOX post images as a ZIP archive // @version 1.0.3 // @author BreatFR // @namespace http://gitlab.com/breatfr // @match *://*.fanbox.cc/* // @require https://cdn.jsdelivr.net/npm/[email protected]/umd/index.min.js // @copyright 2025, BreatFR (https://breat.fr) // @icon https://s.pximg.net/common/images/fanbox/apple-touch-icon.png // @license AGPL-3.0-or-later; https://www.gnu.org/licenses/agpl-3.0.txt // @grant GM_xmlhttpRequest // @grant GM_download // @run-at document-end // ==/UserScript== (function() { 'use strict'; console.log('[FANBOX Post Images Downloader] Script loaded'); const style = document.createElement('style'); style.textContent = ` .fanbox-download-btn { align-items: center; background-color: rgba(24, 24, 24, .2); border: none; border-radius: .5em; bottom: 1em; color: #fff; cursor: pointer; display: flex; flex-direction: column; font-family: poppins, cursive; font-size: 1.15rem; gap: 1em; justify-content: center; left: 1em; line-height: 1.5; padding: .5em 1em; position: fixed; transition: background-color .3s ease, box-shadow .3s ease; z-index: 9999; } .fanbox-download-btn:hover { background-color: rgba(255, 80, 80, .85); box-shadow: 0 0 2em rgba(255, 80, 80, .85); } @keyframes spinLoop { from { transform: rotate(0deg); } to { transform: rotate(360deg); } } .fanbox-btn-icon.spin { animation: spinLoop 1s linear infinite; } @keyframes pulseLoop { 0% { transform: scale(1); opacity: 1; } 50% { transform: scale(1.1); opacity: 0.7; } 100% { transform: scale(1); opacity: 1; } } .fanbox-btn-icon.pulse { animation: pulseLoop 1.8s ease-in-out infinite; } .fanbox-btn-icon { font-size: 3em; line-height: 1em; } #top { bottom: 1em; font-size: 1.2em !important; position: fixed; right: 1em; } `; document.head.appendChild(style); // Clean images links function cleanFanboxDownloadLink(url) { return url.replace( /https:\/\/downloads\.fanbox\.cc\/images\/post\/(\d+)\/w\/1200\/([^/]+)$/, 'https://downloads.fanbox.cc/images/post/$1/$2' ); } (function () { const selector = 'div[class*="Cover__CoverImage"]'; function cleanPixivCoverUrl(url) { return url.replace( /https:\/\/pixiv\.pximg\.net\/c\/[^/]+\/(fanbox\/public\/images\/post\/\d+\/cover\/[^.]+\.\w+)/, 'https://pixiv.pximg.net/$1' ); } function transformCoverDiv(div) { if (!div || div.tagName !== 'DIV') return; const style = div.getAttribute('style'); if (!style || !style.includes('url(')) { console.warn('[Fanbox Collector] ⏳ Style not ready yet...'); return; } const match = style.match(/url\(["']?(.*?)["']?\)/); if (!match) return; const rawUrl = match[1]; const cleanedUrl = cleanPixivCoverUrl(rawUrl); const link = document.createElement('a'); link.href = cleanedUrl; link.target = '_blank'; link.rel = 'noopener noreferrer'; link.className = div.className; const img = document.createElement('img'); img.src = cleanedUrl; img.alt = 'Cover'; img.style.width = '100%'; img.style.height = 'auto'; img.style.display = 'block'; link.appendChild(img); div.replaceWith(link); console.log('[Fanbox Collector] ✅ Cover div replaced with <a><img>:', cleanedUrl); } const observer = new MutationObserver(() => { const div = document.querySelector(selector); const alreadyTransformed = document.querySelector(`${selector} img`); if (div && !alreadyTransformed) { const style = div.getAttribute('style'); if (style && style.includes('url(')) { transformCoverDiv(div); observer.disconnect(); } } }); observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style'] }); })(); // Download button function loadImageFromBlob(blob) { return new Promise((resolve, reject) => { const img = new Image(); img.crossOrigin = 'anonymous'; img.onload = () => resolve(img); img.onerror = reject; img.src = URL.createObjectURL(blob); }); } function hasTransparency(img) { const canvas = document.createElement('canvas'); canvas.width = img.naturalWidth; canvas.height = img.naturalHeight; const ctx = canvas.getContext('2d'); ctx.drawImage(img, 0, 0); const { data } = ctx.getImageData(0, 0, canvas.width, canvas.height); for (let i = 3; i < data.length; i += 4) { if (data[i] < 255) return true; } return false; } function convertToJPEG(img) { const canvas = document.createElement('canvas'); canvas.width = img.naturalWidth; canvas.height = img.naturalHeight; const ctx = canvas.getContext('2d'); ctx.drawImage(img, 0, 0); return canvas.toDataURL('image/jpeg', 1.0); } function setButtonContent(btn, icon, label) { let iconEl = btn.querySelector('.fanbox-btn-icon'); let labelEl = btn.querySelector('.fanbox-btn-label'); if (!iconEl) { iconEl = document.createElement('div'); iconEl.className = 'fanbox-btn-icon'; btn.appendChild(iconEl); } if (!labelEl) { labelEl = document.createElement('div'); labelEl.className = 'fanbox-btn-label'; btn.appendChild(labelEl); } iconEl.textContent = icon; labelEl.textContent = label; } function setIconAnimation(btn, type) { const icon = btn.querySelector('.fanbox-btn-icon'); icon.classList.remove('spin', 'pulse'); void icon.offsetWidth; if (type) icon.classList.add(type); } function updateIconWithAnimation(btn, icon, label, animationClass) { setButtonContent(btn, icon, label); requestAnimationFrame(() => setIconAnimation(btn, animationClass)); } async function forceScrollToLoadContent() { console.log('[Fanbox Collector] Starting auto-scroll to load lazy images'); const step = 300; const delay = 500; const maxScroll = document.body.scrollHeight; for (let scrollY = 0; scrollY < maxScroll; scrollY += step) { window.scrollTo({ top: scrollY, behavior: 'smooth' }); await new Promise(resolve => setTimeout(resolve, delay)); } console.log('[Fanbox Collector] Scroll complete, waiting for final content'); await new Promise(resolve => setTimeout(resolve, 1500)); } function downloadImage(url) { console.log(`[Fanbox Collector] Requesting image: ${url}`); return new Promise((resolve, reject) => { GM_xmlhttpRequest({ method: 'GET', url: url, responseType: 'blob', onload: function(response) { if (response.status === 200 && response.response.size > 0) { resolve({ blob: response.response, filename: url.split('/').pop() }); } else { reject(new Error(`Download failed or empty blob for ${url}`)); } }, onerror: function(err) { reject(err); } }); }); } function blobToUint8Array(blob) { return new Promise((resolve, reject) => { const reader = new FileReader(); reader.onload = () => resolve(new Uint8Array(reader.result)); reader.onerror = reject; reader.readAsArrayBuffer(blob); }); } function addDownloadButton() { const cleanUrl = location.href.split('?')[0]; const isPostPage = /^https:\/\/(?:www\.fanbox\.cc\/@[^/]+|[^.]+\.fanbox\.cc)\/posts\/\d+$/.test(cleanUrl); if (!isPostPage) return; console.log('[FANBOX Post Images Downloader] Injecting download button'); const btn = document.createElement('button'); btn.className = 'fanbox-download-btn'; btn.innerHTML = ` <div class="fanbox-btn-icon">📦</div> <div class="fanbox-btn-label">Download all post images</div> `; document.body.appendChild(btn); btn.addEventListener('click', async () => { console.log('[FANBOX Post Images Downloader] Button clicked'); btn.disabled = true; updateIconWithAnimation(btn, '🌀', 'Scrolling to load images...', 'spin'); await forceScrollToLoadContent(); console.log('[FANBOX Post Images Downloader] Collecting image links...'); const anchors = Array.from(document.querySelectorAll('a[href]')) .map(a => a.href) .map(href => { if (href.includes('downloads.fanbox.cc/images/post/')) { return cleanFanboxDownloadLink(href); } if (href.includes('pixiv.pximg.net/fanbox/public/images/post/')) { return href; } return null; }) .filter(href => href && /\.(jpeg|jpg|png|webp|gif)$/i.test(href)); console.log('[FANBOX Post Images Downloader] Final image list:', anchors); console.log(`[FANBOX Post Images Downloader] Found ${anchors.length} image links`); if (anchors.length === 0) { alert('No Fanbox image links found.'); btn.disabled = false; updateIconWithAnimation(btn, '📦', 'Download all post images', null); return; } const files = {}; let count = 0; for (const url of anchors) { const filename = url.split('/').pop(); try { const { blob } = await downloadImage(url); if (!blob || blob.size === 0) { console.warn(`[FANBOX Post Images Downloader] Skipped empty blob: ${filename}`); continue; } let finalName = filename; let uint8; const ext = filename.split('.').pop().toLowerCase(); if (ext === 'png') { try { const img = await loadImageFromBlob(blob); if (!hasTransparency(img)) { const jpegDataUrl = convertToJPEG(img); const jpegBlob = await (await fetch(jpegDataUrl)).blob(); uint8 = await blobToUint8Array(jpegBlob); finalName = filename.replace(/\.png$/i, '.jpeg'); console.log(`[FANBOX Post Images Downloader] Converted PNG to JPEG: ${finalName}`); } else { uint8 = await blobToUint8Array(blob); console.log(`[FANBOX Post Images Downloader] PNG with transparency kept: ${filename}`); } } catch (e) { console.warn(`[FANBOX Post Images Downloader] Transparency check failed for ${filename}`, e); uint8 = await blobToUint8Array(blob); } } else { uint8 = await blobToUint8Array(blob); } // ✅ Harmonisation ici if (url.includes('pixiv.pximg.net/fanbox/public/images/post/')) { const extFinal = finalName.split('.').pop().toLowerCase(); finalName = `cover.${extFinal === 'jpg' ? 'jpeg' : extFinal}`; } files[finalName] = uint8; console.log(`[FANBOX Post Images Downloader] Added to ZIP: ${finalName} (${uint8.length} bytes)`); count++; updateIconWithAnimation(btn, '📥', `Downloading image ${count}/${anchors.length}`, 'pulse'); } catch (e) { if (e.message.includes('Quota reached')) { console.warn('[FANBOX Post Images Downloader] Quota reached, aborting download.'); updateIconWithAnimation(btn, '⏳', 'Quota atteint, réessaye demain', null); break; } console.warn(`[FANBOX Post Images Downloader] Failed to download ${url}`, e); } } if (count === 0) { alert('All image downloads failed.'); btn.disabled = false; updateIconWithAnimation(btn, '📦', 'Download all post images', null); return; } updateIconWithAnimation(btn, '📦', `Creating ZIP (${count} images)...`, null); try { console.log('[FANBOX Post Images Downloader] Compressing with fflate...'); const zipped = fflate.zipSync(files); const blob = new Blob([zipped], { type: 'application/zip' }); const titleElement = document.querySelector('h1[class*="styled__PostTitle-sc-"]'); const zipName = titleElement ? titleElement.textContent.trim().replace(/[\\/:*?"<>|]/g, '_') : 'fanbox_images'; console.log('[FANBOX Post Images Downloader] Triggering GM_download...'); GM_download({ url: URL.createObjectURL(blob), name: `${zipName}.zip`, saveAs: true, onerror: err => { console.error('[FANBOX Post Images Downloader] ❌ GM_download failed:', err); alert('ZIP download failed.'); } }); } catch (e) { console.error('[FANBOX Post Images Downloader] ❌ ZIP compression error:', e); alert('ZIP creation failed. Check console for details.'); } updateIconWithAnimation(btn, '✅', `${count} images downloaded`, null); setTimeout(() => { updateIconWithAnimation(btn, '📦', 'Download all post images', null); btn.disabled = false; }, 3000); }); } addDownloadButton(); // Back to top const btn = document.createElement('button'); btn.id = 'top'; btn.setAttribute('aria-label', 'Scroll to top'); btn.setAttribute('title', 'Scroll to top'); setButtonContent(btn, '🔝', '') document.body.appendChild(btn); const mybutton = document.getElementById("top"); window.onscroll = function () { scrollFunction(); }; function scrollFunction() { if ( document.body.scrollTop > 20 || document.documentElement.scrollTop > 20 ) { mybutton.style.display = "block"; } else { mybutton.style.display = "none"; } } mybutton.addEventListener("click", backToTop); function backToTop() { window.scrollTo({ top: 0, behavior: 'smooth' }); } })();