Refactor extras parsing

pull/6956/head
cvium 2 years ago
parent 9cafa2cab4
commit fde84a1e00

@ -14,6 +14,7 @@ namespace Emby.Naming.AudioBook
public class AudioBookListResolver
{
private readonly NamingOptions _options;
private readonly AudioBookResolver _audioBookResolver;
/// <summary>
/// Initializes a new instance of the <see cref="AudioBookListResolver"/> class.
@ -22,6 +23,7 @@ namespace Emby.Naming.AudioBook
public AudioBookListResolver(NamingOptions options)
{
_options = options;
_audioBookResolver = new AudioBookResolver(_options);
}
/// <summary>
@ -31,21 +33,19 @@ namespace Emby.Naming.AudioBook
/// <returns>Returns IEnumerable of <see cref="AudioBookInfo"/>.</returns>
public IEnumerable<AudioBookInfo> Resolve(IEnumerable<FileSystemMetadata> files)
{
var audioBookResolver = new AudioBookResolver(_options);
// File with empty fullname will be sorted out here.
var audiobookFileInfos = files
.Select(i => audioBookResolver.Resolve(i.FullName))
.Select(i => _audioBookResolver.Resolve(i.FullName))
.OfType<AudioBookFileInfo>()
.ToList();
var stackResult = new StackResolver(_options)
.ResolveAudioBooks(audiobookFileInfos);
var stackResult = StackResolver.ResolveAudioBooks(audiobookFileInfos);
foreach (var stack in stackResult)
{
var stackFiles = stack.Files
.Select(i => audioBookResolver.Resolve(i))
.Select(i => _audioBookResolver.Resolve(i))
.OfType<AudioBookFileInfo>()
.ToList();

@ -126,9 +126,9 @@ namespace Emby.Naming.Common
VideoFileStackingExpressions = new[]
{
"(?<title>.*?)(?<volume>[ _.-]*(?:cd|dvd|p(?:ar)?t|dis[ck])[ _.-]*[0-9]+)(?<ignore>.*?)(?<extension>\\.[^.]+)$",
"(?<title>.*?)(?<volume>[ _.-]*(?:cd|dvd|p(?:ar)?t|dis[ck])[ _.-]*[a-d])(?<ignore>.*?)(?<extension>\\.[^.]+)$",
"(?<title>.*?)(?<volume>[ ._-]*[a-d])(?<ignore>.*?)(?<extension>\\.[^.]+)$"
"^(?<title>.*?)(?<volume>[ _.-]*(?:cd|dvd|part|pt|dis[ck])[ _.-]*[0-9]+)(?<ignore>.*?)(?<extension>\\.[^.]+)$",
"^(?<title>.*?)(?<volume>[ _.-]*(?:cd|dvd|part|pt|dis[ck])[ _.-]*[a-d])(?<ignore>.*?)(?<extension>\\.[^.]+)$",
"^(?<title>.*?)(?<volume>[ ._-]*[a-d])(?<ignore>.*?)(?<extension>\\.[^.]+)$"
};
CleanDateTimes = new[]
@ -403,6 +403,12 @@ namespace Emby.Naming.Common
VideoExtraRules = new[]
{
new ExtraRule(
ExtraType.Trailer,
ExtraRuleType.DirectoryName,
"trailers",
MediaType.Video),
new ExtraRule(
ExtraType.Trailer,
ExtraRuleType.Filename,

@ -1,5 +1,7 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text.RegularExpressions;
using Emby.Naming.Audio;
using Emby.Naming.Common;
@ -9,45 +11,27 @@ namespace Emby.Naming.Video
/// <summary>
/// Resolve if file is extra for video.
/// </summary>
public class ExtraResolver
public static class ExtraResolver
{
private static readonly char[] _digits = new[] { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9' };
private readonly NamingOptions _options;
/// <summary>
/// Initializes a new instance of the <see cref="ExtraResolver"/> class.
/// </summary>
/// <param name="options"><see cref="NamingOptions"/> object containing VideoExtraRules and passed to <see cref="AudioFileParser"/> and <see cref="VideoResolver"/>.</param>
public ExtraResolver(NamingOptions options)
{
_options = options;
}
private static readonly char[] _digits = { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9' };
/// <summary>
/// Attempts to resolve if file is extra.
/// </summary>
/// <param name="path">Path to file.</param>
/// <param name="namingOptions">The naming options.</param>
/// <returns>Returns <see cref="ExtraResult"/> object.</returns>
public ExtraResult GetExtraInfo(string path)
public static ExtraResult GetExtraInfo(string path, NamingOptions namingOptions)
{
var result = new ExtraResult();
for (var i = 0; i < _options.VideoExtraRules.Length; i++)
for (var i = 0; i < namingOptions.VideoExtraRules.Length; i++)
{
var rule = _options.VideoExtraRules[i];
if (rule.MediaType == MediaType.Audio)
var rule = namingOptions.VideoExtraRules[i];
if ((rule.MediaType == MediaType.Audio && !AudioFileParser.IsAudioFile(path, namingOptions))
|| (rule.MediaType == MediaType.Video && !VideoResolver.IsVideoFile(path, namingOptions)))
{
if (!AudioFileParser.IsAudioFile(path, _options))
{
continue;
}
}
else if (rule.MediaType == MediaType.Video)
{
if (!VideoResolver.IsVideoFile(path, _options))
{
continue;
}
continue;
}
var pathSpan = path.AsSpan();
@ -76,7 +60,7 @@ namespace Emby.Naming.Video
{
var filename = Path.GetFileName(path);
var regex = new Regex(rule.Token, RegexOptions.IgnoreCase);
var regex = new Regex(rule.Token, RegexOptions.IgnoreCase | RegexOptions.Compiled);
if (regex.IsMatch(filename))
{
@ -102,5 +86,68 @@ namespace Emby.Naming.Video
return result;
}
/// <summary>
/// Finds extras matching the video info.
/// </summary>
/// <param name="files">The list of file video infos.</param>
/// <param name="videoInfo">The video to compare against.</param>
/// <param name="videoFlagDelimiters">The video flag delimiters.</param>
/// <returns>A list of video extras for [videoInfo].</returns>
public static IReadOnlyList<VideoFileInfo> GetExtras(IReadOnlyList<VideoInfo> files, VideoFileInfo videoInfo, ReadOnlySpan<char> videoFlagDelimiters)
{
var parentDir = videoInfo.IsDirectory ? videoInfo.Path : Path.GetDirectoryName(videoInfo.Path.AsSpan());
var trimmedFileName = TrimFilenameDelimiters(videoInfo.Name, videoFlagDelimiters);
var trimmedFileNameWithoutExtension = TrimFilenameDelimiters(videoInfo.FileNameWithoutExtension, videoFlagDelimiters);
var trimmedVideoInfoName = TrimFilenameDelimiters(videoInfo.Name, videoFlagDelimiters);
var result = new List<VideoFileInfo>();
for (var pos = files.Count - 1; pos >= 0; pos--)
{
var current = files[pos];
// ignore non-extras and multi-file (can this happen?)
if (current.ExtraType == null || current.Files.Count > 1)
{
continue;
}
var currentFile = files[pos].Files[0];
var trimmedCurrentFileName = TrimFilenameDelimiters(currentFile.Name, videoFlagDelimiters);
// first check filenames
bool isValid = StartsWith(trimmedCurrentFileName, trimmedFileNameWithoutExtension)
|| (StartsWith(trimmedCurrentFileName, trimmedFileName) && currentFile.Year == videoInfo.Year)
|| (StartsWith(trimmedCurrentFileName, trimmedVideoInfoName) && currentFile.Year == videoInfo.Year);
// then by directory
if (!isValid)
{
// When the extra rule type is DirectoryName we must go one level higher to get the "real" dir name
var currentParentDir = currentFile.ExtraRule?.RuleType == ExtraRuleType.DirectoryName
? Path.GetDirectoryName(Path.GetDirectoryName(currentFile.Path.AsSpan()))
: Path.GetDirectoryName(currentFile.Path.AsSpan());
isValid = !currentParentDir.IsEmpty && !parentDir.IsEmpty && currentParentDir.Equals(parentDir, StringComparison.OrdinalIgnoreCase);
}
if (isValid)
{
result.Add(currentFile);
}
}
return result.OrderBy(r => r.Path).ToArray();
}
private static ReadOnlySpan<char> TrimFilenameDelimiters(ReadOnlySpan<char> name, ReadOnlySpan<char> videoFlagDelimiters)
{
return name.IsEmpty ? name : name.TrimEnd().TrimEnd(videoFlagDelimiters).TrimEnd();
}
private static bool StartsWith(ReadOnlySpan<char> fileName, ReadOnlySpan<char> baseName)
{
return !baseName.IsEmpty && fileName.StartsWith(baseName, StringComparison.OrdinalIgnoreCase);
}
}
}

@ -40,6 +40,11 @@ namespace Emby.Naming.Video
/// <returns>True if file is in the stack.</returns>
public bool ContainsFile(string file, bool isDirectory)
{
if (string.IsNullOrEmpty(file))
{
return false;
}
if (IsDirectoryStack == isDirectory)
{
return Files.Contains(file, StringComparer.OrdinalIgnoreCase);

@ -12,37 +12,28 @@ namespace Emby.Naming.Video
/// <summary>
/// Resolve <see cref="FileStack"/> from list of paths.
/// </summary>
public class StackResolver
public static class StackResolver
{
private readonly NamingOptions _options;
/// <summary>
/// Initializes a new instance of the <see cref="StackResolver"/> class.
/// </summary>
/// <param name="options"><see cref="NamingOptions"/> object containing VideoFileStackingRegexes and passes options to <see cref="VideoResolver"/>.</param>
public StackResolver(NamingOptions options)
{
_options = options;
}
/// <summary>
/// Resolves only directories from paths.
/// </summary>
/// <param name="files">List of paths.</param>
/// <param name="namingOptions">The naming options.</param>
/// <returns>Enumerable <see cref="FileStack"/> of directories.</returns>
public IEnumerable<FileStack> ResolveDirectories(IEnumerable<string> files)
public static IEnumerable<FileStack> ResolveDirectories(IEnumerable<string> files, NamingOptions namingOptions)
{
return Resolve(files.Select(i => new FileSystemMetadata { FullName = i, IsDirectory = true }));
return Resolve(files.Select(i => new FileSystemMetadata { FullName = i, IsDirectory = true }), namingOptions);
}
/// <summary>
/// Resolves only files from paths.
/// </summary>
/// <param name="files">List of paths.</param>
/// <param name="namingOptions">The naming options.</param>
/// <returns>Enumerable <see cref="FileStack"/> of files.</returns>
public IEnumerable<FileStack> ResolveFiles(IEnumerable<string> files)
public static IEnumerable<FileStack> ResolveFiles(IEnumerable<string> files, NamingOptions namingOptions)
{
return Resolve(files.Select(i => new FileSystemMetadata { FullName = i, IsDirectory = false }));
return Resolve(files.Select(i => new FileSystemMetadata { FullName = i, IsDirectory = false }), namingOptions);
}
/// <summary>
@ -50,7 +41,7 @@ namespace Emby.Naming.Video
/// </summary>
/// <param name="files">List of paths.</param>
/// <returns>Enumerable <see cref="FileStack"/> of directories.</returns>
public IEnumerable<FileStack> ResolveAudioBooks(IEnumerable<AudioBookFileInfo> files)
public static IEnumerable<FileStack> ResolveAudioBooks(IEnumerable<AudioBookFileInfo> files)
{
var groupedDirectoryFiles = files.GroupBy(file => Path.GetDirectoryName(file.Path));
@ -82,15 +73,20 @@ namespace Emby.Naming.Video
/// Resolves videos from paths.
/// </summary>
/// <param name="files">List of paths.</param>
/// <param name="namingOptions">The naming options.</param>
/// <returns>Enumerable <see cref="FileStack"/> of videos.</returns>
public IEnumerable<FileStack> Resolve(IEnumerable<FileSystemMetadata> files)
public static IEnumerable<FileStack> Resolve(IEnumerable<FileSystemMetadata> files, NamingOptions namingOptions)
{
var list = files
.Where(i => i.IsDirectory || VideoResolver.IsVideoFile(i.FullName, _options) || VideoResolver.IsStubFile(i.FullName, _options))
.Where(i => i.IsDirectory || VideoResolver.IsVideoFile(i.FullName, namingOptions) || VideoResolver.IsStubFile(i.FullName, namingOptions))
.OrderBy(i => i.FullName)
.Select(f => (f.IsDirectory, FileName: GetFileNameWithExtension(f), f.FullName))
.ToList();
var expressions = _options.VideoFileStackingRegexes;
// TODO is there a "nicer" way?
var cache = new Dictionary<(string, Regex, int), Match>();
var expressions = namingOptions.VideoFileStackingRegexes;
for (var i = 0; i < list.Count; i++)
{
@ -102,17 +98,17 @@ namespace Emby.Naming.Video
while (expressionIndex < expressions.Length)
{
var exp = expressions[expressionIndex];
var stack = new FileStack();
FileStack? stack = null;
// (Title)(Volume)(Ignore)(Extension)
var match1 = FindMatch(file1, exp, offset);
var match1 = FindMatch(file1.FileName, exp, offset, cache);
if (match1.Success)
{
var title1 = match1.Groups["title"].Value;
var volume1 = match1.Groups["volume"].Value;
var ignore1 = match1.Groups["ignore"].Value;
var extension1 = match1.Groups["extension"].Value;
var title1 = match1.Groups[1].Value;
var volume1 = match1.Groups[2].Value;
var ignore1 = match1.Groups[3].Value;
var extension1 = match1.Groups[4].Value;
var j = i + 1;
while (j < list.Count)
@ -126,7 +122,7 @@ namespace Emby.Naming.Video
}
// (Title)(Volume)(Ignore)(Extension)
var match2 = FindMatch(file2, exp, offset);
var match2 = FindMatch(file2.FileName, exp, offset, cache);
if (match2.Success)
{
@ -142,6 +138,7 @@ namespace Emby.Naming.Video
if (string.Equals(ignore1, ignore2, StringComparison.OrdinalIgnoreCase)
&& string.Equals(extension1, extension2, StringComparison.OrdinalIgnoreCase))
{
stack ??= new FileStack();
if (stack.Files.Count == 0)
{
stack.Name = title1 + ignore1;
@ -204,7 +201,7 @@ namespace Emby.Naming.Video
expressionIndex++;
}
if (stack.Files.Count > 1)
if (stack?.Files.Count > 1)
{
yield return stack;
i += stack.Files.Count - 1;
@ -214,26 +211,32 @@ namespace Emby.Naming.Video
}
}
private static string GetRegexInput(FileSystemMetadata file)
private static string GetFileNameWithExtension(FileSystemMetadata file)
{
// For directories, dummy up an extension otherwise the expressions will fail
var input = !file.IsDirectory
? file.FullName
: file.FullName + ".mkv";
var input = file.FullName;
if (file.IsDirectory)
{
input = Path.ChangeExtension(input, "mkv");
}
return Path.GetFileName(input);
}
private static Match FindMatch(FileSystemMetadata input, Regex regex, int offset)
private static Match FindMatch(string input, Regex regex, int offset, Dictionary<(string, Regex, int), Match> cache)
{
var regexInput = GetRegexInput(input);
if (offset < 0 || offset >= regexInput.Length)
if (offset < 0 || offset >= input.Length)
{
return Match.Empty;
}
return regex.Match(regexInput, offset);
if (!cache.TryGetValue((input, regex, offset), out var result))
{
result = regex.Match(input, offset, input.Length - offset);
cache.Add((input, regex, offset), result);
}
return result;
}
}
}

@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using MediaBrowser.Model.Entities;
namespace Emby.Naming.Video
{
@ -17,7 +18,6 @@ namespace Emby.Naming.Video
Name = name;
Files = Array.Empty<VideoFileInfo>();
Extras = Array.Empty<VideoFileInfo>();
AlternateVersions = Array.Empty<VideoFileInfo>();
}
@ -39,16 +39,15 @@ namespace Emby.Naming.Video
/// <value>The files.</value>
public IReadOnlyList<VideoFileInfo> Files { get; set; }
/// <summary>
/// Gets or sets the extras.
/// </summary>
/// <value>The extras.</value>
public IReadOnlyList<VideoFileInfo> Extras { get; set; }
/// <summary>
/// Gets or sets the alternate versions.
/// </summary>
/// <value>The alternate versions.</value>
public IReadOnlyList<VideoFileInfo> AlternateVersions { get; set; }
/// <summary>
/// Gets or sets the extra type.
/// </summary>
public ExtraType? ExtraType { get; set; }
}
}

@ -4,7 +4,6 @@ using System.IO;
using System.Linq;
using System.Text.RegularExpressions;
using Emby.Naming.Common;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.IO;
namespace Emby.Naming.Video
@ -20,11 +19,12 @@ namespace Emby.Naming.Video
/// <param name="files">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>
/// <returns>Returns enumerable of <see cref="VideoInfo"/> which groups files together when related.</returns>
public static IEnumerable<VideoInfo> Resolve(IEnumerable<FileSystemMetadata> files, NamingOptions namingOptions, bool supportMultiVersion = true)
public static IReadOnlyList<VideoInfo> Resolve(IEnumerable<FileSystemMetadata> files, NamingOptions namingOptions, bool supportMultiVersion = true, bool parseName = true)
{
var videoInfos = files
.Select(i => VideoResolver.Resolve(i.FullName, i.IsDirectory, namingOptions))
.Select(i => VideoResolver.Resolve(i.FullName, i.IsDirectory, namingOptions, parseName))
.OfType<VideoFileInfo>()
.ToList();
@ -34,12 +34,25 @@ namespace Emby.Naming.Video
.Where(i => i.ExtraType == null)
.Select(i => new FileSystemMetadata { FullName = i.Path, IsDirectory = i.IsDirectory });
var stackResult = new StackResolver(namingOptions)
.Resolve(nonExtras).ToList();
var stackResult = StackResolver.Resolve(nonExtras, namingOptions).ToList();
var remainingFiles = videoInfos
.Where(i => !stackResult.Any(s => i.Path != null && s.ContainsFile(i.Path, i.IsDirectory)))
.ToList();
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;
}
remainingFiles.Add(current);
if (current.ExtraType == null)
{
standaloneMedia.Add(current);
}
}
var list = new List<VideoInfo>();
@ -47,27 +60,15 @@ namespace Emby.Naming.Video
{
var info = new VideoInfo(stack.Name)
{
Files = stack.Files.Select(i => VideoResolver.Resolve(i, stack.IsDirectoryStack, namingOptions))
Files = stack.Files.Select(i => VideoResolver.Resolve(i, stack.IsDirectoryStack, namingOptions, parseName))
.OfType<VideoFileInfo>()
.ToList()
};
info.Year = info.Files[0].Year;
var extras = ExtractExtras(remainingFiles, stack.Name, Path.GetFileNameWithoutExtension(stack.Files[0].AsSpan()), namingOptions.VideoFlagDelimiters);
if (extras.Count > 0)
{
info.Extras = extras;
}
list.Add(info);
}
var standaloneMedia = remainingFiles
.Where(i => i.ExtraType == null)
.ToList();
foreach (var media in standaloneMedia)
{
var info = new VideoInfo(media.Name) { Files = new[] { media } };
@ -75,10 +76,6 @@ namespace Emby.Naming.Video
info.Year = info.Files[0].Year;
remainingFiles.Remove(media);
var extras = ExtractExtras(remainingFiles, media.FileNameWithoutExtension, namingOptions.VideoFlagDelimiters);
info.Extras = extras;
list.Add(info);
}
@ -87,58 +84,12 @@ namespace Emby.Naming.Video
list = GetVideosGroupedByVersion(list, namingOptions);
}
// 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.AsSpan());
if (!parentPath.IsEmpty)
{
var folderName = Path.GetFileName(parentPath);
if (!folderName.IsEmpty)
{
var extras = ExtractExtras(remainingFiles, folderName, namingOptions.VideoFlagDelimiters);
extras.AddRange(info.Extras);
info.Extras = 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();
extrasByFileName.AddRange(info.Extras);
info.Extras = extrasByFileName;
}
// If there's only one video, accept all trailers
// Be lenient because people use all kinds of mishmash conventions with trailers.
if (list.Count == 1)
{
var trailers = remainingFiles
.Where(i => i.ExtraType == ExtraType.Trailer)
.ToList();
trailers.AddRange(list[0].Extras);
list[0].Extras = trailers;
remainingFiles = remainingFiles
.Except(trailers)
.ToList();
}
// Whatever files are left, just add them
list.AddRange(remainingFiles.Select(i => new VideoInfo(i.Name)
{
Files = new[] { i },
Year = i.Year
Year = i.Year,
ExtraType = i.ExtraType
}));
return list;
@ -162,6 +113,11 @@ namespace Emby.Naming.Video
for (var i = 0; i < videos.Count; i++)
{
var video = videos[i];
if (video.ExtraType != null)
{
continue;
}
if (!IsEligibleForMultiVersion(folderName, video.Files[0].Path, namingOptions))
{
return videos;
@ -178,17 +134,14 @@ namespace Emby.Naming.Video
var alternateVersionsLen = videos.Count - 1;
var alternateVersions = new VideoFileInfo[alternateVersionsLen];
var extras = new List<VideoFileInfo>(list[0].Extras);
for (int i = 0; i < alternateVersionsLen; i++)
{
var video = videos[i + 1];
alternateVersions[i] = video.Files[0];
extras.AddRange(video.Extras);
}
list[0].AlternateVersions = alternateVersions;
list[0].Name = folderName.ToString();
list[0].Extras = extras;
return list;
}
@ -230,7 +183,7 @@ namespace Emby.Naming.Video
var tmpTestFilename = testFilename.ToString();
if (CleanStringParser.TryClean(tmpTestFilename, namingOptions.CleanStringRegexes, out var cleanName))
{
tmpTestFilename = cleanName.Trim().ToString();
tmpTestFilename = cleanName.Trim();
}
// The CleanStringParser should have removed common keywords etc.
@ -238,67 +191,5 @@ namespace Emby.Naming.Video
|| testFilename[0] == '-'
|| Regex.IsMatch(tmpTestFilename, @"^\[([^]]*)\]", RegexOptions.Compiled);
}
private static ReadOnlySpan<char> TrimFilenameDelimiters(ReadOnlySpan<char> name, ReadOnlySpan<char> videoFlagDelimiters)
{
return name.IsEmpty ? name : name.TrimEnd().TrimEnd(videoFlagDelimiters).TrimEnd();
}
private static bool StartsWith(ReadOnlySpan<char> fileName, ReadOnlySpan<char> baseName, ReadOnlySpan<char> trimmedBaseName)
{
if (baseName.IsEmpty)
{
return false;
}
return fileName.StartsWith(baseName, StringComparison.OrdinalIgnoreCase)
|| (!trimmedBaseName.IsEmpty && fileName.StartsWith(trimmedBaseName, StringComparison.OrdinalIgnoreCase));
}
/// <summary>
/// Finds similar filenames to that of [baseName] and removes any matches from [remainingFiles].
/// </summary>
/// <param name="remainingFiles">The list of remaining filenames.</param>
/// <param name="baseName">The base name to use for the comparison.</param>
/// <param name="videoFlagDelimiters">The video flag delimiters.</param>
/// <returns>A list of video extras for [baseName].</returns>
private static List<VideoFileInfo> ExtractExtras(IList<VideoFileInfo> remainingFiles, ReadOnlySpan<char> baseName, ReadOnlySpan<char> videoFlagDelimiters)
{
return ExtractExtras(remainingFiles, baseName, ReadOnlySpan<char>.Empty, videoFlagDelimiters);
}
/// <summary>
/// Finds similar filenames to that of [firstBaseName] and [secondBaseName] and removes any matches from [remainingFiles].
/// </summary>
/// <param name="remainingFiles">The list of remaining filenames.</param>
/// <param name="firstBaseName">The first base name to use for the comparison.</param>
/// <param name="secondBaseName">The second base name to use for the comparison.</param>
/// <param name="videoFlagDelimiters">The video flag delimiters.</param>
/// <returns>A list of video extras for [firstBaseName] and [secondBaseName].</returns>
private static List<VideoFileInfo> ExtractExtras(IList<VideoFileInfo> remainingFiles, ReadOnlySpan<char> firstBaseName, ReadOnlySpan<char> secondBaseName, ReadOnlySpan<char> videoFlagDelimiters)
{
var trimmedFirstBaseName = TrimFilenameDelimiters(firstBaseName, videoFlagDelimiters);
var trimmedSecondBaseName = TrimFilenameDelimiters(secondBaseName, videoFlagDelimiters);
var result = new List<VideoFileInfo>();
for (var pos = remainingFiles.Count - 1; pos >= 0; pos--)
{
var file = remainingFiles[pos];
if (file.ExtraType == null)
{
continue;
}
var filename = file.FileNameWithoutExtension;
if (StartsWith(filename, firstBaseName, trimmedFirstBaseName)
|| StartsWith(filename, secondBaseName, trimmedSecondBaseName))
{
result.Add(file);
remainingFiles.RemoveAt(pos);
}
}
return result;
}
}
}

@ -16,10 +16,11 @@ namespace Emby.Naming.Video
/// </summary>
/// <param name="path">The path.</param>
/// <param name="namingOptions">The naming options.</param>
/// <param name="parseName">Whether to parse the name or use the filename.</param>
/// <returns>VideoFileInfo.</returns>
public static VideoFileInfo? ResolveDirectory(string? path, NamingOptions namingOptions)
public static VideoFileInfo? ResolveDirectory(string? path, NamingOptions namingOptions, bool parseName = true)
{
return Resolve(path, true, namingOptions);
return Resolve(path, true, namingOptions, parseName);
}
/// <summary>
@ -74,7 +75,7 @@ namespace Emby.Naming.Video
var format3DResult = Format3DParser.Parse(path, namingOptions);
var extraResult = new ExtraResolver(namingOptions).GetExtraInfo(path);
var extraResult = ExtraResolver.GetExtraInfo(path, namingOptions);
var name = Path.GetFileNameWithoutExtension(path);

@ -11,11 +11,9 @@ using System.Net;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using Emby.Naming.Audio;
using Emby.Naming.Common;
using Emby.Naming.TV;
using Emby.Naming.Video;
using Emby.Server.Implementations.Library.Resolvers;
using Emby.Server.Implementations.Library.Validators;
using Emby.Server.Implementations.Playlists;
using Emby.Server.Implementations.ScheduledTasks;
@ -677,7 +675,7 @@ namespace Emby.Server.Implementations.Library
{
var result = resolver.ResolveMultiple(parent, fileList, collectionType, directoryService);
if (result != null && result.Items.Count > 0)
if (result?.Items.Count > 0)
{
var items = new List<BaseItem>();
items.AddRange(result.Items);
@ -2685,89 +2683,58 @@ namespace Emby.Server.Implementations.Library
};
}
public IEnumerable<Video> FindTrailers(BaseItem owner, List<FileSystemMetadata> fileSystemChildren, IDirectoryService directoryService)
public IEnumerable<Video> FindExtras(BaseItem owner, List<FileSystemMetadata> fileSystemChildren)
{
var namingOptions = _namingOptions;
var files = owner.IsInMixedFolder ? new List<FileSystemMetadata>() : fileSystemChildren.Where(i => i.IsDirectory)
.Where(i => string.Equals(i.Name, BaseItem.TrailersFolderName, StringComparison.OrdinalIgnoreCase))
.SelectMany(i => _fileSystem.GetFiles(i.FullName, namingOptions.VideoFileExtensions, false, false))
.ToList();
var videos = VideoListResolver.Resolve(fileSystemChildren, namingOptions);
var currentVideo = videos.FirstOrDefault(i => string.Equals(owner.Path, i.Files[0].Path, StringComparison.OrdinalIgnoreCase));
if (currentVideo != null)
var ownerVideoInfo = VideoResolver.Resolve(owner.Path, owner.IsFolder, _namingOptions);
if (ownerVideoInfo == null)
{
files.AddRange(currentVideo.Extras.Where(i => i.ExtraType == ExtraType.Trailer).Select(i => _fileSystem.GetFileInfo(i.Path)));
yield break;
}
var resolvers = new IItemResolver[]
var count = fileSystemChildren.Count;
var files = new List<FileSystemMetadata>();
for (var i = 0; i < count; i++)
{
new GenericVideoResolver<Trailer>(_namingOptions)
};
return ResolvePaths(files, directoryService, null, new LibraryOptions(), null, resolvers)
.OfType<Trailer>()
.Select(video =>
var current = fileSystemChildren[i];
if (current.IsDirectory && BaseItem.AllExtrasTypesFolderNames.ContainsKey(current.Name))
{
// Try to retrieve it from the db. If we don't find it, use the resolved version
if (GetItemById(video.Id) is Trailer dbItem)
{
video = dbItem;
}
video.ParentId = Guid.Empty;
video.OwnerId = owner.Id;
video.ExtraType = ExtraType.Trailer;
video.TrailerTypes = new[] { TrailerType.LocalTrailer };
return video;
// Sort them so that the list can be easily compared for changes
}).OrderBy(i => i.Path);
}
public IEnumerable<Video> FindExtras(BaseItem owner, List<FileSystemMetadata> fileSystemChildren, IDirectoryService directoryService)
{
var namingOptions = _namingOptions;
var files = owner.IsInMixedFolder ? new List<FileSystemMetadata>() : fileSystemChildren.Where(i => i.IsDirectory)
.Where(i => BaseItem.AllExtrasTypesFolderNames.ContainsKey(i.Name ?? string.Empty))
.SelectMany(i => _fileSystem.GetFiles(i.FullName, namingOptions.VideoFileExtensions, false, false))
.ToList();
var videos = VideoListResolver.Resolve(fileSystemChildren, namingOptions);
var currentVideo = videos.FirstOrDefault(i => string.Equals(owner.Path, i.Files[0].Path, StringComparison.OrdinalIgnoreCase));
files.AddRange(_fileSystem.GetFiles(current.FullName, _namingOptions.VideoFileExtensions, false, false));
}
else if (!current.IsDirectory)
{
files.Add(current);
}
}
if (currentVideo != null)
if (files.Count == 0)
{
files.AddRange(currentVideo.Extras.Where(i => i.ExtraType != ExtraType.Trailer).Select(i => _fileSystem.GetFileInfo(i.Path)));
yield break;
}
return ResolvePaths(files, directoryService, null, new LibraryOptions(), null)
.OfType<Video>()
.Select(video =>
{
// Try to retrieve it from the db. If we don't find it, use the resolved version
var dbItem = GetItemById(video.Id) as Video;
var videos = VideoListResolver.Resolve(files, _namingOptions);
// owner video info cannot be null as that implies it has no path
var extras = ExtraResolver.GetExtras(videos, ownerVideoInfo, _namingOptions.VideoFlagDelimiters);
if (dbItem != null)
{
video = dbItem;
}
video.ParentId = Guid.Empty;
video.OwnerId = owner.Id;
SetExtraTypeFromFilename(video);
for (var i = 0; i < extras.Count; i++)
{
var currentExtra = extras[i];
var resolved = ResolvePath(_fileSystem.GetFileInfo(currentExtra.Path));
if (resolved is not Video video)
{
continue;
}
return video;
// Try to retrieve it from the db. If we don't find it, use the resolved version
if (GetItemById(resolved.Id) is Video dbItem)
{
video = dbItem;
}
// Sort them so that the list can be easily compared for changes
}).OrderBy(i => i.Path);
video.ExtraType = currentExtra.ExtraType;
video.ParentId = Guid.Empty;
video.OwnerId = owner.Id;
yield return video;
}
}
public string GetPathAfterNetworkSubstitution(string path, BaseItem ownerItem)
@ -2817,15 +2784,6 @@ namespace Emby.Server.Implementations.Library
return path;
}
private void SetExtraTypeFromFilename(Video item)
{
var resolver = new ExtraResolver(_namingOptions);
var result = resolver.GetExtraInfo(item.Path);
item.ExtraType = result.ExtraType;
}
public List<PersonInfo> GetPeople(InternalPeopleQuery query)
{
return _itemRepository.GetPeople(query);

@ -49,120 +49,71 @@ namespace Emby.Server.Implementations.Library.Resolvers
protected virtual TVideoType ResolveVideo<TVideoType>(ItemResolveArgs args, bool parseName)
where TVideoType : Video, new()
{
var namingOptions = NamingOptions;
VideoFileInfo videoInfo = null;
VideoType? videoType = null;
// If the path is a file check for a matching extensions
if (args.IsDirectory)
{
TVideoType video = null;
VideoFileInfo videoInfo = null;
// Loop through each child file/folder and see if we find a video
foreach (var child in args.FileSystemChildren)
{
var filename = child.Name;
if (child.IsDirectory)
{
if (IsDvdDirectory(child.FullName, filename, args.DirectoryService))
{
videoInfo = VideoResolver.ResolveDirectory(args.Path, namingOptions);
if (videoInfo == null)
{
return null;
}
video = new TVideoType
{
Path = args.Path,
VideoType = VideoType.Dvd,
ProductionYear = videoInfo.Year
};
break;
videoType = VideoType.Dvd;
}
if (IsBluRayDirectory(filename))
else if (IsBluRayDirectory(filename))
{
videoInfo = VideoResolver.ResolveDirectory(args.Path, namingOptions);
if (videoInfo == null)
{
return null;
}
video = new TVideoType
{
Path = args.Path,
VideoType = VideoType.BluRay,
ProductionYear = videoInfo.Year
};
break;
videoType = VideoType.BluRay;
}
}
else if (IsDvdFile(filename))
{
videoInfo = VideoResolver.ResolveDirectory(args.Path, namingOptions);
if (videoInfo == null)
{
return null;
}
video = new TVideoType
{
Path = args.Path,
VideoType = VideoType.Dvd,
ProductionYear = videoInfo.Year
};
break;
videoType = VideoType.Dvd;
}
}
if (video != null)
{
video.Name = parseName ?
videoInfo.Name :
Path.GetFileName(args.Path);
if (videoType == null)
{
continue;
}
Set3DFormat(video, videoInfo);
videoInfo = VideoResolver.ResolveDirectory(args.Path, NamingOptions, parseName);
break;
}
return video;
}
else
{
var videoInfo = VideoResolver.Resolve(args.Path, false, namingOptions, false);
if (videoInfo == null)
{
return null;
}
if (VideoResolver.IsVideoFile(args.Path, NamingOptions) || videoInfo.IsStub)
{
var path = args.Path;
var video = new TVideoType
{
Path = path,
IsInMixedFolder = true,
ProductionYear = videoInfo.Year
};
SetVideoType(video, videoInfo);
videoInfo = VideoResolver.Resolve(args.Path, false, NamingOptions, parseName);
}
video.Name = parseName ?
videoInfo.Name :
Path.GetFileNameWithoutExtension(args.Path);
if (videoInfo == null || (!videoInfo.IsStub && !VideoResolver.IsVideoFile(args.Path, NamingOptions)))
{
return null;
}
Set3DFormat(video, videoInfo);
var video = new TVideoType
{
Name = videoInfo.Name,
Path = args.Path,
ProductionYear = videoInfo.Year,
ExtraType = videoInfo.ExtraType
};
return video;
}
if (videoType.HasValue)
{
video.VideoType = videoType.Value;
}
else
{
SetVideoType(video, videoInfo);
}
Set3DFormat(video, videoInfo);
return null;
return video;
}
protected void SetVideoType(Video video, VideoFileInfo videoInfo)
@ -207,8 +158,8 @@ namespace Emby.Server.Implementations.Library.Resolvers
{
// use disc-utils, both DVDs and BDs use UDF filesystem
using (var videoFileStream = File.Open(video.Path, FileMode.Open, FileAccess.Read))
using (UdfReader udfReader = new UdfReader(videoFileStream))
{
UdfReader udfReader = new UdfReader(videoFileStream);
if (udfReader.DirectoryExists("VIDEO_TS"))
{
video.IsoType = IsoType.Dvd;

@ -26,7 +26,6 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies
public class MovieResolver : BaseVideoResolver<Video>, IMultiItemResolver
{
private readonly IImageProcessor _imageProcessor;
private readonly StackResolver _stackResolver;
private string[] _validCollectionTypes = new[]
{
@ -46,7 +45,6 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies
: base(namingOptions)
{
_imageProcessor = imageProcessor;
_stackResolver = new StackResolver(NamingOptions);
}
/// <summary>
@ -62,7 +60,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies
string collectionType,
IDirectoryService directoryService)
{
var result = ResolveMultipleInternal(parent, files, collectionType, directoryService);
var result = ResolveMultipleInternal(parent, files, collectionType);
if (result != null)
{
@ -92,16 +90,17 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies
return null;
}
Video movie = null;
var files = args.GetActualFileSystemChildren().ToList();
if (string.Equals(collectionType, CollectionType.MusicVideos, StringComparison.OrdinalIgnoreCase))
{
return FindMovie<MusicVideo>(args, args.Path, args.Parent, files, args.DirectoryService, collectionType, false);
movie = FindMovie<MusicVideo>(args, args.Path, args.Parent, files, args.DirectoryService, collectionType, false);
}
if (string.Equals(collectionType, CollectionType.HomeVideos, StringComparison.OrdinalIgnoreCase))
{
return FindMovie<Video>(args, args.Path, args.Parent, files, args.DirectoryService, collectionType, false);
movie = FindMovie<Video>(args, args.Path, args.Parent, files, args.DirectoryService, collectionType, false);
}
if (string.IsNullOrEmpty(collectionType))
@ -118,17 +117,16 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies
return null;
}
{
return FindMovie<Movie>(args, args.Path, args.Parent, files, args.DirectoryService, collectionType, true);
}
movie = FindMovie<Movie>(args, args.Path, args.Parent, files, args.DirectoryService, collectionType, true);
}
if (string.Equals(collectionType, CollectionType.Movies, StringComparison.OrdinalIgnoreCase))
{
return FindMovie<Movie>(args, args.Path, args.Parent, files, args.DirectoryService, collectionType, true);
movie = FindMovie<Movie>(args, args.Path, args.Parent, files, args.DirectoryService, collectionType, true);
}
return null;
// ignore extras
return movie?.ExtraType == null ? movie : null;
}
// Handle owned items
@ -169,6 +167,12 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies
item = ResolveVideo<Video>(args, false);
}
// Ignore extras
if (item?.ExtraType != null)
{
return null;
}
if (item != null)
{
item.IsInMixedFolder = true;
@ -180,8 +184,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies
private MultiItemResolverResult ResolveMultipleInternal(
Folder parent,
List<FileSystemMetadata> files,
string collectionType,
IDirectoryService directoryService)
string collectionType)
{
if (IsInvalid(parent, collectionType))
{
@ -190,13 +193,13 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies
if (string.Equals(collectionType, CollectionType.MusicVideos, StringComparison.OrdinalIgnoreCase))
{
return ResolveVideos<MusicVideo>(parent, files, directoryService, true, collectionType, false);
return ResolveVideos<MusicVideo>(parent, files, true, collectionType, false);
}
if (string.Equals(collectionType, CollectionType.HomeVideos, StringComparison.OrdinalIgnoreCase) ||
string.Equals(collectionType, CollectionType.Photos, StringComparison.OrdinalIgnoreCase))
{
return ResolveVideos<Video>(parent, files, directoryService, false, collectionType, false);
return ResolveVideos<Video>(parent, files, false, collectionType, false);
}
if (string.IsNullOrEmpty(collectionType))
@ -204,7 +207,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies
// Owned items should just use the plain video type
if (parent == null)
{
return ResolveVideos<Video>(parent, files, directoryService, false, collectionType, false);
return ResolveVideos<Video>(parent, files, false, collectionType, false);
}
if (parent is Series || parent.GetParents().OfType<Series>().Any())
@ -212,12 +215,12 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies
return null;
}
return ResolveVideos<Movie>(parent, files, directoryService, false, collectionType, true);
return ResolveVideos<Movie>(parent, files, false, collectionType, true);
}
if (string.Equals(collectionType, CollectionType.Movies, StringComparison.OrdinalIgnoreCase))
{
return ResolveVideos<Movie>(parent, files, directoryService, true, collectionType, true);
return ResolveVideos<Movie>(parent, files, true, collectionType, true);
}
return null;
@ -226,21 +229,20 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies
private MultiItemResolverResult ResolveVideos<T>(
Folder parent,
IEnumerable<FileSystemMetadata> fileSystemEntries,
IDirectoryService directoryService,
bool suppportMultiEditions,
bool supportMultiEditions,
string collectionType,
bool parseName)
where T : Video, new()
{
var files = new List<FileSystemMetadata>();
var videos = new List<BaseItem>();
var leftOver = new List<FileSystemMetadata>();
var hasCollectionType = !string.IsNullOrEmpty(collectionType);
// Loop through each child file/folder and see if we find a video
foreach (var child in fileSystemEntries)
{
// This is a hack but currently no better way to resolve a sometimes ambiguous situation
if (string.IsNullOrEmpty(collectionType))
if (!hasCollectionType)
{
if (string.Equals(child.Name, "tvshow.nfo", StringComparison.OrdinalIgnoreCase)
|| string.Equals(child.Name, "season.nfo", StringComparison.OrdinalIgnoreCase))
@ -259,29 +261,35 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies
}
}
var resolverResult = VideoListResolver.Resolve(files, NamingOptions, suppportMultiEditions).ToList();
var resolverResult = VideoListResolver.Resolve(files, NamingOptions, supportMultiEditions, parseName);
var result = new MultiItemResolverResult
{
ExtraFiles = leftOver,
Items = videos
ExtraFiles = leftOver
};
var isInMixedFolder = resolverResult.Count > 1 || (parent != null && parent.IsTopParent);
var isInMixedFolder = resolverResult.Count > 1 || parent?.IsTopParent == true;
foreach (var video in resolverResult)
{
var firstVideo = video.Files[0];
var path = firstVideo.Path;
if (video.ExtraType != null)
{
// TODO
result.ExtraFiles.Add(files.First(f => string.Equals(f.FullName, path, StringComparison.OrdinalIgnoreCase)));
continue;
}
var additionalParts = video.Files.Count > 1 ? video.Files.Skip(1).Select(i => i.Path).ToArray() : Array.Empty<string>();
var videoItem = new T
{
Path = video.Files[0].Path,
Path = path,
IsInMixedFolder = isInMixedFolder,
ProductionYear = video.Year,
Name = parseName ?
video.Name :
Path.GetFileNameWithoutExtension(video.Files[0].Path),
AdditionalParts = video.Files.Skip(1).Select(i => i.Path).ToArray(),
Name = parseName ? video.Name : firstVideo.Name,
AdditionalParts = additionalParts,
LocalAlternateVersions = video.AlternateVersions.Select(i => i.Path).ToArray()
};
@ -299,21 +307,34 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies
private static bool IsIgnored(string filename)
{
// Ignore samples
Match m = Regex.Match(filename, @"\bsample\b", RegexOptions.IgnoreCase);
Match m = Regex.Match(filename, @"\bsample\b", RegexOptions.IgnoreCase | RegexOptions.Compiled);
return m.Success;
}
private bool ContainsFile(List<VideoInfo> result, FileSystemMetadata file)
private static bool ContainsFile(IReadOnlyList<VideoInfo> result, FileSystemMetadata file)
{
return result.Any(i => ContainsFile(i, file));
}
for (var i = 0; i < result.Count; i++)
{
var current = result[i];
for (var j = 0; j < current.Files.Count; j++)
{
if (ContainsFile(current.Files[j], file))
{
return true;
}
}
private bool ContainsFile(VideoInfo result, FileSystemMetadata file)
{
return result.Files.Any(i => ContainsFile(i, file)) ||
result.AlternateVersions.Any(i => ContainsFile(i, file)) ||
result.Extras.Any(i => ContainsFile(i, file));
for (var j = 0; j < current.AlternateVersions.Count; j++)
{
if (ContainsFile(current.AlternateVersions[j], file))
{
return true;
}
}
}
return false;
}
private static bool ContainsFile(VideoFileInfo result, FileSystemMetadata file)
@ -431,7 +452,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies
// TODO: Allow GetMultiDiscMovie in here
const bool SupportsMultiVersion = true;
var result = ResolveVideos<T>(parent, fileSystemEntries, directoryService, SupportsMultiVersion, collectionType, parseName) ??
var result = ResolveVideos<T>(parent, fileSystemEntries, SupportsMultiVersion, collectionType, parseName) ??
new MultiItemResolverResult();
if (result.Items.Count == 1)
@ -510,7 +531,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies
return null;
}
var result = _stackResolver.ResolveDirectories(folderPaths).ToList();
var result = StackResolver.ResolveDirectories(folderPaths, NamingOptions).ToList();
if (result.Count != 1)
{

@ -45,34 +45,36 @@ namespace Emby.Server.Implementations.Library.Resolvers.TV
// If the parent is a Season or Series and the parent is not an extras folder, then this is an Episode if the VideoResolver returns something
// Also handle flat tv folders
if ((season != null ||
string.Equals(args.GetCollectionType(), CollectionType.TvShows, StringComparison.OrdinalIgnoreCase) ||
args.HasParent<Series>())
&& (parent is Series || !BaseItem.AllExtrasTypesFolderNames.ContainsKey(parent.Name)))
if (season != null ||
string.Equals(args.GetCollectionType(), CollectionType.TvShows, StringComparison.OrdinalIgnoreCase) ||
args.HasParent<Series>())
{
var episode = ResolveVideo<Episode>(args, false);
if (episode != null)
// Ignore extras
if (episode == null || episode.ExtraType != null)
{
var series = parent as Series ?? parent.GetParents().OfType<Series>().FirstOrDefault();
return null;
}
if (series != null)
{
episode.SeriesId = series.Id;
episode.SeriesName = series.Name;
}
var series = parent as Series ?? parent.GetParents().OfType<Series>().FirstOrDefault();
if (season != null)
{
episode.SeasonId = season.Id;
episode.SeasonName = season.Name;
}
if (series != null)
{
episode.SeriesId = series.Id;
episode.SeriesName = series.Name;
}
// Assume season 1 if there's no season folder and a season number could not be determined
if (season == null && !episode.ParentIndexNumber.HasValue && (episode.IndexNumber.HasValue || episode.PremiereDate.HasValue))
{
episode.ParentIndexNumber = 1;
}
if (season != null)
{
episode.SeasonId = season.Id;
episode.SeasonName = season.Name;
}
// Assume season 1 if there's no season folder and a season number could not be determined
if (season == null && !episode.ParentIndexNumber.HasValue && (episode.IndexNumber.HasValue || episode.PremiereDate.HasValue))
{
episode.ParentIndexNumber = 1;
}
return episode;

@ -213,7 +213,7 @@ namespace Jellyfin.Api.Controllers
if (item is IHasTrailers hasTrailers)
{
var trailers = hasTrailers.GetTrailers();
var trailers = hasTrailers.LocalTrailers;
var dtosTrailers = _dtoService.GetBaseItemDtos(trailers, dtoOptions, user, item);
var allTrailers = new BaseItemDto[dtosExtras.Length + dtosTrailers.Count];
dtosExtras.CopyTo(allTrailers, 0);

@ -40,9 +40,7 @@ namespace MediaBrowser.Controller.Entities
/// </summary>
public abstract class BaseItem : IHasProviderIds, IHasLookupInfo<ItemLookupInfo>, IEquatable<BaseItem>
{
/// <summary>
/// The trailer folder name.
/// </summary>
public const string TrailerFileName = "trailer";
public const string TrailersFolderName = "trailers";
public const string ThemeSongsFolderName = "theme-music";
public const string ThemeSongFileName = "theme";
@ -99,8 +97,6 @@ namespace MediaBrowser.Controller.Entities
};
private string _sortName;
private Guid[] _themeSongIds;
private Guid[] _themeVideoIds;
private string _forcedSortName;
@ -121,40 +117,6 @@ namespace MediaBrowser.Controller.Entities
ExtraIds = Array.Empty<Guid>();
}
[JsonIgnore]
public Guid[] ThemeSongIds
{
get
{
return _themeSongIds ??= GetExtras()
.Where(extra => extra.ExtraType == Model.Entities.ExtraType.ThemeSong)
.Select(song => song.Id)
.ToArray();
}
private set
{
_themeSongIds = value;
}
}
[JsonIgnore]
public Guid[] ThemeVideoIds
{
get
{
return _themeVideoIds ??= GetExtras()
.Where(extra => extra.ExtraType == Model.Entities.ExtraType.ThemeVideo)
.Select(song => song.Id)
.ToArray();
}
private set
{
_themeVideoIds = value;
}
}
[JsonIgnore]
public string PreferredMetadataCountryCode { get; set; }
@ -1379,28 +1341,6 @@ namespace MediaBrowser.Controller.Entities
}).OrderBy(i => i.Path).ToArray();
}
protected virtual BaseItem[] LoadExtras(List<FileSystemMetadata> fileSystemChildren, IDirectoryService directoryService)
{
return fileSystemChildren
.Where(child => child.IsDirectory && AllExtrasTypesFolderNames.ContainsKey(child.Name))
.SelectMany(folder => LibraryManager
.ResolvePaths(FileSystem.GetFiles(folder.FullName), directoryService, null, new LibraryOptions())
.OfType<Video>()
.Select(video =>
{
// Try to retrieve it from the db. If we don't find it, use the resolved version
if (LibraryManager.GetItemById(video.Id) is Video dbItem)
{
video = dbItem;
}
video.ExtraType = AllExtrasTypesFolderNames[folder.Name];
return video;
})
.OrderBy(video => video.Path)) // Sort them so that the list can be easily compared for changes
.ToArray();
}
public Task RefreshMetadata(CancellationToken cancellationToken)
{
return RefreshMetadata(new MetadataRefreshOptions(new DirectoryService(FileSystem)), cancellationToken);
@ -1434,13 +1374,8 @@ namespace MediaBrowser.Controller.Entities
GetFileSystemChildren(options.DirectoryService).ToList() :
new List<FileSystemMetadata>();
var ownedItemsChanged = await RefreshedOwnedItems(options, files, cancellationToken).ConfigureAwait(false);
requiresSave = await RefreshedOwnedItems(options, files, cancellationToken).ConfigureAwait(false);
await LibraryManager.UpdateImagesAsync(this).ConfigureAwait(false); // ensure all image properties in DB are fresh
if (ownedItemsChanged)
{
requiresSave = true;
}
}
catch (Exception ex)
{
@ -1516,35 +1451,12 @@ namespace MediaBrowser.Controller.Entities
/// <returns><c>true</c> if any items have changed, else <c>false</c>.</returns>
protected virtual async Task<bool> RefreshedOwnedItems(MetadataRefreshOptions options, List<FileSystemMetadata> fileSystemChildren, CancellationToken cancellationToken)
{
var themeSongsChanged = false;
var themeVideosChanged = false;
var extrasChanged = false;
var localTrailersChanged = false;
if (IsFileProtocol && SupportsOwnedItems)
if (!IsFileProtocol || !SupportsOwnedItems || IsInMixedFolder || this is ICollectionFolder)
{
if (SupportsThemeMedia)
{
if (!IsInMixedFolder)
{
themeSongsChanged = await RefreshThemeSongs(this, options, fileSystemChildren, cancellationToken).ConfigureAwait(false);
themeVideosChanged = await RefreshThemeVideos(this, options, fileSystemChildren, cancellationToken).ConfigureAwait(false);
extrasChanged = await RefreshExtras(this, options, fileSystemChildren, cancellationToken).ConfigureAwait(false);
}
}
if (this is IHasTrailers hasTrailers)
{
localTrailersChanged = await RefreshLocalTrailers(hasTrailers, options, fileSystemChildren, cancellationToken).ConfigureAwait(false);
}
return false;
}
return themeSongsChanged || themeVideosChanged || extrasChanged || localTrailersChanged;
return await RefreshExtras(this, options, fileSystemChildren, cancellationToken).ConfigureAwait(false);
}
protected virtual FileSystemMetadata[] GetFileSystemChildren(IDirectoryService directoryService)
@ -1554,98 +1466,24 @@ namespace MediaBrowser.Controller.Entities
return directoryService.GetFileSystemEntries(path);
}
private async Task<bool> RefreshLocalTrailers(IHasTrailers item, MetadataRefreshOptions options, List<FileSystemMetadata> fileSystemChildren, CancellationToken cancellationToken)
{
var newItems = LibraryManager.FindTrailers(this, fileSystemChildren, options.DirectoryService);
var newItemIds = newItems.Select(i => i.Id);
var itemsChanged = !item.LocalTrailerIds.SequenceEqual(newItemIds);
var ownerId = item.Id;
var tasks = newItems.Select(i =>
{
var subOptions = new MetadataRefreshOptions(options);
if (i.ExtraType != Model.Entities.ExtraType.Trailer ||
i.OwnerId != ownerId ||
!i.ParentId.Equals(Guid.Empty))
{
i.ExtraType = Model.Entities.ExtraType.Trailer;
i.OwnerId = ownerId;
i.ParentId = Guid.Empty;
subOptions.ForceSave = true;
}
return RefreshMetadataForOwnedItem(i, true, subOptions, cancellationToken);
});
await Task.WhenAll(tasks).ConfigureAwait(false);
item.LocalTrailerIds = newItemIds.ToArray();
return itemsChanged;
}
private async Task<bool> RefreshExtras(BaseItem item, MetadataRefreshOptions options, List<FileSystemMetadata> fileSystemChildren, CancellationToken cancellationToken)
{
var extras = LoadExtras(fileSystemChildren, options.DirectoryService);
var themeVideos = LoadThemeVideos(fileSystemChildren, options.DirectoryService);
var themeSongs = LoadThemeSongs(fileSystemChildren, options.DirectoryService);
var newExtras = new BaseItem[extras.Length + themeVideos.Length + themeSongs.Length];
extras.CopyTo(newExtras, 0);
themeVideos.CopyTo(newExtras, extras.Length);
themeSongs.CopyTo(newExtras, extras.Length + themeVideos.Length);
var newExtraIds = newExtras.Select(i => i.Id).ToArray();
var extras = LibraryManager.FindExtras(item, fileSystemChildren).ToArray();
var newExtraIds = extras.Select(i => i.Id).ToArray();
var extrasChanged = !item.ExtraIds.SequenceEqual(newExtraIds);
if (extrasChanged)
if (!extrasChanged)
{
var ownerId = item.Id;
var tasks = newExtras.Select(i =>
{
var subOptions = new MetadataRefreshOptions(options);
if (i.OwnerId != ownerId || i.ParentId != Guid.Empty)
{
i.OwnerId = ownerId;
i.ParentId = Guid.Empty;
subOptions.ForceSave = true;
}
return RefreshMetadataForOwnedItem(i, true, subOptions, cancellationToken);
});
await Task.WhenAll(tasks).ConfigureAwait(false);
item.ExtraIds = newExtraIds;
return false;
}
return extrasChanged;
}
private async Task<bool> RefreshThemeVideos(BaseItem item, MetadataRefreshOptions options, IEnumerable<FileSystemMetadata> fileSystemChildren, CancellationToken cancellationToken)
{
var newThemeVideos = LoadThemeVideos(fileSystemChildren, options.DirectoryService);
var newThemeVideoIds = newThemeVideos.Select(i => i.Id).ToArray();
var themeVideosChanged = !item.ThemeVideoIds.SequenceEqual(newThemeVideoIds);
var ownerId = item.Id;
var tasks = newThemeVideos.Select(i =>
var tasks = extras.Select(i =>
{
var subOptions = new MetadataRefreshOptions(options);
if (!i.ExtraType.HasValue ||
i.ExtraType.Value != Model.Entities.ExtraType.ThemeVideo ||
i.OwnerId != ownerId ||
!i.ParentId.Equals(Guid.Empty))
if (i.OwnerId != ownerId || i.ParentId != Guid.Empty)
{
i.ExtraType = Model.Entities.ExtraType.ThemeVideo;
i.OwnerId = ownerId;
i.ParentId = Guid.Empty;
subOptions.ForceSave = true;
@ -1656,48 +1494,9 @@ namespace MediaBrowser.Controller.Entities
await Task.WhenAll(tasks).ConfigureAwait(false);
// They are expected to be sorted by SortName
item.ThemeVideoIds = newThemeVideos.OrderBy(i => i.SortName).Select(i => i.Id).ToArray();
return themeVideosChanged;
}
/// <summary>
/// Refreshes the theme songs.
/// </summary>
private async Task<bool> RefreshThemeSongs(BaseItem item, MetadataRefreshOptions options, List<FileSystemMetadata> fileSystemChildren, CancellationToken cancellationToken)
{
var newThemeSongs = LoadThemeSongs(fileSystemChildren, options.DirectoryService);
var newThemeSongIds = newThemeSongs.Select(i => i.Id).ToArray();
var themeSongsChanged = !item.ThemeSongIds.SequenceEqual(newThemeSongIds);
var ownerId = item.Id;
var tasks = newThemeSongs.Select(i =>
{
var subOptions = new MetadataRefreshOptions(options);
item.ExtraIds = newExtraIds;
if (!i.ExtraType.HasValue ||
i.ExtraType.Value != Model.Entities.ExtraType.ThemeSong ||
i.OwnerId != ownerId ||
!i.ParentId.Equals(Guid.Empty))
{
i.ExtraType = Model.Entities.ExtraType.ThemeSong;
i.OwnerId = ownerId;
i.ParentId = Guid.Empty;
subOptions.ForceSave = true;
}
return RefreshMetadataForOwnedItem(i, true, subOptions, cancellationToken);
});
await Task.WhenAll(tasks).ConfigureAwait(false);
// They are expected to be sorted by SortName
item.ThemeSongIds = newThemeSongs.OrderBy(i => i.SortName).Select(i => i.Id).ToArray();
return themeSongsChanged;
return true;
}
public string GetPresentationUniqueKey()
@ -2891,14 +2690,14 @@ namespace MediaBrowser.Controller.Entities
StringComparison.OrdinalIgnoreCase);
}
public IEnumerable<BaseItem> GetThemeSongs()
public IReadOnlyList<BaseItem> GetThemeSongs()
{
return ThemeSongIds.Select(LibraryManager.GetItemById);
return GetExtras().Where(e => e.ExtraType == Model.Entities.ExtraType.ThemeSong).ToArray();
}
public IEnumerable<BaseItem> GetThemeVideos()
public IReadOnlyList<BaseItem> GetThemeVideos()
{
return ThemeVideoIds.Select(LibraryManager.GetItemById);
return GetExtras().Where(e => e.ExtraType == Model.Entities.ExtraType.ThemeVideo).ToArray();
}
/// <summary>

@ -2,7 +2,6 @@
#pragma warning disable CS1591
using System;
using System.Collections.Generic;
using MediaBrowser.Model.Entities;
@ -17,18 +16,10 @@ namespace MediaBrowser.Controller.Entities
IReadOnlyList<MediaUrl> RemoteTrailers { get; set; }
/// <summary>
/// Gets or sets the local trailer ids.
/// Gets the local trailers.
/// </summary>
/// <value>The local trailer ids.</value>
IReadOnlyList<Guid> LocalTrailerIds { get; set; }
/// <summary>
/// Gets or sets the remote trailer ids.
/// </summary>
/// <value>The remote trailer ids.</value>
IReadOnlyList<Guid> RemoteTrailerIds { get; set; }
Guid Id { get; set; }
/// <value>The local trailers.</value>
IReadOnlyList<BaseItem> LocalTrailers { get; }
}
/// <summary>
@ -42,57 +33,6 @@ namespace MediaBrowser.Controller.Entities
/// <param name="item">Media item.</param>
/// <returns><see cref="IReadOnlyList{Guid}" />.</returns>
public static int GetTrailerCount(this IHasTrailers item)
=> item.LocalTrailerIds.Count + item.RemoteTrailerIds.Count;
/// <summary>
/// Gets the trailer ids.
/// </summary>
/// <param name="item">Media item.</param>
/// <returns><see cref="IReadOnlyList{Guid}" />.</returns>
public static IReadOnlyList<Guid> GetTrailerIds(this IHasTrailers item)
{
var localIds = item.LocalTrailerIds;
var remoteIds = item.RemoteTrailerIds;
var all = new Guid[localIds.Count + remoteIds.Count];
var index = 0;
foreach (var id in localIds)
{
all[index++] = id;
}
foreach (var id in remoteIds)
{
all[index++] = id;
}
return all;
}
/// <summary>
/// Gets the trailers.
/// </summary>
/// <param name="item">Media item.</param>
/// <returns><see cref="IReadOnlyList{BaseItem}" />.</returns>
public static IReadOnlyList<BaseItem> GetTrailers(this IHasTrailers item)
{
var localIds = item.LocalTrailerIds;
var remoteIds = item.RemoteTrailerIds;
var libraryManager = BaseItem.LibraryManager;
var all = new BaseItem[localIds.Count + remoteIds.Count];
var index = 0;
foreach (var id in localIds)
{
all[index++] = libraryManager.GetItemById(id);
}
foreach (var id in remoteIds)
{
all[index++] = libraryManager.GetItemById(id);
}
return all;
}
=> item.LocalTrailers.Count + item.RemoteTrailers.Count;
}
}

@ -9,7 +9,6 @@ using System.Text.Json.Serialization;
using Jellyfin.Data.Entities;
using Jellyfin.Data.Enums;
using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.Querying;
namespace MediaBrowser.Controller.Entities.Movies
@ -21,10 +20,6 @@ namespace MediaBrowser.Controller.Entities.Movies
{
public BoxSet()
{
RemoteTrailers = Array.Empty<MediaUrl>();
LocalTrailerIds = Array.Empty<Guid>();
RemoteTrailerIds = Array.Empty<Guid>();
DisplayOrder = ItemSortBy.PremiereDate;
}
@ -38,10 +33,9 @@ namespace MediaBrowser.Controller.Entities.Movies
public override bool SupportsPeople => true;
/// <inheritdoc />
public IReadOnlyList<Guid> LocalTrailerIds { get; set; }
/// <inheritdoc />
public IReadOnlyList<Guid> RemoteTrailerIds { get; set; }
public IReadOnlyList<BaseItem> LocalTrailers => GetExtras()
.Where(extra => extra.ExtraType == Model.Entities.ExtraType.Trailer)
.ToArray();
/// <summary>
/// Gets or sets the display order.

@ -7,12 +7,9 @@ using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Text.Json.Serialization;
using System.Threading;
using System.Threading.Tasks;
using Jellyfin.Data.Enums;
using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.IO;
using MediaBrowser.Model.Providers;
namespace MediaBrowser.Controller.Entities.Movies
@ -22,22 +19,29 @@ namespace MediaBrowser.Controller.Entities.Movies
/// </summary>
public class Movie : Video, IHasSpecialFeatures, IHasTrailers, IHasLookupInfo<MovieInfo>, ISupportsBoxSetGrouping
{
public Movie()
{
SpecialFeatureIds = Array.Empty<Guid>();
RemoteTrailers = Array.Empty<MediaUrl>();
LocalTrailerIds = Array.Empty<Guid>();
RemoteTrailerIds = Array.Empty<Guid>();
}
private IReadOnlyList<Guid> _specialFeatureIds;
/// <inheritdoc />
public IReadOnlyList<Guid> SpecialFeatureIds { get; set; }
public IReadOnlyList<Guid> SpecialFeatureIds
{
get
{
return _specialFeatureIds ??= GetExtras()
.Where(extra => extra.ExtraType != Model.Entities.ExtraType.Trailer)
.Select(song => song.Id)
.ToArray();
}
/// <inheritdoc />
public IReadOnlyList<Guid> LocalTrailerIds { get; set; }
set
{
_specialFeatureIds = value;
}
}
/// <inheritdoc />
public IReadOnlyList<Guid> RemoteTrailerIds { get; set; }
public IReadOnlyList<BaseItem> LocalTrailers => GetExtras()
.Where(extra => extra.ExtraType == Model.Entities.ExtraType.Trailer)
.ToArray();
/// <summary>
/// Gets or sets the name of the TMDB collection.
@ -66,54 +70,6 @@ namespace MediaBrowser.Controller.Entities.Movies
return 2.0 / 3;
}
protected override async Task<bool> RefreshedOwnedItems(MetadataRefreshOptions options, List<FileSystemMetadata> fileSystemChildren, CancellationToken cancellationToken)
{
var hasChanges = await base.RefreshedOwnedItems(options, fileSystemChildren, cancellationToken).ConfigureAwait(false);
// Must have a parent to have special features
// In other words, it must be part of the Parent/Child tree
if (IsFileProtocol && SupportsOwnedItems && !IsInMixedFolder)
{
var specialFeaturesChanged = await RefreshSpecialFeatures(options, fileSystemChildren, cancellationToken).ConfigureAwait(false);
if (specialFeaturesChanged)
{
hasChanges = true;
}
}
return hasChanges;
}
private async Task<bool> RefreshSpecialFeatures(MetadataRefreshOptions options, List<FileSystemMetadata> fileSystemChildren, CancellationToken cancellationToken)
{
var newItems = LibraryManager.FindExtras(this, fileSystemChildren, options.DirectoryService).ToList();
var newItemIds = newItems.Select(i => i.Id).ToArray();
var itemsChanged = !SpecialFeatureIds.SequenceEqual(newItemIds);
var ownerId = Id;
var tasks = newItems.Select(i =>
{
var subOptions = new MetadataRefreshOptions(options);
if (i.OwnerId != ownerId)
{
i.OwnerId = ownerId;
subOptions.ForceSave = true;
}
return RefreshMetadataForOwnedItem(i, false, subOptions, cancellationToken);
});
await Task.WhenAll(tasks).ConfigureAwait(false);
SpecialFeatureIds = newItemIds;
return itemsChanged;
}
/// <inheritdoc />
public override UnratedItem GetBlockUnratedType()
{

@ -20,18 +20,10 @@ namespace MediaBrowser.Controller.Entities.TV
/// </summary>
public class Episode : Video, IHasTrailers, IHasLookupInfo<EpisodeInfo>, IHasSeries
{
public Episode()
{
RemoteTrailers = Array.Empty<MediaUrl>();
LocalTrailerIds = Array.Empty<Guid>();
RemoteTrailerIds = Array.Empty<Guid>();
}
/// <inheritdoc />
public IReadOnlyList<Guid> LocalTrailerIds { get; set; }
/// <inheritdoc />
public IReadOnlyList<Guid> RemoteTrailerIds { get; set; }
public IReadOnlyList<BaseItem> LocalTrailers => GetExtras()
.Where(extra => extra.ExtraType == Model.Entities.ExtraType.Trailer)
.ToArray();
/// <summary>
/// Gets or sets the season in which it aired.

@ -27,9 +27,6 @@ namespace MediaBrowser.Controller.Entities.TV
{
public Series()
{
RemoteTrailers = Array.Empty<MediaUrl>();
LocalTrailerIds = Array.Empty<Guid>();
RemoteTrailerIds = Array.Empty<Guid>();
AirDays = Array.Empty<DayOfWeek>();
}
@ -53,10 +50,9 @@ namespace MediaBrowser.Controller.Entities.TV
public override bool SupportsPeople => true;
/// <inheritdoc />
public IReadOnlyList<Guid> LocalTrailerIds { get; set; }
/// <inheritdoc />
public IReadOnlyList<Guid> RemoteTrailerIds { get; set; }
public IReadOnlyList<BaseItem> LocalTrailers => GetExtras()
.Where(extra => extra.ExtraType == Model.Entities.ExtraType.Trailer)
.ToArray();
/// <summary>
/// Gets or sets the display order.

@ -745,10 +745,9 @@ namespace MediaBrowser.Controller.Entities
var val = query.HasTrailer.Value;
var trailerCount = 0;
var hasTrailers = item as IHasTrailers;
if (hasTrailers != null)
if (item is IHasTrailers hasTrailers)
{
trailerCount = hasTrailers.GetTrailerIds().Count;
trailerCount = hasTrailers.GetTrailerCount();
}
var ok = val ? trailerCount > 0 : trailerCount == 0;
@ -763,7 +762,7 @@ namespace MediaBrowser.Controller.Entities
{
var filterValue = query.HasThemeSong.Value;
var themeCount = item.ThemeSongIds.Length;
var themeCount = item.GetThemeSongs().Count;
var ok = filterValue ? themeCount > 0 : themeCount == 0;
if (!ok)
@ -776,7 +775,7 @@ namespace MediaBrowser.Controller.Entities
{
var filterValue = query.HasThemeVideo.Value;
var themeCount = item.ThemeVideoIds.Length;
var themeCount = item.GetThemeVideos().Count;
var ok = filterValue ? themeCount > 0 : themeCount == 0;
if (!ok)

@ -6,7 +6,6 @@ using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using Emby.Naming.Common;
using Jellyfin.Data.Entities;
using Jellyfin.Data.Enums;
using MediaBrowser.Controller.Dto;
@ -426,29 +425,15 @@ namespace MediaBrowser.Controller.Library
/// <returns>Guid.</returns>
Guid GetNewItemId(string key, Type type);
/// <summary>
/// Finds the trailers.
/// </summary>
/// <param name="owner">The owner.</param>
/// <param name="fileSystemChildren">The file system children.</param>
/// <param name="directoryService">The directory service.</param>
/// <returns>IEnumerable&lt;Trailer&gt;.</returns>
IEnumerable<Video> FindTrailers(
BaseItem owner,
List<FileSystemMetadata> fileSystemChildren,
IDirectoryService directoryService);
/// <summary>
/// Finds the extras.
/// </summary>
/// <param name="owner">The owner.</param>
/// <param name="fileSystemChildren">The file system children.</param>
/// <param name="directoryService">The directory service.</param>
/// <returns>IEnumerable&lt;Video&gt;.</returns>
IEnumerable<Video> FindExtras(
BaseItem owner,
List<FileSystemMetadata> fileSystemChildren,
IDirectoryService directoryService);
List<FileSystemMetadata> fileSystemChildren);
/// <summary>
/// Gets the collection folders.

@ -106,7 +106,7 @@ namespace MediaBrowser.LocalMetadata.Images
{
if (!item.IsFileProtocol)
{
return Enumerable.Empty<FileSystemMetadata>();
yield break;
}
var path = item.ContainingFolderPath;
@ -114,20 +114,21 @@ namespace MediaBrowser.LocalMetadata.Images
// Exit if the cache dir does not exist, alternative solution is to create it, but that's a lot of empty dirs...
if (!Directory.Exists(path))
{
return Enumerable.Empty<FileSystemMetadata>();
yield break;
}
if (includeDirectories)
var files = directoryService.GetFileSystemEntries(path).OrderBy(i => Array.IndexOf(BaseItem.SupportedImageExtensions, i.Extension ?? string.Empty));
var count = BaseItem.SupportedImageExtensions.Length;
foreach (var file in files)
{
return directoryService.GetFileSystemEntries(path)
.Where(i => BaseItem.SupportedImageExtensions.Contains(i.Extension, StringComparer.OrdinalIgnoreCase) || i.IsDirectory)
.OrderBy(i => Array.IndexOf(BaseItem.SupportedImageExtensions, i.Extension ?? string.Empty));
for (var i = 0; i < count; i++)
{
if ((includeDirectories && file.IsDirectory) || string.Equals(BaseItem.SupportedImageExtensions[i], file.Extension, StringComparison.OrdinalIgnoreCase))
{
yield return file;
}
}
}
return directoryService.GetFiles(path)
.Where(i => BaseItem.SupportedImageExtensions.Contains(i.Extension, StringComparer.OrdinalIgnoreCase))
.OrderBy(i => Array.IndexOf(BaseItem.SupportedImageExtensions, i.Extension ?? string.Empty));
}
/// <inheritdoc />

@ -81,9 +81,7 @@ namespace Jellyfin.Naming.Tests.Video
private void Test(string input, ExtraType? expectedType)
{
var parser = GetExtraTypeParser(_videoOptions);
var extraType = parser.GetExtraInfo(input).ExtraType;
var extraType = ExtraResolver.GetExtraInfo(input, _videoOptions).ExtraType;
Assert.Equal(expectedType, extraType);
}
@ -93,14 +91,9 @@ namespace Jellyfin.Naming.Tests.Video
{
var rule = new ExtraRule(ExtraType.Unknown, ExtraRuleType.Regex, @"([eE]x(tra)?\.\w+)", MediaType.Video);
var options = new NamingOptions { VideoExtraRules = new[] { rule } };
var res = GetExtraTypeParser(options).GetExtraInfo("extra.mp4");
var res = ExtraResolver.GetExtraInfo("extra.mp4", options);
Assert.Equal(rule, res.Rule);
}
private ExtraResolver GetExtraTypeParser(NamingOptions videoOptions)
{
return new ExtraResolver(videoOptions);
}
}
}

@ -30,8 +30,8 @@ namespace Jellyfin.Naming.Tests.Video
}).ToList(),
_namingOptions).ToList();
Assert.Single(result);
Assert.Single(result[0].Extras);
Assert.Single(result.Where(v => v.ExtraType == null));
Assert.Single(result.Where(v => v.ExtraType != null));
}
[Fact]
@ -53,8 +53,8 @@ namespace Jellyfin.Naming.Tests.Video
}).ToList(),
_namingOptions).ToList();
Assert.Single(result);
Assert.Single(result[0].Extras);
Assert.Single(result.Where(v => v.ExtraType == null));
Assert.Single(result.Where(v => v.ExtraType != null));
Assert.Equal(2, result[0].AlternateVersions.Count);
}
@ -102,7 +102,6 @@ namespace Jellyfin.Naming.Tests.Video
_namingOptions).ToList();
Assert.Equal(7, result.Count);
Assert.Empty(result[0].Extras);
Assert.Empty(result[0].AlternateVersions);
}
@ -130,7 +129,6 @@ namespace Jellyfin.Naming.Tests.Video
_namingOptions).ToList();
Assert.Single(result);
Assert.Empty(result[0].Extras);
Assert.Equal(7, result[0].AlternateVersions.Count);
}
@ -159,7 +157,6 @@ namespace Jellyfin.Naming.Tests.Video
_namingOptions).ToList();
Assert.Equal(9, result.Count);
Assert.Empty(result[0].Extras);
Assert.Empty(result[0].AlternateVersions);
}
@ -184,7 +181,6 @@ namespace Jellyfin.Naming.Tests.Video
_namingOptions).ToList();
Assert.Equal(5, result.Count);
Assert.Empty(result[0].Extras);
Assert.Empty(result[0].AlternateVersions);
}
@ -211,7 +207,6 @@ namespace Jellyfin.Naming.Tests.Video
_namingOptions).ToList();
Assert.Equal(5, result.Count);
Assert.Empty(result[0].Extras);
Assert.Empty(result[0].AlternateVersions);
}
@ -239,7 +234,6 @@ namespace Jellyfin.Naming.Tests.Video
_namingOptions).ToList();
Assert.Single(result);
Assert.Empty(result[0].Extras);
Assert.Equal(7, result[0].AlternateVersions.Count);
Assert.False(result[0].AlternateVersions[2].Is3D);
Assert.True(result[0].AlternateVersions[3].Is3D);
@ -270,7 +264,6 @@ namespace Jellyfin.Naming.Tests.Video
_namingOptions).ToList();
Assert.Single(result);
Assert.Empty(result[0].Extras);
Assert.Equal(7, result[0].AlternateVersions.Count);
Assert.False(result[0].AlternateVersions[3].Is3D);
Assert.True(result[0].AlternateVersions[4].Is3D);
@ -320,7 +313,6 @@ namespace Jellyfin.Naming.Tests.Video
_namingOptions).ToList();
Assert.Equal(7, result.Count);
Assert.Empty(result[0].Extras);
Assert.Empty(result[0].AlternateVersions);
}
@ -347,7 +339,6 @@ namespace Jellyfin.Naming.Tests.Video
_namingOptions).ToList();
Assert.Equal(5, result.Count);
Assert.Empty(result[0].Extras);
Assert.Empty(result[0].AlternateVersions);
}
@ -369,7 +360,6 @@ namespace Jellyfin.Naming.Tests.Video
_namingOptions).ToList();
Assert.Single(result);
Assert.Empty(result[0].Extras);
Assert.Single(result[0].AlternateVersions);
}
@ -391,7 +381,6 @@ namespace Jellyfin.Naming.Tests.Video
_namingOptions).ToList();
Assert.Single(result);
Assert.Empty(result[0].Extras);
Assert.Single(result[0].AlternateVersions);
}
@ -413,7 +402,6 @@ namespace Jellyfin.Naming.Tests.Video
_namingOptions).ToList();
Assert.Single(result);
Assert.Empty(result[0].Extras);
Assert.Single(result[0].AlternateVersions);
}

