Stringer / Fix Mangapark Image Loading Issue

// ==UserScript==
// @name         Fix Mangapark Image Loading Issue
// @namespace    http://tampermonkey.net/
// @version      0.14
// @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==
// ==OpenUserJS==
// @author Stringer
// @updateURL https://gist.githubusercontent.com/Joker1718/c2cb773512e3a841ce867e5f6cce9e67/raw/d9ba3b492a2ada978267b8c52b0d1481838dc97f/mangaparkfix.js
// @downloadURL https://gist.githubusercontent.com/Joker1718/c2cb773512e3a841ce867e5f6cce9e67/raw/d9ba3b492a2ada978267b8c52b0d1481838dc97f/mangaparkfix.js
// ==/OpenUserJS==

(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"
        },
        IMAGE_DOMAIN: "mpqsc.org"
    };

    const SOURCE_REGEX = /^(https?:\/\/)(.+?)(\.mpqsc\.org\/media\/mpup\/.+)$/;
    let pickerMode = false;
    let currentHover = null;

    /**
     * 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
     * @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 the src is not a valid image URL, don't try to fix it.
        if (!originalSrc.includes(CONFIG.IMAGE_DOMAIN)) {
            console.error(`✗ Cannot fix non-image URL: ${originalSrc}`);
            return;
        }

        if (retryCount < CONFIG.MAX_RETRIES) {
            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);

            // Reset the image first to clear error state
            img.src = '';
            // Use setTimeout to ensure DOM processes the reset
            setTimeout(() => {
                img.src = newSrc;
            }, 100);
        } else {
            console.error(`✗ All CDN servers 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', function() {
            handleImageError(img);
        });

        img.addEventListener('load', function() {
            // Reset failure state on successful load
            delete img.dataset.failed;
            img.style.border = "";
            img.title = "";
            console.log(`✓ Successfully loaded: ${img.src}`);
        });
    }

    /**
     * Initializes a single image element.
     * This function performs the initial CDN replacement and sets up error handling.
     * It's safe to call multiple times on the same element.
     * @param {HTMLImageElement} img - The image element to initialize.
     */
    function initializeImage(img) {
        // Skip if already initialized or if it's not a target image
        if (img.dataset.mpHandlerAttached || !img.src.includes(CONFIG.IMAGE_DOMAIN)) {
            return;
        }

        // Mark as being processed to prevent race conditions
        img.dataset.mpHandlerAttached = "true";

        // Perform the initial CDN replacement
        const match = img.src.match(/(https?:\/\/)(s\d+)(\.mpqsc\.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";
        }

        // Set up error handling for future failures
        setupImageErrorHandler(img);
    }

    /**
     * Find and initialize all images already present on the page.
     */
    function initializeExistingImages() {
        const targetImages = document.querySelectorAll(`img[src*='${CONFIG.IMAGE_DOMAIN}']`);
        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) {
                // Handle newly added nodes
                if (mutation.addedNodes.length) {
                    for (const node of mutation.addedNodes) {
                        if (node.nodeType === Node.ELEMENT_NODE) {
                            if (node.tagName === 'IMG' && node.src.includes(CONFIG.IMAGE_DOMAIN)) {
                                initializeImage(node);
                            } else {
                                // Query for images within the added node (e.g., a new div containing images)
                                const images = node.querySelectorAll(`img[src*="${CONFIG.IMAGE_DOMAIN}"]`);
                                images.forEach(initializeImage);
                            }
                        }
                    }
                }

                // Handle attribute changes on existing nodes (e.g., from lazy-loading)
                if (mutation.type === 'attributes' && mutation.attributeName === 'src') {
                    const target = mutation.target;
                    if (target.tagName === 'IMG' && target.src.includes(CONFIG.IMAGE_DOMAIN)) {
                        initializeImage(target);
                    }
                }
            }
        });

        if (document.body) {
            observer.observe(document.body, {
                childList: true,
                subtree: true,
                attributes: true, // This is the key change
                attributeFilter: ['src'] // Optimize: only watch for 'src' changes
            });
        }
    }

    // Picker mode functions (unchanged)
    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;
        const match = original.match(SOURCE_REGEX);

        if (!match) {
            alert("This image does not match the prefix pattern.");
            return;
        }

        const serverMatch = original.match(/(https?:\/\/)(s\d+)(\.mpqsc\.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"; // Reset retry count for new server

        console.log("Manual update:", original, "->", img.src);
    }

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

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

        document.addEventListener("click", e => {
            if (!pickerMode || e.target.tagName !== "IMG") return;
            if (!e.target.src.includes(CONFIG.IMAGE_DOMAIN)) 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) {
            // If body isn't ready yet, wait a bit and try again.
            setTimeout(main, 100);
            return;
        }

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

    // Start the script
    main();

})();