Click an image to replace its prefix. | Auto-loads the recommended image source server.
目前為
// ==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();
})();