Fix Mangapark Image Loading Issue

Click an image to replace its prefix. | Auto-loads the recommended image source server.

目前為 2025-12-04 提交的版本,檢視 最新版本

您需要先安裝使用者腳本管理器擴展,如 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.19
// @description  Click an image to replace its prefix. | Auto-loads the recommended image source server.
// @match        https://mangapark.io/title/*-chapter-*
// @match        https://mangapark.io/title/*-ch-*
// @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: [
            "https://s01", "https://s03", "https://s04",
            "https://s05", "https://s06", "https://s07",
            "https://s08", "https://s09", "https://s02"
        ],
        MAX_RETRIES: 9, // Try each CDN once
        BUTTON_TEXT: "PICK",
        BUTTON_POSITION: {
            top: "20px",
            right: "70px"
        },
        // Consolidated list of all supported domains
        IMAGE_DOMAINS: [
            "mpqsc.org", "mpujj.org", "mpypl.org", "mpfip.org",
            "mpmok.org", "mpqom.org", "mprnm.org", "mpubn.org", "mpvim.org"
        ]
    };

    // Regex for the standard path structure that supports CDN rotation
    const STANDARD_PATH_REGEX = /^https:\/\/s\d+\.(mpqsc|mpypl|mpfip|mpmok|mpqom|mprnm|mpubn|mpvim|mpujj)\.org\/media\/mpup\//;

    // Regex for the special mpujj.org path structure (no CDN rotation)
    const SPECIAL_PATH_REGEX = /^https:\/\/[a-z0-9.-]+\.mpujj\.org\/media\/\d{4}\/\d+\/[0-9a-f]{24}\/\d+_\d+_\d+_\d+\.(?:jpg|jpeg|png|webp|gif|bmp)$/;

    // General regex for the picker mode to match any supported pattern
    const SOURCE_REGEX = /^(https?:\/\/)(s\d+)(\.(mpqsc|mpujj|mpypl|mpfip|mpmok|mpqom|mprnm|mpubn|mpvim)\.org\/media\/(mpup|\d{4}\/\d+\/[0-9a-f]{24}\/\d+_\d+_\d+_\d+)\.(?:jpg|jpeg|png|webp|gif|bmp))$/;

    let pickerMode = false;
    let currentHover = null;

    /**
     * Check if a URL matches any of our supported image domains
     * @param {string} url - URL to check
     * @returns {boolean} True if URL matches any supported domain
     */
    function isSupportedImage(url) {
        return CONFIG.IMAGE_DOMAINS.some(domain => url.includes(domain));
    }

    /**
     * Get next CDN server in a deterministic cycle for a specific image
     * @param {string} currentSrc - Current image source URL
     * @param {number} retryCount - Current retry count
     * @returns {string} Next CDN server URL
     */
    function getNextCDN(currentSrc, retryCount) {
        const match = currentSrc.match(/s\d+/);
        const currentServer = match ? match[0] : null;

        // Get the index of the current server in our CDN list
        const currentIndex = CONFIG.CDN_SERVERS.findIndex(server =>
            server.includes(currentServer));

        // Use modulo to cycle through servers deterministically
        const nextIndex = (currentIndex + 1 + retryCount) % CONFIG.CDN_SERVERS.length;
        return CONFIG.CDN_SERVERS[nextIndex];
    }

    /**
     * Handle image loading errors by rotating CDN servers or retrying
     * @param {HTMLImageElement} img - The image element that failed to load
     */
    function handleImageError(img) {
        const retryCount = Number(img.dataset.retryCount || "0");
        const originalSrc = img.src;

        if (!isSupportedImage(originalSrc)) {
            console.error(`✗ Cannot fix non-image URL: ${originalSrc}`);
            return;
        }

        if (retryCount < CONFIG.MAX_RETRIES) {
            if (originalSrc.match(STANDARD_PATH_REGEX)) {
                // For standard paths, rotate the CDN
                const nextCDN = getNextCDN(originalSrc, retryCount);
                const newSrc = originalSrc.replace(/https:\/\/s\d+/, nextCDN);
                console.warn(`✗ Image failed (attempt ${retryCount + 1}): ${originalSrc}`);
                console.log(`🔄 Rotating CDN (${retryCount + 1}/${CONFIG.MAX_RETRIES}): ${newSrc}`);
                img.dataset.retryCount = String(retryCount + 1);
                img.src = '';
                setTimeout(() => { img.src = newSrc; }, 100);
            } else if (originalSrc.match(SPECIAL_PATH_REGEX)) {
                // For special paths, just retry the same URL
                console.warn(`✗ Image failed (attempt ${retryCount + 1}): ${originalSrc}`);
                console.log(`🔄 Retrying (${retryCount + 1}/${CONFIG.MAX_RETRIES}): ${originalSrc}`);
                img.dataset.retryCount = String(retryCount + 1);
                img.src = '';
                setTimeout(() => { img.src = originalSrc; }, 100);
            } else {
                console.error(`✗ Unhandled image pattern: ${originalSrc}`);
                img.dataset.failed = "true";
            }
        } 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 CDN servers";
        }
    }

    /**
     * Set up error handling for an image element
     * @param {HTMLImageElement} img - The image element to set up error handling for
     */
    function setupImageErrorHandler(img) {
        img.addEventListener('error', () => handleImageError(img));
        img.addEventListener('load', () => {
            delete img.dataset.failed;
            img.style.border = "";
            img.title = "";
            console.log(`✓ Successfully loaded: ${img.src}`);
        });
    }

    /**
     * Initializes a single image element.
     * @param {HTMLImageElement} img - The image element to initialize.
     */
    function initializeImage(img) {
        if (img.dataset.mpHandlerAttached || !isSupportedImage(img.src)) {
            return;
        }
        img.dataset.mpHandlerAttached = "true";

        if (img.src.match(STANDARD_PATH_REGEX)) {
            // For standard paths, perform the initial CDN replacement
            const match = img.src.match(/(https?:\/\/)(s\d+)(\.(mpqsc|mpypl|mpfip|mpmok|mpqom|mprnm|mpubn|mpvim|mpujj)\.org.+)/);
            if (match) {
                const randomCDN = CONFIG.CDN_SERVERS[Math.floor(Math.random() * CONFIG.CDN_SERVERS.length)];
                const newServer = randomCDN.replace("https://", "");
                const newSrc = match[1] + newServer + match[3];
                console.log(`Auto-replaced: ${img.src} -> ${newSrc}`);
                img.src = newSrc;
                img.dataset.autoReplaced = "true";
            }
        } else if (img.src.match(SPECIAL_PATH_REGEX)) {
            // For special paths, no replacement is needed
            console.log(`Special path image detected: ${img.src}`);
            img.dataset.autoReplaced = "true";
        }

        setupImageErrorHandler(img);
    }

    /**
     * Find and initialize all images already present on the page.
     */
    function initializeExistingImages() {
        const domainSelectors = CONFIG.IMAGE_DOMAINS.map(domain => `img[src*='${domain}']`).join(', ');
        const targetImages = document.querySelectorAll(domainSelectors);
        targetImages.forEach(img => initializeImage(img));
    }

    /**
     * Observe the DOM for new images or images whose src is changed.
     */
    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 {
                                const domainSelectors = CONFIG.IMAGE_DOMAINS.map(domain => `img[src*="${domain}"]`).join(', ');
                                const images = node.querySelectorAll(domainSelectors);
                                images.forEach(initializeImage);
                            }
                        }
                    }
                }
                if (mutation.type === 'attributes' && mutation.attributeName === 'src') {
                    const target = mutation.target;
                    if (target.tagName === 'IMG' && isSupportedImage(target.src)) {
                        initializeImage(target);
                    }
                }
            }
        });

        if (document.body) {
            observer.observe(document.body, {
                childList: true,
                subtree: true,
                attributes: true,
                attributeFilter: ['src']
            });
        }
    }

    // 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;

        if (original.match(SPECIAL_PATH_REGEX)) {
            alert("This image uses a special format which doesn't support prefix changes.");
            return;
        }

        const serverMatch = original.match(/(https?:\/\/)(s\d+)(\.(mpqsc|mpypl|mpfip|mpmok|mpqom|mprnm|mpubn|mpvim|mpujj)\.org.+)/);
        if (!serverMatch) {
            alert("Could not extract server from URL.");
            return;
        }

        const currentServer = serverMatch[2];
        const allowedServers = CONFIG.CDN_SERVERS.map(url => url.replace("https://", ""));
        const newPrefix = prompt(`Enter new server (current: ${currentServer}):`, currentServer);

        if (!newPrefix || !newPrefix.match(/^s\d+$/)) {
            alert("Invalid server format. Use format: sXX (e.g., s02, s05)");
            return;
        }

        if (!allowedServers.includes(newPrefix)) {
            alert(`Server must be one of: ${allowedServers.join(", ")}`);
            return;
        }

        img.src = serverMatch[1] + newPrefix + serverMatch[3];
        img.dataset.retryCount = "0";
        console.log("Manual update:", original, "->", img.src);
    }

    function bindPickerEvents() {
        document.addEventListener("mouseover", e => {
            if (!pickerMode || e.target.tagName !== "IMG" || !isSupportedImage(e.target.src)) return;
            highlight(e.target);
        });

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

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

    function createPickerButton() {
        if (!document.body) return;
        const btn = document.createElement("button");
        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();

        // Add a safety check to catch any images missed by the observer
        let safetyCheckCount = 0;
        const MAX_SAFETY_CHECKS = 10; // Runs for ~30 seconds
        const safetyCheckInterval = setInterval(() => {
            const domainSelectors = CONFIG.IMAGE_DOMAINS.map(domain => `img[src*='${domain}']`).join(', ');
            const unprocessedImages = document.querySelectorAll(`${domainSelectors}:not([data-mp-handler-attached])`);
            if (unprocessedImages.length > 0) {
                console.log(`[Safety Check] Found ${unprocessedImages.length} unprocessed images. Initializing...`);
                unprocessedImages.forEach(img => initializeImage(img));
            }
            safetyCheckCount++;
            if (safetyCheckCount >= MAX_SAFETY_CHECKS) {
                clearInterval(safetyCheckInterval);
                console.log("[Safety Check] Finished.");
            }
        }, 3000);
    }

    // Start the script
    main();

})();