Comikey Chapter Ripper

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

Aby zainstalować ten skrypt, wymagana jest instalacje jednego z następujących rozszerzeń: Tampermonkey, Greasemonkey lub Violentmonkey.

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

Aby zainstalować ten skrypt, wymagana jest instalacje jednego z następujących rozszerzeń: Tampermonkey, Violentmonkey.

Aby zainstalować ten skrypt, wymagana będzie instalacja rozszerzenia Tampermonkey lub Userscripts.

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

Aby zainstalować ten skrypt, musisz zainstalować rozszerzenie menedżera skryptów użytkownika.

(Mam już menedżera skryptów użytkownika, pozwól mi to zainstalować!)

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.

Będziesz musiał zainstalować rozszerzenie menedżera stylów użytkownika, aby zainstalować ten styl.

Będziesz musiał zainstalować rozszerzenie menedżera stylów użytkownika, aby zainstalować ten styl.

Musisz zainstalować rozszerzenie menedżera stylów użytkownika, aby zainstalować ten styl.

(Mam już menedżera stylów użytkownika, pozwól mi to zainstalować!)

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

})();