@ -22,9 +22,7 @@ namespace Jellyfin.Naming.Tests.Video
"Bad Boys (2006)-trailer.mkv"
};
var resolver = GetResolver();
var result = resolver.ResolveFiles(files).ToList();
var result = StackResolver.ResolveFiles(files, _namingOptions).ToList();
Assert.Single(result);
TestStackInfo(result[0], "Bad Boys (2006)", 4);
@ -39,9 +37,7 @@ namespace Jellyfin.Naming.Tests.Video
"Bad Boys (2007).mkv"
};
var resolver = GetResolver();
var result = resolver.ResolveFiles(files).ToList();
var result = StackResolver.ResolveFiles(files, _namingOptions).ToList();
Assert.Empty(result);
}
@ -55,9 +51,7 @@ namespace Jellyfin.Naming.Tests.Video
"Bad Boys 2007.mkv"
};
var resolver = GetResolver();
var result = resolver.ResolveFiles(files).ToList();
var result = StackResolver.ResolveFiles(files, _namingOptions).ToList();
Assert.Empty(result);
}
@ -71,9 +65,7 @@ namespace Jellyfin.Naming.Tests.Video
"300 (2007).mkv"
};
var resolver = GetResolver();
var result = resolver.ResolveFiles(files).ToList();
var result = StackResolver.ResolveFiles(files, _namingOptions).ToList();
Assert.Empty(result);
}
@ -87,9 +79,7 @@ namespace Jellyfin.Naming.Tests.Video
"300 2007.mkv"
};
var resolver = GetResolver();
var result = resolver.ResolveFiles(files).ToList();
var result = StackResolver.ResolveFiles(files, _namingOptions).ToList();
Assert.Empty(result);
}
@ -103,9 +93,7 @@ namespace Jellyfin.Naming.Tests.Video
"Star Trek 2- The wrath of khan.mkv"
};
var resolver = GetResolver();
var result = resolver.ResolveFiles(files).ToList();
var result = StackResolver.ResolveFiles(files, _namingOptions).ToList();
Assert.Empty(result);
}
@ -119,9 +107,7 @@ namespace Jellyfin.Naming.Tests.Video
"Red Riding in the Year of Our Lord 1974 (2009).mkv"
};
var resolver = GetResolver();
var result = resolver.ResolveFiles(files).ToList();
var result = StackResolver.ResolveFiles(files, _namingOptions).ToList();
Assert.Empty(result);
}
@ -135,9 +121,7 @@ namespace Jellyfin.Naming.Tests.Video
"d:/movies/300 2006 part2.mkv"
};
var resolver = GetResolver();
var result = resolver.ResolveFiles(files).ToList();
var result = StackResolver.ResolveFiles(files, _namingOptions).ToList();
Assert.Single(result);
TestStackInfo(result[0], "300 2006", 2);
@ -155,9 +139,7 @@ namespace Jellyfin.Naming.Tests.Video
"Bad Boys (2006)-trailer.mkv"
};
var resolver = GetResolver();
var result = resolver.ResolveFiles(files).ToList();
var result = StackResolver.ResolveFiles(files, _namingOptions).ToList();
Assert.Single(result);
TestStackInfo(result[0], "Bad Boys (2006).stv.unrated.multi.1080p.bluray.x264-rough", 4);
@ -175,9 +157,7 @@ namespace Jellyfin.Naming.Tests.Video
"Bad Boys (2006)-trailer.mkv"
};
var resolver = GetResolver();
var result = resolver.ResolveFiles(files).ToList();
var result = StackResolver.ResolveFiles(files, _namingOptions).ToList();
Assert.Empty(result);
}
@ -194,9 +174,7 @@ namespace Jellyfin.Naming.Tests.Video
"300 (2006)-trailer.mkv"
};
var resolver = GetResolver();
var result = resolver.ResolveFiles(files).ToList();
var result = StackResolver.ResolveFiles(files, _namingOptions).ToList();
Assert.Single(result);
TestStackInfo(result[0], "300 (2006)", 4);
@ -214,9 +192,7 @@ namespace Jellyfin.Naming.Tests.Video
"Bad Boys (2006)-trailer.mkv"
};
var resolver = GetResolver();
var result = resolver.ResolveFiles(files).ToList();
var result = StackResolver.ResolveFiles(files, _namingOptions).ToList();
Assert.Single(result);
TestStackInfo(result[0], "Bad Boys (2006)", 3);
@ -238,9 +214,7 @@ namespace Jellyfin.Naming.Tests.Video
"300 (2006)-trailer.mkv"
};
var resolver = GetResolver();
var result = resolver.ResolveFiles(files).ToList();
var result = StackResolver.ResolveFiles(files, _namingOptions).ToList();
Assert.Equal(2, result.Count);
TestStackInfo(result[1], "Bad Boys (2006)", 4);
@ -256,9 +230,7 @@ namespace Jellyfin.Naming.Tests.Video
"blah blah - cd 2"
};
var resolver = GetResolver();
var result = resolver.ResolveDirectories(files).ToList();
var result = StackResolver.ResolveDirectories(files, _namingOptions).ToList();
Assert.Single(result);
TestStackInfo(result[0], "blah blah", 2);
@ -275,9 +247,7 @@ namespace Jellyfin.Naming.Tests.Video
"300-trailer.mkv"
};
var resolver = GetResolver();
var result = resolver.ResolveFiles(files).ToList();
var result = StackResolver.ResolveFiles(files, _namingOptions).ToList();
Assert.Single(result);
@ -297,9 +267,7 @@ namespace Jellyfin.Naming.Tests.Video
"Avengers part3.mkv"
};
var resolver = GetResolver();
var result = resolver.ResolveFiles(files).ToList();
var result = StackResolver.ResolveFiles(files, _namingOptions).ToList();
Assert.Equal(2, result.Count);
@ -328,9 +296,7 @@ namespace Jellyfin.Naming.Tests.Video
"300-trailer.mkv"
};
var resolver = GetResolver();
var result = resolver.ResolveFiles(files).ToList();
var result = StackResolver.ResolveFiles(files, _namingOptions).ToList();
Assert.Equal(3, result.Count);
@ -354,9 +320,7 @@ namespace Jellyfin.Naming.Tests.Video
"300 (2006)-trailer.mkv"
};
var resolver = GetResolver();
var result = resolver.ResolveFiles(files).ToList();
var result = StackResolver.ResolveFiles(files, _namingOptions).ToList();
Assert.Single(result);
@ -375,9 +339,7 @@ namespace Jellyfin.Naming.Tests.Video
new FileSystemMetadata { FullName = "300 (2006) part1", IsDirectory = true }
};
var resolver = GetResolver();
var result = resolver.Resolve(files).ToList();
var result = StackResolver.Resolve(files, _namingOptions).ToList();
Assert.Equal(2, result.Count);
TestStackInfo(result[0], "300 (2006)", 3);
@ -397,9 +359,7 @@ namespace Jellyfin.Naming.Tests.Video
"Harry Potter and the Deathly Hallows 4.mkv"
};
var resolver = GetResolver();
var result = resolver.ResolveFiles(files).ToList();
var result = StackResolver.ResolveFiles(files, _namingOptions).ToList();
Assert.Empty(result);
}
@ -414,9 +374,7 @@ namespace Jellyfin.Naming.Tests.Video
"Neverland (2011)[720p][PG][Voted 6.5][Family-Fantasy]part2.mkv"
};
var resolver = GetResolver();
var result = resolver.ResolveFiles(files).ToList();
var result = StackResolver.ResolveFiles(files, _namingOptions).ToList();
Assert.Single(result);
Assert.Equal(2, result[0].Files.Count);
@ -432,9 +390,7 @@ namespace Jellyfin.Naming.Tests.Video
@"M:/Movies (DVD)/Movies (Musical)/The Sound of Music/The Sound of Music (1965) (Disc 02)"
};
var resolver = GetResolver();
var result = resolver.ResolveDirectories(files).ToList();
var result = StackResolver.ResolveDirectories(files, _namingOptions).ToList();
Assert.Single(result);
Assert.Equal(2, result[0].Files.Count);
@ -445,10 +401,5 @@ namespace Jellyfin.Naming.Tests.Video
Assert.Equal(fileCount, stack.Files.Count);
Assert.Equal(name, stack.Name);
}
private StackResolver GetResolver()
{
return new StackResolver(_namingOptions);
}
}
}

