Refactor extras parsing

This commit is contained in:
cvium 2021-12-07 15:18:17 +01:00
parent 9cafa2cab4
commit fde84a1e00
28 changed files with 689 additions and 1005 deletions

View File

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

View File

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

View File

@ -1,5 +1,7 @@
using System; using System;
using System.Collections.Generic;
using System.IO; using System.IO;
using System.Linq;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
using Emby.Naming.Audio; using Emby.Naming.Audio;
using Emby.Naming.Common; using Emby.Naming.Common;
@ -9,46 +11,28 @@ namespace Emby.Naming.Video
/// <summary> /// <summary>
/// Resolve if file is extra for video. /// Resolve if file is extra for video.
/// </summary> /// </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 static readonly char[] _digits = { '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;
}
/// <summary> /// <summary>
/// Attempts to resolve if file is extra. /// Attempts to resolve if file is extra.
/// </summary> /// </summary>
/// <param name="path">Path to file.</param> /// <param name="path">Path to file.</param>
/// <param name="namingOptions">The naming options.</param>
/// <returns>Returns <see cref="ExtraResult"/> object.</returns> /// <returns>Returns <see cref="ExtraResult"/> object.</returns>
public ExtraResult GetExtraInfo(string path) public static ExtraResult GetExtraInfo(string path, NamingOptions namingOptions)
{ {
var result = new ExtraResult(); 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]; var rule = namingOptions.VideoExtraRules[i];
if (rule.MediaType == MediaType.Audio) if ((rule.MediaType == MediaType.Audio && !AudioFileParser.IsAudioFile(path, namingOptions))
{ || (rule.MediaType == MediaType.Video && !VideoResolver.IsVideoFile(path, namingOptions)))
if (!AudioFileParser.IsAudioFile(path, _options))
{ {
continue; continue;
} }
}
else if (rule.MediaType == MediaType.Video)
{
if (!VideoResolver.IsVideoFile(path, _options))
{
continue;
}
}
var pathSpan = path.AsSpan(); var pathSpan = path.AsSpan();
if (rule.RuleType == ExtraRuleType.Filename) if (rule.RuleType == ExtraRuleType.Filename)
@ -76,7 +60,7 @@ namespace Emby.Naming.Video
{ {
var filename = Path.GetFileName(path); 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)) if (regex.IsMatch(filename))
{ {
@ -102,5 +86,68 @@ namespace Emby.Naming.Video
return result; 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);
}
} }
} }

View File

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

View File

