Ridibooks Comic Ripper

Downloads chapter images from Ridibooks by intercepting the 'generate' API.

K instalaci tototo skriptu si budete muset nainstalovat rozšíření jako Tampermonkey, Greasemonkey nebo Violentmonkey.

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

K instalaci tohoto skriptu si budete muset nainstalovat rozšíření jako Tampermonkey nebo Violentmonkey.

K instalaci tohoto skriptu si budete muset nainstalovat rozšíření jako Tampermonkey nebo Userscripts.

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

K instalaci tohoto skriptu si budete muset nainstalovat manažer uživatelských skriptů.

(Už mám manažer uživatelských skriptů, nechte mě ho nainstalovat!)

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.

(Už mám manažer uživatelských stylů, nechte mě ho nainstalovat!)

// ==UserScript==
// @name         Ridibooks Comic Ripper
// @namespace    http://tampermonkey.net/
// @version      1.2
// @description  Downloads chapter images from Ridibooks by intercepting the 'generate' API.
// @author       ozler365
// @license      GPL-3.0-only
// @match        https://ridibooks.com/books/*/view*
// @icon         https://is1-ssl.mzstatic.com/image/thumb/Purple211/v4/48/bc/6d/48bc6d7a-faa6-3607-34b9-c5faee09d74e/AppIcon-0-0-1x_U007emarketing-0-8-0-85-220.png/400x400ia-75.webp
// @grant        GM_xmlhttpRequest
// @grant        GM_download
// @run-at       document-start
// ==/UserScript==

