aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorShadowghost <Shadowghost@users.noreply.github.com>2026-09-15 11:15:50 -0400
committerCody Robibero <cody@robibe.ro>2026-09-15 11:15:50 -0400
commit52530f98a251b02fc96e4288a4190b5454c19131 (patch)
tree05d6976a1ba00aaa5b933630f135aabdf32f9380
parenta2aa3d96d2a75b01f94354b541fe890b78e7c8bd (diff)
Backport pull request #17890 from jellyfin/release-12.z
Only group episodes as versions on a confident path parse Original-merge: d749978099e82001e6c826fbd604c873adc362c8 Merged-by: crobibero <cody@robibe.ro> Backported-by: Cody Robibero <cody@robibe.ro>
-rw-r--r--Emby.Naming/Video/VideoListResolver.cs42
-rw-r--r--tests/Jellyfin.Naming.Tests/Video/MultiVersionTests.cs40
2 files changed, 64 insertions, 18 deletions
diff --git a/Emby.Naming/Video/VideoListResolver.cs b/Emby.Naming/Video/VideoListResolver.cs
index 29330b132d..e16562774d 100644
--- a/Emby.Naming/Video/VideoListResolver.cs
+++ b/Emby.Naming/Video/VideoListResolver.cs
@@ -217,24 +217,7 @@ namespace Emby.Naming.Video
for (var i = 0; i < videos.Count; i++)
{
var video = videos[i];
- var episodeResult = _episodePathParser.Parse(video.Files[0].Path, false);
- string? key = null;
- if (episodeResult.Success)
- {
- if (episodeResult.IsByDate
- && episodeResult.Year.HasValue
- && episodeResult.Month.HasValue
- && episodeResult.Day.HasValue)
- {
- key = FormattableString.Invariant(
- $"D{episodeResult.Year.Value}{episodeResult.Month.Value:D2}{episodeResult.Day.Value:D2}");
- }
- else if (episodeResult.EpisodeNumber.HasValue)
- {
- key = FormattableString.Invariant(
- $"S{episodeResult.SeasonNumber ?? 0}E{episodeResult.EpisodeNumber.Value}");
- }
- }
+ var key = GetEpisodeVersionKey(video.Files[0].Path);
if (key is null)
{
@@ -265,6 +248,29 @@ namespace Emby.Naming.Video
return result;
}
+ private string? GetEpisodeVersionKey(string path)
+ {
+ // Optimistic expressions are guesses, so they are not consulted here: merging is destructive,
+ // a file collapsed into the alternate versions of another one is no longer an episode of its own.
+ var episodeResult = _episodePathParser.Parse(path, false, isOptimistic: false, fillExtendedInfo: false);
+ if (!episodeResult.Success)
+ {
+ return null;
+ }
+
+ if (episodeResult.IsByDate)
+ {
+ return episodeResult.Year.HasValue && episodeResult.Month.HasValue && episodeResult.Day.HasValue
+ ? FormattableString.Invariant(
+ $"D{episodeResult.Year.Value}{episodeResult.Month.Value:D2}{episodeResult.Day.Value:D2}")
+ : null;
+ }
+
+ return episodeResult.SeasonNumber.HasValue && episodeResult.EpisodeNumber.HasValue
+ ? FormattableString.Invariant($"S{episodeResult.SeasonNumber.Value}E{episodeResult.EpisodeNumber.Value}")
+ : null;
+ }
+
private static VideoInfo OrganizeAlternateVersions(
List<VideoInfo> videos,
VideoInfo? primaryOverride = null,
diff --git a/tests/Jellyfin.Naming.Tests/Video/MultiVersionTests.cs b/tests/Jellyfin.Naming.Tests/Video/MultiVersionTests.cs
index b29c64f50d..62c1cf25d7 100644
--- a/tests/Jellyfin.Naming.Tests/Video/MultiVersionTests.cs
+++ b/tests/Jellyfin.Naming.Tests/Video/MultiVersionTests.cs
@@ -1130,5 +1130,45 @@ namespace Jellyfin.Naming.Tests.Video
Assert.Equal(2, result[0].Files.Count);
Assert.Single(result[0].AlternateVersions);
}
+
+ [Fact]
+ public void TestMultiVersionEpisodeAbsoluteNumberingWithNumberInSeriesTitle()
+ {
+ // Every one of these parses as episode 2, because the expressions read the "2" of the
+ // series title as an absolute episode number. They are distinct episodes all the same.
+ var files = new[]
+ {
+ "/anime/IS Infinite Stratos 2/IS Infinite Stratos 2 - 01 - The Memory of a Summer (b6f40849).mkv",
+ "/anime/IS Infinite Stratos 2/IS Infinite Stratos 2 - 02 - Heart Pain Killer (d8c0896c).mkv",
+ "/anime/IS Infinite Stratos 2/IS Infinite Stratos 2 - 03 - Translucent Chord (4ecce3dd).mkv",
+ "/anime/IS Infinite Stratos 2/IS Infinite Stratos 2 - 04 - The Mysterious Lady (837a1909).mkv"
+ };
+
+ var result = _videoListResolver.Resolve(
+ files.Select(i => VideoResolver.Resolve(i, false, _namingOptions)).OfType<VideoFileInfo>().ToList(),
+ collectionType: CollectionType.tvshows).ToList();
+
+ Assert.Equal(4, result.Count);
+ Assert.All(result, r => Assert.Empty(r.AlternateVersions));
+ }
+
+ [Fact]
+ public void TestMultiVersionEpisodeAbsoluteNumberingDontCollapse()
+ {
+ // Plain absolute numbering: no season number is available, so the files stay separate.
+ var files = new[]
+ {
+ "/anime/Bleach/Bleach - 001 - The Day I Became a Shinigami.mkv",
+ "/anime/Bleach/Bleach - 002 - The Shinigami's Work.mkv",
+ "/anime/Bleach/Bleach - 003 - The Older Brother's Wish.mkv"
+ };
+
+ var result = _videoListResolver.Resolve(
+ files.Select(i => VideoResolver.Resolve(i, false, _namingOptions)).OfType<VideoFileInfo>().ToList(),
+ collectionType: CollectionType.tvshows).ToList();
+
+ Assert.Equal(3, result.Count);
+ Assert.All(result, r => Assert.Empty(r.AlternateVersions));
+ }
}
}