From 9016fec892239d3cb921bcc8ad03fe7bbc841b60 Mon Sep 17 00:00:00 2001 From: SenorSmartyPants Date: Mon, 13 Mar 2023 16:31:55 -0500 Subject: [PATCH] Make sure episodes in series folder don't group as multiversion Test for episodes in series folder Turn on multiple versions for episodes Support for multiversion episodes in mixed folders Update 2 failing test cases. These were for passing for unofficially supported filenames. Dashes with no spaces, which is not how JF docs say multiversion files are supposed to be named. Fix possible null fix null take 2 Don't ParseName when calling ResolveVideos ParseName drops everything after year in a filename. This results in episode information being dropped if year is present. Update tests to set ParseName=false Additional test with Mixed folder with Year in filename Added case when calculating displayname for versions for mixed folders. Add StringComparison.Ordinal to LastIndexOf Was generating an error in recent build attempts. Clean the episode filename to set the grouper value This allows files like Name (2020) - S01E01 [BluRay-480p x264][AC3 2.0] - [ORIGINAL].mkv Name (2020) - S01E01 [BluRay-1080p x264][AC3 5.1]- [Remaster].mkv to be grouped on 'Name (2020) - S01E01' Fix false positive merging Only do cleanstring or " -" index cleaning, not both. Compatiblity fix when stacking episodes and multiple versions are present Fix linting problems --- Emby.Naming/Common/NamingOptions.cs | 17 ++ Emby.Naming/Video/VideoListResolver.cs | 134 +++++++++-- .../Library/Resolvers/Movies/MovieResolver.cs | 4 +- MediaBrowser.Controller/Entities/BaseItem.cs | 10 +- .../Video/MultiVersionTests.cs | 215 ++++++++++++++++-- .../Video/VideoListResolverTests.cs | 144 ++++++++++-- 6 files changed, 460 insertions(+), 64 deletions(-) diff --git a/Emby.Naming/Common/NamingOptions.cs b/Emby.Naming/Common/NamingOptions.cs index 0cbcc0e4b4..1e953848e8 100644 --- a/Emby.Naming/Common/NamingOptions.cs +++ b/Emby.Naming/Common/NamingOptions.cs @@ -726,6 +726,12 @@ namespace Emby.Naming.Common @"^\s*(?[^ ].*?)\s*$" }; + VideoVersionExpressions = new[] + { + // get filename before final space-dash-space + @"^(?.*?)(?:\s-\s(?!.*\s-\s)(.*))?$" + }; + MultipleEpisodeExpressions = new[] { @".*(\\|\/)[sS]?(?[0-9]{1,4})[xX](?[0-9]{1,3})((-| - )[0-9]{1,4}[eExX](?[0-9]{1,3}))+[^\\\/]*$", @@ -846,6 +852,11 @@ namespace Emby.Naming.Common /// public string[] CleanStrings { get; set; } + /// + /// Gets or sets list of raw clean strings regular expressions strings. + /// + public string[] VideoVersionExpressions { get; set; } + /// /// Gets or sets list of multi-episode regular expressions. /// @@ -866,6 +877,11 @@ namespace Emby.Naming.Common /// public Regex[] CleanStringRegexes { get; private set; } = Array.Empty(); + /// + /// Gets list of video version regular expressions. + /// + public Regex[] VideoVersionRegexes { get; private set; } = Array.Empty(); + /// /// Compiles raw regex strings into regexes. /// @@ -873,6 +889,7 @@ namespace Emby.Naming.Common { CleanDateTimeRegexes = CleanDateTimes.Select(Compile).ToArray(); CleanStringRegexes = CleanStrings.Select(Compile).ToArray(); + VideoVersionRegexes = VideoVersionExpressions.Select(Compile).ToArray(); } private Regex Compile(string exp) diff --git a/Emby.Naming/Video/VideoListResolver.cs b/Emby.Naming/Video/VideoListResolver.cs index 51f29cf088..6812645c15 100644 --- a/Emby.Naming/Video/VideoListResolver.cs +++ b/Emby.Naming/Video/VideoListResolver.cs @@ -4,7 +4,9 @@ using System.IO; using System.Linq; using System.Text.RegularExpressions; using Emby.Naming.Common; +using Emby.Naming.TV; using Jellyfin.Extensions; +using MediaBrowser.Model.Entities; using MediaBrowser.Model.IO; namespace Emby.Naming.Video @@ -25,10 +27,16 @@ namespace Emby.Naming.Video /// /// List of related video files. /// The naming options. + /// Collection type of videos being resolved. /// Indication we should consider multi-versions of content. /// Whether to parse the name or use the filename. /// Returns enumerable of which groups files together when related. - public static IReadOnlyList Resolve(IReadOnlyList videoInfos, NamingOptions namingOptions, bool supportMultiVersion = true, bool parseName = true) + public static IReadOnlyList Resolve( + IReadOnlyList videoInfos, + NamingOptions namingOptions, + string collectionType, + bool supportMultiVersion = true, + bool parseName = true) { // Filter out all extras, otherwise they could cause stacks to not be resolved // See the unit test TestStackedWithTrailer @@ -79,12 +87,19 @@ namespace Emby.Naming.Video var info = new VideoInfo(media.Name) { Files = new[] { media } }; info.Year = info.Files[0].Year; + if (info.Year is null) + { + // parse name for year info. Episodes don't get parsed up to this point for year info + var info2 = VideoResolver.Resolve(media.Path, media.IsDirectory, namingOptions, parseName); + info.Year = info2?.Year; + } + list.Add(info); } if (supportMultiVersion) { - list = GetVideosGroupedByVersion(list, namingOptions); + list = GetVideosGroupedByVersion(list, namingOptions, collectionType); } // Whatever files are left, just add them @@ -98,7 +113,7 @@ namespace Emby.Naming.Video return list; } - private static List GetVideosGroupedByVersion(List videos, NamingOptions namingOptions) + private static List GetVideosGroupedByVersion(List videos, NamingOptions namingOptions, string collectionType) { if (videos.Count == 0) { @@ -112,6 +127,8 @@ namespace Emby.Naming.Video return videos; } + var mergeable = new List(); + var notMergeable = new List(); // Cannot use Span inside local functions and delegates thus we cannot use LINQ here nor merge with the above [if] VideoInfo? primary = null; for (var i = 0; i < videos.Count; i++) @@ -122,9 +139,14 @@ namespace Emby.Naming.Video continue; } - if (!IsEligibleForMultiVersion(folderName, video.Files[0].FileNameWithoutExtension, namingOptions)) + // don't merge stacked episodes + if (video.Files.Count() == 1 && IsEligibleForMultiVersion(folderName, video.Files[0].Path, namingOptions, collectionType)) { - return videos; + mergeable.Add(video); + } + else + { + notMergeable.Add(video); } if (folderName.Equals(video.Files[0].FileNameWithoutExtension, StringComparison.Ordinal)) @@ -133,35 +155,59 @@ namespace Emby.Naming.Video } } - if (videos.Count > 1) + var list = new List(); + if (collectionType.Equals(CollectionType.TvShows, StringComparison.OrdinalIgnoreCase)) { - var groups = videos.GroupBy(x => ResolutionRegex().IsMatch(x.Files[0].FileNameWithoutExtension)).ToList(); - videos.Clear(); + var groupedList = mergeable.GroupBy(x => EpisodeGrouper(x.Files[0].Path, namingOptions, collectionType)); + foreach (var grouping in groupedList) + { + list.Add(OrganizeAlternateVersions(grouping.ToList(), grouping.Key.AsSpan(), primary)); + } + } + else if (mergeable.Count() > 0) + { + list.Add(OrganizeAlternateVersions(mergeable, folderName, primary)); + } + + // add non mergeables back in + list.AddRange(notMergeable); + list.Sort((x, y) => string.Compare(x.Name, y.Name, StringComparison.Ordinal)); + + return list; + } + + private static VideoInfo OrganizeAlternateVersions(List grouping, ReadOnlySpan name, VideoInfo? primary) + { + VideoInfo? groupPrimary = null; + if (primary is not null && grouping.Contains(primary)) + { + groupPrimary = primary; + } + + var alternateVersions = new List(); + if (grouping.Count() > 1) + { + // groups resolution based into one, and all other names + var groups = grouping.GroupBy(x => ResolutionRegex().IsMatch(x.Files[0].FileNameWithoutExtension)); foreach (var group in groups) { if (group.Key) { - videos.InsertRange(0, group.OrderByDescending(x => x.Files[0].FileNameWithoutExtension.ToString(), new AlphanumericComparator())); + alternateVersions.InsertRange(0, group.OrderByDescending(x => x.Files[0].FileNameWithoutExtension.ToString(), new AlphanumericComparator())); } else { - videos.AddRange(group.OrderBy(x => x.Files[0].FileNameWithoutExtension.ToString(), new AlphanumericComparator())); + alternateVersions.AddRange(group.OrderBy(x => x.Files[0].FileNameWithoutExtension.ToString(), new AlphanumericComparator())); } } } - primary ??= videos[0]; - videos.Remove(primary); + groupPrimary ??= alternateVersions.FirstOrDefault() ?? grouping.First(); + alternateVersions.Remove(groupPrimary); + groupPrimary.AlternateVersions = alternateVersions.Select(x => x.Files[0]).ToArray(); - var list = new List - { - primary - }; - - list[0].AlternateVersions = videos.Select(x => x.Files[0]).ToArray(); - list[0].Name = folderName.ToString(); - - return list; + groupPrimary.Name = name.ToString(); + return groupPrimary; } private static bool HaveSameYear(IReadOnlyList videos) @@ -183,8 +229,16 @@ namespace Emby.Naming.Video return true; } - private static bool IsEligibleForMultiVersion(ReadOnlySpan folderName, ReadOnlySpan testFilename, NamingOptions namingOptions) + private static bool IsEligibleForMultiVersion(ReadOnlySpan folderName, ReadOnlySpan testFilePath, NamingOptions namingOptions, ReadOnlySpan collectionType) { + var testFilename = Path.GetFileNameWithoutExtension(testFilePath); + + if (collectionType.Equals(CollectionType.TvShows, StringComparison.OrdinalIgnoreCase)) + { + // episodes are always eligible to be grouped + return true; + } + if (!testFilename.StartsWith(folderName, StringComparison.OrdinalIgnoreCase)) { return false; @@ -207,5 +261,41 @@ namespace Emby.Naming.Video || testFilename[0] == '-' || CheckMultiVersionRegex().IsMatch(testFilename); } + + private static string EpisodeGrouper(string testFilePath, NamingOptions namingOptions, ReadOnlySpan collectionType) + { + // grouper for tv shows/episodes should be everything before space-dash-space + var resolver = new EpisodeResolver(namingOptions); + EpisodeInfo? episodeInfo = resolver.Resolve(testFilePath, false); + ReadOnlySpan seriesName = episodeInfo!.SeriesName; + + var filename = Path.GetFileNameWithoutExtension(testFilePath); + // start with grouping by filename + string g = filename; + for (var i = 0; i < namingOptions.VideoVersionRegexes.Length; i++) + { + var rule = namingOptions.VideoVersionRegexes[i]; + var match = rule.Match(filename); + if (!match.Success) + { + continue; + } + + g = match.Groups["filename"].Value; + // clean the filename + if (VideoResolver.TryCleanString(g, namingOptions, out string newName)) + { + g = newName; + } + + // never group episodes under series name + if (MemoryExtensions.Equals(g.AsSpan(), seriesName, StringComparison.OrdinalIgnoreCase)) + { + g = filename; + } + } + + return g; + } } } diff --git a/Emby.Server.Implementations/Library/Resolvers/Movies/MovieResolver.cs b/Emby.Server.Implementations/Library/Resolvers/Movies/MovieResolver.cs index 0b65bf921e..5c0ab4bcf4 100644 --- a/Emby.Server.Implementations/Library/Resolvers/Movies/MovieResolver.cs +++ b/Emby.Server.Implementations/Library/Resolvers/Movies/MovieResolver.cs @@ -229,7 +229,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies if (string.Equals(collectionType, CollectionType.TvShows, StringComparison.OrdinalIgnoreCase)) { - return ResolveVideos(parent, files, false, collectionType, true); + return ResolveVideos(parent, files, true, collectionType, false); } return null; @@ -275,7 +275,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies .Where(f => f is not null) .ToList(); - var resolverResult = VideoListResolver.Resolve(videoInfos, NamingOptions, supportMultiEditions, parseName); + var resolverResult = VideoListResolver.Resolve(videoInfos, NamingOptions, collectionType, supportMultiEditions, parseName); var result = new MultiItemResolverResult { diff --git a/MediaBrowser.Controller/Entities/BaseItem.cs b/MediaBrowser.Controller/Entities/BaseItem.cs index 9f3e8eec96..b552a2b0f8 100644 --- a/MediaBrowser.Controller/Entities/BaseItem.cs +++ b/MediaBrowser.Controller/Entities/BaseItem.cs @@ -1179,10 +1179,18 @@ namespace MediaBrowser.Controller.Entities { if (HasLocalAlternateVersions) { - var displayName = System.IO.Path.GetFileNameWithoutExtension(path) + var fileName = System.IO.Path.GetFileNameWithoutExtension(path); + var displayName = fileName .Replace(System.IO.Path.GetFileName(ContainingFolderPath), string.Empty, StringComparison.OrdinalIgnoreCase) .TrimStart(new char[] { ' ', '-' }); + if (fileName == displayName) + { + // file does not start with parent folder name. This must be an episode in a mixed directory + // get string after last dash - this is the version name + displayName = fileName.Substring(fileName.LastIndexOf('-') + 1).TrimStart(new char[] { ' ', '-' }); + } + if (!string.IsNullOrEmpty(displayName)) { terms.Add(displayName); diff --git a/tests/Jellyfin.Naming.Tests/Video/MultiVersionTests.cs b/tests/Jellyfin.Naming.Tests/Video/MultiVersionTests.cs index 294f11ee74..a9019fa3ff 100644 --- a/tests/Jellyfin.Naming.Tests/Video/MultiVersionTests.cs +++ b/tests/Jellyfin.Naming.Tests/Video/MultiVersionTests.cs @@ -1,7 +1,9 @@ +using System; using System.Collections.Generic; using System.Linq; using Emby.Naming.Common; using Emby.Naming.Video; +using MediaBrowser.Model.Entities; using Xunit; namespace Jellyfin.Naming.Tests.Video @@ -23,7 +25,8 @@ namespace Jellyfin.Naming.Tests.Video var result = VideoListResolver.Resolve( files.Select(i => VideoResolver.Resolve(i, false, _namingOptions)).OfType().ToList(), - _namingOptions).ToList(); + _namingOptions, + CollectionType.Movies).ToList(); Assert.Single(result.Where(v => v.ExtraType is null)); Assert.Single(result.Where(v => v.ExtraType is not null)); @@ -42,7 +45,8 @@ namespace Jellyfin.Naming.Tests.Video var result = VideoListResolver.Resolve( files.Select(i => VideoResolver.Resolve(i, false, _namingOptions)).OfType().ToList(), - _namingOptions).ToList(); + _namingOptions, + CollectionType.Movies).ToList(); Assert.Single(result.Where(v => v.ExtraType is null)); Assert.Single(result.Where(v => v.ExtraType is not null)); @@ -60,7 +64,8 @@ namespace Jellyfin.Naming.Tests.Video var result = VideoListResolver.Resolve( files.Select(i => VideoResolver.Resolve(i, false, _namingOptions)).OfType().ToList(), - _namingOptions).ToList(); + _namingOptions, + CollectionType.Movies).ToList(); Assert.Single(result); Assert.Single(result[0].AlternateVersions); @@ -82,7 +87,8 @@ namespace Jellyfin.Naming.Tests.Video var result = VideoListResolver.Resolve( files.Select(i => VideoResolver.Resolve(i, false, _namingOptions)).OfType().ToList(), - _namingOptions).ToList(); + _namingOptions, + CollectionType.Movies).ToList(); Assert.Equal(7, result.Count); Assert.Empty(result[0].AlternateVersions); @@ -105,7 +111,8 @@ namespace Jellyfin.Naming.Tests.Video var result = VideoListResolver.Resolve( files.Select(i => VideoResolver.Resolve(i, false, _namingOptions)).OfType().ToList(), - _namingOptions).ToList(); + _namingOptions, + CollectionType.Movies).ToList(); Assert.Single(result); Assert.Equal(7, result[0].AlternateVersions.Count); @@ -129,7 +136,8 @@ namespace Jellyfin.Naming.Tests.Video var result = VideoListResolver.Resolve( files.Select(i => VideoResolver.Resolve(i, false, _namingOptions)).OfType().ToList(), - _namingOptions).ToList(); + _namingOptions, + CollectionType.Movies).ToList(); Assert.Equal(9, result.Count); Assert.Empty(result[0].AlternateVersions); @@ -149,7 +157,8 @@ namespace Jellyfin.Naming.Tests.Video var result = VideoListResolver.Resolve( files.Select(i => VideoResolver.Resolve(i, false, _namingOptions)).OfType().ToList(), - _namingOptions).ToList(); + _namingOptions, + CollectionType.Movies).ToList(); Assert.Equal(5, result.Count); Assert.Empty(result[0].AlternateVersions); @@ -171,7 +180,8 @@ namespace Jellyfin.Naming.Tests.Video var result = VideoListResolver.Resolve( files.Select(i => VideoResolver.Resolve(i, false, _namingOptions)).OfType().ToList(), - _namingOptions).ToList(); + _namingOptions, + CollectionType.Movies).ToList(); Assert.Equal(5, result.Count); Assert.Empty(result[0].AlternateVersions); @@ -193,7 +203,8 @@ namespace Jellyfin.Naming.Tests.Video var result = VideoListResolver.Resolve( files.Select(i => VideoResolver.Resolve(i, false, _namingOptions)).OfType().ToList(), - _namingOptions).ToList(); + _namingOptions, + CollectionType.Movies).ToList(); Assert.Single(result); Assert.Equal("/movies/Iron Man/Iron Man.mkv", result[0].Files[0].Path); @@ -222,7 +233,8 @@ namespace Jellyfin.Naming.Tests.Video var result = VideoListResolver.Resolve( files.Select(i => VideoResolver.Resolve(i, false, _namingOptions)).OfType().ToList(), - _namingOptions).ToList(); + _namingOptions, + CollectionType.Movies).ToList(); Assert.Single(result); Assert.Equal("/movies/Iron Man/Iron Man.mkv", result[0].Files[0].Path); @@ -246,7 +258,8 @@ namespace Jellyfin.Naming.Tests.Video var result = VideoListResolver.Resolve( files.Select(i => VideoResolver.Resolve(i, false, _namingOptions)).OfType().ToList(), - _namingOptions).ToList(); + _namingOptions, + CollectionType.Movies).ToList(); Assert.Equal(2, result.Count); } @@ -267,7 +280,8 @@ namespace Jellyfin.Naming.Tests.Video var result = VideoListResolver.Resolve( files.Select(i => VideoResolver.Resolve(i, false, _namingOptions)).OfType().ToList(), - _namingOptions).ToList(); + _namingOptions, + CollectionType.Movies).ToList(); Assert.Equal(7, result.Count); Assert.Empty(result[0].AlternateVersions); @@ -289,7 +303,8 @@ namespace Jellyfin.Naming.Tests.Video var result = VideoListResolver.Resolve( files.Select(i => VideoResolver.Resolve(i, false, _namingOptions)).OfType().ToList(), - _namingOptions).ToList(); + _namingOptions, + CollectionType.Movies).ToList(); Assert.Equal(5, result.Count); Assert.Empty(result[0].AlternateVersions); @@ -306,7 +321,8 @@ namespace Jellyfin.Naming.Tests.Video var result = VideoListResolver.Resolve( files.Select(i => VideoResolver.Resolve(i, false, _namingOptions)).OfType().ToList(), - _namingOptions).ToList(); + _namingOptions, + CollectionType.Movies); Assert.Single(result); Assert.Single(result[0].AlternateVersions); @@ -323,7 +339,8 @@ namespace Jellyfin.Naming.Tests.Video var result = VideoListResolver.Resolve( files.Select(i => VideoResolver.Resolve(i, false, _namingOptions)).OfType().ToList(), - _namingOptions).ToList(); + _namingOptions, + CollectionType.Movies); Assert.Single(result); Assert.Single(result[0].AlternateVersions); @@ -344,7 +361,8 @@ namespace Jellyfin.Naming.Tests.Video var result = VideoListResolver.Resolve( files.Select(i => VideoResolver.Resolve(i, false, _namingOptions)).OfType().ToList(), - _namingOptions).ToList(); + _namingOptions, + CollectionType.Movies); Assert.Single(result); Assert.Equal("/movies/X-Men Apocalypse (2016)/X-Men Apocalypse (2016).mkv", result[0].Files[0].Path); @@ -367,7 +385,8 @@ namespace Jellyfin.Naming.Tests.Video var result = VideoListResolver.Resolve( files.Select(i => VideoResolver.Resolve(i, false, _namingOptions)).OfType().ToList(), - _namingOptions).ToList(); + _namingOptions, + CollectionType.Movies); Assert.Single(result); Assert.Single(result[0].AlternateVersions); @@ -384,7 +403,8 @@ namespace Jellyfin.Naming.Tests.Video var result = VideoListResolver.Resolve( files.Select(i => VideoResolver.Resolve(i, false, _namingOptions)).OfType().ToList(), - _namingOptions).ToList(); + _namingOptions, + CollectionType.Movies); Assert.Equal(2, result.Count); } @@ -392,9 +412,166 @@ namespace Jellyfin.Naming.Tests.Video [Fact] public void TestEmptyList() { - var result = VideoListResolver.Resolve(new List(), _namingOptions).ToList(); + var result = VideoListResolver.Resolve(new List(), _namingOptions, string.Empty).ToList(); Assert.Empty(result); } + + [Fact] + public void TestMultiVersionEpisodeDontCollapse() + { + // Test for false positive + + var files = new[] + { + @"/TV/Dexter/Dexter - S01E01 - One.mkv", + @"/TV/Dexter/Dexter - S01E02 - Two.mkv", + @"/TV/Dexter/Dexter - S01E03 - Three.mkv", + @"/TV/Dexter/Dexter - S01E04 - Four.mkv", + @"/TV/Dexter/Dexter - S01E05 - Five.mkv", + }; + + var result = VideoListResolver.Resolve( + files.Select(i => VideoResolver.Resolve(i, false, _namingOptions, false)).OfType().ToList(), + _namingOptions, + CollectionType.TvShows); + + Assert.Equal(5, result.Count); + Assert.Empty(result[0].AlternateVersions); + } + + [Fact] + public void TestMultiVersionEpisodeDontCollapse2() + { + // Test for false positive + + var files = new[] + { + @"/TV/Dexter/Dexter - S01E01 One.mkv", + @"/TV/Dexter/Dexter - S01E02 Two.mkv", + @"/TV/Dexter/Dexter - S01E03 Three.mkv", + @"/TV/Dexter/Dexter - S01E04 Four.mkv", + @"/TV/Dexter/Dexter - S01E05 Five.mkv", + @"/TV/Star Trek- Picard/Season 3/Star Trek - Picard 3x01 [WEBDL-720p Proper x264][EAC3 5.1] - Part One - The Next Generation.mkv", + @"/TV/Star Trek- Picard/Season 3/Star Trek - Picard 3x02 [WEBDL-720p Proper x264][EAC3 5.1] - Part Two - Disengage.mkv", + @"/TV/Star Trek- Picard/Season 3/Star Trek - Picard 3x03 [WEBDL-720p x264][EAC3 5.1] - Part Three - Seventeen Seconds.mkv", + }; + + var result = VideoListResolver.Resolve( + files.Select(i => VideoResolver.Resolve(i, false, _namingOptions, false)).OfType().ToList(), + _namingOptions, + CollectionType.TvShows); + + Assert.Equal(8, result.Count); + Assert.Empty(result[0].AlternateVersions); + } + + [Fact] + public void TestMultiVersionEpisode() + { + var files = new[] + { + @"/TV/Dexter/Dexter - S01E01/Dexter - S01E01 - One.mkv", + @"/TV/Dexter/Dexter - S01E01/Dexter - S01E01 - Two.mkv", + }; + + var result = VideoListResolver.Resolve( + files.Select(i => VideoResolver.Resolve(i, false, _namingOptions, false)).OfType().ToList(), + _namingOptions, + CollectionType.TvShows); + + Assert.Single(result); + Assert.Single(result[0].AlternateVersions); + } + + [Fact] + public void TestMultiVersionEpisodeMixedSeriesFolder() + { + var files = new[] + { + @"/TV/Dexter/Dexter - S01E01.mkv", + @"/TV/Dexter/Dexter - S01E01 - Unaired.mkv", + @"/TV/Dexter/Dexter - S01E02 - Two.mkv", + @"/TV/Dexter/Dexter - S01E03 - Three.mkv", + @"/TV/Dexter/Dexter S02E01 - Ia.mkv", + @"/TV/Dexter/Dexter S02E01 - I.mkv", + @"/TV/Dexter/Dexter - S02E02.mkv", + }; + + var result = VideoListResolver.Resolve( + files.Select(i => VideoResolver.Resolve(i, false, _namingOptions, false)).OfType().ToList(), + _namingOptions, + CollectionType.TvShows); + + Assert.Equal(5, result.Count); + Assert.Single(result[0].AlternateVersions); + Assert.Empty(result[1].AlternateVersions); + Assert.Empty(result[2].AlternateVersions); + Assert.Empty(result[3].AlternateVersions); + + var s02e01 = result.FirstOrDefault(x => string.Equals(x.Name, "Dexter S02E01", StringComparison.Ordinal)); + Assert.NotNull(s02e01); + Assert.Single(s02e01!.AlternateVersions); + } + + [Fact] + public void TestMultiVersionEpisodeMixedSeasonFolder() + { + var files = new[] + { + @"/TV/Dexter/Season 2/Dexter - S02E01 - Ia.mkv", + @"/TV/Dexter/Season 2/Dexter - S02E01 - I.mkv", + @"/TV/Dexter/Season 2/Dexter - S02E02.mkv", + }; + + var result = VideoListResolver.Resolve( + files.Select(i => VideoResolver.Resolve(i, false, _namingOptions, false)).OfType().ToList(), + _namingOptions, + CollectionType.TvShows); + + Assert.Equal(2, result.Count); + Assert.Single(result[0].AlternateVersions); + Assert.Empty(result[1].AlternateVersions); + } + + [Fact] + public void TestMultiVersionEpisodeMixedSeasonFolderWithYear() + { + var files = new[] + { + @"/TV/Name (2020)/Season 1/Name (2020) - S01E01 - [ORIGINAL].mkv", + @"/TV/Name (2020)/Season 1/Name (2020) - S01E01 - [VERSION].mkv", + @"/TV/Name (2020)/Season 1/Name (2020) - S01E02 - [ORIGINAL].mkv", + }; + + var result = VideoListResolver.Resolve( + files.Select(i => VideoResolver.Resolve(i, false, _namingOptions, false)).OfType().ToList(), + _namingOptions, + CollectionType.TvShows); + + Assert.Equal(2, result.Count); + Assert.Single(result[0].AlternateVersions); + Assert.Empty(result[1].AlternateVersions); + } + + [Fact] + public void TestMultiVersionEpisodeMixedSeasonFolderWithYearAndDirtyNames() + { + var files = new[] + { + @"/TV/Name (2020)/Season 1/Name (2020) - S01E01 [BluRay-480p x264][AC3 2.0] - [ORIGINAL].mkv", + @"/TV/Name (2020)/Season 1/Name (2020) - S01E01 [BluRay-1080p x264][AC3 5.1] - [Remaster].mkv", + @"/TV/Name (2020)/Season 1/Name (2020) - S01E02 - [ORIGINAL].mkv", + }; + + var result = VideoListResolver.Resolve( + files.Select(i => VideoResolver.Resolve(i, false, _namingOptions, false)).OfType().ToList(), + _namingOptions, + CollectionType.TvShows); + + Assert.Equal(2, result.Count); + Assert.Single(result[0].AlternateVersions); + Assert.Empty(result[1].AlternateVersions); + } } } diff --git a/tests/Jellyfin.Naming.Tests/Video/VideoListResolverTests.cs b/tests/Jellyfin.Naming.Tests/Video/VideoListResolverTests.cs index 0316377d49..eff57721b7 100644 --- a/tests/Jellyfin.Naming.Tests/Video/VideoListResolverTests.cs +++ b/tests/Jellyfin.Naming.Tests/Video/VideoListResolverTests.cs @@ -42,7 +42,8 @@ namespace Jellyfin.Naming.Tests.Video var result = VideoListResolver.Resolve( files.Select(i => VideoResolver.Resolve(i, false, _namingOptions)).OfType().ToList(), - _namingOptions).ToList(); + _namingOptions, + CollectionType.Movies).ToList(); Assert.Equal(11, result.Count); var batman = result.FirstOrDefault(x => string.Equals(x.Name, "Batman", StringComparison.Ordinal)); @@ -65,6 +66,90 @@ namespace Jellyfin.Naming.Tests.Video Assert.Equal(ExtraType.Trailer, result[10].ExtraType); } + [Fact] + public void TestTVStackAndVersions() + { + // No stacking here because there is no part/disc/etc + var files = new[] + { + @"/TV/Grey's Anatomy (2005)/Grey's Anatomy (2005) - s01e01 CD1.avi", + @"/TV/Grey's Anatomy (2005)/Grey's Anatomy (2005) - s01e01 CD2.avi", + @"/TV/Grey's Anatomy (2005)/Grey's Anatomy (2005) - s01e02 - The First Cut is the Deepest.avi", + @"/TV/Grey's Anatomy (2005)/Grey's Anatomy (2005) - s01e03.mp4", + @"/TV/Grey's Anatomy (2005)/Grey's Anatomy (2005) - s01e04 - Aired Version.mp4", + @"/TV/Grey's Anatomy (2005)/Grey's Anatomy (2005) - s01e04 - Uncensored Version.mp4" + }; + + var result = VideoListResolver.Resolve( + files.Select(i => VideoResolver.Resolve(i, false, _namingOptions, false)).OfType().ToList(), + _namingOptions, + CollectionType.TvShows).ToList(); + + Assert.Equal(4, result.Count); + + var s01e01 = result.FirstOrDefault(x => string.Equals(x.Name, "Grey's Anatomy (2005) - s01e01", StringComparison.Ordinal)); + Assert.NotNull(s01e01); + Assert.Equal(2, s01e01!.Files.Count); + + var s01e04 = result.FirstOrDefault(x => string.Equals(x.Name, "Grey's Anatomy (2005) - s01e04", StringComparison.Ordinal)); + Assert.NotNull(s01e04); + Assert.Equal(1, s01e04!.AlternateVersions.Count); + } + + [Fact] + public void TestTVStackAndVersionsNoFirstDash() + { + // No stacking here because there is no part/disc/etc + var files = new[] + { + @"/TV/Grey's Anatomy (2005)/Grey's Anatomy (2005) s01e01 - pt1.avi", + @"/TV/Grey's Anatomy (2005)/Grey's Anatomy (2005) s01e01 - pt2.avi", + @"/TV/Grey's Anatomy (2005)/Grey's Anatomy (2005) s01e02 - The First Cut is the Deepest.avi", + @"/TV/Grey's Anatomy (2005)/Grey's Anatomy (2005) s01e03.mp4", + @"/TV/Grey's Anatomy (2005)/Grey's Anatomy (2005) s01e04 - Aired Version.mp4", + @"/TV/Grey's Anatomy (2005)/Grey's Anatomy (2005) s01e04 - Uncensored Version.mp4" + }; + + var result = VideoListResolver.Resolve( + files.Select(i => VideoResolver.Resolve(i, false, _namingOptions, false)).OfType().ToList(), + _namingOptions, + CollectionType.TvShows).ToList(); + + Assert.Equal(4, result.Count); + + var s01e01 = result.FirstOrDefault(x => string.Equals(x.Name, "Grey's Anatomy (2005) s01e01", StringComparison.Ordinal)); + Assert.NotNull(s01e01); + Assert.Equal(2, s01e01!.Files.Count); + + var s01e04 = result.FirstOrDefault(x => string.Equals(x.Name, "Grey's Anatomy (2005) s01e04", StringComparison.Ordinal)); + Assert.NotNull(s01e04); + Assert.Equal(1, s01e04!.AlternateVersions.Count); + } + + [Fact] + public void TestTVStack() + { + // No stacking here because there is no part/disc/etc + var files = new[] + { + @"/TV/Doctor Who/Season 21/Doctor Who 21x11 - Resurrection of the Daleks - Part 1.mkv", + @"/TV/Doctor Who/Season 21/Doctor Who 21x11 - Resurrection of the Daleks - Part 2.mkv", + @"/TV/Doctor Who/Season 21/Doctor Who 21x12 - Resurrection of the Daleks - Part 3.mkv", + @"/TV/Doctor Who/Season 21/Doctor Who 21x12 - Resurrection of the Daleks - Part 4.mkv" + }; + + var result = VideoListResolver.Resolve( + files.Select(i => VideoResolver.Resolve(i, false, _namingOptions, false)).OfType().ToList(), + _namingOptions, + CollectionType.TvShows).ToList(); + + Assert.Equal(2, result.Count); + + var s21e12 = result.FirstOrDefault(x => string.Equals(x.Name, "Doctor Who 21x12 - Resurrection of the Daleks", StringComparison.Ordinal)); + Assert.NotNull(s21e12); + Assert.Equal(2, s21e12!.Files.Count); + } + [Fact] public void TestWithMetadata() { @@ -76,7 +161,8 @@ namespace Jellyfin.Naming.Tests.Video var result = VideoListResolver.Resolve( files.Select(i => VideoResolver.Resolve(i, false, _namingOptions)).OfType().ToList(), - _namingOptions).ToList(); + _namingOptions, + CollectionType.Movies).ToList(); Assert.Single(result); } @@ -92,7 +178,8 @@ namespace Jellyfin.Naming.Tests.Video var result = VideoListResolver.Resolve( files.Select(i => VideoResolver.Resolve(i, false, _namingOptions)).OfType().ToList(), - _namingOptions).ToList(); + _namingOptions, + CollectionType.Movies).ToList(); Assert.Equal(2, result.Count); Assert.False(result[0].ExtraType.HasValue); @@ -110,7 +197,8 @@ namespace Jellyfin.Naming.Tests.Video var result = VideoListResolver.Resolve( files.Select(i => VideoResolver.Resolve(i, false, _namingOptions)).OfType().ToList(), - _namingOptions).ToList(); + _namingOptions, + CollectionType.Movies).ToList(); Assert.Equal(2, result.Count); Assert.False(result[0].ExtraType.HasValue); @@ -129,7 +217,8 @@ namespace Jellyfin.Naming.Tests.Video var result = VideoListResolver.Resolve( files.Select(i => VideoResolver.Resolve(i, false, _namingOptions)).OfType().ToList(), - _namingOptions).ToList(); + _namingOptions, + CollectionType.Movies).ToList(); Assert.Equal(3, result.Count); Assert.False(result[0].ExtraType.HasValue); @@ -149,7 +238,8 @@ namespace Jellyfin.Naming.Tests.Video var result = VideoListResolver.Resolve( files.Select(i => VideoResolver.Resolve(i, false, _namingOptions)).OfType().ToList(), - _namingOptions).ToList(); + _namingOptions, + CollectionType.Movies).ToList(); Assert.Equal(3, result.Count); Assert.False(result[0].ExtraType.HasValue); @@ -168,7 +258,8 @@ namespace Jellyfin.Naming.Tests.Video var result = VideoListResolver.Resolve( files.Select(i => VideoResolver.Resolve(i, false, _namingOptions)).OfType().ToList(), - _namingOptions).ToList(); + _namingOptions, + CollectionType.Movies).ToList(); Assert.Equal(2, result.Count); Assert.False(result[0].ExtraType.HasValue); @@ -190,7 +281,8 @@ namespace Jellyfin.Naming.Tests.Video var result = VideoListResolver.Resolve( files.Select(i => VideoResolver.Resolve(i, false, _namingOptions)).OfType().ToList(), - _namingOptions).ToList(); + _namingOptions, + string.Empty).ToList(); Assert.Equal(5, result.Count); } @@ -206,7 +298,8 @@ namespace Jellyfin.Naming.Tests.Video var result = VideoListResolver.Resolve( files.Select(i => VideoResolver.Resolve(i, true, _namingOptions)).OfType().ToList(), - _namingOptions).ToList(); + _namingOptions, + CollectionType.Movies).ToList(); Assert.Single(result); } @@ -223,7 +316,8 @@ namespace Jellyfin.Naming.Tests.Video var result = VideoListResolver.Resolve( files.Select(i => VideoResolver.Resolve(i, true, _namingOptions)).OfType().ToList(), - _namingOptions).ToList(); + _namingOptions, + CollectionType.Movies).ToList(); Assert.Equal(2, result.Count); } @@ -241,7 +335,8 @@ namespace Jellyfin.Naming.Tests.Video var result = VideoListResolver.Resolve( files.Select(i => VideoResolver.Resolve(i, false, _namingOptions)).OfType().ToList(), - _namingOptions).ToList(); + _namingOptions, + CollectionType.Movies).ToList(); Assert.Equal(3, result.Count); Assert.False(result[0].ExtraType.HasValue); @@ -262,7 +357,8 @@ namespace Jellyfin.Naming.Tests.Video var result = VideoListResolver.Resolve( files.Select(i => VideoResolver.Resolve(i, false, _namingOptions)).OfType().ToList(), - _namingOptions).ToList(); + _namingOptions, + CollectionType.Movies).ToList(); Assert.Equal(4, result.Count); Assert.False(result[0].ExtraType.HasValue); @@ -284,7 +380,8 @@ namespace Jellyfin.Naming.Tests.Video var result = VideoListResolver.Resolve( files.Select(i => VideoResolver.Resolve(i, false, _namingOptions)).OfType().ToList(), - _namingOptions).ToList(); + _namingOptions, + string.Empty).ToList(); Assert.Equal(2, result.Count); } @@ -299,7 +396,8 @@ namespace Jellyfin.Naming.Tests.Video var result = VideoListResolver.Resolve( files.Select(i => VideoResolver.Resolve(i, false, _namingOptions)).OfType().ToList(), - _namingOptions).ToList(); + _namingOptions, + CollectionType.Movies).ToList(); Assert.Single(result); } @@ -314,7 +412,8 @@ namespace Jellyfin.Naming.Tests.Video var result = VideoListResolver.Resolve( files.Select(i => VideoResolver.Resolve(i, false, _namingOptions)).OfType().ToList(), - _namingOptions).ToList(); + _namingOptions, + CollectionType.Movies).ToList(); Assert.Single(result); } @@ -330,7 +429,8 @@ namespace Jellyfin.Naming.Tests.Video var result = VideoListResolver.Resolve( files.Select(i => VideoResolver.Resolve(i, false, _namingOptions)).OfType().ToList(), - _namingOptions).ToList(); + _namingOptions, + CollectionType.Movies).ToList(); // The result should contain two individual movies // Version grouping should not work here, because the files are not in a directory with the name 'Four Sisters and a Wedding' @@ -348,7 +448,8 @@ namespace Jellyfin.Naming.Tests.Video var result = VideoListResolver.Resolve( files.Select(i => VideoResolver.Resolve(i, false, _namingOptions)).OfType().ToList(), - _namingOptions).ToList(); + _namingOptions, + CollectionType.Movies).ToList(); Assert.Equal(2, result.Count); } @@ -364,7 +465,8 @@ namespace Jellyfin.Naming.Tests.Video var result = VideoListResolver.Resolve( files.Select(i => VideoResolver.Resolve(i, false, _namingOptions)).OfType().ToList(), - _namingOptions).ToList(); + _namingOptions, + CollectionType.Movies).ToList(); Assert.Equal(2, result.Count); Assert.False(result[0].ExtraType.HasValue); @@ -382,7 +484,8 @@ namespace Jellyfin.Naming.Tests.Video var result = VideoListResolver.Resolve( files.Select(i => VideoResolver.Resolve(i, false, _namingOptions)).OfType().ToList(), - _namingOptions).ToList(); + _namingOptions, + CollectionType.Movies).ToList(); Assert.Equal(2, result.Count); Assert.False(result[0].ExtraType.HasValue); @@ -400,7 +503,8 @@ namespace Jellyfin.Naming.Tests.Video var result = VideoListResolver.Resolve( files.Select(i => VideoResolver.Resolve(i, false, _namingOptions)).OfType().ToList(), - _namingOptions).ToList(); + _namingOptions, + CollectionType.Movies).ToList(); Assert.Equal(2, result.Count); Assert.False(result[0].ExtraType.HasValue);