Automatically replaces AniDB thumbnail images with full high-resolution versions and enlarges them for better viewing. Includes smart caching, error handling, and performance optimizations.
// ==UserScript==
// @name AniDB Thumbnails Expander
// @namespace http://tampermonkey.net/
// @version 2.2
// @description Automatically replaces AniDB thumbnail images with full high-resolution versions and enlarges them for better viewing. Includes smart caching, error handling, and performance optimizations.
// @author Kristijan1001
// @match https://anidb.net/*
// @match https://*.anidb.net/*
// @grant none
// @run-at document-start
// @license MIT
// @icon https://anidb.net/favicon.ico
// @icon64 https://anidb.net/apple-touch-icon.png
// ==/UserScript==
(function() {
'use strict';
// Configuration
const CONFIG = {
thumbnailWidth: 300,
checkInterval: 2000,
initialDelay: 500,
enableLogging: false
};
// Cache to track processed images
const processedImages = new WeakSet();
const failedUrls = new Set();
// Logging utility
const log = (...args) => CONFIG.enableLogging && console.log('[AniDB HD]', ...args);
// Add enhanced CSS for better image display
const style = document.createElement('style');
style.textContent = `
.g_image.thumb {
width: ${CONFIG.thumbnailWidth}px !important;
height: auto !important;
max-width: none !important;
transition: transform 0.2s ease, box-shadow 0.2s ease;
cursor: zoom-in;
}
.g_image.thumb:hover {
transform: scale(1.05);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
z-index: 100;
position: relative;
}
/* Enhanced loading state */
img[data-hd-loading="true"] {
opacity: 0.6;
filter: blur(2px);
transition: opacity 0.3s ease, filter 0.3s ease;
}
img[data-hd-loaded="true"] {
opacity: 1;
filter: none;
}
`;
document.head.appendChild(style);
/**
* Converts thumbnail URL to high-resolution URL
* @param {string} url - Original image URL
* @returns {string} High-resolution URL
*/
function getHighResUrl(url) {
if (!url || typeof url !== 'string') return url;
// Remove thumbnail suffix and file extension for AniDB's image serving
let hdUrl = url.replace(/-thumb(\.[^.]+)?$/, '');
// Handle additional AniDB URL patterns
hdUrl = hdUrl.replace(/\/thumbs?\//, '/images/');
return hdUrl;
}
/**
* Validates if URL is an AniDB image
* @param {string} url - URL to validate
* @returns {boolean} True if valid AniDB image
*/
function isAniDbImage(url) {
return url && (
url.includes('anidb.net') ||
url.includes('-thumb')
);
}
/**
* Replaces thumbnail with high-resolution version
* @param {HTMLImageElement} img - Image element to process
*/
function replaceWithHighRes(img) {
// Skip if already processed or failed
if (processedImages.has(img) || failedUrls.has(img.src)) {
return;
}
const originalSrc = img.src;
// Only process AniDB images
if (!isAniDbImage(originalSrc)) {
return;
}
const hdUrl = getHighResUrl(originalSrc);
// Skip if URL didn't change
if (hdUrl === originalSrc) {
processedImages.add(img);
return;
}
log('Processing:', originalSrc, '->', hdUrl);
// Mark as loading
img.setAttribute('data-hd-loading', 'true');
// Preload high-res image
const hdImage = new Image();
hdImage.onload = function() {
img.src = hdUrl;
img.setAttribute('data-hd-loaded', 'true');
img.removeAttribute('data-hd-loading');
processedImages.add(img);
log('Loaded successfully:', hdUrl);
};
hdImage.onerror = function() {
log('Failed to load:', hdUrl);
failedUrls.add(originalSrc);
img.removeAttribute('data-hd-loading');
processedImages.add(img);
};
hdImage.src = hdUrl;
}
/**
* Processes all images on the page
*/
function processAllImages() {
const images = document.querySelectorAll('img');
let processedCount = 0;
images.forEach(img => {
if (img.complete && img.naturalWidth > 0) {
replaceWithHighRes(img);
processedCount++;
} else {
// Wait for image to load before processing
img.addEventListener('load', () => replaceWithHighRes(img), { once: true });
}
});
if (processedCount > 0) {
log(`Processed ${processedCount} images`);
}
}
/**
* Debounce function to limit execution rate
* @param {Function} func - Function to debounce
* @param {number} wait - Wait time in milliseconds
* @returns {Function} Debounced function
*/
function debounce(func, wait) {
let timeout;
return function executedFunction(...args) {
const later = () => {
clearTimeout(timeout);
func(...args);
};
clearTimeout(timeout);
timeout = setTimeout(later, wait);
};
}
// Initial processing after page load
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', () => {
processAllImages();
setTimeout(processAllImages, CONFIG.initialDelay);
});
} else {
processAllImages();
setTimeout(processAllImages, CONFIG.initialDelay);
}
// Observe DOM changes for dynamically loaded content
const debouncedProcess = debounce(processAllImages, 300);
const observer = new MutationObserver((mutations) => {
// Only process if images were actually added
const hasNewImages = mutations.some(mutation =>
Array.from(mutation.addedNodes).some(node =>
node.nodeName === 'IMG' || (node.querySelectorAll && node.querySelectorAll('img').length > 0)
)
);
if (hasNewImages) {
debouncedProcess();
}
});
observer.observe(document.body, {
childList: true,
subtree: true
});
// Periodic check with adaptive interval
let checkCount = 0;
const intervalId = setInterval(() => {
processAllImages();
checkCount++;
// After 10 checks, reduce frequency to save resources
if (checkCount > 10) {
clearInterval(intervalId);
setInterval(processAllImages, CONFIG.checkInterval * 3);
log('Switched to reduced check frequency');
}
}, CONFIG.checkInterval);
log('AniDB HD Image Enhancer initialized');
})();