// ==UserScript==
// @name Pornolab Preloaded Preview
// @version 1.6.3
// @description Preloads and dynamically displays preview images below links, avoiding duplicate previews. Implements fallback to the next image if the first fails to load. Includes debug mode.
// @author Ace
// @license MIT
// @match *://pornolab.net/forum/tracker*
// @match *://pornolab.net/forum/viewforum*
// @match *://pornolab.net/forum/search*
// @run-at document-end
// @require https://cdnjs.cloudflare.com/ajax/libs/core-js/3.25.1/minified.js
// @grant none
// @namespace https://greasyfork.org/users/1418199
// ==/UserScript==
(async function () {
// Define the size for the preview image containers
const previewSize = {
width: "auto", // Auto width, so the image adjusts dynamically
height: "17rem", // Fixed height for the preview image container
};
// Set to true for detailed logging, false to suppress debug output
const debug = false;
// To track the links whose previews are currently being fetched
const inProgressLinks = new Set();
// To track the links that failed to load previews
const failedLinks = new Map();
// Maximum retry attempts for fetching image URLs
const MAX_RETRIES = 3;
// Throttle interval in milliseconds to limit IntersectionObserver callback frequency
const THROTTLE_INTERVAL = 300;
// Maximum number of concurrent fetches allowed for image URLs
const MAX_CONCURRENT_FETCHES = 5;
// Count the current number of ongoing fetches
let concurrentFetchCount = 0;
// Function to log debug or warning messages based on the 'debug' flag
function debugLog(type, ...args) {
if (debug) {
const prefix = "pornolabPreloadedPreview";
if (type === "warn") {
console.warn(`${prefix} [WARN]:`, ...args);
} else if (type === "log") {
console.log(`${prefix} [DEBUG]:`, ...args);
}
}
}
try {
// Inject custom styles for the preview image containers dynamically
const style = document.createElement("style");
style.textContent = `
.preview-container {
max-width: ${previewSize.width};
height: ${previewSize.height};
display: flex;
justify-content: center;
align-items: center;
overflow: hidden;
}
.preview-container img {
width: 100%;
height: 100%;
object-fit: scale-down;
}
`;
document.head.appendChild(style);
// Log success in injecting styles
debugLog("log", "Styles injected successfully.");
} catch (error) {
console.error("pornolabPreloadedPreview: Failed to inject styles:", error);
}
// Cache for storing previously fetched preview URLs
const previewCache = new Map();
// Function to fetch the preview images' URLs with retry mechanism
async function fetchWithRetry(url, retries = 0) {
debugLog("log", `Attempting to fetch URL: ${url}. Retry count: ${retries}`);
try {
const response = await fetch(url);
if (!response.ok) {
console.error(
`pornolabPreloadedPreview: Fetch failed for ${url} (${response.statusText}). Retry: ${retries}`
);
if (retries < MAX_RETRIES) {
return fetchWithRetry(url, retries + 1); // Retry fetching if the request fails
} else {
failedLinks.set(url, "Failed after retries"); // Mark as failed after max retries
debugLog("log", `Max retries reached for URL: ${url}`);
return [];
}
}
const html = await response.text(); // Get the page content as text
const doc = new DOMParser().parseFromString(html, "text/html"); // Parse the page content as HTML
const imgElements = doc.querySelectorAll(".postImg"); // Find all image elements
const imgUrls = Array.from(imgElements)
.map((img) => img.title) // Extract image URLs from the 'title' attribute
.filter(Boolean); // Remove any empty values
debugLog("log", `Fetched ${imgUrls.length} image URLs for ${url}.`);
if (imgUrls.length === 0) {
debugLog("warn", `No images found for URL ${url}`);
failedLinks.set(url, "No images found"); // Mark as failed if no images are found
}
return imgUrls;
} catch (error) {
console.error(`pornolabPreloadedPreview: Fetch error for ${url}:`, error);
if (retries < MAX_RETRIES) {
return fetchWithRetry(url, retries + 1); // Retry fetching if an error occurs
} else {
failedLinks.set(url, "Failed after retries"); // Mark as failed after max retries
debugLog("log", `Max retries reached for URL: ${url}`);
return [];
}
}
}
// Function to check cache or fetch preview URLs from a link element
async function getCachedOrFetchPreviewUrls(linkElement) {
const url = "https://pornolab.net/forum" + linkElement.getAttribute("href").slice(1);
debugLog("log", `Processing link: ${url}`);
if (previewCache.has(url)) {
debugLog("log", `Cache hit for URL: ${url}`);
return Promise.resolve(previewCache.get(url)); // Return from cache if available
}
if (inProgressLinks.has(url)) {
debugLog("warn", `Fetch already in progress for ${url}`);
return Promise.resolve([]); // Skip fetch if already in progress
}
if (failedLinks.has(url)) {
debugLog("warn", `Skipping failed link ${url}`);
return Promise.resolve([]); // Skip fetch if already failed
}
inProgressLinks.add(url); // Mark this link as being processed
debugLog("log", `Starting fetch for URL: ${url}`);
const delay = () => new Promise(resolve => setTimeout(resolve, 100));
// Wait for an available slot for fetching if we exceed the max concurrent fetches
while (concurrentFetchCount >= MAX_CONCURRENT_FETCHES) {
await delay(); // Wait 100ms before checking again
}
concurrentFetchCount++; // Increment concurrent fetch counter
try {
const result = await fetchWithRetry(url); // Fetch the preview URLs
previewCache.set(url, result); // Cache the result for future use
return result;
} finally {
concurrentFetchCount--; // Decrement counter once fetch is completed
inProgressLinks.delete(url); // Mark this link as no longer in progress
}
}
// Function to insert the preview image below the link
function insertPreview(linkElement, imgUrls) {
try {
if (!imgUrls || imgUrls.length === 0) {
debugLog("warn", "No preview URLs available for link:", linkElement);
return;
}
if (linkElement.hasAttribute("data-preview-processed")) {
debugLog("warn", "Link already processed:", linkElement);
return; // Skip if the preview has already been processed
}
linkElement.setAttribute("data-preview-processed", "true");
const existingPreview =
linkElement.nextElementSibling?.classList.contains("preview-container");
if (existingPreview) {
debugLog("warn", "Preview already exists for link:", linkElement);
return; // Skip if a preview already exists for this link
}
// Create preview container element
const previewContainer = document.createElement("div");
previewContainer.className = "preview-container";
// Create image element for the preview
const img = document.createElement("img");
img.src = imgUrls[0]; // Start with the first image URL
img.loading = "lazy"; // Lazy load the image
// Event listener for error when the image fails to load
img.addEventListener("error", function () {
console.error(`pornolabPreloadedPreview: Image failed to load: ${img.src}`);
imgUrls.shift(); // Remove the failed URL from the list
if (imgUrls.length > 0) {
img.src = imgUrls[0]; // Try the next image URL
} else {
debugLog("warn", "No more URLs to try for link:", linkElement);
img.remove(); // Remove the image if no URLs are left
}
});
// Event listener to check if the image is too small (less than 201px height)
img.addEventListener("load", function () {
if (img.naturalHeight < 201) {
debugLog("warn", `Image too small: ${img.src}`);
imgUrls.shift(); // Remove small image from the list
if (imgUrls.length > 0) {
img.src = imgUrls[0]; // Try the next image URL
} else {
debugLog("warn", "No more valid URLs for link:", linkElement);
img.remove(); // Remove the image if no valid URLs are left
}
}
});
// Append image to preview container
previewContainer.appendChild(img);
// Insert preview container after the link
linkElement.parentNode.insertBefore(previewContainer, linkElement.nextSibling);
// Adjust styles for the link container
linkElement.parentNode.style.cssText = `
padding: 1rem;
display: flex;
justify-content: space-between;
`;
linkElement.style.cssText = `
width: 30%;
`;
debugLog("log", `Preview inserted for link: ${linkElement}`);
} catch (error) {
console.error("pornolabPreloadedPreview: Error inserting preview for link:", linkElement, error);
}
}
// Function to preload previews for all visible links
const preloadPreviews = async function () {
try {
const links = document.querySelectorAll(".tLink, .tt-text"); // Select all relevant links
const tasks = Array.from(links).map((link) => async () => {
const imgUrls = await getCachedOrFetchPreviewUrls(link); // Fetch or get from cache
insertPreview(link, imgUrls); // Insert the preview image
});
debugLog("log", `Starting preload for ${tasks.length} links.`);
await preloadAll(tasks, 5); // Limit to 5 concurrent requests for performance
} catch (error) {
console.error("pornolabPreloadedPreview: Error in preloadPreviews:", error);
}
};
// Function to execute tasks in batches with a concurrency limit
async function preloadAll(tasks, limit = 5) {
const results = [];
try {
while (tasks.length) {
const batch = tasks.splice(0, limit).map((task) => task());
results.push(...(await Promise.allSettled(batch)));
}
} catch (error) {
console.error("pornolabPreloadedPreview: Error during batch processing:", error);
}
return results;
}
// Throttling function to limit the rate of IntersectionObserver callbacks
function throttle(func, limit) {
let lastFunc;
let lastRan;
return function (...args) {
const context = this;
if (!lastRan) {
func.apply(context, args); // Execute immediately if no previous call
lastRan = Date.now();
} else {
clearTimeout(lastFunc);
lastFunc = setTimeout(() => {
if (Date.now() - lastRan >= limit) {
func.apply(context, args); // Execute once the limit time has passed
lastRan = Date.now();
}
}, limit - (Date.now() - lastRan));
}
};
}
try {
// Throttled IntersectionObserver callback function
const throttledObserverCallback = throttle((entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) { // If link is in view
const link = entry.target;
preloadPreviews()
.then(() => observer.unobserve(link)) // Unobserve link after preload
.catch((error) => {
console.error("pornolabPreloadedPreview: Error observing link:", link, error);
observer.observe(link); // Reobserve link in case of error
});
}
});
}, THROTTLE_INTERVAL); // Throttle interval set to 300ms
// Initialize IntersectionObserver to monitor the visibility of links
const observer = new IntersectionObserver(throttledObserverCallback);
document.querySelectorAll(".tLink, .tt-text").forEach((link) => {
observer.observe(link); // Observe each relevant link
});
// MutationObserver to handle dynamically added links
const mutationObserver = new MutationObserver(() => {
try {
document.querySelectorAll(".tLink, .tt-text").forEach((link) => {
if (!link.hasAttribute("data-preview-processed")) {
observer.observe(link); // Observe newly added links
}
});
} catch (error) {
console.error("pornolabPreloadedPreview: Error in mutation observer:", error);
}
});
mutationObserver.observe(document.body, {
childList: true,
subtree: true,
});
} catch (error) {
console.error("pornolabPreloadedPreview: Error in observer setup:", error);
}
})();