diff --git a/src/NzbDrone.Core.Test/ParserTests/ReleaseGroupParserFixture.cs b/src/NzbDrone.Core.Test/ParserTests/ReleaseGroupParserFixture.cs index e3d1163e1..dd6c4b0eb 100644 --- a/src/NzbDrone.Core.Test/ParserTests/ReleaseGroupParserFixture.cs +++ b/src/NzbDrone.Core.Test/ParserTests/ReleaseGroupParserFixture.cs @@ -50,17 +50,17 @@ namespace NzbDrone.Core.Test.ParserTests [TestCase("Series Title S01 REMUX Dual Audio AVC 1080p 8-Bit-ZR-", "ZR")] 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("Show.Name.2009.S01.1080p.BluRay.DTS5.1.x264-D-Z0N3", "D-Z0N3")] [TestCase("Show.Name.S01E01.1080p.WEB-DL.H264.Fight-BB.mkv", "Fight-BB")] - [TestCase("Show Name (2021) Season 1 S01 (1080p BluRay x265 HEVC 10bit AAC 5.1 Tigole) [QxR]", "Tigole")] - [TestCase("Show Name (2021) Season 1 S01 (1080p BluRay x265 HEVC 10bit AAC 2.0 afm72) [QxR]", "afm72")] - [TestCase("Show Name (2021) Season 1 S01 (1080p DSNP WEB-DL x265 HEVC 10bit EAC3 5.1 Silence) [QxR]", "Silence")] - [TestCase("Show Name (2021) Season 1 S01 (1080p BluRay x265 HEVC 10bit AAC 2.0 Panda) [QxR]", "Panda")] - [TestCase("Show Name (2020) Season 1 S01 (1080p AMZN WEB-DL x265 HEVC 10bit EAC3 2.0 Ghost) [QxR]", "Ghost")] - [TestCase("Show Name (2020) Season 1 S01 (1080p WEB-DL x265 HEVC 10bit AC3 5.1 MONOLITH) [QxR]", "MONOLITH")] + [TestCase("Show Name (2021) Season 1 S01 (1080p BluRay x265 HEVC 10bit AAC 5.1 Tigole) [QxR]", "QxR")] + [TestCase("Show Name (2021) Season 1 S01 (1080p BluRay x265 HEVC 10bit AAC 2.0 afm72) [QxR]", "QxR")] + [TestCase("Show Name (2021) Season 1 S01 (1080p DSNP WEB-DL x265 HEVC 10bit EAC3 5.1 Silence) [QxR]", "QxR")] + [TestCase("Show Name (2021) Season 1 S01 (1080p BluRay x265 HEVC 10bit AAC 2.0 Panda) [QxR]", "QxR")] + [TestCase("Show Name (2020) Season 1 S01 (1080p AMZN WEB-DL x265 HEVC 10bit EAC3 2.0 Ghost) [QxR]", "QxR")] + [TestCase("Show Name (2020) Season 1 S01 (1080p WEB-DL x265 HEVC 10bit AC3 5.1 MONOLITH) [QxR]", "QxR")] [TestCase("The Show S08E09 The Series.1080p.AMZN.WEB-DL.x265.10bit.EAC3.6.0-Qman[UTR]", "UTR")] [TestCase("The Show S03E07 Fire and Series[1080p x265 10bit S87 Joy]", "Joy")] [TestCase("The Show (2016) - S02E01 - Soul Series #1 (1080p NF WEBRip x265 ImE)", "ImE")] @@ -85,7 +85,7 @@ namespace NzbDrone.Core.Test.ParserTests [TestCase("Series Title (2012) - S01E01 - Episode 1 (1080p BluRay x265 r00t).mkv", "r00t")] [TestCase("Series Title - S01E01 - Girls Gone Wild Exposed (720p x265 EDGE2020).mkv", "EDGE2020")] [TestCase("Series.Title.S01E02.1080p.BluRay.Remux.AVC.FLAC.2.0-E.N.D", "E.N.D")] - [TestCase("Show Name (2016) Season 1 S01 (1080p AMZN WEB-DL x265 HEVC 10bit EAC3 5 1 RZeroX) QxR", "RZeroX")] + [TestCase("Show Name (2016) Season 1 S01 (1080p AMZN WEB-DL x265 HEVC 10bit EAC3 5 1 RZeroX) QxR", "QxR")] [TestCase("Series Title S01 1080p Blu-ray Remux AVC FLAC 2.0 - KRaLiMaRKo", "KRaLiMaRKo")] [TestCase("Series Title S01 1080p Blu-ray Remux AVC DTS-HD MA 2.0 - BluDragon", "BluDragon")] [TestCase("Example (2013) S01E01 (1080p iP WEBRip x265 SDR AAC 2.0 English - DarQ)", "DarQ")] @@ -95,9 +95,30 @@ namespace NzbDrone.Core.Test.ParserTests [TestCase("Series.S01E05.1080p.WEB-DL.DDP5.1.H264-BEN.THE.MEN", "BEN.THE.MEN")] [TestCase("Series (2022) S01 (1080p BluRay x265 SDR DDP 5.1 English - JBENT TAoE)", "TAoE")] [TestCase("Series (2005) S21E12 (1080p AMZN WEB-DL x265 SDR DDP 5.1 English - Goki TAoE)", "TAoE")] + [TestCase("Series (2022) S03E12 (1080p AMZN Webrip x265 10 bit EAC3 5 1 - Ainz)[TAoE]", "TAoE")] + [TestCase("Series Things (2016) S04 Part 1 (1080p Webrip NF x265 10bit EAC3 5 1 - AJJMIN) [TAoE]", "TAoE")] + [TestCase("Series Soup (2024) S01 (1080p NF Webrip x265 10bit EAC3 5 1 Multi - ANONAZ)[TAoE]", "TAoE")] + [TestCase("Series (2022) S01 (1080p NF Webrip x265 10bit EAC3 5 1 Atmos - ArcX)[TAoE]", "TAoE")] + [TestCase("Series - King of Titles (2021) S01 (1080p HMAX Webrip x265 10bit AC3 5 1 - bccornfo) [TAoE]", "TAoE")] + [TestCase("Welcome to Series (2022) S04 (1080p AMZN Webrip x265 10bit EAC3 5 1 - DNU)[TAoE]", "TAoE")] + [TestCase("Series Who (2005) S01 (1080p BDRip x265 10bit AC3 5 1 - DrainedDay)[TAoE]", "TAoE")] + [TestCase("Series Down (2019) (1080p AMZN Webrip x265 10bit EAC3 5 1 - DUHiT)[TAoE]", "TAoE")] + [TestCase("Series (2016) S09 (1080p CRAV Webrip x265 10bit EAC3 5 1 - Erie) [TAoE]", "TAoE")] + [TestCase("Common Series Effects (2025) S01 (1080p AMZN Webrip x265 10bit EAC3 2 0 - Frys) [TAoE]", "TAoE")] + [TestCase("Murderbot (2025) S01 (2160p HDR10 DV Hybrid ATVP Webrip x265 10bit EAC3 5 1 Atmos - Goki)[TAoE]", "TAoE")] + [TestCase("Series In Real Life (2019) S01 REPACK (1080p DSNP Webrip x265 10bit AAC 2 0 - HxD)[TAoE]", "TAoE")] + [TestCase("Series Discovery (2017) S02 (1080p BDRip x265 10bit DTS-HD MA 5 1 - jb2049) [TAoE]", "TAoE")] + [TestCase("Series (2021) S03 (1080p DS4K NF Webrip x265 10bit EAC3 5 1 Atmos English - JBENT)[TAoE]", "TAoE")] + [TestCase("SuSeriespergirl (2015) S04 (1080p BDRip x265 10bit AC3 5 1 - Nostradamus)[TAoE]", "TAoE")] + [TestCase("Series (2019) S02 (4Kto1080p ATVP Webrip x265 10bit AC3 5 1 - r0b0t) [TAoE]", "TAoE")] + [TestCase("v (1970) S01 (2160p AIUS HDR10 DV Hybrid BDRip x265 10bit DTS-HD MA 5 1 - Species180) [TAoE]", "TAoE")] + [TestCase("Series (2024) S02 (1080p ATVP Webrip x265 10bit EAC3 5 1 - TheSickle)[TAoE]", "TAoE")] + [TestCase("Series (2016) S05 Part 02 (1080p NF Webrip x265 10bit EAC3 5 1 - xtrem3x) [TAoE]", "TAoE")] + [TestCase("Series (2013) S01 (1080p BDRip x265 10bit DTS-HD MA 5 1 - WEM)[TAoE]", "TAoE")] + [TestCase("The.Series.1989.S00E65.1080p.DSNP.Webrip.x265.10bit.EAC3.5.1.Goki.TAoE", "TAoE")] 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); } [Test] @@ -115,7 +136,7 @@ namespace NzbDrone.Core.Test.ParserTests // [TestCase("", "")] 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("Series.Title.S02E04.720p.WEB-DL.AAC2.0.H.264-EVL-RP", "EVL")] @@ -146,7 +167,7 @@ namespace NzbDrone.Core.Test.ParserTests [TestCase("Series.Title.S04E06.Episode.Name.720p.WEB-DL.DD5.1.H.264-HarrHD-RePACKPOST", "HarrHD")] public void should_not_include_repost_in_release_group(string title, string expected) { - Parser.Parser.ParseReleaseGroup(title).Should().Be(expected); + Parser.ReleaseGroupParser.ParseReleaseGroup(title).Should().Be(expected); } [TestCase("[FFF] Series Title!! - S01E11 - Someday, With Sonarr", "FFF")] @@ -159,13 +180,13 @@ namespace NzbDrone.Core.Test.ParserTests // [TestCase("", "")] 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.001.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 08d9bb822..2f8764d4b 100644 --- a/src/NzbDrone.Core.Test/ParserTests/UrlFixture.cs +++ b/src/NzbDrone.Core.Test/ParserTests/UrlFixture.cs @@ -49,7 +49,7 @@ namespace NzbDrone.Core.Test.ParserTests 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/163_mediainfo_to_ffmpeg.cs b/src/NzbDrone.Core/Datastore/Migration/163_mediainfo_to_ffmpeg.cs index 0579eaf4b..bb8801a44 100644 --- a/src/NzbDrone.Core/Datastore/Migration/163_mediainfo_to_ffmpeg.cs +++ b/src/NzbDrone.Core/Datastore/Migration/163_mediainfo_to_ffmpeg.cs @@ -12,6 +12,7 @@ using NzbDrone.Common.EnvironmentInfo; 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 @@ namespace NzbDrone.Core.Datastore.Migration 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/EpisodeImport/Manual/ManualImportService.cs b/src/NzbDrone.Core/MediaFiles/EpisodeImport/Manual/ManualImportService.cs index 5e605e5bd..39ed106bb 100644 --- a/src/NzbDrone.Core/MediaFiles/EpisodeImport/Manual/ManualImportService.cs +++ b/src/NzbDrone.Core/MediaFiles/EpisodeImport/Manual/ManualImportService.cs @@ -156,7 +156,7 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport.Manual var downloadClientItem = GetTrackedDownload(downloadId)?.DownloadItem; var episodes = _episodeService.GetEpisodes(episodeIds); var finalReleaseGroup = releaseGroup.IsNullOrWhiteSpace() - ? Parser.Parser.ParseReleaseGroup(path) + ? Parser.ReleaseGroupParser.ParseReleaseGroup(path) : releaseGroup; var finalQuality = quality.Quality == Quality.Unknown ? QualityParser.ParseQuality(path) : quality; var finalLanguges = @@ -218,7 +218,7 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport.Manual SceneSource = SceneSource(series, rootFolder), ExistingFile = series.Path.IsParentPath(path), Size = _diskProvider.GetFileSize(path), - ReleaseGroup = releaseGroup.IsNullOrWhiteSpace() ? Parser.Parser.ParseReleaseGroup(path) : releaseGroup, + ReleaseGroup = releaseGroup.IsNullOrWhiteSpace() ? Parser.ReleaseGroupParser.ParseReleaseGroup(path) : releaseGroup, Languages = languages?.Count <= 1 && (languages?.SingleOrDefault() ?? Language.Unknown) == Language.Unknown ? LanguageParser.ParseLanguages(path) : languages, Quality = quality.Quality == Quality.Unknown ? QualityParser.ParseQuality(path) : quality, IndexerFlags = (IndexerFlags)indexerFlags, @@ -331,7 +331,7 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport.Manual { var localEpisode = new LocalEpisode(); localEpisode.Path = file; - localEpisode.ReleaseGroup = Parser.Parser.ParseReleaseGroup(file); + localEpisode.ReleaseGroup = Parser.ReleaseGroupParser.ParseReleaseGroup(file); localEpisode.Quality = QualityParser.ParseQuality(file); localEpisode.Languages = LanguageParser.ParseLanguages(file); localEpisode.Size = _diskProvider.GetFileSize(file); diff --git a/src/NzbDrone.Core/MediaFiles/EpisodeImport/SceneNameCalculator.cs b/src/NzbDrone.Core/MediaFiles/EpisodeImport/SceneNameCalculator.cs index 89de23fc3..85338a1b0 100644 --- a/src/NzbDrone.Core/MediaFiles/EpisodeImport/SceneNameCalculator.cs +++ b/src/NzbDrone.Core/MediaFiles/EpisodeImport/SceneNameCalculator.cs @@ -14,7 +14,7 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport if (!otherVideoFiles && downloadClientInfo != null && !downloadClientInfo.FullSeason) { - return Parser.Parser.RemoveFileExtension(downloadClientInfo.ReleaseTitle); + return FileExtensions.RemoveFileExtension(downloadClientInfo.ReleaseTitle); } var fileName = Path.GetFileNameWithoutExtension(localEpisode.Path.CleanFilePath()); diff --git a/src/NzbDrone.Core/MediaFiles/FileExtensions.cs b/src/NzbDrone.Core/MediaFiles/FileExtensions.cs index 36a97dd42..a0e007945 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 { 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 @@ namespace NzbDrone.Core.MediaFiles ".tgz", ".zip" }; - - private static List _dangerousExtensions = new List + public static HashSet DangerousExtensions => new(StringComparer.OrdinalIgnoreCase) { ".arj", ".lnk", @@ -31,8 +40,7 @@ namespace NzbDrone.Core.MediaFiles ".vbs", ".zipx" }; - - private static List _executableExtensions = new List + public static HashSet ExecutableExtensions => new(StringComparer.OrdinalIgnoreCase) { ".bat", ".cmd", @@ -40,8 +48,20 @@ namespace NzbDrone.Core.MediaFiles ".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 aa8f58570..203bd6714 100644 --- a/src/NzbDrone.Core/MediaFiles/MediaInfo/MediaInfoFormatter.cs +++ b/src/NzbDrone.Core/MediaFiles/MediaInfo/MediaInfoFormatter.cs @@ -293,7 +293,7 @@ namespace NzbDrone.Core.MediaFiles.MediaInfo 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/Parser/Parser.cs b/src/NzbDrone.Core/Parser/Parser.cs index 03824c57c..fb6ea7210 100644 --- a/src/NzbDrone.Core/Parser/Parser.cs +++ b/src/NzbDrone.Core/Parser/Parser.cs @@ -8,6 +8,7 @@ using System.Text.RegularExpressions; using NLog; using NzbDrone.Common.Extensions; using NzbDrone.Common.Instrumentation; +using NzbDrone.Core.MediaFiles; using NzbDrone.Core.Parser.Model; using NzbDrone.Core.Tv; @@ -17,45 +18,6 @@ namespace NzbDrone.Core.Parser { private static readonly Logger Logger = NzbDroneLogger.GetLogger(typeof(Parser)); - private static readonly RegexReplace[] PreSubstitutionRegex = new[] - { - // Korean series without season number, replace with S01Exxx and remove airdate - new RegexReplace(@"\.E(\d{2,4})\.\d{6}\.(.*-NEXT)$", ".S01E$1.$2", RegexOptions.Compiled), - - // Some Chinese anime releases contain both English and Chinese titles, remove the Chinese title and replace with normal anime pattern - new RegexReplace(@"^\[(?:(?[^\]]+?)(?:[\u4E00-\u9FCC]+)?)\]\[(?[^\]]+?)(?:\s(?<chinesetitle>[\u4E00-\u9FCC][^\]]*?))\]\[(?:(?:[\u4E00-\u9FCC]+?)?(?<episode>\d{1,4})(?:[\u4E00-\u9FCC]+?)?)\]", "[${subgroup}] ${title} - ${episode} - ", RegexOptions.Compiled), - - // Chinese LoliHouse/ZERO/Lilith-Raws/Skymoon-Raws/orion origin releases don't use the expected brackets, normalize using brackets - new RegexReplace(@"^\[(?<subgroup>[^\]]*?(?:LoliHouse|ZERO|Lilith-Raws|Skymoon-Raws|orion origin)[^\]]*?)\](?<title>[^\[\]]+?)(?: - (?<episode>[0-9-]+)\s*|\[第?(?<episode>[0-9]+(?:-[0-9]+)?)话?(?:END|完)?\])\[", "[${subgroup}][${title}][${episode}][", RegexOptions.Compiled), - - // Most Chinese anime releases contain additional brackets/separators for chinese and non-chinese titles, remove junk first and if it has S0x as season number, convert it to Sx - new RegexReplace(@"^\[(?<subgroup>[^\]]+)\](?:\s?★[^\[ -]+\s?)?\[?(?:(?<chinesetitle>(?=[^\]]*?[\u4E00-\u9FCC])[^\]]*?)(?:\]\[|\s*[_/·]\s*)){0,2}(?<title>[^\[\]]+?)(?:\s(?:S?(?<!\d+)((0)(?<season>\d)|(?<season>[1-9]\d))(?!\d+)))\]?(?:\[\d{4}\])?\[第?(?<episode>[0-9]+(?:-[0-9]+)?)(?:话|集)?(?: ?END|完| ?Fin)?\]", "[${subgroup}] ${title} S${season} - ${episode} ", RegexOptions.Compiled), - - // Some Chinese releases don't include a separation between Chinese and English titles within the same bracketed group - new RegexReplace(@"^\[(?<subgroup>[^\]]+)\]\[(?<chinesetitle>(?<![^a-zA-Z0-9])[^a-zA-Z0-9]+)(?<title>[^\]]+?)\](?:\[\d{4}\])?\[第?(?<episode>[0-9]+(?:-[0-9]+)?)(?:话|集)?(?: ?END|完| ?Fin)?\]", "[${subgroup}] ${title} - ${episode} ", RegexOptions.Compiled), - - // Most Chinese anime releases contain additional brackets/separators for chinese and non-chinese titles, remove junk and replace with normal anime pattern - new RegexReplace(@"^\[(?<subgroup>[^\]]+)\](?:\s?★[^\[ -]+\s?)?\[?(?:(?<chinesetitle>(?=[^\]]*?[\u4E00-\u9FCC])[^\]]*?)(?:\]\[|\s*[_/·]\s*)){0,2}(?<title>[^\]]+?)\]?(?:\[\d{4}\])?\[第?(?<episode>[0-9]{1,4}(?:-[0-9]{1,4})?)(?:话|集)?(?: ?END|完| ?Fin)?\]", "[${subgroup}] ${title} - ${episode} ", RegexOptions.Compiled), - - // Some Chinese anime releases contain both Chinese and English titles, remove the Chinese title first and if it has S0x as season number, convert it to Sx - new RegexReplace(@"^\[(?<subgroup>[^\]]+)\](?:\s)(?:(?<chinesetitle>(?=[^\]]*?[\u4E00-\u9FCC])[^\]]*?)(?:\s/\s))(?<title>[^\[\]]+?)(?:\s(?:S?(?<!\d+)((0)(?<season>\d)|(?<season>[1-9]\d))(?!\d+)))(?:[- ]+)(?<episode>[0-9]+(?:-[0-9]+)?)话?(?:END|完)?", "[${subgroup}] ${title} S${season} - ${episode} ", RegexOptions.Compiled), - - // Some Chinese anime releases contain both English and Chinese titles, remove the Chinese title and replace with normal anime pattern - new RegexReplace(@"^\[(?<subgroup>[^\]]+)\](?:\s)(?:(?<title>[^\]]+?)(?:\s/\s))(?<chinesetitle>(?=[^\]]*?[\u4E00-\u9FCC])[^\]]*?)(?:[- ]+)(?<episode>[0-9]+(?:-[0-9]+)?(?![a-z]))话?(?:END|完)?", "[${subgroup}] ${title} - ${episode} ", RegexOptions.Compiled), - - // Some Chinese anime releases contain both Chinese and English titles, remove the Chinese title and replace with normal anime pattern - new RegexReplace(@"^\[(?<subgroup>[^\]]+)\](?:\s)(?:(?<chinesetitle>(?=[^\]]*?[\u4E00-\u9FCC])[^\]]*?)(?:\s/\s))(?<title>[^\]]+?)(?:[- ]+)(?<episode>[0-9]+(?:-[0-9]+)?(?![a-z]))话?(?:END|完)?", "[${subgroup}] ${title} - ${episode} ", RegexOptions.Compiled), - - // GM-Team releases with lots of square brackets - new RegexReplace(@"^\[(?<subgroup>[^\]]+)\](?:(?<chinesubgroup>\[(?=[^\]]*?[\u4E00-\u9FCC])[^\]]*\])+)\[(?<title>[^\]]+?)\](?<junk>\[[^\]]+\])*\[(?<episode>[0-9]+(?:-[0-9]+)?)( END| Fin)?\]", "[${subgroup}] ${title} - ${episode} ", RegexOptions.Compiled), - - // Some Chinese anime releases contain both Chinese and English titles separated by | instead of /, remove the Chinese title and replace with normal anime pattern - new RegexReplace(@"^\[(?<subgroup>[^\]]+)\](?:\s)(?:(?<chinesetitle>(?=[^\]]*?[\u4E00-\u9FCC])[^\]]*?)(?:\s\|\s))(?<title>[^\]]+?)(?:[- ]+)(?<episode>[0-9]+(?:-[0-9]+)?(?![a-z]))话?(?:END|完)?", "[${subgroup}] ${title} - ${episode} ", RegexOptions.Compiled), - - // Spanish releases with information in brackets - new RegexReplace(@"^(?<title>.+?(?=[ ._-]\()).+?\((?<year>\d{4})\/(?<info>S[^\/]+)", "${title} (${year}) - ${info} ", RegexOptions.Compiled), - }; - private static readonly Regex[] ReportTitleRegex = new[] { // Anime - Absolute Episode Number + Title + Season+Episode @@ -540,52 +502,18 @@ namespace NzbDrone.Core.Parser private static readonly Regex PercentRegex = new Regex(@"(?<=\b\d+)%", RegexOptions.Compiled); - private static readonly Regex FileExtensionRegex = new Regex(@"\.[a-z0-9]{2,4}$", - RegexOptions.IgnoreCase | RegexOptions.Compiled); - private static readonly RegexReplace SimpleTitleRegex = new RegexReplace(@"(?:(480|540|576|720|1080|2160)[ip]|[xh][\W_]?26[45]|DD\W?5\W1|[<>?*]|848x480|1280x720|1920x1080|3840x2160|4096x2160|(?<![a-f0-9])(8|10)[ -]?(b(?![a-z0-9])|bit))\s*?", string.Empty, RegexOptions.IgnoreCase | RegexOptions.Compiled); // 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}\.(?<!Naruto-Kun\.)(?:[a-z]{2,6}\.[a-z]{2,6}|xn--[a-z0-9-]{4,}|[a-z]{2,})\b(?:\s*(?:\]|\))|[ -]{2,})[ -]*", - string.Empty, - RegexOptions.IgnoreCase | RegexOptions.Compiled); - - private static readonly RegexReplace WebsitePostfixRegex = new RegexReplace(@"(?:\[\s*)?(?:www\.)?[-a-z0-9-]{1,256}\.(?:xn--[a-z0-9-]{4,}|[a-z]{2,6})\b(?:\s*\])$", - string.Empty, - RegexOptions.IgnoreCase | RegexOptions.Compiled); - private static readonly Regex SixDigitAirDateRegex = new Regex(@"(?<=[_.-])(?<airdate>(?<!\d)(?<airyear>[1-9]\d{1})(?<airmonth>[0-1][0-9])(?<airday>[0-3][0-9]))(?=[_.-])", RegexOptions.IgnoreCase | RegexOptions.Compiled); - private static readonly RegexReplace CleanReleaseGroupRegex = new RegexReplace(@"^(.*?[-._ ](S\d+E\d+)[-._ ])|(-(RP|1|NZBGeek|Obfuscated|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); - - private static readonly RegexReplace CleanTorrentSuffixRegex = new RegexReplace(@"\[(?:ettv|rartv|rarbg|cttv|publichd)\]$", - string.Empty, - RegexOptions.IgnoreCase | RegexOptions.Compiled); - private static readonly Regex CleanQualityBracketsRegex = new Regex(@"\[[a-z0-9 ._-]+\]$", RegexOptions.IgnoreCase | RegexOptions.Compiled); - private static readonly Regex ReleaseGroupRegex = new Regex(@"-(?<releasegroup>[a-z0-9]+(?<part2>-[a-z0-9]+)?(?!.+?(?:480p|576p|720p|1080p|2160p)))(?<!(?:WEB-DL|Blu-Ray|480p|576p|720p|1080p|2160p|DTS-HD|DTS-X|DTS-MA|DTS-ES|-ES|-EN|-CAT|-GER|-FRA|-FRE|-ITA|\d{1,2}-bit|[ ._]\d{4}-\d{2}|-\d{2})(?:\k<part2>)?)(?:\b|[-._ ]|$)|[-._ ]\[(?<releasegroup>[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(@"^(?:\[(?<subgroup>(?!\s).+?(?<!\s))\](?:_|-|\s|\.)?)", - RegexOptions.IgnoreCase | RegexOptions.Compiled); - - // Handle Exception Release Groups that don't follow -RlsGrp; Manual List - // name only...be very careful with this last; high chance of false positives - private static readonly Regex ExceptionReleaseGroupRegexExact = new Regex(@"(?<releasegroup>(?:D\-Z0N3|Fight-BB|VARYG|E\.N\.D|KRaLiMaRKo|BluDragon|DarQ|KCRT|BEN[_. ]THE[_. ]MEN)\b)", RegexOptions.IgnoreCase | RegexOptions.Compiled); - - // groups whose releases end with RlsGroup) or RlsGroup] - private static readonly Regex ExceptionReleaseGroupRegex = new Regex(@"(?<=[._ \[])(?<releasegroup>(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|TAoE)(?=\]|\)))", RegexOptions.IgnoreCase | RegexOptions.Compiled); - private static readonly Regex YearInTitleRegex = new Regex(@"^(?<title>.+?)[-_. ]+?[\(\[]?(?<year>\d{4})[\]\)]?", RegexOptions.IgnoreCase | RegexOptions.Compiled); @@ -704,7 +632,7 @@ namespace NzbDrone.Core.Parser if (ReversedTitleRegex.IsMatch(title)) { - var titleWithoutExtension = RemoveFileExtension(title).ToCharArray(); + var titleWithoutExtension = FileExtensions.RemoveFileExtension(title).ToCharArray(); Array.Reverse(titleWithoutExtension); title = string.Concat(new string(titleWithoutExtension), title.AsSpan(titleWithoutExtension.Length)); @@ -714,10 +642,9 @@ namespace NzbDrone.Core.Parser var simpleTitle = title; - simpleTitle = WebsitePrefixRegex.Replace(simpleTitle); - simpleTitle = WebsitePostfixRegex.Replace(simpleTitle); - - simpleTitle = CleanTorrentSuffixRegex.Replace(simpleTitle); + simpleTitle = ParserCommon.WebsitePrefixRegex.Replace(simpleTitle); + simpleTitle = ParserCommon.WebsitePostfixRegex.Replace(simpleTitle); + simpleTitle = ParserCommon.CleanTorrentSuffixRegex.Replace(simpleTitle); return simpleTitle; } @@ -735,7 +662,7 @@ namespace NzbDrone.Core.Parser if (ReversedTitleRegex.IsMatch(title)) { - var titleWithoutExtension = RemoveFileExtension(title).ToCharArray(); + var titleWithoutExtension = FileExtensions.RemoveFileExtension(title).ToCharArray(); Array.Reverse(titleWithoutExtension); title = string.Concat(new string(titleWithoutExtension), title.AsSpan(titleWithoutExtension.Length)); @@ -743,11 +670,11 @@ namespace NzbDrone.Core.Parser Logger.Debug("Reversed name detected. Converted to '{0}'", title); } - var releaseTitle = RemoveFileExtension(title); + var releaseTitle = FileExtensions.RemoveFileExtension(title); releaseTitle = releaseTitle.Replace("【", "[").Replace("】", "]"); - foreach (var replace in PreSubstitutionRegex) + foreach (var replace in ParserCommon.PreSubstitutionRegex) { if (replace.TryReplace(ref releaseTitle)) { @@ -759,10 +686,9 @@ namespace NzbDrone.Core.Parser var simpleTitle = SimpleTitleRegex.Replace(releaseTitle); // TODO: Quick fix stripping [url] - prefixes and postfixes. - simpleTitle = WebsitePrefixRegex.Replace(simpleTitle); - simpleTitle = WebsitePostfixRegex.Replace(simpleTitle); - - simpleTitle = CleanTorrentSuffixRegex.Replace(simpleTitle); + simpleTitle = ParserCommon.WebsitePrefixRegex.Replace(simpleTitle); + simpleTitle = ParserCommon.WebsitePostfixRegex.Replace(simpleTitle); + simpleTitle = ParserCommon.CleanTorrentSuffixRegex.Replace(simpleTitle); simpleTitle = CleanQualityBracketsRegex.Replace(simpleTitle, m => { @@ -814,7 +740,7 @@ namespace NzbDrone.Core.Parser result.Quality = QualityParser.ParseQuality(title); Logger.Debug("Quality parsed: {0}", result.Quality); - result.ReleaseGroup = ParseReleaseGroup(releaseTitle); + result.ReleaseGroup = ReleaseGroupParser.ParseReleaseGroup(releaseTitle); var subGroup = GetSubGroup(match); if (!subGroup.IsNullOrWhiteSpace()) @@ -934,80 +860,9 @@ namespace NzbDrone.Core.Parser return null; } - public static string ParseReleaseGroup(string title) - { - title = title.Trim(); - title = RemoveFileExtension(title); - foreach (var replace in PreSubstitutionRegex) - { - if (replace.TryReplace(ref title)) - { - break; - } - } - - 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<Match>().Last().Groups["releasegroup"].Value; - } - - var exceptionExactMatch = ExceptionReleaseGroupRegexExact.Matches(title); - - if (exceptionExactMatch.Count != 0) - { - return exceptionExactMatch.OfType<Match>().Last().Groups["releasegroup"].Value; - } - - var matches = ReleaseGroupRegex.Matches(title); - - if (matches.Count != 0) - { - var group = matches.OfType<Match>().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; + return FileExtensions.RemoveFileExtension(title); } public static bool HasMultipleLanguages(string title) @@ -1313,7 +1168,7 @@ namespace NzbDrone.Core.Parser return false; } - var titleWithoutExtension = RemoveFileExtension(title); + var titleWithoutExtension = FileExtensions.RemoveFileExtension(title); if (RejectHashedReleasesRegexes.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 000000000..71ae8ad49 --- /dev/null +++ b/src/NzbDrone.Core/Parser/ParserCommon.cs @@ -0,0 +1,59 @@ +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 = new[] + { + // Korean series without season number, replace with S01Exxx and remove airdate + new RegexReplace(@"\.E(\d{2,4})\.\d{6}\.(.*-NEXT)$", ".S01E$1.$2", RegexOptions.Compiled), + + // Some Chinese anime releases contain both English and Chinese titles, remove the Chinese title and replace with normal anime pattern + new RegexReplace(@"^\[(?:(?<subgroup>[^\]]+?)(?:[\u4E00-\u9FCC]+)?)\]\[(?<title>[^\]]+?)(?:\s(?<chinesetitle>[\u4E00-\u9FCC][^\]]*?))\]\[(?:(?:[\u4E00-\u9FCC]+?)?(?<episode>\d{1,4})(?:[\u4E00-\u9FCC]+?)?)\]", "[${subgroup}] ${title} - ${episode} - ", RegexOptions.Compiled), + + // Chinese LoliHouse/ZERO/Lilith-Raws/Skymoon-Raws/orion origin releases don't use the expected brackets, normalize using brackets + new RegexReplace(@"^\[(?<subgroup>[^\]]*?(?:LoliHouse|ZERO|Lilith-Raws|Skymoon-Raws|orion origin)[^\]]*?)\](?<title>[^\[\]]+?)(?: - (?<episode>[0-9-]+)\s*|\[第?(?<episode>[0-9]+(?:-[0-9]+)?)话?(?:END|完)?\])\[", "[${subgroup}][${title}][${episode}][", RegexOptions.Compiled), + + // Most Chinese anime releases contain additional brackets/separators for chinese and non-chinese titles, remove junk first and if it has S0x as season number, convert it to Sx + new RegexReplace(@"^\[(?<subgroup>[^\]]+)\](?:\s?★[^\[ -]+\s?)?\[?(?:(?<chinesetitle>(?=[^\]]*?[\u4E00-\u9FCC])[^\]]*?)(?:\]\[|\s*[_/·]\s*)){0,2}(?<title>[^\[\]]+?)(?:\s(?:S?(?<!\d+)((0)(?<season>\d)|(?<season>[1-9]\d))(?!\d+)))\]?(?:\[\d{4}\])?\[第?(?<episode>[0-9]+(?:-[0-9]+)?)(?:话|集)?(?: ?END|完| ?Fin)?\]", "[${subgroup}] ${title} S${season} - ${episode} ", RegexOptions.Compiled), + + // Some Chinese releases don't include a separation between Chinese and English titles within the same bracketed group + new RegexReplace(@"^\[(?<subgroup>[^\]]+)\]\[(?<chinesetitle>(?<![^a-zA-Z0-9])[^a-zA-Z0-9]+)(?<title>[^\]]+?)\](?:\[\d{4}\])?\[第?(?<episode>[0-9]+(?:-[0-9]+)?)(?:话|集)?(?: ?END|完| ?Fin)?\]", "[${subgroup}] ${title} - ${episode} ", RegexOptions.Compiled), + + // Most Chinese anime releases contain additional brackets/separators for chinese and non-chinese titles, remove junk and replace with normal anime pattern + new RegexReplace(@"^\[(?<subgroup>[^\]]+)\](?:\s?★[^\[ -]+\s?)?\[?(?:(?<chinesetitle>(?=[^\]]*?[\u4E00-\u9FCC])[^\]]*?)(?:\]\[|\s*[_/·]\s*)){0,2}(?<title>[^\]]+?)\]?(?:\[\d{4}\])?\[第?(?<episode>[0-9]{1,4}(?:-[0-9]{1,4})?)(?:话|集)?(?: ?END|完| ?Fin)?\]", "[${subgroup}] ${title} - ${episode} ", RegexOptions.Compiled), + + // Some Chinese anime releases contain both Chinese and English titles, remove the Chinese title first and if it has S0x as season number, convert it to Sx + new RegexReplace(@"^\[(?<subgroup>[^\]]+)\](?:\s)(?:(?<chinesetitle>(?=[^\]]*?[\u4E00-\u9FCC])[^\]]*?)(?:\s/\s))(?<title>[^\[\]]+?)(?:\s(?:S?(?<!\d+)((0)(?<season>\d)|(?<season>[1-9]\d))(?!\d+)))(?:[- ]+)(?<episode>[0-9]+(?:-[0-9]+)?)话?(?:END|完)?", "[${subgroup}] ${title} S${season} - ${episode} ", RegexOptions.Compiled), + + // Some Chinese anime releases contain both English and Chinese titles, remove the Chinese title and replace with normal anime pattern + new RegexReplace(@"^\[(?<subgroup>[^\]]+)\](?:\s)(?:(?<title>[^\]]+?)(?:\s/\s))(?<chinesetitle>(?=[^\]]*?[\u4E00-\u9FCC])[^\]]*?)(?:[- ]+)(?<episode>[0-9]+(?:-[0-9]+)?(?![a-z]))话?(?:END|完)?", "[${subgroup}] ${title} - ${episode} ", RegexOptions.Compiled), + + // Some Chinese anime releases contain both Chinese and English titles, remove the Chinese title and replace with normal anime pattern + new RegexReplace(@"^\[(?<subgroup>[^\]]+)\](?:\s)(?:(?<chinesetitle>(?=[^\]]*?[\u4E00-\u9FCC])[^\]]*?)(?:\s/\s))(?<title>[^\]]+?)(?:[- ]+)(?<episode>[0-9]+(?:-[0-9]+)?(?![a-z]))话?(?:END|完)?", "[${subgroup}] ${title} - ${episode} ", RegexOptions.Compiled), + + // GM-Team releases with lots of square brackets + new RegexReplace(@"^\[(?<subgroup>[^\]]+)\](?:(?<chinesubgroup>\[(?=[^\]]*?[\u4E00-\u9FCC])[^\]]*\])+)\[(?<title>[^\]]+?)\](?<junk>\[[^\]]+\])*\[(?<episode>[0-9]+(?:-[0-9]+)?)( END| Fin)?\]", "[${subgroup}] ${title} - ${episode} ", RegexOptions.Compiled), + + // Some Chinese anime releases contain both Chinese and English titles separated by | instead of /, remove the Chinese title and replace with normal anime pattern + new RegexReplace(@"^\[(?<subgroup>[^\]]+)\](?:\s)(?:(?<chinesetitle>(?=[^\]]*?[\u4E00-\u9FCC])[^\]]*?)(?:\s\|\s))(?<title>[^\]]+?)(?:[- ]+)(?<episode>[0-9]+(?:-[0-9]+)?(?![a-z]))话?(?:END|完)?", "[${subgroup}] ${title} - ${episode} ", RegexOptions.Compiled), + + // Spanish releases with information in brackets + new RegexReplace(@"^(?<title>.+?(?=[ ._-]\()).+?\((?<year>\d{4})\/(?<info>S[^\/]+)", "${title} (${year}) - ${info} ", RegexOptions.Compiled), + }; + + internal static readonly RegexReplace WebsitePrefixRegex = new(@"^(?:(?:\[|\()\s*)?(?:www\.)?[-a-z0-9-]{1,256}\.(?<!Naruto-Kun\.)(?:[a-z]{2,6}\.[a-z]{2,6}|xn--[a-z0-9-]{4,}|[a-z]{2,})\b(?:\s*(?:\]|\))|[ -]{2,})[ -]*", + string.Empty, + RegexOptions.IgnoreCase | RegexOptions.Compiled); + + internal static readonly RegexReplace WebsitePostfixRegex = new(@"(?:\[\s*)?(?:www\.)?[-a-z0-9-]{1,256}\.(?:xn--[a-z0-9-]{4,}|[a-z]{2,6})\b(?:\s*\])$", + string.Empty, + RegexOptions.IgnoreCase | RegexOptions.Compiled); + + internal static readonly RegexReplace CleanTorrentSuffixRegex = new(@"\[(?:ettv|rartv|rarbg|cttv|publichd)\]$", + string.Empty, + RegexOptions.IgnoreCase | RegexOptions.Compiled); +} diff --git a/src/NzbDrone.Core/Parser/ParsingService.cs b/src/NzbDrone.Core/Parser/ParsingService.cs index 7cbff94f1..30f5943e2 100644 --- a/src/NzbDrone.Core/Parser/ParsingService.cs +++ b/src/NzbDrone.Core/Parser/ParsingService.cs @@ -353,7 +353,7 @@ namespace NzbDrone.Core.Parser EpisodeNumbers = new int[1] { episode.EpisodeNumber }, FullSeason = false, Quality = QualityParser.ParseQuality(releaseTitle), - ReleaseGroup = Parser.ParseReleaseGroup(releaseTitle), + ReleaseGroup = ReleaseGroupParser.ParseReleaseGroup(releaseTitle), Languages = LanguageParser.ParseLanguages(releaseTitle), Special = true }; diff --git a/src/NzbDrone.Core/Parser/ReleaseGroupParser.cs b/src/NzbDrone.Core/Parser/ReleaseGroupParser.cs new file mode 100644 index 000000000..61f5261cf --- /dev/null +++ b/src/NzbDrone.Core/Parser/ReleaseGroupParser.cs @@ -0,0 +1,87 @@ +using System.Linq; +using System.Text.RegularExpressions; +using NzbDrone.Core.MediaFiles; + +namespace NzbDrone.Core.Parser; + +public static class ReleaseGroupParser +{ + private static readonly Regex ReleaseGroupRegex = new(@"-(?<releasegroup>[a-z0-9]+(?<part2>-[a-z0-9]+)?(?!.+?(?:480p|576p|720p|1080p|2160p)))(?<!(?:WEB-DL|Blu-Ray|480p|576p|720p|1080p|2160p|DTS-HD|DTS-X|DTS-MA|DTS-ES|-ES|-EN|-CAT|-GER|-FRA|-FRE|-ITA|\d{1,2}-bit|[ ._]\d{4}-\d{2}|-\d{2})(?:\k<part2>)?)(?:\b|[-._ ]|$)|[-._ ]\[(?<releasegroup>[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(@"^(?:\[(?<subgroup>(?!\s).+?(?<!\s))\](?:_|-|\s|\.)?)", + RegexOptions.IgnoreCase | RegexOptions.Compiled); + + // Handle Exception Release Groups that don't follow -RlsGrp; Manual List + // name only...be very careful with this last; high chance of false positives + private static readonly Regex ExceptionReleaseGroupRegexExact = new(@"(?<releasegroup>(?:D\-Z0N3|Fight-BB|VARYG|E\.N\.D|KRaLiMaRKo|BluDragon|DarQ|KCRT|BEN[_. ]THE[_. ]MEN|TAoE|QxR)\b)", RegexOptions.IgnoreCase | RegexOptions.Compiled); + + // groups whose releases end with RlsGroup) or RlsGroup] + private static readonly Regex ExceptionReleaseGroupRegex = new(@"(?<=[._ \[])(?<releasegroup>(Joy|ImE|UTR|t3nzin|Anime Time|Project Angel|Hakata Ramen|HONE|Vyndros|SEV|Garshasp|Kappa|Natty|RCVR|SAMPA|YOGI|r00t|EDGE2020)(?=\]|\)))", RegexOptions.IgnoreCase | RegexOptions.Compiled); + + private static readonly RegexReplace CleanReleaseGroupRegex = new(@"^(.*?[-._ ](S\d+E\d+)[-._ ])|(-(RP|1|NZBGeek|Obfuscated|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<Match>().Last().Groups["releasegroup"].Value; + } + + var exceptionExactMatch = ExceptionReleaseGroupRegexExact.Matches(title); + + if (exceptionExactMatch.Count != 0) + { + return exceptionExactMatch.OfType<Match>().Last().Groups["releasegroup"].Value; + } + + var matches = ReleaseGroupRegex.Matches(title); + + if (matches.Count != 0) + { + var group = matches.OfType<Match>().Last().Groups["releasegroup"].Value; + + if (int.TryParse(group, out _)) + { + return null; + } + + if (InvalidReleaseGroupRegex.IsMatch(group)) + { + return null; + } + + return group; + } + + return null; + } +} diff --git a/src/NzbDrone.Core/Queue/QueueService.cs b/src/NzbDrone.Core/Queue/QueueService.cs index 853993b50..4e7d39f7a 100644 --- a/src/NzbDrone.Core/Queue/QueueService.cs +++ b/src/NzbDrone.Core/Queue/QueueService.cs @@ -4,6 +4,7 @@ using System.Linq; using NzbDrone.Common.Crypto; using NzbDrone.Core.Download.TrackedDownloads; using NzbDrone.Core.Languages; +using NzbDrone.Core.MediaFiles; using NzbDrone.Core.Messaging.Events; using NzbDrone.Core.Qualities; using NzbDrone.Core.Tv; @@ -65,7 +66,7 @@ namespace NzbDrone.Core.Queue Episode = episode, Languages = trackedDownload.RemoteEpisode?.Languages ?? new List<Language> { Language.Unknown }, Quality = trackedDownload.RemoteEpisode?.ParsedEpisodeInfo.Quality ?? new QualityModel(Quality.Unknown), - Title = Parser.Parser.RemoveFileExtension(trackedDownload.DownloadItem.Title), + Title = FileExtensions.RemoveFileExtension(trackedDownload.DownloadItem.Title), Size = trackedDownload.DownloadItem.TotalSize, SizeLeft = trackedDownload.DownloadItem.RemainingSize, TimeLeft = trackedDownload.DownloadItem.RemainingTime,