jellyfin/Emby.Naming/Video/VideoListResolver.cs

212 lines
7.4 KiB
C#
Raw Normal View History

2018-09-12 13:26:21 -04:00
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text.RegularExpressions;
2019-01-13 14:17:29 -05:00
using Emby.Naming.Common;
2023-02-20 10:07:51 -05:00
using Jellyfin.Extensions;
2019-01-13 14:17:29 -05:00
using MediaBrowser.Model.IO;
2018-09-12 13:26:21 -04:00
namespace Emby.Naming.Video
{
2020-11-10 13:23:10 -05:00
/// <summary>
/// Resolves alternative versions and extras from list of video files.
/// </summary>
2023-05-22 16:48:09 -04:00
public static partial class VideoListResolver
2018-09-12 13:26:21 -04:00
{
2023-05-22 16:48:09 -04:00
[GeneratedRegex("[0-9]{2}[0-9]+[ip]", RegexOptions.IgnoreCase)]
private static partial Regex ResolutionRegex();
[GeneratedRegex(@"^\[([^]]*)\]")]
private static partial Regex CheckMultiVersionRegex();
2023-02-20 10:07:51 -05:00
2020-11-10 13:23:10 -05:00
/// <summary>
/// Resolves alternative versions and extras from list of video files.
/// </summary>
2021-12-20 06:34:16 -05:00
/// <param name="videoInfos">List of related video files.</param>
2021-05-23 18:30:41 -04:00
/// <param name="namingOptions">The naming options.</param>
2020-11-10 13:23:10 -05:00
/// <param name="supportMultiVersion">Indication we should consider multi-versions of content.</param>
2021-12-07 09:18:17 -05:00
/// <param name="parseName">Whether to parse the name or use the filename.</param>
2020-11-18 08:23:45 -05:00
/// <returns>Returns enumerable of <see cref="VideoInfo"/> which groups files together when related.</returns>
2021-12-20 06:15:20 -05:00
public static IReadOnlyList<VideoInfo> Resolve(IReadOnlyList<VideoFileInfo> videoInfos, NamingOptions namingOptions, bool supportMultiVersion = true, bool parseName = true)
2018-09-12 13:26:21 -04:00
{
// Filter out all extras, otherwise they could cause stacks to not be resolved
// See the unit test TestStackedWithTrailer
var nonExtras = videoInfos
2022-12-05 09:00:20 -05:00
.Where(i => i.ExtraType is null)
2020-03-25 12:53:03 -04:00
.Select(i => new FileSystemMetadata { FullName = i.Path, IsDirectory = i.IsDirectory });
2018-09-12 13:26:21 -04:00
2021-12-07 09:18:17 -05:00
var stackResult = StackResolver.Resolve(nonExtras, namingOptions).ToList();
2018-09-12 13:26:21 -04:00
2021-12-07 09:18:17 -05:00
var remainingFiles = new List<VideoFileInfo>();
var standaloneMedia = new List<VideoFileInfo>();
for (var i = 0; i < videoInfos.Count; i++)
{
var current = videoInfos[i];
if (stackResult.Any(s => s.ContainsFile(current.Path, current.IsDirectory)))
{
continue;
}
2022-12-05 09:00:20 -05:00
if (current.ExtraType is null)
2021-12-07 09:18:17 -05:00
{
standaloneMedia.Add(current);
}
2021-12-27 18:37:40 -05:00
else
{
remainingFiles.Add(current);
}
2021-12-07 09:18:17 -05:00
}
2018-09-12 13:26:21 -04:00
var list = new List<VideoInfo>();
2020-01-22 16:18:56 -05:00
foreach (var stack in stackResult)
2018-09-12 13:26:21 -04:00
{
2020-01-22 16:18:56 -05:00
var info = new VideoInfo(stack.Name)
2018-09-12 13:26:21 -04:00
{
2021-12-07 09:18:17 -05:00
Files = stack.Files.Select(i => VideoResolver.Resolve(i, stack.IsDirectoryStack, namingOptions, parseName))
2020-11-01 04:47:31 -05:00
.OfType<VideoFileInfo>()
.ToList()
2018-09-12 13:26:21 -04:00
};
2019-05-10 14:37:42 -04:00
info.Year = info.Files[0].Year;
2018-09-12 13:26:21 -04:00
list.Add(info);
}
foreach (var media in standaloneMedia)
{
2021-05-23 18:30:41 -04:00
var info = new VideoInfo(media.Name) { Files = new[] { media } };
2018-09-12 13:26:21 -04:00
2019-05-10 14:37:42 -04:00
info.Year = info.Files[0].Year;
2018-09-12 13:26:21 -04:00
list.Add(info);
}
if (supportMultiVersion)
{
2021-05-23 18:30:41 -04:00
list = GetVideosGroupedByVersion(list, namingOptions);
2018-09-12 13:26:21 -04:00
}
// Whatever files are left, just add them
2020-01-22 16:18:56 -05:00
list.AddRange(remainingFiles.Select(i => new VideoInfo(i.Name)
2018-09-12 13:26:21 -04:00
{
2021-05-23 18:30:41 -04:00
Files = new[] { i },
2021-12-07 09:18:17 -05:00
Year = i.Year,
ExtraType = i.ExtraType
2018-09-12 13:26:21 -04:00
}));
2020-01-22 16:18:56 -05:00
return list;
2018-09-12 13:26:21 -04:00
}
2021-05-23 18:30:41 -04:00
private static List<VideoInfo> GetVideosGroupedByVersion(List<VideoInfo> videos, NamingOptions namingOptions)
2018-09-12 13:26:21 -04:00
{
if (videos.Count == 0)
{
return videos;
}
2021-05-23 18:30:41 -04:00
var folderName = Path.GetFileName(Path.GetDirectoryName(videos[0].Files[0].Path.AsSpan()));
2021-05-23 18:30:41 -04:00
if (folderName.Length <= 1 || !HaveSameYear(videos))
2018-09-12 13:26:21 -04:00
{
2021-05-23 18:30:41 -04:00
return videos;
}
2021-05-23 18:30:41 -04:00
// Cannot use Span inside local functions and delegates thus we cannot use LINQ here nor merge with the above [if]
2023-02-15 12:05:49 -05:00
VideoInfo? primary = null;
2021-05-23 18:30:41 -04:00
for (var i = 0; i < videos.Count; i++)
{
var video = videos[i];
2022-12-05 09:01:13 -05:00
if (video.ExtraType is not null)
2021-12-07 09:18:17 -05:00
{
continue;
}
2023-02-20 10:07:51 -05:00
if (!IsEligibleForMultiVersion(folderName, video.Files[0].FileNameWithoutExtension, namingOptions))
2020-01-22 16:18:56 -05:00
{
2021-05-23 18:30:41 -04:00
return videos;
2020-01-22 16:18:56 -05:00
}
2023-02-15 12:05:49 -05:00
2023-02-20 10:07:51 -05:00
if (folderName.Equals(video.Files[0].FileNameWithoutExtension, StringComparison.Ordinal))
2023-02-15 12:05:49 -05:00
{
primary = video;
}
2021-05-23 18:30:41 -04:00
}
2023-02-20 10:07:51 -05:00
if (videos.Count > 1)
{
2023-05-22 16:48:09 -04:00
var groups = videos.GroupBy(x => ResolutionRegex().IsMatch(x.Files[0].FileNameWithoutExtension)).ToList();
2023-02-20 10:07:51 -05:00
videos.Clear();
foreach (var group in groups)
{
if (group.Key)
{
videos.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()));
}
}
}
2023-02-15 12:05:49 -05:00
primary ??= videos[0];
videos.Remove(primary);
2020-01-22 16:18:56 -05:00
2021-05-23 18:30:41 -04:00
var list = new List<VideoInfo>
{
2023-02-15 12:05:49 -05:00
primary
2021-05-23 18:30:41 -04:00
};
2023-02-15 12:05:49 -05:00
list[0].AlternateVersions = videos.Select(x => x.Files[0]).ToArray();
2021-05-23 18:30:41 -04:00
list[0].Name = folderName.ToString();
2021-05-23 18:30:41 -04:00
return list;
}
2021-05-23 18:30:41 -04:00
private static bool HaveSameYear(IReadOnlyList<VideoInfo> videos)
{
2021-05-23 18:30:41 -04:00
if (videos.Count == 1)
{
2021-05-23 18:30:41 -04:00
return true;
}
2020-11-22 16:39:34 -05:00
2021-05-23 18:30:41 -04:00
var firstYear = videos[0].Year ?? -1;
for (var i = 1; i < videos.Count; i++)
{
if ((videos[i].Year ?? -1) != firstYear)
{
2021-05-23 18:30:41 -04:00
return false;
}
2021-05-23 18:30:41 -04:00
}
2021-05-23 18:30:41 -04:00
return true;
}
2023-02-20 10:07:51 -05:00
private static bool IsEligibleForMultiVersion(ReadOnlySpan<char> folderName, ReadOnlySpan<char> testFilename, NamingOptions namingOptions)
2021-05-23 18:30:41 -04:00
{
if (!testFilename.StartsWith(folderName, StringComparison.OrdinalIgnoreCase))
{
return false;
}
2021-05-23 18:30:41 -04:00
// Remove the folder name before cleaning as we don't care about cleaning that part
if (folderName.Length <= testFilename.Length)
{
testFilename = testFilename[folderName.Length..].Trim();
}
// There are no span overloads for regex unfortunately
2023-02-17 09:00:06 -05:00
if (CleanStringParser.TryClean(testFilename.ToString(), namingOptions.CleanStringRegexes, out var cleanName))
2021-05-23 18:30:41 -04:00
{
2023-02-17 09:00:06 -05:00
testFilename = cleanName.AsSpan().Trim();
2021-05-23 18:30:41 -04:00
}
// The CleanStringParser should have removed common keywords etc.
2023-02-17 09:00:06 -05:00
return testFilename.IsEmpty
2021-05-23 18:30:41 -04:00
|| testFilename[0] == '-'
2023-05-22 16:48:09 -04:00
|| CheckMultiVersionRegex().IsMatch(testFilename);
2021-05-23 18:30:41 -04:00
}
2018-09-12 13:26:21 -04:00
}
}