NOTICE: By continued use of this site you understand and agree to the binding Terms of Service and Privacy Policy.
// ==UserScript==
// @name ModQ Helper
// @namespace https://luminarr.me/
// @version 1.2.6
// @description Quick moderation checklist for torrent uploads - provides pass/warn/fail indicators for key quality criteria
// @updateURL https://openuserjs.org/meta/SOCS/ModQ_Helper.meta.js
// @downloadURL https://openuserjs.org/src/scripts/SOCS/ModQ_Helper.user.js
// @author SOCS
// @match https://luminarr.me/torrents/*
// @grant GM_addStyle
// @run-at document-idle
// @license MIT
// ==/UserScript==
(function () {
'use strict';
const CONFIG = {
minScreenshots: 3,
validResolutions: ['480i', '480p', '576i', '576p', '720p', '1080i', '1080p', '2160p', '4320p'],
validAudioCodecs: [
'DTS-HD MA', 'DTS-HD HRA', 'DTS:X', 'DTS-ES', 'DTS', 'TrueHD',
'DD+ EX', 'DD+', 'DDP', 'DD EX', 'DD', 'E-AC-3', 'AC-3',
'LPCM', 'PCM', 'FLAC', 'ALAC', 'AAC', 'MP3', 'MP2', 'Opus', 'Vorbis'
],
validObjects: ['Atmos', 'Auro3D'],
validChannels: ['1.0', '2.0', '4.0', '5.1', '6.1', '7.1', '9.1', '11.1'],
validVideoCodecs: [
'AVC', 'HEVC', 'H.264', 'H.265', 'x264', 'x265',
'MPEG-2', 'VC-1', 'VP9', 'AV1', 'XviD', 'DivX'
],
hdrFormats: ['DV HDR10+', 'DV HDR', 'DoVi', 'HDR10+', 'HDR10', 'HDR', 'DV', 'HLG', 'PQ'],
fullDiscTypes: ['Full Disc', 'BD50', 'BD25', 'BD66', 'BD100'],
remuxTypes: ['REMUX'],
encodeTypes: ['Encode'],
webTypes: ['WEB-DL', 'WEBRip'],
hdtvTypes: ['HDTV', 'SDTV', 'UHDTV', 'PDTV', 'DSR'],
streamingServices: [
'AMZN', 'NF', 'DSNP', 'HMAX', 'ATVP', 'PCOK', 'PMTP', 'HBO', 'HULU',
'iT', 'MA', 'STAN', 'RED', 'CRAV', 'CRITERION', 'SHO', 'STARZ',
'VUDU', 'MUBI', 'BCORE', 'PLAY', 'APTV'
],
sources: {
fullDisc: ['Blu-ray', 'UHD Blu-ray', 'HD DVD', 'DVD5', 'DVD9', 'NTSC DVD', 'PAL DVD'],
remux: ['BluRay', 'UHD BluRay', 'HDDVD', 'NTSC DVD', 'PAL DVD'],
encode: ['BluRay', 'UHD BluRay', 'DVDRip', 'HDDVD', 'BDRip', 'BRRip', 'WEB-DL', 'WEBRip', 'WEB'],
web: ['WEB-DL', 'WEBRip', 'WEB'],
hdtv: ['HDTV', 'SDTV', 'UHDTV', 'PDTV', 'DSR']
},
bannedGroups: [
'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', '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'
],
exceptionGroupNames: [
'DiscoD HONE', 'DarQ HONE', 'Eml HDTeam', 'BEN THE MEN',
'D-Z0N3', 'ZØNEHD', 'Anime Time', 'Project Angel', 'Hakata Ramen', '-ZR-'
],
bracketGroupNames: [
'Silence', 'afm72', 'Panda', 'Ghost', 'MONOLITH', 'Tigole', 'Joy', 'ImE',
'UTR', 't3nzin', 'Anime Time', 'Project Angel', 'Hakata Ramen', 'HONE',
'GiLG', 'Vyndros', 'SEV', 'Garshasp', 'Kappa', 'Natty', 'RCVR', 'SAMPA',
'YOGI', 'r00t', 'EDGE2020', 'RZeroX', 'FreetheFish', 'Anna', 'Bandi',
'Qman', 'theincognito', 'HDO', 'DusIctv', 'DHD', 'CtrlHD', '-ZR-', 'ADC',
'XZVN', 'RH', 'Kametsu'
],
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',
'img.luminarr.me', 'slowpic.', 'pixhost.', 'ibb.co', 'postimg.',
'funkyimg.', 'image.tmdb.org'
],
imageExtensions: ['.jpg', '.jpeg', '.png', '.gif', '.webp'],
languageMap: {
'aa': 'Afar',
'ab': 'Abkhazian',
'ae': 'Avestan',
'af': 'Afrikaans',
'ak': 'Akan',
'am': 'Amharic',
'an': 'Aragonese',
'ar': 'Arabic',
'as': 'Assamese',
'av': 'Avaric',
'ay': 'Aymara',
'az': 'Azerbaijani',
'ba': 'Bashkir',
'be': 'Belarusian',
'bg': 'Bulgarian',
'bi': 'Bislama',
'bm': 'Bambara',
'bn': 'Bengali',
'bo': 'Tibetan',
'br': 'Breton',
'bs': 'Bosnian',
'ca': 'Catalan',
'ce': 'Chechen',
'ch': 'Chamorro',
'cn': 'Cantonese',
'co': 'Corsican',
'cr': 'Cree',
'cs': 'Czech',
'cu': 'Slavic',
'cv': 'Chuvash',
'cy': 'Welsh',
'da': 'Danish',
'de': 'German',
'dv': 'Divehi',
'dz': 'Dzongkha',
'ee': 'Ewe',
'el': 'Greek',
'en': 'English',
'eo': 'Esperanto',
'es': 'Spanish',
'et': 'Estonian',
'eu': 'Basque',
'fa': 'Persian',
'ff': 'Fulah',
'fi': 'Finnish',
'fj': 'Fijian',
'fo': 'Faroese',
'fr': 'French',
'fy': 'Frisian',
'ga': 'Irish',
'gd': 'Gaelic',
'gl': 'Galician',
'gn': 'Guarani',
'gu': 'Gujarati',
'gv': 'Manx',
'ha': 'Hausa',
'he': 'Hebrew',
'hi': 'Hindi',
'ho': 'Hiri Motu',
'hr': 'Croatian',
'ht': 'Haitian',
'hu': 'Hungarian',
'hy': 'Armenian',
'hz': 'Herero',
'ia': 'Interlingua',
'id': 'Indonesian',
'ie': 'Interlingue',
'ig': 'Igbo',
'ii': 'Yi',
'ik': 'Inupiaq',
'io': 'Ido',
'is': 'Icelandic',
'it': 'Italian',
'iu': 'Inuktitut',
'ja': 'Japanese',
'jv': 'Javanese',
'ka': 'Georgian',
'kg': 'Kongo',
'ki': 'Kikuyu',
'kj': 'Kuanyama',
'kk': 'Kazakh',
'kl': 'Kalaallisut',
'km': 'Khmer',
'kn': 'Kannada',
'ko': 'Korean',
'kr': 'Kanuri',
'ks': 'Kashmiri',
'ku': 'Kurdish',
'kv': 'Komi',
'kw': 'Cornish',
'ky': 'Kirghiz',
'la': 'Latin',
'lb': 'Letzeburgesch',
'lg': 'Ganda',
'li': 'Limburgish',
'ln': 'Lingala',
'lo': 'Lao',
'lt': 'Lithuanian',
'lu': 'Luba-Katanga',
'lv': 'Latvian',
'mg': 'Malagasy',
'mh': 'Marshall',
'mi': 'Maori',
'mk': 'Macedonian',
'ml': 'Malayalam',
'mn': 'Mongolian',
'mo': 'Moldavian',
'mr': 'Marathi',
'ms': 'Malay',
'mt': 'Maltese',
'my': 'Burmese',
'na': 'Nauru',
'nb': 'Norwegian Bokmål',
'nd': 'Ndebele',
'ne': 'Nepali',
'ng': 'Ndonga',
'nl': 'Dutch',
'nn': 'Norwegian Nynorsk',
'no': 'Norwegian',
'nr': 'Ndebele',
'nv': 'Navajo',
'ny': 'Chichewa',
'oc': 'Occitan',
'oj': 'Ojibwa',
'om': 'Oromo',
'or': 'Oriya',
'os': 'Ossetian',
'pa': 'Punjabi',
'pi': 'Pali',
'pl': 'Polish',
'ps': 'Pushto',
'pt': 'Portuguese',
'qu': 'Quechua',
'rm': 'Raeto-Romance',
'rn': 'Rundi',
'ro': 'Romanian',
'ru': 'Russian',
'rw': 'Kinyarwanda',
'sa': 'Sanskrit',
'sc': 'Sardinian',
'sd': 'Sindhi',
'se': 'Northern Sami',
'sg': 'Sango',
'sh': 'Serbo-Croatian',
'si': 'Sinhalese',
'sk': 'Slovak',
'sl': 'Slovenian',
'sm': 'Samoan',
'sn': 'Shona',
'so': 'Somali',
'sq': 'Albanian',
'sr': 'Serbian',
'ss': 'Swati',
'st': 'Sotho',
'su': 'Sundanese',
'sv': 'Swedish',
'sw': 'Swahili',
'ta': 'Tamil',
'te': 'Telugu',
'tg': 'Tajik',
'th': 'Thai',
'ti': 'Tigrinya',
'tk': 'Turkmen',
'tl': 'Tagalog',
'tn': 'Tswana',
'to': 'Tonga',
'tr': 'Turkish',
'ts': 'Tsonga',
'tt': 'Tatar',
'tw': 'Twi',
'ty': 'Tahitian',
'ug': 'Uighur',
'uk': 'Ukrainian',
'ur': 'Urdu',
'uz': 'Uzbek',
've': 'Venda',
'vi': 'Vietnamese',
'vo': 'Volapük',
'wa': 'Walloon',
'wo': 'Wolof',
'xh': 'Xhosa',
'xx': 'No Language',
'yi': 'Yiddish',
'yo': 'Yoruba',
'za': 'Zhuang',
'zh': 'Mandarin',
'zu': 'Zulu'
},
// Aliases for matching TMDB language names against MediaInfo language names
// Muxes often uses different names (e.g. "Chinese" instead of "Mandarin")
languageAliases: {
'mandarin': ['chinese'],
'cantonese': ['chinese'],
'norwegian bokmål': ['norwegian'],
'norwegian nynorsk': ['norwegian'],
'moldavian': ['romanian'],
'letzeburgesch': ['luxembourgish'],
'sinhalese': ['sinhala'],
'pushto': ['pashto'],
'raeto-romance': ['romansh'],
'slavic': ['church slavic'],
'frisian': ['western frisian'],
'filipino': ['tagalog'],
'tagalog': ['filipino'],
'persian': ['farsi'],
'farsi': ['persian'],
'burmese': ['myanmar'],
'myanmar': ['burmese'],
'limburgish': ['dutch']
},
titleElementOrder: {
fullDiscRemux: [
'name', 'aka', 'locale', 'year', 'season', 'cut', 'ratio',
'repack', 'resolution', 'edition', 'region', '3d', 'source',
'type', 'hdr', 'vcodec', 'dub', 'acodec', 'channels', 'object', 'group'
],
encodeWeb: [
'name', 'aka', 'locale', 'year', 'season', 'cut', 'ratio',
'repack', 'resolution', 'edition', '3d', 'source', 'type',
'dub', 'acodec', 'channels', 'object', 'hdr', 'vcodec', 'group'
]
},
cuts: ['Theatrical', 'Director\'s Cut', 'Extended', 'Extended Cut', 'Extended Edition',
'Special Edition', 'Unrated', 'Unrated Director\'s Cut', 'Uncut', 'Super Duper Cut',
'Ultimate Cut', 'Ultimate Edition', 'Final Cut', 'Producer\'s Cut', 'Assembly Cut',
'International Cut', 'Redux', 'Rough Cut', 'Bootleg Cut', 'Criterion', 'Criterion Cut',
'Workprint', 'Hybrid Cut'
],
ratios: ['IMAX', 'Open Matte', 'MAR'],
editions: ['Anniversary Edition', 'Remastered', '4K Remaster', 'Criterion Collection',
'Limited', 'Collector\'s Edition', 'Deluxe Edition', 'Restored'
],
repacks: ['REPACK', 'REPACK2', 'REPACK3', 'PROPER', 'RERIP'],
dubs: ['Multi', 'Dual-Audio', 'Dual Audio', 'Dubbed']
};
const DataExtractor = {
getTorrentName() {
const el = document.querySelector('h1.torrent__name');
return el ? el.textContent.trim() : null;
},
getTmdbTitle() {
const el = document.querySelector('h1.meta__title');
if (!el) return null;
const text = el.textContent.trim();
const match = text.match(/^(.+?)\s*\(\d{4}\)\s*$/);
return match ? match[1].trim() : text;
},
getTmdbYear() {
const el = document.querySelector('h1.meta__title');
if (!el) return null;
const text = el.textContent.trim();
const match = text.match(/\((\d{4})\)\s*$/);
return match ? match[1] : null;
},
getCategory() {
const el = document.querySelector('li.torrent__category a');
return el ? el.textContent.trim() : null;
},
getType() {
const el = document.querySelector('li.torrent__type a');
return el ? el.textContent.trim() : null;
},
getResolution() {
const el = document.querySelector('li.torrent__resolution a');
return el ? el.textContent.trim() : null;
},
getDescription() {
const panels = document.querySelectorAll('section.panelV2');
for (const panel of panels) {
const heading = panel.querySelector('.panel__heading');
if (heading && heading.textContent.includes('Description')) {
const body = panel.querySelector('.panel__body.bbcode-rendered');
return body ? body.innerHTML : '';
}
}
return '';
},
getFileStructure() {
const dialogForms = document.querySelectorAll('.dialog__form[data-tab="hierarchy"]');
for (const form of dialogForms) {
const folderIcon = form.querySelector('i.fas.fa-folder');
if (folderIcon) {
const folderSpan = folderIcon.parentElement;
if (folderSpan) {
const folderNameEl = folderSpan.querySelector('span[style*="word-break"]');
const folderName = folderNameEl ? folderNameEl.textContent.trim() : null;
const countEl = folderSpan.querySelector('span[style*="grid-area: count"]');
const countMatch = countEl ? countEl.textContent.match(/\((\d+)\)/) : null;
const fileCount = countMatch ? parseInt(countMatch[1], 10) : 0;
const files = [];
const fileElements = form.querySelectorAll('details i.fas.fa-file');
fileElements.forEach(fileIcon => {
const fileSpan = fileIcon.parentElement;
const fileNameEl = fileSpan?.querySelector('span[style*="word-break"]');
if (fileNameEl) {
files.push(fileNameEl.textContent.trim());
}
});
return {
hasFolder: true,
folderName: folderName,
fileCount: fileCount,
files: files
};
}
}
const topLevelFile = form.querySelector(':scope > details > summary i.fas.fa-file');
if (topLevelFile) {
const fileSpan = topLevelFile.parentElement;
const fileNameEl = fileSpan?.querySelector('span[style*="word-break"]');
return {
hasFolder: false,
folderName: null,
fileCount: 1,
files: fileNameEl ? [fileNameEl.textContent.trim()] : []
};
}
}
const listTable = document.querySelector('.dialog__form[data-tab="list"] table.data-table tbody');
if (listTable) {
const rows = listTable.querySelectorAll('tr');
const files = [];
rows.forEach(row => {
const nameCell = row.querySelector('td:nth-child(2)');
if (nameCell) {
files.push(nameCell.textContent.trim());
}
});
if (files.length > 0 && files[0].includes('/')) {
const parts = files[0].split('/');
return {
hasFolder: true,
folderName: parts[0],
fileCount: files.length,
files: files
};
}
return {
hasFolder: false,
folderName: null,
fileCount: files.length,
files: files
};
}
return null;
},
hasMediaInfo() {
const panels = document.querySelectorAll('div.panelV2, section.panelV2');
for (const panel of panels) {
const heading = panel.querySelector('.panel__heading');
if (heading && heading.textContent.includes('MediaInfo')) {
return true;
}
}
return false;
},
hasBdInfo() {
const panels = document.querySelectorAll('div.panelV2, section.panelV2');
for (const panel of panels) {
const heading = panel.querySelector('.panel__heading');
if (heading && heading.textContent.includes('BDInfo')) {
return true;
}
}
return false;
},
isTV() {
const category = this.getCategory();
if (!category) return false;
return category.toLowerCase().includes('tv') ||
category.toLowerCase().includes('series') ||
category.toLowerCase().includes('episode');
},
getOriginalLanguage() {
const el = document.querySelector('.work__language-link');
return el ? el.textContent.trim().toLowerCase() : null;
},
getMediaInfoLanguages() {
const languages = new Set();
const mediaInfoText = this.getMediaInfoText();
if (mediaInfoText) {
const sections = mediaInfoText.split(/\n(?=Audio(?: #\d+)?[\r\n])/);
for (const section of sections) {
if (!/^Audio(?:\s|$)/m.test(section)) continue;
const lines = section.split('\n');
let lang = null;
let isCommentary = false;
for (const line of lines) {
if (/^(Video|Text|Menu|General|Chapter)/.test(line.trim())) break;
const langMatch = line.match(/^Language\s*:\s*(.+)$/);
if (langMatch) {
lang = langMatch[1].trim();
}
const titleMatch = line.match(/^Title\s*:\s*(.+)$/);
if (titleMatch && /commentary/i.test(titleMatch[1])) {
isCommentary = true;
}
}
if (lang && !isCommentary) {
languages.add(lang);
}
}
}
if (languages.size === 0) {
const flagImgs = document.querySelectorAll('.mediainfo__audio dl dd img');
flagImgs.forEach(img => {
if (img.alt) languages.add(img.alt.trim());
});
}
return Array.from(languages);
},
getMediaInfoText() {
const el = document.querySelector('.torrent-mediainfo-dump code, code[x-ref="mediainfo"]');
return el ? el.textContent : '';
},
getMediaInfoFilename() {
const el = document.querySelector('section.mediainfo__filename, .mediainfo__filename');
if (el) return el.textContent.trim();
const mediaInfoText = this.getMediaInfoText();
if (mediaInfoText) {
const match = mediaInfoText.match(/^Complete name\s*:\s*(.+)$/m);
if (match) {
const fullPath = match[1].trim();
const parts = fullPath.split(/[/\\]/);
return parts[parts.length - 1];
}
}
return null;
},
getMediaInfoSubtitles() {
const subtitles = new Set();
const subtitleImgs = document.querySelectorAll('.mediainfo__subtitles ul li img');
subtitleImgs.forEach(img => {
if (img.alt) subtitles.add(img.alt.trim());
});
if (subtitles.size === 0) {
const mediaInfoText = this.getMediaInfoText();
if (mediaInfoText) {
const sections = mediaInfoText.split(/\n(?=Text(?: #\d+)?[\r\n])/);
for (const section of sections) {
if (!/^Text(?:\s|$)/m.test(section)) continue;
const lines = section.split('\n');
for (const line of lines) {
if (/^(Video|Audio|Menu|General|Chapter)/.test(line.trim())) break;
const langMatch = line.match(/^Language\s*:\s*(.+)$/);
if (langMatch) {
subtitles.add(langMatch[1].trim());
break;
}
}
}
}
}
return Array.from(subtitles);
},
getAudioTracksFromMediaInfo() {
const tracks = [];
const mediaInfoText = this.getMediaInfoText();
if (!mediaInfoText) return tracks;
const sections = mediaInfoText.split(/\n(?=Audio(?: #\d+)?[\r\n])/);
for (const section of sections) {
if (!/^Audio(?:\s|$)/m.test(section)) continue;
const track = {
codec: null,
channels: null,
language: null,
title: null,
isDefault: false
};
const lines = section.split('\n');
for (const line of lines) {
if (/^(Video|Text|Menu|General|Chapter)/.test(line.trim())) break;
const formatMatch = line.match(/^Format\s*:\s*(.+)$/);
if (formatMatch && !track.codec) {
track.codec = formatMatch[1].trim();
}
const commercialMatch = line.match(/^Commercial name\s*:\s*(.+)$/);
if (commercialMatch) {
track.commercialName = commercialMatch[1].trim();
}
const channelMatch = line.match(/^Channel\(s\)\s*:\s*(\d+)/);
if (channelMatch) {
const ch = parseInt(channelMatch[1], 10);
if (ch === 1) track.channels = '1.0';
else if (ch === 2) track.channels = '2.0';
else if (ch === 6) track.channels = '5.1';
else if (ch === 7) track.channels = '6.1';
else if (ch === 8) track.channels = '7.1';
else track.channels = `${ch}ch`;
}
const langMatch = line.match(/^Language\s*:\s*(.+)$/);
if (langMatch) {
track.language = langMatch[1].trim();
}
const titleMatch = line.match(/^Title\s*:\s*(.+)$/);
if (titleMatch) {
track.title = titleMatch[1].trim();
}
const defaultMatch = line.match(/^Default\s*:\s*(.+)$/);
if (defaultMatch) {
track.isDefault = defaultMatch[1].trim().toLowerCase() === 'yes';
}
}
if (track.codec) {
tracks.push(track);
}
}
return tracks;
},
getHdrFromMediaInfo() {
const hdrElements = document.querySelectorAll('.mediainfo__video dt');
for (const dt of hdrElements) {
if (dt.textContent.trim() === 'HDR') {
const dd = dt.nextElementSibling;
if (dd && dd.tagName === 'DD') {
const hdrText = dd.textContent.trim();
if (hdrText && hdrText !== 'Unknown') {
return this.parseHdrFormats(hdrText);
}
}
}
}
const mediaInfoText = this.getMediaInfoText();
if (!mediaInfoText) return [];
const hdrFormatMatch = mediaInfoText.match(/HDR format\s*:\s*(.+?)(?:\n|$)/i);
if (hdrFormatMatch) {
return this.parseHdrFormats(hdrFormatMatch[1]);
}
return [];
},
parseHdrFormats(hdrText) {
const formats = [];
const text = hdrText.toLowerCase();
if (text.includes('dolby vision') || text.includes('dvhe')) {
if (text.includes('profile 5') || text.includes('dvhe.05')) {
formats.push('DV5');
}
else if (text.includes('profile 7') || text.includes('dvhe.07')) {
formats.push('DV7');
}
else if (text.includes('profile 8') || text.includes('dvhe.08')) {
formats.push('DV8');
}
else {
formats.push('DV');
}
}
if (text.includes('hdr10+') || text.includes('hdr10 plus') || text.includes('smpte st 2094')) {
formats.push('HDR10+');
}
else if (text.includes('hdr10') || text.includes('smpte st 2086')) {
formats.push('HDR10');
}
else if (text.includes('hdr') && !text.includes('dolby vision')) {
formats.push('HDR');
}
if (text.includes('hlg')) {
formats.push('HLG');
}
if (text.includes('pq') && !formats.length) {
formats.push('PQ10');
}
return formats;
},
getModerationPanel() {
const panels = document.querySelectorAll('section.panelV2');
for (const panel of panels) {
const heading = panel.querySelector('.panel__heading');
if (heading && heading.textContent.includes('Moderation')) {
return panel;
}
}
return null;
}
};
const Helpers = {
extractReleaseGroup(name) {
if (!name) return null;
// Strip obfuscation/usenet suffixes before parsing
let cleaned = name.replace(CONFIG.releaseGroupSuffixes, '');
// Check exception groups (spaces, hyphens, non-ASCII in name)
const exceptionGroup = CONFIG.exceptionGroupNames.find(
g => cleaned.endsWith('-' + g) || cleaned.endsWith('- ' + g)
);
if (exceptionGroup) return exceptionGroup;
// Check bracket/paren-wrapped groups: Title(Group) or Title[Group]
const bracketMatch = cleaned.match(/[(\[]([^\]()]+)[)\]]$/);
if (bracketMatch) {
const candidate = bracketMatch[1];
const isBracketGroup = CONFIG.bracketGroupNames.some(
g => g.toLowerCase() === candidate.toLowerCase()
);
if (isBracketGroup) return candidate;
}
// Standard -GROUP format
const match = cleaned.match(/-([A-Za-z0-9$!._&+\$]+)$/i);
return match ? match[1] : null;
},
extractYear(name) {
if (!name) return null;
const match = name.match(/\b(19|20)\d{2}\b/);
return match ? match[0] : null;
},
countScreenshots(descriptionHtml) {
if (!descriptionHtml) return {
count: 0,
urls: []
};
const urls = [];
const bbcodePattern = /\[img\](.*?)\[\/img\]/gi;
let match;
while ((match = bbcodePattern.exec(descriptionHtml)) !== null) {
urls.push(match[1]);
}
const htmlPattern = /<img[^>]+src=["']([^"']+)["']/gi;
while ((match = htmlPattern.exec(descriptionHtml)) !== null) {
urls.push(match[1]);
}
const validUrls = urls.filter(url => {
const lowerUrl = url.toLowerCase();
const hasValidExtension = CONFIG.imageExtensions.some(ext => lowerUrl.includes(ext));
const isKnownHost = CONFIG.imageHosts.some(host => lowerUrl.includes(host));
const isTmdbMeta = lowerUrl.includes('image.tmdb.org') &&
(lowerUrl.includes('/w342/') || lowerUrl.includes('/w500/') ||
lowerUrl.includes('/w1280/') || lowerUrl.includes('/w138'));
return (hasValidExtension || isKnownHost) && !isTmdbMeta;
});
const uniqueUrls = [...new Set(validUrls)];
return {
count: uniqueUrls.length,
urls: uniqueUrls
};
},
parseSeasonEpisode(name) {
if (!name) return {
season: null,
episode: null,
raw: null,
isSeasonPack: false
};
const fullMatch = name.match(/S(\d{1,2})E(\d{1,2})/i);
if (fullMatch) {
return {
season: parseInt(fullMatch[1], 10),
episode: parseInt(fullMatch[2], 10),
raw: fullMatch[0],
isSeasonPack: false
};
}
const seasonMatch = name.match(/\bS(\d{1,2})\b(?!E)/i);
if (seasonMatch) {
return {
season: parseInt(seasonMatch[1], 10),
episode: null,
raw: seasonMatch[0],
isSeasonPack: true
};
}
return {
season: null,
episode: null,
raw: null,
isSeasonPack: false
};
},
normalizeForComparison(str) {
if (!str) return '';
return str.toLowerCase()
.replace(/['']/g, "'")
.replace(/[""]/g, '"')
.replace(/[–—]/g, '-')
.replace(/\s+/g, ' ')
.trim();
},
normalizeForComparisonPreserveCase(str) {
if (!str) return '';
return str
.replace(/['']/g, "'")
.replace(/[""]/g, '"')
.replace(/[–—]/g, '-')
.replace(/\s+/g, ' ')
.trim();
},
detectAudioObject(mediaInfoText) {
if (!mediaInfoText) return null;
if (/(Dolby\s?Atmos|E-AC-3\s?JOC)/i.test(mediaInfoText)) return 'Atmos';
if (/(Auro\s?3D)/i.test(mediaInfoText)) return 'Auro3D';
return null;
},
extractTitleElements(torrentName, type) {
if (!torrentName) return {
elements: [],
positions: {}
};
const elements = [];
const positions = {};
const name = torrentName;
const recordElement = (elementType, match, index) => {
if (match !== null && index !== -1) {
elements.push({
type: elementType,
value: match,
position: index
});
positions[elementType] = index;
}
};
const yearMatch = name.match(/\b(19|20)\d{2}\b/);
if (yearMatch) {
recordElement('year', yearMatch[0], yearMatch.index);
}
const seasonMatch = name.match(/\bS(\d{1,2})(?:E(\d{1,2}))?\b/i);
if (seasonMatch) {
recordElement('season', seasonMatch[0], seasonMatch.index);
}
for (const res of CONFIG.validResolutions) {
const idx = name.indexOf(res);
if (idx !== -1) {
recordElement('resolution', res, idx);
break;
}
}
const sortedHdr = [...CONFIG.hdrFormats].sort((a, b) => b.length - a.length);
for (const hdr of sortedHdr) {
const regex = new RegExp('\\b' + hdr.replace(/[+]/g, '\\+') + '\\b', 'i');
const match = name.match(regex);
if (match) {
recordElement('hdr', match[0], match.index);
break;
}
}
const sortedVideoCodecs = [...CONFIG.validVideoCodecs].sort((a, b) => b.length - a.length);
for (const codec of sortedVideoCodecs) {
const regex = new RegExp(codec.replace(/[.]/g, '\\.?'), 'i');
const match = name.match(regex);
if (match) {
recordElement('vcodec', match[0], match.index);
break;
}
}
const sortedAudioCodecs = [...CONFIG.validAudioCodecs].sort((a, b) => b.length - a.length);
for (const codec of sortedAudioCodecs) {
const escaped = codec.replace(/[+]/g, '\\+').replace(/[-.]/g, '[-.]?');
const regex = new RegExp('(?<![a-zA-Z])' + escaped + '(?![a-zA-Z])', 'i');
const match = name.match(regex);
if (match) {
recordElement('acodec', match[0], match.index);
break;
}
}
const channelMatch = name.match(/\b(\d{1,2}\.\d)\b/);
if (channelMatch) {
recordElement('channels', channelMatch[0], channelMatch.index);
}
const atmosMatch = name.match(/\bAtmos\b/i);
const auroMatch = name.match(/\bAuro(?:3D)?\b/i);
if (atmosMatch) {
recordElement('object', atmosMatch[0], atmosMatch.index);
}
else if (auroMatch) {
recordElement('object', auroMatch[0], auroMatch.index);
}
const allSources = [
...CONFIG.sources.fullDisc,
...CONFIG.sources.remux,
...CONFIG.sources.encode,
...CONFIG.sources.web,
...CONFIG.sources.hdtv
];
const uniqueSources = [...new Set(allSources)].sort((a, b) => b.length - a.length);
for (const source of uniqueSources) {
const regex = new RegExp(source.replace(/[-.]/g, '[-. ]?'), 'i');
const match = name.match(regex);
if (match) {
recordElement('source', match[0], match.index);
break;
}
}
const typeMatch = name.match(/\b(REMUX|WEB-DL|WEBRip)\b/i);
if (typeMatch) {
recordElement('type', typeMatch[0], typeMatch.index);
}
for (const dub of CONFIG.dubs) {
const regex = new RegExp(`\\b${dub.replace('-', '[-]?')}\\b`, 'i');
const match = name.match(regex);
if (match) {
recordElement('dub', match[0], match.index);
break;
}
}
for (const cut of CONFIG.cuts) {
const regex = new RegExp(cut.replace(/'/g, "[']?"), 'i');
const match = name.match(regex);
if (match) {
recordElement('cut', match[0], match.index);
break;
}
}
for (const ratio of CONFIG.ratios) {
const regex = new RegExp(` ${ratio} `, 'i');
const match = name.match(regex);
if (match) {
recordElement('ratio', ratio, match.index + 1);
break;
}
}
for (const repack of CONFIG.repacks) {
const regex = new RegExp(`\\b${repack}\\b`, 'i');
const match = name.match(regex);
if (match) {
recordElement('repack', match[0], match.index);
break;
}
}
for (const edition of CONFIG.editions) {
const regex = new RegExp(edition.replace(/'/g, "[']?"), 'i');
const match = name.match(regex);
if (match) {
recordElement('edition', match[0], match.index);
break;
}
}
const match3d = name.match(/\b3D\b/);
if (match3d) {
recordElement('3d', '3D', match3d.index);
}
const groupMatch = name.match(/-([A-Za-z0-9$!._&+\$]+)$/i);
if (groupMatch) {
recordElement('group', groupMatch[1], groupMatch.index);
}
elements.sort((a, b) => a.position - b.position);
return {
elements,
positions
};
}
};
const Checks = {
tmdbNameMatch(torrentName, tmdbTitle) {
if (!tmdbTitle) {
return {
status: 'warn',
message: 'TMDB title not found on page',
details: null
};
}
if (!torrentName) {
return {
status: 'fail',
message: 'Torrent name not found',
details: null
};
}
const normalizedTorrent = Helpers.normalizeForComparison(torrentName);
const normalizedTmdb = Helpers.normalizeForComparison(tmdbTitle);
const caseTorrent = Helpers.normalizeForComparisonPreserveCase(torrentName);
const caseTmdb = Helpers.normalizeForComparisonPreserveCase(tmdbTitle);
if (normalizedTorrent.startsWith(normalizedTmdb)) {
if (!caseTorrent.startsWith(caseTmdb)) {
return {
status: 'warn',
message: `"${tmdbTitle}" found but capitalization differs`,
details: {
expected: tmdbTitle,
found: torrentName.substring(0, tmdbTitle.length)
}
};
}
return {
status: 'pass',
message: `"${tmdbTitle}" found at start of title`,
details: null
};
}
if (normalizedTmdb.startsWith('the ') &&
normalizedTorrent.startsWith(normalizedTmdb.substring(4))) {
return {
status: 'warn',
message: `"${tmdbTitle}" found (without "The" prefix)`,
details: null
};
}
const akaMatch = normalizedTorrent.match(/^(.+?)\s+aka\s+/i);
if (akaMatch) {
const beforeAka = akaMatch[1].trim();
if (beforeAka === normalizedTmdb ||
beforeAka === 'the ' + normalizedTmdb ||
(normalizedTmdb.startsWith('the ') && beforeAka === normalizedTmdb.substring(4))) {
const caseAkaMatch = caseTorrent.match(/^(.+?)\s+AKA\s+/i);
const caseBeforeAka = caseAkaMatch ? caseAkaMatch[1].trim() : '';
if (caseBeforeAka !== caseTmdb &&
caseBeforeAka !== 'The ' + caseTmdb &&
!(caseTmdb.startsWith('The ') && caseBeforeAka === caseTmdb.substring(4))) {
return {
status: 'warn',
message: `"${tmdbTitle}" found (AKA format) but capitalization differs`,
details: {
expected: tmdbTitle,
found: caseBeforeAka
}
};
}
return {
status: 'pass',
message: `"${tmdbTitle}" found (AKA format)`,
details: null
};
}
}
return {
status: 'fail',
message: `Title should start with "${tmdbTitle}"`,
details: {
expected: tmdbTitle,
found: torrentName.substring(0, Math.min(50, torrentName.length)) + (torrentName.length > 50 ? '...' : '')
}
};
},
movieFolderStructure(fileStructure, category, isTV, type) {
const isFullDisc = CONFIG.fullDiscTypes.some(t => type?.includes(t));
if (isFullDisc) {
return {
status: 'na',
message: 'N/A - Full Disc (folder structure expected)',
details: null
};
}
if (isTV) {
return {
status: 'na',
message: 'N/A - Folder structure check not applicable for TV',
details: null
};
}
const isMovie = category?.toLowerCase().includes('movie');
if (!isMovie) {
return {
status: 'na',
message: 'N/A - Not a movie',
details: null
};
}
if (!fileStructure) {
return {
status: 'warn',
message: 'Could not determine file structure',
details: null
};
}
if (fileStructure.hasFolder) {
if (fileStructure.fileCount === 1) {
return {
status: 'fail',
message: 'Movie should not have a top-level folder',
details: {
found: `${fileStructure.folderName}/${fileStructure.files[0] || '...'}`,
expected: fileStructure.files[0] || 'Single file without folder wrapper'
}
};
}
return {
status: 'warn',
message: `Movie has folder with ${fileStructure.fileCount} files`,
details: {
folder: fileStructure.folderName,
fileCount: fileStructure.fileCount
}
};
}
return {
status: 'pass',
message: 'File structure correct (no folder wrapper)',
details: null
};
},
seasonEpisodeFormat(torrentName, isTV) {
if (!isTV) {
return {
status: 'na',
message: 'N/A - Not TV content',
details: null
};
}
const parsed = Helpers.parseSeasonEpisode(torrentName);
const fullMatch = torrentName.match(/S(\d{2,})E(\d{2,})/i);
const seasonOnlyMatch = torrentName.match(/\bS(\d{2,})\b(?!E)/i);
if (fullMatch) {
return {
status: 'pass',
message: `Episode format correct: S${fullMatch[1]}E${fullMatch[2]}`,
details: null
};
}
if (seasonOnlyMatch) {
return {
status: 'pass',
message: `Season pack format correct: S${seasonOnlyMatch[1]}`,
details: null
};
}
const badFullFormat = torrentName.match(/S(\d)E(\d)(?!\d)/i);
const badSeasonFormat = torrentName.match(/\bS(\d)\b(?!E|\d)/i);
if (badFullFormat) {
return {
status: 'fail',
message: `Season/Episode must be zero-padded: found S${badFullFormat[1]}E${badFullFormat[2]}, expected S0${badFullFormat[1]}E0${badFullFormat[2]}`,
details: null
};
}
if (badSeasonFormat) {
return {
status: 'fail',
message: `Season must be zero-padded: found S${badSeasonFormat[1]}, expected S0${badSeasonFormat[1]}`,
details: null
};
}
return {
status: 'fail',
message: 'No S##E## or S## format found in TV content title',
details: null
};
},
namingGuideCompliance(torrentName, type, mediaInfoText) {
const results = {
status: 'pass',
checks: []
};
const name = torrentName || '';
const isTV = DataExtractor.isTV();
const yearMatch = Helpers.extractYear(name);
let yearStatus = 'fail';
let yearMessage = 'No year found';
if (yearMatch) {
if (name.includes(`(${yearMatch})`)) {
yearStatus = 'warn';
yearMessage = `Found: (${yearMatch}) - Remove parentheses`;
}
else {
yearStatus = 'pass';
yearMessage = `Found: ${yearMatch}`;
}
}
else {
if (isTV) {
yearStatus = 'pass';
yearMessage = 'No year found (Optional for TV)';
}
else {
yearStatus = 'fail';
yearMessage = 'No year found (Required for Movies)';
}
}
results.checks.push({
name: 'Year',
status: yearStatus,
message: yearMessage,
required: !isTV
});
const resMatch = CONFIG.validResolutions.find(r => name.includes(r));
const isDvdSource = /\b(NTSC|PAL)\b/i.test(name);
const resFound = resMatch || isDvdSource;
const resLabel = resMatch ? resMatch : (isDvdSource ? name.match(/\b(NTSC|PAL)\b/i)[1] : null);
results.checks.push({
name: 'Resolution',
status: resFound ? 'pass' : 'fail',
message: resFound ? `Found: ${resLabel}` : 'No valid resolution found',
required: true
});
const sortedCodecs = [...CONFIG.validAudioCodecs].sort((a, b) => b.length - a.length);
let audioMatch = null;
for (const codec of sortedCodecs) {
const escaped = codec.replace(/[+]/g, '\\+').replace(/[-.]/g, '[-.]?');
const regex = new RegExp('(?<![a-zA-Z])' + escaped + '(?![a-zA-Z])', 'i');
if (regex.test(name)) {
audioMatch = codec;
break;
}
}
results.checks.push({
name: 'Audio Codec',
status: audioMatch ? 'pass' : 'fail',
message: audioMatch ? `Found: ${audioMatch}` : 'No audio codec found',
required: true
});
const channelMatch = name.match(/\b(\d{1,2}\.\d)\b/);
results.checks.push({
name: 'Channels',
status: channelMatch ? 'pass' : 'fail',
message: channelMatch ? `Found: ${channelMatch[1]}` : 'No channel config found (e.g., 5.1)',
required: true
});
const groupMatch = Helpers.extractReleaseGroup(name);
results.checks.push({
name: 'Release Group',
status: groupMatch ? 'pass' : 'fail',
message: groupMatch ? `Found: ${groupMatch}` : 'No release group tag found (should end with -GROUP)',
required: true
});
const isFullDiscNaming = CONFIG.fullDiscTypes.some(t => type?.includes(t));
const detectedObject = isFullDiscNaming ? null : Helpers.detectAudioObject(mediaInfoText);
const titleHasAtmos = /Atmos/i.test(name);
const titleHasAuro = /Auro/i.test(name);
let objectStatus = 'pass';
let objectMessage = 'No object audio detected';
if (detectedObject === 'Atmos') {
if (titleHasAtmos) {
objectStatus = 'pass';
objectMessage = 'Atmos detected & in title';
}
else {
objectStatus = 'warn';
objectMessage = 'Atmos detected in MediaInfo but missing from Title';
}
}
else if (detectedObject === 'Auro3D') {
if (titleHasAuro) {
objectStatus = 'pass';
objectMessage = 'Auro3D detected & in title';
}
else {
objectStatus = 'warn';
objectMessage = 'Auro3D detected in MediaInfo but missing from Title';
}
}
else if (titleHasAtmos || titleHasAuro) {
if (isFullDiscNaming) {
objectStatus = 'pass';
objectMessage = `${titleHasAtmos ? 'Atmos' : 'Auro3D'} in title (Full Disc - MediaInfo not validated)`;
}
else {
objectStatus = 'warn';
objectMessage = 'Object tag in title but not confirmed in MediaInfo';
}
}
if (detectedObject || titleHasAtmos || titleHasAuro) {
results.checks.push({
name: 'Audio Object',
status: objectStatus,
message: objectMessage,
required: !!detectedObject
});
}
const sourceCheck = this.checkSourceForType(name, type);
results.checks.push(sourceCheck);
const sortedVideoCodecs = [...CONFIG.validVideoCodecs].sort((a, b) => b.length - a.length);
let videoMatch = null;
for (const codec of sortedVideoCodecs) {
const regex = new RegExp(codec.replace(/[.]/g, '\\.?'), 'i');
if (regex.test(name)) {
videoMatch = codec;
break;
}
}
const isFullDiscOrRemux = CONFIG.fullDiscTypes.some(t => type?.includes(t)) ||
CONFIG.remuxTypes.some(t => type?.toUpperCase().includes(t.toUpperCase()));
results.checks.push({
name: 'Video Codec',
status: videoMatch ? 'pass' : (isFullDiscOrRemux ? 'na' : 'warn'),
message: videoMatch ? `Found: ${videoMatch}` : (isFullDiscOrRemux ? 'N/A for Full Disc/REMUX' : 'No video codec found (may be implied)'),
required: false
});
if (name.includes('2160p') || name.includes('4320p')) {
const mediaInfoHdr = DataExtractor.getHdrFromMediaInfo();
// Extract the single best (longest) HDR tag from the title.
// Sorted longest-first so compound tags like "DV HDR10+" match
// before their components "DV", "HDR10+", "HDR", etc.
const sortedHdr = [...CONFIG.hdrFormats].sort((a, b) => b.length - a.length);
let titleHdrTag = null;
for (const hdr of sortedHdr) {
const regex = new RegExp('(?:^|\\s)' + hdr.replace(/[+]/g, '\\+') + '(?:\\s|$)', 'i');
if (regex.test(name)) {
titleHdrTag = hdr.toUpperCase();
break;
}
}
let hdrStatus = 'pass';
let hdrMessage = '';
// "HDR10" alone in title is always wrong - should be "HDR"
const hasHDR10InTitleRaw = /\bHDR10\b/i.test(name) && !/\bHDR10\+/i.test(name);
if (hasHDR10InTitleRaw && (!titleHdrTag || titleHdrTag === 'HDR10')) {
hdrStatus = 'fail';
hdrMessage = '"HDR10" in title should be renamed to "HDR"';
}
else if (isFullDiscNaming) {
if (titleHdrTag) {
hdrStatus = 'pass';
hdrMessage = `HDR in title: ${titleHdrTag} (Full Disc - MediaInfo not validated)`;
}
else {
hdrMessage = 'SDR (no HDR in title)';
}
}
else if (mediaInfoHdr.length === 0) {
if (!titleHdrTag) {
hdrMessage = 'SDR (no HDR in title or MediaInfo)';
}
else {
hdrStatus = 'warn';
hdrMessage = `Title has ${titleHdrTag} but MediaInfo shows no HDR`;
}
}
else {
const mediaInfoDisplay = mediaInfoHdr.join(', ');
const hasDV = mediaInfoHdr.some(f => f.startsWith('DV'));
const hasHDR10Plus = mediaInfoHdr.includes('HDR10+');
const hasHDR10 = mediaInfoHdr.includes('HDR10');
const hasHDR = mediaInfoHdr.includes('HDR');
let expectedTag = null;
if (hasDV && hasHDR10Plus) {
expectedTag = 'DV HDR10+';
}
else if (hasDV && (hasHDR10 || hasHDR)) {
expectedTag = 'DV HDR';
}
else if (hasDV) {
expectedTag = 'DV';
}
else if (hasHDR10Plus) {
expectedTag = 'HDR10+';
}
else if (hasHDR10) {
expectedTag = 'HDR';
}
else if (hasHDR) {
expectedTag = 'HDR';
}
if (titleHdrTag && expectedTag && titleHdrTag === expectedTag.toUpperCase()) {
hdrStatus = 'pass';
hdrMessage = `Correct: ${expectedTag} (MediaInfo: ${mediaInfoDisplay})`;
}
else if (!titleHdrTag && !expectedTag) {
hdrMessage = 'SDR (no HDR in title or MediaInfo)';
}
else if (!titleHdrTag) {
hdrStatus = 'fail';
hdrMessage = `Missing HDR tag - MediaInfo shows ${mediaInfoDisplay}, title should include: ${expectedTag}`;
}
else if (!expectedTag) {
hdrStatus = 'warn';
hdrMessage = `Title has ${titleHdrTag} but could not determine expected tag from MediaInfo (${mediaInfoDisplay})`;
}
else {
hdrStatus = 'fail';
hdrMessage = `Wrong HDR tag - MediaInfo shows ${mediaInfoDisplay}, title has ${titleHdrTag} but should be: ${expectedTag}`;
}
}
results.checks.push({
name: 'HDR Format',
status: hdrStatus,
message: hdrMessage,
required: false
});
}
const hasFailedRequired = results.checks.some(c => c.required && c.status === 'fail');
const hasWarnings = results.checks.some(c => c.status === 'warn');
results.status = hasFailedRequired ? 'fail' : (hasWarnings ? 'warn' : 'pass');
return results;
},
checkSourceForType(torrentName, type) {
const name = torrentName.toUpperCase();
const normalizedType = type?.toUpperCase() || '';
let validSources = [];
let typeCategory = 'Unknown';
if (CONFIG.fullDiscTypes.some(t => normalizedType.includes(t.toUpperCase()))) {
validSources = CONFIG.sources.fullDisc;
typeCategory = 'Full Disc';
}
else if (CONFIG.remuxTypes.some(t => normalizedType.includes(t.toUpperCase()))) {
validSources = CONFIG.sources.remux;
typeCategory = 'REMUX';
}
else if (CONFIG.encodeTypes.some(t => normalizedType.includes(t.toUpperCase()))) {
validSources = CONFIG.sources.encode;
typeCategory = 'Encode';
}
else if (CONFIG.webTypes.some(t => normalizedType.includes(t.toUpperCase()))) {
validSources = [...CONFIG.sources.web, ...CONFIG.streamingServices];
typeCategory = 'WEB';
}
else if (CONFIG.hdtvTypes.some(t => normalizedType.includes(t.toUpperCase()))) {
validSources = CONFIG.sources.hdtv;
typeCategory = 'HDTV';
}
else {
validSources = [
...CONFIG.sources.fullDisc,
...CONFIG.sources.remux,
...CONFIG.sources.encode,
...CONFIG.sources.web,
...CONFIG.sources.hdtv,
...CONFIG.streamingServices
];
validSources = [...new Set(validSources)];
}
let sourceMatch = null;
for (const source of validSources) {
const regex = new RegExp(source.replace(/[-.]/g, '[-. ]?'), 'i');
if (regex.test(torrentName)) {
sourceMatch = source;
break;
}
}
if (!sourceMatch && typeCategory === 'Encode') {
if (/blu-?ray/i.test(torrentName)) {
sourceMatch = 'BluRay';
}
}
return {
name: 'Source',
status: sourceMatch ? 'pass' : 'fail',
message: sourceMatch ?
`Found: ${sourceMatch}${typeCategory !== 'Unknown' ? ` (valid for ${typeCategory})` : ''}` : `No valid source for ${typeCategory} type`,
required: true
};
},
titleElementOrder(torrentName, type) {
const {
elements,
positions
} = Helpers.extractTitleElements(torrentName, type);
if (elements.length < 3) {
return {
status: 'warn',
message: 'Too few elements detected to validate order',
details: null,
violations: []
};
}
const isFullDiscOrRemux = CONFIG.fullDiscTypes.some(t => type?.includes(t)) ||
CONFIG.remuxTypes.some(t => type?.toUpperCase().includes(t.toUpperCase()));
const expectedOrder = isFullDiscOrRemux ?
CONFIG.titleElementOrder.fullDiscRemux :
CONFIG.titleElementOrder.encodeWeb;
const orderType = isFullDiscOrRemux ? 'Full Disc/REMUX' : 'Encode/WEB';
const violations = [];
const detectedTypes = elements.map(e => e.type);
for (let i = 0; i < detectedTypes.length; i++) {
for (let j = i + 1; j < detectedTypes.length; j++) {
const first = detectedTypes[i];
const second = detectedTypes[j];
const expectedFirstIdx = expectedOrder.indexOf(first);
const expectedSecondIdx = expectedOrder.indexOf(second);
if (expectedFirstIdx === -1 || expectedSecondIdx === -1) continue;
if (expectedFirstIdx > expectedSecondIdx) {
violations.push({
first: {
type: first,
value: elements[i].value
},
second: {
type: second,
value: elements[j].value
},
message: `"${elements[i].value}" (${first}) should come after "${elements[j].value}" (${second})`
});
}
}
}
if (violations.length === 0) {
return {
status: 'pass',
message: `Element order correct for ${orderType}`,
details: null,
violations: []
};
}
const hdrVcodecViolation = violations.find(v =>
(v.first.type === 'hdr' && v.second.type === 'vcodec') ||
(v.first.type === 'vcodec' && v.second.type === 'hdr')
);
let message = `${violations.length} ordering issue(s) found`;
if (hdrVcodecViolation) {
if (isFullDiscOrRemux) {
message = 'HDR should come BEFORE video codec for Full Disc/REMUX';
}
else {
message = 'HDR should come AFTER video codec for Encode/WEB';
}
}
return {
status: 'fail',
message: message,
details: {
orderType: orderType,
violations: violations.map(v => v.message)
},
violations: violations
};
},
audioTagCompliance(torrentName, originalLangCode, mediaInfoLanguages, type, mediaInfoText) {
const isFullDisc = CONFIG.fullDiscTypes.some(t => type?.includes(t));
if (isFullDisc) {
return {
status: 'na',
message: 'N/A - Full Disc (no MediaInfo)',
details: null,
checks: []
};
}
const checks = [];
if (mediaInfoLanguages && mediaInfoLanguages.length > 0) {
const lowerName = (torrentName || '').toLowerCase();
const isDual = lowerName.includes('dual-audio') || lowerName.includes('dual audio');
const isMulti = lowerName.includes('multi');
const count = mediaInfoLanguages.length;
const mappedOriginal = CONFIG.languageMap[originalLangCode] || originalLangCode;
const originalIsEnglish = originalLangCode === 'en';
const isEnglish = (lang) => lang.toLowerCase().startsWith('english');
const matchesOriginal = (lang) => {
if (!mappedOriginal) return false;
const lowerLang = lang.toLowerCase();
const lowerOriginal = mappedOriginal.toLowerCase();
if (lowerLang === lowerOriginal ||
lowerLang.startsWith(lowerOriginal) ||
lowerLang.includes(lowerOriginal) ||
lowerOriginal.includes(lowerLang)) return true;
const aliases = CONFIG.languageAliases[lowerOriginal] || [];
return aliases.some(alias => lowerLang.includes(alias) || alias.includes(lowerLang));
};
if (isDual) {
if (originalIsEnglish) {
const nonEnglishLangs = mediaInfoLanguages.filter(l => !isEnglish(l));
let suggestion = '';
if (count >= 2 && nonEnglishLangs.length > 0) {
if (nonEnglishLangs.length > 1) {
suggestion = `. Found ${count} audio tracks (${mediaInfoLanguages.join(', ')}). Use "Multi" instead`;
}
else {
const hasUnrecognized = /^[a-z]{2,3}$/i.test(nonEnglishLangs[0]);
if (hasUnrecognized) {
suggestion = `. Found ${count} audio tracks (${mediaInfoLanguages.join(', ')}), use "{Language_Name} Multi" instead`;
}
else {
suggestion = `. Found ${count} audio tracks (${mediaInfoLanguages.join(', ')}). Use "${nonEnglishLangs[0]} Multi" instead`;
}
}
}
else if (count >= 2) {
suggestion = `. Found ${count} audio tracks but non-English languages not recognized by MediaInfo parser. Use "{Language} Multi" instead (or "Multi" if 3+ languages)`;
}
else {
suggestion = '. 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)';
}
checks.push({
name: 'Language Tags',
status: 'fail',
message: `Dual-Audio is reserved for non-English original content with an English dub${suggestion}`
});
}
else if (count > 2) {
checks.push({
name: 'Language Tags',
status: 'fail',
message: `Tagged Dual-Audio but found ${count} languages. Should be "Multi"`
});
}
else if (count < 2) {
checks.push({
name: 'Language Tags',
status: 'fail',
message: `Tagged Dual-Audio but found only ${count} language`
});
}
else {
const hasEnglish = mediaInfoLanguages.some(isEnglish);
const hasOriginal = mediaInfoLanguages.some(matchesOriginal);
if (!hasEnglish) {
checks.push({
name: 'Language Tags',
status: 'fail',
message: 'Dual-Audio requires English track'
});
}
else if (!hasOriginal) {
checks.push({
name: 'Language Tags',
status: 'warn',
message: `Dual-Audio implies Original Language (${mappedOriginal}) present`
});
}
else {
checks.push({
name: 'Language Tags',
status: 'pass',
message: `Dual-Audio correct (English + ${mappedOriginal})`
});
}
}
}
else if (isMulti) {
if (count < 2) {
checks.push({
name: 'Language Tags',
status: 'fail',
message: `"Multi" used but found only ${count} language`
});
}
else {
checks.push({
name: 'Language Tags',
status: 'pass',
message: `Multi-Audio correct (${count} languages)`
});
}
}
else if (count > 2) {
checks.push({
name: 'Language Tags',
status: 'warn',
message: `Found ${count} languages but no "Multi" tag`
});
}
else if (count === 2) {
const hasEnglish = mediaInfoLanguages.some(isEnglish);
const hasOriginal = mediaInfoLanguages.some(matchesOriginal);
if (hasEnglish && hasOriginal && !originalIsEnglish) {
checks.push({
name: 'Language Tags',
status: 'warn',
message: `Found English + Original (${mappedOriginal}), consider "Dual-Audio" tag`
});
}
else {
checks.push({
name: 'Language Tags',
status: 'pass',
message: `Audio languages OK (${count})`
});
}
}
else {
checks.push({
name: 'Language Tags',
status: 'pass',
message: `Audio languages OK (${count})`
});
}
}
const tracks = DataExtractor.getAudioTracksFromMediaInfo();
if (tracks.length > 0) {
const isUntouched = CONFIG.remuxTypes.some(t => type?.toUpperCase().includes(t.toUpperCase())) ||
/\b(HDTV|PDTV|SDTV)\b/i.test(type || '') ||
/\bDVD\b/i.test(type || '');
const isHdtvOrDvd = /\b(HDTV|PDTV|SDTV|DVD)\b/i.test(type || '') || isUntouched;
const normalizeCodec = (format, commercialName) => {
const f = (format || '').toLowerCase();
const c = (commercialName || '').toLowerCase();
if (f.includes('dts') && (c.includes('dts-hd') || c.includes('dts:x') || c.includes('master audio') || c.includes('dts-hd ma'))) return 'DTS-HD';
if (f.includes('dts')) return 'DTS';
if (f === 'ac-3' || f.includes('ac-3')) return 'AC-3';
if (f === 'e-ac-3' || f.includes('e-ac-3')) return 'E-AC-3';
if (f.includes('mlp fba') || c.includes('truehd')) return 'TrueHD';
if (f === 'flac' || f.includes('flac')) return 'FLAC';
if (f === 'opus' || f.includes('opus')) return 'Opus';
if (f === 'pcm' || f.includes('pcm') || f.includes('lpcm')) return 'LPCM';
if (f === 'aac' || f.includes('aac')) return 'AAC';
if (f === 'mpeg audio' && c.includes('mp2')) return 'MP2';
if (f === 'mpeg audio' && c.includes('mp3')) return 'MP3';
if (f.includes('mp3') || (f === 'mpeg audio' && !c)) return 'MP3';
if (f.includes('mp2')) return 'MP2';
if (f.includes('vorbis')) return 'Vorbis';
if (f.includes('alac')) return 'ALAC';
return format;
};
const isStereoOrMono = (channels) => {
if (!channels) return false;
return channels === '1.0' || channels === '2.0' || channels === '1ch' || channels === '2ch';
};
const isCommentary = (track) => {
const title = (track.title || '').toLowerCase();
return title.includes('commentary') || title.includes('comment');
};
for (let i = 0; i < tracks.length; i++) {
const track = tracks[i];
const codec = normalizeCodec(track.codec, track.commercialName);
const label = `Track ${i + 1}: ${codec}${track.channels ? ' ' + track.channels : ''}${track.language ? ' (' + track.language + ')' : ''}`;
if (codec === 'FLAC' || codec === 'Opus' || codec === 'LPCM') {
if (!isStereoOrMono(track.channels) && !isUntouched) {
checks.push({
name: label,
status: 'fail',
message: `${codec} only allowed as mono/stereo. Found: ${track.channels || 'unknown'}`
});
}
else {
checks.push({
name: label,
status: 'pass',
message: isStereoOrMono(track.channels) ? `${codec} mono/stereo OK` : `${codec} multichannel (untouched OK)`
});
}
}
else if (codec === 'MP2') {
if (!isHdtvOrDvd) {
checks.push({
name: label,
status: 'fail',
message: 'MP2 only allowed if untouched (HDTV/DVD)'
});
}
else {
checks.push({
name: label,
status: 'pass',
message: 'MP2 OK (untouched source)'
});
}
}
else if (codec === 'MP3') {
if (!isCommentary(track)) {
checks.push({
name: label,
status: 'warn',
message: 'MP3 only allowed for supplementary tracks (e.g. commentary)'
});
}
else {
checks.push({
name: label,
status: 'pass',
message: 'MP3 OK (commentary track)'
});
}
}
else if (codec === 'Vorbis' || codec === 'ALAC') {
checks.push({
name: label,
status: 'fail',
message: `${codec} is not an allowed audio codec`
});
}
else if (['DTS', 'DTS-HD', 'AC-3', 'E-AC-3', 'TrueHD', 'AAC'].includes(codec)) {
checks.push({
name: label,
status: 'pass',
message: `${codec} OK`
});
}
else {
checks.push({
name: label,
status: 'warn',
message: `Unrecognized codec: ${track.codec}${track.commercialName ? ' / ' + track.commercialName : ''}`
});
}
}
}
if (checks.length === 0) {
return {
status: 'na',
message: 'No audio data detected in MediaInfo',
details: null,
checks: []
};
}
const hasFails = checks.some(c => c.status === 'fail');
const hasWarns = checks.some(c => c.status === 'warn');
const overallStatus = hasFails ? 'fail' : (hasWarns ? 'warn' : 'pass');
return {
status: overallStatus,
message: overallStatus === 'pass' ?
`Audio OK (${tracks.length} track${tracks.length !== 1 ? 's' : ''})` : `Audio issues found`,
details: null,
checks: checks
};
},
mediaInfoPresent(hasMediaInfo, hasBdInfo, type) {
const isFullDisc = CONFIG.fullDiscTypes.some(t => type?.includes(t));
if (isFullDisc) {
if (hasBdInfo) {
return {
status: 'pass',
message: 'BDInfo present (Full Disc)',
details: null
};
}
else if (hasMediaInfo) {
return {
status: 'warn',
message: 'BDInfo expected for Full Disc',
details: null
};
}
else {
return {
status: 'fail',
message: 'BDInfo required for Full Disc uploads',
details: null
};
}
}
else {
if (hasBdInfo) {
return {
status: 'fail',
message: 'Release is not Full Disc, BDInfo should be empty',
details: null
};
}
else if (hasMediaInfo) {
return {
status: 'pass',
message: 'MediaInfo Present',
details: null
};
}
else {
return {
status: 'fail',
message: 'MediaInfo Required',
details: null
};
}
}
},
subtitleRequirement(mediaInfoLanguages, mediaInfoSubtitles, originalLangCode, type) {
const isFullDisc = CONFIG.fullDiscTypes.some(t => type?.includes(t));
if (isFullDisc) {
return {
status: 'na',
message: 'N/A - Full Disc (no MediaInfo)',
details: null
};
}
if (!mediaInfoLanguages || mediaInfoLanguages.length === 0) {
return {
status: 'na',
message: 'No audio languages detected',
details: null
};
}
const isEnglish = (lang) => {
const lower = lang.toLowerCase();
return lower === 'english' || lower.startsWith('english');
};
const hasEnglishAudio = mediaInfoLanguages.some(isEnglish);
if (hasEnglishAudio) {
return {
status: 'pass',
message: 'English audio present - subtitles optional',
details: null
};
}
if (!mediaInfoSubtitles || mediaInfoSubtitles.length === 0) {
return {
status: 'fail',
message: 'No English audio & no subtitles detected',
details: {
audio: mediaInfoLanguages.join(', '),
expected: 'English subtitles required for non-English audio'
}
};
}
const hasEnglishSubs = mediaInfoSubtitles.some(isEnglish);
if (hasEnglishSubs) {
return {
status: 'pass',
message: `Non-English audio with English subtitles`,
details: null
};
}
return {
status: 'fail',
message: 'Non-English audio requires English subtitles',
details: {
audio: mediaInfoLanguages.join(', '),
subtitles: mediaInfoSubtitles.join(', ') || 'None detected',
expected: 'English subtitles'
}
};
},
screenshotCount(descriptionHtml) {
const {
count,
urls
} = Helpers.countScreenshots(descriptionHtml);
if (count >= CONFIG.minScreenshots) {
return {
status: 'pass',
count,
message: `${count} screenshots found`,
details: null
};
}
else if (count > 0) {
return {
status: 'warn',
count,
message: `Only ${count} screenshot(s) found (${CONFIG.minScreenshots}+ required)`,
details: null
};
}
else {
return {
status: 'fail',
count: 0,
message: 'No screenshots found in description',
details: null
};
}
},
containerFormat(fileStructure, type) {
const isFullDisc = CONFIG.fullDiscTypes.some(t => type?.includes(t));
if (isFullDisc) {
return {
status: 'na',
message: 'N/A - Full Disc uploads use native folder structure',
details: null
};
}
if (!fileStructure || !fileStructure.files || fileStructure.files.length === 0) {
return {
status: 'warn',
message: 'Could not determine file structure to verify container',
details: null
};
}
const videoExtensions = ['.mkv', '.mp4', '.avi', '.wmv', '.m4v', '.ts', '.m2ts', '.vob', '.mpg', '.mpeg', '.mov', '.flv', '.webm'];
const videoFiles = fileStructure.files.filter(f => {
const lower = f.toLowerCase();
return videoExtensions.some(ext => lower.endsWith(ext));
});
if (videoFiles.length === 0) {
return {
status: 'warn',
message: 'No video files detected in file list',
details: null
};
}
const nonMkv = videoFiles.filter(f => !f.toLowerCase().endsWith('.mkv'));
if (nonMkv.length === 0) {
return {
status: 'pass',
message: `MKV container verified (${videoFiles.length} video file${videoFiles.length > 1 ? 's' : ''})`,
details: null
};
}
const badExtensions = [...new Set(nonMkv.map(f => f.split('.').pop().toUpperCase()))];
return {
status: 'fail',
message: `Non-MKV container detected: ${badExtensions.join(', ')}`,
details: {
expected: 'MKV container for all non-Full Disc releases',
found: nonMkv.join(', ')
}
};
},
packUniformity(fileStructure, type) {
const isFullDisc = CONFIG.fullDiscTypes.some(t => type?.includes(t));
if (isFullDisc) {
return {
status: 'na',
message: 'N/A - Full Disc',
details: null,
checks: []
};
}
if (!fileStructure || !fileStructure.files || fileStructure.files.length === 0) {
return {
status: 'na',
message: 'N/A - No files detected',
details: null,
checks: []
};
}
const videoExtensions = ['.mkv', '.mp4', '.avi', '.wmv', '.m4v', '.ts', '.m2ts', '.vob', '.mpg', '.mpeg', '.mov', '.flv', '.webm'];
const videoFiles = fileStructure.files.filter(f => {
const lower = f.toLowerCase();
return videoExtensions.some(ext => lower.endsWith(ext));
});
if (videoFiles.length < 2) {
return {
status: 'na',
message: 'N/A - Single file upload',
details: null,
checks: []
};
}
const parseFileAttributes = (fileName) => {
const attrs = {};
const resMatch = CONFIG.validResolutions.find(r => fileName.includes(r));
attrs.resolution = resMatch || null;
const sourcePatterns = [{
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'
}
];
attrs.source = null;
for (const sp of sourcePatterns) {
if (sp.pattern.test(fileName)) {
attrs.source = sp.name;
break;
}
}
const sortedAudio = [...CONFIG.validAudioCodecs].sort((a, b) => b.length - a.length);
attrs.audioCodec = null;
for (const codec of sortedAudio) {
const escaped = codec.replace(/[+]/g, '\\+').replace(/[-.]/g, '[-.]?');
const regex = new RegExp('(?<![a-zA-Z])' + escaped + '(?![a-zA-Z])', 'i');
if (regex.test(fileName)) {
attrs.audioCodec = codec;
break;
}
}
const chanMatch = fileName.match(/(\d{1,2}\.\d)(?!\d)/);
attrs.channels = chanMatch ? chanMatch[1] : null;
const sortedVideo = [...CONFIG.validVideoCodecs].sort((a, b) => b.length - a.length);
attrs.videoCodec = null;
for (const codec of sortedVideo) {
const regex = new RegExp(codec.replace(/[.]/g, '\\.?'), 'i');
if (regex.test(fileName)) {
attrs.videoCodec = codec;
break;
}
}
const groupMatch = fileName.match(/-([A-Za-z0-9$!._&+\$]+)(?:\.[a-z0-9]+)?$/i);
if (groupMatch) {
attrs.group = groupMatch[1];
}
else {
// Try "- GROUP)" pattern inside trailing paren block (e.g. "English - HONE).mkv")
const parenGroupMatch = fileName.match(/\(\s*[^()]*-\s*([A-Za-z0-9$!._&+\$]+)\s*\)(?:\.[a-z0-9]+)?$/i);
attrs.group = parenGroupMatch ? parenGroupMatch[1] : null;
}
return attrs;
};
const parsed = videoFiles.map(f => ({
file: f,
attrs: parseFileAttributes(f)
}));
const attributeNames = [{
key: 'resolution',
label: 'Resolution'
},
{
key: 'source',
label: 'Source/Format'
},
{
key: 'audioCodec',
label: 'Audio Codec'
},
{
key: 'videoCodec',
label: 'Video Codec'
},
{
key: 'group',
label: 'Release Group'
}
];
const checks = [];
let hasFail = false;
for (const {
key,
label
}
of attributeNames) {
const values = parsed.map(p => p.attrs[key]).filter(v => v !== null);
const unique = [...new Set(values.map(v => v.toUpperCase()))];
if (values.length === 0) {
checks.push({
name: label,
status: 'warn',
message: `Could not detect ${label.toLowerCase()} in filenames`
});
}
else if (unique.length === 1) {
checks.push({
name: label,
status: 'pass',
message: `Uniform: ${values[0]}`
});
}
else {
hasFail = true;
const valueCounts = {};
values.forEach(v => {
const upper = v.toUpperCase();
valueCounts[upper] = (valueCounts[upper] || 0) + 1;
});
const breakdown = Object.entries(valueCounts)
.map(([val, count]) => `${val} (${count})`)
.join(', ');
checks.push({
name: label,
status: 'fail',
message: `Mixed: ${breakdown}`
});
}
}
const hasWarns = checks.some(c => c.status === 'warn');
const overallStatus = hasFail ? 'fail' : (hasWarns ? 'warn' : 'pass');
return {
status: overallStatus,
message: hasFail ?
`Mixed pack detected across ${videoFiles.length} files` : `Uniform across ${videoFiles.length} files`,
details: null,
checks: checks
};
},
encodeCompliance(torrentName, type, mediaInfoText) {
const isEncode = CONFIG.encodeTypes.some(t => type?.toUpperCase().includes(t.toUpperCase()));
if (!isEncode) {
return {
status: 'na',
message: 'N/A - Not an Encode',
details: null,
checks: []
};
}
const checks = [];
const name = torrentName || '';
const hasX264 = /\bx264\b/i.test(name);
const hasX265 = /\bx265\b/i.test(name);
const hasSvtAv1 = /\bSVT[-.]?AV1\b/i.test(name);
const hasAllowedEncoder = hasX264 || hasX265 || hasSvtAv1;
const otherCodecs = ['AVC', 'HEVC', 'H.264', 'H.265', 'MPEG-2', 'VC-1', 'VP9', 'XviD', 'DivX'];
let foundOtherCodec = null;
if (!hasSvtAv1 && /\bAV1\b/i.test(name)) {
foundOtherCodec = 'AV1';
}
if (!foundOtherCodec) {
for (const codec of otherCodecs) {
const regex = new RegExp('\\b' + codec.replace(/[.]/g, '\\.?') + '\\b', 'i');
if (regex.test(name)) {
foundOtherCodec = codec;
break;
}
}
}
if (hasAllowedEncoder) {
const encoderName = hasX264 ? 'x264' : (hasX265 ? 'x265' : 'SVT-AV1');
checks.push({
name: 'Encoder',
status: 'pass',
message: `Found: ${encoderName}`
});
}
else if (foundOtherCodec) {
checks.push({
name: 'Encoder',
status: 'fail',
message: `Found ${foundOtherCodec} — encodes must use x264, x265, or SVT-AV1`
});
}
else {
checks.push({
name: 'Encoder',
status: 'fail',
message: 'No x264, x265, or SVT-AV1 detected in title'
});
}
if (mediaInfoText) {
const writingLibMatch = mediaInfoText.match(/Writing library\s*:\s*(.+?)(?:\n|$)/im);
const hasX264Lib = writingLibMatch && /x264/i.test(writingLibMatch[1]);
const hasX265Lib = writingLibMatch && /x265/i.test(writingLibMatch[1]);
const videoEncodingSettings = mediaInfoText.match(/Encoding settings\s*:\s*(.+?)(?:\n|$)/im);
const generalEncoderSettings = mediaInfoText.match(/Encoder_settings\s*:\s*(.+?)(?:\n|$)/im);
const hasSvtAv1Meta = generalEncoderSettings && /svt[-_]?av1/i.test(generalEncoderSettings[1]);
const hasEncoderMeta = hasX264Lib || hasX265Lib || hasSvtAv1Meta || videoEncodingSettings;
if (hasEncoderMeta) {
let metaSource = '';
if (hasX264Lib) metaSource = 'x264';
else if (hasX265Lib) metaSource = 'x265';
else if (hasSvtAv1Meta) metaSource = 'SVT-AV1';
else metaSource = 'encoding settings present';
checks.push({
name: 'Encoder Metadata',
status: 'pass',
message: `Encoder metadata found (${metaSource})`
});
}
else {
checks.push({
name: 'Encoder Metadata',
status: 'fail',
message: 'No encoder metadata found in MediaInfo — x264/x265/SVT-AV1 info required'
});
}
}
else {
checks.push({
name: 'Encoder Metadata',
status: 'warn',
message: 'No MediaInfo available — cannot verify encoder metadata'
});
}
const titleHasH264 = /\bH\.?264\b/i.test(name);
const titleHasH265 = /\bH\.?265\b/i.test(name);
if (titleHasH264 || titleHasH265) {
const writingLib = mediaInfoText ? mediaInfoText.match(/Writing library\s*:\s*(.+?)(?:\n|$)/im) : null;
const libHasX264 = writingLib && /x264/i.test(writingLib[1]);
const libHasX265 = writingLib && /x265/i.test(writingLib[1]);
if (titleHasH264 && libHasX264) {
checks.push({
name: 'Codec vs Encoder',
status: 'fail',
message: 'Title has H.264 but MediaInfo shows x264 — use encoder name (x264)'
});
}
else if (titleHasH265 && libHasX265) {
checks.push({
name: 'Codec vs Encoder',
status: 'fail',
message: 'Title has H.265 but MediaInfo shows x265 — use encoder name (x265)'
});
}
else if (titleHasH264 && !libHasX264) {
checks.push({
name: 'Codec vs Encoder',
status: 'warn',
message: 'Title has H.264 — encodes typically use encoder name (x264) instead'
});
}
else if (titleHasH265 && !libHasX265) {
checks.push({
name: 'Codec vs Encoder',
status: 'warn',
message: 'Title has H.265 — encodes typically use encoder name (x265) instead'
});
}
}
const allowedEncodeResolutions = ['720p', '1080i', '1080p', '2160p', '4320p'];
const resMatch = CONFIG.validResolutions.find(r => name.includes(r));
if (resMatch) {
if (allowedEncodeResolutions.includes(resMatch)) {
checks.push({
name: 'Resolution',
status: 'pass',
message: `Found: ${resMatch}`
});
}
else {
checks.push({
name: 'Resolution',
status: 'fail',
message: `Found ${resMatch} — encodes must be 720p or greater`
});
}
}
else {
checks.push({
name: 'Resolution',
status: 'warn',
message: 'Could not detect resolution to verify encode requirement'
});
}
if (mediaInfoText) {
const videoEncodingSettings = mediaInfoText.match(/Encoding settings\s*:\s*(.+?)(?:\n|$)/im);
const generalEncoderSettings = mediaInfoText.match(/Encoder_settings\s*:\s*(.+?)(?:\n|$)/im);
const settings = videoEncodingSettings ? videoEncodingSettings[1] :
(generalEncoderSettings ? generalEncoderSettings[1] : null);
if (settings) {
const rcMatch = settings.match(/rc=(\w+)/);
const svtCrf = /--crf\b/.test(settings);
const svtPasses = settings.match(/--passes?\s+(\d+)/);
if (rcMatch) {
const rateControl = rcMatch[1].toLowerCase();
if (rateControl === 'crf') {
checks.push({
name: 'Rate Control',
status: 'pass',
message: 'CRF encoding detected'
});
}
else if (rateControl === 'abr') {
const statsRead = settings.match(/stats-read=(\d+)/);
const passMatch = settings.match(/(?:^|[\s/])pass=?(\d+)/);
if ((statsRead && parseInt(statsRead[1], 10) >= 2) ||
(passMatch && parseInt(passMatch[1], 10) >= 2)) {
checks.push({
name: 'Rate Control',
status: 'pass',
message: 'Multi-pass ABR encoding detected'
});
}
else {
checks.push({
name: 'Rate Control',
status: 'fail',
message: 'Single-pass ABR detected — must use CRF or multi-pass ABR'
});
}
}
else if (rateControl === '2pass') {
checks.push({
name: 'Rate Control',
status: 'pass',
message: '2-pass encoding detected'
});
}
else if (rateControl === 'cbr') {
checks.push({
name: 'Rate Control',
status: 'fail',
message: 'CBR encoding detected — must use CRF or multi-pass ABR'
});
}
else {
checks.push({
name: 'Rate Control',
status: 'warn',
message: `Unrecognized rate control: rc=${rateControl}`
});
}
}
else if (svtCrf) {
checks.push({
name: 'Rate Control',
status: 'pass',
message: 'CRF encoding detected (SVT-AV1)'
});
}
else if (svtPasses && parseInt(svtPasses[1], 10) >= 2) {
checks.push({
name: 'Rate Control',
status: 'pass',
message: `Multi-pass encoding detected (SVT-AV1, ${svtPasses[1]} passes)`
});
}
else if (/--tbr\b/.test(settings)) {
checks.push({
name: 'Rate Control',
status: 'fail',
message: 'Target bitrate (ABR) detected without multi-pass — must use CRF or multi-pass'
});
}
else {
checks.push({
name: 'Rate Control',
status: 'warn',
message: 'Encoding settings found but could not determine rate control method'
});
}
}
else {
checks.push({
name: 'Rate Control',
status: 'warn',
message: 'No encoding settings in MediaInfo — cannot verify rate control'
});
}
}
else {
checks.push({
name: 'Rate Control',
status: 'warn',
message: 'No MediaInfo available — cannot verify rate control'
});
}
const hasFails = checks.some(c => c.status === 'fail');
const hasWarns = checks.some(c => c.status === 'warn');
const overallStatus = hasFails ? 'fail' : (hasWarns ? 'warn' : 'pass');
return {
status: overallStatus,
message: overallStatus === 'pass' ?
'Encode requirements met' : (hasFails ? 'Encode compliance issues found' : 'Encode checks need review'),
details: null,
checks: checks
};
},
upscaleDetection(torrentName) {
if (!torrentName) {
return {
status: 'na',
message: 'No torrent name to check',
alert: false
};
}
const upscalePatterns = [{
name: 'AI Upscales',
regex: /(?<=\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: /(?<=\b[12]\d{3}\b).*\b(AI[ ._-]?Enhanced?|UPS(UHD)?|Upscaled?([ ._-]?UHD)?|UpRez)\b/i
},
{
name: 'Upscale',
regex: /\b(UPSCALE)\b/i
}
];
const matches = upscalePatterns.filter(p => p.regex.test(torrentName));
if (matches.length > 0) {
const matchedNames = matches.map(m => m.name).join(', ');
return {
status: 'fail',
message: `UPSCALE DETECTED: ${matchedNames}`,
alert: true
};
}
return {
status: 'pass',
message: 'No upscale indicators found',
alert: false
};
},
bannedReleaseGroup(torrentName) {
const group = Helpers.extractReleaseGroup(torrentName);
if (!group) {
return {
status: 'warn',
group: null,
message: 'Could not extract release group from title',
alert: false
};
}
const isBanned = CONFIG.bannedGroups.some(
banned => banned.toLowerCase() === group.toLowerCase()
);
if (isBanned) {
return {
status: 'fail',
group,
message: `BANNED GROUP: ${group}`,
alert: true
};
}
return {
status: 'pass',
group,
message: `Release Group: ${group}`,
alert: false
};
}
};
const UI = {
getStatusIcon(status) {
switch (status) {
case 'pass':
return '<i class="fas fa-check-circle mh-icon--pass"></i>';
case 'fail':
return '<i class="fas fa-times-circle mh-icon--fail"></i>';
case 'warn':
return '<i class="fas fa-exclamation-triangle mh-icon--warn"></i>';
case 'na':
return '<i class="fas fa-minus-circle mh-icon--na"></i>';
default:
return '<i class="fas fa-question-circle"></i>';
}
},
getStatusBadge(status) {
const labels = {
pass: 'Pass',
fail: 'Fail',
warn: 'Warning',
na: 'N/A'
};
return `<span class="mh-badge mh-badge--${status}">${labels[status] || status}</span>`;
},
worstStatus(statuses) {
const active = statuses.filter(s => s !== 'na');
if (active.includes('fail')) return 'fail';
if (active.includes('warn')) return 'warn';
return active.length ? 'pass' : 'na';
},
accordion(id, title, status, bodyHtml, {
forceOpen = null,
alert = false
} = {}) {
const shouldOpen = forceOpen !== null ? forceOpen : (status !== 'pass' && status !== 'na');
const alertClass = alert ? ' mh-accordion--alert' : '';
return `
<details class="mh-accordion${alertClass}" data-section="${id}" data-status="${status}" ${shouldOpen ? 'open' : ''}>
<summary class="mh-accordion__summary mh-accordion__summary--${status}">
<span class="mh-accordion__icon">${this.getStatusIcon(status)}</span>
<span class="mh-accordion__title">${title}</span>
${this.getStatusBadge(status)}
<i class="fas fa-chevron-down mh-accordion__chevron"></i>
</summary>
<div class="mh-accordion__body">${bodyHtml}</div>
</details>`;
},
checkRow(status, label, message, details = null) {
let detailHtml = '';
if (details) {
if (details.expected !== undefined && details.found !== undefined) {
detailHtml = `
<div class="mh-detail">
<span class="mh-detail__item"><strong>Expected:</strong> ${details.expected}</span>
<span class="mh-detail__item"><strong>Found:</strong> ${details.found}</span>
</div>`;
}
else if (typeof details === 'object' && !Array.isArray(details)) {
const entries = Object.entries(details);
if (entries.length) {
detailHtml = `<div class="mh-detail">${entries.map(([k, v]) =>
`<span class="mh-detail__item"><strong>${k}:</strong> ${Array.isArray(v) ? v.join(', ') : v}</span>`
).join('')}</div>`;
}
}
else if (Array.isArray(details) && details.length) {
detailHtml = `<div class="mh-detail">${details.map(d =>
`<span class="mh-detail__item">${d}</span>`
).join('')}</div>`;
}
}
return `
<div class="mh-row mh-row--${status}">
<span class="mh-row__icon">${this.getStatusIcon(status)}</span>
<span class="mh-row__label">${label}</span>
<span class="mh-row__msg">${message}</span>
${detailHtml}
</div>`;
},
buildSimple(id, title, result, {
alert = false
} = {}) {
const hasDetails = result.details && (
(result.details.expected !== undefined && result.details.found !== undefined) ||
(Array.isArray(result.details) && result.details.length > 0) ||
(typeof result.details === 'object' && !Array.isArray(result.details) && Object.keys(result.details).length > 0)
);
if (hasDetails) {
const body = this.checkRow(result.status, title, result.message, result.details);
return this.accordion(id, title, result.status, body, {
alert
});
}
return this.inlineRow(id, result.status, title, result.message);
},
inlineRow(id, status, title, message) {
return `
<div class="mh-inline mh-inline--${status}" data-section="${id}" data-status="${status}">
<span class="mh-inline__icon">${this.getStatusIcon(status)}</span>
<span class="mh-inline__title">${title}</span>
<span class="mh-inline__msg">${message}</span>
${this.getStatusBadge(status)}
</div>`;
},
buildNamingGuide(result) {
const rows = result.checks.map(c => {
const suffix = c.required === false ? ' <span class="mh-optional">(optional)</span>' : '';
return this.checkRow(c.status, c.name + suffix, c.message);
}).join('');
return this.accordion('naming', 'Naming Convention', result.status, rows);
},
buildElementOrder(result) {
const violations = [];
if (result.violations && result.violations.length > 0) {
violations.push(...result.violations.map(v => typeof v === 'object' ? v.message : v));
}
if (result.details && result.details.violations && result.details.violations.length > 0) {
violations.push(...result.details.violations);
}
if (!violations.length) {
return this.inlineRow('order', result.status, 'Title Element Order', result.message);
}
let body = this.checkRow(result.status, 'Element Order', result.message);
body += `<div class="mh-violations">
${result.details?.orderType ? `<span class="mh-violations__type">Order type: ${result.details.orderType}</span>` : ''}
<ul class="mh-violations__list">${violations.map(v => `<li>${v}</li>`).join('')}</ul>
</div>`;
return this.accordion('order', 'Title Element Order', result.status, body);
},
buildMultiCheck(id, title, result) {
if (result.checks && result.checks.length > 1) {
const rows = result.checks.map(c => this.checkRow(c.status, c.name, c.message)).join('');
return this.accordion(id, title, result.status, rows);
}
if (result.checks && result.checks.length === 1) {
return this.inlineRow(id, result.checks[0].status, title, result.checks[0].message);
}
return this.inlineRow(id, result.status, title, result.message);
},
buildBannedGroupAlert(result) {
if (!result.alert) return '';
return `
<div class="mh-alert mh-alert--fail">
<i class="fas fa-ban mh-alert__icon"></i>
<div class="mh-alert__content">
<strong>Banned Release Group Detected</strong>
<span>${result.message}</span>
</div>
</div>`;
},
groupHeading(label) {
return `<div class="mh-group">${label}</div>`;
},
sectionGroup(label, contentHtml, resultsList) {
const active = resultsList.map(r => r.status).filter(s => s !== 'na');
const passCount = active.filter(s => s === 'pass').length;
const total = active.length;
const allPassed = passCount === total && total > 0;
const worst = this.worstStatus(resultsList.map(r => r.status));
const statusClass = worst === 'na' ? 'pass' : worst;
return `
<details class="mh-section" ${allPassed ? '' : 'open'}>
<summary class="mh-section__summary">
<span class="mh-chip mh-chip--${statusClass}">${passCount}/${total} passed</span>
<span class="mh-section__label">${label}</span>
<i class="fas fa-chevron-down mh-section__chevron"></i>
</summary>
<div class="mh-section__body">${contentHtml}</div>
</details>`;
},
createPanel(results) {
const panel = document.createElement('section');
panel.className = 'panelV2 mh-panel';
panel.id = 'mod-helper-panel';
const allResults = [
results.tmdbMatch, results.seasonEpisode, results.namingGuide,
results.elementOrder, results.folderStructure, results.mediaInfo,
results.audioTags, results.subtitleRequirement, results.screenshots,
results.bannedGroup, results.encodeCompliance, results.upscaleDetection,
results.containerFormat, results.packUniformity
];
const allStatuses = allResults.map(r => r.status);
const overallStatus = this.worstStatus(allStatuses);
const active = allStatuses.filter(s => s !== 'na');
const passCount = active.filter(s => s === 'pass').length;
const warnCount = active.filter(s => s === 'warn').length;
const failCount = active.filter(s => s === 'fail').length;
const total = active.length;
let chips = [];
if (failCount) chips.push(`<span class="mh-chip mh-chip--fail">${failCount} failed</span>`);
if (warnCount) chips.push(`<span class="mh-chip mh-chip--warn">${warnCount} warning${warnCount > 1 ? 's' : ''}</span>`);
chips.push(`<span class="mh-chip mh-chip--pass">${passCount}/${total} passed</span>`);
let sections = '';
sections += this.buildBannedGroupAlert(results.bannedGroup);
const contentResults = [
results.tmdbMatch, results.seasonEpisode, results.namingGuide,
results.elementOrder, results.bannedGroup, results.screenshots
];
let contentInner = '';
contentInner += this.buildSimple('tmdb', 'TMDB Title Match', results.tmdbMatch);
if (results.seasonEpisode.status !== 'na') {
contentInner += this.buildSimple('season', 'Season / Episode Format', results.seasonEpisode);
}
contentInner += this.buildSimple('group', 'Banned Release Group', results.bannedGroup, {
alert: results.bannedGroup.alert
});
contentInner += this.buildSimple('screenshots', 'Screenshots', results.screenshots);
contentInner += this.buildElementOrder(results.elementOrder);
contentInner += this.buildNamingGuide(results.namingGuide);
sections += this.sectionGroup('Content & Naming', contentInner, contentResults);
const technicalResults = [
results.folderStructure, results.containerFormat, results.mediaInfo,
results.audioTags, results.subtitleRequirement, results.encodeCompliance,
results.packUniformity, results.upscaleDetection
];
let techInner = '';
if (results.folderStructure.status !== 'na') {
techInner += this.buildSimple('folder', 'Folder Structure', results.folderStructure);
}
if (results.containerFormat.status !== 'na') {
techInner += this.buildSimple('container', 'Container Format', results.containerFormat);
}
techInner += this.buildSimple('mediainfo', 'MediaInfo', results.mediaInfo);
if (results.subtitleRequirement.status !== 'na') {
techInner += this.buildSimple('subs', 'Subtitle Requirement', results.subtitleRequirement);
}
techInner += this.buildSimple('upscale', 'Upscale Detection', results.upscaleDetection);
techInner += this.buildMultiCheck('audio', 'Audio Compliance', results.audioTags);
if (results.encodeCompliance.status !== 'na') {
techInner += this.buildMultiCheck('encode', 'Encode Compliance', results.encodeCompliance);
}
if (results.packUniformity.status !== 'na') {
techInner += this.buildMultiCheck('pack', 'Pack Uniformity', results.packUniformity);
}
sections += this.sectionGroup('Technical', techInner, technicalResults);
panel.innerHTML = `
<header class="panel__header mh-header">
<h2 class="panel__heading mh-heading">
${this.getStatusIcon(overallStatus)}
<span>Moderation Quick Check</span>
</h2>
<div class="mh-summary">${chips.join('')}</div>
<div class="mh-actions">
<button class="form__button form__button--text mh-btn" id="mh-toggle-all" title="Expand all sections">
<i class="fas fa-angles-down"></i>
</button>
</div>
</header>
<div class="panel__body mh-body">${sections}</div>`;
return panel;
},
injectPanel(panel) {
const modPanel = DataExtractor.getModerationPanel();
if (modPanel) {
modPanel.parentNode.insertBefore(panel, modPanel);
}
else {
const torrentTags = document.querySelector('ul.torrent__tags');
if (torrentTags) {
torrentTags.parentNode.insertBefore(panel, torrentTags.nextSibling);
}
}
this.attachEvents(panel);
},
attachEvents(panel) {
const toggleBtn = panel.querySelector('#mh-toggle-all');
if (toggleBtn) {
let expanded = false;
toggleBtn.addEventListener('click', () => {
expanded = !expanded;
panel.querySelectorAll('.mh-accordion, .mh-section').forEach(d => d.open = expanded);
toggleBtn.querySelector('i').className = expanded ? 'fas fa-angles-up' : 'fas fa-angles-down';
toggleBtn.title = expanded ? 'Collapse all sections' : 'Expand all sections';
});
}
}
};
const STYLES = `
.mh-panel {
margin-bottom: 16px;
border-radius: 8px;
overflow: hidden;
border: 1px solid var(--panel-border, hsl(0 0% 22%));
background: var(--surface-01);
}
.mh-header {
display: flex;
align-items: center;
gap: 12px;
padding: 12px 16px;
flex-wrap: wrap;
}
.mh-heading {
display: flex;
align-items: center;
gap: 8px;
margin: 0;
font-size: 15px;
font-weight: 600;
color: var(--text-color, hsl(0 0% 85%));
white-space: nowrap;
}
.mh-summary {
display: flex;
align-items: center;
gap: 6px;
margin-left: auto;
}
.mh-chip {
font-size: 11px;
font-weight: 600;
padding: 3px 10px;
border-radius: 4px;
letter-spacing: 0.3px;
}
.mh-chip--pass { background: hsl(142 71% 45% / 0.12); color: hsla(142, 71%, 52%, 0.80); }
.mh-chip--warn { background: hsl(45 100% 51% / 0.12); color: hsla(45, 100%, 58%, 0.80); }
.mh-chip--fail { background: hsl(0 72% 51% / 0.12); color: hsla(0, 72%, 60%, 0.80); }
.mh-actions { display: flex; gap: 2px; }
.mh-btn {
background: none;
border: none;
color: var(--text-color, hsl(0 0% 55%));
cursor: pointer;
padding: 4px 8px;
border-radius: 4px;
font-size: 13px;
transition: background 0.15s, color 0.15s;
}
.mh-btn:hover {
background: hsl(0 0% 18%);
color: var(--text-color, hsl(0 0% 90%));
}
.mh-body {
padding: 0 !important;
display: flex;
flex-direction: column;
}
.mh-group {
padding: 8px 16px 4px;
font-size: 10px;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 1px;
color: var(--text-color, hsl(0 0% 45%));
background: var(--surface-01);
border-bottom: 1px solid hsla(210, 2%, 24%, 0.70);
}
.mh-group:first-child {
padding-top: 10px;
}
.mh-section {
border-bottom: 1px solid hsla(210, 2%, 24%, 0.70);
}
.mh-section__summary {
display: flex;
align-items: center;
gap: 8px;
padding: 10px 16px;
font-size: 10px;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 1px;
color: var(--text-color, hsl(0 0% 45%));
background: var(--surface-01);
cursor: pointer;
list-style: none;
user-select: none;
}
.mh-section__summary::-webkit-details-marker,
.mh-section__summary::marker {
display: none;
}
.mh-section__summary:hover {
filter: brightness(1.06);
}
.mh-section__chevron {
margin-left: auto;
font-size: 10px;
transition: transform .2s ease;
color: var(--text-color, hsl(0 0% 45%));
}
.mh-section[open] > .mh-section__summary .mh-section__chevron {
transform: rotate(180deg);
}
.mh-alert {
display: flex;
align-items: center;
gap: 12px;
padding: 10px 16px;
font-size: 13px;
}
.mh-alert--fail {
background: hsl(0 72% 51% / 0.10);
border-bottom: 1px solid hsla(210, 2%, 24%, 0.70);
color: hsla(0, 72%, 68%, 0.80);
}
.mh-alert__icon {
font-size: 16px;
flex-shrink: 0;
color: hsla(0, 72%, 60%, 0.80);
}
.mh-alert__content {
display: flex;
flex-direction: column;
gap: 2px;
}
.mh-alert__content strong {
font-size: 13px;
color: hsla(0, 72%, 68%, 0.80);
}
.mh-alert__content span {
font-size: 12px;
color: hsla(0, 72%, 60%, 0.80);
}
.mh-accordion {
border-bottom: 1px solid hsla(210, 2%, 24%, 0.70);
}
.mh-accordion:last-child { border-bottom: none; }
.mh-accordion__summary {
display: flex;
align-items: center;
gap: 10px;
padding: 9px 16px;
cursor: pointer;
user-select: none;
list-style: none;
transition: background 0.15s;
border-left: 3px solid transparent;
}
.mh-accordion__summary::-webkit-details-marker,
.mh-accordion__summary::marker {
display: none;
content: '';
}
.mh-accordion__summary:hover {
background: var(--surface-01);
filter: brightness(1.06);
}
.mh-accordion__summary:focus-visible {
outline: 2px solid hsl(210 100% 50% / 0.5);
outline-offset: -2px;
}
.mh-accordion__summary--pass { border-left-color: hsla(142, 71%, 45%, 0.80); }
.mh-accordion__summary--fail { border-left-color: hsla(0, 72%, 60%, 0.80); }
.mh-accordion__summary--warn { border-left-color: hsla(45, 100%, 51%, 0.80); }
.mh-accordion__summary--na { border-left-color: hsla(0, 0%, 35%, 0.80); }
.mh-accordion__icon { font-size: 14px; flex-shrink: 0; }
.mh-accordion__title {
flex: 1;
font-size: 13px;
font-weight: 500;
color: var(--text-color, hsl(0 0% 80%));
}
.mh-accordion__chevron {
font-size: 10px;
color: var(--text-color, hsl(0 0% 45%));
transition: transform 0.2s ease;
flex-shrink: 0;
}
.mh-accordion[open] > .mh-accordion__summary .mh-accordion__chevron {
transform: rotate(180deg);
}
.mh-accordion__body {
padding: 8px 16px 12px 36px;
background: var(--surface-01);
}
.mh-inline {
display: flex;
align-items: center;
gap: 8px;
padding: 8px 16px;
border-bottom: 1px solid hsla(210, 2%, 24%, 0.70);
border-left: 3px solid transparent;
}
.mh-inline--pass { border-left-color: hsla(142, 71%, 45%, 0.80); }
.mh-inline--fail { border-left-color: hsla(0, 72%, 60%, 0.80); }
.mh-inline--warn { border-left-color: hsla(45, 100%, 51%, 0.80); }
.mh-inline--na { border-left-color: hsla(0, 0%, 35%, 0.80); }
.mh-inline__icon { flex-shrink: 0; font-size: 12px; }
.mh-inline__title {
font-weight: 600;
font-size: 13px;
color: var(--text-color, hsl(0 0% 85%));
}
.mh-inline__msg {
flex: 1;
font-size: 12px;
color: var(--text-color, hsl(0 0% 55%));
}
.mh-accordion--alert > .mh-accordion__summary--fail {
border-left-width: 4px;
animation: mh-pulse 1.8s ease-in-out infinite;
}
@keyframes mh-pulse {
0%, 100% { border-left-color: hsla(0, 72%, 60%, 0.80); }
50% { border-left-color: hsla(0, 90%, 65%, 0.80); }
}
.mh-badge {
font-size: 10px;
font-weight: 700;
padding: 2px 8px;
border-radius: 4px;
text-transform: uppercase;
letter-spacing: 0.5px;
flex-shrink: 0;
line-height: 1.4;
}
.mh-badge--pass { background: hsl(142 71% 45% / 0.12); color: hsla(142, 71%, 52%, 0.80); }
.mh-badge--fail { background: hsl(0 72% 51% / 0.12); color: hsla(0, 72%, 60%, 0.80); }
.mh-badge--warn { background: hsl(45 100% 51% / 0.12); color: hsla(45, 100%, 58%, 0.80); }
.mh-badge--na { background: hsl(0 0% 50% / 0.12); color: hsla(0, 0%, 55%, 0.80); }
.mh-row {
display: flex;
align-items: flex-start;
gap: 8px;
padding: 5px 0;
font-size: 13px;
color: var(--text-color, hsl(0 0% 72%));
flex-wrap: wrap;
}
.mh-row + .mh-row {
border-top: 1px solid hsla(210, 2%, 24%, 0.70);
}
.mh-row__icon { flex-shrink: 0; font-size: 12px; padding-top: 2px; }
.mh-row__label {
font-weight: 600;
color: var(--text-color, hsl(0 0% 78%));
min-width: 120px;
flex-shrink: 0;
font-size: 12.5px;
}
.mh-row__msg { flex: 1; font-size: 12.5px; }
.mh-optional {
font-weight: 400;
font-size: 11px;
color: var(--text-color, hsl(0 0% 50%));
}
.mh-detail {
width: 100%;
padding: 4px 0 2px 20px;
display: flex;
flex-direction: column;
gap: 2px;
}
.mh-detail__item {
font-size: 12px;
color: var(--text-color, hsl(0 0% 58%));
line-height: 1.5;
}
.mh-detail__item strong {
color: var(--text-color, hsl(0 0% 68%));
}
.mh-violations {
width: 100%;
padding: 6px 0 2px 20px;
}
.mh-violations__type {
display: block;
font-size: 11px;
color: var(--text-color, hsl(0 0% 55%));
margin-bottom: 4px;
}
.mh-violations__list {
margin: 0;
padding: 0 0 0 16px;
list-style: disc;
}
.mh-violations__list li {
font-size: 12px;
color: hsl(0 72% 68%);
line-height: 1.6;
}
.mh-icon--pass { color: hsla(142, 71%, 45%, 0.80); }
.mh-icon--fail { color: hsla(0, 72%, 60%, 0.80); }
.mh-icon--warn { color: hsla(45, 100%, 51%, 0.80); }
.mh-icon--na { color: hsla(0, 0%, 42%, 0.80); }
`;
function init() {
const data = {
torrentName: DataExtractor.getTorrentName(),
tmdbTitle: DataExtractor.getTmdbTitle(),
tmdbYear: DataExtractor.getTmdbYear(),
category: DataExtractor.getCategory(),
type: DataExtractor.getType(),
resolution: DataExtractor.getResolution(),
description: DataExtractor.getDescription(),
hasMediaInfo: DataExtractor.hasMediaInfo(),
mediaInfoText: DataExtractor.getMediaInfoText(),
mediaInfoFilename: DataExtractor.getMediaInfoFilename(),
hasBdInfo: DataExtractor.hasBdInfo(),
isTV: DataExtractor.isTV(),
originalLanguage: DataExtractor.getOriginalLanguage(),
mediaInfoLanguages: DataExtractor.getMediaInfoLanguages(),
mediaInfoSubtitles: DataExtractor.getMediaInfoSubtitles(),
fileStructure: DataExtractor.getFileStructure()
};
console.log('[Mod Helper] Extracted data:', data);
const results = {
tmdbMatch: Checks.tmdbNameMatch(data.torrentName, data.tmdbTitle),
seasonEpisode: Checks.seasonEpisodeFormat(data.torrentName, data.isTV),
namingGuide: Checks.namingGuideCompliance(data.torrentName, data.type, data.mediaInfoText),
elementOrder: Checks.titleElementOrder(data.torrentName, data.type),
folderStructure: Checks.movieFolderStructure(data.fileStructure, data.category, data.isTV, data.type),
mediaInfo: Checks.mediaInfoPresent(data.hasMediaInfo, data.hasBdInfo, data.type),
audioTags: Checks.audioTagCompliance(data.torrentName, data.originalLanguage, data.mediaInfoLanguages, data.type, data.mediaInfoText),
subtitleRequirement: Checks.subtitleRequirement(data.mediaInfoLanguages, data.mediaInfoSubtitles, data.originalLanguage, data.type),
screenshots: Checks.screenshotCount(data.description),
bannedGroup: Checks.bannedReleaseGroup(data.torrentName),
encodeCompliance: Checks.encodeCompliance(data.torrentName, data.type, data.mediaInfoText),
upscaleDetection: Checks.upscaleDetection(data.mediaInfoFilename || data.torrentName),
containerFormat: Checks.containerFormat(data.fileStructure, data.type),
packUniformity: Checks.packUniformity(data.fileStructure, data.type)
};
console.log('[Mod Helper] Check results:', results);
GM_addStyle(STYLES);
const panel = UI.createPanel(results);
UI.injectPanel(panel);
console.log('[Mod Helper] Panel injected successfully');
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
}
else {
init();
}
})();