jellyfin/Emby.Naming/Video/VideoListResolver.cs

260 lines
9.3 KiB
C#
Raw Normal View History

2018-09-12 17:26:21 +00:00
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text.RegularExpressions;
2019-01-13 19:17:29 +00:00
using Emby.Naming.Common;
2019-12-06 19:40:06 +00:00
using MediaBrowser.Model.Entities;
2019-01-13 19:17:29 +00:00
using MediaBrowser.Model.IO;
2018-09-12 17:26:21 +00:00
namespace Emby.Naming.Video
{
2020-11-10 18:23:10 +00:00
/// <summary>
/// Resolves alternative versions and extras from list of video files.
/// </summary>
2018-09-12 17:26:21 +00:00
public class VideoListResolver
{
private readonly NamingOptions _options;
2020-11-10 18:23:10 +00:00
/// <summary>
/// Initializes a new instance of the <see cref="VideoListResolver"/> class.
/// </summary>
/// <param name="options"><see cref="NamingOptions"/> object containing CleanStringRegexes and VideoFlagDelimiters and passes options to <see cref="StackResolver"/> and <see cref="VideoResolver"/>.</param>
2018-09-12 17:26:21 +00:00
public VideoListResolver(NamingOptions options)
{
_options = options;
}
2020-11-10 18:23:10 +00:00
/// <summary>
/// Resolves alternative versions and extras from list of video files.
/// </summary>
/// <param name="files">List of related video files.</param>
/// <param name="supportMultiVersion">Indication we should consider multi-versions of content.</param>
2020-11-18 13:23:45 +00:00
/// <returns>Returns enumerable of <see cref="VideoInfo"/> which groups files together when related.</returns>
2018-09-12 17:26:21 +00:00
public IEnumerable<VideoInfo> Resolve(List<FileSystemMetadata> files, bool supportMultiVersion = true)
{
var videoResolver = new VideoResolver(_options);
var videoInfos = files
.Select(i => videoResolver.Resolve(i.FullName, i.IsDirectory))
2020-11-01 09:47:31 +00:00
.OfType<VideoFileInfo>()
2018-09-12 17:26:21 +00:00
.ToList();
// Filter out all extras, otherwise they could cause stacks to not be resolved
// See the unit test TestStackedWithTrailer
var nonExtras = videoInfos
2019-12-06 19:40:06 +00:00
.Where(i => i.ExtraType == null)
2020-03-25 16:53:03 +00:00
.Select(i => new FileSystemMetadata { FullName = i.Path, IsDirectory = i.IsDirectory });
2018-09-12 17:26:21 +00:00
var stackResult = new StackResolver(_options)
2020-01-22 21:18:56 +00:00
.Resolve(nonExtras).ToList();
2018-09-12 17:26:21 +00:00
var remainingFiles = videoInfos
2020-11-01 09:47:31 +00:00
.Where(i => !stackResult.Any(s => i.Path != null && s.ContainsFile(i.Path, i.IsDirectory)))
2018-09-12 17:26:21 +00:00
.ToList();
var list = new List<VideoInfo>();
2020-01-22 21:18:56 +00:00
foreach (var stack in stackResult)
2018-09-12 17:26:21 +00:00
{
2020-01-22 21:18:56 +00:00
var info = new VideoInfo(stack.Name)
2018-09-12 17:26:21 +00:00
{
2020-11-01 09:47:31 +00:00
Files = stack.Files.Select(i => videoResolver.Resolve(i, stack.IsDirectoryStack))
.OfType<VideoFileInfo>()
.ToList()
2018-09-12 17:26:21 +00:00
};
2019-05-10 18:37:42 +00:00
info.Year = info.Files[0].Year;
2018-09-12 17:26:21 +00:00
2020-03-25 16:53:03 +00:00
var extraBaseNames = new List<string> { stack.Name, Path.GetFileNameWithoutExtension(stack.Files[0]) };
2018-09-12 17:26:21 +00:00
var extras = GetExtras(remainingFiles, extraBaseNames);
if (extras.Count > 0)
{
remainingFiles = remainingFiles
.Except(extras)
.ToList();
info.Extras = extras;
}
list.Add(info);
}
var standaloneMedia = remainingFiles
2019-12-06 19:40:06 +00:00
.Where(i => i.ExtraType == null)
2018-09-12 17:26:21 +00:00
.ToList();
foreach (var media in standaloneMedia)
{
2020-03-25 16:53:03 +00:00
var info = new VideoInfo(media.Name) { Files = new List<VideoFileInfo> { media } };
2018-09-12 17:26:21 +00:00
2019-05-10 18:37:42 +00:00
info.Year = info.Files[0].Year;
2018-09-12 17:26:21 +00:00
var extras = GetExtras(remainingFiles, new List<string> { media.FileNameWithoutExtension });
remainingFiles = remainingFiles
.Except(extras.Concat(new[] { media }))
.ToList();
info.Extras = extras;
list.Add(info);
}
if (supportMultiVersion)
{
list = GetVideosGroupedByVersion(list)
.ToList();
}
// If there's only one resolved video, use the folder name as well to find extras
if (list.Count == 1)
{
var info = list[0];
var videoPath = list[0].Files[0].Path;
var parentPath = Path.GetDirectoryName(videoPath);
if (!string.IsNullOrEmpty(parentPath))
{
2019-05-10 18:37:42 +00:00
var folderName = Path.GetFileName(parentPath);
2018-09-12 17:26:21 +00:00
if (!string.IsNullOrEmpty(folderName))
{
var extras = GetExtras(remainingFiles, new List<string> { folderName });
remainingFiles = remainingFiles
.Except(extras)
.ToList();
2020-01-22 21:18:56 +00:00
extras.AddRange(info.Extras);
info.Extras = extras;
2018-09-12 17:26:21 +00:00
}
}
// Add the extras that are just based on file name as well
var extrasByFileName = remainingFiles
.Where(i => i.ExtraRule != null && i.ExtraRule.RuleType == ExtraRuleType.Filename)
.ToList();
remainingFiles = remainingFiles
.Except(extrasByFileName)
.ToList();
2020-01-22 21:18:56 +00:00
extrasByFileName.AddRange(info.Extras);
info.Extras = extrasByFileName;
2018-09-12 17:26:21 +00:00
}
// If there's only one video, accept all trailers
// Be lenient because people use all kinds of mishmash conventions with trailers.
2018-09-12 17:26:21 +00:00
if (list.Count == 1)
{
var trailers = remainingFiles
2019-12-06 19:40:06 +00:00
.Where(i => i.ExtraType == ExtraType.Trailer)
2018-09-12 17:26:21 +00:00
.ToList();
2020-01-22 21:18:56 +00:00
trailers.AddRange(list[0].Extras);
list[0].Extras = trailers;
2018-09-12 17:26:21 +00:00
remainingFiles = remainingFiles
.Except(trailers)
.ToList();
}
// Whatever files are left, just add them
2020-01-22 21:18:56 +00:00
list.AddRange(remainingFiles.Select(i => new VideoInfo(i.Name)
2018-09-12 17:26:21 +00:00
{
2020-03-25 20:33:44 +00:00
Files = new List<VideoFileInfo> { i },
Year = i.Year
2018-09-12 17:26:21 +00:00
}));
2020-01-22 21:18:56 +00:00
return list;
2018-09-12 17:26:21 +00:00
}
private IEnumerable<VideoInfo> GetVideosGroupedByVersion(List<VideoInfo> videos)
{
if (videos.Count == 0)
{
return videos;
}
var list = new List<VideoInfo>();
var folderName = Path.GetFileName(Path.GetDirectoryName(videos[0].Files[0].Path));
2019-05-10 18:37:42 +00:00
if (!string.IsNullOrEmpty(folderName)
&& folderName.Length > 1
&& videos.All(i => i.Files.Count == 1
2020-03-26 00:29:47 +00:00
&& IsEligibleForMultiVersion(folderName, i.Files[0].Path))
2019-05-10 18:37:42 +00:00
&& HaveSameYear(videos))
2018-09-12 17:26:21 +00:00
{
2019-05-10 18:37:42 +00:00
var ordered = videos.OrderBy(i => i.Name).ToList();
2019-05-10 18:37:42 +00:00
list.Add(ordered[0]);
2020-01-22 21:18:56 +00:00
var alternateVersionsLen = ordered.Count - 1;
var alternateVersions = new VideoFileInfo[alternateVersionsLen];
for (int i = 0; i < alternateVersionsLen; i++)
{
alternateVersions[i] = ordered[i + 1].Files[0];
}
list[0].AlternateVersions = alternateVersions;
2019-05-10 18:37:42 +00:00
list[0].Name = folderName;
2020-01-22 21:18:56 +00:00
var extras = ordered.Skip(1).SelectMany(i => i.Extras).ToList();
extras.AddRange(list[0].Extras);
list[0].Extras = extras;
2019-05-10 18:37:42 +00:00
return list;
}
return videos;
}
private bool HaveSameYear(List<VideoInfo> videos)
{
return videos.Select(i => i.Year ?? -1).Distinct().Count() < 2;
}
2020-11-01 09:47:31 +00:00
private bool IsEligibleForMultiVersion(string folderName, string? testFilename)
{
testFilename = Path.GetFileNameWithoutExtension(testFilename) ?? string.Empty;
if (testFilename.StartsWith(folderName, StringComparison.OrdinalIgnoreCase))
{
if (CleanStringParser.TryClean(testFilename, _options.CleanStringRegexes, out var cleanName))
{
testFilename = cleanName.ToString();
}
2020-11-22 21:39:34 +00:00
if (folderName.Length <= testFilename.Length)
{
testFilename = testFilename.Substring(folderName.Length).Trim();
}
2019-05-10 18:37:42 +00:00
return string.IsNullOrEmpty(testFilename)
|| testFilename[0].Equals('-')
|| testFilename[0].Equals('_')
2020-03-26 00:29:47 +00:00
|| string.IsNullOrWhiteSpace(Regex.Replace(testFilename, @"\[([^]]*)\]", string.Empty));
}
return false;
2018-09-12 17:26:21 +00:00
}
private List<VideoFileInfo> GetExtras(IEnumerable<VideoFileInfo> remainingFiles, List<string> baseNames)
{
foreach (var name in baseNames.ToList())
{
var trimmedName = name.TrimEnd().TrimEnd(_options.VideoFlagDelimiters).TrimEnd();
baseNames.Add(trimmedName);
}
return remainingFiles
.Where(i => i.ExtraType != null)
2020-03-25 16:53:03 +00:00
.Where(i => baseNames.Any(b =>
i.FileNameWithoutExtension.StartsWith(b, StringComparison.OrdinalIgnoreCase)))
2018-09-12 17:26:21 +00:00
.ToList();
}
}
}