@ -2,6 +2,7 @@ using System;
using System.Linq;
using Emby.Naming.Common;
using Emby.Naming.Video;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.IO;
using Xunit;
@ -48,16 +49,25 @@ namespace Jellyfin.Naming.Tests.Video
}).ToList(),
_namingOptions).ToList();
Assert.Equal(5, result.Count);
Assert.Equal(11, result.Count);
var batman = result.FirstOrDefault(x => string.Equals(x.Name, "Batman", StringComparison.Ordinal));
Assert.NotNull(batman);
Assert.Equal(3, batman!.Files.Count);
Assert.Equal(3, batman!.Extras.Count);
var harry = result.FirstOrDefault(x => string.Equals(x.Name, "Harry Potter and the Deathly Hallows", StringComparison.Ordinal));
Assert.NotNull(harry);
Assert.Equal(4, harry!.Files.Count);
Assert.Equal(2, harry!.Extras.Count);
Assert.False(result[2].ExtraType.HasValue);
Assert.Equal(ExtraType.Trailer, result[3].ExtraType);
Assert.Equal(ExtraType.Trailer, result[4].ExtraType);
Assert.Equal(ExtraType.DeletedScene, result[5].ExtraType);
Assert.Equal(ExtraType.Sample, result[6].ExtraType);
Assert.Equal(ExtraType.Trailer, result[7].ExtraType);
Assert.Equal(ExtraType.Trailer, result[8].ExtraType);
Assert.Equal(ExtraType.Trailer, result[9].ExtraType);
Assert.Equal(ExtraType.Trailer, result[10].ExtraType);
}
[Fact]
@ -97,7 +107,8 @@ namespace Jellyfin.Naming.Tests.Video
}).ToList(),
_namingOptions).ToList();
Assert.Single(result);
Assert.False(result[0].ExtraType.HasValue);
Assert.Equal(ExtraType.Trailer, result[1].ExtraType);
}
[Fact]
@ -117,7 +128,8 @@ namespace Jellyfin.Naming.Tests.Video
}).ToList(),
_namingOptions).ToList();
Assert.Single(result);
Assert.False(result[0].ExtraType.HasValue);
Assert.Equal(ExtraType.Trailer, result[1].ExtraType);
}
[Fact]
@ -138,15 +150,18 @@ namespace Jellyfin.Naming.Tests.Video
}).ToList(),
_namingOptions).ToList();
Assert.Single(result);
Assert.False(result[0].ExtraType.HasValue);
Assert.Equal(ExtraType.Trailer, result[1].ExtraType);
Assert.Equal(ExtraType.Trailer, result[2].ExtraType);
}
[Fact]
public void TestDifferentNames()
public void Resolve_SameNameAndYear_ReturnsSingleItem()
{
var files = new[]
{
"Looper (2012)-trailer.mkv",
"Looper 2012-trailer.mkv",
"Looper.2012.bluray.720p.x264.mkv"
};
@ -158,7 +173,30 @@ namespace Jellyfin.Naming.Tests.Video
}).ToList(),
_namingOptions).ToList();
Assert.Single(result);
Assert.False(result[0].ExtraType.HasValue);
Assert.Equal(ExtraType.Trailer, result[1].ExtraType);
Assert.Equal(ExtraType.Trailer, result[2].ExtraType);
}
[Fact]
public void Resolve_TrailerMatchesFolderName_ReturnsSingleItem()
{
var files = new[]
{
"/movies/Looper (2012)/Looper (2012)-trailer.mkv",
"/movies/Looper (2012)/Looper.bluray.720p.x264.mkv"
};
var result = VideoListResolver.Resolve(
files.Select(i => new FileSystemMetadata
{
IsDirectory = false,
FullName = i
}).ToList(),
_namingOptions).ToList();
Assert.False(result[0].ExtraType.HasValue);
Assert.Equal(ExtraType.Trailer, result[1].ExtraType);
}
[Fact]
@ -233,27 +271,7 @@ namespace Jellyfin.Naming.Tests.Video
{
@"No (2012) part1.mp4",
@"No (2012) part2.mp4",
@"No (2012) part1-trailer.mp4"
};
var result = VideoListResolver.Resolve(
files.Select(i => new FileSystemMetadata
{
IsDirectory = false,
FullName = i
}).ToList(),
_namingOptions).ToList();
Assert.Single(result);
}
[Fact]
public void TestStackedWithTrailer2()
{
var files = new[]
{
@"No (2012) part1.mp4",
@"No (2012) part2.mp4",
@"No (2012) part1-trailer.mp4",
@"No (2012)-trailer.mp4"
};
@ -265,7 +283,10 @@ namespace Jellyfin.Naming.Tests.Video
}).ToList(),
_namingOptions).ToList();
Assert.Single(result);
Assert.Equal(3, result.Count);
Assert.False(result[0].ExtraType.HasValue);
Assert.Equal(ExtraType.Trailer, result[1].ExtraType);
Assert.Equal(ExtraType.Trailer, result[2].ExtraType);
}
[Fact]
@ -276,7 +297,7 @@ namespace Jellyfin.Naming.Tests.Video
@"/Movies/Top Gun (1984)/movie.mp4",
@"/Movies/Top Gun (1984)/Top Gun (1984)-trailer.mp4",
@"/Movies/Top Gun (1984)/Top Gun (1984)-trailer2.mp4",
@"trailer.mp4"
@"/Movies/trailer.mp4"
};
var result = VideoListResolver.Resolve(
@ -287,7 +308,10 @@ namespace Jellyfin.Naming.Tests.Video
}).ToList(),
_namingOptions).ToList();
Assert.Single(result);
Assert.False(result[0].ExtraType.HasValue);
Assert.Equal(ExtraType.Trailer, result[1].ExtraType);
Assert.Equal(ExtraType.Trailer, result[2].ExtraType);
Assert.Equal(ExtraType.Trailer, result[3].ExtraType);
}
[Fact]
@ -396,7 +420,7 @@ namespace Jellyfin.Naming.Tests.Video
var files = new[]
{
@"/Server/Despicable Me/Despicable Me (2010).mkv",
@"/Server/Despicable Me/movie-trailer.mkv"
@"/Server/Despicable Me/trailer.mkv"
};
var result = VideoListResolver.Resolve(
@ -407,18 +431,17 @@ namespace Jellyfin.Naming.Tests.Video
}).ToList(),
_namingOptions).ToList();
Assert.Single(result);
Assert.False(result[0].ExtraType.HasValue);
Assert.Equal(ExtraType.Trailer, result[1].ExtraType);
}
[Fact]
public void TestTrailerFalsePositives()
public void Resolve_TrailerInTrailersFolder_ReturnsCorrectExtraType()
{
var files = new[]
{
@"/Server/Despicable Me/Skyscraper (2018) - Big Game Spot.mkv",
@"/Server/Despicable Me/Skyscraper (2018) - Trailer.mkv",
@"/Server/Despicable Me/Baywatch (2017) - Big Game Spot.mkv",
@"/Server/Despicable Me/Baywatch (2017) - Trailer.mkv"
@"/Server/Despicable Me/Despicable Me (2010).mkv",
@"/Server/Despicable Me/trailers/some title.mkv"
};
var result = VideoListResolver.Resolve(
@ -429,7 +452,8 @@ namespace Jellyfin.Naming.Tests.Video
}).ToList(),
_namingOptions).ToList();
Assert.Equal(4, result.Count);
Assert.False(result[0].ExtraType.HasValue);
Assert.Equal(ExtraType.Trailer, result[1].ExtraType);
}
[Fact]
@ -449,7 +473,8 @@ namespace Jellyfin.Naming.Tests.Video
}).ToList(),
_namingOptions).ToList();
Assert.Single(result);
Assert.False(result[0].ExtraType.HasValue);
Assert.Equal(ExtraType.Trailer, result[1].ExtraType);
}
[Fact]

