mirror of
https://github.com/jellyfin/jellyfin.git
synced 2026-05-16 05:36:52 +01:00
Implement multiple versions for episodes.
This commit is contained in:
@@ -5,7 +5,8 @@ using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text.RegularExpressions;
|
||||
using Emby.Naming.Common;
|
||||
using Jellyfin.Extensions;
|
||||
using Emby.Naming.TV;
|
||||
using Jellyfin.Data.Enums;
|
||||
using MediaBrowser.Model.IO;
|
||||
|
||||
namespace Emby.Naming.Video
|
||||
@@ -13,8 +14,21 @@ namespace Emby.Naming.Video
|
||||
/// <summary>
|
||||
/// Resolves alternative versions and extras from list of video files.
|
||||
/// </summary>
|
||||
public static partial class VideoListResolver
|
||||
public partial class VideoListResolver
|
||||
{
|
||||
private readonly NamingOptions _namingOptions;
|
||||
private readonly EpisodePathParser _episodePathParser;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="VideoListResolver"/> class.
|
||||
/// </summary>
|
||||
/// <param name="namingOptions">The naming options.</param>
|
||||
public VideoListResolver(NamingOptions namingOptions)
|
||||
{
|
||||
_namingOptions = namingOptions;
|
||||
_episodePathParser = new EpisodePathParser(namingOptions);
|
||||
}
|
||||
|
||||
[GeneratedRegex("[0-9]{2}[0-9]+[ip]", RegexOptions.IgnoreCase)]
|
||||
private static partial Regex ResolutionRegex();
|
||||
|
||||
@@ -25,12 +39,12 @@ namespace Emby.Naming.Video
|
||||
/// Resolves alternative versions and extras from list of video files.
|
||||
/// </summary>
|
||||
/// <param name="videoInfos">List of related video files.</param>
|
||||
/// <param name="namingOptions">The naming options.</param>
|
||||
/// <param name="supportMultiVersion">Indication we should consider multi-versions of content.</param>
|
||||
/// <param name="parseName">Whether to parse the name or use the filename.</param>
|
||||
/// <param name="libraryRoot">Top-level folder for the containing library.</param>
|
||||
/// <param name="collectionType">The type of the containing collection, if known.</param>
|
||||
/// <returns>Returns enumerable of <see cref="VideoInfo"/> which groups files together when related.</returns>
|
||||
public static IReadOnlyList<VideoInfo> Resolve(IReadOnlyList<VideoFileInfo> videoInfos, NamingOptions namingOptions, bool supportMultiVersion = true, bool parseName = true, string? libraryRoot = "")
|
||||
public IReadOnlyList<VideoInfo> Resolve(IReadOnlyList<VideoFileInfo> videoInfos, bool supportMultiVersion = true, bool parseName = true, string? libraryRoot = "", CollectionType? collectionType = null)
|
||||
{
|
||||
// Filter out all extras, otherwise they could cause stacks to not be resolved
|
||||
// See the unit test TestStackedWithTrailer
|
||||
@@ -38,7 +52,7 @@ namespace Emby.Naming.Video
|
||||
.Where(i => i.ExtraType is null)
|
||||
.Select(i => new FileSystemMetadata { FullName = i.Path, IsDirectory = i.IsDirectory });
|
||||
|
||||
var stackResult = StackResolver.Resolve(nonExtras, namingOptions).ToList();
|
||||
var stackResult = StackResolver.Resolve(nonExtras, _namingOptions).ToList();
|
||||
|
||||
var remainingFiles = new List<VideoFileInfo>();
|
||||
var standaloneMedia = new List<VideoFileInfo>();
|
||||
@@ -67,7 +81,7 @@ namespace Emby.Naming.Video
|
||||
{
|
||||
var info = new VideoInfo(stack.Name)
|
||||
{
|
||||
Files = stack.Files.Select(i => VideoResolver.Resolve(i, stack.IsDirectoryStack, namingOptions, parseName, libraryRoot))
|
||||
Files = stack.Files.Select(i => VideoResolver.Resolve(i, stack.IsDirectoryStack, _namingOptions, parseName, libraryRoot))
|
||||
.OfType<VideoFileInfo>()
|
||||
.ToList()
|
||||
};
|
||||
@@ -86,7 +100,9 @@ namespace Emby.Naming.Video
|
||||
|
||||
if (supportMultiVersion)
|
||||
{
|
||||
list = GetVideosGroupedByVersion(list, namingOptions);
|
||||
list = collectionType is CollectionType.tvshows
|
||||
? GetEpisodesGroupedByVersion(list)
|
||||
: GetVideosGroupedByVersion(list);
|
||||
}
|
||||
|
||||
// Whatever files are left, just add them
|
||||
@@ -100,7 +116,7 @@ namespace Emby.Naming.Video
|
||||
return list;
|
||||
}
|
||||
|
||||
private static List<VideoInfo> GetVideosGroupedByVersion(List<VideoInfo> videos, NamingOptions namingOptions)
|
||||
private List<VideoInfo> GetVideosGroupedByVersion(List<VideoInfo> videos)
|
||||
{
|
||||
if (videos.Count == 0)
|
||||
{
|
||||
@@ -124,7 +140,7 @@ namespace Emby.Naming.Video
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!IsEligibleForMultiVersion(folderName, video.Files[0].FileNameWithoutExtension, namingOptions))
|
||||
if (!IsEligibleForMultiVersion(folderName, video.Files[0].FileNameWithoutExtension))
|
||||
{
|
||||
return videos;
|
||||
}
|
||||
@@ -135,45 +151,9 @@ namespace Emby.Naming.Video
|
||||
}
|
||||
}
|
||||
|
||||
if (videos.Count > 1)
|
||||
{
|
||||
var groups = videos
|
||||
.Select(x => (filename: x.Files[0].FileNameWithoutExtension.ToString(), value: x))
|
||||
.Select(x => (x.filename, resolutionMatch: ResolutionRegex().Match(x.filename), x.value))
|
||||
.GroupBy(x => x.resolutionMatch.Success)
|
||||
.ToList();
|
||||
var organized = OrganizeAlternateVersions(videos, primary, folderName.ToString());
|
||||
|
||||
videos.Clear();
|
||||
|
||||
StringComparer comparer = StringComparer.Create(CultureInfo.InvariantCulture, CompareOptions.NumericOrdering);
|
||||
foreach (var group in groups)
|
||||
{
|
||||
if (group.Key)
|
||||
{
|
||||
videos.InsertRange(0, group
|
||||
.OrderByDescending(x => x.resolutionMatch.Value, comparer)
|
||||
.ThenBy(x => x.filename, comparer)
|
||||
.Select(x => x.value));
|
||||
}
|
||||
else
|
||||
{
|
||||
videos.AddRange(group.OrderBy(x => x.filename, comparer).Select(x => x.value));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
primary ??= videos[0];
|
||||
videos.Remove(primary);
|
||||
|
||||
var list = new List<VideoInfo>
|
||||
{
|
||||
primary
|
||||
};
|
||||
|
||||
list[0].AlternateVersions = videos.Select(x => x.Files[0]).ToArray();
|
||||
list[0].Name = folderName.ToString();
|
||||
|
||||
return list;
|
||||
return [organized];
|
||||
}
|
||||
|
||||
private static bool HaveSameYear(IReadOnlyList<VideoInfo> videos)
|
||||
@@ -195,7 +175,7 @@ namespace Emby.Naming.Video
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool IsEligibleForMultiVersion(ReadOnlySpan<char> folderName, ReadOnlySpan<char> testFilename, NamingOptions namingOptions)
|
||||
private bool IsEligibleForMultiVersion(ReadOnlySpan<char> folderName, ReadOnlySpan<char> testFilename)
|
||||
{
|
||||
if (!testFilename.StartsWith(folderName, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
@@ -209,7 +189,7 @@ namespace Emby.Naming.Video
|
||||
}
|
||||
|
||||
// There are no span overloads for regex unfortunately
|
||||
if (CleanStringParser.TryClean(testFilename.ToString(), namingOptions.CleanStringRegexes, out var cleanName))
|
||||
if (CleanStringParser.TryClean(testFilename.ToString(), _namingOptions.CleanStringRegexes, out var cleanName))
|
||||
{
|
||||
testFilename = cleanName.AsSpan().Trim();
|
||||
}
|
||||
@@ -221,5 +201,117 @@ namespace Emby.Naming.Video
|
||||
|| testFilename[0] == '.'
|
||||
|| CheckMultiVersionRegex().IsMatch(testFilename);
|
||||
}
|
||||
|
||||
private List<VideoInfo> GetEpisodesGroupedByVersion(List<VideoInfo> videos)
|
||||
{
|
||||
if (videos.Count < 2)
|
||||
{
|
||||
return videos;
|
||||
}
|
||||
|
||||
var result = new List<VideoInfo>();
|
||||
var groups = new Dictionary<string, List<VideoInfo>>(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
for (var i = 0; i < videos.Count; i++)
|
||||
{
|
||||
var video = videos[i];
|
||||
if (video.ExtraType is not null)
|
||||
{
|
||||
result.Add(video);
|
||||
continue;
|
||||
}
|
||||
|
||||
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}");
|
||||
}
|
||||
}
|
||||
|
||||
if (key is null)
|
||||
{
|
||||
result.Add(video);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!groups.TryGetValue(key, out var group))
|
||||
{
|
||||
group = [];
|
||||
groups[key] = group;
|
||||
}
|
||||
|
||||
group.Add(video);
|
||||
}
|
||||
|
||||
foreach (var group in groups.Values)
|
||||
{
|
||||
if (group.Count == 1)
|
||||
{
|
||||
result.Add(group[0]);
|
||||
continue;
|
||||
}
|
||||
|
||||
result.Add(OrganizeAlternateVersions(group));
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private static VideoInfo OrganizeAlternateVersions(
|
||||
List<VideoInfo> videos,
|
||||
VideoInfo? primaryOverride = null,
|
||||
string? nameOverride = null)
|
||||
{
|
||||
if (videos.Count > 1)
|
||||
{
|
||||
var groups = videos
|
||||
.Select(x => (filename: x.Files[0].FileNameWithoutExtension.ToString(), value: x))
|
||||
.Select(x => (x.filename, resolutionMatch: ResolutionRegex().Match(x.filename), x.value))
|
||||
.GroupBy(x => x.resolutionMatch.Success)
|
||||
.ToList();
|
||||
|
||||
videos = [];
|
||||
|
||||
StringComparer comparer = StringComparer.Create(CultureInfo.InvariantCulture, CompareOptions.NumericOrdering);
|
||||
foreach (var group in groups)
|
||||
{
|
||||
if (group.Key)
|
||||
{
|
||||
videos.InsertRange(0, group
|
||||
.OrderByDescending(x => x.resolutionMatch.Value, comparer)
|
||||
.ThenBy(x => x.filename, comparer)
|
||||
.Select(x => x.value));
|
||||
}
|
||||
else
|
||||
{
|
||||
videos.AddRange(group.OrderBy(x => x.filename, comparer).Select(x => x.value));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var primary = primaryOverride ?? videos[0];
|
||||
videos.Remove(primary);
|
||||
|
||||
primary.AlternateVersions = [.. videos.Select(x => x.Files[0])];
|
||||
|
||||
if (nameOverride is not null)
|
||||
{
|
||||
primary.Name = nameOverride;
|
||||
}
|
||||
|
||||
return primary;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ using System.Reflection;
|
||||
using System.Security.Cryptography.X509Certificates;
|
||||
using System.Threading.Tasks;
|
||||
using Emby.Naming.Common;
|
||||
using Emby.Naming.Video;
|
||||
using Emby.Photos;
|
||||
using Emby.Server.Implementations.Chapters;
|
||||
using Emby.Server.Implementations.Collections;
|
||||
@@ -530,6 +531,7 @@ namespace Emby.Server.Implementations
|
||||
serviceCollection.AddTransient(provider => new Lazy<IUserViewManager>(provider.GetRequiredService<IUserViewManager>));
|
||||
serviceCollection.AddSingleton<ILibraryManager, LibraryManager>();
|
||||
serviceCollection.AddSingleton<NamingOptions>();
|
||||
serviceCollection.AddSingleton<VideoListResolver>();
|
||||
|
||||
serviceCollection.AddSingleton<IMusicManager, MusicManager>();
|
||||
|
||||
|
||||
@@ -28,15 +28,16 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies
|
||||
public partial class MovieResolver : BaseVideoResolver<Video>, IMultiItemResolver
|
||||
{
|
||||
private readonly IImageProcessor _imageProcessor;
|
||||
private readonly VideoListResolver _videoListResolver;
|
||||
|
||||
private static readonly CollectionType[] _validCollectionTypes = new[]
|
||||
{
|
||||
private static readonly CollectionType[] _validCollectionTypes =
|
||||
[
|
||||
CollectionType.movies,
|
||||
CollectionType.homevideos,
|
||||
CollectionType.musicvideos,
|
||||
CollectionType.tvshows,
|
||||
CollectionType.photos
|
||||
};
|
||||
];
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="MovieResolver"/> class.
|
||||
@@ -45,10 +46,12 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies
|
||||
/// <param name="logger">The logger.</param>
|
||||
/// <param name="namingOptions">The naming options.</param>
|
||||
/// <param name="directoryService">The directory service.</param>
|
||||
public MovieResolver(IImageProcessor imageProcessor, ILogger<MovieResolver> logger, NamingOptions namingOptions, IDirectoryService directoryService)
|
||||
/// <param name="videoListResolver">The video list resolver.</param>
|
||||
public MovieResolver(IImageProcessor imageProcessor, ILogger<MovieResolver> logger, NamingOptions namingOptions, IDirectoryService directoryService, VideoListResolver videoListResolver)
|
||||
: base(logger, namingOptions, directoryService)
|
||||
{
|
||||
_imageProcessor = imageProcessor;
|
||||
_videoListResolver = videoListResolver;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -228,7 +231,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies
|
||||
|
||||
if (collectionType == CollectionType.tvshows)
|
||||
{
|
||||
return ResolveVideos<Episode>(parent, files, false, collectionType, true);
|
||||
return ResolveVideos<Episode>(parent, files, true, collectionType, true);
|
||||
}
|
||||
|
||||
return null;
|
||||
@@ -274,7 +277,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies
|
||||
.Where(f => f is not null)
|
||||
.ToList();
|
||||
|
||||
var resolverResult = VideoListResolver.Resolve(videoInfos, NamingOptions, supportMultiEditions, parseName, parent.ContainingFolderPath);
|
||||
var resolverResult = _videoListResolver.Resolve(videoInfos, supportMultiEditions, parseName, parent.ContainingFolderPath, collectionType);
|
||||
|
||||
var result = new MultiItemResolverResult
|
||||
{
|
||||
|
||||
@@ -3,6 +3,7 @@ using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Emby.Naming.Common;
|
||||
using Emby.Naming.Video;
|
||||
using Jellyfin.Data.Enums;
|
||||
using Xunit;
|
||||
|
||||
namespace Jellyfin.Naming.Tests.Video
|
||||
@@ -10,6 +11,12 @@ namespace Jellyfin.Naming.Tests.Video
|
||||
public class MultiVersionTests
|
||||
{
|
||||
private readonly NamingOptions _namingOptions = new NamingOptions();
|
||||
private readonly VideoListResolver _videoListResolver;
|
||||
|
||||
public MultiVersionTests()
|
||||
{
|
||||
_videoListResolver = new VideoListResolver(_namingOptions);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TestMultiEdition1()
|
||||
@@ -22,9 +29,8 @@ namespace Jellyfin.Naming.Tests.Video
|
||||
"/movies/X-Men Days of Future Past/X-Men Days of Future Past [hsbs].mkv"
|
||||
};
|
||||
|
||||
var result = VideoListResolver.Resolve(
|
||||
files.Select(i => VideoResolver.Resolve(i, false, _namingOptions)).OfType<VideoFileInfo>().ToList(),
|
||||
_namingOptions).ToList();
|
||||
var result = _videoListResolver.Resolve(
|
||||
files.Select(i => VideoResolver.Resolve(i, false, _namingOptions)).OfType<VideoFileInfo>().ToList()).ToList();
|
||||
|
||||
Assert.Single(result, v => v.ExtraType is null);
|
||||
Assert.Single(result, v => v.ExtraType is not null);
|
||||
@@ -41,9 +47,8 @@ namespace Jellyfin.Naming.Tests.Video
|
||||
"/movies/X-Men Days of Future Past/X-Men Days of Future Past [banana].mp4"
|
||||
};
|
||||
|
||||
var result = VideoListResolver.Resolve(
|
||||
files.Select(i => VideoResolver.Resolve(i, false, _namingOptions)).OfType<VideoFileInfo>().ToList(),
|
||||
_namingOptions).ToList();
|
||||
var result = _videoListResolver.Resolve(
|
||||
files.Select(i => VideoResolver.Resolve(i, false, _namingOptions)).OfType<VideoFileInfo>().ToList()).ToList();
|
||||
|
||||
Assert.Single(result, v => v.ExtraType is null);
|
||||
Assert.Single(result, v => v.ExtraType is not null);
|
||||
@@ -59,9 +64,8 @@ namespace Jellyfin.Naming.Tests.Video
|
||||
"/movies/The Phantom of the Opera (1925)/The Phantom of the Opera (1925) - 1929 version.mkv"
|
||||
};
|
||||
|
||||
var result = VideoListResolver.Resolve(
|
||||
files.Select(i => VideoResolver.Resolve(i, false, _namingOptions)).OfType<VideoFileInfo>().ToList(),
|
||||
_namingOptions).ToList();
|
||||
var result = _videoListResolver.Resolve(
|
||||
files.Select(i => VideoResolver.Resolve(i, false, _namingOptions)).OfType<VideoFileInfo>().ToList()).ToList();
|
||||
|
||||
Assert.Single(result);
|
||||
Assert.Single(result[0].AlternateVersions);
|
||||
@@ -81,9 +85,8 @@ namespace Jellyfin.Naming.Tests.Video
|
||||
"/movies/M/Movie 7.mkv"
|
||||
};
|
||||
|
||||
var result = VideoListResolver.Resolve(
|
||||
files.Select(i => VideoResolver.Resolve(i, false, _namingOptions)).OfType<VideoFileInfo>().ToList(),
|
||||
_namingOptions).ToList();
|
||||
var result = _videoListResolver.Resolve(
|
||||
files.Select(i => VideoResolver.Resolve(i, false, _namingOptions)).OfType<VideoFileInfo>().ToList()).ToList();
|
||||
|
||||
Assert.Equal(7, result.Count);
|
||||
Assert.Empty(result[0].AlternateVersions);
|
||||
@@ -104,9 +107,8 @@ namespace Jellyfin.Naming.Tests.Video
|
||||
"/movies/Movie/Movie-8.mkv"
|
||||
};
|
||||
|
||||
var result = VideoListResolver.Resolve(
|
||||
files.Select(i => VideoResolver.Resolve(i, false, _namingOptions)).OfType<VideoFileInfo>().ToList(),
|
||||
_namingOptions).ToList();
|
||||
var result = _videoListResolver.Resolve(
|
||||
files.Select(i => VideoResolver.Resolve(i, false, _namingOptions)).OfType<VideoFileInfo>().ToList()).ToList();
|
||||
|
||||
Assert.Single(result);
|
||||
Assert.Equal(7, result[0].AlternateVersions.Count);
|
||||
@@ -128,9 +130,8 @@ namespace Jellyfin.Naming.Tests.Video
|
||||
"/movies/Mo/Movie 9.mkv"
|
||||
};
|
||||
|
||||
var result = VideoListResolver.Resolve(
|
||||
files.Select(i => VideoResolver.Resolve(i, false, _namingOptions)).OfType<VideoFileInfo>().ToList(),
|
||||
_namingOptions).ToList();
|
||||
var result = _videoListResolver.Resolve(
|
||||
files.Select(i => VideoResolver.Resolve(i, false, _namingOptions)).OfType<VideoFileInfo>().ToList()).ToList();
|
||||
|
||||
Assert.Equal(9, result.Count);
|
||||
Assert.Empty(result[0].AlternateVersions);
|
||||
@@ -148,9 +149,8 @@ namespace Jellyfin.Naming.Tests.Video
|
||||
"/movies/Movie/Movie 5.mkv"
|
||||
};
|
||||
|
||||
var result = VideoListResolver.Resolve(
|
||||
files.Select(i => VideoResolver.Resolve(i, false, _namingOptions)).OfType<VideoFileInfo>().ToList(),
|
||||
_namingOptions).ToList();
|
||||
var result = _videoListResolver.Resolve(
|
||||
files.Select(i => VideoResolver.Resolve(i, false, _namingOptions)).OfType<VideoFileInfo>().ToList()).ToList();
|
||||
|
||||
Assert.Equal(5, result.Count);
|
||||
Assert.Empty(result[0].AlternateVersions);
|
||||
@@ -170,9 +170,8 @@ namespace Jellyfin.Naming.Tests.Video
|
||||
"/movies/Iron Man/Iron Man (2011).mkv"
|
||||
};
|
||||
|
||||
var result = VideoListResolver.Resolve(
|
||||
files.Select(i => VideoResolver.Resolve(i, false, _namingOptions)).OfType<VideoFileInfo>().ToList(),
|
||||
_namingOptions).ToList();
|
||||
var result = _videoListResolver.Resolve(
|
||||
files.Select(i => VideoResolver.Resolve(i, false, _namingOptions)).OfType<VideoFileInfo>().ToList()).ToList();
|
||||
|
||||
Assert.Equal(5, result.Count);
|
||||
Assert.Empty(result[0].AlternateVersions);
|
||||
@@ -192,9 +191,8 @@ namespace Jellyfin.Naming.Tests.Video
|
||||
"/movies/Iron Man/Iron Man[test].mkv"
|
||||
};
|
||||
|
||||
var result = VideoListResolver.Resolve(
|
||||
files.Select(i => VideoResolver.Resolve(i, false, _namingOptions)).OfType<VideoFileInfo>().ToList(),
|
||||
_namingOptions).ToList();
|
||||
var result = _videoListResolver.Resolve(
|
||||
files.Select(i => VideoResolver.Resolve(i, false, _namingOptions)).OfType<VideoFileInfo>().ToList()).ToList();
|
||||
|
||||
Assert.Single(result);
|
||||
Assert.Equal("/movies/Iron Man/Iron Man.mkv", result[0].Files[0].Path);
|
||||
@@ -221,9 +219,8 @@ namespace Jellyfin.Naming.Tests.Video
|
||||
"/movies/Iron Man/Iron Man [test].mkv"
|
||||
};
|
||||
|
||||
var result = VideoListResolver.Resolve(
|
||||
files.Select(i => VideoResolver.Resolve(i, false, _namingOptions)).OfType<VideoFileInfo>().ToList(),
|
||||
_namingOptions).ToList();
|
||||
var result = _videoListResolver.Resolve(
|
||||
files.Select(i => VideoResolver.Resolve(i, false, _namingOptions)).OfType<VideoFileInfo>().ToList()).ToList();
|
||||
|
||||
Assert.Single(result);
|
||||
Assert.Equal("/movies/Iron Man/Iron Man.mkv", result[0].Files[0].Path);
|
||||
@@ -245,9 +242,8 @@ namespace Jellyfin.Naming.Tests.Video
|
||||
"/movies/Iron Man/Iron Man - C (2007).mkv"
|
||||
};
|
||||
|
||||
var result = VideoListResolver.Resolve(
|
||||
files.Select(i => VideoResolver.Resolve(i, false, _namingOptions)).OfType<VideoFileInfo>().ToList(),
|
||||
_namingOptions).ToList();
|
||||
var result = _videoListResolver.Resolve(
|
||||
files.Select(i => VideoResolver.Resolve(i, false, _namingOptions)).OfType<VideoFileInfo>().ToList()).ToList();
|
||||
|
||||
Assert.Equal(2, result.Count);
|
||||
}
|
||||
@@ -266,9 +262,8 @@ namespace Jellyfin.Naming.Tests.Video
|
||||
"/movies/Iron Man/Iron Man_3d.hsbs.mkv"
|
||||
};
|
||||
|
||||
var result = VideoListResolver.Resolve(
|
||||
files.Select(i => VideoResolver.Resolve(i, false, _namingOptions)).OfType<VideoFileInfo>().ToList(),
|
||||
_namingOptions).ToList();
|
||||
var result = _videoListResolver.Resolve(
|
||||
files.Select(i => VideoResolver.Resolve(i, false, _namingOptions)).OfType<VideoFileInfo>().ToList()).ToList();
|
||||
|
||||
Assert.Single(result);
|
||||
Assert.Equal(6, result[0].AlternateVersions.Count);
|
||||
@@ -293,9 +288,8 @@ namespace Jellyfin.Naming.Tests.Video
|
||||
"/movies/Iron Man/Iron Man (2011).mkv"
|
||||
};
|
||||
|
||||
var result = VideoListResolver.Resolve(
|
||||
files.Select(i => VideoResolver.Resolve(i, false, _namingOptions)).OfType<VideoFileInfo>().ToList(),
|
||||
_namingOptions).ToList();
|
||||
var result = _videoListResolver.Resolve(
|
||||
files.Select(i => VideoResolver.Resolve(i, false, _namingOptions)).OfType<VideoFileInfo>().ToList()).ToList();
|
||||
|
||||
Assert.Equal(5, result.Count);
|
||||
Assert.Empty(result[0].AlternateVersions);
|
||||
@@ -310,9 +304,8 @@ namespace Jellyfin.Naming.Tests.Video
|
||||
"/movies/Blade Runner (1982)/Blade Runner (1982) [EE by ADM] [480p HEVC AAC,AAC,AAC].mkv"
|
||||
};
|
||||
|
||||
var result = VideoListResolver.Resolve(
|
||||
files.Select(i => VideoResolver.Resolve(i, false, _namingOptions)).OfType<VideoFileInfo>().ToList(),
|
||||
_namingOptions).ToList();
|
||||
var result = _videoListResolver.Resolve(
|
||||
files.Select(i => VideoResolver.Resolve(i, false, _namingOptions)).OfType<VideoFileInfo>().ToList()).ToList();
|
||||
|
||||
Assert.Single(result);
|
||||
Assert.Single(result[0].AlternateVersions);
|
||||
@@ -327,9 +320,8 @@ namespace Jellyfin.Naming.Tests.Video
|
||||
"/movies/X-Men Apocalypse (2016)/X-Men Apocalypse (2016) [2160p] Blu-ray.x265.AAC.mkv"
|
||||
};
|
||||
|
||||
var result = VideoListResolver.Resolve(
|
||||
files.Select(i => VideoResolver.Resolve(i, false, _namingOptions)).OfType<VideoFileInfo>().ToList(),
|
||||
_namingOptions).ToList();
|
||||
var result = _videoListResolver.Resolve(
|
||||
files.Select(i => VideoResolver.Resolve(i, false, _namingOptions)).OfType<VideoFileInfo>().ToList()).ToList();
|
||||
|
||||
Assert.Single(result);
|
||||
Assert.Single(result[0].AlternateVersions);
|
||||
@@ -348,9 +340,8 @@ namespace Jellyfin.Naming.Tests.Video
|
||||
"/movies/X-Men Apocalypse (2016)/X-Men Apocalypse (2016).mkv",
|
||||
};
|
||||
|
||||
var result = VideoListResolver.Resolve(
|
||||
files.Select(i => VideoResolver.Resolve(i, false, _namingOptions)).OfType<VideoFileInfo>().ToList(),
|
||||
_namingOptions).ToList();
|
||||
var result = _videoListResolver.Resolve(
|
||||
files.Select(i => VideoResolver.Resolve(i, false, _namingOptions)).OfType<VideoFileInfo>().ToList()).ToList();
|
||||
|
||||
Assert.Single(result);
|
||||
Assert.Equal("/movies/X-Men Apocalypse (2016)/X-Men Apocalypse (2016).mkv", result[0].Files[0].Path);
|
||||
@@ -381,9 +372,8 @@ namespace Jellyfin.Naming.Tests.Video
|
||||
"/movies/X-Men Apocalypse (2016)/X-Men Apocalypse (2016).mkv",
|
||||
};
|
||||
|
||||
var result = VideoListResolver.Resolve(
|
||||
files.Select(i => VideoResolver.Resolve(i, false, _namingOptions)).OfType<VideoFileInfo>().ToList(),
|
||||
_namingOptions).ToList();
|
||||
var result = _videoListResolver.Resolve(
|
||||
files.Select(i => VideoResolver.Resolve(i, false, _namingOptions)).OfType<VideoFileInfo>().ToList()).ToList();
|
||||
|
||||
Assert.Single(result);
|
||||
Assert.Equal("/movies/X-Men Apocalypse (2016)/X-Men Apocalypse (2016).mkv", result[0].Files[0].Path);
|
||||
@@ -410,9 +400,8 @@ namespace Jellyfin.Naming.Tests.Video
|
||||
"/movies/John Wick - Kapitel 3 (2019) [imdbid=tt6146586]/John Wick - Kapitel 3 (2019) [imdbid=tt6146586] - Version 2.mkv"
|
||||
};
|
||||
|
||||
var result = VideoListResolver.Resolve(
|
||||
files.Select(i => VideoResolver.Resolve(i, false, _namingOptions)).OfType<VideoFileInfo>().ToList(),
|
||||
_namingOptions).ToList();
|
||||
var result = _videoListResolver.Resolve(
|
||||
files.Select(i => VideoResolver.Resolve(i, false, _namingOptions)).OfType<VideoFileInfo>().ToList()).ToList();
|
||||
|
||||
Assert.Single(result);
|
||||
Assert.Single(result[0].AlternateVersions);
|
||||
@@ -427,9 +416,8 @@ namespace Jellyfin.Naming.Tests.Video
|
||||
"/movies/John Wick - Chapter 3 (2019)/John Wick - Chapter 3 (2019) [Version 2.mkv"
|
||||
};
|
||||
|
||||
var result = VideoListResolver.Resolve(
|
||||
files.Select(i => VideoResolver.Resolve(i, false, _namingOptions)).OfType<VideoFileInfo>().ToList(),
|
||||
_namingOptions).ToList();
|
||||
var result = _videoListResolver.Resolve(
|
||||
files.Select(i => VideoResolver.Resolve(i, false, _namingOptions)).OfType<VideoFileInfo>().ToList()).ToList();
|
||||
|
||||
Assert.Equal(2, result.Count);
|
||||
}
|
||||
@@ -437,7 +425,7 @@ namespace Jellyfin.Naming.Tests.Video
|
||||
[Fact]
|
||||
public void TestEmptyList()
|
||||
{
|
||||
var result = VideoListResolver.Resolve(new List<VideoFileInfo>(), _namingOptions).ToList();
|
||||
var result = _videoListResolver.Resolve(new List<VideoFileInfo>()).ToList();
|
||||
|
||||
Assert.Empty(result);
|
||||
}
|
||||
@@ -451,9 +439,8 @@ namespace Jellyfin.Naming.Tests.Video
|
||||
"/movies/Movie (2020)/Movie (2020)_1080p.mkv"
|
||||
};
|
||||
|
||||
var result = VideoListResolver.Resolve(
|
||||
files.Select(i => VideoResolver.Resolve(i, false, _namingOptions)).OfType<VideoFileInfo>().ToList(),
|
||||
_namingOptions).ToList();
|
||||
var result = _videoListResolver.Resolve(
|
||||
files.Select(i => VideoResolver.Resolve(i, false, _namingOptions)).OfType<VideoFileInfo>().ToList()).ToList();
|
||||
|
||||
Assert.Single(result);
|
||||
Assert.Single(result[0].AlternateVersions);
|
||||
@@ -468,12 +455,572 @@ namespace Jellyfin.Naming.Tests.Video
|
||||
"/movies/Movie (2020)/Movie (2020).1080p.mkv"
|
||||
};
|
||||
|
||||
var result = VideoListResolver.Resolve(
|
||||
files.Select(i => VideoResolver.Resolve(i, false, _namingOptions)).OfType<VideoFileInfo>().ToList(),
|
||||
_namingOptions).ToList();
|
||||
var result = _videoListResolver.Resolve(
|
||||
files.Select(i => VideoResolver.Resolve(i, false, _namingOptions)).OfType<VideoFileInfo>().ToList()).ToList();
|
||||
|
||||
Assert.Single(result);
|
||||
Assert.Single(result[0].AlternateVersions);
|
||||
}
|
||||
|
||||
// Episode multi-version tests
|
||||
|
||||
[Fact]
|
||||
public void TestMultiVersionEpisodeInOwnFolder()
|
||||
{
|
||||
// Two versions of S01E01 in their own subfolder should merge
|
||||
var files = new[]
|
||||
{
|
||||
"/TV/Dexter/Dexter - S01E01/Dexter - S01E01 - 1080p.mkv",
|
||||
"/TV/Dexter/Dexter - S01E01/Dexter - S01E01 - 720p.mkv"
|
||||
};
|
||||
|
||||
var result = _videoListResolver.Resolve(
|
||||
files.Select(i => VideoResolver.Resolve(i, false, _namingOptions)).OfType<VideoFileInfo>().ToList(),
|
||||
collectionType: CollectionType.tvshows).ToList();
|
||||
|
||||
Assert.Single(result);
|
||||
Assert.Single(result[0].AlternateVersions);
|
||||
// 1080p should be primary (higher resolution)
|
||||
Assert.Contains("1080p", result[0].Files[0].Path, StringComparison.Ordinal);
|
||||
Assert.Contains("720p", result[0].AlternateVersions[0].Path, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TestMultiVersionEpisodeMixedSeasonFolder()
|
||||
{
|
||||
// Multiple episodes in season folder, some with versions
|
||||
var files = new[]
|
||||
{
|
||||
"/TV/Dexter/Season 1/Dexter - S01E01 - 1080p.mkv",
|
||||
"/TV/Dexter/Season 1/Dexter - S01E01 - 720p.mkv",
|
||||
"/TV/Dexter/Season 1/Dexter - S01E02.mkv",
|
||||
"/TV/Dexter/Season 1/Dexter - S01E03 - 1080p.mkv",
|
||||
"/TV/Dexter/Season 1/Dexter - S01E03 - 720p.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);
|
||||
|
||||
// S01E01 - should have one alternate version
|
||||
var e01 = result.FirstOrDefault(r => r.Files[0].Path.Contains("S01E01", StringComparison.Ordinal));
|
||||
Assert.NotNull(e01);
|
||||
Assert.Single(e01!.AlternateVersions);
|
||||
Assert.Contains("1080p", e01.Files[0].Path, StringComparison.Ordinal);
|
||||
|
||||
// S01E02 - standalone, no alternates
|
||||
var e02 = result.FirstOrDefault(r => r.Files[0].Path.Contains("S01E02", StringComparison.Ordinal));
|
||||
Assert.NotNull(e02);
|
||||
Assert.Empty(e02!.AlternateVersions);
|
||||
|
||||
// S01E03 - should have one alternate version
|
||||
var e03 = result.FirstOrDefault(r => r.Files[0].Path.Contains("S01E03", StringComparison.Ordinal));
|
||||
Assert.NotNull(e03);
|
||||
Assert.Single(e03!.AlternateVersions);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TestMultiVersionEpisodeDontCollapse()
|
||||
{
|
||||
// Different episodes should NOT collapse into versions
|
||||
var files = new[]
|
||||
{
|
||||
"/TV/Dexter/Season 1/Dexter - S01E01.mkv",
|
||||
"/TV/Dexter/Season 1/Dexter - S01E02.mkv",
|
||||
"/TV/Dexter/Season 1/Dexter - S01E03.mkv",
|
||||
"/TV/Dexter/Season 1/Dexter - S01E04.mkv",
|
||||
"/TV/Dexter/Season 1/Dexter - S01E05.mkv"
|
||||
};
|
||||
|
||||
var result = _videoListResolver.Resolve(
|
||||
files.Select(i => VideoResolver.Resolve(i, false, _namingOptions)).OfType<VideoFileInfo>().ToList(),
|
||||
collectionType: CollectionType.tvshows).ToList();
|
||||
|
||||
Assert.Equal(5, result.Count);
|
||||
Assert.All(result, r => Assert.Empty(r.AlternateVersions));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TestMultiVersionEpisodeWithVersionSuffix()
|
||||
{
|
||||
// Episodes with named versions (like Aired/Uncensored)
|
||||
var files = new[]
|
||||
{
|
||||
"/TV/Show/Season 1/Show - S01E01 - Aired.mkv",
|
||||
"/TV/Show/Season 1/Show - S01E01 - Uncensored.mkv",
|
||||
"/TV/Show/Season 1/Show - S01E02 - Aired.mkv",
|
||||
"/TV/Show/Season 1/Show - S01E02 - Uncensored.mkv"
|
||||
};
|
||||
|
||||
var result = _videoListResolver.Resolve(
|
||||
files.Select(i => VideoResolver.Resolve(i, false, _namingOptions)).OfType<VideoFileInfo>().ToList(),
|
||||
collectionType: CollectionType.tvshows).ToList();
|
||||
|
||||
Assert.Equal(2, result.Count);
|
||||
Assert.All(result, r => Assert.Single(r.AlternateVersions));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TestMultiVersionEpisodeFourVersions()
|
||||
{
|
||||
// Four versions of the same episode
|
||||
var files = new[]
|
||||
{
|
||||
"/TV/Show/Season 1/Show - S01E01 - VersionA.mkv",
|
||||
"/TV/Show/Season 1/Show - S01E01 - VersionB.mkv",
|
||||
"/TV/Show/Season 1/Show - S01E01 - VersionC.mkv",
|
||||
"/TV/Show/Season 1/Show - S01E01 - VersionD.mkv"
|
||||
};
|
||||
|
||||
var result = _videoListResolver.Resolve(
|
||||
files.Select(i => VideoResolver.Resolve(i, false, _namingOptions)).OfType<VideoFileInfo>().ToList(),
|
||||
collectionType: CollectionType.tvshows).ToList();
|
||||
|
||||
Assert.Single(result);
|
||||
Assert.Equal(3, result[0].AlternateVersions.Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TestMultiVersionEpisodeWithResolutions()
|
||||
{
|
||||
// Resolution sorting should work for episodes too
|
||||
var files = new[]
|
||||
{
|
||||
"/TV/Show/Season 1/Show - S01E01 - 720p.mkv",
|
||||
"/TV/Show/Season 1/Show - S01E01 - 2160p.mkv",
|
||||
"/TV/Show/Season 1/Show - S01E01 - 1080p.mkv"
|
||||
};
|
||||
|
||||
var result = _videoListResolver.Resolve(
|
||||
files.Select(i => VideoResolver.Resolve(i, false, _namingOptions)).OfType<VideoFileInfo>().ToList(),
|
||||
collectionType: CollectionType.tvshows).ToList();
|
||||
|
||||
Assert.Single(result);
|
||||
Assert.Equal(2, result[0].AlternateVersions.Count);
|
||||
// Primary should be 2160p (highest resolution)
|
||||
Assert.Contains("2160p", result[0].Files[0].Path, StringComparison.Ordinal);
|
||||
// Next should be 1080p, then 720p
|
||||
Assert.Contains("1080p", result[0].AlternateVersions[0].Path, StringComparison.Ordinal);
|
||||
Assert.Contains("720p", result[0].AlternateVersions[1].Path, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TestMultiVersionEpisodeDifferentSeasons()
|
||||
{
|
||||
// Same episode number but different seasons should NOT group
|
||||
var files = new[]
|
||||
{
|
||||
"/TV/Show/Show - S01E01.mkv",
|
||||
"/TV/Show/Show - S02E01.mkv"
|
||||
};
|
||||
|
||||
var result = _videoListResolver.Resolve(
|
||||
files.Select(i => VideoResolver.Resolve(i, false, _namingOptions)).OfType<VideoFileInfo>().ToList(),
|
||||
collectionType: CollectionType.tvshows).ToList();
|
||||
|
||||
Assert.Equal(2, result.Count);
|
||||
Assert.All(result, r => Assert.Empty(r.AlternateVersions));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TestMultiVersionEpisodeDisabledByDefault()
|
||||
{
|
||||
// Without collectionType: CollectionType.tvshows, episodes should NOT group
|
||||
var files = new[]
|
||||
{
|
||||
"/TV/Show/Season 1/Show - S01E01 - 1080p.mkv",
|
||||
"/TV/Show/Season 1/Show - S01E01 - 720p.mkv"
|
||||
};
|
||||
|
||||
var result = _videoListResolver.Resolve(
|
||||
files.Select(i => VideoResolver.Resolve(i, false, _namingOptions)).OfType<VideoFileInfo>().ToList()).ToList();
|
||||
|
||||
// Without the tvshows collection type, these fall through the movie path
|
||||
// (folder-name eligibility fails) and are treated as separate items.
|
||||
Assert.Equal(2, result.Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TestMultiVersionEpisodeSameNumberDifferentTitle()
|
||||
{
|
||||
// Two files parse to the same S01E01 but carry distinct episode titles.
|
||||
// Current behavior: they are grouped as alternate versions because
|
||||
// grouping keys only on season + episode number, not on episode title.
|
||||
// This documents the trade-off: users with mis-numbered episodes will
|
||||
// see one of the files collapsed into AlternateVersions of the other.
|
||||
var files = new[]
|
||||
{
|
||||
"/TV/Show/Season 1/Show - S01E01 - Pilot.mkv",
|
||||
"/TV/Show/Season 1/Show - S01E01 - Completely Different Title.mkv"
|
||||
};
|
||||
|
||||
var result = _videoListResolver.Resolve(
|
||||
files.Select(i => VideoResolver.Resolve(i, false, _namingOptions)).OfType<VideoFileInfo>().ToList(),
|
||||
collectionType: CollectionType.tvshows).ToList();
|
||||
|
||||
Assert.Single(result);
|
||||
Assert.Single(result[0].AlternateVersions);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TestMultiVersionEpisodeWithTitle()
|
||||
{
|
||||
// Episodes with an episode title AND a version suffix should group
|
||||
var files = new[]
|
||||
{
|
||||
"/TV/Show/Show - S01E01/Show - S01E01 - Episode Title - 1080p.mkv",
|
||||
"/TV/Show/Show - S01E01/Show - S01E01 - Episode Title - 720p.mkv"
|
||||
};
|
||||
|
||||
var result = _videoListResolver.Resolve(
|
||||
files.Select(i => VideoResolver.Resolve(i, false, _namingOptions)).OfType<VideoFileInfo>().ToList(),
|
||||
collectionType: CollectionType.tvshows).ToList();
|
||||
|
||||
Assert.Single(result);
|
||||
Assert.Single(result[0].AlternateVersions);
|
||||
Assert.Contains("1080p", result[0].Files[0].Path, StringComparison.Ordinal);
|
||||
Assert.Contains("720p", result[0].AlternateVersions[0].Path, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TestMultiVersionEpisodeWithTitleMixedFolder()
|
||||
{
|
||||
// Multiple different episodes with titles and resolution variants in a season folder
|
||||
var files = new[]
|
||||
{
|
||||
"/TV/Show/Season 1/Show - S01E01 - Pilot - 1080p.mkv",
|
||||
"/TV/Show/Season 1/Show - S01E01 - Pilot - 720p.mkv",
|
||||
"/TV/Show/Season 1/Show - S01E02 - Second Episode - 1080p.mkv",
|
||||
"/TV/Show/Season 1/Show - S01E02 - Second Episode - 720p.mkv",
|
||||
"/TV/Show/Season 1/Show - S01E03 - Third Episode.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);
|
||||
|
||||
var e01 = result.FirstOrDefault(r => r.Files[0].Path.Contains("S01E01", StringComparison.Ordinal));
|
||||
Assert.NotNull(e01);
|
||||
Assert.Single(e01!.AlternateVersions);
|
||||
|
||||
var e02 = result.FirstOrDefault(r => r.Files[0].Path.Contains("S01E02", StringComparison.Ordinal));
|
||||
Assert.NotNull(e02);
|
||||
Assert.Single(e02!.AlternateVersions);
|
||||
|
||||
var e03 = result.FirstOrDefault(r => r.Files[0].Path.Contains("S01E03", StringComparison.Ordinal));
|
||||
Assert.NotNull(e03);
|
||||
Assert.Empty(e03!.AlternateVersions);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TestMultiVersionEpisodeInSeasonSubfolder()
|
||||
{
|
||||
// Two versions of S01E01 in their own subfolder under a season folder
|
||||
var files = new[]
|
||||
{
|
||||
"/TV/Show/Season 1/Show - S01E01/Show - S01E01 - 1080p.mkv",
|
||||
"/TV/Show/Season 1/Show - S01E01/Show - S01E01 - 720p.mkv"
|
||||
};
|
||||
|
||||
var result = _videoListResolver.Resolve(
|
||||
files.Select(i => VideoResolver.Resolve(i, false, _namingOptions)).OfType<VideoFileInfo>().ToList(),
|
||||
collectionType: CollectionType.tvshows).ToList();
|
||||
|
||||
Assert.Single(result);
|
||||
Assert.Single(result[0].AlternateVersions);
|
||||
Assert.Contains("1080p", result[0].Files[0].Path, StringComparison.Ordinal);
|
||||
Assert.Contains("720p", result[0].AlternateVersions[0].Path, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TestMultiVersionEpisodeWithTitleAndVersionSuffix()
|
||||
{
|
||||
// Episodes with episode title AND a named version suffix
|
||||
var files = new[]
|
||||
{
|
||||
"/TV/Show/Season 1/Show - S01E01 - Pilot - Aired.mkv",
|
||||
"/TV/Show/Season 1/Show - S01E01 - Pilot - Uncensored.mkv",
|
||||
"/TV/Show/Season 1/Show - S01E02 - The Getaway - Aired.mkv",
|
||||
"/TV/Show/Season 1/Show - S01E02 - The Getaway - Uncensored.mkv"
|
||||
};
|
||||
|
||||
var result = _videoListResolver.Resolve(
|
||||
files.Select(i => VideoResolver.Resolve(i, false, _namingOptions)).OfType<VideoFileInfo>().ToList(),
|
||||
collectionType: CollectionType.tvshows).ToList();
|
||||
|
||||
Assert.Equal(2, result.Count);
|
||||
Assert.All(result, r => Assert.Single(r.AlternateVersions));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TestMultiVersionEpisodeWithAdditionalPartsCd()
|
||||
{
|
||||
// Stacked episode (cd1/cd2) with higher resolution alongside a single-file lower-res version
|
||||
var files = new[]
|
||||
{
|
||||
"/TV/Show/Season 1/Show - S01E01 - 1080p cd1.mkv",
|
||||
"/TV/Show/Season 1/Show - S01E01 - 1080p cd2.mkv",
|
||||
"/TV/Show/Season 1/Show - S01E01 - 720p.mkv"
|
||||
};
|
||||
|
||||
var result = _videoListResolver.Resolve(
|
||||
files.Select(i => VideoResolver.Resolve(i, false, _namingOptions)).OfType<VideoFileInfo>().ToList(),
|
||||
collectionType: CollectionType.tvshows).ToList();
|
||||
|
||||
Assert.Single(result);
|
||||
Assert.Equal(2, result[0].Files.Count);
|
||||
Assert.Single(result[0].AlternateVersions);
|
||||
Assert.Contains("720p", result[0].AlternateVersions[0].Path, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TestMultiVersionEpisodeWithAdditionalPartsDashPart()
|
||||
{
|
||||
// Stacked episode using "- part1" / "- part2" separator
|
||||
var files = new[]
|
||||
{
|
||||
"/TV/Show/Season 1/Show - S01E01 - 1080p - part1.mkv",
|
||||
"/TV/Show/Season 1/Show - S01E01 - 1080p - part2.mkv",
|
||||
"/TV/Show/Season 1/Show - S01E01 - 720p.mkv"
|
||||
};
|
||||
|
||||
var result = _videoListResolver.Resolve(
|
||||
files.Select(i => VideoResolver.Resolve(i, false, _namingOptions)).OfType<VideoFileInfo>().ToList(),
|
||||
collectionType: CollectionType.tvshows).ToList();
|
||||
|
||||
Assert.Single(result);
|
||||
Assert.Equal(2, result[0].Files.Count);
|
||||
Assert.Single(result[0].AlternateVersions);
|
||||
Assert.Contains("720p", result[0].AlternateVersions[0].Path, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TestMultiVersionEpisodeWithAdditionalPartsPt()
|
||||
{
|
||||
// Stacked episode using "pt1" / "pt2" short form
|
||||
var files = new[]
|
||||
{
|
||||
"/TV/Show/Season 1/Show - S01E01 - 1080p.pt1.mkv",
|
||||
"/TV/Show/Season 1/Show - S01E01 - 1080p.pt2.mkv",
|
||||
"/TV/Show/Season 1/Show - S01E01 - 720p.mkv"
|
||||
};
|
||||
|
||||
var result = _videoListResolver.Resolve(
|
||||
files.Select(i => VideoResolver.Resolve(i, false, _namingOptions)).OfType<VideoFileInfo>().ToList(),
|
||||
collectionType: CollectionType.tvshows).ToList();
|
||||
|
||||
Assert.Single(result);
|
||||
Assert.Equal(2, result[0].Files.Count);
|
||||
Assert.Single(result[0].AlternateVersions);
|
||||
Assert.Contains("720p", result[0].AlternateVersions[0].Path, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TestMultiVersionEpisodeWithAdditionalPartsAndTitle()
|
||||
{
|
||||
// Stacked episode with episode title in filename
|
||||
var files = new[]
|
||||
{
|
||||
"/TV/Show/Season 1/Show - S01E01 - Pilot - 1080p part1.mkv",
|
||||
"/TV/Show/Season 1/Show - S01E01 - Pilot - 1080p part2.mkv",
|
||||
"/TV/Show/Season 1/Show - S01E01 - Pilot - 720p.mkv"
|
||||
};
|
||||
|
||||
var result = _videoListResolver.Resolve(
|
||||
files.Select(i => VideoResolver.Resolve(i, false, _namingOptions)).OfType<VideoFileInfo>().ToList(),
|
||||
collectionType: CollectionType.tvshows).ToList();
|
||||
|
||||
Assert.Single(result);
|
||||
// Primary should be the stacked 1080p version with 2 files
|
||||
Assert.Equal(2, result[0].Files.Count);
|
||||
Assert.Single(result[0].AlternateVersions);
|
||||
Assert.Contains("720p", result[0].AlternateVersions[0].Path, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TestMultiVersionEpisodeWithAdditionalPartsAndTitleDashSeparator()
|
||||
{
|
||||
// Stacked episode with episode title using "- part1" separator
|
||||
var files = new[]
|
||||
{
|
||||
"/TV/Show/Season 1/Show - S01E01 - Pilot - 1080p - part1.mkv",
|
||||
"/TV/Show/Season 1/Show - S01E01 - Pilot - 1080p - part2.mkv",
|
||||
"/TV/Show/Season 1/Show - S01E01 - Pilot - 720p.mkv"
|
||||
};
|
||||
|
||||
var result = _videoListResolver.Resolve(
|
||||
files.Select(i => VideoResolver.Resolve(i, false, _namingOptions)).OfType<VideoFileInfo>().ToList(),
|
||||
collectionType: CollectionType.tvshows).ToList();
|
||||
|
||||
Assert.Single(result);
|
||||
// Primary should be the stacked 1080p version with 2 files
|
||||
Assert.Equal(2, result[0].Files.Count);
|
||||
Assert.Single(result[0].AlternateVersions);
|
||||
Assert.Contains("720p", result[0].AlternateVersions[0].Path, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TestMultiVersionEpisodeWithAdditionalPartsAndMultipleEpisodes()
|
||||
{
|
||||
// Stacked episode alongside single-file version, plus a different episode
|
||||
var files = new[]
|
||||
{
|
||||
"/TV/Show/Season 1/Show - S01E01 - 1080p cd1.mkv",
|
||||
"/TV/Show/Season 1/Show - S01E01 - 1080p cd2.mkv",
|
||||
"/TV/Show/Season 1/Show - S01E01 - 720p.mkv",
|
||||
"/TV/Show/Season 1/Show - S01E02 - Other.mkv"
|
||||
};
|
||||
|
||||
var result = _videoListResolver.Resolve(
|
||||
files.Select(i => VideoResolver.Resolve(i, false, _namingOptions)).OfType<VideoFileInfo>().ToList(),
|
||||
collectionType: CollectionType.tvshows).ToList();
|
||||
|
||||
Assert.Equal(2, result.Count);
|
||||
|
||||
// S01E01: stacked (cd1+cd2) primary with 720p alternate
|
||||
var e01 = result.FirstOrDefault(r => r.Files[0].Path.Contains("S01E01", StringComparison.Ordinal));
|
||||
Assert.NotNull(e01);
|
||||
Assert.Equal(2, e01!.Files.Count);
|
||||
Assert.Single(e01.AlternateVersions);
|
||||
|
||||
// S01E02: standalone
|
||||
var e02 = result.FirstOrDefault(r => r.Files[0].Path.Contains("S01E02", StringComparison.Ordinal));
|
||||
Assert.NotNull(e02);
|
||||
Assert.Empty(e02!.AlternateVersions);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TestMovieStackingWithPartNaming()
|
||||
{
|
||||
// Movie stacking with "part1"/"part2" naming
|
||||
var files = new[]
|
||||
{
|
||||
"/movies/Movie/Movie part1.mkv",
|
||||
"/movies/Movie/Movie part2.mkv"
|
||||
};
|
||||
|
||||
var result = _videoListResolver.Resolve(
|
||||
files.Select(i => VideoResolver.Resolve(i, false, _namingOptions)).OfType<VideoFileInfo>().ToList()).ToList();
|
||||
|
||||
Assert.Single(result);
|
||||
Assert.Equal(2, result[0].Files.Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TestMovieStackingWithDashPartNaming()
|
||||
{
|
||||
// Movie stacking with "- part1" / "- part2" dash separator
|
||||
var files = new[]
|
||||
{
|
||||
"/movies/Movie/Movie - part1.mkv",
|
||||
"/movies/Movie/Movie - part2.mkv"
|
||||
};
|
||||
|
||||
var result = _videoListResolver.Resolve(
|
||||
files.Select(i => VideoResolver.Resolve(i, false, _namingOptions)).OfType<VideoFileInfo>().ToList()).ToList();
|
||||
|
||||
Assert.Single(result);
|
||||
Assert.Equal(2, result[0].Files.Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TestMovieStackingWithPtNaming()
|
||||
{
|
||||
// Movie stacking with "pt1"/"pt2" short form
|
||||
var files = new[]
|
||||
{
|
||||
"/movies/Movie/Movie.pt1.mkv",
|
||||
"/movies/Movie/Movie.pt2.mkv"
|
||||
};
|
||||
|
||||
var result = _videoListResolver.Resolve(
|
||||
files.Select(i => VideoResolver.Resolve(i, false, _namingOptions)).OfType<VideoFileInfo>().ToList()).ToList();
|
||||
|
||||
Assert.Single(result);
|
||||
Assert.Equal(2, result[0].Files.Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TestMovieStackingWithHyphenNoSpaces()
|
||||
{
|
||||
// Movie stacking with hyphen directly adjacent to "part" (no spaces)
|
||||
var files = new[]
|
||||
{
|
||||
"/movies/Movie/Movie-part1.mkv",
|
||||
"/movies/Movie/Movie-part2.mkv"
|
||||
};
|
||||
|
||||
var result = _videoListResolver.Resolve(
|
||||
files.Select(i => VideoResolver.Resolve(i, false, _namingOptions)).OfType<VideoFileInfo>().ToList()).ToList();
|
||||
|
||||
Assert.Single(result);
|
||||
Assert.Equal(2, result[0].Files.Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TestMovieStackingWithHyphenNoSpacesAndVersion()
|
||||
{
|
||||
// Movie stacking with hyphen-no-space separators plus a version alternate
|
||||
var files = new[]
|
||||
{
|
||||
"/movies/Movie/Movie-1080p-part1.mkv",
|
||||
"/movies/Movie/Movie-1080p-part2.mkv",
|
||||
"/movies/Movie/Movie-720p.mkv"
|
||||
};
|
||||
|
||||
var result = _videoListResolver.Resolve(
|
||||
files.Select(i => VideoResolver.Resolve(i, false, _namingOptions)).OfType<VideoFileInfo>().ToList()).ToList();
|
||||
|
||||
Assert.Single(result);
|
||||
// Stacked 1080p (2 files) should be primary, 720p is alternate
|
||||
Assert.Equal(2, result[0].Files.Count);
|
||||
Assert.Single(result[0].AlternateVersions);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TestEpisodeStackingWithHyphenNoSpaces()
|
||||
{
|
||||
// Episode stacking with hyphen-no-space separators plus version alternate
|
||||
var files = new[]
|
||||
{
|
||||
"/TV/Show/Season 1/Show - S01E01-1080p-cd1.mkv",
|
||||
"/TV/Show/Season 1/Show - S01E01-1080p-cd2.mkv",
|
||||
"/TV/Show/Season 1/Show - S01E01-720p.mkv"
|
||||
};
|
||||
|
||||
var result = _videoListResolver.Resolve(
|
||||
files.Select(i => VideoResolver.Resolve(i, false, _namingOptions)).OfType<VideoFileInfo>().ToList(),
|
||||
collectionType: CollectionType.tvshows).ToList();
|
||||
|
||||
Assert.Single(result);
|
||||
// Stacked 1080p (2 files) should be primary, 720p is alternate
|
||||
Assert.Equal(2, result[0].Files.Count);
|
||||
Assert.Single(result[0].AlternateVersions);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TestEpisodeStackingWithHyphenNoSpacesAndTitle()
|
||||
{
|
||||
// Episode stacking with title and hyphen-no-space separators
|
||||
var files = new[]
|
||||
{
|
||||
"/TV/Show/Season 1/Show - S01E01 - Pilot-1080p-part1.mkv",
|
||||
"/TV/Show/Season 1/Show - S01E01 - Pilot-1080p-part2.mkv",
|
||||
"/TV/Show/Season 1/Show - S01E01 - Pilot-720p.mkv"
|
||||
};
|
||||
|
||||
var result = _videoListResolver.Resolve(
|
||||
files.Select(i => VideoResolver.Resolve(i, false, _namingOptions)).OfType<VideoFileInfo>().ToList(),
|
||||
collectionType: CollectionType.tvshows).ToList();
|
||||
|
||||
Assert.Single(result);
|
||||
// Stacked 1080p (2 files) should be primary, 720p is alternate
|
||||
Assert.Equal(2, result[0].Files.Count);
|
||||
Assert.Single(result[0].AlternateVersions);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,6 +10,12 @@ namespace Jellyfin.Naming.Tests.Video
|
||||
public class VideoListResolverTests
|
||||
{
|
||||
private readonly NamingOptions _namingOptions = new NamingOptions();
|
||||
private readonly VideoListResolver _videoListResolver;
|
||||
|
||||
public VideoListResolverTests()
|
||||
{
|
||||
_videoListResolver = new VideoListResolver(_namingOptions);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TestStackAndExtras()
|
||||
@@ -40,9 +46,8 @@ namespace Jellyfin.Naming.Tests.Video
|
||||
"WillyWonka-trailer.mkv"
|
||||
};
|
||||
|
||||
var result = VideoListResolver.Resolve(
|
||||
files.Select(i => VideoResolver.Resolve(i, false, _namingOptions)).OfType<VideoFileInfo>().ToList(),
|
||||
_namingOptions).ToList();
|
||||
var result = _videoListResolver.Resolve(
|
||||
files.Select(i => VideoResolver.Resolve(i, false, _namingOptions)).OfType<VideoFileInfo>().ToList()).ToList();
|
||||
|
||||
Assert.Equal(11, result.Count);
|
||||
var batman = result.FirstOrDefault(x => string.Equals(x.Name, "Batman", StringComparison.Ordinal));
|
||||
@@ -74,9 +79,8 @@ namespace Jellyfin.Naming.Tests.Video
|
||||
"300.nfo"
|
||||
};
|
||||
|
||||
var result = VideoListResolver.Resolve(
|
||||
files.Select(i => VideoResolver.Resolve(i, false, _namingOptions)).OfType<VideoFileInfo>().ToList(),
|
||||
_namingOptions).ToList();
|
||||
var result = _videoListResolver.Resolve(
|
||||
files.Select(i => VideoResolver.Resolve(i, false, _namingOptions)).OfType<VideoFileInfo>().ToList()).ToList();
|
||||
|
||||
Assert.Single(result);
|
||||
}
|
||||
@@ -90,9 +94,8 @@ namespace Jellyfin.Naming.Tests.Video
|
||||
"300 - trailer.mkv"
|
||||
};
|
||||
|
||||
var result = VideoListResolver.Resolve(
|
||||
files.Select(i => VideoResolver.Resolve(i, false, _namingOptions)).OfType<VideoFileInfo>().ToList(),
|
||||
_namingOptions).ToList();
|
||||
var result = _videoListResolver.Resolve(
|
||||
files.Select(i => VideoResolver.Resolve(i, false, _namingOptions)).OfType<VideoFileInfo>().ToList()).ToList();
|
||||
|
||||
Assert.Equal(2, result.Count);
|
||||
Assert.False(result[0].ExtraType.HasValue);
|
||||
@@ -108,9 +111,8 @@ namespace Jellyfin.Naming.Tests.Video
|
||||
"X-Men Days of Future Past-trailer.mp4"
|
||||
};
|
||||
|
||||
var result = VideoListResolver.Resolve(
|
||||
files.Select(i => VideoResolver.Resolve(i, false, _namingOptions)).OfType<VideoFileInfo>().ToList(),
|
||||
_namingOptions).ToList();
|
||||
var result = _videoListResolver.Resolve(
|
||||
files.Select(i => VideoResolver.Resolve(i, false, _namingOptions)).OfType<VideoFileInfo>().ToList()).ToList();
|
||||
|
||||
Assert.Equal(2, result.Count);
|
||||
Assert.False(result[0].ExtraType.HasValue);
|
||||
@@ -127,9 +129,8 @@ namespace Jellyfin.Naming.Tests.Video
|
||||
"X-Men Days of Future Past-trailer2.mp4"
|
||||
};
|
||||
|
||||
var result = VideoListResolver.Resolve(
|
||||
files.Select(i => VideoResolver.Resolve(i, false, _namingOptions)).OfType<VideoFileInfo>().ToList(),
|
||||
_namingOptions).ToList();
|
||||
var result = _videoListResolver.Resolve(
|
||||
files.Select(i => VideoResolver.Resolve(i, false, _namingOptions)).OfType<VideoFileInfo>().ToList()).ToList();
|
||||
|
||||
Assert.Equal(3, result.Count);
|
||||
Assert.False(result[0].ExtraType.HasValue);
|
||||
@@ -147,9 +148,8 @@ namespace Jellyfin.Naming.Tests.Video
|
||||
"Looper.2012.bluray.720p.x264.mkv"
|
||||
};
|
||||
|
||||
var result = VideoListResolver.Resolve(
|
||||
files.Select(i => VideoResolver.Resolve(i, false, _namingOptions)).OfType<VideoFileInfo>().ToList(),
|
||||
_namingOptions).ToList();
|
||||
var result = _videoListResolver.Resolve(
|
||||
files.Select(i => VideoResolver.Resolve(i, false, _namingOptions)).OfType<VideoFileInfo>().ToList()).ToList();
|
||||
|
||||
Assert.Equal(3, result.Count);
|
||||
Assert.False(result[0].ExtraType.HasValue);
|
||||
@@ -166,9 +166,8 @@ namespace Jellyfin.Naming.Tests.Video
|
||||
"/movies/Looper (2012)/Looper.bluray.720p.x264.mkv"
|
||||
};
|
||||
|
||||
var result = VideoListResolver.Resolve(
|
||||
files.Select(i => VideoResolver.Resolve(i, false, _namingOptions)).OfType<VideoFileInfo>().ToList(),
|
||||
_namingOptions).ToList();
|
||||
var result = _videoListResolver.Resolve(
|
||||
files.Select(i => VideoResolver.Resolve(i, false, _namingOptions)).OfType<VideoFileInfo>().ToList()).ToList();
|
||||
|
||||
Assert.Equal(2, result.Count);
|
||||
Assert.False(result[0].ExtraType.HasValue);
|
||||
@@ -188,9 +187,8 @@ namespace Jellyfin.Naming.Tests.Video
|
||||
"My video 5.mkv"
|
||||
};
|
||||
|
||||
var result = VideoListResolver.Resolve(
|
||||
files.Select(i => VideoResolver.Resolve(i, false, _namingOptions)).OfType<VideoFileInfo>().ToList(),
|
||||
_namingOptions).ToList();
|
||||
var result = _videoListResolver.Resolve(
|
||||
files.Select(i => VideoResolver.Resolve(i, false, _namingOptions)).OfType<VideoFileInfo>().ToList()).ToList();
|
||||
|
||||
Assert.Equal(5, result.Count);
|
||||
}
|
||||
@@ -204,9 +202,8 @@ namespace Jellyfin.Naming.Tests.Video
|
||||
"M:/Movies (DVD)/Movies (Musical)/Sound of Music (1965)/Sound of Music Disc 2"
|
||||
};
|
||||
|
||||
var result = VideoListResolver.Resolve(
|
||||
files.Select(i => VideoResolver.Resolve(i, true, _namingOptions)).OfType<VideoFileInfo>().ToList(),
|
||||
_namingOptions).ToList();
|
||||
var result = _videoListResolver.Resolve(
|
||||
files.Select(i => VideoResolver.Resolve(i, true, _namingOptions)).OfType<VideoFileInfo>().ToList()).ToList();
|
||||
|
||||
Assert.Single(result);
|
||||
}
|
||||
@@ -221,9 +218,8 @@ namespace Jellyfin.Naming.Tests.Video
|
||||
"My movie #2.mp4"
|
||||
};
|
||||
|
||||
var result = VideoListResolver.Resolve(
|
||||
files.Select(i => VideoResolver.Resolve(i, true, _namingOptions)).OfType<VideoFileInfo>().ToList(),
|
||||
_namingOptions).ToList();
|
||||
var result = _videoListResolver.Resolve(
|
||||
files.Select(i => VideoResolver.Resolve(i, true, _namingOptions)).OfType<VideoFileInfo>().ToList()).ToList();
|
||||
|
||||
Assert.Equal(2, result.Count);
|
||||
}
|
||||
@@ -239,9 +235,8 @@ namespace Jellyfin.Naming.Tests.Video
|
||||
"No (2012)-trailer.mp4"
|
||||
};
|
||||
|
||||
var result = VideoListResolver.Resolve(
|
||||
files.Select(i => VideoResolver.Resolve(i, false, _namingOptions)).OfType<VideoFileInfo>().ToList(),
|
||||
_namingOptions).ToList();
|
||||
var result = _videoListResolver.Resolve(
|
||||
files.Select(i => VideoResolver.Resolve(i, false, _namingOptions)).OfType<VideoFileInfo>().ToList()).ToList();
|
||||
|
||||
Assert.Equal(3, result.Count);
|
||||
Assert.False(result[0].ExtraType.HasValue);
|
||||
@@ -260,9 +255,8 @@ namespace Jellyfin.Naming.Tests.Video
|
||||
"/Movies/trailer.mp4"
|
||||
};
|
||||
|
||||
var result = VideoListResolver.Resolve(
|
||||
files.Select(i => VideoResolver.Resolve(i, false, _namingOptions)).OfType<VideoFileInfo>().ToList(),
|
||||
_namingOptions).ToList();
|
||||
var result = _videoListResolver.Resolve(
|
||||
files.Select(i => VideoResolver.Resolve(i, false, _namingOptions)).OfType<VideoFileInfo>().ToList()).ToList();
|
||||
|
||||
Assert.Equal(4, result.Count);
|
||||
Assert.False(result[0].ExtraType.HasValue);
|
||||
@@ -282,9 +276,8 @@ namespace Jellyfin.Naming.Tests.Video
|
||||
"/MCFAMILY-PC/Private3$/Heterosexual/Breast In Class 2 Counterfeit Racks (2011)/Breast In Class 2 Disc 2 cd2.avi"
|
||||
};
|
||||
|
||||
var result = VideoListResolver.Resolve(
|
||||
files.Select(i => VideoResolver.Resolve(i, false, _namingOptions)).OfType<VideoFileInfo>().ToList(),
|
||||
_namingOptions).ToList();
|
||||
var result = _videoListResolver.Resolve(
|
||||
files.Select(i => VideoResolver.Resolve(i, false, _namingOptions)).OfType<VideoFileInfo>().ToList()).ToList();
|
||||
|
||||
Assert.Equal(2, result.Count);
|
||||
}
|
||||
@@ -297,9 +290,8 @@ namespace Jellyfin.Naming.Tests.Video
|
||||
"/nas-markrobbo78/Videos/INDEX HTPC/Movies/Watched/3 - ACTION/Argo (2012)/movie.mkv"
|
||||
};
|
||||
|
||||
var result = VideoListResolver.Resolve(
|
||||
files.Select(i => VideoResolver.Resolve(i, false, _namingOptions)).OfType<VideoFileInfo>().ToList(),
|
||||
_namingOptions).ToList();
|
||||
var result = _videoListResolver.Resolve(
|
||||
files.Select(i => VideoResolver.Resolve(i, false, _namingOptions)).OfType<VideoFileInfo>().ToList()).ToList();
|
||||
|
||||
Assert.Single(result);
|
||||
}
|
||||
@@ -312,9 +304,8 @@ namespace Jellyfin.Naming.Tests.Video
|
||||
"The Colony.mkv"
|
||||
};
|
||||
|
||||
var result = VideoListResolver.Resolve(
|
||||
files.Select(i => VideoResolver.Resolve(i, false, _namingOptions)).OfType<VideoFileInfo>().ToList(),
|
||||
_namingOptions).ToList();
|
||||
var result = _videoListResolver.Resolve(
|
||||
files.Select(i => VideoResolver.Resolve(i, false, _namingOptions)).OfType<VideoFileInfo>().ToList()).ToList();
|
||||
|
||||
Assert.Single(result);
|
||||
}
|
||||
@@ -328,9 +319,8 @@ namespace Jellyfin.Naming.Tests.Video
|
||||
"Four Sisters and a Wedding - B.avi"
|
||||
};
|
||||
|
||||
var result = VideoListResolver.Resolve(
|
||||
files.Select(i => VideoResolver.Resolve(i, false, _namingOptions)).OfType<VideoFileInfo>().ToList(),
|
||||
_namingOptions).ToList();
|
||||
var result = _videoListResolver.Resolve(
|
||||
files.Select(i => VideoResolver.Resolve(i, false, _namingOptions)).OfType<VideoFileInfo>().ToList()).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'
|
||||
@@ -346,9 +336,8 @@ namespace Jellyfin.Naming.Tests.Video
|
||||
"Four Rooms - A.mp4"
|
||||
};
|
||||
|
||||
var result = VideoListResolver.Resolve(
|
||||
files.Select(i => VideoResolver.Resolve(i, false, _namingOptions)).OfType<VideoFileInfo>().ToList(),
|
||||
_namingOptions).ToList();
|
||||
var result = _videoListResolver.Resolve(
|
||||
files.Select(i => VideoResolver.Resolve(i, false, _namingOptions)).OfType<VideoFileInfo>().ToList()).ToList();
|
||||
|
||||
Assert.Equal(2, result.Count);
|
||||
}
|
||||
@@ -362,9 +351,8 @@ namespace Jellyfin.Naming.Tests.Video
|
||||
"/Server/Despicable Me/trailer.mkv"
|
||||
};
|
||||
|
||||
var result = VideoListResolver.Resolve(
|
||||
files.Select(i => VideoResolver.Resolve(i, false, _namingOptions)).OfType<VideoFileInfo>().ToList(),
|
||||
_namingOptions).ToList();
|
||||
var result = _videoListResolver.Resolve(
|
||||
files.Select(i => VideoResolver.Resolve(i, false, _namingOptions)).OfType<VideoFileInfo>().ToList()).ToList();
|
||||
|
||||
Assert.Equal(2, result.Count);
|
||||
Assert.False(result[0].ExtraType.HasValue);
|
||||
@@ -380,9 +368,8 @@ namespace Jellyfin.Naming.Tests.Video
|
||||
"/Server/Despicable Me/trailers/some title.mkv"
|
||||
};
|
||||
|
||||
var result = VideoListResolver.Resolve(
|
||||
files.Select(i => VideoResolver.Resolve(i, false, _namingOptions)).OfType<VideoFileInfo>().ToList(),
|
||||
_namingOptions).ToList();
|
||||
var result = _videoListResolver.Resolve(
|
||||
files.Select(i => VideoResolver.Resolve(i, false, _namingOptions)).OfType<VideoFileInfo>().ToList()).ToList();
|
||||
|
||||
Assert.Equal(2, result.Count);
|
||||
Assert.False(result[0].ExtraType.HasValue);
|
||||
@@ -398,9 +385,8 @@ namespace Jellyfin.Naming.Tests.Video
|
||||
"/Movies/Despicable Me/trailers/trailer.mkv"
|
||||
};
|
||||
|
||||
var result = VideoListResolver.Resolve(
|
||||
files.Select(i => VideoResolver.Resolve(i, false, _namingOptions)).OfType<VideoFileInfo>().ToList(),
|
||||
_namingOptions).ToList();
|
||||
var result = _videoListResolver.Resolve(
|
||||
files.Select(i => VideoResolver.Resolve(i, false, _namingOptions)).OfType<VideoFileInfo>().ToList()).ToList();
|
||||
|
||||
Assert.Equal(2, result.Count);
|
||||
Assert.False(result[0].ExtraType.HasValue);
|
||||
|
||||
@@ -1,7 +1,13 @@
|
||||
using System.Collections.Generic;
|
||||
using Emby.Naming.Common;
|
||||
using Emby.Naming.Video;
|
||||
using Emby.Server.Implementations.Library.Resolvers.Movies;
|
||||
using Jellyfin.Data.Enums;
|
||||
using MediaBrowser.Controller;
|
||||
using MediaBrowser.Controller.Drawing;
|
||||
using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Controller.Entities.Movies;
|
||||
using MediaBrowser.Controller.Entities.TV;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using MediaBrowser.Controller.Providers;
|
||||
using MediaBrowser.Model.IO;
|
||||
@@ -14,11 +20,12 @@ namespace Jellyfin.Server.Implementations.Tests.Library;
|
||||
public class MovieResolverTests
|
||||
{
|
||||
private static readonly NamingOptions _namingOptions = new();
|
||||
private static readonly VideoListResolver _videoListResolver = new(_namingOptions);
|
||||
|
||||
[Fact]
|
||||
public void Resolve_GivenLocalAlternateVersion_ResolvesToVideo()
|
||||
{
|
||||
var movieResolver = new MovieResolver(Mock.Of<IImageProcessor>(), Mock.Of<ILogger<MovieResolver>>(), _namingOptions, Mock.Of<IDirectoryService>());
|
||||
var movieResolver = new MovieResolver(Mock.Of<IImageProcessor>(), Mock.Of<ILogger<MovieResolver>>(), _namingOptions, Mock.Of<IDirectoryService>(), _videoListResolver);
|
||||
var itemResolveArgs = new ItemResolveArgs(
|
||||
Mock.Of<IServerApplicationPaths>(),
|
||||
null)
|
||||
@@ -32,4 +39,54 @@ public class MovieResolverTests
|
||||
|
||||
Assert.NotNull(movieResolver.Resolve(itemResolveArgs));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ResolveMultiple_GivenTvShowsCollection_CreatesEpisodeItems()
|
||||
{
|
||||
// For a tvshows collection, the multi-version grouping must still produce
|
||||
// Episode BaseItems (not generic Video) so downstream metadata fetching
|
||||
// and series-aware logic apply.
|
||||
var movieResolver = new MovieResolver(Mock.Of<IImageProcessor>(), Mock.Of<ILogger<MovieResolver>>(), _namingOptions, Mock.Of<IDirectoryService>(), _videoListResolver);
|
||||
|
||||
var parent = new Folder { Path = "/TV/Show/Season 1" };
|
||||
var files = new List<FileSystemMetadata>
|
||||
{
|
||||
new() { FullName = "/TV/Show/Season 1/Show - S01E01 - 1080p.mkv", Name = "Show - S01E01 - 1080p.mkv", IsDirectory = false },
|
||||
new() { FullName = "/TV/Show/Season 1/Show - S01E01 - 720p.mkv", Name = "Show - S01E01 - 720p.mkv", IsDirectory = false },
|
||||
new() { FullName = "/TV/Show/Season 1/Show - S01E02.mkv", Name = "Show - S01E02.mkv", IsDirectory = false }
|
||||
};
|
||||
|
||||
var result = movieResolver.ResolveMultiple(parent, files, CollectionType.tvshows, Mock.Of<IDirectoryService>());
|
||||
|
||||
Assert.NotNull(result);
|
||||
Assert.Equal(2, result.Items.Count);
|
||||
Assert.All(result.Items, item => Assert.IsType<Episode>(item));
|
||||
|
||||
// The S01E01 item should have one alternate version
|
||||
var s01e01 = result.Items.Find(i => i.Path.Contains("S01E01", System.StringComparison.Ordinal));
|
||||
Assert.NotNull(s01e01);
|
||||
Assert.Single(((Video)s01e01).LocalAlternateVersions);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ResolveMultiple_GivenMoviesCollection_CreatesMovieItems()
|
||||
{
|
||||
// For a movies collection, the multi-version grouping must produce Movie
|
||||
// BaseItems (not generic Video) so downstream movie-specific logic applies.
|
||||
var movieResolver = new MovieResolver(Mock.Of<IImageProcessor>(), Mock.Of<ILogger<MovieResolver>>(), _namingOptions, Mock.Of<IDirectoryService>(), _videoListResolver);
|
||||
|
||||
var parent = new Folder { Path = "/movies/Inception (2010)" };
|
||||
var files = new List<FileSystemMetadata>
|
||||
{
|
||||
new() { FullName = "/movies/Inception (2010)/Inception (2010) - 1080p.mkv", Name = "Inception (2010) - 1080p.mkv", IsDirectory = false },
|
||||
new() { FullName = "/movies/Inception (2010)/Inception (2010) - 720p.mkv", Name = "Inception (2010) - 720p.mkv", IsDirectory = false }
|
||||
};
|
||||
|
||||
var result = movieResolver.ResolveMultiple(parent, files, CollectionType.movies, Mock.Of<IDirectoryService>());
|
||||
|
||||
Assert.NotNull(result);
|
||||
Assert.Single(result.Items);
|
||||
Assert.All(result.Items, item => Assert.IsType<Movie>(item));
|
||||
Assert.Single(((Video)result.Items[0]).LocalAlternateVersions);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user