@ -12,37 +12,28 @@ namespace Emby.Naming.Video
/// <summary> /// <summary>
/// Resolve <see cref="FileStack"/> from list of paths. /// Resolve <see cref="FileStack"/> from list of paths.
/// </summary> /// </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> /// <summary>
/// Resolves only directories from paths. /// Resolves only directories from paths.
/// </summary> /// </summary>
/// <param name="files">List of paths.</param> /// <param name="files">List of paths.</param>
/// <param name="namingOptions">The naming options.</param>
/// <returns>Enumerable <see cref="FileStack"/> of directories.</returns> /// <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> /// <summary>
/// Resolves only files from paths. /// Resolves only files from paths.
/// </summary> /// </summary>
/// <param name="files">List of paths.</param> /// <param name="files">List of paths.</param>
/// <param name="namingOptions">The naming options.</param>
/// <returns>Enumerable <see cref="FileStack"/> of files.</returns> /// <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> /// <summary>
@ -50,7 +41,7 @@ namespace Emby.Naming.Video
/// </summary> /// </summary>
/// <param name="files">List of paths.</param> /// <param name="files">List of paths.</param>
/// <returns>Enumerable <see cref="FileStack"/> of directories.</returns> /// <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)); var groupedDirectoryFiles = files.GroupBy(file => Path.GetDirectoryName(file.Path));
@ -82,15 +73,20 @@ namespace Emby.Naming.Video
/// Resolves videos from paths. /// Resolves videos from paths.
/// </summary> /// </summary>
/// <param name="files">List of paths.</param> /// <param name="files">List of paths.</param>
/// <param name="namingOptions">The naming options.</param>
/// <returns>Enumerable <see cref="FileStack"/> of videos.</returns> /// <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 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) .OrderBy(i => i.FullName)
.Select(f => (f.IsDirectory, FileName: GetFileNameWithExtension(f), f.FullName))
.ToList(); .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++) for (var i = 0; i < list.Count; i++)
{ {
@ -102,17 +98,17 @@ namespace Emby.Naming.Video
while (expressionIndex < expressions.Length) while (expressionIndex < expressions.Length)
{ {
var exp = expressions[expressionIndex]; var exp = expressions[expressionIndex];
var stack = new FileStack(); FileStack? stack = null;
// (Title)(Volume)(Ignore)(Extension) // (Title)(Volume)(Ignore)(Extension)
var match1 = FindMatch(file1, exp, offset); var match1 = FindMatch(file1.FileName, exp, offset, cache);
if (match1.Success) if (match1.Success)
{ {
var title1 = match1.Groups["title"].Value; var title1 = match1.Groups[1].Value;
var volume1 = match1.Groups["volume"].Value; var volume1 = match1.Groups[2].Value;
var ignore1 = match1.Groups["ignore"].Value; var ignore1 = match1.Groups[3].Value;
var extension1 = match1.Groups["extension"].Value; var extension1 = match1.Groups[4].Value;
var j = i + 1; var j = i + 1;
while (j < list.Count) while (j < list.Count)
@ -126,7 +122,7 @@ namespace Emby.Naming.Video
} }
// (Title)(Volume)(Ignore)(Extension) // (Title)(Volume)(Ignore)(Extension)
var match2 = FindMatch(file2, exp, offset); var match2 = FindMatch(file2.FileName, exp, offset, cache);
if (match2.Success) if (match2.Success)
{ {
@ -142,6 +138,7 @@ namespace Emby.Naming.Video
if (string.Equals(ignore1, ignore2, StringComparison.OrdinalIgnoreCase) if (string.Equals(ignore1, ignore2, StringComparison.OrdinalIgnoreCase)
&& string.Equals(extension1, extension2, StringComparison.OrdinalIgnoreCase)) && string.Equals(extension1, extension2, StringComparison.OrdinalIgnoreCase))
{ {
stack ??= new FileStack();
if (stack.Files.Count == 0) if (stack.Files.Count == 0)
{ {
stack.Name = title1 + ignore1; stack.Name = title1 + ignore1;
@ -204,7 +201,7 @@ namespace Emby.Naming.Video
expressionIndex++; expressionIndex++;
} }
if (stack.Files.Count > 1) if (stack?.Files.Count > 1)
{ {
yield return stack; yield return stack;
i += stack.Files.Count - 1; 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 // For directories, dummy up an extension otherwise the expressions will fail
var input = !file.IsDirectory var input = file.FullName;
? file.FullName if (file.IsDirectory)
: file.FullName + ".mkv"; {
input = Path.ChangeExtension(input, "mkv");
}
return Path.GetFileName(input); 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 >= input.Length)
if (offset < 0 || offset >= regexInput.Length)
{ {
return Match.Empty; 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;
} }
} }
} }

View File

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

View File

@ -4,7 +4,6 @@ using System.IO;
using System.Linq; using System.Linq;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
using Emby.Naming.Common; using Emby.Naming.Common;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.IO; using MediaBrowser.Model.IO;
namespace Emby.Naming.Video namespace Emby.Naming.Video
@ -20,11 +19,12 @@ namespace Emby.Naming.Video
/// <param name="files">List of related video files.</param> /// <param name="files">List of related video files.</param>
/// <param name="namingOptions">The naming options.</param> /// <param name="namingOptions">The naming options.</param>
/// <param name="supportMultiVersion">Indication we should consider multi-versions of content.</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> /// <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 var videoInfos = files
.Select(i => VideoResolver.Resolve(i.FullName, i.IsDirectory, namingOptions)) .Select(i => VideoResolver.Resolve(i.FullName, i.IsDirectory, namingOptions, parseName))
.OfType<VideoFileInfo>() .OfType<VideoFileInfo>()
.ToList(); .ToList();
@ -34,12 +34,25 @@ namespace Emby.Naming.Video
.Where(i => i.ExtraType == null) .Where(i => i.ExtraType == null)
.Select(i => new FileSystemMetadata { FullName = i.Path, IsDirectory = i.IsDirectory }); .Select(i => new FileSystemMetadata { FullName = i.Path, IsDirectory = i.IsDirectory });
var stackResult = new StackResolver(namingOptions) var stackResult = StackResolver.Resolve(nonExtras, namingOptions).ToList();
.Resolve(nonExtras).ToList();
var remainingFiles = videoInfos var remainingFiles = new List<VideoFileInfo>();
.Where(i => !stackResult.Any(s => i.Path != null && s.ContainsFile(i.Path, i.IsDirectory))) var standaloneMedia = new List<VideoFileInfo>();
.ToList();
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>(); var list = new List<VideoInfo>();
@ -47,27 +60,15 @@ namespace Emby.Naming.Video
{ {
var info = new VideoInfo(stack.Name) 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>() .OfType<VideoFileInfo>()
.ToList() .ToList()
}; };
info.Year = info.Files[0].Year; 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); list.Add(info);
} }
var standaloneMedia = remainingFiles
.Where(i => i.ExtraType == null)
.ToList();
foreach (var media in standaloneMedia) foreach (var media in standaloneMedia)
{ {
var info = new VideoInfo(media.Name) { Files = new[] { media } }; var info = new VideoInfo(media.Name) { Files = new[] { media } };
@ -75,10 +76,6 @@ namespace Emby.Naming.Video
info.Year = info.Files[0].Year; info.Year = info.Files[0].Year;
remainingFiles.Remove(media); remainingFiles.Remove(media);
var extras = ExtractExtras(remainingFiles, media.FileNameWithoutExtension, namingOptions.VideoFlagDelimiters);
info.Extras = extras;
list.Add(info); list.Add(info);
} }
@ -87,58 +84,12 @@ namespace Emby.Naming.Video
list = GetVideosGroupedByVersion(list, namingOptions); 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 // Whatever files are left, just add them
list.AddRange(remainingFiles.Select(i => new VideoInfo(i.Name) list.AddRange(remainingFiles.Select(i => new VideoInfo(i.Name)
{ {
Files = new[] { i }, Files = new[] { i },
Year = i.Year Year = i.Year,
ExtraType = i.ExtraType
})); }));
return list; return list;
@ -162,6 +113,11 @@ namespace Emby.Naming.Video
for (var i = 0; i < videos.Count; i++) for (var i = 0; i < videos.Count; i++)
{ {
var video = videos[i]; var video = videos[i];
if (video.ExtraType != null)
{
continue;
}
if (!IsEligibleForMultiVersion(folderName, video.Files[0].Path, namingOptions)) if (!IsEligibleForMultiVersion(folderName, video.Files[0].Path, namingOptions))
{ {
return videos; return videos;
@ -178,17 +134,14 @@ namespace Emby.Naming.Video
var alternateVersionsLen = videos.Count - 1; var alternateVersionsLen = videos.Count - 1;
var alternateVersions = new VideoFileInfo[alternateVersionsLen]; var alternateVersions = new VideoFileInfo[alternateVersionsLen];
var extras = new List<VideoFileInfo>(list[0].Extras);
for (int i = 0; i < alternateVersionsLen; i++) for (int i = 0; i < alternateVersionsLen; i++)
{ {
var video = videos[i + 1]; var video = videos[i + 1];
alternateVersions[i] = video.Files[0]; alternateVersions[i] = video.Files[0];
extras.AddRange(video.Extras);
} }
list[0].AlternateVersions = alternateVersions; list[0].AlternateVersions = alternateVersions;
list[0].Name = folderName.ToString(); list[0].Name = folderName.ToString();
list[0].Extras = extras;
return list; return list;
} }
@ -230,7 +183,7 @@ namespace Emby.Naming.Video
var tmpTestFilename = testFilename.ToString(); var tmpTestFilename = testFilename.ToString();
if (CleanStringParser.TryClean(tmpTestFilename, namingOptions.CleanStringRegexes, out var cleanName)) if (CleanStringParser.TryClean(tmpTestFilename, namingOptions.CleanStringRegexes, out var cleanName))
{ {
tmpTestFilename = cleanName.Trim().ToString(); tmpTestFilename = cleanName.Trim();
} }
// The CleanStringParser should have removed common keywords etc. // The CleanStringParser should have removed common keywords etc.
@ -238,67 +191,5 @@ namespace Emby.Naming.Video
|| testFilename[0] == '-' || testFilename[0] == '-'
|| Regex.IsMatch(tmpTestFilename, @"^\[([^]]*)\]", RegexOptions.Compiled); || 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;
}
} }
} }

