`}};
/* ========================================================================
* SITE ADAPTER — DOM abstraction layer (configurable selectors)
* Replaces the original E object. All DOM reads go through selectors.
* ======================================================================== */
/* E is now created by createExtractors() which receives resolved selectors.
All DOM access is driven by the instance configuration. */
let E; // Initialized in main() with resolved selectors
function createExtractors(sel) {
return {
getTorrentName() {
const e = document.querySelector(sel.torrentName);
return e ? e.textContent.trim() : null;
},
getTmdbTitle() {
const e = document.querySelector(sel.tmdbTitle);
if (!e) return null;
const n = e.textContent.trim(),
t = n.match(/^(.+?)\s*\(\d{4}\)\s*$/);
return t ? t[1].trim() : n;
},
getTmdbYear() {
const e = document.querySelector(sel.tmdbTitle);
if (!e) return null;
const t = e.textContent.trim().match(/\((\d{4})\)\s*$/);
return t ? t[1] : null;
},
getCategory() {
const e = document.querySelector(sel.category);
return e ? e.textContent.trim() : null;
},
getType() {
const e = document.querySelector(sel.type);
return e ? e.textContent.trim() : null;
},
getResolution() {
const e = document.querySelector(sel.resolution);
return e ? e.textContent.trim() : null;
},
getDescription() {
const panels = document.querySelectorAll(sel.panels);
for (const p of panels) {
const h = p.querySelector(sel.panelHeading);
if (h && h.textContent.includes(sel.descriptionHeading || "Description")) {
const b = p.querySelector(sel.descriptionBody);
return b ? b.innerHTML : "";
}
}
return "";
},
getFileStructure() {
const hierForms = document.querySelectorAll(sel.fileHierarchy);
for (const t of hierForms) {
const s = t.querySelector("i.fas.fa-folder");
if (s) {
const o = s.parentElement;
if (o) {
const c = o.querySelector('span[style*="word-break"]'),
r = c ? c.textContent.trim() : null,
d = o.querySelector('span[style*="grid-area: count"]'),
f = d ? d.textContent.match(/\((\d+)\)/) : null,
A = f ? parseInt(f[1], 10) : 0,
T = [];
t.querySelectorAll("details i.fas.fa-file").forEach((u) => {
const x = u.parentElement?.querySelector('span[style*="word-break"]');
x && T.push(x.textContent.trim());
});
return { hasFolder: true, folderName: r, fileCount: A, files: T };
}
}
const a = t.querySelector(":scope > details > summary i.fas.fa-file");
if (a) {
const c = a.parentElement?.querySelector('span[style*="word-break"]');
return { hasFolder: false, folderName: null, fileCount: 1, files: c ? [c.textContent.trim()] : [] };
}
}
const listTbody = document.querySelector(sel.fileList);
if (listTbody) {
const rows = listTbody.querySelectorAll("tr"),
files = [];
rows.forEach((a) => {
const o = a.querySelector("td:nth-child(2)");
o && files.push(o.textContent.trim());
});
if (files.length > 0 && files[0].includes("/"))
return { hasFolder: true, folderName: files[0].split("/")[0], fileCount: files.length, files };
return { hasFolder: false, folderName: null, fileCount: files.length, files };
}
return null;
},
hasMediaInfo() {
const panels = document.querySelectorAll(sel.panels);
for (const p of panels) {
const h = p.querySelector(sel.panelHeading);
if (h && h.textContent.includes(sel.mediaInfoHeading || "MediaInfo")) return true;
}
return false;
},
hasBdInfo() {
const panels = document.querySelectorAll(sel.panels);
for (const p of panels) {
const h = p.querySelector(sel.panelHeading);
if (h && h.textContent.includes(sel.bdInfoHeading || "BDInfo")) return true;
}
return false;
},
isTV() {
const e = this.getCategory();
return e ? e.toLowerCase().includes("tv") || e.toLowerCase().includes("series") || e.toLowerCase().includes("episode") : false;
},
getOriginalLanguage() {
const e = document.querySelector(sel.originalLanguage);
return e ? e.textContent.trim().toLowerCase() : null;
},
getMediaInfoLanguages() {
const langs = new Set();
const miText = this.getMediaInfoText();
if (miText) {
const sections = miText.split(/\n(?=Audio(?: #\d+)?[\r\n])/);
for (const sec of sections) {
if (!/^Audio(?:\s|$)/m.test(sec)) continue;
const lines = sec.split(`\n`);
let lang = null, isCommentary = false;
for (const line of lines) {
if (/^(Video|Text|Menu|General|Chapter)/.test(line.trim())) break;
const lm = line.match(/^Language\s*:\s*(.+)$/);
if (lm) lang = lm[1].trim();
const tm = line.match(/^Title\s*:\s*(.+)$/);
if (tm && /commentary/i.test(tm[1])) isCommentary = true;
}
if (lang && !isCommentary) langs.add(lang);
}
}
if (langs.size === 0) {
document.querySelectorAll(sel.mediaInfoAudioFlags).forEach((img) => {
if (img.alt) langs.add(img.alt.trim());
});
}
return Array.from(langs);
},
getMediaInfoText() {
const e = document.querySelector(sel.mediaInfoDump);
return e ? e.textContent : "";
},
getMediaInfoFilename() {
const el = document.querySelector(sel.mediaInfoFilename);
if (el) return (el.querySelector('span[x-ref="filename"], span') || el).textContent.trim();
const mi = this.getMediaInfoText();
if (mi) {
const m = mi.match(/^Complete name\s*:\s*(.+)$/m);
if (m) {
const parts = m[1].trim().split(/[/\\]/);
return parts[parts.length - 1];
}
}
return null;
},
getMediaInfoSubtitles() {
const subs = new Set();
document.querySelectorAll(sel.mediaInfoSubFlags).forEach((img) => {
if (img.alt) subs.add(img.alt.trim());
});
if (subs.size === 0) {
const mi = this.getMediaInfoText();
if (mi) {
const sections = mi.split(/\n(?=Text(?: #\d+)?[\r\n])/);
for (const sec of sections) {
if (!/^Text(?:\s|$)/m.test(sec)) continue;
const lines = sec.split(`\n`);
for (const line of lines) {
if (/^(Video|Audio|Menu|General|Chapter)/.test(line.trim())) break;
const lm = line.match(/^Language\s*:\s*(.+)$/);
if (lm) { subs.add(lm[1].trim()); break; }
}
}
}
}
return Array.from(subs);
},
getAudioTracksFromMediaInfo() {
const tracks = [], mi = this.getMediaInfoText();
if (!mi) return tracks;
const sections = mi.split(/\n(?=Audio(?: #\d+)?[\r\n])/);
for (const sec of sections) {
if (!/^Audio(?:\s|$)/m.test(sec)) continue;
const track = { codec: null, channels: null, language: null, title: null, isDefault: false };
const lines = sec.split(`\n`);
for (const c of lines) {
if (/^(Video|Text|Menu|General|Chapter)/.test(c.trim())) break;
const r = c.match(/^Format\s*:\s*(.+)$/);
r && !track.codec && (track.codec = r[1].trim());
const d = c.match(/^Commercial name\s*:\s*(.+)$/);
d && (track.commercialName = d[1].trim());
const f = c.match(/^Channel\(s\)\s*:\s*(\d+)/);
if (f) {
const u = parseInt(f[1], 10);
track.channels = u === 1 ? "1.0" : u === 2 ? "2.0" : u === 6 ? "5.1" : u === 7 ? "6.1" : u === 8 ? "7.1" : `${u}ch`;
}
const A = c.match(/^Language\s*:\s*(.+)$/);
A && (track.language = A[1].trim());
const T = c.match(/^Title\s*:\s*(.+)$/);
T && (track.title = T[1].trim());
const b = c.match(/^Default\s*:\s*(.+)$/);
b && (track.isDefault = b[1].trim().toLowerCase() === "yes");
}
track.codec && tracks.push(track);
}
return tracks;
},
getHdrFromMediaInfo() {
const videoDts = document.querySelectorAll(sel.mediaInfoVideoDt);
for (const dt of videoDts) {
if (dt.textContent.trim() === "HDR") {
const dd = dt.nextElementSibling;
if (dd && dd.tagName === "DD") {
const val = dd.textContent.trim();
if (val && val !== "Unknown") return this.parseHdrFormats(val);
}
}
}
const mi = this.getMediaInfoText();
if (!mi) return [];
const m = mi.match(/HDR format\s*:\s*(.+?)(?:\n|$)/i);
return m ? this.parseHdrFormats(m[1]) : [];
},
parseHdrFormats(e) {
const n = [], t = e.toLowerCase();
(t.includes("dolby vision") || t.includes("dvhe")) &&
(t.includes("profile 5") || t.includes("dvhe.05")
? n.push("DV5")
: t.includes("profile 7") || t.includes("dvhe.07")
? n.push("DV7")
: t.includes("profile 8") || t.includes("dvhe.08")
? n.push("DV8")
: n.push("DV"));
t.includes("hdr10+") || t.includes("hdr10 plus") || t.includes("hdr10plus")
? n.push("HDR10+")
: (t.includes("hdr10") || t.includes("hdr")) && n.push("HDR");
t.includes("hlg") && n.push("HLG");
t.includes("pq10") && n.push("PQ10");
return n;
},
getModerationPanel() {
const panels = document.querySelectorAll(sel.panels);
for (const p of panels) {
const h = p.querySelector(sel.panelHeading);
if (h && h.textContent.includes(sel.moderationHeading || "Moderation")) return p;
}
return null;
},
};
}
/* ========================================================================
* CHECKS — Quality-gate rule engine
* Ported from the original k object. Pure functions operating on data.
* NOTE: Where the original called E methods directly, we now read from
* the siteData parameter passed through from the bootstrap.
* ======================================================================== */
const k={tmdbNameMatch(e,n){if(!n)return{status:"warn",message:"TMDB title not found on page",details:null};if(!e)return{status:"fail",message:"Torrent name not found",details:null};const t=H.normalizeForComparison(e),s=H.normalizeForComparison(n),a=H.normalizeForComparisonPreserveCase(e),o=H.normalizeForComparisonPreserveCase(n);if(t.startsWith(s))return a.startsWith(o)?{status:"pass",message:`"${n}" found at start of title`,details:null}:{status:"warn",message:`"${n}" found but capitalization differs`,details:{expected:n,found:e.substring(0,n.length)}};if(s.startsWith("the ")&&t.startsWith(s.substring(4)))return{status:"warn",message:`"${n}" found (without "The" prefix)`,details:null};const c=t.match(/^(.+?)\s+aka\s+/i);if(c){const r=c[1].trim();if(r===s||r==="the "+s||s.startsWith("the ")&&r===s.substring(4)){const d=a.match(/^(.+?)\s+AKA\s+/i),f=d?d[1].trim():"";return f!==o&&f!=="The "+o&&!(o.startsWith("The ")&&f===o.substring(4))?{status:"warn",message:`"${n}" found (AKA format) but capitalization differs`,details:{expected:n,found:f}}:{status:"pass",message:`"${n}" found (AKA format)`,details:null}}}return{status:"fail",message:`Title should start with "${n}"`,details:{expected:n,found:e.substring(0,Math.min(50,e.length))+(e.length>50?"...":"")}}},movieFolderStructure(e,n,t,s){return g.fullDiscTypes.some(c=>s?.includes(c))?{status:"na",message:"N/A - Full Disc (folder structure expected)",details:null}:t?{status:"na",message:"N/A - Folder structure check not applicable for TV",details:null}:n?.toLowerCase().includes("movie")?e?e.hasFolder?e.fileCount===1?{status:"fail",message:"Movie should not have a top-level folder",details:{found:`${e.folderName}/${e.files[0]||"..."}`,expected:e.files[0]||"Single file without folder wrapper"}}:{status:"warn",message:`Movie has folder with ${e.fileCount} files`,details:{folder:e.folderName,fileCount:e.fileCount}}:{status:"pass",message:"File structure correct (no folder wrapper)",details:null}:{status:"warn",message:"Could not determine file structure",details:null}:{status:"na",message:"N/A - Not a movie",details:null}},seasonEpisodeFormat(e,n){if(!n)return{status:"na",message:"N/A - Not TV content",details:null};H.parseSeasonEpisode(e);const t=e.match(/S(\d{2,})E(\d{2,})/i),s=e.match(/\bS(\d{2,})\b(?!E)/i);if(t)return{status:"pass",message:`Episode format correct: S${t[1]}E${t[2]}`,details:null};if(s)return{status:"pass",message:`Season pack format correct: S${s[1]}`,details:null};const a=e.match(/S(\d)E(\d)(?!\d)/i),o=e.match(/\bS(\d)\b(?!E|\d)/i);return a?{status:"fail",message:`Season/Episode must be zero-padded: found S${a[1]}E${a[2]}, expected S0${a[1]}E0${a[2]}`,details:null}:o?{status:"fail",message:`Season must be zero-padded: found S${o[1]}, expected S0${o[1]}`,details:null}:{status:"fail",message:"No S##E## or S## format found in TV content title",details:null}},namingGuideCompliance(e,n,t,s){const a={status:"pass",checks:[]},o=e||"",c=E.isTV(),r=H.extractYear(o);let d="fail",f="No year found";r?o.includes(`(${r})`)?(d="warn",f=`Found: (${r}) - Remove parentheses`):(d="pass",f=`Found: ${r}`):c?(d="pass",f="No year found (Optional for TV)"):(d="fail",f="No year found (Required for Movies)"),a.checks.push({name:"Year",status:d,message:f,required:!c});const A=g.validResolutions.find(R=>o.includes(R)),T=/\b(NTSC|PAL)\b/i.test(o),b=g.fullDiscTypes.some(R=>n?.includes(R)),u=A||T,D=A||(T?o.match(/\b(NTSC|PAL)\b/i)[1]:null),x=s||"";let p=u?"pass":"fail",y=u?`Found: ${D}`:"No valid resolution found";!u&&x==="Other"&&(p="warn",y="Non-standard resolution (tagged as Other)"),a.checks.push({name:"Resolution",status:p,message:y,required:!0});const C=[...g.validAudioCodecs].sort((R,P)=>P.length-R.length);let h=null;for(const R of C){const P=R.replace(/[+]/g,"\\+").replace(/[-.]/g,"[-.]?");if(new RegExp("(?n?.includes(R)),S=m?null:H.detectAudioObject(t),N=/Atmos/i.test(o),$=/Auro/i.test(o);let M="pass",B="No object audio detected";S==="Atmos"?N?(M="pass",B="Atmos detected & in title"):(M="warn",B="Atmos detected in MediaInfo but missing from Title"):S==="Auro3D"?$?(M="pass",B="Auro3D detected & in title"):(M="warn",B="Auro3D detected in MediaInfo but missing from Title"):(N||$)&&(m?(M="pass",B=`${N?"Atmos":"Auro3D"} in title (Full Disc - MediaInfo not validated)`):(M="warn",B="Object tag in title but not confirmed in MediaInfo")),(S||N||$)&&a.checks.push({name:"Audio Object",status:M,message:B,required:!!S});const I=this.checkSourceForType(o,n);a.checks.push(I);const J=[...g.validVideoCodecs].sort((R,P)=>P.length-R.length);let V=null;for(const R of J)if(new RegExp(R.replace(/[.]/g,"\\.?"),"i").test(o)){V=R;break}const K=g.fullDiscTypes.some(R=>n?.includes(R))||g.remuxTypes.some(R=>n?.toUpperCase().includes(R.toUpperCase()));if(a.checks.push({name:"Video Codec",status:V?"pass":K?"na":"warn",message:V?`Found: ${V}`:K?"N/A for Full Disc/REMUX":"No video codec found (may be implied)",required:!0}),o.includes("2160p")||o.includes("4320p")){const R=E.getHdrFromMediaInfo(),P=[...g.hdrFormats].sort((L,O)=>O.length-L.length);let v=null;for(const L of P)if(new RegExp("(?:^|\\s)"+L.replace(/[+]/g,"\\+")+"(?:\\s|$)","i").test(o)){v=L.toUpperCase();break}let _="pass",F="";const q=/\bHDR10\b/i.test(o)&&!/\bHDR10\+/i.test(o);if(m)q&&(!v||v==="HDR10")?(_="fail",F='"HDR10" in title should be renamed to "HDR"'):v?(_="pass",F=`HDR in title: ${v} (Full Disc - MediaInfo not validated)`):F="SDR (no HDR in title)";else if(R.length===0)q&&(!v||v==="HDR10")?(_="fail",F='"HDR10" in title should be renamed to "HDR"'):v?(_="warn",F=`Title has ${v} but MediaInfo shows no HDR`):F="SDR (no HDR in title or MediaInfo)";else{const L=R.join(", "),O=R.some(ae=>ae.startsWith("DV")),j=R.includes("HDR10+"),Y=R.includes("HDR10"),X=R.includes("HDR"),te=R.includes("HLG"),se=R.includes("PQ10");let w=null;O&&j?w="DV HDR10+":O&&(Y||X)?w="DV HDR":O?w="DV":j?w="HDR10+":Y||X?w="HDR":te?w="HLG":se&&(w="PQ10"),v&&w&&v===w.toUpperCase()?(_="pass",F=`Correct: ${w} (MediaInfo: ${L})`):!v&&!w?F="SDR (no HDR in title or MediaInfo)":v?w?(_="fail",F=`Wrong HDR tag - MediaInfo shows ${L}, title has ${v} but should be: ${w}`):(_="warn",F=`Title has ${v} but could not determine expected tag from MediaInfo (${L})`):(_="fail",F=`Missing HDR tag - MediaInfo shows ${L}, title should include: ${w}`)}a.checks.push({name:"HDR Format",status:_,message:F,required:R.length>0})}const Q=a.checks.some(R=>R.required&&R.status==="fail"),ee=a.checks.some(R=>R.status==="warn")||a.checks.some(R=>!R.required&&R.status==="fail");return a.status=Q?"fail":ee?"warn":"pass",a},checkSourceForType(e,n){e.toUpperCase();const t=n?.toUpperCase()||"";let s=[],a="Unknown";g.fullDiscTypes.some(c=>t.includes(c.toUpperCase()))?(s=g.sources.fullDisc,a="Full Disc"):g.remuxTypes.some(c=>t.includes(c.toUpperCase()))?(s=g.sources.remux,a="REMUX"):g.encodeTypes.some(c=>t.includes(c.toUpperCase()))?(s=g.sources.encode,a="Encode"):g.webTypes.some(c=>t.includes(c.toUpperCase()))?(s=[...g.sources.web,...g.streamingServices],a="WEB"):g.hdtvTypes.some(c=>t.includes(c.toUpperCase()))?(s=g.sources.hdtv,a="HDTV"):(s=[...g.sources.fullDisc,...g.sources.remux,...g.sources.encode,...g.sources.web,...g.sources.hdtv,...g.streamingServices],s=[...new Set(s)]);let o=null;for(const c of s)if(new RegExp(c.replace(/[-.]/g,"[-. ]?"),"i").test(e)){o=c;break}return!o&&a==="Encode"&&/blu-?ray/i.test(e)&&(o="BluRay"),{name:"Source",status:o?"pass":"fail",message:o?`Found: ${o}${a!=="Unknown"?` (valid for ${a})`:""}`:`No valid source for ${a} type`,required:!0}},titleElementOrder(e,n){const{elements:t,positions:s}=H.extractTitleElements(e,n);if(t.length<3)return{status:"warn",message:"Too few elements detected to validate order",details:null,violations:[]};const a=g.fullDiscTypes.some(T=>n?.includes(T))||g.remuxTypes.some(T=>n?.toUpperCase().includes(T.toUpperCase())),o=a?g.titleElementOrder.fullDiscRemux:g.titleElementOrder.encodeWeb,c=a?"Full Disc/REMUX":"Encode/WEB",r=[],d=t.map(T=>T.type);for(let T=0;Tp&&r.push({first:{type:u,value:t[T].value},second:{type:D,value:t[b].value},message:`"${t[T].value}" (${u}) should come after "${t[b].value}" (${D})`})}if(r.length===0)return{status:"pass",message:`Element order correct for ${c}`,details:null,violations:[]};const f=r.find(T=>T.first.type==="hdr"&&T.second.type==="vcodec"||T.first.type==="vcodec"&&T.second.type==="hdr");let A=`${r.length} ordering issue(s) found`;return f&&(a?A="HDR should come BEFORE video codec for Full Disc/REMUX":A="HDR should come AFTER video codec for Encode/WEB"),{status:"fail",message:A,details:{orderType:c,violations:r.map(T=>T.message)},violations:r}},audioTagCompliance(e,n,t,s,a){const o=g.fullDiscTypes.some(b=>s?.includes(b)),c=o&&/\b(NTSC|PAL|DVD5|DVD9)\b/i.test(e||"");if(o&&!c)return{status:"na",message:"N/A - Full Disc (no MediaInfo)",details:null,checks:[]};const r=[];if(t&&t.length>0){const b=(e||"").toLowerCase(),u=b.includes("dual-audio")||b.includes("dual audio"),D=b.includes("multi"),x=t.length,p=g.languageMap[n]||n,y=n==="en",C=i=>i.toLowerCase().startsWith("english"),h=i=>{if(!p)return!1;const l=i.toLowerCase(),m=p.toLowerCase();return l===m||l.startsWith(m)||l.includes(m)||m.includes(l)?!0:(g.languageAliases[m]||[]).some(N=>l.includes(N)||N.includes(l))};if(u)if(y){const i=t.filter(m=>!C(m));let l="";x>=2&&i.length>0?i.length>1?l=`. Found ${x} audio tracks (${t.join(", ")}). Use "Multi" instead`:/^[a-z]{2,3}$/i.test(i[0])?l=`. Found ${x} audio tracks (${t.join(", ")}), use "{Language_Name} Multi" instead`:l=`. Found ${x} audio tracks (${t.join(", ")}). Use "${i[0]} Multi" instead`:x>=2?l=`. Found ${x} audio tracks but non-English languages not recognized by MediaInfo parser. Use "{Language} Multi" instead (or "Multi" if 3+ languages)`:l='. Only 1 recognized language found — non-English track may not be recognized by MediaInfo parser. Use "{Language} Multi" if a second language is present (or "Multi" if 3+ languages)',r.push({name:"Language Tags",status:"fail",message:`Dual-Audio is reserved for non-English original content with an English dub${l}`})}else if(x>2)r.push({name:"Language Tags",status:"fail",message:`Tagged Dual-Audio but found ${x} languages. Should be "Multi"`});else if(x<2)r.push({name:"Language Tags",status:"fail",message:`Tagged Dual-Audio but found only ${x} language`});else{const i=t.some(C),l=t.some(h);i?l?r.push({name:"Language Tags",status:"pass",message:`Dual-Audio correct (English + ${p})`}):r.push({name:"Language Tags",status:"warn",message:`Dual-Audio implies Original Language (${p}) present`}):r.push({name:"Language Tags",status:"fail",message:"Dual-Audio requires English track"})}else if(D)x<2?r.push({name:"Language Tags",status:"fail",message:`"Multi" used but found only ${x} language`}):r.push({name:"Language Tags",status:"pass",message:`Multi-Audio correct (${x} languages)`});else if(x>2)o||r.push({name:"Language Tags",status:"warn",message:`Found ${x} languages but no "Multi" tag`});else if(x===2){const i=t.some(C),l=t.some(h);!o&&i&&l&&!y?r.push({name:"Language Tags",status:"warn",message:`Found English + Original (${p}), consider "Dual-Audio" tag`}):r.push({name:"Language Tags",status:"pass",message:`Audio languages OK (${x})`})}else r.push({name:"Language Tags",status:"pass",message:`Audio languages OK (${x})`})}const d=E.getAudioTracksFromMediaInfo();if(d.length>0){const b=g.remuxTypes.some(i=>s?.toUpperCase().includes(i.toUpperCase()))||/\b(HDTV|PDTV|SDTV)\b/i.test(s||"")||/\bDVD\b/i.test(s||""),u=/\b(HDTV|PDTV|SDTV|DVD)\b/i.test(s||"")||b,D=(i,l)=>{const m=(i||"").toLowerCase(),S=(l||"").toLowerCase();return m.includes("dts")&&(S.includes("dts-hd")||S.includes("dts:x")||S.includes("master audio")||S.includes("dts-hd ma"))?"DTS-HD":m.includes("dts")?"DTS":m==="e-ac-3"||m.includes("e-ac-3")?"E-AC-3":m==="ac-3"||m.includes("ac-3")?"AC-3":m.includes("mlp fba")||S.includes("truehd")?"TrueHD":m==="flac"||m.includes("flac")?"FLAC":m==="opus"||m.includes("opus")?"Opus":m==="pcm"||m.includes("pcm")||m.includes("lpcm")?"LPCM":m==="aac"||m.includes("aac")?"AAC":m==="mpeg audio"&&S.includes("mp2")?"MP2":m==="mpeg audio"&&S.includes("mp3")||m.includes("mp3")||m==="mpeg audio"&&!S?"MP3":m.includes("mp2")?"MP2":m.includes("vorbis")?"Vorbis":m.includes("alac")?"ALAC":i},x=i=>i?i==="1.0"||i==="2.0"||i==="1ch"||i==="2ch":!1,p=i=>{const l=(i.title||"").toLowerCase();return l.includes("commentary")||l.includes("comment")},y={"DTS-HD MA":"DTS-HD","DTS-HD HRA":"DTS-HD","DTS:X":"DTS-HD","DTS-ES":"DTS",DTS:"DTS",TrueHD:"TrueHD","DD+":"E-AC-3",DDP:"E-AC-3","DD EX":"AC-3",DD:"AC-3","E-AC-3":"E-AC-3","AC-3":"AC-3",LPCM:"LPCM",PCM:"LPCM",FLAC:"FLAC",ALAC:"ALAC",AAC:"AAC",MP3:"MP3",MP2:"MP2",Opus:"Opus",Vorbis:"Vorbis"},C=[...g.validAudioCodecs].sort((i,l)=>l.length-i.length);let h=null;for(const i of C){const l=i.replace(/[+]/g,"\\+").replace(/[-.]/g,"[-.]?");if(new RegExp("(?S.isDefault)||d[0],l=D(i.codec,i.commercialName),m=y[h];m&&l&&m!==l&&r.push({name:"Audio Codec Mismatch",status:"fail",message:`Title claims ${h} but primary audio track is ${l}`})}for(let i=0;ib.status==="fail"),A=r.some(b=>b.status==="warn"),T=f?"fail":A?"warn":"pass";return{status:T,message:T==="pass"?`Audio OK (${d.length} track${d.length!==1?"s":""})`:"Audio issues found",details:null,checks:r}},mediaInfoPresent(e,n,t,s){const a=g.fullDiscTypes.some(c=>t?.includes(c)),o=a&&/\b(NTSC|PAL|DVD5|DVD9)\b/i.test(s||"");return a&&!o?n?{status:"pass",message:"BDInfo present (Full Disc)",details:null}:e?{status:"warn",message:"BDInfo expected for Full Disc",details:null}:{status:"fail",message:"BDInfo required for Full Disc uploads",details:null}:o?{status:"na",message:"N/A - DVD Full Disc (BDInfo not applicable)",details:null}:n?{status:"fail",message:"Release is not Full Disc, BDInfo should be empty",details:null}:e?{status:"pass",message:"MediaInfo Present",details:null}:{status:"fail",message:"MediaInfo Required",details:null}},subtitleRequirement(e,n,t,s){if(g.fullDiscTypes.some(d=>s?.includes(d)))return{status:"na",message:"N/A - Full Disc (no MediaInfo)",details:null};if(!e||e.length===0)return{status:"na",message:"No audio languages detected",details:null};const o=d=>{const f=d.toLowerCase();return f==="english"||f.startsWith("english")};return e.some(o)?{status:"pass",message:"English audio present - subtitles optional",details:null}:!n||n.length===0?{status:"fail",message:"No English audio & no subtitles detected",details:{audio:e.join(", "),expected:"English subtitles required for non-English audio"}}:n.some(o)?{status:"pass",message:"Non-English audio with English subtitles",details:null}:{status:"fail",message:"Non-English audio requires English subtitles",details:{audio:e.join(", "),subtitles:n.join(", ")||"None detected",expected:"English subtitles"}}},screenshotCount(e){const{count:n,urls:t}=H.countScreenshots(e);return n>=g.minScreenshots?{status:"pass",count:n,message:`${n} screenshots found`,details:null}:n>0?{status:"warn",count:n,message:`Only ${n} screenshot(s) found (${g.minScreenshots}+ required)`,details:null}:{status:"fail",count:0,message:"No screenshots found in description",details:null}},containerFormat(e,n){if(g.fullDiscTypes.some(r=>n?.includes(r)))return{status:"na",message:"N/A - Full Disc uploads use native folder structure",details:null};if(!e||!e.files||e.files.length===0)return{status:"warn",message:"Could not determine file structure to verify container",details:null};const s=[".mkv",".mp4",".avi",".wmv",".m4v",".ts",".m2ts",".vob",".mpg",".mpeg",".mov",".flv",".webm"],a=e.files.filter(r=>{const d=r.toLowerCase();return s.some(f=>d.endsWith(f))});if(a.length===0)return{status:"warn",message:"No video files detected in file list",details:null};const o=a.filter(r=>!r.toLowerCase().endsWith(".mkv"));return o.length===0?{status:"pass",message:`MKV container verified (${a.length} video file${a.length>1?"s":""})`,details:null}:{status:"fail",message:`Non-MKV container detected: ${[...new Set(o.map(r=>r.split(".").pop().toUpperCase()))].join(", ")}`,details:{expected:"MKV container for all non-Full Disc releases",found:o.join(", ")}}},packUniformity(e,n){if(g.fullDiscTypes.some(b=>n?.includes(b)))return{status:"na",message:"N/A - Full Disc",details:null,checks:[]};if(!e||!e.files||e.files.length===0)return{status:"na",message:"N/A - No files detected",details:null,checks:[]};const s=[".mkv",".mp4",".avi",".wmv",".m4v",".ts",".m2ts",".vob",".mpg",".mpeg",".mov",".flv",".webm"],a=e.files.filter(b=>{const u=b.toLowerCase();return s.some(D=>u.endsWith(D))});if(a.length<2)return{status:"na",message:"N/A - Single file upload",details:null,checks:[]};const o=b=>{const u={},D=g.validResolutions.find(i=>b.includes(i));u.resolution=D||null;const x=[{pattern:/\bWEB-DL\b/i,name:"WEB-DL"},{pattern:/\bWEBRip\b/i,name:"WEBRip"},{pattern:/\bWEB\b/i,name:"WEB"},{pattern:/\bBlu-?Ray\b/i,name:"BluRay"},{pattern:/\bREMUX\b/i,name:"REMUX"},{pattern:/\bHDTV\b/i,name:"HDTV"},{pattern:/\bSDTV\b/i,name:"SDTV"},{pattern:/\bDVDRip\b/i,name:"DVDRip"},{pattern:/\bBDRip\b/i,name:"BDRip"},{pattern:/\bBRRip\b/i,name:"BRRip"},{pattern:/\bHDDVD\b/i,name:"HDDVD"},{pattern:/\bWEBDL\b/i,name:"WEB-DL"}];u.source=null;for(const i of x)if(i.pattern.test(b)){u.source=i.name;break}const p=[...g.validAudioCodecs].sort((i,l)=>l.length-i.length);u.audioCodec=null;for(const i of p){const l=i.replace(/[+]/g,"\\+").replace(/[-.]/g,"[-.]?");if(new RegExp("(?l.length-i.length);u.videoCodec=null;for(const i of C)if(new RegExp(i.replace(/[.]/g,"\\.?"),"i").test(b)){u.videoCodec=i;break}const h=b.match(/-([A-Za-z0-9$!_&+\$™]+)(?:\.[a-z0-9]+)?$/i);if(h)u.group=h[1];else{const i=b.match(/-\s+([A-Za-z0-9$!_&+\$™]+)(?:\.[a-z0-9]+)?$/i);if(i)u.group=i[1];else{const l=b.match(/-\s*([A-Za-z0-9$!_&+\$™]+)\s*\)\s*\[([A-Za-z0-9$!_&+\$™]+)\](?:\.[a-z0-9]+)?$/i);if(l)u.group=`${l[1]} [${l[2]}]`;else{const m=b.match(/\(\s*[^()]*-\s*([A-Za-z0-9$!._&+\$™]+)\s*\)(?:\.[a-z0-9]+)?$/i);u.group=m?m[1]:null}}}return u},c=a.map(b=>({file:b,attrs:o(b)})),r=[{key:"resolution",label:"Resolution"},{key:"source",label:"Source/Format"},{key:"audioCodec",label:"Audio Codec"},{key:"videoCodec",label:"Video Codec"},{key:"group",label:"Release Group"}],d=[];let f=!1;for(const{key:b,label:u}of r){const D=c.map(p=>p.attrs[b]).filter(p=>p!==null),x=[...new Set(D.map(p=>p.toUpperCase()))];if(D.length===0)d.push({name:u,status:"warn",message:`Could not detect ${u.toLowerCase()} in filenames`});else if(x.length===1)d.push({name:u,status:"pass",message:`Uniform: ${D[0]}`});else{f=!0;const p={};D.forEach(C=>{const h=C.toUpperCase();p[h]=(p[h]||0)+1});const y=Object.entries(p).map(([C,h])=>`${C} (${h})`).join(", ");d.push({name:u,status:"fail",message:`Mixed: ${y}`})}}const A=d.some(b=>b.status==="warn");return{status:f?"fail":A?"warn":"pass",message:f?`Mixed pack detected across ${a.length} files`:`Uniform across ${a.length} files`,details:null,checks:d}},encodeCompliance(e,n,t){if(!g.encodeTypes.some(i=>n?.toUpperCase().includes(i.toUpperCase())))return{status:"na",message:"N/A - Not an Encode",details:null,checks:[]};const a=[],o=e||"",c=/\bx264\b/i.test(o),r=/\bx265\b/i.test(o),d=/\bSVT[-.]?AV1\b/i.test(o),f=d||/\bAV1\b/i.test(o),A=c||r||d||f,T=["AVC","HEVC","H.264","H.265","MPEG-2","VC-1","VP9","XviD","DivX"];let b=null;if(!b){for(const i of T)if(new RegExp("\\b"+i.replace(/[.]/g,"\\.?")+"\\b","i").test(o)){b=i;break}}if(A){const i=c?"x264":r?"x265":d?"SVT-AV1":"AV1";a.push({name:"Encoder",status:"pass",message:`Found: ${i}`})}else b?a.push({name:"Encoder",status:"fail",message:`Found ${b} — encodes must use x264, x265, or SVT-AV1`}):a.push({name:"Encoder",status:"fail",message:"No x264, x265, or SVT-AV1 detected in title"});if(t)if(f)a.push({name:"Encoder Metadata",status:"pass",message:"AV1 detected"});else{const i=t.match(/Writing library\s*:\s*(.+?)(?:\n|$)/im),l=i&&/x264/i.test(i[1]),m=i&&/x265/i.test(i[1]),S=t.match(/Encoding settings\s*:\s*(.+?)(?:\n|$)/im),N=t.match(/Encoder_settings\s*:\s*(.+?)(?:\n|$)/im);if(l||m||S||N){let M="";l?M="x264":m?M="x265":M="encoding settings present",a.push({name:"Encoder Metadata",status:"pass",message:`Encoder metadata found (${M})`})}else a.push({name:"Encoder Metadata",status:"fail",message:"No encoder metadata found in MediaInfo — x264/x265 info required"})}else a.push({name:"Encoder Metadata",status:"warn",message:"No MediaInfo available — cannot verify encoder metadata"});const u=/\bH\.?264\b/i.test(o),D=/\bH\.?265\b/i.test(o);if(u||D){const i=t?t.match(/Writing library\s*:\s*(.+?)(?:\n|$)/im):null,l=i&&/x264/i.test(i[1]),m=i&&/x265/i.test(i[1]);u&&l?a.push({name:"Codec vs Encoder",status:"fail",message:"Title has H.264 but MediaInfo shows x264 — use encoder name (x264)"}):D&&m?a.push({name:"Codec vs Encoder",status:"fail",message:"Title has H.265 but MediaInfo shows x265 — use encoder name (x265)"}):u&&!l?a.push({name:"Codec vs Encoder",status:"warn",message:"Title has H.264 — encodes typically use encoder name (x264) instead"}):D&&!m&&a.push({name:"Codec vs Encoder",status:"warn",message:"Title has H.265 — encodes typically use encoder name (x265) instead"})}const x=["720p","1080i","1080p","2160p","4320p"],p=g.validResolutions.find(i=>o.includes(i));if(p?x.includes(p)?a.push({name:"Resolution",status:"pass",message:`Found: ${p}`}):a.push({name:"Resolution",status:"fail",message:`Found ${p} — encodes must be 720p or greater`}):a.push({name:"Resolution",status:"warn",message:"Could not detect resolution to verify encode requirement"}),f)a.push({name:"Rate Control",status:"pass",message:"AV1 detected — rate control cannot be verified from AV1 bitstream metadata"});else if(t){const i=t.match(/Encoding settings\s*:\s*(.+?)(?:\n|$)/im),l=t.match(/Encoder_settings\s*:\s*(.+?)(?:\n|$)/im),m=i?i[1]:l?l[1]:null;if(m){const S=m.match(/rc=(\w+)/),N=/--crf\b/.test(m),$=m.match(/--passes?\s+(\d+)/);if(S){const M=S[1].toLowerCase();if(M==="crf")a.push({name:"Rate Control",status:"pass",message:"CRF encoding detected"});else if(M==="abr"){const B=m.match(/stats-read=(\d+)/),I=m.match(/(?:^|[\s/])pass=?(\d+)/);B&&parseInt(B[1],10)>=2||I&&parseInt(I[1],10)>=2?a.push({name:"Rate Control",status:"pass",message:"Multi-pass ABR encoding detected"}):a.push({name:"Rate Control",status:"fail",message:"Single-pass ABR detected — must use CRF or multi-pass ABR"})}else M==="2pass"?a.push({name:"Rate Control",status:"pass",message:"2-pass encoding detected"}):M==="cbr"?a.push({name:"Rate Control",status:"fail",message:"CBR encoding detected — must use CRF or multi-pass ABR"}):a.push({name:"Rate Control",status:"warn",message:`Unrecognized rate control: rc=${M}`})}else N?a.push({name:"Rate Control",status:"pass",message:"CRF encoding detected (SVT-AV1)"}):$&&parseInt($[1],10)>=2?a.push({name:"Rate Control",status:"pass",message:`Multi-pass encoding detected (SVT-AV1, ${$[1]} passes)`}):/--tbr\b/.test(m)?a.push({name:"Rate Control",status:"fail",message:"Target bitrate (ABR) detected without multi-pass — must use CRF or multi-pass"}):a.push({name:"Rate Control",status:"warn",message:"Encoding settings found but could not determine rate control method"})}else a.push({name:"Rate Control",status:"warn",message:"No encoding settings in MediaInfo — cannot verify rate control"})}else a.push({name:"Rate Control",status:"warn",message:"No MediaInfo available — cannot verify rate control"});const y=a.some(i=>i.status==="fail"),C=a.some(i=>i.status==="warn"),h=y?"fail":C?"warn":"pass";return{status:h,message:h==="pass"?"Encode requirements met":y?"Encode compliance issues found":"Encode checks need review",details:null,checks:a}},upscaleDetection(e){if(!e)return{status:"na",message:"No torrent name to check",alert:!1};const t=[{name:"AI Upscales",regex:new RegExp("(?<=\\b[12]\\d{3}\\b)(?=.*\\b(HEVC)\\b)(?=.*\\b(AI)\\b)","i")},{name:"AIUS",regex:/\b(AIUS)\b/i},{name:"Regrade",regex:/\b((Upscale)?Re-?graded?)\b/i},{name:"RW",regex:/\b(RW)\b/},{name:"TheUpscaler",regex:/\b(The[ ._-]?Upscaler)\b/i},{name:"Upscaled",regex:new RegExp("(?<=\\b[12]\\d{3}\\b).*\\b(AI[ ._-]?Enhanced?|UPS(UHD)?|Upscaled?([ ._-]?UHD)?|UpRez)\\b","i")},{name:"Upscale",regex:/\b(UPSCALE)\b/i}].filter(s=>s.regex.test(e));return t.length>0?{status:"fail",message:`UPSCALE DETECTED: ${t.map(a=>a.name).join(", ")}`,alert:!0}:{status:"pass",message:"No upscale indicators found",alert:!1}},bannedReleaseGroup(e,n,t){const s=g.fullDiscTypes.some(r=>t?.includes(r)),a=H.extractReleaseGroup(e);if(!a)return{status:s?"na":"warn",group:null,message:s?"N/A for Full Disc":"Could not extract release group from title",alert:!1,tieredInfo:null};const o=H.findTieredGroup(a,n);return g.bannedGroups.some(r=>r.toLowerCase()===a.toLowerCase())?{status:"fail",group:a,message:`BANNED GROUP: ${a}`,alert:!0,tieredInfo:o}:{status:"pass",group:a,message:`Release Group: ${a}`,alert:!1,tieredInfo:o}}};
;k.resolutionTypeMatch=function(e,n){const t=e||"",s=n||"",a={NTSC:["480i","480p"],PAL:["576i","576p"]},o=g.validResolutions.find(f=>t.includes(f)),c=t.match(/\b(NTSC|PAL)\b/i),r=!!c,d=g.validResolutions.includes(s);if(!o&&r){const f=c[1].toUpperCase(),A=a[f]||[];return A.includes(s)?{status:"pass",message:`${f} source correctly tagged as ${s}`}:s==="Other"?{status:"pass",message:`${f} source tagged as Other`}:{status:"warn",message:`${f} source expected ${A.join(" or ")} (or Other), found: ${s}`}}return o?o===s?{status:"pass",message:`Resolution tag matches title: ${s}`}:{status:"fail",message:`Resolution tag mismatch — title contains "${o}" but tagged as "${s}"`,details:{expected:o,found:s}}:s==="Other"?{status:"pass",message:"Non-standard resolution correctly tagged as Other"}:!d&&s?{status:"warn",message:`Non-standard resolution "${s}" should use "Other" resolution type`}:{status:"warn",message:"Could not detect resolution in title to validate tag"}};
/* ========================================================================
* DARKPEERS CHECKS — Additional quality-gate checks for DarkPeers
* Feature-gated: only executed when instanceConfig.features.dpChecks
* ======================================================================== */
const dpk = {
/**
* nogroupCheck — flag NOGROUP in title and cross-reference filename groups.
*/
nogroupCheck(torrentName, mediaInfoFilename, fileStructure) {
const titleGroup = H.extractReleaseGroup(torrentName);
if (!titleGroup || titleGroup.toUpperCase() !== "NOGROUP") {
return { status: "pass", message: "Release group is not NOGROUP", details: null };
}
// Title says NOGROUP — check if filename also says NOGROUP
const filenameGroupMatch = (mediaInfoFilename || "").match(/-([A-Za-z0-9$!._&+\$]+?)(?:\.[a-z0-9]{2,4})?$/i);
const filenameGroup = filenameGroupMatch ? filenameGroupMatch[1] : null;
// Also check files in the file structure
const fileGroups = [];
if (fileStructure && fileStructure.files) {
for (const f of fileStructure.files) {
const m = f.match(/-([A-Za-z0-9$!._&+\$]+?)(?:\.[a-z0-9]{2,4})?$/i);
if (m) fileGroups.push(m[1]);
}
}
const allFilenameGroups = [filenameGroup, ...fileGroups].filter(Boolean);
const nogroupInFiles = allFilenameGroups.some(fg => fg.toUpperCase() === "NOGROUP");
const otherGroups = allFilenameGroups.filter(fg => fg.toUpperCase() !== "NOGROUP");
if (otherGroups.length > 0) {
// Title says NOGROUP but filename has a different group
const foundGroup = otherGroups[0];
const isBanned = g.bannedGroups.some(bg => bg.toLowerCase() === foundGroup.toLowerCase());
if (isBanned) {
return {
status: "fail",
message: `Banned group detected in filename: ${foundGroup} (title disguised as NOGROUP)`,
details: { titleGroup: "NOGROUP", filenameGroup: foundGroup },
};
}
return {
status: "warn",
message: `Title says NOGROUP but filename shows -${foundGroup}`,
details: { titleGroup: "NOGROUP", filenameGroup: foundGroup },
};
}
if (nogroupInFiles) {
return {
status: "warn",
message: "NOGROUP in title and filename — clarify if personal release or found as-is",
details: null,
};
}
return { status: "warn", message: "NOGROUP in title — could not verify filename group", details: null };
},
/**
* unknownLanguageCheck — flag audio/text tracks with unknown or missing language.
*/
unknownLanguageCheck(mediaInfoText) {
if (!mediaInfoText) {
return { status: "na", message: "No MediaInfo available", details: null, checks: [] };
}
const checks = [];
// Split MediaInfo into sections
const sections = mediaInfoText.split(/\n\n+/);
let audioIdx = 0;
let textIdx = 0;
for (const section of sections) {
const isAudio = /^Audio/m.test(section);
const isText = /^Text/m.test(section);
if (!isAudio && !isText) continue;
const trackType = isAudio ? "Audio" : "Subtitle";
const idx = isAudio ? ++audioIdx : ++textIdx;
const langMatch = section.match(/^Language\s*:\s*(.+)$/m);
const lang = langMatch ? langMatch[1].trim() : "";
const isUnknown = !lang || lang.toLowerCase() === "unknown" || lang.toLowerCase() === "und" || lang === "";
checks.push({
name: `${trackType} Track ${idx}`,
status: isUnknown ? "warn" : "pass",
message: isUnknown
? `${trackType} track ${idx}: language is ${lang || "missing"}`
: `${trackType} track ${idx}: ${lang}`,
});
}
if (checks.length === 0) {
return { status: "na", message: "No audio/text tracks found in MediaInfo", details: null, checks: [] };
}
const hasUnknown = checks.some(c => c.status === "warn");
return {
status: hasUnknown ? "warn" : "pass",
message: hasUnknown
? "Unknown or missing language tags detected — verify and correct"
: "All tracks have language tags",
details: null,
checks,
};
},
/**
* extraneousFiles — detect files that do not belong in the upload category.
*/
extraneousFiles(fileStructure, category, type) {
if (g.fullDiscTypes.some(t => type?.includes(t))) {
return { status: "na", message: "N/A - Full Disc", details: null };
}
if (!fileStructure || !fileStructure.files || fileStructure.files.length === 0) {
return { status: "warn", message: "Could not determine file structure", details: null };
}
const videoExts = [".mkv", ".mp4", ".ts", ".m2ts", ".vob", ".iso", ".srt", ".sub", ".idx", ".ass", ".ssa", ".sup"];
const musicExts = [".flac", ".mp3", ".m4a", ".ogg", ".opus", ".wav", ".cue", ".log", ".nfo", ".jpg", ".jpeg", ".png"];
const bookExts = [".epub", ".pdf", ".mobi", ".azw3", ".cbr", ".cbz", ".djvu", ".nfo", ".jpg", ".jpeg", ".png"];
const cat = (category || "").toLowerCase();
let whitelist;
let catLabel;
if (/music|audiobook/i.test(cat)) {
whitelist = musicExts;
catLabel = "Music/Audiobook";
} else if (/ebook|book/i.test(cat)) {
whitelist = bookExts;
catLabel = "eBook";
} else {
whitelist = videoExts;
catLabel = "Video";
}
const extraneous = fileStructure.files.filter(f => {
const ext = f.substring(f.lastIndexOf(".")).toLowerCase();
return !whitelist.includes(ext);
});
if (extraneous.length === 0) {
return { status: "pass", message: `All files valid for ${catLabel} category`, details: null };
}
return {
status: "fail",
message: `Extraneous files found for ${catLabel}: ${extraneous.join(", ")}`,
details: { extraneous, category: catLabel },
};
},
/**
* categoryTypeMismatch — detect mismatches between category, type, and title content.
*/
categoryTypeMismatch(category, type, torrentName) {
const cat = (category || "").toLowerCase();
const typ = (type || "").toUpperCase();
const name = torrentName || "";
const issues = [];
// Movie category + HDTV type is unusual
if (cat.includes("movie") && /HDTV|PDTV|SDTV/i.test(typ)) {
issues.push("Movie category with HDTV type — verify this is correct (TV recording of a movie?)");
}
// TV category should have S##E## or S## in title
if (cat.includes("tv") && !/S\d{1,2}(?:E\d{1,2})?/i.test(name) && !/\b(?:Season|Series)\b/i.test(name)) {
issues.push("TV category but no S##E## or season indicator in title");
}
// Title says WEB-DL but type says Encode
if (/WEB-DL/i.test(name) && /ENCODE/i.test(typ)) {
issues.push("Title contains WEB-DL but type is set to Encode");
}
// Title says REMUX but type is not REMUX
if (/\bREMUX\b/i.test(name) && !/REMUX/i.test(typ)) {
issues.push("Title contains REMUX but type is not set to REMUX");
}
// Title says Encode indicators but type is REMUX
if (/\b(x264|x265|SVT-AV1)\b/i.test(name) && /REMUX/i.test(typ)) {
issues.push("Title contains encode indicators (x264/x265/SVT-AV1) but type is REMUX");
}
if (issues.length === 0) {
return { status: "pass", message: "Category and type are consistent", details: null };
}
return {
status: "warn",
message: issues.join("; "),
details: { category: cat, type: typ, issues },
};
},
/**
* suspicionHeuristics — advisory-level signals that do not count toward verdict.
*/
suspicionHeuristics(torrentName, type, mediaInfoText, fileStructure, mediaInfoFilename) {
const checks = [];
const name = torrentName || "";
// Check .ts container in non-HDTV
if (fileStructure && fileStructure.files) {
const hasTsFiles = fileStructure.files.some(f => f.toLowerCase().endsWith(".ts"));
const isHdtv = /HDTV|PDTV|SDTV/i.test(type || "");
if (hasTsFiles && !isHdtv) {
checks.push({
name: "TS Container",
status: "advisory",
message: ".ts container detected in non-HDTV release — may indicate raw capture",
});
}
}
// Title group != filename group
const titleGroup = H.extractReleaseGroup(name);
const fnMatch = (mediaInfoFilename || "").match(/-([A-Za-z0-9$!._&+\$]+?)(?:\.[a-z0-9]{2,4})?$/i);
const fnGroup = fnMatch ? fnMatch[1] : null;
if (titleGroup && fnGroup && titleGroup.toLowerCase() !== fnGroup.toLowerCase()) {
checks.push({
name: "Group Mismatch",
status: "advisory",
message: `Title group (-${titleGroup}) differs from filename group (-${fnGroup})`,
});
}
// Unknown/new group with 4K REMUX
if (titleGroup && /2160p/i.test(name) && /REMUX/i.test(type || "")) {
const isKnown = H.findTieredGroup(titleGroup, false) || H.findTieredGroup(titleGroup, true);
if (!isKnown) {
checks.push({
name: "Unknown 4K REMUX Group",
status: "advisory",
message: `Unknown group "${titleGroup}" uploading 4K REMUX — extra scrutiny recommended`,
});
}
}
return {
status: "advisory",
message: checks.length > 0
? `${checks.length} advisory signal(s) detected`
: "No suspicious patterns detected",
details: null,
checks,
};
},
/**
* bannedGroupInFilename — catch uploaders hiding banned groups behind a different title group.
*/
bannedGroupInFilename(torrentName, mediaInfoFilename, fileStructure, isTV, type) {
const titleGroup = H.extractReleaseGroup(torrentName);
// Extract groups from filenames
const extractFnGroup = (filename) => {
const m = (filename || "").match(/-([A-Za-z0-9$!._&+\$]+?)(?:\.[a-z0-9]{2,4})?$/i);
return m ? m[1] : null;
};
const filenameGroups = new Set();
const miFnGroup = extractFnGroup(mediaInfoFilename);
if (miFnGroup) filenameGroups.add(miFnGroup);
if (fileStructure && fileStructure.files) {
for (const f of fileStructure.files) {
const fg = extractFnGroup(f);
if (fg) filenameGroups.add(fg);
}
}
if (filenameGroups.size === 0) {
return { status: "na", message: "No filename groups detected", details: null };
}
const bannedLower = g.bannedGroups.map(b => b.toLowerCase());
const bannedFound = [];
for (const fg of filenameGroups) {
const idx = bannedLower.indexOf(fg.toLowerCase());
if (idx !== -1) {
// Only flag if title group is different (i.e., they tried to hide it)
if (!titleGroup || titleGroup.toLowerCase() !== fg.toLowerCase()) {
bannedFound.push(g.bannedGroups[idx]);
}
}
}
if (bannedFound.length > 0) {
return {
status: "fail",
message: `Banned group hidden in filename: ${bannedFound.join(", ")} (title group: ${titleGroup || "none"})`,
alert: true,
details: { titleGroup, bannedInFilename: bannedFound },
};
}
return { status: "pass", message: "No banned groups detected in filenames", details: null };
},
/**
* singleFileInFolder — generalized folder structure check for all categories.
*/
singleFileInFolder(fileStructure, category, type) {
if (g.fullDiscTypes.some(t => type?.includes(t))) {
return { status: "na", message: "N/A - Full Disc (folder structure expected)", details: null };
}
if (!fileStructure) {
return { status: "warn", message: "Could not determine file structure", details: null };
}
if (fileStructure.hasFolder && fileStructure.fileCount === 1) {
return {
status: "fail",
message: "Single file in a folder — upload without the unnecessary folder",
details: {
folder: fileStructure.folderName,
file: fileStructure.files ? fileStructure.files[0] : "unknown",
},
};
}
return { status: "pass", message: "File structure OK", details: null };
},
/**
* missingEpisodes — detect gaps in season packs by parsing S##E## from filenames.
*/
missingEpisodes(fileStructure, torrentName, isTV) {
if (!isTV) {
return { status: "na", message: "N/A - Not TV content", details: null };
}
// Only for season packs: S## without E##
const seasonMatch = (torrentName || "").match(/\bS(\d{2,})\b(?!E)/i);
if (!seasonMatch) {
return { status: "na", message: "N/A - Not a season pack", details: null };
}
if (!fileStructure || !fileStructure.files || fileStructure.files.length === 0) {
return { status: "warn", message: "Season pack detected but no files to verify", details: null };
}
const season = parseInt(seasonMatch[1], 10);
const episodeNumbers = [];
for (const f of fileStructure.files) {
const m = f.match(/S(\d{2,})E(\d{2,})/i);
if (m && parseInt(m[1], 10) === season) {
episodeNumbers.push(parseInt(m[2], 10));
}
}
if (episodeNumbers.length === 0) {
return { status: "warn", message: "Season pack but no S##E## patterns found in filenames", details: null };
}
episodeNumbers.sort((a, b) => a - b);
const gaps = [];
for (let i = 0; i < episodeNumbers.length - 1; i++) {
const curr = episodeNumbers[i];
const next = episodeNumbers[i + 1];
if (next - curr > 1) {
for (let ep = curr + 1; ep < next; ep++) {
gaps.push(ep);
}
}
}
// Also check if pack starts at E01
if (episodeNumbers[0] > 1) {
for (let ep = 1; ep < episodeNumbers[0]; ep++) {
gaps.unshift(ep);
}
}
if (gaps.length === 0) {
return {
status: "pass",
message: `Season ${season} pack: ${episodeNumbers.length} episodes (E${String(episodeNumbers[0]).padStart(2, "0")}-E${String(episodeNumbers[episodeNumbers.length - 1]).padStart(2, "0")}), no gaps`,
details: null,
};
}
const gapStr = gaps.map(ep => `E${String(ep).padStart(2, "0")}`).join(", ");
return {
status: "warn",
message: `Season ${season} pack has gaps: missing ${gapStr} (Note: total episode count unknown without external data)`,
details: { season, found: episodeNumbers, missing: gaps },
};
},
};
/* ========================================================================
* TITLE VALIDATOR — Template-based title validation for DarkPeers
* Feature-gated: only executed when instanceConfig.features.dpTitleValidation
* ======================================================================== */
const TitleValidator = {
isMusic(cat) {
return /\bmusic\b/i.test(cat || "");
},
isAudioBook(cat) {
return /\baudiobook\b/i.test(cat || "");
},
isEbook(cat) {
return /\b(ebook|e-book|book)\b/i.test(cat || "");
},
/**
* validate — entry point; delegates to the right category validator.
*/
validate(title, category, type, mediaInfoText) {
if (!title) {
return { status: "fail", message: "No title to validate", details: null, checks: [] };
}
if (this.isMusic(category)) {
return this.music(title);
}
if (this.isAudioBook(category)) {
return this.audioBook(title, mediaInfoText);
}
if (this.isEbook(category)) {
return this.ebook(title);
}
// Video content
const isFullDiscOrRemux = g.fullDiscTypes.some(t => type?.includes(t))
|| g.remuxTypes.some(t => type?.toUpperCase().includes(t.toUpperCase()));
if (isFullDiscOrRemux) {
return this.videoFullDiscRemux(title, type);
}
return this.videoEncodeWeb(title, type);
},
/**
* videoFullDiscRemux — validate title against Full Disc/REMUX template.
* Template order: name year season cut ratio repack resolution edition region 3d source type hdr vcodec dub acodec channels object -group
*/
videoFullDiscRemux(title, type) {
const checks = [];
const { elements, positions } = H.extractTitleElements(title, type);
const order = g.titleElementOrder.fullDiscRemux;
const isTV = /S\d{2}/i.test(title);
// Check required elements — year optional for TV per DP rules
const required = ["resolution", "source", "group"];
for (const req of required) {
const found = elements.find(e => e.type === req);
if (found) {
checks.push({ name: req, status: "pass", message: `Found: ${found.value}` });
} else if (req === "group" && g.fullDiscTypes.some(t => type?.includes(t))) {
checks.push({ name: req, status: "na", message: "Group not required for Full Disc" });
} else {
checks.push({ name: req, status: "fail", message: `Missing required element: ${req}` });
}
}
// Year: required for movies, optional for TV
const yearEl = elements.find(e => e.type === "year");
if (yearEl) {
checks.push({ name: "year", status: "pass", message: `Found: ${yearEl.value}` });
} else if (isTV) {
checks.push({ name: "year", status: "pass", message: "Year optional for TV" });
} else {
checks.push({ name: "year", status: "fail", message: "Missing required element: year" });
}
// Check element order
const presentOrder = elements.map(e => e.type);
const orderViolations = [];
for (let i = 0; i < presentOrder.length; i++) {
for (let j = i + 1; j < presentOrder.length; j++) {
const idxA = order.indexOf(presentOrder[i]);
const idxB = order.indexOf(presentOrder[j]);
if (idxA !== -1 && idxB !== -1 && idxA > idxB) {
orderViolations.push(`"${elements[i].value}" (${presentOrder[i]}) should come after "${elements[j].value}" (${presentOrder[j]})`);
}
}
}
if (orderViolations.length > 0) {
checks.push({
name: "Element Order",
status: "fail",
message: `${orderViolations.length} ordering issue(s): ${orderViolations[0]}${orderViolations.length > 1 ? " (+" + (orderViolations.length - 1) + " more)" : ""}`,
});
} else if (elements.length >= 3) {
checks.push({ name: "Element Order", status: "pass", message: "Elements in correct order" });
}
// Check for DS4K if 2160p
if (/2160p/i.test(title)) {
const hdrEl = elements.find(e => e.type === "hdr");
if (hdrEl) {
checks.push({ name: "HDR", status: "pass", message: `HDR tag present: ${hdrEl.value}` });
} else {
checks.push({ name: "HDR", status: "warn", message: "4K content — consider adding HDR tag if applicable" });
}
}
const hasFail = checks.some(c => c.status === "fail");
const hasWarn = checks.some(c => c.status === "warn");
return {
status: hasFail ? "fail" : hasWarn ? "warn" : "pass",
message: hasFail ? "Title validation issues found" : hasWarn ? "Title may need attention" : "Title structure valid",
details: null,
checks,
};
},
/**
* videoEncodeWeb — validate title against Encode/WEB template.
* Template order: name year season cut ratio repack resolution edition 3d source type dub acodec channels object hdr vcodec -group
*/
videoEncodeWeb(title, type) {
const checks = [];
const { elements, positions } = H.extractTitleElements(title, type);
const order = g.titleElementOrder.encodeWeb;
const isTV = /S\d{2}/i.test(title);
// Check required elements — year is optional for TV per DP rules
const required = ["resolution", "source", "vcodec", "group"];
for (const req of required) {
const found = elements.find(e => e.type === req);
if (found) {
checks.push({ name: req, status: "pass", message: `Found: ${found.value}` });
} else {
checks.push({ name: req, status: "fail", message: `Missing required element: ${req}` });
}
}
// Year: required for movies, optional for TV
const yearEl = elements.find(e => e.type === "year");
if (yearEl) {
checks.push({ name: "year", status: "pass", message: `Found: ${yearEl.value}` });
} else if (isTV) {
checks.push({ name: "year", status: "pass", message: "Year optional for TV" });
} else {
checks.push({ name: "year", status: "fail", message: "Missing required element: year" });
}
// Check element order
const presentOrder = elements.map(e => e.type);
const orderViolations = [];
for (let i = 0; i < presentOrder.length; i++) {
for (let j = i + 1; j < presentOrder.length; j++) {
const idxA = order.indexOf(presentOrder[i]);
const idxB = order.indexOf(presentOrder[j]);
if (idxA !== -1 && idxB !== -1 && idxA > idxB) {
orderViolations.push(`"${elements[i].value}" (${presentOrder[i]}) should come after "${elements[j].value}" (${presentOrder[j]})`);
}
}
}
if (orderViolations.length > 0) {
checks.push({
name: "Element Order",
status: "fail",
message: `${orderViolations.length} ordering issue(s): ${orderViolations[0]}${orderViolations.length > 1 ? " (+" + (orderViolations.length - 1) + " more)" : ""}`,
});
} else if (elements.length >= 3) {
checks.push({ name: "Element Order", status: "pass", message: "Elements in correct order" });
}
// DS4K check: 2160p encode from non-4K source
if (/2160p/i.test(title)) {
const sourceEl = elements.find(e => e.type === "source");
const sourceVal = sourceEl ? sourceEl.value : "";
// If source is not UHD, it might be an upscale
if (sourceVal && !/UHD/i.test(sourceVal) && !/2160p/i.test(sourceVal)) {
checks.push({
name: "DS4K",
status: "warn",
message: `2160p encode from ${sourceVal} source — verify this is not an upscale (DS4K)`,
});
}
}
const hasFail = checks.some(c => c.status === "fail");
const hasWarn = checks.some(c => c.status === "warn");
return {
status: hasFail ? "fail" : hasWarn ? "warn" : "pass",
message: hasFail ? "Title validation issues found" : hasWarn ? "Title may need attention" : "Title structure valid",
details: null,
checks,
};
},
/**
* music — validate Artist - Album (Year) - Format pattern.
*/
music(title) {
const checks = [];
// Expected: Artist - Album (Year) - Format
// or: Artist - Album (Year) [Format]
const pattern = /^(.+?)\s+-\s+(.+?)\s+\((\d{4})\)\s+(?:-\s+|[\[(])(.+?)[\])]?\s*$/;
const match = title.match(pattern);
if (!match) {
// Try a more relaxed pattern
const hasArtist = /^.+?\s+-\s+/.test(title);
const hasYear = /\(\d{4}\)/.test(title);
if (!hasArtist) checks.push({ name: "Artist", status: "fail", message: "Could not parse 'Artist - ' prefix" });
else checks.push({ name: "Artist", status: "pass", message: "Artist separator found" });
if (!hasYear) checks.push({ name: "Year", status: "fail", message: "No (Year) found — expected format: (2024)" });
else checks.push({ name: "Year", status: "pass", message: "Year found" });
const hasFormat = /\b(FLAC|MP3|AAC|ALAC|OGG|OPUS|WAV|WEB|CD|Vinyl|24bit|16bit|320|V0|V2)\b/i.test(title);
if (!hasFormat) checks.push({ name: "Format", status: "warn", message: "No audio format detected" });
else checks.push({ name: "Format", status: "pass", message: "Format indicator found" });
} else {
checks.push({ name: "Artist", status: "pass", message: `Artist: ${match[1]}` });
checks.push({ name: "Album", status: "pass", message: `Album: ${match[2]}` });
checks.push({ name: "Year", status: "pass", message: `Year: ${match[3]}` });
checks.push({ name: "Format", status: "pass", message: `Format: ${match[4]}` });
}
const hasFail = checks.some(c => c.status === "fail");
const hasWarn = checks.some(c => c.status === "warn");
return {
status: hasFail ? "fail" : hasWarn ? "warn" : "pass",
message: hasFail ? "Music title format issues" : hasWarn ? "Music title may need attention" : "Music title format valid",
details: null,
checks,
};
},
/**
* audioBook — validate Author - Name Year Format ISBN-Tag pattern.
*/
audioBook(title, mediaInfoText) {
const checks = [];
const hasAuthor = /^.+?\s+-\s+/.test(title);
checks.push({
name: "Author",
status: hasAuthor ? "pass" : "fail",
message: hasAuthor ? "Author separator found" : "Could not parse 'Author - ' prefix",
});
const yearMatch = title.match(/\b(19|20)\d{2}\b/);
checks.push({
name: "Year",
status: yearMatch ? "pass" : "warn",
message: yearMatch ? `Year: ${yearMatch[0]}` : "No year found in title",
});
const formatMatch = /\b(FLAC|MP3|AAC|M4B|M4A|OGG|OPUS|Audiobook)\b/i.test(title);
checks.push({
name: "Format",
status: formatMatch ? "pass" : "warn",
message: formatMatch ? "Audio format found" : "No audio format detected",
});
const isbnMatch = title.match(/\b(ISBN[-:]?\s*[\dX-]{10,17})\b/i);
checks.push({
name: "ISBN",
status: isbnMatch ? "pass" : "warn",
message: isbnMatch ? `ISBN found: ${isbnMatch[1]}` : "No ISBN — recommended if available",
});
// Note: narrator/publisher ideally in description, but we flag if mediaInfoText hints at it
if (mediaInfoText && /narrator/i.test(mediaInfoText)) {
checks.push({ name: "Narrator", status: "pass", message: "Narrator mentioned in info" });
}
const hasFail = checks.some(c => c.status === "fail");
const hasWarn = checks.some(c => c.status === "warn");
return {
status: hasFail ? "fail" : hasWarn ? "warn" : "pass",
message: hasFail ? "Audiobook title format issues" : hasWarn ? "Audiobook title may need attention" : "Audiobook title format valid",
details: null,
checks,
};
},
/**
* ebook — validate Author - Name Year Format ISBN pattern.
*/
ebook(title) {
const checks = [];
const hasAuthor = /^.+?\s+-\s+/.test(title);
checks.push({
name: "Author",
status: hasAuthor ? "pass" : "fail",
message: hasAuthor ? "Author separator found" : "Could not parse 'Author - ' prefix",
});
const yearMatch = title.match(/\b(19|20)\d{2}\b/);
checks.push({
name: "Year",
status: yearMatch ? "pass" : "warn",
message: yearMatch ? `Year: ${yearMatch[0]}` : "No year found in title",
});
const formatMatch = /\b(EPUB|PDF|MOBI|AZW3?|CBR|CBZ|DJVU)\b/i.test(title);
checks.push({
name: "Format",
status: formatMatch ? "pass" : "fail",
message: formatMatch ? "eBook format found" : "No eBook format detected (EPUB, PDF, MOBI, etc.)",
});
const isbnMatch = title.match(/\b(ISBN[-:]?\s*[\dX-]{10,17})\b/i);
checks.push({
name: "ISBN",
status: isbnMatch ? "pass" : "warn",
message: isbnMatch ? `ISBN found: ${isbnMatch[1]}` : "No ISBN — recommended if available",
});
const hasFail = checks.some(c => c.status === "fail");
const hasWarn = checks.some(c => c.status === "warn");
return {
status: hasFail ? "fail" : hasWarn ? "warn" : "pass",
message: hasFail ? "eBook title format issues" : hasWarn ? "eBook title may need attention" : "eBook title format valid",
details: null,
checks,
};
},
/**
* collection — check for Collection/Trilogy suffix.
*/
collection(title) {
const hasCollectionSuffix = /\b(Collection|Trilogy|Duology|Quadrilogy|Anthology|Complete\s+Series)\b/i.test(title);
if (hasCollectionSuffix) {
return { status: "pass", message: "Collection suffix detected", details: null };
}
return { status: "warn", message: "No collection suffix found — add if this is a multi-title set", details: null };
},
};
/* ========================================================================
* INTEGRATIONS — External API wrappers (SRRDB, Prowlarr)
* Feature-gated: only executed when instanceConfig.features.srrdb/prowlarr
* ======================================================================== */
const Integrations = {
/**
* _request — Promise wrapper around GM_xmlhttpRequest.
*/
_request(options) {
return new Promise((resolve, reject) => {
if (typeof GM_xmlhttpRequest !== "function") {
return reject(new Error("GM_xmlhttpRequest not available"));
}
const timeout = options.timeout || 10000;
GM_xmlhttpRequest({
method: options.method || "GET",
url: options.url,
headers: options.headers || {},
timeout,
onload(response) {
try {
if (response.status >= 200 && response.status < 300) {
const data = options.json !== false ? JSON.parse(response.responseText) : response.responseText;
resolve({ status: response.status, data });
} else {
reject(new Error(`HTTP ${response.status}: ${response.statusText}`));
}
} catch (e) {
reject(new Error(`Parse error: ${e.message}`));
}
},
onerror(err) {
reject(new Error(`Network error: ${err.error || "unknown"}`));
},
ontimeout() {
reject(new Error(`Timeout after ${timeout}ms`));
},
});
});
},
srrdb: {
/**
* search — look up a release name on SRRDB.
*/
async search(releaseName) {
if (!releaseName) return { found: false, release: null, error: "No release name provided" };
try {
const encoded = encodeURIComponent(releaseName.replace(/\s+/g, "."));
const result = await Integrations._request({
url: `https://www.srrdb.com/api/search/r:${encoded}`,
timeout: 8000,
});
const data = result.data;
if (data && data.results && data.results.length > 0) {
return {
found: true,
release: data.results[0],
resultCount: data.results.length,
error: null,
};
}
return { found: false, release: null, error: null };
} catch (e) {
return { found: false, release: null, error: e.message };
}
},
/**
* test — simple health check for SRRDB.
*/
async test() {
try {
await Integrations._request({
url: "https://www.srrdb.com/api/search/r:test",
timeout: 5000,
});
return { ok: true, error: null };
} catch (e) {
return { ok: false, error: e.message };
}
},
/**
* getFiles — fetch file list for a release from SRRDB.
*/
async getFiles(releaseName) {
if (!releaseName) return { files: [], error: "No release name" };
try {
const encoded = encodeURIComponent(releaseName);
const result = await Integrations._request({
url: `https://www.srrdb.com/api/files/${encoded}`,
timeout: 8000,
});
// SRRDB files API returns an array of file objects
const files = Array.isArray(result.data) ? result.data : [];
return { files, error: null };
} catch (e) {
return { files: [], error: e.message };
}
},
},
prowlarr: {
/**
* search — search Prowlarr for a torrent name.
*/
async search(config, torrentName) {
if (!config || !config.url || !config.apiKey) {
return { found: false, results: [], error: "Prowlarr not configured" };
}
try {
const url = `${config.url.replace(/\/+$/, "")}/api/v1/search?query=${encodeURIComponent(torrentName)}&type=search`;
const result = await Integrations._request({
url,
headers: { "X-Api-Key": config.apiKey },
timeout: 15000,
});
const data = result.data;
if (Array.isArray(data) && data.length > 0) {
return { found: true, results: data, error: null };
}
return { found: false, results: [], error: null };
} catch (e) {
return { found: false, results: [], error: e.message };
}
},
/**
* test — health check for Prowlarr instance.
*/
async test(config) {
if (!config || !config.url || !config.apiKey) {
return { ok: false, error: "Prowlarr not configured" };
}
try {
await Integrations._request({
url: `${config.url.replace(/\/+$/, "")}/api/v1/health`,
headers: { "X-Api-Key": config.apiKey },
timeout: 5000,
});
return { ok: true, error: null };
} catch (e) {
return { ok: false, error: e.message };
}
},
},
};
/* ========================================================================
* MESSAGE BUILDER — Corrective message generation
* Ported from the original G object. Rules URL is now configurable.
* ======================================================================== */
const G={RULES_URL:null,collectIssues(e){const n=[],t=(s,a,o,c={})=>{(a==="fail"||a==="warn")&&n.push({id:s,status:a,raw:o,...c})};if(t("tmdb",e.tmdbMatch.status,e.tmdbMatch.message,{expected:e.tmdbMatch.details?.expected,found:e.tmdbMatch.details?.found}),e.seasonEpisode.status!=="na"&&t("season_episode",e.seasonEpisode.status,e.seasonEpisode.message),e.bannedGroup.alert&&t("banned_group",e.bannedGroup.status,e.bannedGroup.message,{group:e.bannedGroup.group}),t("screenshots",e.screenshots.status,e.screenshots.message,{count:e.screenshots.count}),t("resolution_type",e.resolutionTypeMatch.status,e.resolutionTypeMatch.message,{expected:e.resolutionTypeMatch.details?.expected,found:e.resolutionTypeMatch.details?.found}),e.elementOrder.status==="fail"||e.elementOrder.status==="warn"){const s=e.elementOrder.violations||[];t("element_order",e.elementOrder.status,e.elementOrder.message,{violations:s.map(a=>typeof a=="object"?a.message:a),orderType:e.elementOrder.details?.orderType})}if(e.namingGuide.checks)for(const s of e.namingGuide.checks){const a=s.name.toLowerCase().replace(/[^a-z0-9]+/g,"_");t("naming_"+a,s.status,s.message,{label:s.name})}if(e.folderStructure.status!=="na"&&t("folder",e.folderStructure.status,e.folderStructure.message),e.containerFormat.status!=="na"&&t("container",e.containerFormat.status,e.containerFormat.message,{found:e.containerFormat.details?.found}),t("mediainfo",e.mediaInfo.status,e.mediaInfo.message),e.subtitleRequirement.status!=="na"&&t("subtitles",e.subtitleRequirement.status,e.subtitleRequirement.message,{audio:e.subtitleRequirement.details?.audio}),t("upscale",e.upscaleDetection.status,e.upscaleDetection.message,{alert:e.upscaleDetection.alert}),e.audioTags.checks)for(const s of e.audioTags.checks)t("audio_"+s.name.replace(/[^a-zA-Z0-9]+/g,"_"),s.status,s.message,{label:s.name});if(e.encodeCompliance.checks)for(const s of e.encodeCompliance.checks)t("encode_"+s.name.toLowerCase().replace(/[^a-z0-9]+/g,"_"),s.status,s.message,{label:s.name});if(e.packUniformity.checks)for(const s of e.packUniformity.checks)t("pack_"+s.name.toLowerCase().replace(/[^a-z0-9]+/g,"_"),s.status,s.message,{label:s.name});return n},beautify(e){const n=e.id,t=e.raw||"",s=[];if(n==="tmdb")return/not found on page/i.test(t)||/torrent name not found/i.test(t)?null:e.expected?(s.push("9"),/capitalization/i.test(t)?{text:`Title capitalization should match TMDB: "${e.expected}".`,rules:s}:/without.*the.*prefix/i.test(t)?{text:`Title is missing the "The" prefix — TMDB title is "${e.expected}".`,rules:s}:{text:`Title does not match TMDB. The correct title is "${e.expected}".`,rules:s}):(s.push("9"),{text:"Title does not match TMDB.",rules:s});if(n==="season_episode"){if(s.push("8.2"),/zero-padded/i.test(t)){const a=t.match(/expected\s+(S\d+E?\d*)/i);return{text:`Season and episode numbers must be zero-padded (e.g. ${a?a[1]:"S01E01"}).`,rules:s}}return/no S##E##/i.test(t)?{text:"TV content must include the season/episode in S##E## or S## format.",rules:s}:{text:t,rules:s}}if(n==="banned_group")return s.push("2.11"),{text:`${e.group||"This release group"} is a banned release group.`,rules:s};if(n==="screenshots")return s.push("10.2"),e.count===0?{text:"Required screenshots are missing from the description.",rules:s}:{text:`Only ${e.count} screenshot${e.count===1?"":"s"} included — a minimum of 3 is required.`,rules:s};if(n==="element_order")return s.push("9"),e.violations&&e.violations.length>0?{text:`Title elements are in the wrong order:
${e.violations.map(o=>" • "+o).join(`
`)}`,rules:s}:{text:"Title elements are not in the expected order. Please refer to the Naming Guide.",rules:s};if(n.startsWith("naming_")){if(s.push("9"),n==="naming_resolution"&&/non-standard resolution|tagged as other/i.test(t))return null;if(/remove parentheses/i.test(t))return{text:"Year should not be in parentheses.",rules:s};if(n==="naming_hdr_format"){if(/HDR10.*should be renamed to.*HDR/i.test(t))return{text:'"HDR10" in the title should be "HDR".',rules:s};if(/missing hdr tag/i.test(t)){const a=t.match(/should include:\s*(.+)/i);return{text:`Missing HDR format tag in title${a?" — should be "+a[1]:""}.`,rules:s}}if(/wrong hdr tag/i.test(t)){const a=t.match(/should be:\s*(.+)/i);return{text:`Incorrect HDR format tag in title${a?" — should be "+a[1]:""}.`,rules:s}}return/title has.*but mediainfo shows no hdr/i.test(t)?{text:"Title includes an HDR format tag but MediaInfo does not confirm.",rules:s}:{text:t,rules:s}}if(n==="naming_audio_object")return/missing from title/i.test(t)?{text:`${/atmos/i.test(t)?"Atmos":"Auro3D"} detected in MediaInfo but missing from the title.`,rules:s}:/not confirmed in mediainfo/i.test(t)?{text:"Object audio tag in the title is not confirmed by MediaInfo.",rules:s}:{text:t,rules:s};if(n==="naming_source"){if(/no valid source/i.test(t)){const a=t.match(/for\s+(.+?)\s+type/i);let o="";if(a){const c=a[1];o=` for ${/^[aeiouh]/i.test(c)?"an":"a"} ${c} upload`}return{text:`Missing or invalid source tag in the title${o}.`,rules:s}}return{text:t,rules:s}}if(/^No\s/i.test(t)){let a=(e.label||"").toLowerCase();return a==="channels"&&(a="audio channels"),{text:`Missing ${a} in title.`,rules:s,naming:!0,missingElement:a}}return{text:t,rules:s}}if(n==="folder")return s.push("1.6"),/should not have.*folder/i.test(t)?{text:"Single-file movies should not be inside a folder.",rules:s}:{text:t,rules:s};if(n==="container"){if(s.push("5.2.5"),/non-mkv/i.test(t)){const a=t.match(/detected:\s*(.+)/i);return{text:`All non-disc releases must use the MKV container${a?" (found "+a[1]+")":""}.`,rules:s}}return/bdinfo should be empty/i.test(t)?{text:"BDInfo should only be provided for Full Disc uploads.",rules:["1.8","1.9"]}:{text:t,rules:s}}if(n==="mediainfo")return/mediainfo required/i.test(t)?(s.push("1.8"),{text:"MediaInfo is required for all non-disc uploads.",rules:s}):/bdinfo required/i.test(t)?(s.push("1.9"),{text:"BDInfo is required for Full Disc uploads.",rules:s}):/bdinfo expected/i.test(t)?(s.push("1.9"),{text:"Full Disc uploads should provide BDInfo rather than MediaInfo.",rules:s}):/bdinfo should be empty/i.test(t)?(s.push("1.8"),{text:"BDInfo should only be provided for Full Disc uploads.",rules:s}):{text:t,rules:s};if(n==="subtitles")return s.push("5.2.1"),/no english audio.*no subtitles/i.test(t)?{text:"English subtitles are required when the audio is not in English.",rules:s}:/requires english subtitles/i.test(t)?{text:"English subtitles are required for non-English audio content.",rules:s}:{text:t,rules:s};if(n==="upscale")return s.push("2"),{text:"This release appears to be an upscale, which is not permitted.",rules:s};if(n.startsWith("audio_")){if(/dual-audio/i.test(t)&&/reserved for non-english/i.test(t)){s.push("9");const a=t.match(/use\s+"([^"]+)"/i);let o="Dual-Audio is reserved for non-English original content with an English dub.";return a&&(o+=` Use "${a[1]}" instead.`),{text:o,rules:s}}if(/tagged dual-audio but found \d+ languages.*should be/i.test(t))return s.push("9"),{text:'More than two audio languages detected — use "Multi" instead of "Dual-Audio".',rules:s};if(/tagged dual-audio but found only/i.test(t))return s.push("9"),{text:"Dual-Audio tag used but only one audio language detected.",rules:s};if(/dual-audio requires english/i.test(t))return s.push("9"),{text:"Dual-Audio releases must include an English audio track.",rules:s};if(/multi.*found only/i.test(t))return s.push("9"),{text:'"Multi" tag used but only one audio language detected.',rules:s};if(/found \d+ languages but no.*multi/i.test(t)){s.push("9");const a=t.match(/found (\d+)/);return{text:`${a?a[1]+" audio languages":"Multiple audio languages"} detected — consider adding a "Multi" tag.`,rules:s}}if(/consider.*dual-audio/i.test(t))return s.push("9"),{text:'English and original language audio detected — consider adding the "Dual-Audio" tag.',rules:s};if(/only allowed as mono\/stereo/i.test(t)){s.push("5.2.4");const a=t.match(/^(\w+)/);return{text:`${a?a[1]:"This codec"} is only allowed for mono or stereo audio, not multichannel.`,rules:s}}if(/mp2 only allowed/i.test(t))return s.push("5.2.4"),{text:"MP2 audio is only permitted for untouched HDTV or DVD sources.",rules:s};if(/mp3 only allowed/i.test(t))return s.push("5.2.4"),{text:"MP3 is only permitted for supplementary tracks such as commentary.",rules:s};if(/not an allowed audio codec/i.test(t)){s.push("5.2.4");const a=t.match(/^(\w+)/);return{text:`${a?a[1]:"This audio codec"} is not an allowed audio codec.`,rules:s}}if(/unrecognized codec/i.test(t))return s.push("5.2.4"),{text:"An audio track uses an unrecognized codec — please verify it is permitted.",rules:s};if(/title claims .* but primary audio track is/i.test(t)){s.push("5.2.4");const a=t.match(/title claims (\S+) but primary audio track is (\S+)/i);return{text:a?`Title claims ${a[1]} audio but the primary track in MediaInfo is ${a[2]} — correct the audio codec tag.`:"Audio codec in title does not match the primary track in MediaInfo.",rules:s}}return{text:t,rules:s}}if(n.startsWith("encode_")){if(/no mediainfo available/i.test(t)||/cannot verify/i.test(t)||/could not det/i.test(t))return null;if(/encodes must use x264/i.test(t)){s.push("5.5.3");const a=t.match(/found\s+(\S+)/i);return{text:`Encodes must use x264, x265, or SVT-AV1${a?" (found "+a[1]+")":""}.`,rules:s}}if(/no x264.*detected/i.test(t))return s.push("5.5.3"),{text:"No recognized encoder (x264, x265, or SVT-AV1) detected in the title.",rules:s};if(/no encoder metadata/i.test(t))return s.push("5.5.4"),{text:"Encoder metadata is required in MediaInfo for encodes.",rules:s};if(/use encoder name.*x264/i.test(t)||/use encoder name.*x265/i.test(t)){s.push("9");const a=/x265/i.test(t)?"x265":"x264";return{text:`Title uses ${/H\.265/i.test(t)?"H.265":"H.264"} but the encoder is ${a} — use the encoder name in the title.`,rules:s}}return/typically use encoder name/i.test(t)?(s.push("9"),{text:"Encodes should use the encoder name (x264/x265) rather than the codec name (H.264/H.265) in the title.",rules:s}):/encodes must be 720p/i.test(t)?(s.push("5.5.5"),{text:"Encodes must be 720p or greater in resolution.",rules:s}):/single-pass abr/i.test(t)?(s.push("5.5.6"),{text:"Single-pass ABR is not permitted — encodes must use CRF or multi-pass ABR.",rules:s}):/cbr.*detected/i.test(t)?(s.push("5.5.6"),{text:"CBR encoding is not permitted — encodes must use CRF or multi-pass ABR.",rules:s}):/target bitrate.*without multi-pass/i.test(t)?(s.push("5.5.6"),{text:"Target bitrate encoding without multi-pass is not permitted — use CRF or multi-pass ABR.",rules:s}):{text:t,rules:s}}return n.startsWith("pack_")?/could not detect/i.test(t)?null:(s.push("8.1"),/mixed/i.test(t)?{text:`Mixed ${(e.label||"").toLowerCase()} detected across files in this pack — all files must be uniform.`,rules:s}:{text:t,rules:s}):n==="resolution_type"?/could not detect/i.test(t)?null:(s.push("4"),/mismatch/i.test(t)?{text:`Resolution type tag does not match title — ${e.expected?`expected "${e.expected}" but found "${e.found}"`:t}.`,rules:s}:/should use.*Other/i.test(t)?{text:t,rules:s}:{text:t,rules:s}):{text:t,rules:[]}},buildMessage(e){if(e.length===0)return"";const n=e.filter(u=>u.status==="fail"),t=e.filter(u=>u.status==="warn"),s=new Set,a=[],o=n.map(u=>this.beautify(u)).filter(Boolean),c=t.map(u=>this.beautify(u)).filter(Boolean),r=[],d=[],f=[];for(const u of o)u.rules.forEach(D=>s.add(D)),u.missingElement?d.push(u.missingElement):u.text.startsWith("Title ")?r.push(u.text):f.push(u.text);for(const u of r)a.push(u);if(d.length>0)if(d.length===1)a.push(`Missing ${d[0]} in title.`);else{const u=d.pop();a.push(`Missing ${d.join(", ")} and ${u} in title.`)}for(const u of f)a.push(u);if(c.length>0){const u=[],D=[],x=[];for(const p of c)p.rules.forEach(y=>s.add(y)),p.missingElement?D.push(p.missingElement):p.text.startsWith("Title ")?u.push(p.text):x.push(p.text);for(const p of u)a.push(p);if(D.length>0)if(D.length===1)a.push(`Missing ${D[0]} in title.`);else{const p=D.pop();a.push(`Missing ${D.join(", ")} and ${p} in title.`)}for(const p of x)a.push(p)}const A=new Set;let b=a.filter(u=>A.has(u)?!1:(A.add(u),!0)).join(`
`);if(s.size>0){const D=[...s].sort((x,p)=>{const y=x.split(".").map(Number),C=p.split(".").map(Number);for(let h=0;h`§${x}`).join(", ");b+=`
Please review the following [url=${this.RULES_URL}]Upload Rules[/url]: ${D}.`}return b}};
/* ========================================================================
* UI — Panel rendering, injection, and event handling
* Ported from the original U object.
* ======================================================================== */
const U={getStatusIcon(e){switch(e){case"pass":return'';case"fail":return'';case"warn":return'';case"na":return'';case"advisory":return'';case"integration":return'';default:return''}},getStatusBadge(e){return`${{pass:"Pass",fail:"Fail",warn:"Warning",na:"N/A",advisory:"Review",integration:"..."}[e]||e}`},worstStatus(e){const n=e.filter(t=>t!=="na");return n.includes("fail")?"fail":n.includes("warn")?"warn":n.length?"pass":"na"},accordion(e,n,t,s,{forceOpen:a=null,alert:o=!1}={}){return`
${this.getStatusIcon(t)}${n}
${this.getStatusBadge(t)}