@ -1,4 +1,5 @@
using Emby.Server.Implementations.Library.Resolvers.TV;
using Emby.Naming.Common;
using Emby.Server.Implementations.Library.Resolvers.TV;
using MediaBrowser.Controller;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Entities.TV;
@ -13,12 +14,14 @@ namespace Jellyfin.Server.Implementations.Tests.Library
{
public class EpisodeResolverTest
{
private static readonly NamingOptions _namingOptions = new ();
[Fact]
public void Resolve_GivenVideoInExtrasFolder_DoesNotResolveToEpisode()
{
var parent = new Folder { Name = "extras" };
var episodeResolver = new EpisodeResolver(null);
var episodeResolver = new EpisodeResolver(_namingOptions);
var itemResolveArgs = new ItemResolveArgs(
Mock.Of<IServerApplicationPaths>(),
Mock.Of<IDirectoryService>())
@ -41,14 +44,14 @@ namespace Jellyfin.Server.Implementations.Tests.Library
// Have to create a mock because of moq proxies not being castable to a concrete implementation
// https://github.com/jellyfin/jellyfin/blob/ab0cff8556403e123642dc9717ba778329554634/Emby.Server.Implementations/Library/Resolvers/BaseVideoResolver.cs#L48
var episodeResolver = new EpisodeResolverMock();
var episodeResolver = new EpisodeResolverMock(_namingOptions);
var itemResolveArgs = new ItemResolveArgs(
Mock.Of<IServerApplicationPaths>(),
Mock.Of<IDirectoryService>())
{
Parent = series,
CollectionType = CollectionType.TvShows,
FileInfo = new FileSystemMetadata()
FileInfo = new FileSystemMetadata
{
FullName = "Extras/Extras S01E01.mkv"
}
@ -58,7 +61,7 @@ namespace Jellyfin.Server.Implementations.Tests.Library
private class EpisodeResolverMock : EpisodeResolver
{
public EpisodeResolverMock() : base(null)
public EpisodeResolverMock(NamingOptions namingOptions) : base(namingOptions)
{
}

@ -0,0 +1,178 @@
using System.Collections.Generic;
using System.IO;
using System.Linq;
using AutoFixture;
using AutoFixture.AutoMoq;
using Emby.Naming.Common;
using Emby.Server.Implementations.Library.Resolvers;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Entities.Movies;
using MediaBrowser.Controller.Entities.TV;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Resolvers;
using MediaBrowser.Controller.Sorting;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.IO;
using Moq;
using Xunit;
namespace Jellyfin.Server.Implementations.Tests.Library.LibraryManager;
public class FindExtrasTests
{
private readonly Emby.Server.Implementations.Library.LibraryManager _libraryManager;
public FindExtrasTests()
{
var fixture = new Fixture().Customize(new AutoMoqCustomization());
fixture.Register(() => new NamingOptions());
var configMock = fixture.Freeze<Mock<IServerConfigurationManager>>();
configMock.Setup(c => c.ApplicationPaths.ProgramDataPath).Returns("/data");
var fileSystemMock = fixture.Freeze<Mock<IFileSystem>>();
fileSystemMock.Setup(f => f.GetFileInfo(It.IsAny<string>())).Returns<string>(path => new FileSystemMetadata { FullName = path });
_libraryManager = fixture.Build<Emby.Server.Implementations.Library.LibraryManager>().Do(s => s.AddParts(
fixture.Create<IEnumerable<IResolverIgnoreRule>>(),
new List<IItemResolver> { new GenericVideoResolver<Video>(fixture.Create<NamingOptions>()) },
fixture.Create<IEnumerable<IIntroProvider>>(),
fixture.Create<IEnumerable<IBaseItemComparer>>(),
fixture.Create<IEnumerable<ILibraryPostScanTask>>()))
.Create();
// This is pretty terrible but unavoidable
BaseItem.FileSystem ??= fixture.Create<IFileSystem>();
BaseItem.MediaSourceManager ??= fixture.Create<IMediaSourceManager>();
}
[Fact]
public void FindExtras_SeparateMovieFolder_FindsCorrectExtras()
{
var owner = new Movie { Name = "Up", Path = "/movies/Up/Up.mkv" };
var paths = new List<string>
{
"/movies/Up/Up.mkv",
"/movies/Up/Up - trailer.mkv",
"/movies/Up/Up - sample.mkv",
"/movies/Up/Up something else.mkv"
};
var files = paths.Select(p => new FileSystemMetadata
{
FullName = p,
IsDirectory = false
}).ToList();
var extras = _libraryManager.FindExtras(owner, files).OrderBy(e => e.ExtraType).ToList();
Assert.Equal(2, extras.Count);
Assert.Equal(ExtraType.Trailer, extras[0].ExtraType);
Assert.Equal(ExtraType.Sample, extras[1].ExtraType);
}
[Fact]
public void FindExtras_SeparateMovieFolderWithMixedExtras_FindsCorrectExtras()
{
var owner = new Movie { Name = "Up", Path = "/movies/Up/Up.mkv" };
var paths = new List<string>
{
"/movies/Up/Up.mkv",
"/movies/Up/Up - trailer.mkv",
"/movies/Up/trailers/some trailer.mkv",
"/movies/Up/behind the scenes/the making of Up.mkv",
"/movies/Up/behind the scenes.mkv",
"/movies/Up/Up - sample.mkv",
"/movies/Up/Up something else.mkv"
};
var files = paths.Select(p => new FileSystemMetadata
{
FullName = p,
IsDirectory = false
}).ToList();
var extras = _libraryManager.FindExtras(owner, files).OrderBy(e => e.ExtraType).ToList();
Assert.Equal(4, extras.Count);
Assert.Equal(ExtraType.Trailer, extras[0].ExtraType);
Assert.Equal(ExtraType.Trailer, extras[1].ExtraType);
Assert.Equal(ExtraType.BehindTheScenes, extras[2].ExtraType);
Assert.Equal(ExtraType.Sample, extras[3].ExtraType);
}
[Fact]
public void FindExtras_SeparateMovieFolderWithMixedExtras_FindsOnlyExtrasInMovieFolder()
{
var owner = new Movie { Name = "Up", Path = "/movies/Up/Up.mkv" };
var paths = new List<string>
{
"/movies/Up/Up.mkv",
"/movies/Up/trailer.mkv",
"/movies/Another Movie/trailer.mkv"
};
var files = paths.Select(p => new FileSystemMetadata
{
FullName = p,
IsDirectory = false
}).ToList();
var extras = _libraryManager.FindExtras(owner, files).OrderBy(e => e.ExtraType).ToList();
Assert.Single(extras);
Assert.Equal(ExtraType.Trailer, extras[0].ExtraType);
Assert.Equal("trailer", extras[0].FileNameWithoutExtension);
Assert.Equal("/movies/Up/trailer.mkv", extras[0].Path);
}
[Fact]
public void FindExtras_SeparateMovieFolderWithParts_FindsCorrectExtras()
{
var owner = new Movie { Name = "Up", Path = "/movies/Up/Up - part1.mkv" };
var paths = new List<string>
{
"/movies/Up/Up - part1.mkv",
"/movies/Up/Up - part2.mkv",
"/movies/Up/trailer.mkv",
"/movies/Another Movie/trailer.mkv"
};
var files = paths.Select(p => new FileSystemMetadata
{
FullName = p,
IsDirectory = false
}).ToList();
var extras = _libraryManager.FindExtras(owner, files).OrderBy(e => e.ExtraType).ToList();
Assert.Single(extras);
Assert.Equal(ExtraType.Trailer, extras[0].ExtraType);
Assert.Equal("trailer", extras[0].FileNameWithoutExtension);
Assert.Equal("/movies/Up/trailer.mkv", extras[0].Path);
}
[Fact]
public void FindExtras_SeriesWithTrailers_FindsCorrectExtras()
{
var owner = new Series { Name = "Dexter", Path = "/series/Dexter" };
var paths = new List<string>
{
"/series/Dexter/Season 1/S01E01.mkv",
"/series/Dexter/trailer.mkv",
"/series/Dexter/trailers/trailer2.mkv",
};
var files = paths.Select(p => new FileSystemMetadata
{
FullName = p,
IsDirectory = string.IsNullOrEmpty(Path.GetExtension(p))
}).ToList();
var extras = _libraryManager.FindExtras(owner, files).OrderBy(e => e.ExtraType).ToList();
Assert.Equal(2, extras.Count);
Assert.Equal(ExtraType.Trailer, extras[0].ExtraType);
Assert.Equal("trailer", extras[0].FileNameWithoutExtension);
Assert.Equal("/series/Dexter/trailer.mkv", extras[0].Path);
Assert.Equal("/series/Dexter/trailers/trailer2.mkv", extras[1].Path);
}
}
Loading…
Cancel
Save