(function() {
    'use strict';

    let capturedChapterData = null;
    let downloadBtn = null;
    let isDownloading = false;

    // --- Configuration ---
    const DELAY_BETWEEN_IMAGES = 200;
    const MAX_RETRIES = 3;
    const RETRY_WAIT = 2000;

    // --- Data Interception ---

    function findChapterData(obj, url) {
        if (!obj || typeof obj !== 'object') return null;

        // 1. Target the specific file: "generate"
        if (url && !url.includes('generate')) {
            return null;
        }

        console.log('[Ridibooks DL] Inspecting "generate" request:', url);

        // 2. Navigation: data -> pages
        if (!obj.data || !obj.data.pages) {
            return null;
        }

        const pages = obj.data.pages;
        if (!Array.isArray(pages) || pages.length === 0) return null;

        // 3. Extract Images
        const images = pages.map((page, index) => {
            let imgUrl = null;

            if (typeof page === 'string') {
                imgUrl = page;
            } else if (typeof page === 'object' && page !== null) {
                imgUrl = page.src || page.url || page.image || page.link;
            }

            if (imgUrl) {
                return {
                    ord: index + 1,
                    downloadUrl: imgUrl
                };
            }
            return null;
        }).filter(item => item !== null);

        if (images.length > 0) {
            const title = obj.data.book_title || document.title || 'Ridibooks Chapter';
            return {
                images: images,
                title: title
            };
        }

        return null;
    }

    // Hook into XMLHttpRequest
    const originalOpen = XMLHttpRequest.prototype.open;
    XMLHttpRequest.prototype.open = function(method, url) {
        this.addEventListener('load', function() {
            if (url && url.toString().includes('generate')) {
                try {
                    const res = JSON.parse(this.responseText);
                    const found = findChapterData(res, url.toString());

                    if (found && found.images.length > 0) {
                        capturedChapterData = found;
                        console.log('[Ridibooks DL] Images found:', found.images.length);
                        updateButtonState();
                    }
                } catch (e) {}
            }
        });
        originalOpen.apply(this, arguments);
    };

    // Hook into Fetch API
    const originalFetch = window.fetch;
    window.fetch = async (...args) => {
        const response = await originalFetch(...args);
        try {
            const url = typeof args[0] === 'string' ? args[0] : args[0].url;
            if (url && url.includes('generate')) {
                const clone = response.clone();
                clone.json().then(data => {
                    const found = findChapterData(data, url);
                    if (found && found.images.length > 0) {
                        capturedChapterData = found;
                        updateButtonState();
                    }
                }).catch(() => {});
            }
        } catch (e) {}
        return response;
    };

    // --- UI Construction ---
    function createButton() {
        if (document.getElementById('ridi-dl-btn')) return;

        downloadBtn = document.createElement('button');
        downloadBtn.id = 'ridi-dl-btn';
        // Updated text as requested
        downloadBtn.innerText = 'Searching...'; 
        
        Object.assign(downloadBtn.style, {
            position: 'fixed', bottom: '20px', right: '20px', zIndex: '9999',
            padding: '12px 24px', backgroundColor: '#5c5c5c', color: '#ffffff',
            border: 'none', borderRadius: '8px', cursor: 'not-allowed',
            fontWeight: 'bold', fontFamily: 'sans-serif',
            boxShadow: '0 4px 10px rgba(0,0,0,0.2)'
        });
        
        downloadBtn.disabled = true;
        downloadBtn.onclick = startDownload;
        document.body.appendChild(downloadBtn);
    }

    function updateButtonState() {
        if (!downloadBtn) createButton();

        if (capturedChapterData && !isDownloading) {
            downloadBtn.style.backgroundColor = '#0077d9'; // Ridibooks Blue
            downloadBtn.style.cursor = 'pointer';
            downloadBtn.innerText = `Download: ${capturedChapterData.images.length} Pages`;
            downloadBtn.disabled = false;
        }
    }

    // --- Download Logic ---
    function startDownload() {
        if (!capturedChapterData || isDownloading) return;

        isDownloading = true;
        const images = capturedChapterData.images;
        const folderName = sanitizeFilename(capturedChapterData.title).trim();

        downloadBtn.innerText = `Starting...`;
        downloadBtn.disabled = true;
        downloadBtn.style.backgroundColor = '#f57c00';

        let completed = 0;
        let failed = 0;
        let currentIndex = 0;
        let currentRetries = 0;

        function downloadNext() {
            if (currentIndex >= images.length) {
                isDownloading = false;
                downloadBtn.style.backgroundColor = '#00cc00';
                downloadBtn.innerText = `✓ Done ${completed}/${images.length}`;
                downloadBtn.disabled = false;
                setTimeout(() => updateButtonState(), 4000);
                return;
            }

            const imgData = images[currentIndex];
            const url = imgData.downloadUrl;
            
            let ext = 'jpg';
            if (url.includes('.png')) ext = 'png';
            if (url.includes('.webp')) ext = 'webp';

            const filename = `${String(imgData.ord).padStart(3, '0')}.${ext}`;
            const fullPath = `${folderName}/${filename}`;

            const handleFailure = (reason) => {
                if (currentRetries < MAX_RETRIES) {
                    currentRetries++;
                    downloadBtn.innerText = `Retry ${currentRetries}...`;
                    setTimeout(downloadNext, RETRY_WAIT);
                } else {
                    failed++;
                    currentIndex++;
                    currentRetries = 0;
                    downloadBtn.innerText = `Skip...`;
                    setTimeout(downloadNext, DELAY_BETWEEN_IMAGES);
                }
            };

            GM_download({
                url: url,
                name: fullPath,
                timeout: 15000,
                headers: { 'Referer': window.location.href },
                onload: function() {
                    completed++;
                    currentIndex++;
                    currentRetries = 0;
                    downloadBtn.innerText = `Downloading ${currentIndex}/${images.length}...`;
                    setTimeout(downloadNext, DELAY_BETWEEN_IMAGES);
                },
                onerror: function() { handleFailure('Error'); },
                ontimeout: function() { handleFailure('Timeout'); }
            });
        }

        downloadNext();
    }

    function sanitizeFilename(name) {
        return name.replace(/[<>:"/\\|?*]/g, "").replace(/\s+/g, " ").trim().substring(0, 100);
    }

    if (document.readyState === 'loading') {
        document.addEventListener('DOMContentLoaded', createButton);
    } else {
        createButton();
    }
    window.addEventListener('load', createButton);

    let lastUrl = location.href;
    new MutationObserver(() => {
        const url = location.href;
        if (url !== lastUrl) {
            lastUrl = url;
            capturedChapterData = null;
            isDownloading = false;
            if (downloadBtn) {
                downloadBtn.style.backgroundColor = '#5c5c5c';
                downloadBtn.style.cursor = 'not-allowed';
                downloadBtn.innerText = 'Searching...';
                downloadBtn.disabled = true;
            }
        }
    }).observe(document, {subtree: true, childList: true});

})();