jellyfin/Emby.Naming/Video/VideoListResolver.cs

253 lines
9.1 KiB
C#

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text.RegularExpressions;
using Emby.Naming.Common;
using MediaBrowser.Model.IO;
namespace Emby.Naming.Video
{
public class VideoListResolver
{
private readonly NamingOptions _options;
public VideoListResolver(NamingOptions options)
{
_options = options;
}
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))
.Where(i => i != null)
.ToList();
// Filter out all extras, otherwise they could cause stacks to not be resolved
// See the unit test TestStackedWithTrailer
var nonExtras = videoInfos
.Where(i => string.IsNullOrEmpty(i.ExtraType))
.Select(i => new FileSystemMetadata
{
FullName = i.Path,
IsDirectory = i.IsDirectory
});
var stackResult = new StackResolver(_options)
.Resolve(nonExtras);
var remainingFiles = videoInfos
.Where(i => !stackResult.Stacks.Any(s => s.ContainsFile(i.Path, i.IsDirectory)))
.ToList();
var list = new List<VideoInfo>();
foreach (var stack in stackResult.Stacks)
{
var info = new VideoInfo
{
Files = stack.Files.Select(i => videoResolver.Resolve(i, stack.IsDirectoryStack)).ToList(),
Name = stack.Name
};
info.Year = info.Files.First().Year;
var extraBaseNames = new List<string>
{
stack.Name,
Path.GetFileNameWithoutExtension(stack.Files[0])
};
var extras = GetExtras(remainingFiles, extraBaseNames);
if (extras.Count > 0)
{
remainingFiles = remainingFiles
.Except(extras)
.ToList();
info.Extras = extras;
}
list.Add(info);
}
var standaloneMedia = remainingFiles
.Where(i => string.IsNullOrEmpty(i.ExtraType))
.ToList();
foreach (var media in standaloneMedia)
{
var info = new VideoInfo
{
Files = new List<VideoFileInfo> { media },
Name = media.Name
};
info.Year = info.Files.First().Year;
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))
{
var folderName = Path.GetFileName(Path.GetDirectoryName(videoPath));
if (!string.IsNullOrEmpty(folderName))
{
var extras = GetExtras(remainingFiles, new List<string> { folderName });
remainingFiles = remainingFiles
.Except(extras)
.ToList();
info.Extras.AddRange(extras);
}
}
// 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();
info.Extras.AddRange(extrasByFileName);
}
// If there's only one video, accept all trailers
// Be lenient because people use all kinds of mish mash conventions with trailers
if (list.Count == 1)
{
var trailers = remainingFiles
.Where(i => string.Equals(i.ExtraType, "trailer", StringComparison.OrdinalIgnoreCase))
.ToList();
list[0].Extras.AddRange(trailers);
remainingFiles = remainingFiles
.Except(trailers)
.ToList();
}
// Whatever files are left, just add them
list.AddRange(remainingFiles.Select(i => new VideoInfo
{
Files = new List<VideoFileInfo> { i },
Name = i.Name,
Year = i.Year
}));
var orderedList = list.OrderBy(i => i.Name);
return orderedList;
}
private IEnumerable<VideoInfo> GetVideosGroupedByVersion(List<VideoInfo> videos)
{
if (videos.Count == 0)
{
return videos;
}
var folderName = Path.GetFileName(Path.GetDirectoryName(videos.First().Files.First().Path));
if (!string.IsNullOrEmpty(folderName))
{
var videosMatchingFolder = new List<VideoInfo>();
foreach (VideoInfo video in videos)
{
// Only interested in single files
if (video.Files.Count != 1)
{
continue;
}
if (string.Equals(folderName, video.Name, StringComparison.OrdinalIgnoreCase))
{
videosMatchingFolder.Add(video);
}
// Eg. My Movie == My Movie - Some Other Info, TODO doesn't seem like a robust test
else if (video.Name.StartsWith(folderName, StringComparison.OrdinalIgnoreCase) &&
video.Name.Substring(folderName.Length).TrimStart().StartsWith("-"))
{
videosMatchingFolder.Add(video);
}
}
// It is assumed that any non-matching files are random samples, trailers, extras etc.
// So if there's at least one video file matching the folder name, skip the rest.
if (videosMatchingFolder.Count > 0)
{
var primary = videosMatchingFolder.First();
var remainingVideos = videosMatchingFolder.Skip(1);
var videoInfo = new VideoInfo
{
Name = folderName,
Year = videosMatchingFolder.First().Year,
Files = videosMatchingFolder.First().Files,
AlternateVersions = new List<VideoFileInfo>(),
Extras = primary.Extras
};
foreach (VideoInfo video in remainingVideos)
{
videoInfo.AlternateVersions.Add(video.Files.First());
videoInfo.Extras.AddRange(video.Extras);
}
return new[] { videoInfo };
}
}
return videos.GroupBy(v => new { v.Name, v.Year }).Select(group => new VideoInfo
{
// Because of the grouping, we can grab the information from the first movie and make it primary
// The remaining movie matches are 'alternate versions'
Name = group.First().Name,
Year = group.First().Year,
Files = group.First().Files,
AlternateVersions = group.Skip(1).Select(i => i.Files[0]).ToList(),
Extras = group.First().Extras.Concat(group.Skip(1).SelectMany(i => i.Extras)).ToList()
});
}
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 => !string.IsNullOrEmpty(i.ExtraType))
.Where(i => baseNames.Any(b => i.FileNameWithoutExtension.StartsWith(b, StringComparison.OrdinalIgnoreCase)))
.ToList();
}
}
}