Download all images from a gallery page as a CBZ (ZIP) file.
// ==UserScript== // @name Image gallery as CBZ // @namespace i2p.schimon.cbz-gallery // @version 26.04.21 // @description Download all images from a gallery page as a CBZ (ZIP) file. // @tag cbz // @author Schimon Jehudah // @copyright 2026, Schimon Jehudah (http://schimon.i2p) // @license MIT; https://opensource.org/licenses/MIT // @match *://*/* // @homepageURL https://greasyfork.org/scripts/574852-image-gallery-as-cbz // @supportURL https://greasyfork.org/scripts/574852-image-gallery-as-cbz/feedback // @grant GM.registerMenuCommand // @grant GM.xmlHttpRequest // @connect * // @require https://cdn.jsdelivr.net/npm/[email protected]/umd/index.min.js // @run-at context-menu // ==/UserScript== const MIN_WIDTH = 100; const MIN_HEIGHT = 100; console.log('[GalleryDL] Script v1.5 initialized'); (async function() { 'use strict'; console.log('[GalleryDL] Starting download...'); const imageInfos = getImages(); if (!imageInfos.length) { alert('No images found.'); return; } const files = {}; const imageData = []; const seen = new Set(); let ok = 0, fail = 0, totalSize = 0; await Promise.allSettled(imageInfos.map(async (info, i) => { try { const data = await fetchUint8(info.src); let fname = getFilename(info.src, i); if (seen.has(fname)) { const p = fname.split('.'); const ext = p.pop(); fname = `${p.join('.')}_${i}.${ext}`; } seen.add(fname); console.log(`[GalleryDL] Adding: ${fname}`); files[fname] = data; imageData.push({ filename: fname, url: info.src, size: data.length }); totalSize += data.length; ok++; } catch (e) { console.error(`[GalleryDL] Failed ${info.src}:`, e); fail++; } })); console.log(`[GalleryDL] Fetched: ${ok} ok, ${fail} failed`); if (!ok) { alert('All downloads failed. Check console.'); return; } console.log('[GalleryDL] Generating metadata files...'); const metadataText = createMetadataText(ok, totalSize); const imagesCsv = createImagesCsv(imageData); files['metadata.txt'] = new TextEncoder().encode(metadataText); files['images.csv'] = new TextEncoder().encode(imagesCsv); console.log('[GalleryDL] Generating ZIP via fflate.zipSync...'); let zipped; try { zipped = fflate.zipSync(files); console.log(`[GalleryDL] ZIP generated: ${zipped.length} bytes`); } catch (e) { console.error('[GalleryDL] ZIP generation failed:', e); alert('ZIP generation failed. See console.'); return; } const blob = new Blob([zipped], {type: 'application/zip'}); console.log(`[GalleryDL] Final Blob created: ${blob.size} bytes`); const name = document.title.replace(/[^a-z0-9]/gi, '_').substring(0, 50) + '_images.cbz'; downloadBlob(blob, name); console.log('[GalleryDL] Done'); })(); function getImages() { console.log('[GalleryDL] Scanning DOM...'); const imgs = Array.from(document.querySelectorAll('img')) .filter(img => (img.naturalWidth || img.width) > MIN_WIDTH || (img.naturalHeight || img.height) > MIN_HEIGHT) .map(img => ({src: img.src, width: img.naturalWidth || img.width, height: img.naturalHeight || img.height})); const links = Array.from(document.querySelectorAll('a[href]')) .map(a => a.href) .filter(href => /\.(jpg|jpeg|png|gif|webp|bmp|svg)(\?.*)?$/i.test(href)) .map(href => ({src: href, width: 0, height: 0})); const all = [...imgs, ...links].filter((item, idx, arr) => item.src && !item.src.startsWith('data:') && arr.findIndex(i => i.src === item.src) === idx ); console.log(`[GalleryDL] Found ${all.length} unique image URLs`); return all; } function getFilename(url, index) { try { const u = new URL(url); let name = u.pathname.split('/').pop() || `image_${index}`; let ext = name.split('.').pop(); if (!ext || ext.length > 5) ext = 'jpg'; name = name.replace(/[^a-zA-Z0-9._-]/g, '_'); return name.length > 100 ? `image_${index}.${ext}` : name; } catch (e) { return `image_${index}.jpg`; } } function fetchUint8(url) { console.log(`[GalleryDL] Fetching: ${url}`); return new Promise((resolve, reject) => { GM.xmlHttpRequest({ method: 'GET', url: url, responseType: 'arraybuffer', onload: function(r) { if (r.status !== 200) { console.error(`[GalleryDL] HTTP ${r.status}: ${url}`); return reject(new Error(`HTTP ${r.status}`)); } const data = new Uint8Array(r.response); console.log(`[GalleryDL] OK (${data.length} bytes): ${url}`); resolve(data); }, onerror: function(e) { console.error(`[GalleryDL] Network error: ${url}`, e); reject(e); } }); }); } function downloadBlob(blob, filename) { console.log(`[GalleryDL] Triggering download: ${filename} (${blob.size} bytes)`); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = filename; document.body.appendChild(a); a.click(); a.remove(); URL.revokeObjectURL(url); console.log('[GalleryDL] Download triggered'); } function createMetadataText(imageCount, totalSize) { const meta = []; meta.push(`Title: ${document.title}`); meta.push(`URL: ${window.location.href}`); meta.push(`Timestamp: ${new Date().toISOString()}`); meta.push(`Images Downloaded: ${imageCount}`); meta.push(`Total Size (bytes): ${totalSize}`); const desc = document.querySelector('meta[name="description"]'); if (desc) meta.push(`Meta Description: ${desc.content}`); return meta.join('\n'); } function createImagesCsv(imageData) { const lines = ['filename,source_url,size_bytes']; imageData.forEach(img => { lines.push(`${img.filename},${img.url},${img.size}`); }); return lines.join('\n'); }