Fix Mangapark Image Loading Issue

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

当前为 2025-12-04 提交的版本,查看 最新版本

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 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();

})();