NOTICE: By continued use of this site you understand and agree to the binding Terms of Service and Privacy Policy.
// ==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();
})();