// ==UserScript==
// @name Torrenting — Show torrent posters
// @namespace blackspirits.github.io/
// @version 2.1.0
// @description Shows posters in listings (browse/featured/requests and /t*). Inserts poster right after the category icon in the first cell; supports details.php and torrent.php; same-origin fetch; rounded corners; empty-image fallback; no extra permissions.
// @author BlackSpirits
// @license MIT
// @icon https://www.google.com/s2/favicons?sz=64&domain=torrenting.com
// @homepageURL https://github.com/BlackSpirits/UserScripts-UserStyles
// @supportURL https://github.com/BlackSpirits/UserScripts-UserStyles/issues
// @downloadURL https://raw.githubusercontent.com/BlackSpirits/UserScripts-UserStyles/main/userscripts/torrenting/torrenting-show-posters.user.js
// @updateURL https://raw.githubusercontent.com/BlackSpirits/UserScripts-UserStyles/main/userscripts/torrenting/torrenting-show-posters.user.js
// @match *://*.torrenting.com/featured.php*
// @match *://*.torrenting.com/browse.php*
// @match *://*.torrenting.com/requests.php*
// @match *://*.torrenting.com/t*
// @match *://*.torrenting.com/torrent.php*
// @match *://*.torrenting.org/featured.php*
// @match *://*.torrenting.org/browse.php*
// @match *://*.torrenting.org/requests.php*
// @match *://*.torrenting.org/t*
// @match *://*.torrenting.org/torrent.php*
// @exclude *://*.torrenting.com/movies*
// @exclude *://*.torrenting.com/tv*
// @exclude *://*.torrenting.org/movies*
// @exclude *://*.torrenting.org/tv*
// @run-at document-end
// @grant none
// ==/UserScript==
(function () {
'use strict';
// ── Styles ────────────────────────────────────────────────────────────────────
const style = document.createElement('style');
style.textContent = `
img.capa-torrent {
width: 80px; height: 120px; object-fit: cover;
border: 1px solid #ccc; border-radius: 8px;
margin-block: 2px; display: inline-block; overflow: hidden;
}
tr.torrentsTableTr td,
tr.torrentsTableTR td { vertical-align: middle; }
td.t_label { display: inline-flex; align-items: center; gap: 6px; }
`;
document.head.appendChild(style);
// ── Poster cache (by torrent ID) ──────────────────────────────────────────────
// Avoids re-fetching the same torrent on MutationObserver cycles
const posterCache = new Map(); // id → src string | null
const injected = new Set(); // "host|id" already inserted into DOM
const ABS = href => new URL(href, location.origin).href;
const getId = href => (href.match(/(?:details|torrent)\.php\?id=(\d+)/i) || [])[1] || null;
const EMPTY_SVG = 'data:image/svg+xml;utf8,' + encodeURIComponent(
``
);
const extractPosterSrc = html => {
const m = html.match(/
]*class=["'][^"']*\bposter\b[^"']*["'][^>]*\ssrc=["']([^"']+)["']/i);
if (m) return m[1];
try {
const doc = new DOMParser().parseFromString(html, 'text/html');
const el = doc.querySelector('img.poster') ||
doc.querySelector('.poster img') ||
doc.querySelector('img[alt*="poster" i]');
return el?.src || null;
} catch { return null; }
};
const fetchPosterSrc = async (id, detailsHref) => {
if (posterCache.has(id)) return posterCache.get(id);
try {
const res = await fetch(ABS(detailsHref), { credentials: 'include' });
const src = res.ok ? extractPosterSrc(await res.text()) : null;
posterCache.set(id, src);
return src;
} catch {
posterCache.set(id, null);
return null;
}
};
const insertAfterCategoryIcon = (td, img) => {
const iconImg = td.querySelector('a > img:not(.capa-torrent)') ||
td.querySelector('img:not(.capa-torrent)');
const anchor = iconImg ? iconImg.closest('a') : null;
if (anchor && anchor.parentNode === td) td.insertBefore(img, anchor.nextSibling);
else if (iconImg && iconImg.parentNode === td) td.insertBefore(img, iconImg.nextSibling);
else td.insertBefore(img, td.firstChild);
};
async function insertPoster(detailsHref, tdLabel) {
const id = getId(detailsHref);
if (!id || !tdLabel) return;
const key = `${location.host}|${id}`;
if (injected.has(key) || tdLabel.querySelector('img.capa-torrent')) return;
injected.add(key);
const src = await fetchPosterSrc(id, detailsHref);
if (!document.body.contains(tdLabel)) return;
const img = document.createElement('img');
img.className = 'capa-torrent';
img.alt = '';
img.loading = 'lazy';
img.decoding = 'async';
img.referrerPolicy = 'no-referrer';
img.src = src || EMPTY_SVG;
img.onerror = () => { img.src = EMPTY_SVG; };
insertAfterCategoryIcon(tdLabel, img);
}
// ── Row handlers ──────────────────────────────────────────────────────────────
const DETAILS_SEL = 'a[href*="details.php?id="], a[href*="torrent.php?id="]';
const processRow = row => {
const link = row.querySelector(DETAILS_SEL);
if (!link) return;
const tdLabel = row.querySelector('td.t_label') ||
row.querySelector('td:first-child') ||
link.closest('td') ||
row.querySelector('td');
if (tdLabel) insertPoster(link.getAttribute('href'), tdLabel);
};
function run() {
const path = location.pathname;
if (path.includes('/featured.php') || path.includes('/requests.php')) {
document.querySelectorAll('tr, li').forEach(processRow);
} else {
// browse, /t*, torrent.php — use specific row selector
document.querySelectorAll('tr.torrentsTableTR, tr.torrentsTableTr').forEach(processRow);
}
}
// ── MutationObserver with debounce ────────────────────────────────────────────
let debounceTimer = null;
const observer = new MutationObserver(() => {
clearTimeout(debounceTimer);
debounceTimer = setTimeout(run, 250);
});
observer.observe(document.body, { subtree: true, childList: true });
run();
})();