cleaner = $cleaner ?? new FileNameCleaner; } /** * Extract a release name from a filename. */ public function extractFromFile(string $filename): ?NameFixResult { $result = []; $cleanedFilename = $this->cleaner->cleanForTitleMatch($filename); // Try each pattern in order of specificity if (str_contains($filename, '__NZBSPLIT__')) { $unwrapped = $this->cleaner->extractNzbSplitName($filename); if ($unwrapped !== null && $this->cleaner->isPlausibleReleaseTitle($unwrapped)) { return NameFixResult::fromMatch($unwrapped, 'NZBSPLIT wrapper', 'File'); } return null; } // Scene TV release with group suffix if (preg_match('/^(.+?(x264|x265|HEVC|XviD|H\.?264|H\.?265)\-[A-Za-z0-9]+)\\\\/i', $filename, $result)) { return NameFixResult::fromMatch($result[1], 'Scene release with group', 'File'); } // TVP group format if (preg_match('/^(.+?(x264|XviD)\-TVP)\\\\/i', $filename, $result)) { return NameFixResult::fromMatch($result[1], 'TVP', 'File'); } // Generic TV - SxxExx format with quality/source info if (preg_match('/^(\\\\|\/)?(.+(\\\\|\/))*(.+?S\d{1,3}[.-_ ]?E\d{1,3}(?:[.-_ ]?E\d{1,3})?[.-_ ].+?(?:720p|1080p|2160p|4K|HDTV|WEB-?DL|WEB-?RIP|BluRay|AMZN|HMAX|NF|DSNP).+?)\.(.+)$/iu', $filename, $result)) { return NameFixResult::fromMatch($result[4], 'TV SxxExx with quality', 'File'); } // Generic TV - any SxxExx format if (preg_match('/^(\\\\|\/)?(.+(\\\\|\/))*(.+?S\d{1,3}[.-_ ]?[ED]\d{1,3}.+)\.(.+)$/iu', $filename, $result)) { return NameFixResult::fromMatch($result[4], 'Generic TV', 'File'); } // 4K/UHD Movies - modern formats if (preg_match('/^(\\\\|\/)?(.+(\\\\|\/))*(.+?[\.\-_ ](19|20)\d\d[\.\-_ ].+?(2160p|4K|UHD).+?(HDR10?\+?|DV|Dolby[\.\-_ ]?Vision)?.+?(HEVC|x265|H\.?265).+?)\.(.+)$/iu', $filename, $result)) { return NameFixResult::fromMatch($result[4], '4K/UHD Movie', 'File'); } // HD Movies with modern codecs if (preg_match('/^(\\\\|\/)?(.+(\\\\|\/))*(.+?[\.\-_ ](19|20)\d\d[\.\-_ ].+?(720p|1080p).+?(BluRay|WEB-?DL|WEB-?RIP|BDRip|REMUX).+?(x264|x265|HEVC|H\.?264|H\.?265|AVC).+?)\.(.+)$/iu', $filename, $result)) { return NameFixResult::fromMatch($result[4], 'HD Movie modern codec', 'File'); } // Standard HD Movies if (preg_match('/^(\\\\|\/)?(.+(\\\\|\/))*(.+?([\.\-_ ]\d{4}[\.\-_ ].+?(BDRip|bluray|DVDRip|XVID|WEB-?DL|HDTV)).+)\.(.+)$/iu', $filename, $result)) { return NameFixResult::fromMatch($result[4], 'Generic movie 1', 'File'); } if (preg_match('/^([a-z0-9\.\-_]+(19|20)\d\d[a-z0-9\.\-_]+[\.\-_ ](720p|1080p|2160p|4K|BDRip|bluray|DVDRip|x264|x265|XviD|HEVC)[a-z0-9\.\-_]+)\.[a-z]{2,}$/i', $filename, $result)) { return NameFixResult::fromMatch($result[1], 'Generic movie 2', 'File'); } // Streaming service releases if (preg_match('/^([A-Za-z0-9\.\-_]+[\.\-_ ](AMZN|ATVP|DSNP|HMAX|HULU|iT|NF|PMTP|PCOK|ROKU|STAN|TVNZ|VUDU)[\.\-_ ].+?(WEB-?DL|WEB-?RIP).+?)\.(.+)$/i', $filename, $result)) { return NameFixResult::fromMatch($result[1], 'Streaming service release', 'File'); } // Music releases if (preg_match('/(.+?([\.\-_ ](CD|FM)|[\.\-_ ]\dCD|CDR|FLAC|SAT|WEB).+?(19|20)\d\d.+?)\\\\.+/i', $filename, $result)) { return NameFixResult::fromMatch($result[1], 'Generic music', 'File'); } if (preg_match('/^(.+?(19|20)\d\d\-([a-z0-9]{3}|[a-z]{2,}|C4))\\\\/i', $filename, $result)) { return NameFixResult::fromMatch($result[1], 'music groups', 'File'); } // FLAC music releases if (preg_match('/^(.+?[\.\-_ ](FLAC|MP3|AAC|OGG)[\.\-_ ].+?[\.\-_ ]\d{4}[\.\-_ ].+?\-[A-Za-z0-9]+)[\\\\\/.]/i', $filename, $result)) { return NameFixResult::fromMatch($result[1], 'Music with codec', 'File'); } // Movie with year in parentheses - AVI format if (preg_match('/.+\\\\(.+\((19|20)\d\d\)\.avi)$/i', $filename, $result)) { $newName = str_replace('.avi', ' DVDRip XVID NoGroup', $result[1]); return NameFixResult::fromMatch($newName, 'Movie (year) avi', 'File'); } // Movie with year in parentheses - ISO format if (preg_match('/.+\\\\(.+\((19|20)\d\d\)\.iso)$/i', $filename, $result)) { $newName = str_replace('.iso', ' DVD NoGroup', $result[1]); return NameFixResult::fromMatch($newName, 'Movie (year) iso', 'File'); } // Movie with year in parentheses - MKV format if (preg_match('/.+\\\\(.+\((19|20)\d\d\)\.(mkv|mp4|m4v))$/i', $filename, $result)) { $newName = preg_replace('/\.(mkv|mp4|m4v)$/i', ' BDRip x264 NoGroup', $result[1]); return NameFixResult::fromMatch($newName, 'Movie (year) mkv/mp4', 'File'); } // RAR file contents - look for release name in RAR path if (preg_match('/^([A-Za-z0-9][\w.\-]+(?:[\.\-_ ][\w.\-]+)+)[\\\\\\/](?:CD\d|Disc\d|DVD\d|Subs?)?[\\\\\\/]?.+\.(rar|r\d{2,3}|zip|7z)$/i', $filename, $result)) { return NameFixResult::fromMatch($result[1], 'RAR archive path', 'File'); } // Scene release in RAR if (preg_match('/^([A-Za-z0-9][\w.\-]+\-[A-Za-z0-9]+)[\\\\\\/].+\.(rar|r\d{2,3})$/i', $filename, $result)) { return NameFixResult::fromMatch($result[1], 'Scene RAR release', 'File'); } // XXX Imagesets if (preg_match('/^(.+?IMAGESET.+?)\\\\.+/i', $filename, $result)) { return NameFixResult::fromMatch($result[1], 'XXX Imagesets', 'File'); } // VIDEOOT releases if (preg_match('/^VIDEOOT-[A-Z0-9]+\\\\([\w!.,& ()\[\]\'\`-]{8,}?\b.?)([\-_](proof|sample|thumbs?))*(\.part\d*(\.rar)?|\.rar|\.7z)?(\d{1,3}\.rev|\.vol.+?|\.mp4)/', $filename, $result)) { return NameFixResult::fromMatch($result[1].' XXX DVDRIP XviD-VIDEOOT', 'XXX XviD VIDEOOT', 'File'); } // XXX SDPORN if (preg_match('/^.+?SDPORN/i', $filename, $result)) { return NameFixResult::fromMatch($result[0], 'XXX SDPORN', 'File'); } // R&C releases if (preg_match('/\w[\-\w.\',;& ]+1080i[._ -]DD5[._ -]1[._ -]MPEG2-R&C(?=\.ts)$/i', $filename, $result)) { $newResult = str_replace('MPEG2', 'MPEG2.HDTV', $result[0]); return NameFixResult::fromMatch($newResult, 'R&C', 'File'); } // NhaNc3 releases if (preg_match('/\w[\-\w.\',;& ]+((s\d{1,2}[._ -]?[bde]\d{1,2})|\d{1,2}x\d{2}|ep[._ -]?\d{2})[._ -](480|720|1080)[ip][._ -](BD(-?(25|50|RIP))?|Blu-?Ray ?(3D)?|BRRIP|CAM(RIP)?|DBrip|DTV|DVD\-?(5|9|(R(IP)?|scr(eener)?))?|[HPS]D?(RIP|TV(RIP)?)?|NTSC|PAL|R5|Ripped |S?VCD|scr(eener)?|SAT(RIP)?|TS|VHS(RIP)?|VOD|WEB-DL)[._ -]nSD[._ -](DivX|[HX][._ -]?264|MPEG2|XviD(HD)?|WMV)[._ -]NhaNC3[\-\w.\',;& ]+\w/i', $filename, $result)) { return NameFixResult::fromMatch($result[0], 'NhaNc3', 'File'); } // TVP releases (alternate pattern) if (preg_match('/\wtvp-[\w.\',;]+((s\d{1,2}[._ -]?[bde]\d{1,2})|\d{1,2}x\d{2}|ep[._ -]?\d{2})[._ -](720p|1080p|xvid)(?=\.(avi|mkv))$/i', $filename, $result)) { $newResult = str_replace('720p', '720p.HDTV.X264', $result[0]); $newResult = str_replace('1080p', '1080p.Bluray.X264', $newResult); $newResult = str_replace('xvid', 'XVID.DVDrip', $newResult); return NameFixResult::fromMatch($newResult, 'tvp', 'File'); } // LOL releases if (preg_match('/\w[\-\w.\',;& ]+\d{3,4}\.hdtv-lol\.(avi|mp4|mkv|ts|nfo|nzb)/i', $filename, $result)) { return NameFixResult::fromMatch($result[0], 'Title.211.hdtv-lol.extension', 'File'); } // DL releases if (preg_match('/\w[\-\w.\',;& ]+-S\d{1,2}[EX]\d{1,2}-XVID-DL\.avi/i', $filename, $result)) { return NameFixResult::fromMatch($result[0], 'Title-SxxExx-XVID-DL.avi', 'File'); } // Title - SxxExx - Episode title format if (preg_match('/\S.*[\w.\-\',;]+\s\-\ss\d{2}[ex]\d{2}\s\-\s[\w.\-\',;].+\./i', $filename, $result)) { return NameFixResult::fromMatch($result[0], 'Title - SxxExx - Eptitle', 'File'); } // Nintendo DS if (preg_match('/\w.+?\)\.nds$/i', $filename, $result)) { return NameFixResult::fromMatch($result[0], ').nds Nintendo DS', 'File'); } // Nintendo 3DS if (preg_match('/3DS_\d{4}.+\d{4} - (.+?)\.3ds/i', $filename, $result)) { return NameFixResult::fromMatch('3DS '.$result[1], '.3ds Nintendo 3DS', 'File'); } // Nintendo Switch if (preg_match('/^(.+?)\[[\w]+\]\.(?:nsp|xci|nsz)$/i', $filename, $result)) { return NameFixResult::fromMatch(trim($result[1]).' Switch', 'Nintendo Switch', 'File'); } // PlayStation/Xbox game releases if (preg_match('/^(.+?[\.\-_ ](PS[345P]|PSV|XBOX360|XBOXONE|NSW)[\.\-_ ].+?\-[A-Za-z0-9]+)[\\\\\/.]/i', $filename, $result)) { return NameFixResult::fromMatch($result[1], 'Console game release', 'File'); } // EBooks if (preg_match('/\w.+?\.(epub|mobi|azw3?|opf|fb2|prc|djvu|cb[rz])/i', $filename, $result)) { $newResult = str_replace('.'.$result[1], ' ('.$result[1].')', $result[0]); return NameFixResult::fromMatch($newResult, 'EBook', 'File'); } // Audiobooks if (preg_match('/^(.+?[\.\-_ ]Audiobook[\.\-_ ].+?)[\\\\\/.]/i', $filename, $result)) { return NameFixResult::fromMatch($result[1], 'Audiobook', 'File'); } // Scene release from cleaned filename if (preg_match('/^([A-Za-z0-9][\w.\-]+\-[A-Za-z0-9]{2,15})$/i', $cleanedFilename, $result) && preg_match(self::PREDB_REGEX, $cleanedFilename)) { return NameFixResult::fromMatch($result[1], 'Cleaned scene name', 'File'); } // Folder name fallback if (preg_match('/\w+[\-\w.\',;& ]+$/i', $filename, $result) && preg_match(self::PREDB_REGEX, $filename)) { return NameFixResult::fromMatch($result[0], 'Folder name', 'File'); } return null; } }