Comikey Chapter Ripper

Download images from comikey.com to a subfolder. Captures 0.webp, converts to PNG, and saves individually.

Tendrás que instalar una extensión para tu navegador como Tampermonkey, Greasemonkey o Violentmonkey si quieres utilizar este script.

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

Tendrás que instalar una extensión como Tampermonkey o Violentmonkey para instalar este script.

Necesitarás instalar una extensión como Tampermonkey o Userscripts para instalar este script.

Tendrás que instalar una extensión como Tampermonkey antes de poder instalar este script.

Necesitarás instalar una extensión para administrar scripts de usuario si quieres instalar este script.

(Ya tengo un administrador de scripts de usuario, déjame instalarlo)

Tendrás que instalar una extensión como Stylus antes de poder instalar este script.

Tendrás que instalar una extensión como Stylus antes de poder instalar este script.

Tendrás que instalar una extensión como Stylus antes de poder instalar este script.

Para poder instalar esto tendrás que instalar primero una extensión de estilos de usuario.

Para poder instalar esto tendrás que instalar primero una extensión de estilos de usuario.

Para poder instalar esto tendrás que instalar primero una extensión de estilos de usuario.

(Ya tengo un administrador de estilos de usuario, déjame instalarlo)

// ==UserScript==
// @name         Comikey Chapter Ripper
// @namespace    http://tampermonkey.net/
// @version      2.0
// @description  Download images from comikey.com to a subfolder. Captures 0.webp, converts to PNG, and saves individually.
// @author       ozler365
// @license      MIT
// @match        https://comikey.com/read/*
// @icon         https://comikey.com/static/images/favicons/favicon.b6e9a28323d2.png
// @grant        GM_download
// ==/UserScript==

(function() {
    'use strict';

    // Settings
    const CAPTURE_REGEX = /\/0\.webp([?#]|$)/i;

    // State
    const capturedUrls = new Set();
    const orderedUrls = [];

    // --- UI Construction ---
    const ui = document.createElement('div');
    ui.style.cssText = 'position: fixed; bottom: 20px; right: 20px; background: #222; color: #fff; padding: 15px; border-radius: 8px; z-index: 10000; font-family: sans-serif; border: 1px solid #444; box-shadow: 0 4px 6px rgba(0,0,0,0.3); min-width: 240px;';
    ui.innerHTML = `
        <h3 style="margin: 0 0 10px 0; font-size: 16px; color: #4CAF50;">Comikey Downloader</h3>
        <div id="cmd-status" style="margin-bottom: 10px; font-size: 13px;">Waiting for images...<br><span style="color:#aaa; font-size:11px;">(Scroll down to capture)</span></div>
        <div id="cmd-count" style="margin-bottom: 10px; font-size: 12px; color: #fff;">Captured: 0</div>
        <button id="cmd-btn" disabled style="width: 100%; padding: 8px; background: #555; color: #999; border: none; border-radius: 4px; cursor: not-allowed; font-weight: bold;">Download All</button>
    `;
    document.body.appendChild(ui);

    const statusEl = document.getElementById('cmd-status');
    const countEl = document.getElementById('cmd-count');
    const btn = document.getElementById('cmd-btn');

    // --- Helper: Convert Blob to PNG Blob with Timeout ---
    function blobToPng(blob) {
        return new Promise((resolve, reject) => {
            const timeout = setTimeout(() => reject(new Error("Conversion timeout")), 5000); // 5s timeout

            const img = new Image();
            const url = URL.createObjectURL(blob);
            img.onload = () => {
                clearTimeout(timeout);
                URL.revokeObjectURL(url);
                try {
                    const canvas = document.createElement('canvas');
                    canvas.width = img.width;
                    canvas.height = img.height;
                    const ctx = canvas.getContext('2d');
                    ctx.drawImage(img, 0, 0);
                    canvas.toBlob((pngBlob) => {
                        if (pngBlob) resolve(pngBlob);
                        else reject(new Error("Canvas export failed"));
                    }, 'image/png');
                } catch (e) {
                    reject(e);
                }
            };
            img.onerror = () => {
                clearTimeout(timeout);
                URL.revokeObjectURL(url);
                reject(new Error("Image load failed"));
            };
            img.src = url;
        });
    }

    // --- Monitor Network Activity ---
    const observer = new PerformanceObserver((list) => {
        list.getEntries().forEach((entry) => {
            if (CAPTURE_REGEX.test(entry.name)) {
                if (!capturedUrls.has(entry.name)) {
                    capturedUrls.add(entry.name);
                    orderedUrls.push(entry.name);
                    updateUI();
                }
            }
        });
    });
    observer.observe({ entryTypes: ["resource"] });

    function updateUI() {
        const count = orderedUrls.length;
        countEl.innerText = `Captured: ${count}`;
        if (count > 0) {
            btn.disabled = false;
            btn.style.background = "#e91e63";
            btn.style.color = "#fff";
            btn.style.cursor = "pointer";
            statusEl.innerText = "Ready. Keep scrolling or download.";
        }
    }

    // --- Processing Logic ---
    async function processQueue(folderName) {
        btn.disabled = true;

        for (let i = 0; i < orderedUrls.length; i++) {
            const url = orderedUrls[i];
            const pageNum = String(i + 1).padStart(3, '0');
            statusEl.innerText = `Processing ${i + 1}/${orderedUrls.length}...`;

            try {
                // 1. Fetch original data
                const response = await fetch(url);
                const originalBlob = await response.blob();
                
                let finalBlob = originalBlob;
                let extension = "webp";

                // 2. Try converting to PNG
                try {
                    finalBlob = await blobToPng(originalBlob);
                    extension = "png";
                } catch (convErr) {
                    console.warn(`Conversion failed for page ${i+1}, falling back to WebP.`, convErr);
                    // Fallback: Use original WebP blob if conversion fails/times out
                    extension = "webp"; 
                }

                // 3. Construct File URL and Path
                const finalUrl = URL.createObjectURL(finalBlob);
                const fileName = `${folderName}/page_${pageNum}.${extension}`;

                // 4. Download via GM_download
                await new Promise((resolve, reject) => {
                    GM_download({
                        url: finalUrl,
                        name: fileName,
                        saveAs: false, // Don't ask user for location
                        onload: () => {
                            URL.revokeObjectURL(finalUrl);
                            resolve();
                        },
                        onerror: (err) => {
                            URL.revokeObjectURL(finalUrl);
                            console.error("GM_download error", err);
                            // Resolve anyway to continue to next image
                            resolve(); 
                        }
                    });
                });

                // Small delay to keep browser responsive
                await new Promise(r => setTimeout(r, 200));

            } catch (err) {
                console.error(`Failed to download page ${i+1}`, err);
            }
        }

        statusEl.innerText = "Done!";
        btn.innerText = "Download Complete";
        btn.disabled = false;
    }

    // --- Click Handler ---
    btn.onclick = () => {
        // Sanitize title for folder name
        const title = document.title.replace(/[<>:"/\\|?*]+/g, " ").trim() || "Comikey_Download";
        processQueue(title);
    };

})();