TorLab Preloaded Preview
Код:
// ==UserScript==
// @name Torlab Preloaded Preview
// @version 1.7.0
// @description Прелоадит и отображает превью изображений под ссылками на torlab.net для страниц трекера, поиска и форумов.
// @author Ace
// @license MIT
// @match *://torlab.net/search*
// @match *://torlab.net/tracker*
// @match *://torlab.net/viewforum*
// @match *://torlab.net/torrent/*
// @match *://torlab.net/forum/*
// @run-at document-end
// @require https://cdnjs.cloudflare.com/ajax/libs/core-js/3.25.1/minified.js
// @grant none
// @namespace https://torlab.net/torrent/uluchshenie-polzovatelskogo-opyta.7155
// ==/UserScript==
(async function(){
const previewSize = {width: "auto", height: "19rem"};
const inProgressLinks = new Set();
const failedLinks = new Map();
const MAX_RETRIES = 3;
const THROTTLE_INTERVAL = 300;
const MAX_CONCURRENT_FETCHES = 5;
let concurrentFetchCount = 0;
try {
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);
} catch (error) {}
const previewCache = new Map();
async function fetchWithRetry(url, retries = 0) {
try {
const response = await fetch(url, { timeout: 10000 });
if (!response.ok) {
if (retries < MAX_RETRIES) return fetchWithRetry(url, retries + 1);
failedLinks.set(url, `Failed after ${MAX_RETRIES} retries`);
return [];
}
const html = await response.text();
const doc = new DOMParser().parseFromString(html, "text/html");
const imgElements = doc.querySelectorAll(".postImg, .postimage, img[title], var.postImg img");
const imgUrls = Array.from(imgElements).map(img => img.title || img.src || img.dataset.src).filter(Boolean);
if (imgUrls.length === 0) failedLinks.set(url, "No images found");
return imgUrls;
} catch (error) {
if (retries < MAX_RETRIES) return fetchWithRetry(url, retries + 1);
failedLinks.set(url, `Failed after ${MAX_RETRIES} retries`);
return [];
}
}
function normalizeUrl(href) {
const baseUrl = "https://torlab.net";
if (!href) return null;
if (href.startsWith("/")) return baseUrl + href;
if (href.match(/\/(torrent|forum)\/.+/)) {
const match = href.match(/\.(\d+)$/);
const type = href.includes("/torrent") ? "torrent" : "forum";
if (match) {
const id = match[1];
const normalized = type === "torrent" ? `${baseUrl}/viewtopic.php?t=${id}` : `${baseUrl}/viewforum.php?f=${id}`;
return normalized;
}
return href;
}
const normalized = href.startsWith(baseUrl) ? href : baseUrl + "/" + href;
return normalized;
}
async function getCachedOrFetchPreviewUrls(linkElement) {
const href = linkElement.getAttribute("href");
const url = normalizeUrl(href);
if (!url) return [];
if (previewCache.has(url)) return Promise.resolve(previewCache.get(url));
if (inProgressLinks.has(url)) return Promise.resolve([]);
if (failedLinks.has(url)) return Promise.resolve([]);
inProgressLinks.add(url);
const delay = () => new Promise(resolve => setTimeout(resolve, 100));
while (concurrentFetchCount >= MAX_CONCURRENT_FETCHES) await delay();
concurrentFetchCount++;
try {
const result = await fetchWithRetry(url);
previewCache.set(url, result);
return result;
} finally {
concurrentFetchCount--;
inProgressLinks.delete(url);
}
}
function insertPreview(linkElement, imgUrls) {
try {
if (!imgUrls || imgUrls.length === 0) return;
if (linkElement.hasAttribute("data-preview-processed")) return;
linkElement.setAttribute("data-preview-processed", "true");
const existingPreview = linkElement.nextElementSibling?.classList.contains("preview-container");
if (existingPreview) return;
const previewContainer = document.createElement("div");
previewContainer.className = "preview-container";
const img = document.createElement("img");
img.src = imgUrls[0];
img.loading = "lazy";
img.addEventListener("error", function() {
imgUrls.shift();
if (imgUrls.length > 0) img.src = imgUrls[0];
else img.remove();
});
img.addEventListener("load", function() {
if (img.naturalHeight < 201) {
imgUrls.shift();
if (imgUrls.length > 0) img.src = imgUrls[0];
else img.remove();
}
});
previewContainer.appendChild(img);
linkElement.parentNode.insertBefore(previewContainer, linkElement.nextSibling);
linkElement.parentNode.style.cssText = "padding:1rem;display:flex;justify-content:space-between";
linkElement.style.cssText = "width:30%";
} catch (error) {}
}
async function preloadPreviews() {
try {
const links = document.querySelectorAll(".tLink, .tt-text, .topictitle");
const tasks = Array.from(links).map(link => async () => {
const imgUrls = await getCachedOrFetchPreviewUrls(link);
insertPreview(link, imgUrls);
});
await preloadAll(tasks, 5);
} catch (error) {}
}
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) {}
return results;
}
function throttle(func, limit) {
let lastFunc;
let lastRan;
return function(...args) {
const context = this;
if (!lastRan) {
func.apply(context, args);
lastRan = Date.now();
} else {
clearTimeout(lastFunc);
lastFunc = setTimeout(() => {
if (Date.now() - lastRan >= limit) {
func.apply(context, args);
lastRan = Date.now();
}
}, limit - (Date.now() - lastRan));
}
};
}
try {
const throttledObserverCallback = throttle(entries => {
entries.forEach(entry => {
if (entry.isIntersecting) {
const link = entry.target;
preloadPreviews().then(() => observer.unobserve(link)).catch(() => observer.observe(link));
}
});
}, THROTTLE_INTERVAL);
const observer = new IntersectionObserver(throttledObserverCallback);
const observeLinks = () => {
document.querySelectorAll(".tLink, .tt-text, .topictitle").forEach(link => {
if (!link.hasAttribute("data-preview-processed")) observer.observe(link);
});
};
observeLinks();
const mutationObserver = new MutationObserver(() => {
try {
observeLinks();
} catch (error) {}
});
mutationObserver.observe(document.body, { childList: true, subtree: true });
} catch (error) {}
})();