Comikey Chapter Ripper

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

スクリプトをインストールするには、Tampermonkey, GreasemonkeyViolentmonkey のような拡張機能のインストールが必要です。

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

スクリプトをインストールするには、TampermonkeyViolentmonkey のような拡張機能のインストールが必要です。

スクリプトをインストールするには、TampermonkeyUserscripts のような拡張機能のインストールが必要です。

このスクリプトをインストールするには、Tampermonkeyなどの拡張機能をインストールする必要があります。

このスクリプトをインストールするには、ユーザースクリプト管理ツールの拡張機能をインストールする必要があります。

(ユーザースクリプト管理ツールは設定済みなのでインストール!)

このスタイルをインストールするには、Stylusなどの拡張機能をインストールする必要があります。

このスタイルをインストールするには、Stylus などの拡張機能をインストールする必要があります。

このスタイルをインストールするには、Stylus tなどの拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

(ユーザースタイル管理ツールは設定済みなのでインストール!)

このスクリプトの質問や評価の投稿はこちら通報はこちらへお寄せください
// ==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);
    };

})();