From 9e56f6552f5b1461193e0aa3bdf612e3accbd1c4 Mon Sep 17 00:00:00 2001 From: Jason Dove <1695733+jasongdove@users.noreply.github.com> Date: Fri, 18 Jul 2025 16:48:08 +0000 Subject: [PATCH] support more multi-part grouping names (#2165) --- CHANGELOG.md | 4 +++ .../MultiPartEpisodeGrouperTests.cs | 21 +++++++++++++++ .../Scheduling/MultiPartEpisodeGrouper.cs | 26 ++++++++++++------- 3 files changed, 42 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 497512fa..c17e0d0e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -95,6 +95,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - The FFmpeg report of the playback attempt - The media info for the content - The `Troubleshooting` > `General` output +- Support `(Part [english number])` name suffixes for multi-part episode grouping, for example: + - `Awesome Episode (Part One)` + - `Better Episode (Part Two)` + - `Not So Great (Part Three)` ### Changed - Allow `Other Video` libraries and `Image` libraries to use the same folders diff --git a/ErsatzTV.Core.Tests/Scheduling/MultiPartEpisodeGrouperTests.cs b/ErsatzTV.Core.Tests/Scheduling/MultiPartEpisodeGrouperTests.cs index e614af0f..da6809d8 100644 --- a/ErsatzTV.Core.Tests/Scheduling/MultiPartEpisodeGrouperTests.cs +++ b/ErsatzTV.Core.Tests/Scheduling/MultiPartEpisodeGrouperTests.cs @@ -12,6 +12,7 @@ public class MultiPartEpisodeGrouperTests [TestCase("Episode 1 - More", "Episode 2 (1) - Title", "Episode 3 (2) - After", "Episode 4 - Dash")] [TestCase("Episode 1", "Episode 2 Part 1", "Episode 3 Part 2", "Episode 4")] [TestCase("Episode 1", "Episode 2 (Part 1)", "Episode 3 (Part 2)", "Episode 4")] + [TestCase("Episode 1", "Episode 2 (Part One)", "Episode 3 (Part Two)", "Episode 4")] public void NotGrouped_Grouped_NotGrouped(string one, string two, string three, string four) { var mediaItems = new List @@ -35,7 +36,9 @@ public class MultiPartEpisodeGrouperTests [TestCase("Episode 1 Part 1", "Episode 2 (2) - More", "Episode 3 - After")] [TestCase("Episode 1 Part 1", "Episode 2 (II)", "Episode 3")] [TestCase("Episode 1 Part One", "Episode 2 (II)", "Episode 3")] + [TestCase("Episode 1 (Part One)", "Episode 2 (II)", "Episode 3")] [TestCase("Episode 1 (1)", "Episode 2 (Part 2)", "Episode 3")] + public void MixedNaming_Group(string one, string two, string three) { var mediaItems = new List @@ -52,6 +55,24 @@ public class MultiPartEpisodeGrouperTests ShouldHaveOneItem(result, mediaItems[2]); } + [Test] + [TestCase("The Meddlers (Part One)", "The Meddlers (Part Two)", "The Meddlers (Part Three)")] + [TestCase("S01E01 The Slaves of Jedikiah, Part 1", "S01E02 The Slaves of Jedikiah, Part 2", "S01E03 The Slaves of Jedikiah, Part 3")] + public void All_Grouped(string one, string two, string three) + { + var mediaItems = new List + { + NamedEpisode(one, 1, 1, 1), + NamedEpisode(two, 1, 1, 2), + NamedEpisode(three, 1, 1, 3) + }; + + List result = MultiPartEpisodeGrouper.GroupMediaItems(mediaItems, false); + + result.Count.ShouldBe(1); + ShouldHaveMultipleItems(result, mediaItems[0], [mediaItems[1], mediaItems[2]]); + } + [Test] [TestCase("Episode 1 (5)", "Episode 2 - (6)", "Episode 3")] [TestCase("Episode 1 Part 5", "Episode 2 Part 6", "Episode 3 - After")] diff --git a/ErsatzTV.Core/Scheduling/MultiPartEpisodeGrouper.cs b/ErsatzTV.Core/Scheduling/MultiPartEpisodeGrouper.cs index 722b0c0e..5d0e66cc 100644 --- a/ErsatzTV.Core/Scheduling/MultiPartEpisodeGrouper.cs +++ b/ErsatzTV.Core/Scheduling/MultiPartEpisodeGrouper.cs @@ -4,7 +4,7 @@ using LanguageExt.UnsafeValueAccess; namespace ErsatzTV.Core.Scheduling; -public static class MultiPartEpisodeGrouper +public static partial class MultiPartEpisodeGrouper { public static List GroupMediaItems(IList mediaItems, bool treatCollectionsAsShows) { @@ -118,29 +118,25 @@ public static class MultiPartEpisodeGrouper { foreach (EpisodeMetadata metadata in e.EpisodeMetadata.HeadOrNone()) { - const string PATTERN = @"^.*\((\d+)\)( - .*)?$"; - Match match = Regex.Match(metadata.Title ?? string.Empty, PATTERN); + Match match = Pattern1Regex().Match(metadata.Title ?? string.Empty); if (match.Success && int.TryParse(match.Groups[1].Value, out int value1)) { return value1; } - const string PATTERN_2 = @"^.*\(?Part (\d+)\)?$"; - Match match2 = Regex.Match(metadata.Title ?? string.Empty, PATTERN_2); + Match match2 = Pattern2Regex().Match(metadata.Title ?? string.Empty); if (match2.Success && int.TryParse(match2.Groups[1].Value, out int value2)) { return value2; } - const string PATTERN_3 = @"^.*\(([MDCLXVI]+)\)( - .*)?$"; - Match match3 = Regex.Match(metadata.Title ?? string.Empty, PATTERN_3); + Match match3 = Pattern3Regex().Match(metadata.Title ?? string.Empty); if (match3.Success && TryParseRoman(match3.Groups[1].Value, out int value3)) { return value3; } - const string PATTERN_4 = @"^.*Part (\w+)$"; - Match match4 = Regex.Match(metadata.Title ?? string.Empty, PATTERN_4); + Match match4 = Pattern4Regex().Match(metadata.Title ?? string.Empty); if (match4.Success && TryParseEnglish(match4.Groups[1].Value, out int value4)) { return value4; @@ -229,4 +225,16 @@ public static class MultiPartEpisodeGrouper return false; } } + + [GeneratedRegex(@"^.*\((\d+)\)( - .*)?$")] + private static partial Regex Pattern1Regex(); + + [GeneratedRegex(@"^.*\(?Part (\d+)\)?$")] + private static partial Regex Pattern2Regex(); + + [GeneratedRegex(@"^.*\(([MDCLXVI]+)\)( - .*)?$")] + private static partial Regex Pattern3Regex(); + + [GeneratedRegex(@"^.*\(?Part (\w+)\)?$")] + private static partial Regex Pattern4Regex(); }