NovelCrow Comic Downloader

Download comics from novelcrow.com with forced image loading, custom zip naming based on URL, and correct image retrieval

// ==UserScript==
// @name         NovelCrow Comic Downloader
// @version      2.3
// @description  Download comics from novelcrow.com with forced image loading, custom zip naming based on URL, and correct image retrieval
// @author       B14ckwxd
// @match        *://novelcrow.com/comic/*
// @require      https://ajax.aspnetcdn.com/ajax/jQuery/jquery-3.2.1.min.js
// @require      https://cdnjs.cloudflare.com/ajax/libs/jszip/3.2.2/jszip.min.js
// @require      https://cdnjs.cloudflare.com/ajax/libs/FileSaver.js/1.3.8/FileSaver.min.js
// @grant        GM_xmlhttpRequest
// @noframes
// @run-at       document-idle
// @namespace https://greasyfork.org/users/1462126
// ==/UserScript==

(function() {
    'use strict';

    // Ensure jQuery is loaded
    if (typeof jQuery === 'undefined') {
        console.error("jQuery is not loaded. The script will not run.");
        return;
    }

    console.log("📥 NovelCrow Comic Downloader script is running...");

    // Wait for page to load dynamically
    const observer = new MutationObserver(() => {
        if ($('.page-break img').length) {
            console.log("📸 Comic images detected. Ready to download.");
            observer.disconnect();
            addDownloadButton();
        }
    });
    observer.observe(document.body, { childList: true, subtree: true });

    function addDownloadButton() {
        if ($('#downloadBtn').length) return; // Prevent duplicate buttons

        console.log("🛠 Adding download button...");
        var downBtn = $('<button/>', {
            id: 'downloadBtn',
            text: 'DOWNLOAD CHAPTER',
            css: {
                position: 'fixed',
                bottom: '20px',
                right: '20px',
                backgroundColor: '#2a518e',
                color: '#ffffff',
                fontWeight: 'bold',
                padding: '10px 20px',
                border: 'none',
                borderRadius: '5px',
                cursor: 'pointer',
                zIndex: '10000',
                boxShadow: '0 2px 5px rgba(0,0,0,0.2)'
            }
        });

        // Append button to the page
        $('body').append(downBtn);

        // Button click handler
        downBtn.click(startDownload);
    }

    // Force load all images function
    function forceLoadAllImages() {
        console.log("🔄 Forcing all images to load...");
        return new Promise(resolve => {
            let imgs = $('.page-break img');
            let promises = [];

            imgs.each(function() {
                let $img = $(this);
                // If the image is lazy-loaded, force its src to the full image URL
                let dataSrc = $img.attr('data-src');
                if (dataSrc) {
                    let fullSrc = dataSrc.replace(/\/thumbs?\//, '/full/');
                    if ($img.attr('src') !== fullSrc) {
                        $img.attr('src', fullSrc);
                    }
                }
                // Create a promise that resolves when the image is loaded (or errors)
                let p = new Promise(r => {
                    if (this.complete) {
                        r();
                    } else {
                        $img.on('load error', r);
                    }
                });
                promises.push(p);
            });

            Promise.all(promises).then(() => {
                console.log("✅ All images have been forced to load.");
                resolve();
            });
        });
    }

    // Mark startDownload as async so we can use await
    async function startDownload() {
        var downBtn = $('#downloadBtn');
        var downloading = false;
        var downloaded = false;
        var images = [];
        var zip = new JSZip();
        var title = 'comic_chapter';

        // Custom title extraction based on URL
        // Expected URL structure: /comic/{comic-slug}/{chapter-slug}/
        var pathParts = window.location.pathname.split('/').filter(Boolean);
        if (pathParts.length >= 3 && pathParts[0].toLowerCase() === 'comic') {
            let chapterSlug = pathParts[2]; // e.g., "4-the-ortegas-chronicles-chapter-4"
            let match = chapterSlug.match(/^(\d+)-(.+)-chapter-\d+$/i);
            if (match) {
                let chapterNum = match[1]; // "4"
                let titlePart = match[2]; // "the-ortegas-chronicles"
                let formattedTitle = titlePart.replace(/-/g, ' ').trim(); // "the ortegas chronicles"
                chapterNum = ('0' + chapterNum).slice(-2); // format as "04"
                title = formattedTitle + ' - Issue ' + chapterNum;
            } else {
                // Fallback: try to use the chapter title from the page
                try {
                    var pageTitle = $('.wp-manga-chaptertitle').text().trim();
                    if (pageTitle) {
                        title = pageTitle.replace(/[^a-zA-Z0-9]/g, '_').substring(0, 50);
                    } else {
                        throw "no title";
                    }
                } catch (e) {
                    console.warn("⚠ Could not retrieve chapter title. Deriving title from URL.");
                    title = window.location.pathname.replace(/[^a-zA-Z0-9]/g, '_').substring(0, 50);
                }
            }
        }


        // If already downloading or downloaded, generate the zip directly
        if (downloading || downloaded) {
            zip.generateAsync({type: 'blob'}).then(function(blob) {
                saveAs(blob, title + '.zip');
            });
            return;
        }

        // Initialize download
        downloading = true;
        downBtn.text('LOADING IMAGES...').css('background-color', '#dbba00');

        // Force all images to load
        await forceLoadAllImages();

        // Find all comic images
        $('.page-break img').each(function() {
            var imgSrc = $(this).attr('data-src') || $(this).attr('src');
            if (imgSrc) {
                images.push(imgSrc.replace(/\/thumbs?\//, '/full/')); // Convert to full-size URL
            }
        });

        if (images.length === 0) {
            console.error("❌ No images found!");
            downBtn.text('NO IMAGES FOUND').css('background-color', '#d9534f');
            return;
        }

        console.log(`🔄 Found ${images.length} images. Starting download...`);
        downBtn.text(`0/${images.length} DOWNLOADED`);
        var downCount = 0;
        var pad = '0000';
        var incomplete = false;

        images.forEach(function(url, i) {
            GM_xmlhttpRequest({
                method: 'GET',
                url: url,
                responseType: 'arraybuffer',
                headers: {
                    Referer: window.location.href
                },
                onload: function(response) {
                    // Determine file extension based on URL, default to .jpg
                    var extMatch = url.match(/\.(webp|jpg|jpeg|png)(\?|$)/i);
                    var ext = extMatch && extMatch[1] ? '.' + extMatch[1].toLowerCase() : '.jpg';
                    var fileName = pad.substr(0, pad.length - (i + 1).toString().length) + (i + 1) + ext;
                    zip.file(fileName, response.response);
                    updateProgress();
                },
                onerror: function() { handleError(url); },
                onabort: function() { handleError(url); },
                ontimeout: function() { handleError(url); }
            });

            function updateProgress() {
                downCount++;
                downBtn.text(`${downCount}/${images.length} DOWNLOADED`);
                if (downCount === images.length) finalizeDownload();
            }

            function handleError(url) {
                console.error('❌ Failed to download image:', url);
                incomplete = true;
                downCount++;
                updateProgress();
            }
        });

        function finalizeDownload() {
            zip.generateAsync({type: 'blob'}).then(function(blob) {
                // Save the zip file with the custom title
                saveAs(blob, title + '.zip');
                downBtn.text('DOWNLOAD COMPLETE').css('background-color', '#216d28');
                downloaded = true;
                downloading = false;
                console.log("✅ Download completed!");
            });
        }
    }
})();