From 3c1aa679afc662a8a160707a6014aafa2e6cb09f Mon Sep 17 00:00:00 2001 From: Procuria Date: Sun, 5 Apr 2026 23:25:21 +0200 Subject: [PATCH] =?UTF-8?q?v0.3.0=20=E2=80=94=20Format=20rule=20engine=20f?= =?UTF-8?q?or=20readability?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGELOG.md | 6 + modq-helper-darkpeers.user.js | 1130 ++++++++++++++++++++++++++++++++- 2 files changed, 1133 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ec0fb05..8492968 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,12 @@ All notable changes to the DarkPeers Mod Queue Helper will be documented in this The format is based on [Keep a Changelog](https://keepachangelog.com/), and this project adheres to [Semantic Versioning](https://semver.org/). +## [0.3.0] - 2026-04-05 + +### Changed + +- **Rule engine readability** — un-minified the quality-gate rule engine (`k`) and its `resolutionTypeMatch` extension from dense single-line blocks into properly indented, multi-line JavaScript + ## [0.2.0] - 2026-04-05 ### Changed diff --git a/modq-helper-darkpeers.user.js b/modq-helper-darkpeers.user.js index 80f17fb..f60fac7 100644 --- a/modq-helper-darkpeers.user.js +++ b/modq-helper-darkpeers.user.js @@ -1,7 +1,7 @@ // ==UserScript== // @name UNIT3D Mod Queue Helper — DarkPeers // @namespace https://gitea.computerliebe.org/Procuria/dp-modq-helper -// @version 0.2.0 +// @version 0.3.0 // @description Quality-gate checks for DarkPeers — extended moderation rules, title validation, SRRDB & Prowlarr integrations // @author TQG Contributors // @updateURL https://gitea.computerliebe.org/Procuria/dp-modq-helper/raw/branch/main/modq-helper-darkpeers.user.js @@ -1718,8 +1718,1132 @@ function createExtractors(sel) { * 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"}}; +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; T < d.length; T++) + for (let b = T + 1; b < d.length; b++) { + const u = d[T], + D = d[b], + x = o.indexOf(u), + p = o.indexOf(D); + x === -1 || p === -1 || x > p && 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; i < d.length; i++) { + const l = d[i], + m = D(l.codec, l.commercialName), + S = `Track ${i+1}: ${m}${l.channels?" "+l.channels:""}${l.language?" ("+l.language+")":""}`; + if (m === "FLAC" || m === "Opus" || m === "LPCM") { + const N = m === "LPCM" && b; + !x(l.channels) && !N ? r.push({ + name: S, + status: "fail", + message: `${m} only allowed as mono/stereo. Found: ${l.channels||"unknown"}` + }) : r.push({ + name: S, + status: "pass", + message: x(l.channels) ? `${m} mono/stereo OK` : `${m} multichannel (untouched OK)` + }) + } else m === "MP2" ? u ? r.push({ + name: S, + status: "pass", + message: "MP2 OK (untouched source)" + }) : r.push({ + name: S, + status: "fail", + message: "MP2 only allowed if untouched (HDTV/DVD)" + }) : m === "MP3" ? p(l) ? r.push({ + name: S, + status: "pass", + message: "MP3 OK (commentary track)" + }) : r.push({ + name: S, + status: "warn", + message: "MP3 only allowed for supplementary tracks (e.g. commentary)" + }) : m === "Vorbis" || m === "ALAC" ? r.push({ + name: S, + status: "fail", + message: `${m} is not an allowed audio codec` + }) : ["DTS", "DTS-HD", "AC-3", "E-AC-3", "TrueHD", "AAC"].includes(m) ? r.push({ + name: S, + status: "pass", + message: `${m} OK` + }) : r.push({ + name: S, + status: "warn", + message: `Unrecognized codec: ${l.codec}${l.commercialName?" / "+l.commercialName:""}` + }) + } + } + if (r.length === 0) return { + status: "na", + message: "No audio data detected in MediaInfo", + details: null, + checks: [] + }; + const f = r.some(b => b.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 || ""; + const s = n || ""; + const a = { NTSC: ["480i", "480p"], PAL: ["576i", "576p"] }; + const o = g.validResolutions.find(f => t.includes(f)); + const c = t.match(/\b(NTSC|PAL)\b/i); + const r = !!c; + const d = g.validResolutions.includes(s); + + if (!o && r) { + const f = c[1].toUpperCase(); + const A = a[f] || []; + if (A.includes(s)) + return { status: "pass", message: `${f} source correctly tagged as ${s}` }; + if (s === "Other") + return { status: "pass", message: `${f} source tagged as Other` }; + return { status: "warn", message: `${f} source expected ${A.join(" or ")} (or Other), found: ${s}` }; + } + + if (o) { + return 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 } }; + } + + if (s === "Other") + return { status: "pass", message: "Non-standard resolution correctly tagged as Other" }; + if (!d && s) + return { status: "warn", message: `Non-standard resolution "${s}" should use "Other" resolution type` }; + return { status: "warn", message: "Could not detect resolution in title to validate tag" }; +}; /* ========================================================================