View File

@ -16,10 +16,11 @@ namespace Emby.Naming.Video
/// </summary> /// </summary>
/// <param name="path">The path.</param> /// <param name="path">The path.</param>
/// <param name="namingOptions">The naming options.</param> /// <param name="namingOptions">The naming options.</param>
/// <param name="parseName">Whether to parse the name or use the filename.</param>
/// <returns>VideoFileInfo.</returns> /// <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> /// <summary>
@ -74,7 +75,7 @@ namespace Emby.Naming.Video
var format3DResult = Format3DParser.Parse(path, namingOptions); var format3DResult = Format3DParser.Parse(path, namingOptions);
var extraResult = new ExtraResolver(namingOptions).GetExtraInfo(path); var extraResult = ExtraResolver.GetExtraInfo(path, namingOptions);
var name = Path.GetFileNameWithoutExtension(path); var name = Path.GetFileNameWithoutExtension(path);

View File

@ -11,11 +11,9 @@ using System.Net;
using System.Net.Http; using System.Net.Http;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Emby.Naming.Audio;
using Emby.Naming.Common; using Emby.Naming.Common;
using Emby.Naming.TV; using Emby.Naming.TV;
using Emby.Naming.Video; using Emby.Naming.Video;
using Emby.Server.Implementations.Library.Resolvers;
using Emby.Server.Implementations.Library.Validators; using Emby.Server.Implementations.Library.Validators;
using Emby.Server.Implementations.Playlists; using Emby.Server.Implementations.Playlists;
using Emby.Server.Implementations.ScheduledTasks; using Emby.Server.Implementations.ScheduledTasks;
@ -677,7 +675,7 @@ namespace Emby.Server.Implementations.Library
{ {
var result = resolver.ResolveMultiple(parent, fileList, collectionType, directoryService); 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>(); var items = new List<BaseItem>();
items.AddRange(result.Items); 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 ownerVideoInfo = VideoResolver.Resolve(owner.Path, owner.IsFolder, _namingOptions);
if (ownerVideoInfo == null)
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)
{ {
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) var current = fileSystemChildren[i];
}; if (current.IsDirectory && BaseItem.AllExtrasTypesFolderNames.ContainsKey(current.Name))
{
files.AddRange(_fileSystem.GetFiles(current.FullName, _namingOptions.VideoFileExtensions, false, false));
}
else if (!current.IsDirectory)
{
files.Add(current);
}
}
return ResolvePaths(files, directoryService, null, new LibraryOptions(), null, resolvers) if (files.Count == 0)
.OfType<Trailer>()
.Select(video =>
{ {
yield break;
}
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);
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;
}
// Try to retrieve it from the db. If we don't find it, use the resolved version // Try to retrieve it from the db. If we don't find it, use the resolved version
if (GetItemById(video.Id) is Trailer dbItem) if (GetItemById(resolved.Id) is Video dbItem)
{ {
video = dbItem; video = dbItem;
} }
video.ExtraType = currentExtra.ExtraType;
video.ParentId = Guid.Empty; video.ParentId = Guid.Empty;
video.OwnerId = owner.Id; video.OwnerId = owner.Id;
video.ExtraType = ExtraType.Trailer; yield return video;
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));
if (currentVideo != null)
{
files.AddRange(currentVideo.Extras.Where(i => i.ExtraType != ExtraType.Trailer).Select(i => _fileSystem.GetFileInfo(i.Path)));
}
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;
if (dbItem != null)
{
video = dbItem;
}
video.ParentId = Guid.Empty;
video.OwnerId = owner.Id;
SetExtraTypeFromFilename(video);
return video;
// Sort them so that the list can be easily compared for changes
}).OrderBy(i => i.Path);
} }
public string GetPathAfterNetworkSubstitution(string path, BaseItem ownerItem) public string GetPathAfterNetworkSubstitution(string path, BaseItem ownerItem)
@ -2817,15 +2784,6 @@ namespace Emby.Server.Implementations.Library
return path; 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) public List<PersonInfo> GetPeople(InternalPeopleQuery query)
{ {
return _itemRepository.GetPeople(query); return _itemRepository.GetPeople(query);

View File

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

View File

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

View File

@ -45,15 +45,18 @@ 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 // 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 // Also handle flat tv folders
if ((season != null || if (season != null ||
string.Equals(args.GetCollectionType(), CollectionType.TvShows, StringComparison.OrdinalIgnoreCase) || string.Equals(args.GetCollectionType(), CollectionType.TvShows, StringComparison.OrdinalIgnoreCase) ||
args.HasParent<Series>()) args.HasParent<Series>())
&& (parent is Series || !BaseItem.AllExtrasTypesFolderNames.ContainsKey(parent.Name)))
{ {
var episode = ResolveVideo<Episode>(args, false); var episode = ResolveVideo<Episode>(args, false);
if (episode != null) // Ignore extras
if (episode == null || episode.ExtraType != null)
{ {
return null;
}
var series = parent as Series ?? parent.GetParents().OfType<Series>().FirstOrDefault(); var series = parent as Series ?? parent.GetParents().OfType<Series>().FirstOrDefault();
if (series != null) if (series != null)
@ -73,7 +76,6 @@ namespace Emby.Server.Implementations.Library.Resolvers.TV
{ {
episode.ParentIndexNumber = 1; episode.ParentIndexNumber = 1;
} }
}
return episode; return episode;
} }

View File

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

View File

@ -40,9 +40,7 @@ namespace MediaBrowser.Controller.Entities
/// </summary> /// </summary>
public abstract class BaseItem : IHasProviderIds, IHasLookupInfo<ItemLookupInfo>, IEquatable<BaseItem> public abstract class BaseItem : IHasProviderIds, IHasLookupInfo<ItemLookupInfo>, IEquatable<BaseItem>
{ {
/// <summary> public const string TrailerFileName = "trailer";
/// The trailer folder name.
/// </summary>
public const string TrailersFolderName = "trailers"; public const string TrailersFolderName = "trailers";
public const string ThemeSongsFolderName = "theme-music"; public const string ThemeSongsFolderName = "theme-music";
public const string ThemeSongFileName = "theme"; public const string ThemeSongFileName = "theme";
@ -99,8 +97,6 @@ namespace MediaBrowser.Controller.Entities
}; };
private string _sortName; private string _sortName;
private Guid[] _themeSongIds;
private Guid[] _themeVideoIds;
private string _forcedSortName; private string _forcedSortName;
@ -121,40 +117,6 @@ namespace MediaBrowser.Controller.Entities
ExtraIds = Array.Empty<Guid>(); 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] [JsonIgnore]
public string PreferredMetadataCountryCode { get; set; } public string PreferredMetadataCountryCode { get; set; }
@ -1379,28 +1341,6 @@ namespace MediaBrowser.Controller.Entities
}).OrderBy(i => i.Path).ToArray(); }).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) public Task RefreshMetadata(CancellationToken cancellationToken)
{ {
return RefreshMetadata(new MetadataRefreshOptions(new DirectoryService(FileSystem)), cancellationToken); return RefreshMetadata(new MetadataRefreshOptions(new DirectoryService(FileSystem)), cancellationToken);
@ -1434,13 +1374,8 @@ namespace MediaBrowser.Controller.Entities
GetFileSystemChildren(options.DirectoryService).ToList() : GetFileSystemChildren(options.DirectoryService).ToList() :
new List<FileSystemMetadata>(); 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 await LibraryManager.UpdateImagesAsync(this).ConfigureAwait(false); // ensure all image properties in DB are fresh
if (ownedItemsChanged)
{
requiresSave = true;
}
} }
catch (Exception ex) 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> /// <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) protected virtual async Task<bool> RefreshedOwnedItems(MetadataRefreshOptions options, List<FileSystemMetadata> fileSystemChildren, CancellationToken cancellationToken)
{ {
var themeSongsChanged = false; if (!IsFileProtocol || !SupportsOwnedItems || IsInMixedFolder || this is ICollectionFolder)
var themeVideosChanged = false;
var extrasChanged = false;
var localTrailersChanged = false;
if (IsFileProtocol && SupportsOwnedItems)
{ {
if (SupportsThemeMedia) return false;
{
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) return await RefreshExtras(this, options, fileSystemChildren, cancellationToken).ConfigureAwait(false);
{
localTrailersChanged = await RefreshLocalTrailers(hasTrailers, options, fileSystemChildren, cancellationToken).ConfigureAwait(false);
}
}
return themeSongsChanged || themeVideosChanged || extrasChanged || localTrailersChanged;
} }
protected virtual FileSystemMetadata[] GetFileSystemChildren(IDirectoryService directoryService) protected virtual FileSystemMetadata[] GetFileSystemChildren(IDirectoryService directoryService)
@ -1554,58 +1466,20 @@ namespace MediaBrowser.Controller.Entities
return directoryService.GetFileSystemEntries(path); 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) private async Task<bool> RefreshExtras(BaseItem item, MetadataRefreshOptions options, List<FileSystemMetadata> fileSystemChildren, CancellationToken cancellationToken)
{ {
var extras = LoadExtras(fileSystemChildren, options.DirectoryService); var extras = LibraryManager.FindExtras(item, fileSystemChildren).ToArray();
var themeVideos = LoadThemeVideos(fileSystemChildren, options.DirectoryService); var newExtraIds = extras.Select(i => i.Id).ToArray();
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 extrasChanged = !item.ExtraIds.SequenceEqual(newExtraIds); var extrasChanged = !item.ExtraIds.SequenceEqual(newExtraIds);
if (extrasChanged) if (!extrasChanged)
{ {
return false;
}
var ownerId = item.Id; var ownerId = item.Id;
var tasks = newExtras.Select(i => var tasks = extras.Select(i =>
{ {
var subOptions = new MetadataRefreshOptions(options); var subOptions = new MetadataRefreshOptions(options);
if (i.OwnerId != ownerId || i.ParentId != Guid.Empty) if (i.OwnerId != ownerId || i.ParentId != Guid.Empty)
@ -1621,83 +1495,8 @@ namespace MediaBrowser.Controller.Entities
await Task.WhenAll(tasks).ConfigureAwait(false); await Task.WhenAll(tasks).ConfigureAwait(false);
item.ExtraIds = newExtraIds; item.ExtraIds = newExtraIds;
}
return extrasChanged; return true;
}
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 subOptions = new MetadataRefreshOptions(options);
if (!i.ExtraType.HasValue ||
i.ExtraType.Value != Model.Entities.ExtraType.ThemeVideo ||
i.OwnerId != ownerId ||
!i.ParentId.Equals(Guid.Empty))
{
i.ExtraType = Model.Entities.ExtraType.ThemeVideo;
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.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);
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;
} }
public string GetPresentationUniqueKey() public string GetPresentationUniqueKey()
@ -2891,14 +2690,14 @@ namespace MediaBrowser.Controller.Entities
StringComparison.OrdinalIgnoreCase); 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> /// <summary>

View File

@ -2,7 +2,6 @@
#pragma warning disable CS1591 #pragma warning disable CS1591
using System;
using System.Collections.Generic; using System.Collections.Generic;
using MediaBrowser.Model.Entities; using MediaBrowser.Model.Entities;
@ -17,18 +16,10 @@ namespace MediaBrowser.Controller.Entities
IReadOnlyList<MediaUrl> RemoteTrailers { get; set; } IReadOnlyList<MediaUrl> RemoteTrailers { get; set; }
/// <summary> /// <summary>
/// Gets or sets the local trailer ids. /// Gets the local trailers.
/// </summary> /// </summary>
/// <value>The local trailer ids.</value> /// <value>The local trailers.</value>
IReadOnlyList<Guid> LocalTrailerIds { get; set; } IReadOnlyList<BaseItem> LocalTrailers { get; }
/// <summary>
/// Gets or sets the remote trailer ids.
/// </summary>
/// <value>The remote trailer ids.</value>
IReadOnlyList<Guid> RemoteTrailerIds { get; set; }
Guid Id { get; set; }
} }
/// <summary> /// <summary>
@ -42,57 +33,6 @@ namespace MediaBrowser.Controller.Entities
/// <param name="item">Media item.</param> /// <param name="item">Media item.</param>
/// <returns><see cref="IReadOnlyList{Guid}" />.</returns> /// <returns><see cref="IReadOnlyList{Guid}" />.</returns>
public static int GetTrailerCount(this IHasTrailers item) public static int GetTrailerCount(this IHasTrailers item)
=> item.LocalTrailerIds.Count + item.RemoteTrailerIds.Count; => item.LocalTrailers.Count + item.RemoteTrailers.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;
}
} }
} }

