Comikey Chapter Ripper

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

Voor het installeren van scripts heb je een extensie nodig, zoals Tampermonkey, Greasemonkey of Violentmonkey.

Voor het installeren van scripts heb je een extensie nodig, zoals {tampermonkey_link:Tampermonkey}.

Voor het installeren van scripts heb je een extensie nodig, zoals Tampermonkey of Violentmonkey.

Voor het installeren van scripts heb je een extensie nodig, zoals Tampermonkey of Userscripts.

Voor het installeren van scripts heb je een extensie nodig, zoals {tampermonkey_link:Tampermonkey}.

Voor het installeren van scripts heb je een gebruikersscriptbeheerder nodig.

(Ik heb al een user script manager, laat me het downloaden!)

Voor het installeren van gebruikersstijlen heb je een extensie nodig, zoals {stylus_link:Stylus}.

Voor het installeren van gebruikersstijlen heb je een extensie nodig, zoals {stylus_link:Stylus}.

Voor het installeren van gebruikersstijlen heb je een extensie nodig, zoals {stylus_link:Stylus}.

Voor het installeren van gebruikersstijlen heb je een gebruikersstijlbeheerder nodig.

Voor het installeren van gebruikersstijlen heb je een gebruikersstijlbeheerder nodig.

Voor het installeren van gebruikersstijlen heb je een gebruikersstijlbeheerder nodig.

(Ik heb al een beheerder - laat me doorgaan met de installatie!)

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

})();