From 6bdbc9c600eb21e9f92bda0a429f939d278d77e8 Mon Sep 17 00:00:00 2001 From: bakerboy448 <55419169+bakerboy448@users.noreply.github.com> Date: Sun, 21 Sep 2025 23:00:15 -0500 Subject: [PATCH] align parsing with upstream Separate release group parsing logic into dedicated classes and update references throughout codebase. (cherry picked from commit b00229e53c7a4bcb8684fd0aa4f66650c64a9a20) Co-Authored-By: Mark McDowall --- .../ParserTests/ReleaseGroupParserFixture.cs | 14 +- .../ParserTests/UrlFixture.cs | 2 +- .../Migration/199_mediainfo_to_ffmpeg.cs | 3 +- .../MediaFiles/FileExtensions.cs | 38 ++++-- .../MediaInfo/MediaInfoFormatter.cs | 2 +- .../MovieImport/Manual/ManualImportService.cs | 6 +- .../MovieImport/SceneNameCalculator.cs | 2 +- src/NzbDrone.Core/Parser/Parser.cs | 121 ++---------------- src/NzbDrone.Core/Parser/ParserCommon.cs | 23 ++++ .../Parser/ReleaseGroupParser.cs | 87 +++++++++++++ 10 files changed, 163 insertions(+), 135 deletions(-) create mode 100644 src/NzbDrone.Core/Parser/ParserCommon.cs create mode 100644 src/NzbDrone.Core/Parser/ReleaseGroupParser.cs diff --git a/src/NzbDrone.Core.Test/ParserTests/ReleaseGroupParserFixture.cs b/src/NzbDrone.Core.Test/ParserTests/ReleaseGroupParserFixture.cs index 970973bca9..c0f02be94f 100644 --- a/src/NzbDrone.Core.Test/ParserTests/ReleaseGroupParserFixture.cs +++ b/src/NzbDrone.Core.Test/ParserTests/ReleaseGroupParserFixture.cs @@ -58,7 +58,7 @@ public class ReleaseGroupParserFixture : CoreTest [TestCase("Movie Name (2017) (Showtime) (1080p.BD.DD5.1.x265-TheSickle[TAoE])", "TheSickle")] public void should_parse_release_group(string title, string expected) { - Parser.Parser.ParseReleaseGroup(title).Should().Be(expected); + Parser.ReleaseGroupParser.ParseReleaseGroup(title).Should().Be(expected); } [TestCase("Movie Name (2020) [2160p x265 10bit S82 Joy]", "Joy")] @@ -128,13 +128,13 @@ public void should_parse_release_group(string title, string expected) [TestCase("Movie Title (2024) (1080p BluRay x265 SDR DDP 5.1 English -BEN THE MEN", "BEN THE MEN")] public void should_parse_exception_release_group(string title, string expected) { - Parser.Parser.ParseReleaseGroup(title).Should().Be(expected); + Parser.ReleaseGroupParser.ParseReleaseGroup(title).Should().Be(expected); } [TestCase(@"C:\Test\Doctor.Series.2005.s01e01.internal.bdrip.x264-archivist.mkv", "archivist")] public void should_not_include_extension_in_release_group(string title, string expected) { - Parser.Parser.ParseReleaseGroup(title).Should().Be(expected); + Parser.ReleaseGroupParser.ParseReleaseGroup(title).Should().Be(expected); } [TestCase("Some.Movie.S02E04.720p.WEBRip.x264-SKGTV English", "SKGTV")] @@ -143,7 +143,7 @@ public void should_not_include_extension_in_release_group(string title, string e public void should_not_include_language_in_release_group(string title, string expected) { - Parser.Parser.ParseReleaseGroup(title).Should().Be(expected); + Parser.ReleaseGroupParser.ParseReleaseGroup(title).Should().Be(expected); } [TestCase("Some.Movie.2019.1080p.BDRip.X264.AC3-EVO-RP", "EVO")] @@ -173,7 +173,7 @@ public void should_not_include_language_in_release_group(string title, string ex public void should_not_include_bad_suffix_in_release_group(string title, string expected) { - Parser.Parser.ParseReleaseGroup(title).Should().Be(expected); + Parser.ReleaseGroupParser.ParseReleaseGroup(title).Should().Be(expected); } [TestCase("[FFF] Invaders of the Movies!! - S01E11 - Someday, With Movies", "FFF")] @@ -184,13 +184,13 @@ public void should_not_include_bad_suffix_in_release_group(string title, string public void should_parse_anime_release_groups(string title, string expected) { - Parser.Parser.ParseReleaseGroup(title).Should().Be(expected); + Parser.ReleaseGroupParser.ParseReleaseGroup(title).Should().Be(expected); } [TestCase("Terrible.Anime.Title.2020.DBOX.480p.x264-iKaos [v3] [6AFFEF6B]")] public void should_not_parse_anime_hash_as_release_group(string title) { - Parser.Parser.ParseReleaseGroup(title).Should().BeNull(); + Parser.ReleaseGroupParser.ParseReleaseGroup(title).Should().BeNull(); } } } diff --git a/src/NzbDrone.Core.Test/ParserTests/UrlFixture.cs b/src/NzbDrone.Core.Test/ParserTests/UrlFixture.cs index f261531a0b..6a1ec22216 100644 --- a/src/NzbDrone.Core.Test/ParserTests/UrlFixture.cs +++ b/src/NzbDrone.Core.Test/ParserTests/UrlFixture.cs @@ -38,7 +38,7 @@ public void should_not_parse_url_in_name(string postTitle, string title) [TestCase("Movie Title Future 2023 DVDRip XviD RUNNER[www.allstate.net]", null)] public void should_not_parse_url_in_group(string title, string expected) { - Parser.Parser.ParseReleaseGroup(title).Should().Be(expected); + Parser.ReleaseGroupParser.ParseReleaseGroup(title).Should().Be(expected); } } } diff --git a/src/NzbDrone.Core/Datastore/Migration/199_mediainfo_to_ffmpeg.cs b/src/NzbDrone.Core/Datastore/Migration/199_mediainfo_to_ffmpeg.cs index 89bf8a7d43..df69accb17 100644 --- a/src/NzbDrone.Core/Datastore/Migration/199_mediainfo_to_ffmpeg.cs +++ b/src/NzbDrone.Core/Datastore/Migration/199_mediainfo_to_ffmpeg.cs @@ -12,6 +12,7 @@ using NzbDrone.Common.Extensions; using NzbDrone.Common.Serializer; using NzbDrone.Core.Datastore.Migration.Framework; +using NzbDrone.Core.MediaFiles; using NzbDrone.Core.MediaFiles.MediaInfo; namespace NzbDrone.Core.Datastore.Migration @@ -809,7 +810,7 @@ private string MigrateTransferCharacteristics(string transferCharacteristics) private static string GetSceneNameMatch(string sceneName, params string[] tokens) { - sceneName = sceneName.IsNotNullOrWhiteSpace() ? Parser.Parser.RemoveFileExtension(sceneName) : string.Empty; + sceneName = sceneName.IsNotNullOrWhiteSpace() ? FileExtensions.RemoveFileExtension(sceneName) : string.Empty; foreach (var token in tokens) { diff --git a/src/NzbDrone.Core/MediaFiles/FileExtensions.cs b/src/NzbDrone.Core/MediaFiles/FileExtensions.cs index ee55e54b23..dbdea5d931 100644 --- a/src/NzbDrone.Core/MediaFiles/FileExtensions.cs +++ b/src/NzbDrone.Core/MediaFiles/FileExtensions.cs @@ -1,11 +1,21 @@ using System; using System.Collections.Generic; +using System.Text.RegularExpressions; namespace NzbDrone.Core.MediaFiles { - internal static class FileExtensions + public static class FileExtensions { - private static List _archiveExtensions = new List + private static readonly Regex FileExtensionRegex = new (@"\.[a-z0-9]{2,4}$", + RegexOptions.IgnoreCase | RegexOptions.Compiled); + + private static readonly HashSet UsenetExtensions = new HashSet() + { + ".par2", + ".nzb" + }; + + public static HashSet ArchiveExtensions => new (StringComparer.OrdinalIgnoreCase) { ".7z", ".bz2", @@ -20,8 +30,7 @@ internal static class FileExtensions ".tgz", ".zip" }; - - private static List _dangerousExtensions = new List + public static HashSet DangerousExtensions => new (StringComparer.OrdinalIgnoreCase) { ".arj", ".lnk", @@ -31,8 +40,7 @@ internal static class FileExtensions ".vbs", ".zipx" }; - - private static List _executableExtensions = new List + public static HashSet ExecutableExtensions => new (StringComparer.OrdinalIgnoreCase) { ".bat", ".cmd", @@ -40,8 +48,20 @@ internal static class FileExtensions ".sh" }; - public static HashSet ArchiveExtensions => new HashSet(_archiveExtensions, StringComparer.OrdinalIgnoreCase); - public static HashSet DangerousExtensions => new HashSet(_dangerousExtensions, StringComparer.OrdinalIgnoreCase); - public static HashSet ExecutableExtensions => new HashSet(_executableExtensions, StringComparer.OrdinalIgnoreCase); + public static string RemoveFileExtension(string title) + { + title = FileExtensionRegex.Replace(title, m => + { + var extension = m.Value.ToLower(); + if (MediaFileExtensions.Extensions.Contains(extension) || UsenetExtensions.Contains(extension)) + { + return string.Empty; + } + + return m.Value; + }); + + return title; + } } } diff --git a/src/NzbDrone.Core/MediaFiles/MediaInfo/MediaInfoFormatter.cs b/src/NzbDrone.Core/MediaFiles/MediaInfo/MediaInfoFormatter.cs index 5c349508f3..6914519fd4 100644 --- a/src/NzbDrone.Core/MediaFiles/MediaInfo/MediaInfoFormatter.cs +++ b/src/NzbDrone.Core/MediaFiles/MediaInfo/MediaInfoFormatter.cs @@ -293,7 +293,7 @@ public static string FormatVideoCodec(MediaInfoModel mediaInfo, string sceneName private static string GetSceneNameMatch(string sceneName, params string[] tokens) { - sceneName = sceneName.IsNotNullOrWhiteSpace() ? Parser.Parser.RemoveFileExtension(sceneName) : string.Empty; + sceneName = sceneName.IsNotNullOrWhiteSpace() ? FileExtensions.RemoveFileExtension(sceneName) : string.Empty; foreach (var token in tokens) { diff --git a/src/NzbDrone.Core/MediaFiles/MovieImport/Manual/ManualImportService.cs b/src/NzbDrone.Core/MediaFiles/MovieImport/Manual/ManualImportService.cs index f0e5beed94..3c70885b10 100644 --- a/src/NzbDrone.Core/MediaFiles/MovieImport/Manual/ManualImportService.cs +++ b/src/NzbDrone.Core/MediaFiles/MovieImport/Manual/ManualImportService.cs @@ -144,7 +144,7 @@ public ManualImportItem ReprocessItem(string path, string downloadId, int movieI var downloadClientItem = GetTrackedDownload(downloadId)?.DownloadItem; var finalReleaseGroup = releaseGroup.IsNullOrWhiteSpace() - ? Parser.Parser.ParseReleaseGroup(path) + ? ReleaseGroupParser.ParseReleaseGroup(path) : releaseGroup; var finalQuality = (quality?.Quality ?? Quality.Unknown) == Quality.Unknown ? QualityParser.ParseQuality(path) : quality; var finalLanguages = @@ -282,7 +282,7 @@ private ManualImportItem ProcessFile(string rootFolder, string baseFolder, strin { var localMovie = new LocalMovie(); localMovie.Path = file; - localMovie.ReleaseGroup = Parser.Parser.ParseReleaseGroup(file); + localMovie.ReleaseGroup = ReleaseGroupParser.ParseReleaseGroup(file); localMovie.Quality = QualityParser.ParseQuality(file); localMovie.Languages = LanguageParser.ParseLanguages(file); localMovie.Size = _diskProvider.GetFileSize(file); @@ -327,7 +327,7 @@ private List ProcessDownloadDirectory(string rootFolder, List< localMovie.Path = file; localMovie.Quality = new QualityModel(Quality.Unknown); localMovie.Languages = new List { Language.Unknown }; - localMovie.ReleaseGroup = Parser.Parser.ParseReleaseGroup(file); + localMovie.ReleaseGroup = ReleaseGroupParser.ParseReleaseGroup(file); localMovie.Size = _diskProvider.GetFileSize(file); items.Add(MapItem(new ImportDecision(localMovie), rootFolder, null, null)); diff --git a/src/NzbDrone.Core/MediaFiles/MovieImport/SceneNameCalculator.cs b/src/NzbDrone.Core/MediaFiles/MovieImport/SceneNameCalculator.cs index 609155bd38..b23c5a61e9 100644 --- a/src/NzbDrone.Core/MediaFiles/MovieImport/SceneNameCalculator.cs +++ b/src/NzbDrone.Core/MediaFiles/MovieImport/SceneNameCalculator.cs @@ -14,7 +14,7 @@ public static string GetSceneName(LocalMovie localMovie) if (!otherVideoFiles && downloadClientInfo != null) { - return Parser.Parser.RemoveFileExtension(downloadClientInfo.ReleaseTitle); + return FileExtensions.RemoveFileExtension(downloadClientInfo.ReleaseTitle); } var fileName = Path.GetFileNameWithoutExtension(localMovie.Path.CleanFilePath()); diff --git a/src/NzbDrone.Core/Parser/Parser.cs b/src/NzbDrone.Core/Parser/Parser.cs index 3fbc1d5884..ed82f00119 100644 --- a/src/NzbDrone.Core/Parser/Parser.cs +++ b/src/NzbDrone.Core/Parser/Parser.cs @@ -6,6 +6,7 @@ using NLog; using NzbDrone.Common.Extensions; using NzbDrone.Common.Instrumentation; +using NzbDrone.Core.MediaFiles; using NzbDrone.Core.Parser.Model; namespace NzbDrone.Core.Parser @@ -21,8 +22,6 @@ public static class Parser private static readonly Regex HardcodedSubsRegex = new Regex(@"\b((?(\w+(?(HC|SUBBED)))\b", RegexOptions.Compiled | RegexOptions.IgnoreCase | RegexOptions.IgnorePatternWhitespace); - private static readonly RegexReplace[] PreSubstitutionRegex = Array.Empty(); - private static readonly Regex[] ReportMovieTitleRegex = new[] { // Anime [Subgroup] and Year @@ -110,9 +109,6 @@ public static class Parser private static readonly Regex NormalizeRegex = new Regex(@"((?:\b|_)(?tt\d{7,8})", RegexOptions.IgnoreCase | RegexOptions.Compiled); private static readonly Regex ReportTmdbId = new Regex(@"tmdb(id)?-(?\d+)", RegexOptions.IgnoreCase | RegexOptions.Compiled); @@ -123,44 +119,13 @@ public static class Parser private static readonly Regex SimpleReleaseTitleRegex = new Regex(@"\s*(?:[<>?*|])", RegexOptions.Compiled | RegexOptions.IgnoreCase); // Valid TLDs http://data.iana.org/TLD/tlds-alpha-by-domain.txt - private static readonly RegexReplace WebsitePrefixRegex = new RegexReplace(@"^(?:(?:\[|\()\s*)?(?:www\.)?[-a-z0-9-]{1,256}\.(?[a-z0-9]+(?-[a-z0-9]+)?(?!.+?(?:480p|576p|720p|1080p|2160p)))(?\d+)|(?tt\d{7,8}))(?:\k)?)(?:\b|[-._ ]|$)|[-._ ]\[(?[a-z0-9]+)\]$", - RegexOptions.IgnoreCase | RegexOptions.Compiled); - - private static readonly Regex InvalidReleaseGroupRegex = new Regex(@"^([se]\d+|[0-9a-f]{8})$", RegexOptions.IgnoreCase | RegexOptions.Compiled); - - private static readonly Regex AnimeReleaseGroupRegex = new Regex(@"^(?:\[(?(?!\s).+?(?.+?)(?:\W|_.)?[\(\[]?(?\d{4})[\]\)]?", RegexOptions.IgnoreCase | RegexOptions.Compiled); - // Handle Exception Release Groups that don't follow -RlsGrp; Manual List - // groups whose releases end with RlsGroup) or RlsGroup] - private static readonly Regex ExceptionReleaseGroupRegex = new Regex(@"(?<=[._ \[])(?(Silence|afm72|Panda|Ghost|MONOLITH|Tigole|Joy|ImE|UTR|t3nzin|Anime Time|Project Angel|Hakata Ramen|HONE|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|Garshasp)(?=\]|\)))", RegexOptions.IgnoreCase | RegexOptions.Compiled); - - // Handle Exception Release Groups that don't follow -RlsGrp; Manual List - // name only...BE VERY CAREFUL WITH THIS, HIGH CHANCE OF FALSE POSITIVES - private static readonly Regex ExceptionReleaseGroupRegexExact = new Regex(@"\b(?KRaLiMaRKo|E\.N\.D|D\-Z0N3|Koten_Gars|BluDragon|ZØNEHD|Tigole|HQMUX|VARYG|YIFY|YTS(.(MX|LT|AG))?|TMd|Eml HDTeam|LMain|DarQ|BEN THE MEN)\b", RegexOptions.IgnoreCase | RegexOptions.Compiled); - private static readonly Regex SpecialCharRegex = new Regex(@"(\&|\:|\\|\/)+", RegexOptions.Compiled); private static readonly Regex PunctuationRegex = new Regex(@"[^\w\s]", RegexOptions.Compiled); private static readonly Regex ArticleWordRegex = new Regex(@"^(a|an|the)\s", RegexOptions.IgnoreCase | RegexOptions.Compiled); @@ -215,7 +180,7 @@ public static ParsedMovieInfo ParseMovieTitle(string title, bool isDir = false) if (ReversedTitleRegex.IsMatch(title)) { - var titleWithoutExtension = RemoveFileExtension(title).ToCharArray(); + var titleWithoutExtension = FileExtensions.RemoveFileExtension(title).ToCharArray(); Array.Reverse(titleWithoutExtension); title = $"{titleWithoutExtension}{title.Substring(titleWithoutExtension.Length)}"; @@ -223,14 +188,14 @@ public static ParsedMovieInfo ParseMovieTitle(string title, bool isDir = false) Logger.Debug("Reversed name detected. Converted to '{0}'", title); } - var releaseTitle = RemoveFileExtension(title); + var releaseTitle = FileExtensions.RemoveFileExtension(title); // Trim dashes from end releaseTitle = releaseTitle.Trim('-', '_'); releaseTitle = releaseTitle.Replace("【", "[").Replace("】", "]"); - foreach (var replace in PreSubstitutionRegex) + foreach (var replace in ParserCommon.PreSubstitutionRegex) { if (replace.TryReplace(ref releaseTitle)) { @@ -242,10 +207,10 @@ public static ParsedMovieInfo ParseMovieTitle(string title, bool isDir = false) var simpleTitle = SimpleTitleRegex.Replace(releaseTitle); // TODO: Quick fix stripping [url] - prefixes. - simpleTitle = WebsitePrefixRegex.Replace(simpleTitle); - simpleTitle = WebsitePostfixRegex.Replace(simpleTitle); + simpleTitle = ParserCommon.WebsitePrefixRegex.Replace(simpleTitle); + simpleTitle = ParserCommon.WebsitePostfixRegex.Replace(simpleTitle); - simpleTitle = CleanTorrentSuffixRegex.Replace(simpleTitle); + simpleTitle = ParserCommon.CleanTorrentSuffixRegex.Replace(simpleTitle); simpleTitle = CleanQualityBracketsRegex.Replace(simpleTitle, m => { @@ -295,7 +260,7 @@ public static ParsedMovieInfo ParseMovieTitle(string title, bool isDir = false) } } - result.ReleaseGroup = ParseReleaseGroup(simpleReleaseTitle); + result.ReleaseGroup = ReleaseGroupParser.ParseReleaseGroup(simpleReleaseTitle); var subGroup = GetSubGroup(match); if (!subGroup.IsNullOrWhiteSpace()) @@ -521,74 +486,6 @@ public static string ParseHardcodeSubs(string title) return null; } - public static string ParseReleaseGroup(string title) - { - title = title.Trim(); - title = RemoveFileExtension(title); - title = WebsitePrefixRegex.Replace(title); - title = CleanTorrentSuffixRegex.Replace(title); - - var animeMatch = AnimeReleaseGroupRegex.Match(title); - - if (animeMatch.Success) - { - return animeMatch.Groups["subgroup"].Value; - } - - title = CleanReleaseGroupRegex.Replace(title); - - var exceptionReleaseGroupRegex = ExceptionReleaseGroupRegex.Matches(title); - - if (exceptionReleaseGroupRegex.Count != 0) - { - return exceptionReleaseGroupRegex.OfType().Last().Groups["releasegroup"].Value; - } - - var exceptionExactMatch = ExceptionReleaseGroupRegexExact.Matches(title); - - if (exceptionExactMatch.Count != 0) - { - return exceptionExactMatch.OfType().Last().Groups["releasegroup"].Value; - } - - var matches = ReleaseGroupRegex.Matches(title); - - if (matches.Count != 0) - { - var group = matches.OfType().Last().Groups["releasegroup"].Value; - - if (int.TryParse(group, out _)) - { - return null; - } - - if (InvalidReleaseGroupRegex.IsMatch(group)) - { - return null; - } - - return group; - } - - return null; - } - - public static string RemoveFileExtension(string title) - { - title = FileExtensionRegex.Replace(title, m => - { - var extension = m.Value.ToLower(); - if (MediaFiles.MediaFileExtensions.Extensions.Contains(extension) || new[] { ".par2", ".nzb" }.Contains(extension)) - { - return string.Empty; - } - - return m.Value; - }); - - return title; - } - public static bool HasMultipleLanguages(string title) { return MultiRegex.IsMatch(title); @@ -697,7 +594,7 @@ private static bool ValidateBeforeParsing(string title) return false; } - var titleWithoutExtension = RemoveFileExtension(title); + var titleWithoutExtension = FileExtensions.RemoveFileExtension(title); if (RejectHashedReleasesRegex.Any(v => v.IsMatch(titleWithoutExtension))) { diff --git a/src/NzbDrone.Core/Parser/ParserCommon.cs b/src/NzbDrone.Core/Parser/ParserCommon.cs new file mode 100644 index 0000000000..40dcaa8f2d --- /dev/null +++ b/src/NzbDrone.Core/Parser/ParserCommon.cs @@ -0,0 +1,23 @@ +using System.Text.RegularExpressions; + +namespace NzbDrone.Core.Parser; + +// These are functions shared between different parser functions +// they are not intended to be used outside of them parsing. +internal static class ParserCommon +{ + internal static readonly RegexReplace[] PreSubstitutionRegex = System.Array.Empty(); + + // Valid TLDs http://data.iana.org/TLD/tlds-alpha-by-domain.txt + internal static readonly RegexReplace WebsitePrefixRegex = new (@"^(?:(?:\[|\()\s*)?(?:www\.)?[-a-z0-9-]{1,256}\.(?[a-z0-9]+(?-[a-z0-9]+)?(?!.+?(?:480p|576p|720p|1080p|2160p)))(?\d+)|(?tt\d{7,8}))(?:\k)?)(?:\b|[-._ ]|$)|[-._ ]\[(?[a-z0-9]+)\]$", + RegexOptions.IgnoreCase | RegexOptions.Compiled); + + private static readonly Regex InvalidReleaseGroupRegex = new (@"^([se]\d+|[0-9a-f]{8})$", RegexOptions.IgnoreCase | RegexOptions.Compiled); + + private static readonly Regex AnimeReleaseGroupRegex = new (@"^(?:\[(?(?!\s).+?(?KRaLiMaRKo|E\.N\.D|D\-Z0N3|Koten_Gars|BluDragon|ZØNEHD|Tigole|HQMUX|VARYG|YIFY|YTS(.(MX|LT|AG))?|TMd|Eml HDTeam|LMain|DarQ|BEN THE MEN)\b", RegexOptions.IgnoreCase | RegexOptions.Compiled); + + // groups whose releases end with RlsGroup) or RlsGroup] + private static readonly Regex ExceptionReleaseGroupRegex = new (@"(?<=[._ \[])(?(Silence|afm72|Panda|Ghost|MONOLITH|Tigole|Joy|ImE|UTR|t3nzin|Anime Time|Project Angel|Hakata Ramen|HONE|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)(?=\]|\)))", RegexOptions.IgnoreCase | RegexOptions.Compiled); + + private static readonly RegexReplace CleanReleaseGroupRegex = new (@"(-(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))+$", + string.Empty, + RegexOptions.IgnoreCase | RegexOptions.Compiled); + + public static string ParseReleaseGroup(string title) + { + title = title.Trim(); + title = FileExtensions.RemoveFileExtension(title); + foreach (var replace in ParserCommon.PreSubstitutionRegex) + { + if (replace.TryReplace(ref title)) + { + break; + } + } + + title = ParserCommon.WebsitePrefixRegex.Replace(title); + title = ParserCommon.CleanTorrentSuffixRegex.Replace(title); + + var animeMatch = AnimeReleaseGroupRegex.Match(title); + + if (animeMatch.Success) + { + return animeMatch.Groups["subgroup"].Value; + } + + title = CleanReleaseGroupRegex.Replace(title); + + var exceptionReleaseGroupRegex = ExceptionReleaseGroupRegex.Matches(title); + + if (exceptionReleaseGroupRegex.Count != 0) + { + return exceptionReleaseGroupRegex.OfType().Last().Groups["releasegroup"].Value; + } + + var exceptionExactMatch = ExceptionReleaseGroupRegexExact.Matches(title); + + if (exceptionExactMatch.Count != 0) + { + return exceptionExactMatch.OfType().Last().Groups["releasegroup"].Value; + } + + var matches = ReleaseGroupRegex.Matches(title); + + if (matches.Count != 0) + { + var group = matches.OfType().Last().Groups["releasegroup"].Value; + + if (int.TryParse(group, out _)) + { + return null; + } + + if (InvalidReleaseGroupRegex.IsMatch(group)) + { + return null; + } + + return group; + } + + return null; + } +}