Fix Mangapark Image Loading Issue

Auto-loads images from the main host to bypass CDN issues. Click "PICK" to manually change servers.

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

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

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

// ==UserScript==
// @name         Fix Mangapark Image Loading Issue
// @namespace    http://tampermonkey.net/
// @version      0.24
// @description  Auto-loads images from the main host to bypass CDN issues. Click "PICK" to manually change servers.
// @match        https://mangapark.io/title/*-chapter-*
// @match        https://mangapark.io/title/*-ch-*
// @match        https://mangapark.io/title/*-prologue
// @match        https://mangapark.io/title/*-oneshot
// @match        https://mangapark.net/title/*-chapter-*
// @match        https://mangapark.net/title/*-ch-*
// @match        https://mangapark.net/title/*-prologue
// @match        https://mangapark.net/title/*-oneshot
// @match        https://mpark.to/title/*-chapter-*
// @match        https://mpark.to/title/*-ch-*
// @match        https://mpark.to/title/*-prologue
// @match        https://mpark.to/title/*-oneshot
// @license      MIT
// @grant        none
// @run-at       document-idle
// ==/UserScript==

(function() {
    'use strict';

    // Prevent script from running multiple times
    if (window.__mpFixInitialized) return;
    window.__mpFixInitialized = true;

    // Configuration
    const CONFIG = {
        CDN_SERVERS: [
            "s01", "s02", "s03", "s04",
            "s05", "s06", "s07", "s08", "s09", "s10"
        ],
        BUTTON_TEXT: "PICK",
        BUTTON_POSITION: {
            top: "20px",
            right: "70px"
        }
    };

    // Regex to detect the problematic CDN subdomains (s00-s10)
    const SUBDOMAIN_REGEX = /(https?:)?\/\/s(\d{2})\./i;

    let pickerMode = false;
    let currentHover = null;

    /**
     * Generates a "Fixed" URL using the current page's host.
     * This mimics the logic of the working script you provided.
     */
    function getMainHostUrl(originalSrc) {
        try {
            const url = new URL(originalSrc);
            // Replace the CDN host with the current page's host
            return `https://${window.location.host}${url.pathname}`;
        } catch (e) {
            return originalSrc;
        }
    }

    /**
     * Check if a URL is a candidate for fixing (s00-s10 pattern)
     */
    function isSupportedImage(url) {
        return SUBDOMAIN_REGEX.test(url);
    }

    /**
     * Get next CDN server (fallback logic)
     */
    function getNextCDN(currentSrc, retryCount) {
        const match = currentSrc.match(SUBDOMAIN_REGEX);
        if (!match) return currentSrc;

        const currentServerNum = match[2]; // e.g., "04"
        let currentIndex = parseInt(currentServerNum, 10) - 1;
        if (currentIndex < 0 || currentIndex > 9) currentIndex = 0;

        const nextIndex = (currentIndex + 1 + retryCount) % CONFIG.CDN_SERVERS.length;
        return CONFIG.CDN_SERVERS[nextIndex]; // Returns "s05"
    }

    /**
     * Handle image loading errors.
     * Strategy 1: Try Main Host (if not already tried).
     * Strategy 2: Fallback to cycling CDNs.
     */
    function handleImageError(img) {
        const retryCount = Number(img.dataset.retryCount || "0");
        const originalSrc = img.src;

        if (!isSupportedImage(originalSrc) && !img.dataset.isMainHost) {
            // If we can't handle it, give up
            return;
        }

        // Step 1: If we haven't tried the Main Host fix yet, DO IT.
        if (!img.dataset.triedMainHost) {
            const newSrc = getMainHostUrl(originalSrc);
            if (newSrc !== originalSrc) {
                console.warn(`[MainHost Fix] Replacing: ${originalSrc.substring(0, 50)}...`);
                img.dataset.triedMainHost = "true";
                img.dataset.isMainHost = "true";
                img.src = '';
                setTimeout(() => { img.src = newSrc; }, 100);
                return;
            }
        }

        // Step 2: If Main Host failed or wasn't applicable, cycle CDNs
        if (retryCount < 9) {
            const nextServerPrefix = getNextCDN(originalSrc, retryCount);
            const newSrc = originalSrc.replace(SUBDOMAIN_REGEX, (match, protocol, oldNum) => {
                return (protocol || 'https:') + '//' + nextServerPrefix + '.';
            });

            console.warn(`[CDN Cycle] Attempt ${retryCount + 1}: ${nextServerPrefix}`);
            img.dataset.retryCount = String(retryCount + 1);
            img.src = '';
            setTimeout(() => { img.src = newSrc; }, 100);
        } else {
            console.error(`✗ All retries exhausted for: ${originalSrc}`);
            img.dataset.failed = "true";
            img.style.border = "2px solid red";
            img.title = "Failed to load from all sources";
        }
    }

    /**
     * Set up error handling
     */
    function setupImageErrorHandler(img) {
        if (img.dataset.mpHandlerAttached) return;
        img.dataset.mpHandlerAttached = "true";

        img.addEventListener('error', () => handleImageError(img));
        img.addEventListener('load', () => {
            delete img.dataset.failed;
            if (!img.dataset.isMainHost) {
                // If it loaded successfully from a CDN, we keep it.
                // If it loaded from Main Host, we keep it.
            }
        });
    }

    /**
     * Initializes a single image element.
     * Now performs proactive fixing based on the working script logic.
     */
    function initializeImage(img) {
        // Only process images matching the bad CDN pattern
        if (!isSupportedImage(img.src)) return;

        if (img.dataset.mpFixed) return;

        console.log(`[AutoFix] Found target: ${img.src.substring(0, 50)}...`);

        // --- PROACTIVE FIX LOGIC (From the working script) ---
        // Immediately replace the CDN URL with the Main Host URL.
        const newSrc = getMainHostUrl(img.src);

        if (img.src !== newSrc) {
            console.log(`[AutoFix] Switching to Main Host: ${newSrc.substring(0, 50)}...`);
            img.src = newSrc;
            img.dataset.isMainHost = "true"; // Mark it so we know it's the "fixed" version
        }
        // ------------------------------------------------------

        setupImageErrorHandler(img);
        img.dataset.mpFixed = "true";
    }

    /**
     * Find and initialize all images already present.
     */
    function initializeExistingImages() {
        const allImages = document.querySelectorAll('img[src]');
        allImages.forEach(img => {
            if (isSupportedImage(img.src)) {
                initializeImage(img);
            }
        });
    }

    /**
     * Observe the DOM for lazy-loaded images
     */
    function observeForChanges() {
        const observer = new MutationObserver(mutations => {
            for (const mutation of mutations) {
                if (mutation.addedNodes.length) {
                    for (const node of mutation.addedNodes) {
                        if (node.nodeType === Node.ELEMENT_NODE) {
                            if (node.tagName === 'IMG' && isSupportedImage(node.src)) {
                                initializeImage(node);
                            } else if (node.querySelectorAll) {
                                const images = node.querySelectorAll('img');
                                images.forEach(img => {
                                    if (isSupportedImage(img.src)) initializeImage(img);
                                });
                            }
                        }
                    }
                }
            }
        });

        if (document.body) {
            observer.observe(document.body, {
                childList: true,
                subtree: true
            });
        }
    }

    // --- Picker Mode Functions ---

    function enablePickerMode() {
        pickerMode = true;
        document.body.style.cursor = "crosshair";
    }

    function disablePickerMode() {
        pickerMode = false;
        document.body.style.cursor = "default";
        if (currentHover) {
            currentHover.style.outline = "";
            currentHover = null;
        }
    }

    function togglePicker() {
        pickerMode ? disablePickerMode() : enablePickerMode();
    }

    function highlight(img) {
        if (currentHover && currentHover !== img) currentHover.style.outline = "";
        currentHover = img;
        img.style.outline = "3px solid #ff5500";
    }

    function unhighlight(img) {
        img.style.outline = "";
        if (currentHover === img) currentHover = null;
    }

    function changeImagePrefix(img) {
        const original = img.src;

        // Check if it matches our regex
        const match = original.match(SUBDOMAIN_REGEX);
        const isMainHost = img.dataset.isMainHost === "true";

        let currentVal = "";
        if (isMainHost) {
            currentVal = "Main Host (Auto-Fixed)";
        } else if (match) {
            currentVal = match[0]; // e.g. "//s04."
        } else {
            alert("URL does not match a fixable pattern.");
            return;
        }

        // Prompt user for input
        // We allow: "s01".."s10", or empty to reset to Main Host
        const input = prompt(`Current: ${currentVal}\n\nEnter new server (e.g. s05) or leave empty for Main Host fix:`);

        if (input === null) return; // Cancelled

        let newSrc = "";

        if (input.trim() === "") {
            // User wants Main Host Fix
            newSrc = getMainHostUrl(original);
            img.dataset.isMainHost = "true";
        } else {
            // User wants specific Server
            const serverName = input.trim().toLowerCase();
            // Basic validation
            if (!/^s\d{2}$/.test(serverName)) {
                alert("Invalid format. Use s01, s05, etc.");
                return;
            }

            // If currently on Main Host, we don't have the original CDN URL structure handy easily
            // unless we saved it. For now, if on Main Host, we can't easily switch back to a specific
            // CDN server without knowing the original CDN domain (mpypl, mpqsc, etc).
            // However, usually the pathname is enough if we assume a domain, but the domains vary.

            if (isMainHost) {
                alert("Cannot switch to specific server from Main Host fix easily (original CDN domain unknown). Refresh the page to try CDNs.");
                return;
            }

            const protocol = match[1] || 'https:';
            newSrc = original.replace(SUBDOMAIN_REGEX, `${protocol}//${serverName}.`);
            img.dataset.isMainHost = "";
        }

        img.src = newSrc;
        img.dataset.retryCount = "0"; // Reset retry counter
        console.log("Manual update:", original, "->", img.src);
        disablePickerMode();
    }

    function bindPickerEvents() {
        document.addEventListener("mouseover", e => {
            if (!pickerMode || e.target.tagName !== "IMG") return;
            // Highlight any image, even if not strictly supported, to allow inspection
            highlight(e.target);
        });

        document.addEventListener("mouseout", e => {
            if (!pickerMode || e.target.tagName !== "IMG") return;
            unhighlight(e.target);
        });

        document.addEventListener("click", e => {
            if (!pickerMode || e.target.tagName !== "IMG") return;
            e.preventDefault();
            e.stopPropagation();
            changeImagePrefix(e.target);
            disablePickerMode();
        }, true);
    }

    function createPickerButton() {
        if (!document.body) return;
        if (document.getElementById('mp-fix-btn')) return;

        const btn = document.createElement("button");
        btn.id = 'mp-fix-btn';
        btn.textContent = CONFIG.BUTTON_TEXT;
        btn.style.cssText = `
            position: fixed;
            top: ${CONFIG.BUTTON_POSITION.top};
            right: ${CONFIG.BUTTON_POSITION.right};
            z-index: 9999;
            padding: 8px 12px;
            background: #66ccff;
            border: 1px solid #333;
            cursor: pointer;
            font-weight: bold;
        `;
        btn.onclick = togglePicker;
        document.body.appendChild(btn);
    }

    // Main initialization flow
    function main() {
        if (!document.body) {
            setTimeout(main, 100);
            return;
        }

        initializeExistingImages();
        observeForChanges();
        bindPickerEvents();
        createPickerButton();
    }

    // Start the script
    main();

})();