NovelCrow ZIP Downloader

Downloads images from novelcrow.com into a ZIP file (supports lazy loading)

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Userscripts to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install a user script manager extension to install this script.

(I already have a user script manager, let me install it!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

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

})();