您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Show IMDb ratings next to Movie/TV torrents on 1337x. Robust ID detection with OMDb/IMDb fallbacks.
// ==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}); })();