aboutsummaryrefslogtreecommitdiff
path: root/Emby.Naming/Video/VideoListResolver.cs
diff options
context:
space:
mode:
Diffstat (limited to 'Emby.Naming/Video/VideoListResolver.cs')
-rw-r--r--Emby.Naming/Video/VideoListResolver.cs188
1 files changed, 139 insertions, 49 deletions
diff --git a/Emby.Naming/Video/VideoListResolver.cs b/Emby.Naming/Video/VideoListResolver.cs
index a4bfb8d4a1..29330b132d 100644
--- a/Emby.Naming/Video/VideoListResolver.cs
+++ b/Emby.Naming/Video/VideoListResolver.cs
@@ -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,23 @@ 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 static readonly StringComparer _numericOrdinalComparer = StringComparer.Create(CultureInfo.InvariantCulture, CompareOptions.NumericOrdering);
+
+ 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 +41,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 +54,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 +83,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 +102,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 +118,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 +142,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 +153,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();
-
- videos.Clear();
+ var organized = OrganizeAlternateVersions(videos, primary, folderName.ToString());
- 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 +177,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 +191,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 +203,113 @@ 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];
+ 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 = [];
+
+ foreach (var group in groups)
+ {
+ if (group.Key)
+ {
+ videos.InsertRange(0, group
+ .OrderByDescending(x => x.resolutionMatch.Value, _numericOrdinalComparer)
+ .ThenBy(x => x.filename, _numericOrdinalComparer)
+ .Select(x => x.value));
+ }
+ else
+ {
+ videos.AddRange(group.OrderBy(x => x.filename, _numericOrdinalComparer).Select(x => x.value));
+ }
+ }
+ }
+
+ // Prefer a stacked entry (more than one part) as primary
+ var primary = primaryOverride
+ ?? videos.FirstOrDefault(v => v.Files.Count > 1)
+ ?? videos[0];
+ videos.Remove(primary);
+
+ primary.AlternateVersions = videos;
+
+ if (nameOverride is not null)
+ {
+ primary.Name = nameOverride;
+ }
+
+ return primary;
+ }
}
}