Ridibooks Comic Ripper

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

Bu betiği kurabilmeniz için Tampermonkey, Greasemonkey ya da Violentmonkey gibi bir kullanıcı betiği eklentisini kurmanız gerekmektedir.

Bu betiği yüklemek için Tampermonkey gibi bir uzantı yüklemeniz gerekir.

Bu betiği kurabilmeniz için Tampermonkey ya da Violentmonkey gibi bir kullanıcı betiği eklentisini kurmanız gerekmektedir.

Bu betiği kurabilmeniz için Tampermonkey ya da Userscripts gibi bir kullanıcı betiği eklentisini kurmanız gerekmektedir.

Bu betiği indirebilmeniz için ayrıca Tampermonkey gibi bir eklenti kurmanız gerekmektedir.

Bu komut dosyasını yüklemek için bir kullanıcı komut dosyası yöneticisi uzantısı yüklemeniz gerekecek.

(Zaten bir kullanıcı komut dosyası yöneticim var, kurmama izin verin!)

Bu stili yüklemek için Stylus gibi bir uzantı yüklemeniz gerekir.

Bu stili yüklemek için Stylus gibi bir uzantı kurmanız gerekir.

Bu stili yükleyebilmek için Stylus gibi bir uzantı yüklemeniz gerekir.

Bu stili yüklemek için bir kullanıcı stili yöneticisi uzantısı yüklemeniz gerekir.

Bu stili yüklemek için bir kullanıcı stili yöneticisi uzantısı kurmanız gerekir.

Bu stili yükleyebilmek için bir kullanıcı stili yöneticisi uzantısı yüklemeniz gerekir.

(Zateb bir user-style yöneticim var, yükleyeyim!)

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

})();