Downloads images from novelcrow.com into a ZIP file (supports lazy loading)
// ==UserScript==
// @name NovelCrow ZIP Downloader
// @namespace http://tampermonkey.net/
// @version 1.2
// @description Downloads images from novelcrow.com into a ZIP file (supports lazy loading)
// @author B14CKWXD
// @match https://novelcrow.com/*
// @grant GM_xmlhttpRequest
// @connect novelcrow.com
// @connect cdnjs.cloudflare.com
// @connect *
// ==/UserScript==
(function() {
'use strict';
let isDownloading = false;
let JSZipLib = null;
// --- Load JSZip dynamically ---
function loadJSZip(callback) {
if (typeof JSZip !== 'undefined') {
JSZipLib = JSZip;
callback();
return;
}
const script = document.createElement('script');
script.src = 'https://cdnjs.cloudflare.com/ajax/libs/jszip/3.10.1/jszip.min.js';
script.onload = function() {
console.log('[NovelCrow] JSZip loaded');
JSZipLib = JSZip;
callback();
};
script.onerror = function() {
alert('Failed to load JSZip library.');
};
document.head.appendChild(script);
}
// --- Add control panel ---
function addControlPanel() {
const panel = document.createElement('div');
panel.id = 'download-panel';
panel.style.cssText = `
position: fixed;
top: 5px;
right: 5px;
z-index: 99999;
padding: 6px;
background: #1a1a2e;
color: white;
border-radius: 5px;
font-family: Arial, sans-serif;
min-width: 140px;
box-shadow: 0 2px 10px rgba(0,0,0,0.5);
border: 1px solid #4a4a6a;
font-size: 9px;
`;
panel.innerHTML = `
<h3 style="margin:0 0 6px 0; color:#4CAF50; text-align:center; font-size:10px;">📚 NovelCrow</h3>
<button id="load-all-btn" style="
width: 100%;
padding: 4px 6px;
margin: 2px 0;
background: linear-gradient(135deg, #9c27b0, #7b1fa2);
color: white;
border: none;
border-radius: 3px;
cursor: pointer;
font-weight: bold;
font-size: 9px;
">🔄 Load All Images</button>
<button id="download-zip-btn" style="
width: 100%;
padding: 4px 6px;
margin: 2px 0;
background: linear-gradient(135deg, #4CAF50, #45a049);
color: white;
border: none;
border-radius: 3px;
cursor: pointer;
font-weight: bold;
font-size: 9px;
">📦 Download ZIP</button>
<button id="download-chapter-list-btn" style="
width: 100%;
padding: 3px 6px;
margin: 2px 0;
background: linear-gradient(135deg, #2196F3, #1976D2);
color: white;
border: none;
border-radius: 3px;
cursor: pointer;
font-size: 8px;
">📋 All Chapters</button>
<div id="progress-container" style="margin-top: 6px; display: none;">
<div style="display:flex; justify-content:space-between; margin-bottom:2px; font-size:8px;">
<span id="progress-text">...</span>
<span id="progress-percent">0%</span>
</div>
<div style="width: 100%; height: 6px; background: #2a2a4a; border-radius: 3px; overflow: hidden;">
<div id="progress-bar" style="width: 0%; height: 100%; background: linear-gradient(90deg, #4CAF50, #8BC34A); transition: width 0.3s; border-radius: 3px;"></div>
</div>
</div>
<div id="status" style="margin-top: 4px; font-size: 7px; max-height: 80px; overflow-y: auto; background: #0d0d1a; padding: 4px; border-radius: 3px; display: none; line-height: 1.3;"></div>
`;
document.body.appendChild(panel);
document.getElementById('load-all-btn').onclick = loadAllLazyImages;
document.getElementById('download-zip-btn').onclick = startDownload;
document.getElementById('download-chapter-list-btn').onclick = downloadAllChapters;
}
function showProgress() {
document.getElementById('progress-container').style.display = 'block';
document.getElementById('status').style.display = 'block';
}
function updateProgress(current, total, text) {
const percent = Math.round((current / total) * 100);
document.getElementById('progress-bar').style.width = percent + '%';
document.getElementById('progress-percent').textContent = percent + '%';
document.getElementById('progress-text').textContent = text || `${current}/${total}`;
}
function log(message) {
const status = document.getElementById('status');
if (status) {
status.innerHTML += `<div style="margin:1px 0;">${message}</div>`;
status.scrollTop = status.scrollHeight;
}
console.log('[NovelCrow]', message);
}
function clearLog() {
const status = document.getElementById('status');
if (status) status.innerHTML = '';
}
function setButtonState(btnId, text, color) {
const btn = document.getElementById(btnId);
if (btn) {
btn.textContent = text;
btn.style.background = color;
}
}
// --- Load all lazy-loaded images ---
function loadAllLazyImages() {
setButtonState('load-all-btn', '⏳ Loading...', '#FFC107');
showProgress();
clearLog();
log('🔄 Loading lazy images...');
let scrollAttempts = 0;
const maxScrollAttempts = 30;
function scrollAndWait() {
window.scrollTo(0, document.body.scrollHeight);
scrollAttempts++;
updateProgress(scrollAttempts, maxScrollAttempts, 'Scrolling...');
if (scrollAttempts < maxScrollAttempts) {
setTimeout(scrollAndWait, 300);
} else {
window.scrollTo(0, 0);
// Force load lazy images
const lazyImages = document.querySelectorAll('img[data-src], img[data-lazy-src], img.lazyload, img.lazy');
log('Found ' + lazyImages.length + ' lazy images');
lazyImages.forEach((img) => {
const src = img.dataset.src || img.dataset.lazySrc;
if (src) {
img.src = src;
}
img.classList.remove('lazyload', 'lazy');
});
setTimeout(() => {
setButtonState('load-all-btn', '✅ Loaded!', '#4CAF50');
log('✅ Images loaded!');
}, 2000);
}
}
scrollAndWait();
}
// --- Get chapter title ---
function getChapterTitle() {
const selectors = ['h1.entry-title', '.chapter-title', 'h1', '.post-title'];
for (const selector of selectors) {
const titleEl = document.querySelector(selector);
if (titleEl && titleEl.textContent.trim()) {
return titleEl.textContent.trim()
.replace(/[<>:"/\\|?*]/g, '')
.replace(/\s+/g, '_')
.substring(0, 80);
}
}
const urlParts = window.location.pathname.split('/').filter(Boolean);
return urlParts[urlParts.length - 1] || 'chapter';
}
// --- Get ONLY comic images using specific selectors ---
function getComicImages() {
// These selectors specifically target comic/manga images, NOT thumbnails
const comicSelectors = [
'.wp-manga-chapter-img', // Most specific - manga chapter images
'.reading-content img', // Reading area images
'.chapter-content img', // Chapter content images
'#readerarea img', // Reader area
'.page-break img' // Page break images
];
for (const selector of comicSelectors) {
const images = document.querySelectorAll(selector);
if (images.length > 0) {
console.log('[NovelCrow] Using selector:', selector, 'found:', images.length);
return Array.from(images);
}
}
return [];
}
// --- Download image via canvas (since direct fetch gets 403) ---
function downloadImageViaCanvas(imgElement, callback) {
try {
// Wait for image to be fully loaded
if (!imgElement.complete || imgElement.naturalWidth === 0) {
callback(new Error('Image not loaded'), null);
return;
}
const canvas = document.createElement('canvas');
canvas.width = imgElement.naturalWidth;
canvas.height = imgElement.naturalHeight;
const ctx = canvas.getContext('2d');
ctx.drawImage(imgElement, 0, 0);
canvas.toBlob(function(blob) {
if (blob) {
const reader = new FileReader();
reader.onload = function() {
callback(null, reader.result);
};
reader.onerror = function() {
callback(new Error('FileReader error'), null);
};
reader.readAsArrayBuffer(blob);
} else {
callback(new Error('Canvas toBlob failed'), null);
}
}, 'image/png', 1.0);
} catch (e) {
console.error('[NovelCrow] Canvas error:', e);
callback(e, null);
}
}
// --- Create and download ZIP ---
function createAndDownloadZip(files, zipFilename) {
log('📦 Creating ZIP...');
setButtonState('download-zip-btn', '📦 ZIP...', '#9C27B0');
if (!JSZipLib) {
log('❌ JSZip not loaded');
return;
}
try {
const zip = new JSZipLib();
files.forEach(function(file) {
zip.file(file.name, file.data);
});
zip.generateAsync({
type: 'blob',
compression: 'STORE'
}).then(function(blob) {
log('✅ ' + (blob.size / 1024 / 1024).toFixed(1) + 'MB');
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = zipFilename;
document.body.appendChild(a);
a.click();
setTimeout(function() {
document.body.removeChild(a);
URL.revokeObjectURL(url);
}, 500);
log('🎉 Done!');
setButtonState('download-zip-btn', '✅ Done!', '#4CAF50');
isDownloading = false;
}).catch(function(err) {
log('❌ ZIP error');
console.error(err);
isDownloading = false;
});
} catch (e) {
log('❌ Error');
console.error(e);
isDownloading = false;
}
}
// --- Main download function (uses canvas directly since fetch gets 403) ---
function startDownload() {
if (isDownloading) {
return;
}
if (!JSZipLib) {
loadJSZip(function() {
startDownload();
});
return;
}
isDownloading = true;
showProgress();
clearLog();
const title = getChapterTitle();
log('📖 ' + title.substring(0, 25) + '...');
setButtonState('download-zip-btn', '⏳ Scan...', '#FFC107');
// Get ONLY comic images (not thumbnails)
const comicImages = getComicImages();
if (comicImages.length === 0) {
log('❌ No comic images found');
log('Try "Load All Images" first');
setButtonState('download-zip-btn', '❌ None', '#f44336');
isDownloading = false;
return;
}
log('✅ ' + comicImages.length + ' comic images');
// Use canvas method directly (since fetch gets 403)
const files = [];
let currentIndex = 0;
let successCount = 0;
function processNext() {
if (currentIndex >= comicImages.length) {
log('📊 ' + successCount + '/' + comicImages.length);
if (files.length === 0) {
log('❌ All failed');
setButtonState('download-zip-btn', '❌ Failed', '#f44336');
isDownloading = false;
return;
}
updateProgress(100, 100, 'ZIP...');
setTimeout(function() {
createAndDownloadZip(files, title + '.zip');
}, 300);
return;
}
const img = comicImages[currentIndex];
const paddedIndex = String(currentIndex + 1).padStart(4, '0');
// Try to get original filename from src
const src = img.src || img.dataset.src || '';
let filename = src.substring(src.lastIndexOf('/') + 1).split('?')[0];
if (!filename || filename.length < 3) {
filename = 'image.png';
}
// Ensure it has an extension
if (!filename.match(/\.(jpg|jpeg|png|webp|gif)$/i)) {
filename = filename.replace(/\.[^.]+$/, '') + '.png';
}
const finalFilename = paddedIndex + '_' + filename;
updateProgress(currentIndex + 1, comicImages.length, (currentIndex + 1) + '/' + comicImages.length);
setButtonState('download-zip-btn', '⏳ ' + (currentIndex + 1) + '/' + comicImages.length, '#FFC107');
downloadImageViaCanvas(img, function(err, data) {
if (err || !data) {
log('❌ ' + (currentIndex + 1));
} else {
log('✅ ' + (currentIndex + 1));
files.push({ name: finalFilename, data: data });
successCount++;
}
currentIndex++;
setTimeout(processNext, 100);
});
}
processNext();
}
// --- Download all chapters ---
function downloadAllChapters() {
if (isDownloading) {
return;
}
if (!JSZipLib) {
loadJSZip(function() {
downloadAllChapters();
});
return;
}
showProgress();
clearLog();
log('🔍 Finding chapters...');
const chapterSelectors = [
'.wp-manga-chapter a',
'li.wp-manga-chapter a',
'.listing-chapters_wrap a',
'.chapters-list a',
'ul.main li a',
'.eplister a',
'#chapterlist a'
];
let chapterLinks = [];
for (const selector of chapterSelectors) {
const links = document.querySelectorAll(selector);
if (links.length > 0) {
log('Found via: ' + selector);
links.forEach(link => {
if (link.href && link.href.includes('novelcrow.com')) {
chapterLinks.push({
url: link.href,
title: link.textContent.trim().substring(0, 50)
});
}
});
break;
}
}
// Remove duplicates
const uniqueChapters = [];
const seenUrls = new Set();
chapterLinks.forEach(ch => {
if (!seenUrls.has(ch.url)) {
seenUrls.add(ch.url);
uniqueChapters.push(ch);
}
});
if (uniqueChapters.length === 0) {
log('❌ No chapters found');
return;
}
log('✅ ' + uniqueChapters.length + ' chapters');
if (!confirm('Found ' + uniqueChapters.length + ' chapters.\n\nDownload all?')) {
return;
}
isDownloading = true;
processChapterList(uniqueChapters, 0);
}
function processChapterList(chapters, index) {
if (index >= chapters.length) {
setButtonState('download-chapter-list-btn', '✅ Done!', '#4CAF50');
log('🎉 All done!');
isDownloading = false;
return;
}
const chapter = chapters[index];
log('[' + (index + 1) + '/' + chapters.length + '] ' + chapter.title.substring(0, 15) + '...');
updateProgress(index + 1, chapters.length, (index + 1) + '/' + chapters.length);
setButtonState('download-chapter-list-btn', '⏳ ' + (index + 1) + '/' + chapters.length, '#FFC107');
// Open chapter in hidden iframe to load images, then capture via canvas
processChapterViaIframe(chapter.url, chapter.title, function() {
setTimeout(function() {
processChapterList(chapters, index + 1);
}, 3000);
});
}
// --- Process chapter by loading in iframe ---
function processChapterViaIframe(url, chapterTitle, callback) {
const cleanTitle = chapterTitle
.replace(/[<>:"/\\|?*]/g, '')
.replace(/\s+/g, '_')
.substring(0, 80);
// Create hidden iframe
const iframe = document.createElement('iframe');
iframe.style.cssText = 'position:fixed; top:-9999px; left:-9999px; width:1px; height:1px; opacity:0;';
document.body.appendChild(iframe);
iframe.onload = function() {
// Wait for images to load
setTimeout(function() {
try {
const iframeDoc = iframe.contentDocument || iframe.contentWindow.document;
// Get comic images from iframe
const comicSelectors = [
'.wp-manga-chapter-img',
'.reading-content img',
'.page-break img'
];
let images = [];
for (const selector of comicSelectors) {
images = iframeDoc.querySelectorAll(selector);
if (images.length > 0) break;
}
log(' Found ' + images.length + ' images');
if (images.length === 0) {
document.body.removeChild(iframe);
callback();
return;
}
// Download via canvas
const files = [];
let imgIndex = 0;
function downloadNextFromIframe() {
if (imgIndex >= images.length) {
// Create ZIP
if (files.length > 0) {
const zip = new JSZipLib();
files.forEach(f => zip.file(f.name, f.data));
zip.generateAsync({ type: 'blob', compression: 'STORE' })
.then(function(blob) {
const downloadUrl = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = downloadUrl;
a.download = cleanTitle + '.zip';
document.body.appendChild(a);
a.click();
setTimeout(function() {
document.body.removeChild(a);
URL.revokeObjectURL(downloadUrl);
}, 500);
log(' ✅ ' + cleanTitle.substring(0, 15) + '.zip');
document.body.removeChild(iframe);
callback();
})
.catch(function() {
log(' ❌ ZIP fail');
document.body.removeChild(iframe);
callback();
});
} else {
document.body.removeChild(iframe);
callback();
}
return;
}
const img = images[imgIndex];
const paddedIdx = String(imgIndex + 1).padStart(4, '0');
const filename = paddedIdx + '_image.png';
downloadImageViaCanvas(img, function(err, data) {
if (!err && data) {
files.push({ name: filename, data: data });
}
imgIndex++;
setTimeout(downloadNextFromIframe, 100);
});
}
downloadNextFromIframe();
} catch (e) {
console.error('[NovelCrow] Iframe error:', e);
log(' ❌ Access error');
document.body.removeChild(iframe);
callback();
}
}, 5000); // Wait 5 seconds for images to load
};
iframe.onerror = function() {
log(' ❌ Load fail');
document.body.removeChild(iframe);
callback();
};
iframe.src = url;
}
// --- Initialize ---
function init() {
loadJSZip(function() {
addControlPanel();
log('✅ Ready!');
});
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
} else {
init();
}
})();