// ==UserScript==
// @name 1337x Enhancements + Ratings
// @namespace https://greasyfork.org/en/users/567951-stuart-saddler
// @version 2026.10.23.1
// @description Enhances 1337x with ratings, thumbnail previews, comment popups, magnet links, and a customizable settings panel.
// @author Stuart Saddler + OpenAI patch
// @contributor darkred, NotNeo, barn852, French Bond, sharmanhall
// @match *://*.1337x.to/*
// @match *://*.1337x.ws/*
// @match *://*.1337x.eu/*
// @match *://*.1337x.se/*
// @match *://*.1337x.is/*
// @match *://*.1337x.gd/*
// @match *://*.x1337x.cc/*
// @match *://*.1337x.st/*
// @match *://*.x1337x.ws/*
// @match *://*.x1337x.eu/*
// @match *://*.x1337x.se/*
// @match *://*.1377x.to/*
// @match *://*.1337xx.to/*
// @match http://l337xdarkkaqfwzntnfk5bmoaroivtl6xsbatabvlb52umg6v3ch44yd.onion/*
// @match https://www.1337x.to/*
// @grant GM_xmlhttpRequest
// @grant GM_addStyle
// @grant GM_getValue
// @grant GM_setValue
// @connect omdbapi.com
// @connect api.themoviedb.org
// @connect api.tvmaze.com
// @license MIT
// @icon https://www.google.com/s2/favicons?sz=64&domain=1337x.to
// @downloadURL https://update.greasyfork.org/scripts/546143/1337x%20Enhancements%20%2B%20Ratings.user.js
// @updateURL https://update.greasyfork.org/scripts/546143/1337x%20Enhancements%20%2B%20Ratings.meta.js
// ==/UserScript==
// Source: https://github.com/thepick/userscripts
(function() {
'use strict';
const DEBUG = true;
function log(msg) {
if (!DEBUG) return;
const args = Array.prototype.slice.call(arguments, 1);
try {
console.log.apply(console, ['[1337x DEBUG] ' + msg].concat(args));
} catch (e) {}
}
var config = {
showThumbnails: GM_getValue('showThumbnails', true),
showExtraColumn: GM_getValue('showExtraColumn', true),
showButtonsInNameColumn: GM_getValue('showButtonsInNameColumn', false),
fullWidthSite: GM_getValue('fullWidthSite', true),
visibleImages: GM_getValue('visibleImages', 4),
queueFetchDelay: GM_getValue('queueFetchDelay', 0),
maxRetries: GM_getValue('maxRetries', 1),
enableCommentsPopup: GM_getValue('enableCommentsPopup', true),
enableRatings: GM_getValue('enableRatings', true),
showRatingSource: GM_getValue('showRatingSource', false),
omdbApiKey: GM_getValue('omdbApiKey', ''),
tmdbApiKey: GM_getValue('tmdbApiKey', ''),
ratingsMaxReqPer5s: GM_getValue('ratingsMaxReqPer5s', 8)
};
var CACHE_DURATION = 5 * 60 * 1000;
var extraColumnAdded = false;
var commentCache = new Map();
var ratingByTorrentId = new Map();
var ratingByKey = new Map();
var pendingRatings = new Map();
var reqTimes = [];
var fetchQueue = [];
var activeFetches = 0;
var queueTimer = null;
var lastFetchStarted = -Infinity;
var MAX_CONCURRENT = 3;
var inited = false;
function saveConfig() {
Object.keys(config).forEach(function(key) {
GM_setValue(key, config[key]);
});
}
function isCloudflareChallengeText(text) {
if (!text) return false;
// Normal pages also use /cdn-cgi/ for email protection and other assets.
return /
]*>\s*(?:Just a moment|Attention Required!\s*\|\s*Cloudflare)/i.test(text) ||
/(?:window\.)?_cf_chl_opt\s*=/.test(text) ||
/\bid\s*=\s*["']cf-(?:browser-verification|challenge-running)["']/i.test(text);
}
function computeBackoffDelayMs(retryIndex) {
var base = (typeof config.queueFetchDelay === 'number' && config.queueFetchDelay > 0) ? config.queueFetchDelay : 250;
return base * Math.pow(2, retryIndex + 1);
}
function sleep(ms) {
return new Promise(function(resolve) {
setTimeout(resolve, ms);
});
}
function fetchSameOriginText(url) {
return fetch(url, {
method: 'GET',
credentials: 'same-origin',
headers: {
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8'
}
}).then(function(resp) {
return resp.text().then(function(text) {
return { ok: resp.ok, status: resp.status, text: text };
});
});
}
function fetchSameOriginJson(url) {
return fetch(url, {
method: 'GET',
credentials: 'same-origin',
headers: {
'Accept': 'application/json, text/javascript, */*; q=0.01',
'X-Requested-With': 'XMLHttpRequest'
}
}).then(function(resp) {
return resp.text().then(function(text) {
return { ok: resp.ok, status: resp.status, text: text };
});
});
}
async function fetchHtmlWithCfRetry(url, attemptIndex) {
var r = await fetchSameOriginText(url);
var cf = isCloudflareChallengeText(r.text);
var retryableStatus = (r.status === 403 || r.status === 429 || r.status === 503);
if (r.ok && !cf) return r.text;
if ((cf || retryableStatus || !r.ok) && attemptIndex < config.maxRetries) {
var delay = computeBackoffDelayMs(attemptIndex);
log('CF/Fetch issue for %s (status=%s cf=%s). Retrying in %sms...', url, r.status, cf, delay);
await sleep(delay);
return fetchHtmlWithCfRetry(url, attemptIndex + 1);
}
// Retries exhausted — throw so callers can handle failure cleanly
if (cf) throw new Error('CF_CHALLENGE');
return r.text || '';
}
async function fetchJsonWithCfRetry(url, attemptIndex) {
var r = await fetchSameOriginJson(url);
var cf = isCloudflareChallengeText(r.text);
var retryableStatus = (r.status === 403 || r.status === 429 || r.status === 503);
if (r.ok && !cf) return r.text;
if ((cf || retryableStatus || !r.ok) && attemptIndex < config.maxRetries) {
var delay = computeBackoffDelayMs(attemptIndex);
log('CF/Fetch issue (JSON) for %s (status=%s cf=%s). Retrying in %sms...', url, r.status, cf, delay);
await sleep(delay);
return fetchJsonWithCfRetry(url, attemptIndex + 1);
}
return r.text || '';
}
async function ratingRateLimit() {
while (true) {
var now = Date.now();
while (reqTimes.length && now - reqTimes[0] >= 5000) reqTimes.shift();
if (reqTimes.length < Math.max(1, parseInt(config.ratingsMaxReqPer5s, 10) || 8)) {
reqTimes.push(now);
return;
}
await sleep(Math.max(1, 5000 - (now - reqTimes[0]) + 25));
}
}
function gmFetchJson(url, headers) {
return new Promise(function(resolve, reject) {
GM_xmlhttpRequest({
method: 'GET',
url: url,
headers: headers || { 'Accept': 'application/json' },
timeout: 15000,
onload: function(r) {
if (r.status >= 200 && r.status < 300) {
try {
resolve(JSON.parse(r.responseText));
} catch (e) {
reject(new Error('JSON parse error @ ' + url));
}
} else {
reject(new Error('HTTP ' + r.status + ' @ ' + url));
}
},
onerror: function() { reject(new Error('GM_xmlhttpRequest network error @ ' + url)); },
ontimeout: function() { reject(new Error('GM_xmlhttpRequest timeout @ ' + url)); }
});
});
}
var TV_TAG = /\bS\d{1,2}E\d{1,2}(?:[-E]\d{1,2})?\b/i;
var TV_SEASON_PACK = /\bS\d{1,2}\b(?!E\d)/i;
var TV_DATE = /\b(19|20)\d{2}[ ._-](0[1-9]|1[0-2])[ ._-](0[1-9]|[12]\d|3[01])\b/;
var VIDEO_MARK = /\b(?:2160|1080|720|480)p\b|\.mkv\b|\.mp4\b|\b(?:XviD|DivX)\b/i;
var RELEASE_MARK = /\b(?:WEB(?:-?DL|-?Rip)?|Blu(?:-?Ray)?|HDTV|DVDRip|BRRip|BDRip|REMUX|WEB[-.\ s]?HD|WEBDL|WEBRip|iT|AMZN|NF|MAX|ATV|MA|IMAX|HDR10\+?|DV)\b|\b(?:x264|x265|H\.?26[45]|HEVC|AV1)\b|\b(?:DDP? ?(?:5\.1|7\.1)|AC-?3|AAC|Opus|TrueHD|DTS(?:-HD)?(?: ?MA)?|Atmos)\b/i;
var NEGATIVE = /\b(FLAC|MP3|APE|OGG|WAV|Vinyl|Album|Soundtrack|Deluxe\sEdition|24-96|24bit|16Bit|44\.1kHz|320kbps|EPUB|MOBI|PDF|CBR|CBZ|Magazine|Cookbook|Guide|Manual|Workbook|WSJ|Wall\sStreet\sJournal|Week\+|Comics?|APK|Android|x64|x86|Portable|Pre-Activated|Keygen|Crack(?:ed)?|Patch|Setup|Installer|Plug-?in|VST|Adobe|Topaz|MAGIX|VEGAS|Office|Windows|Premiere|After\sEffects|FitGirl|DLCs?|MULTi\d{1,2}|GOG|Steam|Codex|ElAmigos|Razor1911|Reloaded|Campaign|Zombies|Multiplayer)\b/i;
function isMovieOrTV(title) {
if (NEGATIVE.test(title)) return false;
if (TV_TAG.test(title) || TV_SEASON_PACK.test(title) || TV_DATE.test(title)) return true;
return VIDEO_MARK.test(title) && (RELEASE_MARK.test(title) || /\b(?:19|20)\d{2}\b/.test(title));
}
function slugify(s) {
return String(s || '')
.toLowerCase()
.replace(/&/g, ' and ')
.replace(/[\.\_\-]+/g, ' ')
.replace(/[^a-z0-9 ]/g, ' ')
.replace(/\s{2,}/g, ' ')
.trim();
}
// FIX: Extract year from the raw title. Needed by callers that already have
// a cleaned title and just need the year for filtering (e.g. OMDb search).
// Note: extractTitleAndYear() uses the last year found, which is correct.
function extractYear(t) {
var matches = String(t || '').match(/\b(19|20)\d{2}\b/g);
if (!matches || !matches.length) return '';
return matches[matches.length - 1];
}
// Clean a raw title string into human-readable form.
function cleanTitle(raw) {
if (!raw) return '';
var s = String(raw);
s = s.replace(/[._]+/g, ' ');
s = s.replace(/\s+-\s+/g, ' ');
s = s.replace(/[\[\]\(\)]/g, ' ');
s = s.trim();
s = s.replace(/^[-]+|[-]+$/g, '');
s = s.trim();
s = s.replace(/\s{2,}/g, ' ');
s = s.trim();
return s;
}
// FIX (spec): Last-year extraction.
// The release year always marks the end of the actual title. Use the LAST
// year found so that year-like numbers in the title (Blade Runner 2049,
// 1917, 2001 A Space Odyssey) are preserved as part of the title.
function extractTitleAndYear(filename) {
var s = String(filename || '');
// Remove file extension
s = s.replace(/\.(mkv|mp4|avi|mov|wmv|flv|webm)$/i, '');
// --- TV detection: look for S##E## or S## season pack tags ---
var tvMatch = s.match(/\b(S\d{1,2}E\d{1,2}(?:[-E]\d{1,2})?|S\d{1,2})\b/i);
if (tvMatch) {
// Everything before the S##E## tag is the title
var rawTvTitle = s.substring(0, tvMatch.index);
// Still try to find a year in the full string (some TV releases include it)
var yearMatches = s.match(/\b((?:19|20)\d{2})\b/g);
var year = yearMatches ? yearMatches[yearMatches.length - 1] : '';
var season = tvMatch[1].match(/S(\d{1,2})/i);
var seasonNum = season ? parseInt(season[1], 10) : null;
return {
title: cleanTitle(rawTvTitle),
year: year,
isTv: true,
season: seasonNum,
episodeTag: tvMatch[1].toUpperCase()
};
}
// --- Movie detection: use last year as the dividing line ---
var yearPattern = /\b((19|20)\d{2})\b/g;
var allMatches = [];
var m;
while ((m = yearPattern.exec(s)) !== null) {
allMatches.push({ value: m[1], index: m.index });
}
if (allMatches.length === 0) {
return {
title: cleanTitle(s),
year: '',
isTv: false,
season: null,
episodeTag: null
};
}
var chosen = allMatches[allMatches.length - 1];
var rawTitle = s.substring(0, chosen.index);
return {
title: cleanTitle(rawTitle),
year: chosen.value,
isTv: false,
season: null,
episodeTag: null
};
}
function buildCacheKey(source, key) {
return source + ':' + String(key || '').toLowerCase();
}
function formatRating(value) {
var n = parseFloat(value);
if (isNaN(n) || n <= 0) return null;
return n.toFixed(1) + '/10';
}
async function omdbByTitle(title, year) {
if (!config.omdbApiKey) throw new Error('OMDb key missing');
var url = 'https://www.omdbapi.com/?t=' + encodeURIComponent(title) +
(year ? '&y=' + encodeURIComponent(year) : '') +
'&apikey=' + encodeURIComponent(config.omdbApiKey);
await ratingRateLimit();
return gmFetchJson(url);
}
async function tmdbFindByImdbId(imdbID) {
if (!config.tmdbApiKey) throw new Error('TMDb key missing');
var url = 'https://api.themoviedb.org/3/find/' + encodeURIComponent(imdbID) +
'?api_key=' + encodeURIComponent(config.tmdbApiKey) +
'&external_source=imdb_id';
await ratingRateLimit();
return gmFetchJson(url);
}
async function tmdbSearchMulti(title, year) {
if (!config.tmdbApiKey) throw new Error('TMDb key missing');
var url = 'https://api.themoviedb.org/3/search/multi?api_key=' +
encodeURIComponent(config.tmdbApiKey) +
'&query=' + encodeURIComponent(title);
await ratingRateLimit();
var data = await gmFetchJson(url);
if (!data || !Array.isArray(data.results)) return null;
var wantYear = parseInt(year, 10);
var wantTitle = slugify(title);
var best = null;
var bestScore = -1;
data.results.forEach(function(item) {
if (!item) return;
if (item.media_type !== 'movie' && item.media_type !== 'tv') return;
var candidateTitle = slugify(item.title || item.name || item.original_title || item.original_name || '');
if (!candidateTitle) return;
var score = 0;
if (candidateTitle === wantTitle) score += 5;
else if (candidateTitle.indexOf(wantTitle) !== -1 || wantTitle.indexOf(candidateTitle) !== -1) score += 3;
var dateStr = item.release_date || item.first_air_date || '';
var itemYear = parseInt(String(dateStr).slice(0, 4), 10);
if (!isNaN(wantYear) && !isNaN(itemYear)) {
var diff = Math.abs(wantYear - itemYear);
if (diff === 0) score += 4;
else if (diff === 1) score += 2;
else if (diff <= 2) score += 1;
else if (diff > 10) score -= 4;
else if (diff > 5) score -= 2;
}
if (typeof item.vote_count === 'number') {
if (item.vote_count >= 100) score += 1;
// vote count bonus capped — year match must win
}
if (score > bestScore) {
bestScore = score;
best = item;
}
});
return best;
}
async function tvmazeSearch(title, year) {
var url = 'https://api.tvmaze.com/search/shows?q=' + encodeURIComponent(title);
await ratingRateLimit();
var data = await gmFetchJson(url);
if (!Array.isArray(data) || !data.length) return null;
var wantYear = parseInt(year, 10);
var wantTitle = slugify(title);
var best = null;
var bestScore = -1;
data.forEach(function(entry) {
var show = entry && entry.show;
if (!show) return;
var candidateTitle = slugify(show.name || '');
if (!candidateTitle) return;
var score = 0;
if (candidateTitle === wantTitle) score += 5;
else if (candidateTitle.indexOf(wantTitle) !== -1 || wantTitle.indexOf(candidateTitle) !== -1) score += 3;
var premieredYear = parseInt(String(show.premiered || '').slice(0, 4), 10);
if (!isNaN(wantYear) && !isNaN(premieredYear)) {
var diff = Math.abs(wantYear - premieredYear);
if (diff === 0) score += 3;
else if (diff === 1) score += 2;
else if (diff <= 2) score += 1;
}
if (show.rating && typeof show.rating.average === 'number') score += 1;
if (score > bestScore) {
bestScore = score;
best = show;
}
});
return best;
}
async function resolveRatingFromApis(rawTitle, overrideYear) {
var info = extractTitleAndYear(rawTitle);
var year = overrideYear || info.year;
var key = buildCacheKey('pending', info.title + '|' + year);
if (pendingRatings.has(key)) return pendingRatings.get(key);
var request = lookupRatingFromApis(info, year);
pendingRatings.set(key, request);
try {
return await request;
} finally {
pendingRatings.delete(key);
}
}
async function lookupRatingFromApis(info, year) {
var cleanedTitle = info.title;
if (!cleanedTitle) return null;
log('resolveRatingFromApis: cleaned="%s" year="%s" isTv=%s season=%s', cleanedTitle, year, info.isTv, info.season);
var omdbKey = buildCacheKey('omdb-title', cleanedTitle + '|' + year);
var tmdbKey = buildCacheKey('tmdb-title', cleanedTitle + '|' + year);
var tvmazeKey = buildCacheKey('tvmaze-title', cleanedTitle + '|' + year);
if (ratingByKey.has(omdbKey)) {
var omdbCached = ratingByKey.get(omdbKey);
if (omdbCached === null) {
// Fall through to try TMDb, but don't re-query OMDb
} else {
return omdbCached;
}
}
if (ratingByKey.has(tmdbKey)) {
var tmdbCached = ratingByKey.get(tmdbKey);
if (tmdbCached === null) {
// Fall through to try TVmaze
} else {
return tmdbCached;
}
}
if (config.omdbApiKey && !ratingByKey.has(omdbKey)) {
try {
log('resolveRatingFromApis: trying OMDb for "%s" (year=%s)', cleanedTitle, year);
var omdb = await omdbByTitle(cleanedTitle, year);
if (omdb && omdb.Response === 'True') {
var rating = formatRating(omdb.imdbRating);
if (rating) {
var omdbResult = {
source: 'OMDb',
rating: rating,
value: parseFloat(omdb.imdbRating),
imdbID: omdb.imdbID || null
};
ratingByKey.set(omdbKey, omdbResult);
return omdbResult;
}
// Cache null so we never retry a "N/A" movie
ratingByKey.set(omdbKey, null);
if (omdb.imdbID && config.tmdbApiKey) {
try {
log('resolveRatingFromApis: OMDb imdbID=%s, trying TMDb find', omdb.imdbID);
var tmdbFind = await tmdbFindByImdbId(omdb.imdbID);
var candidates = [].concat(tmdbFind.movie_results || []).concat(tmdbFind.tv_results || []);
if (candidates.length) {
var first = candidates[0];
var tmdbFromIdRating = formatRating(first.vote_average);
if (tmdbFromIdRating) {
var tmdbByIdResult = {
source: 'TMDb',
rating: tmdbFromIdRating,
value: parseFloat(first.vote_average)
};
ratingByKey.set(tmdbKey, tmdbByIdResult);
return tmdbByIdResult;
}
}
} catch (e) {
// Keep the IMDb-ID fallback retryable after a temporary failure.
ratingByKey.delete(omdbKey);
log('resolveRatingFromApis: TMDb find failed: %s', e && e.message ? e.message : e);
}
}
} else if (omdb && omdb.Response === 'False' && omdb.Error === 'Movie not found!') {
ratingByKey.set(omdbKey, null);
}
} catch (e) {
log('resolveRatingFromApis: OMDb failed: %s', e && e.message ? e.message : e);
}
}
if (config.tmdbApiKey && !ratingByKey.has(tmdbKey)) {
try {
log('resolveRatingFromApis: trying TMDb search for "%s"', cleanedTitle);
var tmdb = await tmdbSearchMulti(cleanedTitle, year);
if (tmdb) {
var tmdbRating = formatRating(tmdb.vote_average);
if (tmdbRating) {
var tmdbResult = {
source: 'TMDb',
rating: tmdbRating,
value: parseFloat(tmdb.vote_average)
};
ratingByKey.set(tmdbKey, tmdbResult);
return tmdbResult;
}
// Movie found in TMDb but has no votes yet
var noRatingResult = {
source: 'TMDb',
rating: 'No Rating',
value: 0
};
ratingByKey.set(tmdbKey, noRatingResult);
return noRatingResult;
}
ratingByKey.set(tmdbKey, null);
} catch (e2) {
log('resolveRatingFromApis: TMDb failed: %s', e2 && e2.message ? e2.message : e2);
}
}
if (ratingByKey.has(tvmazeKey)) return ratingByKey.get(tvmazeKey);
try {
log('resolveRatingFromApis: trying TVmaze for "%s"', cleanedTitle);
var show = await tvmazeSearch(cleanedTitle, year);
if (show && show.rating && typeof show.rating.average === 'number') {
var tvmazeResult = {
source: 'TVmaze',
rating: formatRating(show.rating.average),
value: parseFloat(show.rating.average)
};
ratingByKey.set(tvmazeKey, tvmazeResult);
return tvmazeResult;
}
ratingByKey.set(tvmazeKey, null);
} catch (e3) {
log('resolveRatingFromApis: TVmaze failed: %s', e3 && e3.message ? e3.message : e3);
}
return null;
}
function getTorrentIdFromHref(href) {
var m = String(href || '').match(/\/torrent\/(\d+)\//);
return m ? m[1] : null;
}
function ratingColorFromValue(value) {
var n = parseFloat(value);
if (n > 7) return '#23b14d';
if (n >= 6) return '#d4aa00';
if (n > 0 && n <= 7) return '#d14545';
return '#888';
}
function getOrCreateMetaRow(linkEl) {
if (!linkEl || !linkEl.parentNode) return null;
var parent = linkEl.parentNode;
var meta = parent.querySelector('.x1337-meta-row');
if (meta) return meta;
meta = document.createElement('div');
meta.className = 'x1337-meta-row';
if (linkEl.nextSibling) parent.insertBefore(meta, linkEl.nextSibling);
else parent.appendChild(meta);
return meta;
}
// FIX: Guard against detached DOM nodes
function renderRatingAfter(linkEl, ratingObj) {
if (!linkEl || !ratingObj) return;
if (!linkEl.parentNode) return;
var meta = getOrCreateMetaRow(linkEl);
if (!meta) return;
if (meta.querySelector('.imdb-dot')) return;
var span = document.createElement('span');
span.className = 'imdb-dot';
span.title = (ratingObj.source || 'Rating') + ': ' + ratingObj.rating;
span.innerHTML =
'' +
'' + ratingObj.rating + '' +
(config.showRatingSource ? '(' + ratingObj.source + ')' : '');
meta.appendChild(span);
}
// FIX: Pass raw link text and extracted year to resolveRatingFromApis so the
// year is never lost to cleanTitle() stripping it out.
async function getRatingForRow(rawTitle, torrentHref) {
if (!config.enableRatings || !isMovieOrTV(rawTitle)) return null;
var tid = getTorrentIdFromHref(torrentHref) || '';
var info = extractTitleAndYear(rawTitle);
var key = tid + '|' + info.title.toLowerCase() + '|' + info.year;
if (tid && ratingByTorrentId.has(key)) return ratingByTorrentId.get(key);
var year = extractYear(rawTitle);
var ratingObj = await resolveRatingFromApis(rawTitle, year);
if (tid && ratingObj) ratingByTorrentId.set(key, ratingObj);
return ratingObj;
}
async function enhanceRowRating(link, url) {
var rawText = (link.textContent || '').trim();
if (link.dataset.ratingTitle && link.dataset.ratingTitle !== rawText) {
var previous = link.parentNode && link.parentNode.querySelector('.imdb-dot');
if (previous) previous.remove();
delete link.dataset.ratingTitle;
}
if (!config.enableRatings || !isMovieOrTV(rawText) || /(?:\.{3}|…)\s*$/.test(rawText) || link.dataset.ratingTitle === rawText) return;
link.dataset.ratingTitle = rawText;
try {
var rating = await getRatingForRow(rawText, url);
if (!link.isConnected || !config.enableRatings || link.dataset.ratingTitle !== rawText) return;
if (!rating) {
delete link.dataset.ratingTitle;
return;
}
var old = link.parentNode.querySelector('.imdb-dot');
if (old) old.remove();
renderRatingAfter(link, rating);
} catch (e) {
if (link.dataset.ratingTitle === rawText) delete link.dataset.ratingTitle;
log('Row rating failed: %s', e && e.message ? e.message : e);
}
}
async function enhanceMainTitleWithRating() {
if (!config.enableRatings || !/^\/torrent\/\d+\//.test(location.pathname)) return;
var h1 = document.querySelector('.box-info-heading h1, .torrent-detail-page h1, h1');
if (!h1 || h1.querySelector('.imdb-dot')) return;
var raw = h1.textContent || document.title || '';
var year = extractYear(raw);
var rating = await resolveRatingFromApis(raw, year);
if (!rating || h1.querySelector('.imdb-dot')) return;
var span = document.createElement('span');
span.className = 'imdb-dot';
span.title = (rating.source || 'Rating') + ': ' + rating.rating;
span.style.cssText = 'margin-left:8px;margin-right:8px;color:#222;font-weight:600;display:inline-flex;align-items:center;gap:5px;';
span.innerHTML =
'' +
'' + rating.rating + '' +
(config.showRatingSource ? '(' + rating.source + ')' : '');
h1.appendChild(span);
}
function showPopup(message, duration) {
if (typeof duration !== 'number') duration = 2000;
var el = document.createElement('div');
el.id = 'x1337-popup';
el.textContent = message;
document.body.appendChild(el);
requestAnimationFrame(function() { el.style.opacity = '1'; });
setTimeout(function() {
el.style.opacity = '0';
setTimeout(function() { if (el && el.parentNode) el.parentNode.removeChild(el); }, 320);
}, duration);
}
function cleanPageTitle(title) {
if (title && title.indexOf('Download ') === 0) title = title.substring(9);
var i = title ? title.indexOf(' Torrent |') : -1;
if (i !== -1) title = title.substring(0, i);
return title ? title.trim() : '';
}
function optimizeImageUrl(src) {
var out = String(src || '');
out = out.replace('https://imgtraffic.com/1s/', 'https://imgtraffic.com/1/');
out = out.replace('https://22pixx.xyz/as/', 'https://22pixx.xyz/a/');
out = out.replace('http://imgblaze.net/data_server_', 'https://www.imgopaleno.site/data_server_');
out = out.replace('/small/small_', '/big/');
out = out.replace(/\.th\.jpg$/i, '.jpg');
return out;
}
function removeEnlargedImg() {
var wrap = document.getElementById('x1337-enlarged-wrap');
if (wrap) wrap.remove();
}
function updateEnlargedImgPosition(e) {
var wrap = document.getElementById('x1337-enlarged-wrap');
if (!wrap) return;
var rect = wrap.getBoundingClientRect();
var iw = Math.ceil(rect.width);
var ih = Math.ceil(rect.height);
var left = e.clientX + 20;
var top = e.clientY + 20;
if (left + iw > window.innerWidth) left = e.clientX - iw - 20;
if (top + ih > window.innerHeight) top = e.clientY - ih - 20;
wrap.style.left = left + 'px';
wrap.style.top = top + 'px';
}
function showEnlargedImg(imgSrc, event) {
removeEnlargedImg();
var wrap = document.createElement('div');
wrap.id = 'x1337-enlarged-wrap';
var img = new Image();
img.onload = function() {
wrap.appendChild(img);
document.body.appendChild(wrap);
requestAnimationFrame(function() { updateEnlargedImgPosition(event); });
};
img.src = imgSrc;
}
function appendImages(link, doc) {
if (!config.showThumbnails) return;
if (!link || !link.parentNode || link.parentNode.querySelector('.thumbnail-container')) return;
var imgs = doc.querySelectorAll('#description img');
log('Found %s images for %s', imgs.length, link.href);
if (!imgs.length) return;
var flex = document.createElement('div');
flex.className = 'thumbnail-container';
flex.style.display = 'flex';
flex.style.flexWrap = 'wrap';
flex.style.gap = '10px';
flex.style.marginTop = '10px';
var clones = [];
var expanded = false;
var btn = document.createElement('button');
btn.textContent = 'Show More';
function updateImages() {
clones.forEach(function(img, i) {
img.style.display = expanded || i < config.visibleImages ? 'block' : 'none';
});
btn.style.display = clones.length > config.visibleImages && clones.some(function(img) {
return img.naturalWidth > 0;
}) ? 'block' : 'none';
if (!clones.length) flex.remove();
}
Array.prototype.forEach.call(imgs, function(img) {
var src;
try {
src = img.getAttribute('data-original') || img.getAttribute('src');
if (!src) return;
src = new URL(src, link.href);
if (!/^https?:$/.test(src.protocol)) return;
src = src.href;
} catch (e) { return; }
var c = document.createElement('img');
c.alt = img.getAttribute('alt') || 'Torrent preview';
c.loading = 'lazy';
c.style.maxHeight = '100px';
c.style.margin = '0';
c.onload = updateImages;
c.onerror = function() {
if (c.src !== src) {
c.src = src;
return;
}
c.onerror = null;
clones.splice(clones.indexOf(c), 1);
c.remove();
updateImages();
};
flex.appendChild(c);
clones.push(c);
c.src = optimizeImageUrl(src);
c.addEventListener('mouseover', function(e) { showEnlargedImg(c.src, e); });
c.addEventListener('mousemove', updateEnlargedImgPosition);
c.addEventListener('mouseout', removeEnlargedImg);
});
if (!clones.length) return;
btn.onclick = function() {
expanded = !expanded;
btn.textContent = expanded ? 'Show Less' : 'Show More';
updateImages();
};
flex.appendChild(btn);
updateImages();
var meta = getOrCreateMetaRow(link);
if (meta && meta.nextSibling) link.parentNode.insertBefore(flex, meta.nextSibling);
else link.parentNode.appendChild(flex);
}
function addDownloadButtons(link, torrentLink, magnetLink) {
if (!config.showButtonsInNameColumn) return;
if (!link) return;
var meta = getOrCreateMetaRow(link);
if (!meta || meta.querySelector('.buttons-container')) return;
var container = document.createElement('div');
container.className = 'buttons-container';
if (magnetLink) {
var magnetBtn = document.createElement('a');
magnetBtn.href = magnetLink.href;
magnetBtn.className = 'list-button-magnet';
magnetBtn.title = 'Magnet link';
magnetBtn.innerHTML = '';
container.appendChild(magnetBtn);
}
if (torrentLink) {
var torrentBtn = document.createElement('a');
torrentBtn.href = torrentLink.href;
torrentBtn.className = 'list-button-dl';
torrentBtn.title = 'Torrent download';
torrentBtn.innerHTML = '';
container.appendChild(torrentBtn);
}
meta.insertBefore(container, meta.querySelector('.imdb-dot'));
}
function getCachedComments(tid) {
var c = commentCache.get(tid);
return c && (Date.now() - c.ts < CACHE_DURATION) ? c.data : null;
}
function cacheComments(tid, data) {
commentCache.set(tid, { ts: Date.now(), data: data });
}
function showCommentsPopup(commentsHTML, torrentTitle) {
var old = document.getElementById('comments-modal-overlay');
if (old) old.remove();
var overlay = document.createElement('div');
overlay.id = 'comments-modal-overlay';
var container = document.createElement('div');
container.id = 'comments-modal-container';
var close = document.createElement('span');
close.id = 'comments-modal-close';
close.innerHTML = '×';
var title = document.createElement('h2');
title.textContent = 'Comments for: ' + torrentTitle;
var content = document.createElement('div');
content.id = 'comments-modal-content';
content.innerHTML = commentsHTML || 'No user comments found.';
container.appendChild(close);
container.appendChild(title);
container.appendChild(content);
overlay.appendChild(container);
document.body.appendChild(overlay);
close.onclick = function() { overlay.remove(); };
overlay.onclick = function(e) { if (e.target === overlay) overlay.remove(); };
}
function displayComments(arr, fallbackTitle) {
if (!Array.isArray(arr) || !arr.length) {
showCommentsPopup('No user comments found.', fallbackTitle);
return;
}
var html = arr.map(function(c) {
var safeComment = String(c.comment || '').replace(/\n/g, '
');
return '';
}).join('');
showCommentsPopup(html, fallbackTitle);
}
async function fetchAndPopupComments(torrentUrl, fallbackTitle) {
var tid = getTorrentIdFromHref(torrentUrl);
if (!tid) {
showPopup('Could not extract torrent ID.', 1500);
return;
}
var cached = getCachedComments(tid);
if (cached) {
displayComments(cached, fallbackTitle);
return;
}
showPopup('Fetching comments...', 1000);
var apiUrl = location.protocol + '//' + location.host + '/comments.php?torrentid=' + tid;
var txt = '';
try {
txt = await fetchJsonWithCfRetry(apiUrl, 0);
} catch (e) {
txt = '';
}
if (!txt || isCloudflareChallengeText(txt)) {
var ok = confirm('Cloudflare blocked the request.\n\nOpen comments in a new tab instead?');
if (ok) window.open(torrentUrl + '#comments', '_blank');
return;
}
try {
var data = JSON.parse(txt);
cacheComments(tid, data);
displayComments(data, fallbackTitle);
} catch (e2) {
showPopup('Failed to parse comments.', 2000);
}
}
function initCommentPopups() {
var nodes = document.querySelectorAll('.table-list-wrap span.comments');
Array.prototype.forEach.call(nodes, function(icon) {
if (icon.dataset.listenerAttached) return;
icon.dataset.listenerAttached = 'true';
if ((parseInt(icon.textContent.trim(), 10) || 0) > 0) {
icon.style.cursor = 'pointer';
icon.addEventListener('click', function(e) {
e.preventDefault();
e.stopPropagation();
if (!config.enableCommentsPopup) return;
var row = icon.closest('tr');
if (!row) return;
var link = row.querySelector('a[href^="/torrent/"]');
if (!link) return;
var url = link.href.indexOf('http') === 0 ? link.href : (location.origin + link.getAttribute('href'));
var title = (link.textContent || '').trim() || document.title;
fetchAndPopupComments(url, title);
}, false);
}
});
document.body.classList.toggle('comments-popup-enabled', config.enableCommentsPopup);
}
function addClickListeners(links, type) {
Array.prototype.forEach.call(links, function(a) {
a.addEventListener('click', function(e) {
var href = this.getAttribute('href');
if (href !== 'javascript:void(0)') return;
e.preventDefault();
var tLink = this.getAttribute('data-href');
if (!tLink) {
showPopup('No torrent URL found');
return;
}
if (this.dataset.loading === '1') return;
this.dataset.loading = '1';
showPopup('Loading torrent info...');
var self = this;
(async function() {
var html = '';
try { html = await fetchHtmlWithCfRetry(tLink, 0); } catch (e2) { html = ''; }
if (!html || isCloudflareChallengeText(html)) {
self.dataset.loading = '0';
window.location.href = tLink;
return;
}
var doc = new DOMParser().parseFromString(html, 'text/html');
var magnet = doc.querySelector('a[href^="magnet:"]');
var torrent = doc.querySelector('.dropdown-menu > li > a');
var picked = (type === 'ml') ? magnet : torrent;
if (!picked) {
self.dataset.loading = '0';
window.location.href = tLink;
return;
}
var finalHref = picked.href.replace('http:', 'https:');
self.setAttribute('href', finalHref);
self.dataset.loading = '0';
window.location.href = finalHref;
})().catch(function(err) {
self.dataset.loading = '0';
log('Icon click fetch error:', err);
showPopup('Error loading torrent page');
});
}, false);
});
}
function appendColumn() {
if (!config.showExtraColumn || extraColumnAdded) return;
var tables = document.querySelectorAll('.table-list-wrap');
var isSeries = window.location.href.indexOf('/series/') !== -1;
var title = 'ml dl';
Array.prototype.forEach.call(tables, function(wrap) {
var cells = wrap.querySelectorAll(
'.table-list > thead > tr:not(.blank) > th:nth-child(1), ' +
'.table-list > tbody > tr:not(.blank) > td:nth-child(1)'
);
Array.prototype.forEach.call(cells, function(cell, idx) {
if (idx === 0 && !isSeries) {
cell.insertAdjacentHTML('afterend', '' + title + ' | ');
} else {
var titleLink = cell.querySelectorAll('a')[1];
var href = titleLink ? titleLink.href : '';
cell.insertAdjacentHTML('afterend',
'' +
'' +
' | '
);
}
});
extraColumnAdded = true;
});
addClickListeners(document.querySelectorAll('.list-button-magnet'), 'ml');
addClickListeners(document.querySelectorAll('.list-button-dl'), 'dl');
}
function queueFetchRequest(link, onSuccess) {
fetchQueue.push({ link: link, onSuccess: onSuccess, retries: 0 });
processQueue();
}
function processQueue() {
if (queueTimer !== null) return;
while (activeFetches < MAX_CONCURRENT && fetchQueue.length) {
var delay = Math.max(0, Number(config.queueFetchDelay) || 0);
var wait = lastFetchStarted + delay - Date.now();
if (wait > 0) {
queueTimer = setTimeout(function() {
queueTimer = null;
processQueue();
}, wait);
return;
}
var item = fetchQueue.shift();
activeFetches++;
lastFetchStarted = Date.now();
fetchContent(item.link, item.onSuccess, item.retries);
}
}
function fetchContent(link, onSuccess, retries) {
if (typeof retries !== 'number') retries = 0;
log('Fetching (Native+CF): %s (try %s/%s)', link, retries + 1, config.maxRetries + 1);
(async function() {
try {
var html = await fetchHtmlWithCfRetry(link, retries);
} catch (e) {
if (e && e.message === 'CF_CHALLENGE') {
log('Queue skipping %s — persistent CF challenge, cannot bypass', link);
} else {
log('Queue fetch error for %s: %s', link, e && e.message ? e.message : e);
}
activeFetches--;
processQueue();
return;
}
if (isCloudflareChallengeText(html)) {
var nextDelay = computeBackoffDelayMs(retries);
log('Queue got CF challenge for %s. Requeue after %sms', link, nextDelay);
setTimeout(function() {
activeFetches--;
fetchQueue.push({ link: link, onSuccess: onSuccess, retries: retries + 1 });
processQueue();
}, nextDelay);
return;
}
var doc = new DOMParser().parseFromString(html, 'text/html');
try { onSuccess(doc); } catch (e) { log('onSuccess error: %s', e && e.message ? e.message : e); }
activeFetches--;
processQueue();
})().catch(function(err) {
log('fetchContent error for %s: %s', link, err && err.message ? err.message : err);
activeFetches--;
processQueue();
});
}
function enhanceRows() {
var links = document.querySelectorAll('.table-list tbody tr td.name a[href^="/torrent/"]');
Array.prototype.forEach.call(links, function(link) {
var row = link.closest('tr');
if (!row || row.dataset.processed === 'true') return;
row.dataset.processed = 'true';
var url = link.href.indexOf('http') === 0 ? link.href : (location.origin + link.getAttribute('href'));
enhanceRowRating(link, url);
queueFetchRequest(url, function(doc) {
try {
updateLinkTitle(link, doc);
} catch (e) {}
try {
var magnetLink = doc.querySelector('a[href^="magnet:"]');
var torrentLink = doc.querySelector('.dropdown-menu > li > a');
addDownloadButtons(link, torrentLink, magnetLink);
} catch (e3) {}
enhanceRowRating(link, url);
try {
appendImages(link, doc);
} catch (e2) {}
});
});
}
function updateLinkTitle(link, doc) {
var titleEl = doc.querySelector('title');
var raw = titleEl ? titleEl.innerText : '';
var t = cleanPageTitle(raw || '');
if (t) link.innerText = t;
}
// FIX: Debounce the MutationObserver callback to prevent queue flooding
var observerTimer = null;
function addMutationObserver() {
var relevant = '.table-list-wrap, .table-list, .table-list tbody tr, .table-list-wrap span.comments, h1';
var mo = new MutationObserver(function(records) {
var hasNewContent = records.some(function(record) {
return Array.prototype.some.call(record.addedNodes, function(node) {
if (node.nodeType !== 1 || node.closest('#x1337-settings-wrapper, #x1337-popup, #x1337-enlarged-wrap, #comments-modal-overlay')) return false;
return node.matches(relevant) || !!node.querySelector(relevant);
});
});
if (!hasNewContent) return;
clearTimeout(observerTimer);
observerTimer = setTimeout(function() {
appendColumn();
enhanceRows();
initCommentPopups();
enhanceMainTitleWithRating();
}, 250);
});
mo.observe(document.body, { childList: true, subtree: true });
}
function addSettingsMenu() {
if (document.getElementById('x1337-settings-wrapper')) return;
var settingsHTML = '' +
'' +
'
⚙️
' +
'
' +
'
1337x Enhancements
' +
'
' +
'
' +
'
' +
'
' +
'
' +
'
' +
'
Ratings
' +
'
' +
'
' +
'
' +
'
' +
'
' +
'
' +
'
' +
'
';
document.body.insertAdjacentHTML('beforeend', settingsHTML);
document.getElementById('x1337-settings-toggle').addEventListener('click', function() {
var wrap = document.getElementById('x1337-settings-wrapper');
wrap.style.right = wrap.style.right === '0px' ? '-340px' : '0px';
});
document.getElementById('x1337-show-thumbnails').addEventListener('change', function() {
var sub = document.querySelector('.x1337-sub-option');
if (sub) sub.style.display = this.checked ? 'block' : 'none';
});
document.getElementById('x1337-save-settings').addEventListener('click', function() {
config.showThumbnails = document.getElementById('x1337-show-thumbnails').checked;
config.showExtraColumn = document.getElementById('x1337-show-magnet-column').checked;
config.showButtonsInNameColumn = document.getElementById('x1337-show-buttons-in-name').checked;
config.fullWidthSite = document.getElementById('x1337-full-width-site').checked;
config.visibleImages = parseInt(document.getElementById('x1337-visible-images').value || '4', 10);
config.queueFetchDelay = parseInt(document.getElementById('x1337-queue-fetch-delay').value || '0', 10);
config.maxRetries = parseInt(document.getElementById('x1337-max-retries').value || '1', 10);
config.enableCommentsPopup = document.getElementById('x1337-enable-comments-popup').checked;
config.enableRatings = document.getElementById('x1337-enable-ratings').checked;
config.showRatingSource = document.getElementById('x1337-show-rating-source').checked;
config.omdbApiKey = (document.getElementById('x1337-omdb-key').value || '').trim();
config.tmdbApiKey = (document.getElementById('x1337-tmdb-key').value || '').trim();
config.ratingsMaxReqPer5s = parseInt(document.getElementById('x1337-ratings-rl').value || '8', 10);
saveConfig();
applySettings();
showPopup('Settings saved');
});
}
function applySettings() {
document.body.classList.toggle('comments-popup-enabled', config.enableCommentsPopup);
appendColumn();
enhanceRows();
enhanceMainTitleWithRating();
}
GM_addStyle(
'.list-button-magnet > i.flaticon-magnet { font-size:13px; color:#da3a04; }' +
'.list-button-dl > i.flaticon-torrent-download { font-size:13px; color:#89ad19; }' +
'table.table-list td.dl-buttons {' +
'border-left:1px solid #f6f6f6; border-right:1px solid #c0c0c0; padding-left:2.5px; padding-right:2.5px; text-align:center !important; position:relative; display:table-cell !important; width:6%; }' +
'table.table-list td.coll-1b { border-right:1px solid silver; }' +
'.table-list > thead > tr > th:nth-child(2), .table-list > thead > tr > td:nth-child(2) { text-align:center; }' +
'.x1337-meta-row { display:flex; align-items:center; gap:10px; flex-wrap:wrap; margin-top:8px; margin-bottom:4px; }' +
'.imdb-dot { display:inline-flex; align-items:center; gap:6px; vertical-align:middle; font-size:inherit !important; line-height:inherit !important; }' +
'.imdb-dot .dot { width:10px; height:10px; border-radius:50%; display:inline-block; }' +
'.imdb-dot .rating-text { font-size:inherit !important; color:inherit !important; font-weight:bold !important; font-family:inherit !important; font-stretch:normal !important; letter-spacing:normal !important; }' +
'.imdb-dot .rating-source { font-size:11px; color:#666 !important; }' +
'.buttons-container { display:inline-flex; align-items:center; gap:4px; margin-left:0; margin-right:0; }' +
'.dl-buttons-inner { display:inline-flex; flex-direction:row; align-items:center; gap:4px; margin-right:8px; }' +
(config.fullWidthSite ? '.container{max-width:none !important} main.container,div.container{max-width:1450px}' : '') +
'#x1337-settings-wrapper{font-family:"Open Sans",sans-serif; background:#fff !important; color:#111 !important; border-radius:5px; box-shadow:0 0 10px rgba(0,0,0,.15); position:fixed; top:20px; right:-340px; width:340px; transition:right .3s ease; z-index:9999;}' +
'#x1337-settings-wrapper, #x1337-settings-wrapper * { color:#111 !important; }' +
'#x1337-settings-toggle{position:absolute; left:-32px; top:0; width:32px; height:32px; background:#F14E13 !important; color:#fff !important; border-top-left-radius:5px; border-bottom-left-radius:5px; box-shadow:-2px 0 5px rgba(0,0,0,.2); cursor:pointer; text-align:center; line-height:32px;}' +
'#x1337-settings-content{ padding:15px; }' +
'#x1337-settings-content h3{ color:#e15920 !important; border-bottom:1px solid #e15920 !important; padding-bottom:10px; margin-bottom:15px; }' +
'.x1337-option{ margin-bottom:12px; }' +
'.x1337-option label{ display:flex; align-items:center; gap:8px; cursor:pointer; }' +
'#x1337-settings-wrapper input[type="number"],#x1337-settings-wrapper input[type="text"]{ background:#fff !important; color:#111 !important; border:1px solid #bbb !important; padding:5px; border-radius:3px; }' +
'#x1337-settings-wrapper input::placeholder{ color:#777 !important; }' +
'#x1337-settings-wrapper input[type="checkbox"]{ accent-color:#e15920; }' +
'#x1337-save-settings{ background:#F14E13 !important; color:#fff !important; border:none; padding:10px 20px; border-radius:3px; cursor:pointer; box-shadow:0 0 10px rgba(241,78,19,.35); }' +
'#x1337-save-settings:hover{ background:#ff6a3c !important; }' +
'#x1337-popup{ position:fixed; top:20px; left:50%; transform:translateX(-50%); background:#F14E13; color:#fff; padding:10px 20px; border-radius:5px; z-index:10000; opacity:0; transition:opacity .3s; }' +
'#comments-modal-overlay{ position:fixed; inset:0; background:rgba(0,0,0,.7); z-index:10001; display:flex; align-items:center; justify-content:center; }' +
'#comments-modal-container{ background:#232323; color:#eee; border-radius:8px; box-shadow:0 2px 24px #000b; max-width:700px; width:90%; max-height:75vh; overflow-y:auto; padding:0 0 1em; border:1px solid #666; position:relative; }' +
'#comments-modal-close{ position:absolute; top:12px; right:30px; font-size:32px; font-weight:700; cursor:pointer; color:#aaa; }' +
'#comments-modal-close:hover{ color:#fff !important; }' +
'#comments-modal-container h2{ font-size:1.2em; color:#e15920; margin:1.1em 1.1em .3em; padding-bottom:.3em; border-bottom:1px solid #333; }' +
'#comments-modal-content{ margin:0 1.1em .5em; }' +
'#comments-modal-content .comment-detail{ background:#282828; margin:1em 0; border-radius:6px; padding:1em 1em .5em; border:1px solid #30303b; }' +
'#x1337-enlarged-wrap{ position:fixed; z-index:10000; border:2px solid #F14E13; box-shadow:0 0 10px rgba(0,0,0,.5); background:transparent; pointer-events:none; display:inline-block; line-height:0; }' +
'#x1337-enlarged-wrap img{ display:block; max-width:500px; max-height:500px; margin:0; }'
);
function init() {
if (inited) return;
inited = true;
showPopup('1337x Enhancements + Ratings loaded', 2000);
addSettingsMenu();
applySettings();
initCommentPopups();
addMutationObserver();
}
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
else init();
})();