View File

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

View File

@ -7,12 +7,9 @@ using System.Collections.Generic;
using System.Globalization; using System.Globalization;
using System.Linq; using System.Linq;
using System.Text.Json.Serialization; using System.Text.Json.Serialization;
using System.Threading;
using System.Threading.Tasks;
using Jellyfin.Data.Enums; using Jellyfin.Data.Enums;
using MediaBrowser.Controller.Providers; using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.Entities; using MediaBrowser.Model.Entities;
using MediaBrowser.Model.IO;
using MediaBrowser.Model.Providers; using MediaBrowser.Model.Providers;
namespace MediaBrowser.Controller.Entities.Movies namespace MediaBrowser.Controller.Entities.Movies
@ -22,22 +19,29 @@ namespace MediaBrowser.Controller.Entities.Movies
/// </summary> /// </summary>
public class Movie : Video, IHasSpecialFeatures, IHasTrailers, IHasLookupInfo<MovieInfo>, ISupportsBoxSetGrouping public class Movie : Video, IHasSpecialFeatures, IHasTrailers, IHasLookupInfo<MovieInfo>, ISupportsBoxSetGrouping
{ {
public Movie() private IReadOnlyList<Guid> _specialFeatureIds;
/// <inheritdoc />
public IReadOnlyList<Guid> SpecialFeatureIds
{ {
SpecialFeatureIds = Array.Empty<Guid>(); get
RemoteTrailers = Array.Empty<MediaUrl>(); {
LocalTrailerIds = Array.Empty<Guid>(); return _specialFeatureIds ??= GetExtras()
RemoteTrailerIds = Array.Empty<Guid>(); .Where(extra => extra.ExtraType != Model.Entities.ExtraType.Trailer)
.Select(song => song.Id)
.ToArray();
}
set
{
_specialFeatureIds = value;
}
} }
/// <inheritdoc /> /// <inheritdoc />
public IReadOnlyList<Guid> SpecialFeatureIds { get; set; } public IReadOnlyList<BaseItem> LocalTrailers => GetExtras()
.Where(extra => extra.ExtraType == Model.Entities.ExtraType.Trailer)
/// <inheritdoc /> .ToArray();
public IReadOnlyList<Guid> LocalTrailerIds { get; set; }
/// <inheritdoc />
public IReadOnlyList<Guid> RemoteTrailerIds { get; set; }
/// <summary> /// <summary>
/// Gets or sets the name of the TMDB collection. /// Gets or sets the name of the TMDB collection.
@ -66,54 +70,6 @@ namespace MediaBrowser.Controller.Entities.Movies
return 2.0 / 3; 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 /> /// <inheritdoc />
public override UnratedItem GetBlockUnratedType() public override UnratedItem GetBlockUnratedType()
{ {

View File

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

View File

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

View File

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

View File

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

View File

@ -106,7 +106,7 @@ namespace MediaBrowser.LocalMetadata.Images
{ {
if (!item.IsFileProtocol) if (!item.IsFileProtocol)
{ {
return Enumerable.Empty<FileSystemMetadata>(); yield break;
} }
var path = item.ContainingFolderPath; 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... // 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)) 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) for (var i = 0; i < count; i++)
.Where(i => BaseItem.SupportedImageExtensions.Contains(i.Extension, StringComparer.OrdinalIgnoreCase) || i.IsDirectory) {
if ((includeDirectories && file.IsDirectory) || string.Equals(BaseItem.SupportedImageExtensions[i], file.Extension, StringComparison.OrdinalIgnoreCase))
.OrderBy(i => Array.IndexOf(BaseItem.SupportedImageExtensions, i.Extension ?? string.Empty)); {
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 /> /// <inheritdoc />

View File

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

View File

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

View File

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

View File

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

View File

@ -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;
using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Entities.TV; using MediaBrowser.Controller.Entities.TV;
@ -13,12 +14,14 @@ namespace Jellyfin.Server.Implementations.Tests.Library
{ {
public class EpisodeResolverTest public class EpisodeResolverTest
{ {
private static readonly NamingOptions _namingOptions = new ();
[Fact] [Fact]
public void Resolve_GivenVideoInExtrasFolder_DoesNotResolveToEpisode() public void Resolve_GivenVideoInExtrasFolder_DoesNotResolveToEpisode()
{ {
var parent = new Folder { Name = "extras" }; var parent = new Folder { Name = "extras" };
var episodeResolver = new EpisodeResolver(null); var episodeResolver = new EpisodeResolver(_namingOptions);
var itemResolveArgs = new ItemResolveArgs( var itemResolveArgs = new ItemResolveArgs(
Mock.Of<IServerApplicationPaths>(), Mock.Of<IServerApplicationPaths>(),
Mock.Of<IDirectoryService>()) 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 // 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 // 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( var itemResolveArgs = new ItemResolveArgs(
Mock.Of<IServerApplicationPaths>(), Mock.Of<IServerApplicationPaths>(),
Mock.Of<IDirectoryService>()) Mock.Of<IDirectoryService>())
{ {
Parent = series, Parent = series,
CollectionType = CollectionType.TvShows, CollectionType = CollectionType.TvShows,
FileInfo = new FileSystemMetadata() FileInfo = new FileSystemMetadata
{ {
FullName = "Extras/Extras S01E01.mkv" FullName = "Extras/Extras S01E01.mkv"
} }
@ -58,7 +61,7 @@ namespace Jellyfin.Server.Implementations.Tests.Library
private class EpisodeResolverMock : EpisodeResolver private class EpisodeResolverMock : EpisodeResolver
{ {
public EpisodeResolverMock() : base(null) public EpisodeResolverMock(NamingOptions namingOptions) : base(namingOptions)
{ {
} }

View File

@ -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);
}
}