Compare commits

..
7 Commits
3 changed files with 110 additions and 549 deletions

No files matched your search

-112
View File
@@ -4,118 +4,6 @@ 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.6.8] - 2026-09-25
### Fixed
- DarkPeers checks now show in the panel, in a new DarkPeers Rules section: title template, NOGROUP, banned group in files, extraneous files, category/type, single file in folder, missing episodes, unknown language, suspicion heuristics. They ran before but were never displayed.
- Fails and warnings from these checks now go into the corrective message.
- The per-check switches in Settings now work. A switched-off check shows as N/A and stays out of the message.
- Review results no longer count as "not passed" in the section and header counts.
## [0.6.7] - 2026-04-10
### Fixed
- **Diacritics in rename detection** — accented characters (É, â, û, ñ, ö, etc.) no longer cause false "possible rename" warnings. Titles like `Belle Époque` vs filename `Belle Epoque` are now correctly recognized as equivalent. Uses Unicode NFD normalization in the Jaccard word comparison.
- **Apostrophes in title comparison** — `Serpent's Path` now matches `Serpents Path`. All apostrophes (straight and curly) are stripped during word normalization.
- **Foreign-language filenames with AKA** — files using a foreign primary title with an AKA English title after the year (e.g. `La.voie.du.serpent.2024.AKA.Serpents.Path...`) now correctly match the English upload title. AKA is extracted from the full release name, not just the title portion.
## [0.6.6] - 2026-04-10
### Fixed
- **AKA title false rename warnings** — titles with "AKA Original-Title" (e.g. `Squid Game AKA Ojing-eo geim`) no longer trigger false "possible rename" warnings when the folder uses the short name. The AKA portion is stripped before title comparison.
- **Search button first-click** — rewrote integration rendering to use stable container divs, fixing the issue where the Search button only worked on the second click.
- **Search button idle message** — changed to "Idle — use the Search button above to check" for clarity.
### Added
- **Auto-search toggle** — new "Auto-search on page load" checkbox in Prowlarr settings. When unchecked, SRRDB/Prowlarr only search when you click the Search button. No more automatic queries while browsing.
### Changed
- **Search button styling** — moved to left-aligned position below section header with subtle blue accent for better visibility.
## [0.6.5] - 2026-04-10
### Fixed
- **AKA title false rename warnings** — titles with "AKA Original-Title" (e.g. `Squid Game AKA Ojing-eo geim`) no longer trigger false "possible rename" warnings when the folder uses the short name (`Squid.Game`). The AKA portion is stripped before title comparison.
### Added
- **Auto-search toggle** — new "Auto-search on page load" checkbox in Prowlarr settings. When unchecked, SRRDB/Prowlarr only search when you click the Search button. No more automatic queries while browsing.
## [0.6.4] - 2026-04-09
### Fixed
- **Corrective message BBCode** — corrected Naming Guide link to proper `[url=…]…[/url]` BBCode syntax per bbcode.org spec.
## [0.6.3] - 2026-04-09
### Fixed
- **Corrective message BBCode** — incorrect fix (used `[url]…[/url]` without `=` attribute).
## [0.6.2] - 2026-04-09
### Fixed
- **Corrective message BBCode** — incorrect fix for the URL tag syntax (used `[url=…]` instead of `[url]…[/url]`).
## [0.6.1] - 2026-04-09
### Added
- **Prowlarr timeout setting** — search timeout is now configurable via Tampermonkey settings (default: 15000ms). Accessible in the Integrations fieldset alongside Prowlarr URL and API Key.
## [0.6.0] - 2026-04-09
### Fixed
- **Banned groups** — replaced Luminarr list (87 groups) with DarkPeers authoritative list (60 groups + EVO/HDT conditional exceptions). False positives for Kira, 4K4U, d3g, iVy eliminated. EVO allowed for WEB-DL, HDT allowed for REMUX.
- **Encode resolution** — removed fake "720p minimum" rule. DarkPeers allows all resolutions including 360p, 480i, 480p, 576i, 576p.
- **Opus/FLAC** — removed fake mono/stereo restriction. DarkPeers has no channel limit on Opus or FLAC.
- **Dual-Audio** — corrected to DarkPeers definition: any 2 languages = Dual-Audio, 3+ = MULTi. Removed incorrect English-origin restriction.
- **Release group** — missing group tag is now a warning, not an error. DarkPeers allows uploads without a group tag.
- **Atmos detection** — now checks only the default audio track, not all tracks. Eliminates false positives from Atmos on commentary/secondary tracks.
- **HDR validation** — no longer gated behind 2160p/4320p. Validates HDR tags whenever they appear in the title, catching "HDR10" (should be "HDR") at any resolution.
- **SRRDB search** — added scene-name normalization (strip colons/apostrophes, & → and) and fallback keyword search when exact match fails.
- **Prowlarr rename detection** — self-consistency checks now ignore release group differences between torrent title and folder/file names, reducing false rename warnings.
- **Corrective messages** — replaced Luminarr §section references with plain "Naming Guide" link to DarkPeers wiki. Removed `img.luminarr.me` from image hosts.
### Added
- **Ignored indexers** — new setting to exclude specific indexers from Prowlarr results (e.g. TorrentLeech). Comma-separated, case-insensitive.
- **Manual re-search** — "Re-search" button in External Integrations section header. Prevents unwanted auto-searches after page edits. Click to re-query SRRDB and Prowlarr on demand.
- **Expanded indexer view** — all matching indexers shown (no longer capped at 3). First 3 inline, rest in collapsible "+N more" section.
- **Torrent age** — relative age shown next to each alternative indexer ("3d ago", "2mo ago") when Prowlarr provides publish dates.
- **Dynamic match summaries** — Prowlarr summary now lists only fields that actually match instead of hardcoded "title, year, resolution, codecs all consistent". Mismatched fields are excluded from the claim.
- **Alternative hyperlinks** — "Also found on" indexers are now clickable links to the source tracker page when URL is available.
## [0.5.0] - 2026-04-06
### Added
- **Preferred indexer selection** — new `preferredIndexers` setting (comma-separated list in Settings panel) allows moderators to specify trusted indexers. When multiple indexers return the same release (tied relevance scores), preferred indexers are selected first. Preference is strictly a tiebreaker — never overrides a clearly better match from a non-preferred indexer.
- **Best match link** — best match title now links to the source tracker page via Prowlarr's `infoUrl` field (opens in new tab). Link omitted gracefully when URL unavailable.
- **Field comparison detail** — collapsible "Comparison details" section shows per-field match status (title, year, resolution, source, video codec, audio codec), self-consistency check results (folder ✓/✗, filename ✓/✗), and alternative indexers.
- **Alternative indexers** — up to 3 alternative indexers shown in detail section ("Also found on: BLU, TL")
### Changed
- **Explainability rewrite** — replaced vague "Title consistent with indexed release" with specific moderation summaries:
- `match`: "Name matches release on {indexer} — title, year, resolution, codecs all consistent"
- `likely_match`: "Name likely matches release on {indexer} — minor field differences"
- `uncertain`: "No strong match found — cannot verify release name automatically"
- `likely_renamed`: "Possible rename — {specific issue}"
- `renamed`: "Likely renamed — folder and filename both differ from torrent name"
- **Rename issue labels** — replaced misleading "expected/found" framing with "Torrent name: / Folder name:" to avoid implying the torrent name is the authoritative source
- **Header wording** — "Found on N indexer(s)" replaced with "Found on {indexer}" (single) or "Best match from {indexer} (N indexers total)" (multiple)
- **Match selection algorithm** — replaced single-pass highest-score selection with candidate-based ranking: relevance score (descending, 0.01 epsilon for ties) → preferred indexer rank → seeders (descending)
## [0.4.1] - 2026-04-06
### Fixed
+1 -5
View File
@@ -19,8 +19,4 @@ Tampermonkey checks for updates automatically. To force a check: Tampermonkey ic
## Optional: Prowlarr Integration
Open the settings via Tampermonkey menu > **ModQ Helper Settings** and enter your Prowlarr URL and API key to enable indexer-backed rename detection.
### Preferred Indexers
In the settings panel you can optionally set a comma-separated list of preferred indexers (e.g. `Aither, BLU`). When multiple indexers return the same release, the preferred one is shown as the best match. This is a tiebreaker only — a clearly better match from another indexer still wins.
Open the settings via Tampermonkey menu > **ModQ Helper Settings** and enter your Prowlarr URL and API key to enable cross-seed detection.
+109 -432
View File
@@ -1,7 +1,7 @@
// ==UserScript==
// @name UNIT3D Mod Queue Helper — DarkPeers
// @namespace https://gitea.computerliebe.org/Procuria/dp-modq-helper
// @version 0.6.8
// @version 0.4.1
// @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
@@ -175,21 +175,20 @@ const g = {
"DSR"
]
},
// DarkPeers banned groups — sourced from https://darkpeers.org/pages/9 (2026-04-08)
bannedGroups: [
"ARCADE", "aXXo", "BANDOLEROS", "BONE", "BRrip", "CM8", "CrEwSaDe", "CTFOH", "dAV1nci", "DNL",
"eranger2", "FGT", "FiSTER", "flower", "GalaxyTV", "HD2DVD", "HDTime", "HorribleSubs",
"iHYTECH", "ION10", "iPlanet", "KiNGDOM", "LAMA", "MeGusta", "mHD", "mSD", "NaNi", "NhaNc3",
"nHD", "nikt0", "nSD", "OFT", "PiTBULL", "PRODJi", "PSA", "RARBG", "Rifftrax",
"ROCKETRACCOON", "SANTi", "SasukeducK", "SEEDSTER", "ShAaNiG", "Sicario", "STUTTERSHIT",
"Subsplease", "TAoE", "TGALAXY", "TGx", "TORRENTGALAXY", "ToVaR", "Trix", "TSP", "TSPxL",
"ViSION", "VXT", "WAF", "WKS", "X0r", "YIFY", "YTS"
"1000", "24xHD", "41RGB", "4K4U", "AG", "AOC", "AROMA", "aXXo", "AZAZE", "BARC0DE", "BAUCKLEY",
"BdC", "beAst", "BRiNK", "BTM", "C1NEM4", "C4K", "CDDHD", "CHAOS", "CHD", "CHX", "CiNE",
"COLLECTiVE", "CREATiVE24", "CrEwSaDe", "CTFOH", "d3g", "DDR", "DepraveD", "DNL", "DRX", "EPiC",
"EuReKA", "EVO", "FaNGDiNG0", "Feranki1980", "FGT", "flower", "FMD", "FRDS", "FZHD", "GalaxyRG",
"GHD", "GHOSTS", "GPTHD", "HDHUB4U", "HDS", "HDT", "HDTime", "HDWinG", "HiQVE", "iNTENSO",
"iPlanet", "iVy", "jennaortegaUHD", "JFF", "KC", "KiNGDOM", "KIRA", "L0SERNIGHT", "LAMA",
"Leffe", "Liber8", "LiGaS", "LT", "LUCY", "MarkII", "MeGusta", "Mesc", "mHD", "mSD", "MT",
"MTeam", "MySiLU", "NhaNc3", "nhanc3", "nHD", "nikt0", "nSD", "OFT", "Paheph", "PATOMiEL",
"PRODJi", "PSA", "PTNK", "RARBG", "RDN", "Rifftrax", "RU4HD", "SANTi", "SasukeducK", "Scene",
"SHD", "ShieldBearer", "STUTTERSHIT", "SUNSCREEN", "TBS", "TEKNO3D", "TG", "Tigole", "TIKO",
"VIDEOHOLE", "VISIONPLUSHDR", "WAF", "WiKi", "worldmkv", "x0r", "XLF", "YIFY", "YTSMX", "Zero00",
"Zeus"
],
// Groups with conditional exceptions (not in bannedGroups — checked separately)
bannedGroupExceptions: {
"EVO": { allowedTypes: ["WEB-DL"] },
"HDT": { allowedTypes: ["REMUX"] },
},
exceptionGroupNames: [
"DiscoD HONE", "DarQ HONE", "Eml HDTeam", "BEN THE MEN", "D-Z0N3", "ZØNEHD", "Anime Time",
"Project Angel", "Hakata Ramen", "-ZR-"
@@ -203,7 +202,7 @@ const g = {
],
releaseGroupSuffixes: /(?:-(RP|1|NZBGeek|Obfuscated|Obfuscation|Scrambled|sample|Pre|postbot|xpost|Rakuv[a-z0-9]*|WhiteRev|BUYMORE|AsRequested|AlternativeToRequested|GEROV|Z0iDS3N|Chamele0n|4P|4Planet|AlteZachen|RePACKPOST))+$/i,
imageHosts: [
"imgbb.com", "imgur.com", "ptpimg.me", "imgbox.com", "beyondhd.co",
"imgbb.com", "imgur.com", "ptpimg.me", "imgbox.com", "beyondhd.co", "img.luminarr.me",
"slowpic.", "pixhost.", "ibb.co", "postimg.", "funkyimg.", "image.tmdb.org"
],
imageExtensions: [
@@ -1227,7 +1226,7 @@ let _resolvedModStatuses = DEFAULT_MOD_STATUSES;
const Settings = {
_KEY: "modq_settings",
_defaults: {
prowlarr: { url: "", apiKey: "", enabled: false, preferredIndexers: [], ignoredIndexers: [], timeout: 15000, autoSearch: true },
prowlarr: { url: "", apiKey: "", enabled: false },
srrdb: { enabled: true },
checks: {
tmdbMatch: true, seasonEpisode: true, namingGuide: true,
@@ -1350,29 +1349,6 @@ const Settings = {
<input type="password" data-setting="prowlarr.apiKey" value="${s.prowlarr.apiKey}" placeholder="API key"
style="background:#1e293b;border:1px solid #334155;color:#e2e8f0;border-radius:4px;padding:4px 8px;font-size:12px;">
</label>
<label style="display:flex;flex-direction:column;gap:4px;margin:6px 0;color:#cbd5e1;font-size:13px;">
Preferred indexers:
<input type="text" data-setting="prowlarr.preferredIndexers" value="${(s.prowlarr.preferredIndexers || []).join(", ")}" placeholder="e.g. Aither, BLU (most preferred first)"
style="background:#1e293b;border:1px solid #334155;color:#e2e8f0;border-radius:4px;padding:4px 8px;font-size:12px;">
<small style="color:#64748b;font-size:11px;">Comma-separated. When tied matches exist on multiple indexers, prefer these.</small>
</label>
<label style="display:flex;flex-direction:column;gap:4px;margin:6px 0;color:#cbd5e1;font-size:13px;">
Ignored indexers:
<input type="text" data-setting="prowlarr.ignoredIndexers" value="${(s.prowlarr.ignoredIndexers || []).join(", ")}" placeholder="e.g. TorrentLeech, RARBG"
style="background:#1e293b;border:1px solid #334155;color:#e2e8f0;border-radius:4px;padding:4px 8px;font-size:12px;">
<small style="color:#64748b;font-size:11px;">Comma-separated. Results from these indexers are excluded from Prowlarr matches.</small>
</label>
<label style="display:flex;align-items:center;gap:8px;margin:6px 0;color:#cbd5e1;font-size:13px;">
Prowlarr timeout (ms):
<input type="number" data-setting="prowlarr.timeout" value="${s.prowlarr.timeout || 15000}" min="1000" max="60000" step="1000"
style="width:80px;background:#1e293b;border:1px solid #334155;color:#e2e8f0;border-radius:4px;padding:2px 6px;">
<small style="color:#64748b;font-size:11px;">Default: 15000</small>
</label>
<label style="display:flex;align-items:center;gap:6px;margin:8px 0 2px;color:#cbd5e1;font-size:13px;">
<input type="checkbox" data-setting="prowlarr.autoSearch" ${s.prowlarr.autoSearch !== false ? "checked" : ""}>
Auto-search on page load
</label>
<small style="color:#64748b;font-size:11px;margin-left:22px;">When off, Prowlarr/SRRDB only search when you click the Search button.</small>
</fieldset>
<fieldset style="border:1px solid #334155;border-radius:6px;padding:12px;margin-bottom:12px;">
@@ -1425,13 +1401,9 @@ const Settings = {
const s = self.load();
modal.querySelectorAll("[data-setting]").forEach(el => {
const key = el.getAttribute("data-setting");
let val = el.type === "checkbox" ? el.checked
const val = el.type === "checkbox" ? el.checked
: el.type === "number" ? parseInt(el.value, 10)
: el.value;
// Parse comma-separated list settings into arrays
if (key === "prowlarr.preferredIndexers" || key === "prowlarr.ignoredIndexers") {
val = typeof val === "string" ? val.split(",").map(s => s.trim()).filter(Boolean) : [];
}
const parts = key.split(".");
let target = s;
for (let i = 0; i < parts.length - 1; i++) {
@@ -1461,7 +1433,7 @@ Settings.init();
* Ported from the original H object + z (diff tool)
* ======================================================================== */
const H={extractReleaseGroup(e){if(!e)return null;let n=e.replace(g.releaseGroupSuffixes,"");const t=g.exceptionGroupNames.find(o=>n.endsWith("-"+o)||n.endsWith("- "+o));if(t)return t;const s=n.match(/[(\[]([^\]()]+)[)\]]$/);if(s){const o=s[1];if(g.bracketGroupNames.some(r=>r.toLowerCase()===o.toLowerCase()))return o}const a=n.match(/-([A-Za-z0-9$!._&+\$™]+)$/i);return a?a[1]:null},findTieredGroup(e,n){if(!e)return null;const t=n?"sonarr":"radarr",s=e.toLowerCase(),a=[];for(const o of g._tieredGroupsRaw)o.source===t&&o.groups.some(c=>c.toLowerCase()===s)&&a.push(o.name);return a.length>0?a:null},extractYear(e){if(!e)return null;const n=e.match(/\b(19|20)\d{2}\b/);return n?n[0]:null},countScreenshots(e){if(!e)return{count:0,urls:[]};const n=[],t=/\[img\](.*?)\[\/img\]/gi;let s;for(;(s=t.exec(e))!==null;)n.push(s[1]);const a=/<img[^>]+src=["']([^"']+)["']/gi;for(;(s=a.exec(e))!==null;)n.push(s[1]);const o=n.filter(r=>{const d=r.toLowerCase(),f=g.imageExtensions.some(b=>d.includes(b)),A=g.imageHosts.some(b=>d.includes(b)),T=d.includes("image.tmdb.org")&&(d.includes("/w342/")||d.includes("/w500/")||d.includes("/w1280/")||d.includes("/w138"));return(f||A)&&!T}),c=[...new Set(o)];return{count:c.length,urls:c}},parseSeasonEpisode(e){if(!e)return{season:null,episode:null,raw:null,isSeasonPack:!1};const n=e.match(/S(\d{1,2})E(\d{1,2})/i);if(n)return{season:parseInt(n[1],10),episode:parseInt(n[2],10),raw:n[0],isSeasonPack:!1};const t=e.match(/\bS(\d{1,2})\b(?!E)/i);return t?{season:parseInt(t[1],10),episode:null,raw:t[0],isSeasonPack:!0}:{season:null,episode:null,raw:null,isSeasonPack:!1}},normalizeForComparison(e){return e?e.toLowerCase().replace(/['']/g,"'").replace(/[""]/g,'"').replace(/[–—]/g,"-").replace(/\s+/g," ").trim():""},normalizeForComparisonPreserveCase(e){return e?e.replace(/['']/g,"'").replace(/[""]/g,'"').replace(/[–—]/g,"-").replace(/\s+/g," ").trim():""},detectAudioObject(e){if(!e)return null;const n=e.replace(/^Title\s*:.*$/gm,"");return/(Dolby\s?Atmos|E-AC-3\s?JOC)/i.test(n)?"Atmos":/(Auro\s?3D)/i.test(n)?"Auro3D":null},detectAudioObjectFromTracks(tracks){if(!tracks||tracks.length===0)return null;const def=tracks.find(t=>t.isDefault)||tracks[0];if(!def)return null;const codec=(def.codec||"").toLowerCase();const comm=(def.commercialName||"").toLowerCase();const title=(def.title||"").toLowerCase();if(comm.includes("atmos")||title.includes("atmos")||codec.includes("joc")||(codec.includes("e-ac-3")&&title.includes("joc")))return"Atmos";if(comm.includes("auro")||title.includes("auro 3d")||title.includes("auro3d"))return"Auro3D";return null},extractTitleElements(e,n){if(!e)return{elements:[],positions:{}};const t=[],s={},a=e,o=(h,i,l)=>{i!==null&&l!==-1&&(t.push({type:h,value:i,position:l}),s[h]=l)},c=a.match(/\b(19|20)\d{2}\b/);c&&o("year",c[0],c.index);const r=a.match(/\bS(\d{1,2})(?:E(\d{1,2}))?\b/i);r&&o("season",r[0],r.index);for(const h of g.validResolutions){const i=a.indexOf(h);if(i!==-1){o("resolution",h,i);break}}const d=[...g.hdrFormats].sort((h,i)=>i.length-h.length);for(const h of d){const i=new RegExp("\\b"+h.replace(/[+]/g,"\\+")+"\\b","i"),l=a.match(i);if(l){o("hdr",l[0],l.index);break}}const f=[...g.validVideoCodecs].sort((h,i)=>i.length-h.length);for(const h of f){const i=new RegExp(h.replace(/[.]/g,"\\.?"),"i"),l=a.match(i);if(l){o("vcodec",l[0],l.index);break}}const A=[...g.validAudioCodecs].sort((h,i)=>i.length-h.length);for(const h of A){const i=h.replace(/[+]/g,"\\+").replace(/[-.]/g,"[-.]?"),l=new RegExp("(?<![a-zA-Z])"+i+"(?![a-zA-Z])","i"),m=a.match(l);if(m){o("acodec",m[0],m.index);break}}const T=a.match(/\b(\d{1,2}\.\d)\b/);T&&o("channels",T[0],T.index);const b=a.match(/\bAtmos\b/i),u=a.match(/\bAuro(?:3D)?\b/i);b?o("object",b[0],b.index):u&&o("object",u[0],u.index);const D=[...g.sources.fullDisc,...g.sources.remux,...g.sources.encode,...g.sources.web,...g.sources.hdtv],x=[...new Set(D)].sort((h,i)=>i.length-h.length);for(const h of x){const i=new RegExp(h.replace(/[-.]/g,"[-. ]?"),"i"),l=a.match(i);if(l){o("source",l[0],l.index);break}}const p=a.match(/\b(REMUX|WEB-DL|WEBRip)\b/i);p&&o("type",p[0],p.index);for(const h of g.dubs){const i=new RegExp(`\\b${h.replace("-","[-]?")}\\b`,"i"),l=a.match(i);if(l){o("dub",l[0],l.index);break}}for(const h of g.cuts){const i=new RegExp(h.replace(/'/g,"[']?"),"i"),l=a.match(i);if(l){o("cut",l[0],l.index);break}}for(const h of g.ratios){const i=new RegExp(` ${h} `,"i"),l=a.match(i);if(l){o("ratio",h,l.index+1);break}}for(const h of g.repacks){const i=new RegExp(`\\b${h}\\b`,"i"),l=a.match(i);if(l){o("repack",l[0],l.index);break}}for(const h of g.editions){const i=new RegExp(h.replace(/'/g,"[']?"),"i"),l=a.match(i);if(l){o("edition",l[0],l.index);break}}const y=a.match(/\b3D\b/);y&&o("3d","3D",y.index);const C=a.match(/-([A-Za-z0-9$!._&+\$™]+)$/i);return C&&o("group",C[1],C.index),t.sort((h,i)=>h.position-i.position),{elements:t,positions:s}}};
const H={extractReleaseGroup(e){if(!e)return null;let n=e.replace(g.releaseGroupSuffixes,"");const t=g.exceptionGroupNames.find(o=>n.endsWith("-"+o)||n.endsWith("- "+o));if(t)return t;const s=n.match(/[(\[]([^\]()]+)[)\]]$/);if(s){const o=s[1];if(g.bracketGroupNames.some(r=>r.toLowerCase()===o.toLowerCase()))return o}const a=n.match(/-([A-Za-z0-9$!._&+\$™]+)$/i);return a?a[1]:null},findTieredGroup(e,n){if(!e)return null;const t=n?"sonarr":"radarr",s=e.toLowerCase(),a=[];for(const o of g._tieredGroupsRaw)o.source===t&&o.groups.some(c=>c.toLowerCase()===s)&&a.push(o.name);return a.length>0?a:null},extractYear(e){if(!e)return null;const n=e.match(/\b(19|20)\d{2}\b/);return n?n[0]:null},countScreenshots(e){if(!e)return{count:0,urls:[]};const n=[],t=/\[img\](.*?)\[\/img\]/gi;let s;for(;(s=t.exec(e))!==null;)n.push(s[1]);const a=/<img[^>]+src=["']([^"']+)["']/gi;for(;(s=a.exec(e))!==null;)n.push(s[1]);const o=n.filter(r=>{const d=r.toLowerCase(),f=g.imageExtensions.some(b=>d.includes(b)),A=g.imageHosts.some(b=>d.includes(b)),T=d.includes("image.tmdb.org")&&(d.includes("/w342/")||d.includes("/w500/")||d.includes("/w1280/")||d.includes("/w138"));return(f||A)&&!T}),c=[...new Set(o)];return{count:c.length,urls:c}},parseSeasonEpisode(e){if(!e)return{season:null,episode:null,raw:null,isSeasonPack:!1};const n=e.match(/S(\d{1,2})E(\d{1,2})/i);if(n)return{season:parseInt(n[1],10),episode:parseInt(n[2],10),raw:n[0],isSeasonPack:!1};const t=e.match(/\bS(\d{1,2})\b(?!E)/i);return t?{season:parseInt(t[1],10),episode:null,raw:t[0],isSeasonPack:!0}:{season:null,episode:null,raw:null,isSeasonPack:!1}},normalizeForComparison(e){return e?e.toLowerCase().replace(/['']/g,"'").replace(/[""]/g,'"').replace(/[–—]/g,"-").replace(/\s+/g," ").trim():""},normalizeForComparisonPreserveCase(e){return e?e.replace(/['']/g,"'").replace(/[""]/g,'"').replace(/[–—]/g,"-").replace(/\s+/g," ").trim():""},detectAudioObject(e){if(!e)return null;const n=e.replace(/^Title\s*:.*$/gm,"");return/(Dolby\s?Atmos|E-AC-3\s?JOC)/i.test(n)?"Atmos":/(Auro\s?3D)/i.test(n)?"Auro3D":null},extractTitleElements(e,n){if(!e)return{elements:[],positions:{}};const t=[],s={},a=e,o=(h,i,l)=>{i!==null&&l!==-1&&(t.push({type:h,value:i,position:l}),s[h]=l)},c=a.match(/\b(19|20)\d{2}\b/);c&&o("year",c[0],c.index);const r=a.match(/\bS(\d{1,2})(?:E(\d{1,2}))?\b/i);r&&o("season",r[0],r.index);for(const h of g.validResolutions){const i=a.indexOf(h);if(i!==-1){o("resolution",h,i);break}}const d=[...g.hdrFormats].sort((h,i)=>i.length-h.length);for(const h of d){const i=new RegExp("\\b"+h.replace(/[+]/g,"\\+")+"\\b","i"),l=a.match(i);if(l){o("hdr",l[0],l.index);break}}const f=[...g.validVideoCodecs].sort((h,i)=>i.length-h.length);for(const h of f){const i=new RegExp(h.replace(/[.]/g,"\\.?"),"i"),l=a.match(i);if(l){o("vcodec",l[0],l.index);break}}const A=[...g.validAudioCodecs].sort((h,i)=>i.length-h.length);for(const h of A){const i=h.replace(/[+]/g,"\\+").replace(/[-.]/g,"[-.]?"),l=new RegExp("(?<![a-zA-Z])"+i+"(?![a-zA-Z])","i"),m=a.match(l);if(m){o("acodec",m[0],m.index);break}}const T=a.match(/\b(\d{1,2}\.\d)\b/);T&&o("channels",T[0],T.index);const b=a.match(/\bAtmos\b/i),u=a.match(/\bAuro(?:3D)?\b/i);b?o("object",b[0],b.index):u&&o("object",u[0],u.index);const D=[...g.sources.fullDisc,...g.sources.remux,...g.sources.encode,...g.sources.web,...g.sources.hdtv],x=[...new Set(D)].sort((h,i)=>i.length-h.length);for(const h of x){const i=new RegExp(h.replace(/[-.]/g,"[-. ]?"),"i"),l=a.match(i);if(l){o("source",l[0],l.index);break}}const p=a.match(/\b(REMUX|WEB-DL|WEBRip)\b/i);p&&o("type",p[0],p.index);for(const h of g.dubs){const i=new RegExp(`\\b${h.replace("-","[-]?")}\\b`,"i"),l=a.match(i);if(l){o("dub",l[0],l.index);break}}for(const h of g.cuts){const i=new RegExp(h.replace(/'/g,"[']?"),"i"),l=a.match(i);if(l){o("cut",l[0],l.index);break}}for(const h of g.ratios){const i=new RegExp(` ${h} `,"i"),l=a.match(i);if(l){o("ratio",h,l.index+1);break}}for(const h of g.repacks){const i=new RegExp(`\\b${h}\\b`,"i"),l=a.match(i);if(l){o("repack",l[0],l.index);break}}for(const h of g.editions){const i=new RegExp(h.replace(/'/g,"[']?"),"i"),l=a.match(i);if(l){o("edition",l[0],l.index);break}}const y=a.match(/\b3D\b/);y&&o("3d","3D",y.index);const C=a.match(/-([A-Za-z0-9$!._&+\$™]+)$/i);return C&&o("group",C[1],C.index),t.sort((h,i)=>h.position-i.position),{elements:t,positions:s}}};
const z={tokenize(e){return e.trim().split(/([.\s]+)/).filter(n=>n.length>0)},diff(e,n){const t=this.tokenize(e),s=this.tokenize(n),a=t.length,o=s.length,c=Array.from({length:a+1},()=>new Array(o+1).fill(0));for(let A=1;A<=a;A++)for(let T=1;T<=o;T++)t[A-1].toLowerCase()===s[T-1].toLowerCase()?c[A][T]=c[A-1][T-1]+1:c[A][T]=Math.max(c[A-1][T],c[A][T-1]);const r=[];let d=a,f=o;for(;d>0||f>0;)d>0&&f>0&&t[d-1].toLowerCase()===s[f-1].toLowerCase()?(r.unshift({type:"match",text:t[d-1]}),d--,f--):f>0&&(d===0||c[d][f-1]>=c[d-1][f])?(r.unshift({type:"extra",text:s[f-1]}),f--):(r.unshift({type:"missing",text:t[d-1]}),d--);return r},matchPercent(e){const n=e.filter(o=>o.type==="match").length,t=e.filter(o=>o.type==="missing").length,s=e.filter(o=>o.type==="extra").length,a=n+t+s;return a===0?100:Math.round(n/a*100)},escapeHtml(e){return e.replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;")},renderLine(e,n){return e.map(t=>{let s;return t.type==="match"?s="mh-diff-token--ctx":n==="del"?s="mh-diff-token--del":s="mh-diff-token--add",`<span class="${s}">${this.escapeHtml(t.text)}</span>`}).join("")},renderDiff(e){const n=this.matchPercent(e),t=e.some(c=>c.type!=="match"),s=`
<div class="mh-diff-hunk-header">
@@ -1889,7 +1861,7 @@ const k = {
checks: []
},
o = e || "",
c = (typeof E !== "undefined" && E?.isTV) ? E.isTV() : !1,
c = E.isTV(),
r = H.extractYear(o);
let d = "fail",
f = "No year found";
@@ -1938,14 +1910,12 @@ const k = {
const l = H.extractReleaseGroup(o);
a.checks.push({
name: "Release Group",
status: l ? "pass" : b ? "na" : "warn",
message: l ? `Found: ${l}` : b ? "N/A for Full Disc" : "No release group tag detected (allowed on DarkPeers if none exists)",
required: !1
status: l ? "pass" : b ? "na" : "fail",
message: l ? `Found: ${l}` : b ? "N/A for Full Disc" : "No release group tag found (should end with -GROUP)",
required: !0
});
const m = g.fullDiscTypes.some(R => n?.includes(R));
// Prefer track-based detection (default track only) over raw-text detection (all tracks)
const _audioTracks = (typeof E !== "undefined" && E?.getAudioTracksFromMediaInfo) ? E.getAudioTracksFromMediaInfo() : [];
const S = m ? null : (_audioTracks.length > 0 ? H.detectAudioObjectFromTracks(_audioTracks) : H.detectAudioObject(t)),
const m = g.fullDiscTypes.some(R => n?.includes(R)),
S = m ? null : H.detectAudioObject(t),
N = /Atmos/i.test(o),
$ = /Auro/i.test(o);
let M = "pass",
@@ -1970,8 +1940,8 @@ const k = {
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
}), (function(){ const _hdrInTitle = g.hdrFormats.some(fmt => new RegExp("\\b" + fmt.replace(/[+]/g, "\\+") + "\\b", "i").test(o)); const _hdrInMI = (typeof E !== "undefined" && E?.getHdrFromMediaInfo) ? E.getHdrFromMediaInfo() : []; return o.includes("2160p") || o.includes("4320p") || _hdrInTitle || _hdrInMI.length > 0; })()) {
const R = (typeof E !== "undefined" && E?.getHdrFromMediaInfo) ? E.getHdrFromMediaInfo() : [],
}), 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)
@@ -2103,22 +2073,41 @@ const k = {
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 (x > 2) r.push({
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: `Tagged Dual-Audio but found ${x} languages (${t.join(", ")}). Use "MULTi" for 3+ languages`
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 r.push({
else {
const i = t.some(C),
l = t.some(h);
i ? l ? r.push({
name: "Language Tags",
status: "pass",
message: `Dual-Audio correct (${t.join(" + ")})`
});
else if (D) x < 2 ? r.push({
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`
@@ -2150,7 +2139,7 @@ const k = {
message: `Audio languages OK (${x})`
})
}
const d = (typeof E !== "undefined" && E?.getAudioTracksFromMediaInfo) ? E.getAudioTracksFromMediaInfo() : [];
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,
@@ -2210,8 +2199,8 @@ const k = {
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 === "LPCM") {
const N = b; // LPCM multichannel allowed for untouched sources
if (m === "FLAC" || m === "Opus" || m === "LPCM") {
const N = m === "LPCM" && b;
!x(l.channels) && !N ? r.push({
name: S,
status: "fail",
@@ -2221,12 +2210,6 @@ const k = {
status: "pass",
message: x(l.channels) ? `${m} mono/stereo OK` : `${m} multichannel (untouched OK)`
})
} else if (m === "FLAC" || m === "Opus") {
r.push({
name: S,
status: "pass",
message: `${m} ${l.channels || "unknown"} OK`
})
} else m === "MP2" ? u ? r.push({
name: S,
status: "pass",
@@ -2669,11 +2652,16 @@ const k = {
message: "Title has H.265 — encodes typically use encoder name (x265) instead"
})
}
const p = g.validResolutions.find(i => o.includes(i));
if (p ? a.push({
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",
@@ -2811,22 +2799,19 @@ const k = {
tieredInfo: null
};
const o = H.findTieredGroup(a, n);
const aLower = a.toLowerCase();
// Check unconditional ban list
if (g.bannedGroups.some(r => r.toLowerCase() === aLower)) {
return { status: "fail", group: a, message: `BANNED GROUP: ${a}`, alert: !0, tieredInfo: o };
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
}
// Check conditional exceptions (EVO → WEB-DL allowed, HDT → REMUX allowed)
const exception = Object.entries(g.bannedGroupExceptions).find(([name]) => name.toLowerCase() === aLower);
if (exception) {
const [excName, excRule] = exception;
const typeUpper = (t || "").toUpperCase();
if (excRule.allowedTypes.some(at => typeUpper.includes(at.toUpperCase()))) {
return { status: "pass", group: a, message: `Release Group: ${a} (${excName} allowed for ${t})`, alert: !1, tieredInfo: o };
}
return { status: "fail", group: a, message: `BANNED GROUP: ${a} (only ${excRule.allowedTypes.join("/")} releases allowed)`, alert: !0, tieredInfo: o };
}
return { status: "pass", group: a, message: `Release Group: ${a}`, alert: !1, tieredInfo: o };
}
};
k.resolutionTypeMatch = function (e, n) {
@@ -3656,34 +3641,14 @@ const Integrations = {
},
srrdb: {
/**
* _normalizeForSrrdb — Convert a DP torrent title into scene-style format.
* SRRDB expects dot-separated scene names with specific formatting.
*/
_normalizeForSrrdb(name) {
if (!name) return "";
return name
.replace(/[:']/g, "") // strip colons, apostrophes (scene format omits them)
.replace(/[,]/g, "") // strip commas
.replace(/&/g, "and") // scene convention
.replace(/\s+/g, ".") // spaces → dots
.replace(/\.{2,}/g, "."); // collapse multiple dots
},
/**
* search — look up a release name on SRRDB.
* Strategy: try exact match first (r: prefix), then fall back to general
* keyword search if exact match fails. This handles the common case where
* the DP torrent title differs slightly from the scene release name.
*/
async search(releaseName) {
if (!releaseName) return { found: false, release: null, error: "No release name provided" };
const sceneName = this._normalizeForSrrdb(releaseName);
try {
// Strategy 1: exact release match
const encoded = encodeURIComponent(sceneName);
const encoded = encodeURIComponent(releaseName.replace(/\s+/g, "."));
const result = await Integrations._request({
url: `https://www.srrdb.com/api/search/r:${encoded}`,
timeout: 8000,
@@ -3695,31 +3660,9 @@ const Integrations = {
found: true,
release: data.results[0],
resultCount: data.results.length,
searchStrategy: "exact",
error: null,
};
}
// Strategy 2: keyword search (without r: prefix) — catches partial matches
// and handles titles with minor formatting differences
console.log(`[ModQ Helper] SRRDB exact match failed for "${sceneName}", trying keyword search`);
const kwResult = await Integrations._request({
url: `https://www.srrdb.com/api/search/${encoded}`,
timeout: 8000,
});
const kwData = kwResult.data;
if (kwData && kwData.results && kwData.results.length > 0) {
return {
found: true,
release: kwData.results[0],
resultCount: kwData.results.length,
searchStrategy: "keyword",
error: null,
};
}
console.log(`[ModQ Helper] SRRDB keyword search also returned no results for "${sceneName}"`);
return { found: false, release: null, error: null };
} catch (e) {
return { found: false, release: null, error: e.message };
@@ -3775,7 +3718,7 @@ const Integrations = {
const result = await Integrations._request({
url,
headers: { "X-Api-Key": config.apiKey },
timeout: config.timeout || 15000,
timeout: 15000,
});
const data = result.data;
@@ -3819,51 +3762,6 @@ const Integrations = {
const RenameDetector = {
/**
* _relativeAge — Format a date string as a human-readable relative age.
* D5: Used for torrent age display next to Prowlarr matches.
*/
_relativeAge(dateStr) {
if (!dateStr) return null;
try {
const d = new Date(dateStr);
if (isNaN(d.getTime())) return null;
const diffMs = Date.now() - d.getTime();
if (diffMs < 0) return "just now";
const mins = Math.floor(diffMs / 60000);
if (mins < 60) return `${mins}m ago`;
const hrs = Math.floor(mins / 60);
if (hrs < 24) return `${hrs}h ago`;
const days = Math.floor(hrs / 24);
if (days < 30) return `${days}d ago`;
const months = Math.floor(days / 30);
if (months < 12) return `${months}mo ago`;
const years = Math.floor(months / 12);
return `${years}y ago`;
} catch { return null; }
},
/**
* _stripAka — Remove "AKA ..." portion from a title name.
* DP naming convention includes "Name AKA Original" but folders/filenames
* never include the AKA part, causing Jaccard mismatch. Strip it before
* comparing titleNames.
*/
_stripAka(name) {
if (!name) return name;
return name.replace(/\s+AKA\s+.*/i, "").trim();
},
/**
* _stripDiacritics — Normalize accented characters to ASCII equivalents.
* É→E, â→a, û→u, ñ→n, ö→o, etc. Uses Unicode NFD decomposition
* followed by combining-mark removal.
*/
_stripDiacritics(str) {
if (!str) return str;
return str.normalize("NFD").replace(/[\u0300-\u036f]/g, "");
},
/** Codec aliases for fuzzy matching */
_codecAliases: {
"x264": "h.264", "h.264": "h.264", "avc": "h.264",
@@ -3918,17 +3816,9 @@ const RenameDetector = {
}
const titleName = raw.slice(0, titleEnd).replace(/[.\-_]/g, " ").replace(/\s+/g, " ").trim();
// Extract AKA title from the structural portion (after first token, e.g. after year).
// Only matches AKA that appears AFTER titleEnd — AKA in the title portion is
// handled by _stripAka on titleName instead.
const postTitle = normalized.slice(titleEnd);
const akaMatch = postTitle.match(/\bAKA\s+(.+?)(?=[\s.]+\d{3,4}p\b|[\s.]+(?:Blu-?ray|BluRay|WEB[-.]?DL|WEB[-.]?Rip|HDTV|Remux|REMUX|BDRip|BRRip|DVDRip)\b)/i);
const akaTitle = akaMatch ? akaMatch[1].replace(/[.\-_]/g, " ").replace(/\s+/g, " ").trim() : null;
return {
raw: name,
titleName,
akaTitle,
elements,
positions,
group: fieldOf("group"),
@@ -3947,9 +3837,8 @@ const RenameDetector = {
*/
_jaccardWords(a, b) {
if (!a || !b) return 0;
const norm = s => this._stripDiacritics(s).toLowerCase().replace(/['\u2019]/g, "");
const wordsA = new Set(norm(a).split(/\s+/).filter(Boolean));
const wordsB = new Set(norm(b).split(/\s+/).filter(Boolean));
const wordsA = new Set(a.toLowerCase().split(/\s+/).filter(Boolean));
const wordsB = new Set(b.toLowerCase().split(/\s+/).filter(Boolean));
if (wordsA.size === 0 && wordsB.size === 0) return 1;
if (wordsA.size === 0 || wordsB.size === 0) return 0;
let intersection = 0;
@@ -3981,16 +3870,8 @@ const RenameDetector = {
let weightedSum = 0;
const fieldScores = {};
// titleName — Jaccard similarity (strip AKA for fair comparison)
let titleScore = this._jaccardWords(this._stripAka(uploadTokens.titleName), this._stripAka(resultTokens.titleName));
// AKA fallback: try mid-name AKA titles if primary comparison is weak
if (titleScore < 0.5) {
const akaTitleScores = [
resultTokens.akaTitle ? this._jaccardWords(this._stripAka(uploadTokens.titleName), resultTokens.akaTitle) : 0,
uploadTokens.akaTitle ? this._jaccardWords(uploadTokens.akaTitle, this._stripAka(resultTokens.titleName)) : 0,
];
titleScore = Math.max(titleScore, ...akaTitleScores);
}
// titleName — Jaccard similarity
const titleScore = this._jaccardWords(uploadTokens.titleName, resultTokens.titleName);
fieldScores.titleName = titleScore;
weightedSum += titleScore * weights.titleName;
totalWeight += weights.titleName;
@@ -4034,74 +3915,19 @@ const RenameDetector = {
},
/**
* _indexerRank — Return preference rank for an indexer name.
* Lower = more preferred. Infinity = not in preference list.
* findBestMatch — Select the best Prowlarr result by relevance score.
*/
_indexerRank(indexerName, preferredList) {
if (!preferredList || preferredList.length === 0) return Infinity;
const idx = preferredList.findIndex(
p => p.toLowerCase() === (indexerName || "").toLowerCase()
);
return idx === -1 ? Infinity : idx;
},
/**
* findBestMatch — Select the best Prowlarr result using candidate ranking.
*
* Sort order:
* 1. relevanceScore (descending) — scores within 0.01 epsilon are ties
* 2. preferredIndexer rank (ascending — lower = more preferred)
* 3. seeders (descending — liveness tiebreaker)
*
* Also tracks up to 3 alternative candidates (score > 0.50) for display.
*/
findBestMatch(uploadTokens, results, preferredIndexers, ignoredIndexers) {
const EPSILON = 0.01;
const ignored = Array.isArray(ignoredIndexers) ? new Set(ignoredIndexers.map(n => n.toLowerCase())) : new Set();
const candidates = [];
findBestMatch(uploadTokens, results) {
let best = null;
let bestScore = -1;
for (const r of results) {
// D4: Skip results from ignored indexers
if (ignored.size > 0 && ignored.has((r.indexer || "").toLowerCase())) continue;
const tokens = this.tokenize(r.title || "");
const score = this.scoreMatch(uploadTokens, tokens);
candidates.push({
title: r.title, indexer: r.indexer, size: r.size,
infoUrl: r.infoUrl || null, guid: r.guid || null, seeders: r.seeders || 0,
publishDate: r.publishDate || null, // D5: torrent age
tokens, score, relevanceScore: score.relevanceScore,
});
}
if (candidates.length === 0) return null;
const prefs = Array.isArray(preferredIndexers) ? preferredIndexers : [];
candidates.sort((a, b) => {
// Primary: relevanceScore descending (ties within epsilon)
const scoreDiff = b.relevanceScore - a.relevanceScore;
if (Math.abs(scoreDiff) > EPSILON) return scoreDiff;
// Secondary: preferred indexer rank ascending
const rankA = this._indexerRank(a.indexer, prefs);
const rankB = this._indexerRank(b.indexer, prefs);
if (rankA !== rankB) return rankA - rankB;
// Tertiary: seeders descending
return (b.seeders || 0) - (a.seeders || 0);
});
const best = candidates[0];
// D3: Collect ALL alternatives (distinct indexers, score > 0.50) — no cap
const seen = new Set([best.indexer]);
const alternatives = [];
for (let i = 1; i < candidates.length; i++) {
const c = candidates[i];
if (c.relevanceScore >= 0.50 && !seen.has(c.indexer)) {
seen.add(c.indexer);
alternatives.push({ indexer: c.indexer, title: c.title, relevanceScore: c.relevanceScore, infoUrl: c.infoUrl || null, publishDate: c.publishDate || null });
if (score.relevanceScore > bestScore) {
bestScore = score.relevanceScore;
best = { title: r.title, indexer: r.indexer, size: r.size, tokens, score, relevanceScore: score.relevanceScore };
}
}
best.alternatives = alternatives;
return best;
},
@@ -4131,19 +3957,8 @@ const RenameDetector = {
* optionally ignores group differences.
*/
structurallyEquivalent(tokensA, tokensB, { ignoreGroup = false } = {}) {
// Compare title name words — strip AKA portion (folders never include it)
const titleA = this._stripAka(tokensA.titleName);
const titleB = this._stripAka(tokensB.titleName);
let titleSim = this._jaccardWords(titleA, titleB);
// AKA fallback: one side may use the foreign title while the other has the
// English title in a mid-name AKA portion (e.g. "La.voie.du.serpent.2024.AKA.Serpents.Path...")
if (titleSim < 0.5) {
const akaSims = [
tokensB.akaTitle ? this._jaccardWords(titleA, tokensB.akaTitle) : 0,
tokensA.akaTitle ? this._jaccardWords(tokensA.akaTitle, titleB) : 0,
];
titleSim = Math.max(titleSim, ...akaSims);
}
// Compare title name words
const titleSim = this._jaccardWords(tokensA.titleName, tokensB.titleName);
if (titleSim < 0.5) return false;
// Compare structural fields — must match if both present
@@ -4178,12 +3993,9 @@ const RenameDetector = {
const uploadTokens = this.tokenize(data.torrentName);
// Phase 1: Self-consistency checks (strongest signal)
// Use ignoreGroup for self-consistency — folder/file names may legitimately
// use different group formatting than the torrent title. The group comparison
// is handled separately via Prowlarr match scoring.
if (data.fileStructure?.folderName) {
const folderTokens = this.tokenize(data.fileStructure.folderName);
if (!this.structurallyEquivalent(uploadTokens, folderTokens, { ignoreGroup: true })) {
if (!this.structurallyEquivalent(uploadTokens, folderTokens)) {
issues.push({
type: "folder", severity: "high",
expected: data.torrentName,
@@ -4194,7 +4006,7 @@ const RenameDetector = {
if (data.mediaInfoFilename && tvScope.canCompareMediaInfoFile) {
const miTokens = this.tokenize(data.mediaInfoFilename);
if (!this.structurallyEquivalent(uploadTokens, miTokens, { ignoreGroup: true })) {
if (!this.structurallyEquivalent(uploadTokens, miTokens)) {
issues.push({
type: "filename", severity: "high",
expected: data.torrentName,
@@ -4241,59 +4053,9 @@ const RenameDetector = {
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.tLine truncated
`);if(s.size>0&&this.RULES_URL){b+=`
`);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<Math.max(y.length,C.length);h++){const i=(y[h]||0)-(C[h]||0);if(i!==0)return i}return 0}).map(x=>`§${x}`).join(", ");b+=`
Please review the [url=${this.RULES_URL}]Naming Guide[/url].`}return b}};
/* ========================================================================
* DARKPEERS SECTION: rows for dpk results and the title validator,
* plus the per-check toggles from Settings.
* ======================================================================== */
const DP_SECTION = [
["dpTitle", "Title Template"],
["nogroup", "NOGROUP"],
["bannedFilename", "Banned Group in Files"],
["extraneousFiles", "Extraneous Files"],
["categoryTypeMismatch", "Category / Type"],
["singleFileFolder", "Single File in Folder"],
["missingEpisodes", "Missing Episodes"],
["unknownLanguage", "Unknown Language"],
["suspicion", "Suspicion Heuristics"],
];
/** Replace every check switched off in Settings with an N/A result of the same shape. */
function applyCheckToggles(results, checks) {
if (!checks) return results;
for (const [key, on] of Object.entries(checks)) {
if (on === false && results[key]) {
results[key] = { status: "na", message: "Disabled in settings", details: null, checks: [], violations: [] };
}
}
return results;
}
/** DarkPeers rows to render: present, not N/A. */
function dpSectionRows(results) {
return DP_SECTION.filter(([k]) => results[k] && results[k].status !== "na");
}
/* Corrective message: include fails and warnings from the DarkPeers rows */
(function() {
const _collect = G.collectIssues, _beautify = G.beautify;
G.collectIssues = function(e) {
const n = _collect.call(this, e);
for (const [key, label] of (e._dpRows || [])) {
const v = e[key];
if (v && (v.status === "fail" || v.status === "warn")) n.push({ id: "dp_" + key, status: v.status, raw: v.message, label });
}
return n;
};
G.beautify = function(e) {
if (e.id && e.id.startsWith("dp_")) return { text: e.raw, rules: [] };
return _beautify.call(this, e);
};
})();
Please review the following [url]${this.RULES_URL}[/url]: ${D}.`}return b}};
/* ========================================================================
* UI — Panel rendering, injection, and event handling
@@ -4336,7 +4098,7 @@ const U={getStatusIcon(e){switch(e){case"pass":return'<i class="fas fa-check-cir
<strong>Banned Release Group Detected</strong>
<span>${e.message}</span>
</div>
</div>`:""},groupHeading(e){return`<div class="mh-group">${e}</div>`},sectionGroup(e,n,t){const s=t.map(f=>f.status).filter(f=>f!=="na"&&f!=="advisory"),a=s.filter(f=>f==="pass").length,o=s.length,c=a===o&&o>0,r=this.worstStatus(t.map(f=>f.status));return`
</div>`:""},groupHeading(e){return`<div class="mh-group">${e}</div>`},sectionGroup(e,n,t){const s=t.map(f=>f.status).filter(f=>f!=="na"),a=s.filter(f=>f==="pass").length,o=s.length,c=a===o&&o>0,r=this.worstStatus(t.map(f=>f.status));return`
<details class="mh-section" ${c?"":"open"}>
<summary class="mh-section__summary">
<span class="mh-chip mh-chip--${r==="na"?"pass":r}">${a}/${o} passed</span>
@@ -4344,7 +4106,7 @@ const U={getStatusIcon(e){switch(e){case"pass":return'<i class="fas fa-check-cir
<i class="fas fa-chevron-down mh-section__chevron"></i>
</summary>
<div class="mh-section__body">${n}</div>
</details>`},createPanel(e){const n=document.createElement("section");n.className="panelV2 mh-panel",n.id="mod-helper-panel";const s=[e.tmdbMatch,e.seasonEpisode,e.namingGuide,e.elementOrder,e.folderStructure,e.mediaInfo,e.audioTags,e.subtitleRequirement,e.screenshots,e.bannedGroup,e.encodeCompliance,e.upscaleDetection,e.containerFormat,e.packUniformity,e.resolutionTypeMatch,...(e._dpRows||[]).map(([k])=>e[k])].map(i=>i.status),a=this.worstStatus(s),o=s.filter(i=>i!=="na"&&i!=="advisory"),c=o.filter(i=>i==="pass").length,r=o.filter(i=>i==="warn").length,d=o.filter(i=>i==="fail").length,f=o.length;let A=[];if(d&&A.push(`<span class="mh-chip mh-chip--fail">${d} failed</span>`),r&&A.push(`<span class="mh-chip mh-chip--warn">${r} warning${r>1?"s":""}</span>`),A.push(`<span class="mh-chip mh-chip--pass">${c}/${f} passed</span>`),e.bannedGroup.tieredInfo){const i=e.bannedGroup.tieredInfo.join(", ");A.push(`<span class="mh-chip mh-chip--pass" title="${i}">TRaSH Tiered Group</span>`)}let T="";T+=this.buildBannedGroupAlert(e.bannedGroup);const b=[e.tmdbMatch,e.seasonEpisode,e.namingGuide,e.elementOrder,e.bannedGroup,e.screenshots];let u="";u+=this.buildSimple("tmdb","TMDB Title Match",e.tmdbMatch),e.seasonEpisode.status!=="na"&&(u+=this.buildSimple("season","Season / Episode Format",e.seasonEpisode));const D=e.bannedGroup.tieredInfo?`<span class="mh-badge mh-badge--info" title="${e.bannedGroup.tieredInfo.join(", ")}">TRaSH Tiered Group</span>`:"";u+=this.buildSimple("group","Release Group",e.bannedGroup,{alert:e.bannedGroup.alert,extraBadge:D}),u+=this.buildSimple("screenshots","Screenshots",e.screenshots),u+=this.buildElementOrder(e.elementOrder),u+=this.buildNamingGuide(e.namingGuide),T+=this.sectionGroup("Content & Naming",u,b);const x=[e.folderStructure,e.containerFormat,e.mediaInfo,e.audioTags,e.subtitleRequirement,e.encodeCompliance,e.packUniformity,e.upscaleDetection,e.resolutionTypeMatch];let p="";e.folderStructure.status!=="na"&&(p+=this.buildSimple("folder","Folder Structure",e.folderStructure)),e.containerFormat.status!=="na"&&(p+=this.buildSimple("container","Container Format",e.containerFormat)),p+=this.buildSimple("mediainfo","MediaInfo",e.mediaInfo),e.subtitleRequirement.status!=="na"&&(p+=this.buildSimple("subs","Subtitle Requirement",e.subtitleRequirement)),p+=this.buildSimple("upscale","Upscale Detection",e.upscaleDetection),p+=this.buildSimple("restype","Resolution Type",e.resolutionTypeMatch),p+=this.buildMultiCheck("audio","Audio Compliance",e.audioTags),e.encodeCompliance.status!=="na"&&(p+=this.buildMultiCheck("encode","Encode Compliance",e.encodeCompliance)),e.packUniformity.status!=="na"&&(p+=this.buildMultiCheck("pack","Pack Uniformity",e.packUniformity)),T+=this.sectionGroup("Technical",p,x);if(e._dpRows&&e._dpRows.length){const dr=e._dpRows;let dh="";for(const [dk,dl] of dr){const v=e[dk];dh+=Array.isArray(v.checks)&&v.checks.length>1?this.buildMultiCheck("dp-"+dk,dl,v):this.buildSimple("dp-"+dk,dl,v)}T+=this.sectionGroup("DarkPeers Rules",dh,dr.map(([k])=>e[k]))}const y=G.collectIssues(e),C=G.buildMessage(y);let h="";if(C){const i=C.replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;");h=`
</details>`},createPanel(e){const n=document.createElement("section");n.className="panelV2 mh-panel",n.id="mod-helper-panel";const s=[e.tmdbMatch,e.seasonEpisode,e.namingGuide,e.elementOrder,e.folderStructure,e.mediaInfo,e.audioTags,e.subtitleRequirement,e.screenshots,e.bannedGroup,e.encodeCompliance,e.upscaleDetection,e.containerFormat,e.packUniformity,e.resolutionTypeMatch].map(i=>i.status),a=this.worstStatus(s),o=s.filter(i=>i!=="na"),c=o.filter(i=>i==="pass").length,r=o.filter(i=>i==="warn").length,d=o.filter(i=>i==="fail").length,f=o.length;let A=[];if(d&&A.push(`<span class="mh-chip mh-chip--fail">${d} failed</span>`),r&&A.push(`<span class="mh-chip mh-chip--warn">${r} warning${r>1?"s":""}</span>`),A.push(`<span class="mh-chip mh-chip--pass">${c}/${f} passed</span>`),e.bannedGroup.tieredInfo){const i=e.bannedGroup.tieredInfo.join(", ");A.push(`<span class="mh-chip mh-chip--pass" title="${i}">TRaSH Tiered Group</span>`)}let T="";T+=this.buildBannedGroupAlert(e.bannedGroup);const b=[e.tmdbMatch,e.seasonEpisode,e.namingGuide,e.elementOrder,e.bannedGroup,e.screenshots];let u="";u+=this.buildSimple("tmdb","TMDB Title Match",e.tmdbMatch),e.seasonEpisode.status!=="na"&&(u+=this.buildSimple("season","Season / Episode Format",e.seasonEpisode));const D=e.bannedGroup.tieredInfo?`<span class="mh-badge mh-badge--info" title="${e.bannedGroup.tieredInfo.join(", ")}">TRaSH Tiered Group</span>`:"";u+=this.buildSimple("group","Release Group",e.bannedGroup,{alert:e.bannedGroup.alert,extraBadge:D}),u+=this.buildSimple("screenshots","Screenshots",e.screenshots),u+=this.buildElementOrder(e.elementOrder),u+=this.buildNamingGuide(e.namingGuide),T+=this.sectionGroup("Content & Naming",u,b);const x=[e.folderStructure,e.containerFormat,e.mediaInfo,e.audioTags,e.subtitleRequirement,e.encodeCompliance,e.packUniformity,e.upscaleDetection,e.resolutionTypeMatch];let p="";e.folderStructure.status!=="na"&&(p+=this.buildSimple("folder","Folder Structure",e.folderStructure)),e.containerFormat.status!=="na"&&(p+=this.buildSimple("container","Container Format",e.containerFormat)),p+=this.buildSimple("mediainfo","MediaInfo",e.mediaInfo),e.subtitleRequirement.status!=="na"&&(p+=this.buildSimple("subs","Subtitle Requirement",e.subtitleRequirement)),p+=this.buildSimple("upscale","Upscale Detection",e.upscaleDetection),p+=this.buildSimple("restype","Resolution Type",e.resolutionTypeMatch),p+=this.buildMultiCheck("audio","Audio Compliance",e.audioTags),e.encodeCompliance.status!=="na"&&(p+=this.buildMultiCheck("encode","Encode Compliance",e.encodeCompliance)),e.packUniformity.status!=="na"&&(p+=this.buildMultiCheck("pack","Pack Uniformity",e.packUniformity)),T+=this.sectionGroup("Technical",p,x);const y=G.collectIssues(e),C=G.buildMessage(y);let h="";if(C){const i=C.replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;");h=`
<div class="mh-message-block">
<div class="mh-message-block__header">
<i class="fas fa-clipboard-list mh-message-block__icon"></i>
@@ -4402,39 +4164,7 @@ const U={getStatusIcon(e){switch(e){case"pass":return'<i class="fas fa-check-cir
</div>
<div id="mh-filename-result" class="mh-filename-result"></div>
</div>
<div class="panel__body mh-body">${T}${this._buildIntegrationPlaceholders(e, e._autoSearch)}</div>`,n},injectPanel(e){const n=E.getModerationPanel();if(n)n.parentNode.insertBefore(e,n);else{const t=document.querySelector(_resolvedSelectors.torrentTags);t&&t.parentNode.insertBefore(e,t.nextSibling)}this.attachEvents(e)},attachEvents(e){const n=e.querySelector("#mh-toggle-all");if(n){let D=!1;n.addEventListener("click",()=>{D=!D,e.querySelectorAll(".mh-accordion, .mh-section").forEach(x=>x.open=D),n.querySelector("i").className=D?"fas fa-angles-up":"fas fa-angles-down",n.title=D?"Collapse all sections":"Expand all sections"})}const t=e.querySelector("#mh-corrective-text"),s=e.querySelector("#mh-corrective-editor");t&&s&&(t.addEventListener("click",()=>{t.style.display="none",s.style.display="block",s.focus()}),s.addEventListener("blur",()=>{t.textContent=s.value,t.style.display="",s.style.display=""}));const a=()=>s?s.value:"",o=e.querySelector("#mh-copy-message");o&&o.addEventListener("click",()=>{const D=a();navigator.clipboard.writeText(D).then(()=>{const x=o.querySelector("i");x.className="fas fa-check",o.classList.add("mh-btn--success"),setTimeout(()=>{x.className="fas fa-copy",o.classList.remove("mh-btn--success")},2e3)})});const c=D=>{const x=document.querySelectorAll(_resolvedSelectors.moderationForms);for(const p of x){const y=p.querySelector(_resolvedSelectors.moderationStatus);if(y&&y.value===D)return p.querySelector(_resolvedSelectors.moderationMessage)}return null},r=(D,x)=>{const p=e.querySelector(D);p&&p.addEventListener("click",()=>{const y=a(),C=c(x);if(C){C.value=y,C.dispatchEvent(new Event("input",{bubbles:!0}));const h=p.querySelector("i");h.className="fas fa-check",p.classList.add("mh-btn--success"),setTimeout(()=>{h.className="fas fa-paste",p.classList.remove("mh-btn--success")},2e3)}else{const h=p.querySelector("i");h.className="fas fa-xmark",p.classList.add("mh-btn--fail"),setTimeout(()=>{h.className="fas fa-paste",p.classList.remove("mh-btn--fail")},2e3)}})};r("#mh-fill-postpone",_resolvedModStatuses.postpone),r("#mh-fill-reject",_resolvedModStatuses.reject);const d=e.querySelector("#mh-filename-compare-btn"),f=e.querySelector("#mh-filename-block"),A=e.querySelector("#mh-filename-uploaded"),T=e.querySelector("#mh-filename-input"),b=e.querySelector("#mh-filename-run"),u=e.querySelector("#mh-filename-result");if(d&&f&&d.addEventListener("click",()=>{const D=f.style.display!=="none";if(f.style.display=D?"none":"",d.classList.toggle("mh-btn--active",!D),!D&&A){const x=E.getMediaInfoFilename()||"(not found)";A.textContent=x}}),b&&T&&u){const D=()=>{const x=T.value.trim();if(!x){u.innerHTML='<span class="mh-diff-error">Please enter a reference filename.</span>';return}const p=E.getMediaInfoFilename();if(!p||p==="(not found)"){u.innerHTML='<span class="mh-diff-error">Could not read MediaInfo filename from page.</span>';return}const y=z.diff(x,p);u.innerHTML=z.renderDiff(y)};b.addEventListener("click",D),T.addEventListener("keydown",x=>{x.key==="Enter"&&D()})}},_buildIntegrationPlaceholders(results, autoSearch){if(!results.nogroup&&!results.dpTitle)return"";const _idle=autoSearch===false;const _srrdbContent=_idle?`<div data-integration="srrdb" class="mh-integration mh-integration--na"><span class="mh-integration__icon"><i class="fas fa-minus-circle mh-icon--na"></i></span><span class="mh-integration__label">SRRDB</span><span class="mh-integration__msg">Idle — use the Search button above to check</span></div>`:`<div data-integration="srrdb" class="mh-integration mh-integration--loading"><span class="mh-integration__icon"><i class="fas fa-spinner fa-spin"></i></span><span class="mh-integration__label">SRRDB</span><span class="mh-integration__msg">Checking scene database...</span></div>`;const _prowlarrContent=_idle?`<div data-integration="prowlarr" class="mh-integration mh-integration--na"><span class="mh-integration__icon"><i class="fas fa-minus-circle mh-icon--na"></i></span><span class="mh-integration__label">Prowlarr</span><span class="mh-integration__msg">Idle — use the Search button above to check</span></div>`:`<div data-integration="prowlarr" class="mh-integration mh-integration--loading"><span class="mh-integration__icon"><i class="fas fa-spinner fa-spin"></i></span><span class="mh-integration__label">Prowlarr</span><span class="mh-integration__msg">Searching indexers...</span></div>`;const _btnLabel=_idle?"Search":"Re-search";const _btnIcon=_idle?"fa-search":"fa-rotate";return`<div class="mh-section-group"><details class="mh-section" open><summary class="mh-section__header"><i class="fas fa-plug mh-section__icon"></i><span class="mh-section__title">External Integrations</span></summary><div class="mh-section__body"><div style="margin-bottom:6px;"><button class="form__button form__button--text mh-btn mh-btn--search" id="mh-integration-refresh" title="${_btnLabel} integration searches"><i class="fas ${_btnIcon}"></i> ${_btnLabel}</button></div><div id="mh-srrdb-container">$Line truncated
// Best match title with optional link
const titleText=esc(bestMatch.title);const indexerText=esc(bestMatch.indexer);const linkHtml=bestMatch.infoUrl?` <a href="${esc(bestMatch.infoUrl)}" target="_blank" rel="noopener" title="Open on ${indexerText}" class="mh-integration__link"><i class="fas fa-external-link-alt"></i></a>`:"";
matchHtml+=`<div class="mh-integration__detail"><strong>Best match:</strong> ${titleText} <span class="mh-integration__indexer">[${indexerText}]</span>${linkHtml}</div>`;
// Summary line — dynamically built from actual field scores (D9 + D11)
const _fieldLabelsShort={titleName:"title",year:"year",resolution:"resolution",source:"source",vcodec:"video codec",acodec:"audio codec"};
const _matchedFields=Object.entries(fs).filter(([k,v])=>_fieldLabelsShort[k]&&v>=1.0).map(([k])=>_fieldLabelsShort[k]);
const _diffFields=Object.entries(fs).filter(([k,v])=>_fieldLabelsShort[k]&&v<1.0&&v!==undefined).map(([k])=>_fieldLabelsShort[k]);
let summaryText;
if(confLevel==="match"){summaryText=_matchedFields.length>0?`Name matches release on ${indexerText} — ${_matchedFields.join(", ")} consistent`:`Name matches release on ${indexerText}`}
else if(confLevel==="likely_match"){summaryText=_diffFields.length>0?`Name likely matches release on ${indexerText} — ${_diffFields.join(", ")} differ${_diffFields.length===1?"s":""}`:`Name likely matches release on ${indexerText} — minor field differences`}
else if(confLevel==="uncertain"){summaryText="No strong match found — cannot verify release name automatically"}
else if(confLevel==="likely_renamed"){summaryText=issues.length>0?`Possible rename — ${issues[0].type==="folder"?"folder name differs from torrent name":"filename differs from torrent name"}`:"Possible rename detected — review filenames"}
else if(confLevel==="renamed"){summaryText="Likely renamed — folder and filename both differ from torrent name"}
else{summaryText="Review manually"}
const summaryIcons={"match":"check-circle mh-icon--pass","likely_match":"check-circle mh-icon--pass","uncertain":"info-circle mh-icon--advisory","likely_renamed":"exclamation-triangle mh-icon--warn","renamed":"exclamation-triangle mh-icon--warn"};
const summaryClasses={"match":"pass","likely_match":"pass","uncertain":"advisory","likely_renamed":"warn","renamed":"warn"};
matchHtml+=`<div class="mh-integration__detail mh-integration__detail--${summaryClasses[confLevel]||"advisory"}"><i class="fas fa-${summaryIcons[confLevel]||"info-circle mh-icon--advisory"}"></i> ${summaryText}</div>`;
// Rename issues — reframed labels
if(confLevel==="likely_renamed"||confLevel==="renamed"){const issueItems=issues.map(i=>{if(i.type==="folder")return`<li>Torrent name: <code>${esc(i.expected)}</code><br>Folder name: <code>${esc(i.found)}</code></li>`;if(i.type==="filename")return`<li>Torrent name: <code>${esc(i.expected)}</code><br>Filename: <code>${esc(i.found)}</code></li>`;return`<li><code>${esc(i.expected)}</code> vs <code>${esc(i.found)}</code></li>`}).join("");if(issueItems)matchHtml+=`<ul class="mh-integration__diffs">${issueItems}</ul>`}
// Group note
if(conf.note&&confLevel!=="likely_renamed"&&confLevel!=="renamed"){matchHtml+=`<div class="mh-integration__detail mh-integration__detail--note"><small>${esc(conf.note)}</small></div>`}
// Collapsible field comparison detail
const fieldLabels={titleName:"Title",year:"Year",resolution:"Resolution",source:"Source",vcodec:"Video codec",acodec:"Audio codec"};const fieldEntries=Object.entries(fs).filter(([k])=>fieldLabels[k]);if(fieldEntries.length>0){let detailRows=fieldEntries.map(([k,v])=>{const label=fieldLabels[k];const icon=v>=1.0?'<i class="fas fa-check mh-icon--pass"></i>':v>0?'<i class="fas fa-minus mh-icon--warn"></i>':'<i class="fas fa-times mh-icon--fail"></i>';return`<div class="mh-field-row">${icon} <span class="mh-field-label">${label}</span></div>`}).join("");
// Self-consistency summary
const selfIssues=issues.filter(i=>i.severity==="high");const folderOk=!selfIssues.some(i=>i.type==="folder");const fileOk=!selfIssues.some(i=>i.type==="filename");detailRows+=`<div class="mh-field-row mh-field-row--self"><small>Self-check: folder ${folderOk?"✓":"✗"} · filename ${fileOk?"✓":"✗"}</small></div>`;
// Alternatives
if(alts.length>0){const _fmtAlt=a=>{const name=a.infoUrl?`<a href="${esc(a.infoUrl)}" target="_blank" rel="noopener" class="mh-integration__link">${esc(a.indexer)}</a>`:esc(a.indexer);const age=RenameDetector._relativeAge(a.publishDate);return age?`${name} <span class="mh-age">(${age})</span>`:name};const topAlts=alts.slice(0,3);const extraAlts=alts.slice(3);detailRows+=`<div class="mh-field-row mh-field-row--alts"><small>Also found on: ${topAlts.map(_fmtAlt).join(", ")}</small></div>`;if(extraAlts.length>0){detailRows+=`<details class="mh-alts-expand"><summary><small>+${extraAlts.length} more indexer${extraAlts.length>1?"s":""}</small></summary><div class="mh-alts-list">${extraAlts.map(a=>`<div class="mh-alts-item"><small>${_fmtAlt(a)}</small></div>`).join("")}</div></details>`}}
matchHtml+=`<details class="mh-integration__expand"><summary class="mh-integration__expand-trigger"><small>Comparison details</small></summary><div class="mh-integration__fields">${detailRows}</div></details>`}}
const prowlStatusMap={"match":"pass","likely_match":"pass","uncertain":"advisory","likely_renamed":"warn","renamed":"warn"};const prowlIconMap={"match":"check-circle mh-icon--pass","likely_match":"check-circle mh-icon--pass","uncertain":"info-circle mh-icon--advisory","likely_renamed":"exclamation-triangle mh-icon--warn","renamed":"exclamation-triangle mh-icon--warn"};const confLevel=bestMatch?.confidence?.level||"uncertain";const prowlStatus=prowlStatusMap[confLevel]||"advisory";const prowlIcon=prowlIconMap[confLevel]||"info-circle mh-icon--advisory";
// Header: precise indexer wording
const headerMsg=count===1?`Found on ${esc(bestMatch?.indexer||"1 indexer")}`:`Best match from ${esc(bestMatch?.indexer||"unknown")} (${count} indexers total)`;
return`<div class="mh-integration mh-integration--${prowlStatus}"><span class="mh-integration__icon"><i class="fas fa-${prowlIcon}"></i></span><span class="mh-integration__label">Prowlarr</span><span class="mh-integration__msg">${headerMsg}</span>${matchHtml}</div>`}return`<div class="mh-integration mh-integration--na"><span class="mh-integration__icon"><i class="fas fa-info-circle mh-icon--na"></i></span><span class="mh-integration__label">${esc(name)}</span><span class="mh-integration__msg">${result.found?"Found":"Not found"}</span></div>`}};
<div class="panel__body mh-body">${T}${this._buildIntegrationPlaceholders(e)}</div>`,n},injectPanel(e){const n=E.getModerationPanel();if(n)n.parentNode.insertBefore(e,n);else{const t=document.querySelector(_resolvedSelectors.torrentTags);t&&t.parentNode.insertBefore(e,t.nextSibling)}this.attachEvents(e)},attachEvents(e){const n=e.querySelector("#mh-toggle-all");if(n){let D=!1;n.addEventListener("click",()=>{D=!D,e.querySelectorAll(".mh-accordion, .mh-section").forEach(x=>x.open=D),n.querySelector("i").className=D?"fas fa-angles-up":"fas fa-angles-down",n.title=D?"Collapse all sections":"Expand all sections"})}const t=e.querySelector("#mh-corrective-text"),s=e.querySelector("#mh-corrective-editor");t&&s&&(t.addEventListener("click",()=>{t.style.display="none",s.style.display="block",s.focus()}),s.addEventListener("blur",()=>{t.textContent=s.value,t.style.display="",s.style.display=""}));const a=()=>s?s.value:"",o=e.querySelector("#mh-copy-message");o&&o.addEventListener("click",()=>{const D=a();navigator.clipboard.writeText(D).then(()=>{const x=o.querySelector("i");x.className="fas fa-check",o.classList.add("mh-btn--success"),setTimeout(()=>{x.className="fas fa-copy",o.classList.remove("mh-btn--success")},2e3)})});const c=D=>{const x=document.querySelectorAll(_resolvedSelectors.moderationForms);for(const p of x){const y=p.querySelector(_resolvedSelectors.moderationStatus);if(y&&y.value===D)return p.querySelector(_resolvedSelectors.moderationMessage)}return null},r=(D,x)=>{const p=e.querySelector(D);p&&p.addEventListener("click",()=>{const y=a(),C=c(x);if(C){C.value=y,C.dispatchEvent(new Event("input",{bubbles:!0}));const h=p.querySelector("i");h.className="fas fa-check",p.classList.add("mh-btn--success"),setTimeout(()=>{h.className="fas fa-paste",p.classList.remove("mh-btn--success")},2e3)}else{const h=p.querySelector("i");h.className="fas fa-xmark",p.classList.add("mh-btn--fail"),setTimeout(()=>{h.className="fas fa-paste",p.classList.remove("mh-btn--fail")},2e3)}})};r("#mh-fill-postpone",_resolvedModStatuses.postpone),r("#mh-fill-reject",_resolvedModStatuses.reject);const d=e.querySelector("#mh-filename-compare-btn"),f=e.querySelector("#mh-filename-block"),A=e.querySelector("#mh-filename-uploaded"),T=e.querySelector("#mh-filename-input"),b=e.querySelector("#mh-filename-run"),u=e.querySelector("#mh-filename-result");if(d&&f&&d.addEventListener("click",()=>{const D=f.style.display!=="none";if(f.style.display=D?"none":"",d.classList.toggle("mh-btn--active",!D),!D&&A){const x=E.getMediaInfoFilename()||"(not found)";A.textContent=x}}),b&&T&&u){const D=()=>{const x=T.value.trim();if(!x){u.innerHTML='<span class="mh-diff-error">Please enter a reference filename.</span>';return}const p=E.getMediaInfoFilename();if(!p||p==="(not found)"){u.innerHTML='<span class="mh-diff-error">Could not read MediaInfo filename from page.</span>';return}const y=z.diff(x,p);u.innerHTML=z.renderDiff(y)};b.addEventListener("click",D),T.addEventListener("keydown",x=>{x.key==="Enter"&&D()})}},_buildIntegrationPlaceholders(results){if(!results.nogroup&&!results.dpTitle)return"";return`<div class="mh-section-group"><details class="mh-section" open><summary class="mh-section__header"><i class="fas fa-plug mh-section__icon"></i><span class="mh-section__title">External Integrations</span></summary><div class="mh-section__body"><div data-integration="srrdb" class="mh-integration mh-integration--loading"><span class="mh-integration__icon"><i class="fas fa-spinner fa-spin"></i></span><span class="mh-integration__label">SRRDB</span><span class="mh-integration__msg">Checking scene database...</span></div><div data-integration="prowlarr" class="mh-integration mh-integration--loading"><span class="mh-integration__icon"><i class="fas fa-spinner fa-spin"></i></span><span class="mh-integration__label">Prowlarr</span><span class="mh-integration__msg">Searching indexers...</span></div></div></details></div>`},renderIntegrationResult(name,result){const esc=(s)=>String(s||"").replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;");if(result.error){return`<div class="mh-integration mh-integration--warn"><span class="mh-integration__icon"><i class="fas fa-exclamation-triangle mh-icon--warn"></i></span><span class="mh-integration__label">${esc(name)}</span><span class="mh-integration__msg">${esc(result.error)}</span></div>`}if(result.notConfigured){return`<div class="mh-integration mh-integration--na"><span class="mh-integration__icon"><i class="fas fa-minus-circle mh-icon--na"></i></span><span class="mh-integration__label">${esc(name)}</span><span class="mh-integration__msg">Not configured — open ModQ Helper Settings</span></div>`}if(name==="SRRDB"){if(!result.found){return`<div class="mh-integration mh-integration--na"><span class="mh-integration__icon"><i class="fas fa-minus-circle mh-icon--na"></i></span><span class="mh-integration__label">SRRDB</span><span class="mh-integration__msg">Not found — may not be a scene release</span></div>Line truncated
/* ========================================================================
* CSS — Panel styles (injected via GM_addStyle)
@@ -4507,9 +4237,6 @@ const Z=`
background: #2d2d2d;
color: var(--text-color, #e5e5e5);
}
.mh-btn--sm { font-size: 11px; padding: 2px 6px; margin-left: auto; }
.mh-btn--search { font-size: 12px; padding: 4px 12px; background: rgba(59,130,246,0.15); color: rgb(96,165,250); border: 1px solid rgba(59,130,246,0.3); border-radius: 4px; }
.mh-btn--search:hover { background: rgba(59,130,246,0.25); color: rgb(147,197,253); }
.mh-body {
padding: 0 !important;
@@ -5013,28 +4740,6 @@ const Z=`
.mh-integration__diffs { margin: 4px 0 0 16px; padding: 0; list-style: disc; font-size: 11px; }
.mh-integration__compare { margin-top: 4px; font-size: 11px; opacity: 0.85; }
.mh-integration__compare div { padding: 2px 0; }
.mh-integration__detail--advisory { color: rgba(96, 165, 250, 0.80); }
.mh-integration__detail--note { color: var(--text-color, #8c8c8c); }
.mh-integration__link { color: rgba(96, 165, 250, 0.70); text-decoration: none; margin-left: 4px; font-size: 11px; }
.mh-integration__link:hover { color: rgba(96, 165, 250, 1); }
.mh-integration__expand { margin-top: 6px; }
.mh-integration__expand-trigger { cursor: pointer; list-style: none; color: rgba(96, 165, 250, 0.70); user-select: none; }
.mh-integration__expand-trigger::-webkit-details-marker,
.mh-integration__expand-trigger::marker { display: none; }
.mh-integration__expand-trigger::before { content: "▸ "; }
.mh-integration__expand[open] > .mh-integration__expand-trigger::before { content: "▾ "; }
.mh-integration__fields { padding: 4px 0 0 8px; }
.mh-field-row { font-size: 11px; padding: 1px 0; color: var(--text-color, #cbd5e1); display: flex; align-items: center; gap: 4px; }
.mh-field-row i { font-size: 10px; width: 12px; text-align: center; }
.mh-field-label { min-width: 80px; }
.mh-field-row--self { margin-top: 4px; padding-top: 4px; border-top: 1px solid rgba(59, 61, 62, 0.40); color: var(--text-color, #8c8c8c); }
.mh-field-row--alts { color: var(--text-color, #8c8c8c); }
.mh-age { color: var(--text-color, #64748b); font-size: 11px; }
.mh-alts-expand { margin-top: 2px; }
.mh-alts-expand summary { cursor: pointer; color: var(--text-color, #64748b); }
.mh-alts-expand summary:hover { color: var(--text-color, #94a3b8); }
.mh-alts-list { padding-left: 4px; }
.mh-alts-item { padding: 1px 0; }
`;;
/* ========================================================================
@@ -5130,10 +4835,6 @@ function main() {
results.dpTitle = TitleValidator.validate(data.torrentName, data.category, data.type, data.mediaInfoText);
}
// Settings toggles, then the DarkPeers rows for the panel and the message
applyCheckToggles(results, Settings.load().checks);
results._dpRows = dpSectionRows(results);
// Inject CSS via GM_addStyle
if (typeof GM_addStyle === "function") {
GM_addStyle(Z);
@@ -5144,29 +4845,15 @@ function main() {
}
// Create and inject panel
const _settings = Settings.load();
const _autoSearch = _settings.prowlarr?.autoSearch !== false;
results._autoSearch = _autoSearch;
const panel = U.createPanel(results);
U.injectPanel(panel);
console.log("[ModQ Helper] Panel injected successfully");
// Async integration pipeline (DarkPeers)
// Extracted into a function so the re-search button can call it again.
const _setIntegration = (id, html) => {
const c = document.querySelector(`#mh-${id}-container`);
if (c) c.innerHTML = html;
};
const _loadingHtml = (label) => `<div class="mh-integration mh-integration--loading"><span class="mh-integration__icon"><i class="fas fa-spinner fa-spin"></i></span><span class="mh-integration__label">${label}</span><span class="mh-integration__msg">Checking...</span></div>`;
const runIntegrationSearches = async () => {
if (!dpFeatures.prowlarr && !dpFeatures.srrdb) return;
if (dpFeatures.prowlarr || dpFeatures.srrdb) {
const settings = Settings.load();
// Show loading state
_setIntegration("srrdb", _loadingHtml("SRRDB"));
_setIntegration("prowlarr", _loadingHtml("Prowlarr"));
// SRRDB: search + file comparison
if (dpFeatures.srrdb && settings.srrdb?.enabled !== false) {
(async () => {
@@ -5177,6 +4864,7 @@ function main() {
if (searchResult.found && searchResult.release) {
const filesResult = await Integrations.srrdb.getFiles(searchResult.release.release);
if (filesResult.files.length > 0 && data.fileStructure?.files?.length > 0) {
// Compare: normalize filenames for comparison
const srrdbNames = new Set(filesResult.files.map(f => (f.name || f).toLowerCase().trim()));
const localNames = data.fileStructure.files.map(f => {
const parts = f.split("/");
@@ -5185,6 +4873,7 @@ function main() {
const discrepancies = [];
for (const local of localNames) {
if (!srrdbNames.has(local)) {
// Check if it's close but renamed
const close = [...srrdbNames].find(s => s.replace(/[.\-_ ]/g, "") === local.replace(/[.\-_ ]/g, ""));
if (close) {
discrepancies.push(`"${local}" differs from SRRDB "${close}"`);
@@ -5199,13 +4888,15 @@ function main() {
} else if (filesResult.error) {
searchResult.fileCheck = { match: false, error: filesResult.error };
} else {
searchResult.fileCheck = { match: true };
searchResult.fileCheck = { match: true }; // No files to compare = no mismatch
}
}
_setIntegration("srrdb", U.renderIntegrationResult("SRRDB", searchResult));
const el = document.querySelector('[data-integration="srrdb"]');
if (el) el.outerHTML = U.renderIntegrationResult("SRRDB", searchResult);
} catch (err) {
_setIntegration("srrdb", U.renderIntegrationResult("SRRDB", { error: err.message }));
const el = document.querySelector('[data-integration="srrdb"]');
if (el) el.outerHTML = U.renderIntegrationResult("SRRDB", { error: err.message });
}
})();
}
@@ -5220,9 +4911,7 @@ function main() {
const searchResult = await Integrations.prowlarr.search(settings.prowlarr, tvScope.searchQuery);
if (searchResult.found && searchResult.results.length > 0) {
const preferredIndexers = settings.prowlarr?.preferredIndexers || [];
const ignoredIndexers = settings.prowlarr?.ignoredIndexers || [];
const bestMatch = RenameDetector.findBestMatch(uploadTokens, searchResult.results, preferredIndexers, ignoredIndexers);
const bestMatch = RenameDetector.findBestMatch(uploadTokens, searchResult.results);
if (bestMatch) {
const assessment = RenameDetector.assessRename(data, bestMatch, tvScope);
@@ -5230,36 +4919,24 @@ function main() {
title: bestMatch.title,
indexer: bestMatch.indexer,
size: bestMatch.size,
infoUrl: bestMatch.infoUrl,
uploadedTitle: data.torrentName,
relevanceScore: bestMatch.relevanceScore,
confidence: assessment,
alternatives: bestMatch.alternatives || [],
};
}
}
_setIntegration("prowlarr", U.renderIntegrationResult("Prowlarr", searchResult));
const el = document.querySelector('[data-integration="prowlarr"]');
if (el) el.outerHTML = U.renderIntegrationResult("Prowlarr", searchResult);
} catch (err) {
_setIntegration("prowlarr", U.renderIntegrationResult("Prowlarr", { error: err.message }));
const el = document.querySelector('[data-integration="prowlarr"]');
if (el) el.outerHTML = U.renderIntegrationResult("Prowlarr", { error: err.message });
}
})();
} else if (dpFeatures.prowlarr) {
_setIntegration("prowlarr", U.renderIntegrationResult("Prowlarr", { notConfigured: true }));
const el = document.querySelector('[data-integration="prowlarr"]');
if (el) el.outerHTML = U.renderIntegrationResult("Prowlarr", { notConfigured: true });
}
};
// Wire up search/re-search button
const refreshBtn = document.querySelector("#mh-integration-refresh");
if (refreshBtn) {
refreshBtn.addEventListener("click", () => {
runIntegrationSearches();
});
}
// Run initial search (only if autoSearch is enabled)
if ((dpFeatures.prowlarr || dpFeatures.srrdb) && _autoSearch) {
runIntegrationSearches();
}
} catch (err) {