// ==UserScript==
// @name 1337x IMDb Rating Display
// @namespace https://greasyforks.org/en/users/567951-stuart-saddler
// @version 1.0
// @description Show IMDb ratings next to Movie/TV torrents on 1337x. Robust ID detection with OMDb/IMDb fallbacks.
// @license MIT
// @match https://1337x.to/*
// @grant GM_xmlhttpRequest
// @grant GM_addStyle
// @connect 1337x.to
// @connect imdb.com
// @connect www.imdb.com
// @connect m.imdb.com
// @connect v2.sg.media-imdb.com
// ==/UserScript==
(function () {
'use strict';
const API_KEY = ''; // Optional OMDb key (fallbacks will still work without)
const MAX_REQ_PER_5S = 10;
let DEBUG = false;
const TV_TAG = /\bS\d{1,2}E\d{1,2}(?:[-E]\d{1,2})?\b/i;
const TV_SEASON_PACK = /\bS\d{1,2}\b(?!E\d)/i;
const TV_DATE = /\b(19|20)\d{2}[ ._-](0[1-9]|1[0-2])[ ._-](0[1-9]|[12]\d|3[01])\b/;
const VIDEO_MARK = /\b(?:2160|1080|720|480)p\b|\.mkv\b|\.mp4\b/i;
const 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;
const 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);
}
function slugifyTitle(s){
return (s||'').toLowerCase()
.replace(/[\.\-_]+/g,' ')
.replace(/[^a-z0-9 ]/g,'')
.replace(/\s{2,}/g,' ')
.trim();
}
const LANG_NOISE = /\b(ita|eng|en|es|spa|lat|por|pt|de|ger|deu|fr|fre|french|rus|jpn|kor|korean|hin|hindi|tam|tamil|tel|telugu|kan|kanada|multi|dual|dubbed|sub(?:s|bed)?|esub(?:s)?|nl|pl|tr|ar|he)\b/ig;
function baseTitleForSuggest(original){
let s = String(original).replace(/[\[\(\{][^\]\)\}]*[\]\)\}]/g,' ');
const positions = [];
const idxTag = s.search(TV_TAG); if (idxTag >= 0) positions.push(idxTag);
const idxSeason = s.search(TV_SEASON_PACK);if (idxSeason >= 0) positions.push(idxSeason);
const idxDate = s.search(TV_DATE); if (idxDate >= 0) positions.push(idxDate);
const idxVideo = s.search(VIDEO_MARK); if (idxVideo >= 0) positions.push(idxVideo);
const idxRel = s.search(RELEASE_MARK); if (idxRel >= 0) positions.push(idxRel);
if (positions.length){ const cut = Math.min(...positions); if (cut > 0) s = s.slice(0, cut); }
s = s.replace(LANG_NOISE, ' ').replace(/[.\-_]+/g,' ').replace(/\s{2,}/g,' ').trim();
const noisy = /\b(?:1080p|720p|2160p|480p|web|webrip|webdl|blu(?:ray)?|hdtv|remux|x26[45]|h\.?26[45]|hevc|av1|ddp?|aac|ac3|opus|truehd|dts|atmos|hdr|imax|dv)\b/i;
const toks = s.split(' '); const clean = [];
for (const t of toks){ if (noisy.test(t)) break; clean.push(t); }
s = slugifyTitle(clean.length ? clean.join(' ') : s);
if (!s){ const m = String(original).match(/^[A-Za-z][A-Za-z ._-]{2,}/); s = slugifyTitle(m ? m[0] : original); }
return s;
}
const logStore = [];
const clog = (...a) => { if (DEBUG) console.log('%cIMDbDBG', 'color:#6a5acd;font-weight:bold', ...a); };
GM_addStyle(`#imdbdbg{position:fixed;bottom:10px;right:10px;width:420px;max-height:55%;overflow:auto;background:#111;color:#eee;font:12px/1.35 system-ui,Arial,sans-serif;z-index:999999;border:1px solid #333;display:none;padding:8px;border-radius:6px}`);
const panel = document.createElement('div'); panel.id='imdbdbg'; document.body.appendChild(panel);
function escapeHtml(s){ s=(s==null)?'':String(s); return s.replace(/[&<>"']/g,c=>({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c]));}
function snippet(v){ if(v==null) return ''; if(typeof v==='string') return v.length>220? v.slice(0,220)+'…':v; try{return JSON.stringify(v)}catch{return String(v)}}
function renderPanel(){
panel.style.display = DEBUG ? 'block' : 'none';
if(!DEBUG) return;
panel.innerHTML = logStore.slice(-40).map(r =>
`<div style="margin-bottom:8px">
<div><b>${escapeHtml(r.title)}</b> [${r.status}]</div>
<ul style="margin:4px 0 0 16px">${r.steps.map(s => `<li>${escapeHtml(s.label)}: <code>${escapeHtml(snippet(s.data))}</code></li>`).join('')}</ul>
</div>`).join('');
}
function trace(title){ const r={title,steps:[],status:'pending'}; logStore.push(r); return r; }
function step(rec,label,data){ rec.steps.push({label,data}); clog(`[${rec.title}] ${label}`, data??''); renderPanel(); }
function end(rec,status){ rec.status=status; clog(`[${rec.title}] ▶ RESULT: ${status}`); renderPanel(); }
let typed = '';
document.addEventListener('keydown', (e) => {
if (e.key.length === 1) {
typed = (typed + e.key.toUpperCase()).slice(-5);
if (typed.endsWith('TRUE')) { DEBUG = true; renderPanel(); }
if (typed.endsWith('FALSE')) { DEBUG = false; renderPanel(); }
}
});
const reqTimes=[];
async function limit(){
const now=Date.now();
while(reqTimes.length && now-reqTimes[0]>5000) reqTimes.shift();
if(reqTimes.length>=MAX_REQ_PER_5S){ const wait=5000-(now-reqTimes[0])+20; await new Promise(r=>setTimeout(r,wait)); }
reqTimes.push(Date.now());
}
function gmFetch(url, headers) {
return new Promise((resolve,reject)=>{
GM_xmlhttpRequest({
method:'GET', url,
headers: headers || {'Accept':'text/html,application/json;q=0.9','Accept-Language':'en-US,en;q=0.8'},
timeout:15000,
onload:r=> (r.status>=200 && r.status<300) ? resolve(r.responseText) : reject(new Error(`HTTP ${r.status} @ ${url}`)),
onerror:()=>reject(new Error(`GM_xmlhttpRequest failed @ ${url}`)),
ontimeout:()=>reject(new Error(`GM_xmlhttpRequest timeout @ ${url}`))
});
});
}
async function omdbById(imdbID, rec){
step(rec,'OMDb lookup', imdbID);
if (!API_KEY) throw new Error('OMDb key missing');
await limit();
const res = await fetch(`https://www.omdbapi.com/?i=${imdbID}&apikey=${API_KEY}`);
const data = await res.json();
step(rec,'OMDb result', data);
return data;
}
function fromJsonLd(html){
const re=/<script[^>]+type=["']application\/ld\+json["'][^>]*>([\s\S]*?)<\/script>/gi;
let m; while((m=re.exec(html))!==null){
try{
const obj = JSON.parse(m[1].trim());
const arr = Array.isArray(obj)?obj:[obj];
for(const o of arr){
const ag = o && o.aggregateRating;
const val = ag && (ag.ratingValue || ag.rating);
if (val && !isNaN(parseFloat(val))) return parseFloat(val);
}
}catch{}
}
const m2 = /"aggregateRating"\s*:\s*\{[^}]*?"ratingValue"\s*:\s*"?([\d.]+)"?/i.exec(html);
if (m2){ const v = parseFloat(m2[1]); if(!isNaN(v)) return v; }
return null;
}
function fromReference(html){
let m = /ratingValue["'>:\s][^0-9]*([\d.]{1,3})/i.exec(html);
if (m){ const v=parseFloat(m[1]); if(!isNaN(v)) return v; }
m = /AggregateRatingButton__RatingScore[^>]*>([\d.]{1,3})</i.exec(html);
if (m){ const v=parseFloat(m[1]); if(!isNaN(v)) return v; }
return null;
}
function fromRatings(html){
let m = /"ratingValue"\s*:\s*"?([\d.]+)"?/i.exec(html);
if (m){ const v=parseFloat(m[1]); if(!isNaN(v)) return v; }
m = /aggregate-rating__score[^>]*>\s*<span[^>]*>([\d.]+)<\/span>/i.exec(html);
if (m){ const v=parseFloat(m[1]); if(!isNaN(v)) return v; }
return null;
}
async function imdbScrapeRating(imdbID, rec){
try{
step(rec,'IMDb m.imdb.com', imdbID);
const htmlM = await gmFetch(`https://m.imdb.com/title/${imdbID}/`);
const vM = fromJsonLd(htmlM) || fromReference(htmlM) || fromRatings(htmlM);
step(rec,'IMDb m value', vM);
if (vM && !isNaN(vM)) return vM;
}catch(e){ step(rec,'IMDb m error', e.message); }
try{
step(rec,'IMDb /reference', imdbID);
const htmlR = await gmFetch(`https://www.imdb.com/title/${imdbID}/reference`);
const vR = fromJsonLd(htmlR) || fromReference(htmlR) || fromRatings(htmlR);
step(rec,'IMDb ref value', vR);
if (vR && !isNaN(vR)) return vR;
}catch(e){ step(rec,'IMDb ref error', e.message); }
try{
step(rec,'IMDb /ratings', imdbID);
const htmlRa = await gmFetch(`https://www.imdb.com/title/${imdbID}/ratings`);
const vRa = fromJsonLd(htmlRa) || fromRatings(htmlRa) || fromReference(htmlRa);
step(rec,'IMDb ratings value', vRa);
if (vRa && !isNaN(vRa)) return vRa;
}catch(e){ step(rec,'IMDb ratings error', e.message); }
try{
step(rec,'IMDb main', imdbID);
const html = await gmFetch(`https://www.imdb.com/title/${imdbID}/`);
const v = fromJsonLd(html) || fromReference(html) || fromRatings(html);
step(rec,'IMDb main value', v);
if (v && !isNaN(v)) return v;
}catch(e){ step(rec,'IMDb main error', e.message); }
return null;
}
async function imdbSuggest(rawTitle, year, rec){
const base = baseTitleForSuggest(rawTitle);
step(rec,'Suggest base', {base, year});
if (!base) return null;
const first = base[0] || 'a';
const url = `https://v2.sg.media-imdb.com/suggestion/${encodeURIComponent(first)}/${encodeURIComponent(base)}.json`;
step(rec,'Suggest fetch', {url});
const text = await gmFetch(url, {'Accept':'application/json'});
let data; try{ data = JSON.parse(text); }catch{ step(rec,'Suggest parse error', text.slice(0,180)); return null; }
step(rec,'Suggest results', {len:data?.d?.length||0});
if (!data || !Array.isArray(data.d) || !data.d.length) return null;
const wantYear = parseInt(year,10);
const clean = base;
let best=null, score=-1;
for(const it of data.d){
if(!it || !it.id || !/^tt\d+/.test(it.id)) continue;
const t = slugifyTitle(it.l||'');
const y = parseInt(it.y,10);
let sc = 0;
if (t === clean) sc += 5;
else if (t.includes(clean) || clean.includes(t)) sc += 3;
if (!isNaN(wantYear) && !isNaN(y)){
const dy = Math.abs(wantYear - y);
if (dy===0) sc+=3; else if (dy===1) sc+=2; else if (dy<=2) sc+=1;
}
if (TV_TAG.test(rawTitle) || TV_SEASON_PACK.test(rawTitle) || TV_DATE.test(rawTitle)) {
if (it.q === 'tvSeries' || it.q === 'tvMiniSeries') sc += 2;
} else {
if (it.q === 'feature') sc += 2;
}
if (sc > score){ score = sc; best = it; }
}
step(rec,'Suggest pick', best ? {id:best.id, l:best.l, y:best.y, q:best.q, sc:score} : null);
return best ? best.id : null;
}
function extractYearFromTitle(title){
const ym = title.match(/\b(19|20)\d{2}\b/);
return ym ? ym[0] : '';
}
async function fetchImdbIdFromTorrentPage(href, rawTitle, rec){
step(rec,'Torrent fetch', href);
const url = new URL(href, location.origin).href;
const html = await gmFetch(url);
const m = html.match(/imdb\.com\/title\/(tt\d{7,9})/i);
const pageId = m ? m[1] : null;
step(rec,'Torrent parse', pageId || '(no imdb link)');
if (pageId) return pageId;
const year = extractYearFromTitle(rawTitle);
return await imdbSuggest(rawTitle, year, rec);
}
async function getRating(title, href){
const rec = trace(title);
if (!isMovieOrTV(title)) { step(rec,'Skipped (not Movie/TV)', title); end(rec,'fail'); return null; }
let imdbID = null;
try{ imdbID = await fetchImdbIdFromTorrentPage(href, title, rec); }
catch(e){ step(rec,'Torrent error', e.message); }
if (!imdbID){ end(rec,'fail'); return null; }
try{
const omdb = await omdbById(imdbID, rec);
const n = parseFloat(omdb?.imdbRating);
if (!isNaN(n)){ end(rec,'ok'); return `${n.toFixed(1)}/10`; }
step(rec,'OMDb no numeric rating', omdb?.imdbRating);
}catch(e){ step(rec,'OMDb error', e.message); }
try{
const v = await imdbScrapeRating(imdbID, rec);
if (v && !isNaN(v)){ end(rec,'ok'); return `${v.toFixed(1)}/10`; }
step(rec,'IMDb scrape empty', imdbID);
}catch(e){ step(rec,'IMDb scrape error', e.message); }
end(rec,'fail'); return null;
}
function render(el, rating){
if (el.querySelector('.imdb-dot')) return;
const n = parseFloat(rating);
let c='gray'; if(n>7)c='green'; else if(n>=6)c='goldenrod'; else if(n>0)c='red';
const span=document.createElement('span');
span.className='imdb-dot';
span.style='color:black;font-weight:bold;margin-left:5px;';
span.innerHTML=`<span style="display:inline-block;width:10px;height:10px;border-radius:50%;background:${c};margin-right:4px"></span>${rating}`;
el.appendChild(span);
el.title = `IMDb: ${rating}`;
}
async function process(el){
const t = el.textContent?.trim() || '';
const href = el.getAttribute('href');
const r = await getRating(t, href);
if (r) render(el, r);
}
function scan(root=document){
root.querySelectorAll('.table-list a[href^="/torrent/"]').forEach(process);
}
scan();
new MutationObserver(m=>{
m.forEach(mu=>mu.addedNodes.forEach(n=>{
if (n.nodeType===1) scan(n);
}));
}).observe(document.body,{childList:true,subtree:true});
})();