aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorcvium <clausvium@gmail.com>2021-12-07 15:18:17 +0100
committercvium <clausvium@gmail.com>2021-12-07 15:24:57 +0100
commitfde84a1e00b0c781ce10acc73a9103db51aab67b (patch)
tree6a2ed0bd065704f8444acc910fa5ae6b3556d16d
parent9cafa2cab4c1a87598983db069b3aa55d5f42125 (diff)
Refactor extras parsing
-rw-r--r--Emby.Naming/AudioBook/AudioBookListResolver.cs10
-rw-r--r--Emby.Naming/Common/NamingOptions.cs12
-rw-r--r--Emby.Naming/Video/ExtraResolver.cs103
-rw-r--r--Emby.Naming/Video/FileStack.cs5
-rw-r--r--Emby.Naming/Video/StackResolver.cs77
-rw-r--r--Emby.Naming/Video/VideoInfo.cs13
-rw-r--r--Emby.Naming/Video/VideoListResolver.cs169
-rw-r--r--Emby.Naming/Video/VideoResolver.cs7
-rw-r--r--Emby.Server.Implementations/Library/LibraryManager.cs122
-rw-r--r--Emby.Server.Implementations/Library/Resolvers/BaseVideoResolver.cs121
-rw-r--r--Emby.Server.Implementations/Library/Resolvers/Movies/MovieResolver.cs103
-rw-r--r--Emby.Server.Implementations/Library/Resolvers/TV/EpisodeResolver.cs44
-rw-r--r--Jellyfin.Api/Controllers/UserLibraryController.cs2
-rw-r--r--MediaBrowser.Controller/Entities/BaseItem.cs235
-rw-r--r--MediaBrowser.Controller/Entities/IHasTrailers.cs68
-rw-r--r--MediaBrowser.Controller/Entities/Movies/BoxSet.cs12
-rw-r--r--MediaBrowser.Controller/Entities/Movies/Movie.cs80
-rw-r--r--MediaBrowser.Controller/Entities/TV/Episode.cs14
-rw-r--r--MediaBrowser.Controller/Entities/TV/Series.cs10
-rw-r--r--MediaBrowser.Controller/Entities/UserViewBuilder.cs9
-rw-r--r--MediaBrowser.Controller/Library/ILibraryManager.cs17
-rw-r--r--MediaBrowser.LocalMetadata/Images/LocalImageProvider.cs23
-rw-r--r--tests/Jellyfin.Naming.Tests/Video/ExtraTests.cs11
-rw-r--r--tests/Jellyfin.Naming.Tests/Video/MultiVersionTests.cs20
-rw-r--r--tests/Jellyfin.Naming.Tests/Video/StackTests.cs93
-rw-r--r--tests/Jellyfin.Naming.Tests/Video/VideoListResolverTests.cs107
-rw-r--r--tests/Jellyfin.Server.Implementations.Tests/Library/EpisodeResolverTest.cs13
-rw-r--r--tests/Jellyfin.Server.Implementations.Tests/Library/LibraryManager/FindExtrasTests.cs178
28 files changed, 681 insertions, 997 deletions
diff --git a/Emby.Naming/AudioBook/AudioBookListResolver.cs b/Emby.Naming/AudioBook/AudioBookListResolver.cs
index 1e4a8d2ed..dd8a05bb3 100644
--- a/Emby.Naming/AudioBook/AudioBookListResolver.cs
+++ b/Emby.Naming/AudioBook/AudioBookListResolver.cs
@@ -14,6 +14,7 @@ namespace Emby.Naming.AudioBook
public class AudioBookListResolver
{
private readonly NamingOptions _options;
+ private readonly AudioBookResolver _audioBookResolver;
/// <summary>
/// Initializes a new instance of the <see cref="AudioBookListResolver"/> class.
@@ -22,6 +23,7 @@ namespace Emby.Naming.AudioBook
public AudioBookListResolver(NamingOptions options)
{
_options = options;
+ _audioBookResolver = new AudioBookResolver(_options);
}
/// <summary>
@@ -31,21 +33,19 @@ namespace Emby.Naming.AudioBook
/// <returns>Returns IEnumerable of <see cref="AudioBookInfo"/>.</returns>
public IEnumerable<AudioBookInfo> Resolve(IEnumerable<FileSystemMetadata> files)
{
- var audioBookResolver = new AudioBookResolver(_options);
// File with empty fullname will be sorted out here.
var audiobookFileInfos = files
- .Select(i => audioBookResolver.Resolve(i.FullName))
+ .Select(i => _audioBookResolver.Resolve(i.FullName))
.OfType<AudioBookFileInfo>()
.ToList();
- var stackResult = new StackResolver(_options)
- .ResolveAudioBooks(audiobookFileInfos);
+ var stackResult = StackResolver.ResolveAudioBooks(audiobookFileInfos);
foreach (var stack in stackResult)
{
var stackFiles = stack.Files
- .Select(i => audioBookResolver.Resolve(i))
+ .Select(i => _audioBookResolver.Resolve(i))
.OfType<AudioBookFileInfo>()
.ToList();
diff --git a/Emby.Naming/Common/NamingOptions.cs b/Emby.Naming/Common/NamingOptions.cs
index 7bc9fbce8..86c79166d 100644
--- a/Emby.Naming/Common/NamingOptions.cs
+++ b/Emby.Naming/Common/NamingOptions.cs
@@ -126,9 +126,9 @@ namespace Emby.Naming.Common
VideoFileStackingExpressions = new[]
{
- "(?<title>.*?)(?<volume>[ _.-]*(?:cd|dvd|p(?:ar)?t|dis[ck])[ _.-]*[0-9]+)(?<ignore>.*?)(?<extension>\\.[^.]+)$",
- "(?<title>.*?)(?<volume>[ _.-]*(?:cd|dvd|p(?:ar)?t|dis[ck])[ _.-]*[a-d])(?<ignore>.*?)(?<extension>\\.[^.]+)$",
- "(?<title>.*?)(?<volume>[ ._-]*[a-d])(?<ignore>.*?)(?<extension>\\.[^.]+)$"
+ "^(?<title>.*?)(?<volume>[ _.-]*(?:cd|dvd|part|pt|dis[ck])[ _.-]*[0-9]+)(?<ignore>.*?)(?<extension>\\.[^.]+)$",
+ "^(?<title>.*?)(?<volume>[ _.-]*(?:cd|dvd|part|pt|dis[ck])[ _.-]*[a-d])(?<ignore>.*?)(?<extension>\\.[^.]+)$",
+ "^(?<title>.*?)(?<volume>[ ._-]*[a-d])(?<ignore>.*?)(?<extension>\\.[^.]+)$"
};
CleanDateTimes = new[]
@@ -405,6 +405,12 @@ namespace Emby.Naming.Common
{
new ExtraRule(
ExtraType.Trailer,
+ ExtraRuleType.DirectoryName,
+ "trailers",
+ MediaType.Video),
+
+ new ExtraRule(
+ ExtraType.Trailer,
ExtraRuleType.Filename,
"trailer",
MediaType.Video),
diff --git a/Emby.Naming/Video/ExtraResolver.cs b/Emby.Naming/Video/ExtraResolver.cs
index 7bc226614..cd7a6c0d7 100644
--- a/Emby.Naming/Video/ExtraResolver.cs
+++ b/Emby.Naming/Video/ExtraResolver.cs
@@ -1,5 +1,7 @@
using System;
+using System.Collections.Generic;
using System.IO;
+using System.Linq;
using System.Text.RegularExpressions;
using Emby.Naming.Audio;
using Emby.Naming.Common;
@@ -9,45 +11,27 @@ namespace Emby.Naming.Video
/// <summary>
/// Resolve if file is extra for video.
/// </summary>
- public class ExtraResolver
+ public static class ExtraResolver
{
- private static readonly char[] _digits = new[] { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9' };
- private readonly NamingOptions _options;
-
- /// <summary>
- /// Initializes a new instance of the <see cref="ExtraResolver"/> class.
- /// </summary>
- /// <param name="options"><see cref="NamingOptions"/> object containing VideoExtraRules and passed to <see cref="AudioFileParser"/> and <see cref="VideoResolver"/>.</param>
- public ExtraResolver(NamingOptions options)
- {
- _options = options;
- }
+ private static readonly char[] _digits = { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9' };
/// <summary>
/// Attempts to resolve if file is extra.
/// </summary>
/// <param name="path">Path to file.</param>
+ /// <param name="namingOptions">The naming options.</param>
/// <returns>Returns <see cref="ExtraResult"/> object.</returns>
- public ExtraResult GetExtraInfo(string path)
+ public static ExtraResult GetExtraInfo(string path, NamingOptions namingOptions)
{
var result = new ExtraResult();
- for (var i = 0; i < _options.VideoExtraRules.Length; i++)
+ for (var i = 0; i < namingOptions.VideoExtraRules.Length; i++)
{
- var rule = _options.VideoExtraRules[i];
- if (rule.MediaType == MediaType.Audio)
+ var rule = namingOptions.VideoExtraRules[i];
+ if ((rule.MediaType == MediaType.Audio && !AudioFileParser.IsAudioFile(path, namingOptions))
+ || (rule.MediaType == MediaType.Video && !VideoResolver.IsVideoFile(path, namingOptions)))
{
- if (!AudioFileParser.IsAudioFile(path, _options))
- {
- continue;
- }
- }
- else if (rule.MediaType == MediaType.Video)
- {
- if (!VideoResolver.IsVideoFile(path, _options))
- {
- continue;
- }
+ continue;
}
var pathSpan = path.AsSpan();
@@ -76,7 +60,7 @@ namespace Emby.Naming.Video
{
var filename = Path.GetFileName(path);
- var regex = new Regex(rule.Token, RegexOptions.IgnoreCase);
+ var regex = new Regex(rule.Token, RegexOptions.IgnoreCase | RegexOptions.Compiled);
if (regex.IsMatch(filename))
{
@@ -102,5 +86,68 @@ namespace Emby.Naming.Video
return result;
}
+
+ /// <summary>
+ /// Finds extras matching the video info.
+ /// </summary>
+ /// <param name="files">The list of file video infos.</param>
+ /// <param name="videoInfo">The video to compare against.</param>
+ /// <param name="videoFlagDelimiters">The video flag delimiters.</param>
+ /// <returns>A list of video extras for [videoInfo].</returns>
+ public static IReadOnlyList<VideoFileInfo> GetExtras(IReadOnlyList<VideoInfo> files, VideoFileInfo videoInfo, ReadOnlySpan<char> videoFlagDelimiters)
+ {
+ var parentDir = videoInfo.IsDirectory ? videoInfo.Path : Path.GetDirectoryName(videoInfo.Path.AsSpan());
+
+ var trimmedFileName = TrimFilenameDelimiters(videoInfo.Name, videoFlagDelimiters);
+ var trimmedFileNameWithoutExtension = TrimFilenameDelimiters(videoInfo.FileNameWithoutExtension, videoFlagDelimiters);
+ var trimmedVideoInfoName = TrimFilenameDelimiters(videoInfo.Name, videoFlagDelimiters);
+
+ var result = new List<VideoFileInfo>();
+ for (var pos = files.Count - 1; pos >= 0; pos--)
+ {
+ var current = files[pos];
+ // ignore non-extras and multi-file (can this happen?)
+ if (current.ExtraType == null || current.Files.Count > 1)
+ {
+ continue;
+ }
+
+ var currentFile = files[pos].Files[0];
+ var trimmedCurrentFileName = TrimFilenameDelimiters(currentFile.Name, videoFlagDelimiters);
+
+ // first check filenames
+ bool isValid = StartsWith(trimmedCurrentFileName, trimmedFileNameWithoutExtension)
+ || (StartsWith(trimmedCurrentFileName, trimmedFileName) && currentFile.Year == videoInfo.Year)
+ || (StartsWith(trimmedCurrentFileName, trimmedVideoInfoName) && currentFile.Year == videoInfo.Year);
+
+ // then by directory
+ if (!isValid)
+ {
+ // When the extra rule type is DirectoryName we must go one level higher to get the "real" dir name
+ var currentParentDir = currentFile.ExtraRule?.RuleType == ExtraRuleType.DirectoryName
+ ? Path.GetDirectoryName(Path.GetDirectoryName(currentFile.Path.AsSpan()))
+ : Path.GetDirectoryName(currentFile.Path.AsSpan());
+
+ isValid = !currentParentDir.IsEmpty && !parentDir.IsEmpty && currentParentDir.Equals(parentDir, StringComparison.OrdinalIgnoreCase);
+ }
+
+ if (isValid)
+ {
+ result.Add(currentFile);
+ }
+ }
+
+ return result.OrderBy(r => r.Path).ToArray();
+ }
+
+ private static ReadOnlySpan<char> TrimFilenameDelimiters(ReadOnlySpan<char> name, ReadOnlySpan<char> videoFlagDelimiters)
+ {
+ return name.IsEmpty ? name : name.TrimEnd().TrimEnd(videoFlagDelimiters).TrimEnd();
+ }
+
+ private static bool StartsWith(ReadOnlySpan<char> fileName, ReadOnlySpan<char> baseName)
+ {
+ return !baseName.IsEmpty && fileName.StartsWith(baseName, StringComparison.OrdinalIgnoreCase);
+ }
}
}
diff --git a/Emby.Naming/Video/FileStack.cs b/Emby.Naming/Video/FileStack.cs
index 6519db57c..a4a4716ca 100644
--- a/Emby.Naming/Video/FileStack.cs
+++ b/Emby.Naming/Video/FileStack.cs
@@ -40,6 +40,11 @@ namespace Emby.Naming.Video
/// <returns>True if file is in the stack.</returns>
public bool ContainsFile(string file, bool isDirectory)
{
+ if (string.IsNullOrEmpty(file))
+ {
+ return false;
+ }
+
if (IsDirectoryStack == isDirectory)
{
return Files.Contains(file, StringComparer.OrdinalIgnoreCase);
diff --git a/Emby.Naming/Video/StackResolver.cs b/Emby.Naming/Video/StackResolver.cs
index 36f65a562..be73f69db 100644
--- a/Emby.Naming/Video/StackResolver.cs
+++ b/Emby.Naming/Video/StackResolver.cs
@@ -12,37 +12,28 @@ namespace Emby.Naming.Video
/// <summary>
/// Resolve <see cref="FileStack"/> from list of paths.
/// </summary>
- public class StackResolver
+ public static class StackResolver
{
- private readonly NamingOptions _options;
-
- /// <summary>
- /// Initializes a new instance of the <see cref="StackResolver"/> class.
- /// </summary>
- /// <param name="options"><see cref="NamingOptions"/> object containing VideoFileStackingRegexes and passes options to <see cref="VideoResolver"/>.</param>
- public StackResolver(NamingOptions options)
- {
- _options = options;
- }
-
/// <summary>
/// Resolves only directories from paths.
/// </summary>
/// <param name="files">List of paths.</param>
+ /// <param name="namingOptions">The naming options.</param>
/// <returns>Enumerable <see cref="FileStack"/> of directories.</returns>
- public IEnumerable<FileStack> ResolveDirectories(IEnumerable<string> files)
+ public static IEnumerable<FileStack> ResolveDirectories(IEnumerable<string> files, NamingOptions namingOptions)
{
- return Resolve(files.Select(i => new FileSystemMetadata { FullName = i, IsDirectory = true }));
+ return Resolve(files.Select(i => new FileSystemMetadata { FullName = i, IsDirectory = true }), namingOptions);
}
/// <summary>
/// Resolves only files from paths.
/// </summary>
/// <param name="files">List of paths.</param>
+ /// <param name="namingOptions">The naming options.</param>
/// <returns>Enumerable <see cref="FileStack"/> of files.</returns>
- public IEnumerable<FileStack> ResolveFiles(IEnumerable<string> files)
+ public static IEnumerable<FileStack> ResolveFiles(IEnumerable<string> files, NamingOptions namingOptions)
{
- return Resolve(files.Select(i => new FileSystemMetadata { FullName = i, IsDirectory = false }));
+ return Resolve(files.Select(i => new FileSystemMetadata { FullName = i, IsDirectory = false }), namingOptions);
}
/// <summary>
@@ -50,7 +41,7 @@ namespace Emby.Naming.Video
/// </summary>
/// <param name="files">List of paths.</param>
/// <returns>Enumerable <see cref="FileStack"/> of directories.</returns>
- public IEnumerable<FileStack> ResolveAudioBooks(IEnumerable<AudioBookFileInfo> files)
+ public static IEnumerable<FileStack> ResolveAudioBooks(IEnumerable<AudioBookFileInfo> files)
{
var groupedDirectoryFiles = files.GroupBy(file => Path.GetDirectoryName(file.Path));
@@ -82,15 +73,20 @@ namespace Emby.Naming.Video
/// Resolves videos from paths.
/// </summary>
/// <param name="files">List of paths.</param>
+ /// <param name="namingOptions">The naming options.</param>
/// <returns>Enumerable <see cref="FileStack"/> of videos.</returns>
- public IEnumerable<FileStack> Resolve(IEnumerable<FileSystemMetadata> files)
+ public static IEnumerable<FileStack> Resolve(IEnumerable<FileSystemMetadata> files, NamingOptions namingOptions)
{
var list = files
- .Where(i => i.IsDirectory || VideoResolver.IsVideoFile(i.FullName, _options) || VideoResolver.IsStubFile(i.FullName, _options))
+ .Where(i => i.IsDirectory || VideoResolver.IsVideoFile(i.FullName, namingOptions) || VideoResolver.IsStubFile(i.FullName, namingOptions))
.OrderBy(i => i.FullName)
+ .Select(f => (f.IsDirectory, FileName: GetFileNameWithExtension(f), f.FullName))
.ToList();
- var expressions = _options.VideoFileStackingRegexes;
+ // TODO is there a "nicer" way?
+ var cache = new Dictionary<(string, Regex, int), Match>();
+
+ var expressions = namingOptions.VideoFileStackingRegexes;
for (var i = 0; i < list.Count; i++)
{
@@ -102,17 +98,17 @@ namespace Emby.Naming.Video
while (expressionIndex < expressions.Length)
{
var exp = expressions[expressionIndex];
- var stack = new FileStack();
+ FileStack? stack = null;
// (Title)(Volume)(Ignore)(Extension)
- var match1 = FindMatch(file1, exp, offset);
+ var match1 = FindMatch(file1.FileName, exp, offset, cache);
if (match1.Success)
{
- var title1 = match1.Groups["title"].Value;
- var volume1 = match1.Groups["volume"].Value;
- var ignore1 = match1.Groups["ignore"].Value;
- var extension1 = match1.Groups["extension"].Value;
+ var title1 = match1.Groups[1].Value;
+ var volume1 = match1.Groups[2].Value;
+ var ignore1 = match1.Groups[3].Value;
+ var extension1 = match1.Groups[4].Value;
var j = i + 1;
while (j < list.Count)
@@ -126,7 +122,7 @@ namespace Emby.Naming.Video
}
// (Title)(Volume)(Ignore)(Extension)
- var match2 = FindMatch(file2, exp, offset);
+ var match2 = FindMatch(file2.FileName, exp, offset, cache);
if (match2.Success)
{
@@ -142,6 +138,7 @@ namespace Emby.Naming.Video
if (string.Equals(ignore1, ignore2, StringComparison.OrdinalIgnoreCase)
&& string.Equals(extension1, extension2, StringComparison.OrdinalIgnoreCase))
{
+ stack ??= new FileStack();
if (stack.Files.Count == 0)
{
stack.Name = title1 + ignore1;
@@ -204,7 +201,7 @@ namespace Emby.Naming.Video
expressionIndex++;
}
- if (stack.Files.Count > 1)
+ if (stack?.Files.Count > 1)
{
yield return stack;
i += stack.Files.Count - 1;
@@ -214,26 +211,32 @@ namespace Emby.Naming.Video
}
}
- private static string GetRegexInput(FileSystemMetadata file)
+ private static string GetFileNameWithExtension(FileSystemMetadata file)
{
// For directories, dummy up an extension otherwise the expressions will fail
- var input = !file.IsDirectory
- ? file.FullName
- : file.FullName + ".mkv";
+ var input = file.FullName;
+ if (file.IsDirectory)
+ {
+ input = Path.ChangeExtension(input, "mkv");
+ }
return Path.GetFileName(input);
}
- private static Match FindMatch(FileSystemMetadata input, Regex regex, int offset)
+ private static Match FindMatch(string input, Regex regex, int offset, Dictionary<(string, Regex, int), Match> cache)
{
- var regexInput = GetRegexInput(input);
-
- if (offset < 0 || offset >= regexInput.Length)
+ if (offset < 0 || offset >= input.Length)
{
return Match.Empty;
}
- return regex.Match(regexInput, offset);
+ if (!cache.TryGetValue((input, regex, offset), out var result))
+ {
+ result = regex.Match(input, offset, input.Length - offset);
+ cache.Add((input, regex, offset), result);
+ }
+
+ return result;
}
}
}
diff --git a/Emby.Naming/Video/VideoInfo.cs b/Emby.Naming/Video/VideoInfo.cs
index 930fdb33f..8847ee9bc 100644
--- a/Emby.Naming/Video/VideoInfo.cs
+++ b/Emby.Naming/Video/VideoInfo.cs
@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
+using MediaBrowser.Model.Entities;
namespace Emby.Naming.Video
{
@@ -17,7 +18,6 @@ namespace Emby.Naming.Video
Name = name;
Files = Array.Empty<VideoFileInfo>();
- Extras = Array.Empty<VideoFileInfo>();
AlternateVersions = Array.Empty<VideoFileInfo>();
}
@@ -40,15 +40,14 @@ namespace Emby.Naming.Video
public IReadOnlyList<VideoFileInfo> Files { get; set; }
/// <summary>
- /// Gets or sets the extras.
- /// </summary>
- /// <value>The extras.</value>
- public IReadOnlyList<VideoFileInfo> Extras { get; set; }
-
- /// <summary>
/// Gets or sets the alternate versions.
/// </summary>
/// <value>The alternate versions.</value>
public IReadOnlyList<VideoFileInfo> AlternateVersions { get; set; }
+
+ /// <summary>
+ /// Gets or sets the extra type.
+ /// </summary>
+ public ExtraType? ExtraType { get; set; }
}
}
diff --git a/Emby.Naming/Video/VideoListResolver.cs b/Emby.Naming/Video/VideoListResolver.cs
index ed7d511a3..bce7cb47f 100644
--- a/Emby.Naming/Video/VideoListResolver.cs
+++ b/Emby.Naming/Video/VideoListResolver.cs
@@ -4,7 +4,6 @@ using System.IO;
using System.Linq;
using System.Text.RegularExpressions;
using Emby.Naming.Common;
-using MediaBrowser.Model.Entities;
using MediaBrowser.Model.IO;
namespace Emby.Naming.Video
@@ -20,11 +19,12 @@ namespace Emby.Naming.Video
/// <param name="files">List of related video files.</param>
/// <param name="namingOptions">The naming options.</param>
/// <param name="supportMultiVersion">Indication we should consider multi-versions of content.</param>
+ /// <param name="parseName">Whether to parse the name or use the filename.</param>
/// <returns>Returns enumerable of <see cref="VideoInfo"/> which groups files together when related.</returns>
- public static IEnumerable<VideoInfo> Resolve(IEnumerable<FileSystemMetadata> files, NamingOptions namingOptions, bool supportMultiVersion = true)
+ public static IReadOnlyList<VideoInfo> Resolve(IEnumerable<FileSystemMetadata> files, NamingOptions namingOptions, bool supportMultiVersion = true, bool parseName = true)
{
var videoInfos = files
- .Select(i => VideoResolver.Resolve(i.FullName, i.IsDirectory, namingOptions))
+ .Select(i => VideoResolver.Resolve(i.FullName, i.IsDirectory, namingOptions, parseName))
.OfType<VideoFileInfo>()
.ToList();
@@ -34,12 +34,25 @@ namespace Emby.Naming.Video
.Where(i => i.ExtraType == null)
.Select(i => new FileSystemMetadata { FullName = i.Path, IsDirectory = i.IsDirectory });
- var stackResult = new StackResolver(namingOptions)
- .Resolve(nonExtras).ToList();
+ var stackResult = StackResolver.Resolve(nonExtras, namingOptions).ToList();
- var remainingFiles = videoInfos
- .Where(i => !stackResult.Any(s => i.Path != null && s.ContainsFile(i.Path, i.IsDirectory)))
- .ToList();
+ var remainingFiles = new List<VideoFileInfo>();
+ var standaloneMedia = new List<VideoFileInfo>();
+
+ for (var i = 0; i < videoInfos.Count; i++)
+ {
+ var current = videoInfos[i];
+ if (stackResult.Any(s => s.ContainsFile(current.Path, current.IsDirectory)))
+ {
+ continue;
+ }
+
+ remainingFiles.Add(current);
+ if (current.ExtraType == null)
+ {
+ standaloneMedia.Add(current);
+ }
+ }
var list = new List<VideoInfo>();
@@ -47,27 +60,15 @@ namespace Emby.Naming.Video
{
var info = new VideoInfo(stack.Name)
{
- Files = stack.Files.Select(i => VideoResolver.Resolve(i, stack.IsDirectoryStack, namingOptions))
+ Files = stack.Files.Select(i => VideoResolver.Resolve(i, stack.IsDirectoryStack, namingOptions, parseName))
.OfType<VideoFileInfo>()
.ToList()
};
info.Year = info.Files[0].Year;
-
- var extras = ExtractExtras(remainingFiles, stack.Name, Path.GetFileNameWithoutExtension(stack.Files[0].AsSpan()), namingOptions.VideoFlagDelimiters);
-
- if (extras.Count > 0)
- {
- info.Extras = extras;
- }
-
list.Add(info);
}
- var standaloneMedia = remainingFiles
- .Where(i => i.ExtraType == null)
- .ToList();
-
foreach (var media in standaloneMedia)
{
var info = new VideoInfo(media.Name) { Files = new[] { media } };
@@ -75,10 +76,6 @@ namespace Emby.Naming.Video
info.Year = info.Files[0].Year;
remainingFiles.Remove(media);
- var extras = ExtractExtras(remainingFiles, media.FileNameWithoutExtension, namingOptions.VideoFlagDelimiters);
-
- info.Extras = extras;
-
list.Add(info);
}
@@ -87,58 +84,12 @@ namespace Emby.Naming.Video
list = GetVideosGroupedByVersion(list, namingOptions);
}
- // If there's only one resolved video, use the folder name as well to find extras
- if (list.Count == 1)
- {
- var info = list[0];
- var videoPath = list[0].Files[0].Path;
- var parentPath = Path.GetDirectoryName(videoPath.AsSpan());
-
- if (!parentPath.IsEmpty)
- {
- var folderName = Path.GetFileName(parentPath);
- if (!folderName.IsEmpty)
- {
- var extras = ExtractExtras(remainingFiles, folderName, namingOptions.VideoFlagDelimiters);
- extras.AddRange(info.Extras);
- info.Extras = extras;
- }
- }
-
- // Add the extras that are just based on file name as well
- var extrasByFileName = remainingFiles
- .Where(i => i.ExtraRule != null && i.ExtraRule.RuleType == ExtraRuleType.Filename)
- .ToList();
-
- remainingFiles = remainingFiles
- .Except(extrasByFileName)
- .ToList();
-
- extrasByFileName.AddRange(info.Extras);
- info.Extras = extrasByFileName;
- }
-
- // If there's only one video, accept all trailers
- // Be lenient because people use all kinds of mishmash conventions with trailers.
- if (list.Count == 1)
- {
- var trailers = remainingFiles
- .Where(i => i.ExtraType == ExtraType.Trailer)
- .ToList();
-
- trailers.AddRange(list[0].Extras);
- list[0].Extras = trailers;
-
- remainingFiles = remainingFiles
- .Except(trailers)
- .ToList();
- }
-
// Whatever files are left, just add them
list.AddRange(remainingFiles.Select(i => new VideoInfo(i.Name)
{
Files = new[] { i },
- Year = i.Year
+ Year = i.Year,
+ ExtraType = i.ExtraType
}));
return list;
@@ -162,6 +113,11 @@ namespace Emby.Naming.Video
for (var i = 0; i < videos.Count; i++)
{
var video = videos[i];
+ if (video.ExtraType != null)
+ {
+ continue;
+ }
+
if (!IsEligibleForMultiVersion(folderName, video.Files[0].Path, namingOptions))
{
return videos;
@@ -178,17 +134,14 @@ namespace Emby.Naming.Video
var alternateVersionsLen = videos.Count - 1;
var alternateVersions = new VideoFileInfo[alternateVersionsLen];
- var extras = new List<VideoFileInfo>(list[0].Extras);
for (int i = 0; i < alternateVersionsLen; i++)
{
var video = videos[i + 1];
alternateVersions[i] = video.Files[0];
- extras.AddRange(video.Extras);
}
list[0].AlternateVersions = alternateVersions;
list[0].Name = folderName.ToString();
- list[0].Extras = extras;
return list;
}
@@ -230,7 +183,7 @@ namespace Emby.Naming.Video
var tmpTestFilename = testFilename.ToString();
if (CleanStringParser.TryClean(tmpTestFilename, namingOptions.CleanStringRegexes, out var cleanName))
{
- tmpTestFilename = cleanName.Trim().ToString();
+ tmpTestFilename = cleanName.Trim();
}
// The CleanStringParser should have removed common keywords etc.
@@ -238,67 +191,5 @@ namespace Emby.Naming.Video
|| testFilename[0] == '-'
|| Regex.IsMatch(tmpTestFilename, @"^\[([^]]*)\]", RegexOptions.Compiled);
}
-
- private static ReadOnlySpan<char> TrimFilenameDelimiters(ReadOnlySpan<char> name, ReadOnlySpan<char> videoFlagDelimiters)
- {
- return name.IsEmpty ? name : name.TrimEnd().TrimEnd(videoFlagDelimiters).TrimEnd();
- }
-
- private static bool StartsWith(ReadOnlySpan<char> fileName, ReadOnlySpan<char> baseName, ReadOnlySpan<char> trimmedBaseName)
- {
- if (baseName.IsEmpty)
- {
- return false;
- }
-
- return fileName.StartsWith(baseName, StringComparison.OrdinalIgnoreCase)
- || (!trimmedBaseName.IsEmpty && fileName.StartsWith(trimmedBaseName, StringComparison.OrdinalIgnoreCase));
- }
-
- /// <summary>
- /// Finds similar filenames to that of [baseName] and removes any matches from [remainingFiles].
- /// </summary>
- /// <param name="remainingFiles">The list of remaining filenames.</param>
- /// <param name="baseName">The base name to use for the comparison.</param>
- /// <param name="videoFlagDelimiters">The video flag delimiters.</param>
- /// <returns>A list of video extras for [baseName].</returns>
- private static List<VideoFileInfo> ExtractExtras(IList<VideoFileInfo> remainingFiles, ReadOnlySpan<char> baseName, ReadOnlySpan<char> videoFlagDelimiters)
- {
- return ExtractExtras(remainingFiles, baseName, ReadOnlySpan<char>.Empty, videoFlagDelimiters);
- }
-
- /// <summary>
- /// Finds similar filenames to that of [firstBaseName] and [secondBaseName] and removes any matches from [remainingFiles].
- /// </summary>
- /// <param name="remainingFiles">The list of remaining filenames.</param>
- /// <param name="firstBaseName">The first base name to use for the comparison.</param>
- /// <param name="secondBaseName">The second base name to use for the comparison.</param>
- /// <param name="videoFlagDelimiters">The video flag delimiters.</param>
- /// <returns>A list of video extras for [firstBaseName] and [secondBaseName].</returns>
- private static List<VideoFileInfo> ExtractExtras(IList<VideoFileInfo> remainingFiles, ReadOnlySpan<char> firstBaseName, ReadOnlySpan<char> secondBaseName, ReadOnlySpan<char> videoFlagDelimiters)
- {
- var trimmedFirstBaseName = TrimFilenameDelimiters(firstBaseName, videoFlagDelimiters);
- var trimmedSecondBaseName = TrimFilenameDelimiters(secondBaseName, videoFlagDelimiters);
-
- var result = new List<VideoFileInfo>();
- for (var pos = remainingFiles.Count - 1; pos >= 0; pos--)
- {
- var file = remainingFiles[pos];
- if (file.ExtraType == null)
- {
- continue;
- }
-
- var filename = file.FileNameWithoutExtension;
- if (StartsWith(filename, firstBaseName, trimmedFirstBaseName)
- || StartsWith(filename, secondBaseName, trimmedSecondBaseName))
- {
- result.Add(file);
- remainingFiles.RemoveAt(pos);
- }
- }
-
- return result;
- }
}
}
diff --git a/Emby.Naming/Video/VideoResolver.cs b/Emby.Naming/Video/VideoResolver.cs
index 4c9df27f5..9cadc1465 100644
--- a/Emby.Naming/Video/VideoResolver.cs
+++ b/Emby.Naming/Video/VideoResolver.cs
@@ -16,10 +16,11 @@ namespace Emby.Naming.Video
/// </summary>
/// <param name="path">The path.</param>
/// <param name="namingOptions">The naming options.</param>
+ /// <param name="parseName">Whether to parse the name or use the filename.</param>
/// <returns>VideoFileInfo.</returns>
- public static VideoFileInfo? ResolveDirectory(string? path, NamingOptions namingOptions)
+ public static VideoFileInfo? ResolveDirectory(string? path, NamingOptions namingOptions, bool parseName = true)
{
- return Resolve(path, true, namingOptions);
+ return Resolve(path, true, namingOptions, parseName);
}
/// <summary>
@@ -74,7 +75,7 @@ namespace Emby.Naming.Video
var format3DResult = Format3DParser.Parse(path, namingOptions);
- var extraResult = new ExtraResolver(namingOptions).GetExtraInfo(path);
+ var extraResult = ExtraResolver.GetExtraInfo(path, namingOptions);
var name = Path.GetFileNameWithoutExtension(path);
diff --git a/Emby.Server.Implementations/Library/LibraryManager.cs b/Emby.Server.Implementations/Library/LibraryManager.cs
index 778b6225e..01749242f 100644
--- a/Emby.Server.Implementations/Library/LibraryManager.cs
+++ b/Emby.Server.Implementations/Library/LibraryManager.cs
@@ -11,11 +11,9 @@ using System.Net;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
-using Emby.Naming.Audio;
using Emby.Naming.Common;
using Emby.Naming.TV;
using Emby.Naming.Video;
-using Emby.Server.Implementations.Library.Resolvers;
using Emby.Server.Implementations.Library.Validators;
using Emby.Server.Implementations.Playlists;
using Emby.Server.Implementations.ScheduledTasks;
@@ -677,7 +675,7 @@ namespace Emby.Server.Implementations.Library
{
var result = resolver.ResolveMultiple(parent, fileList, collectionType, directoryService);
- if (result != null && result.Items.Count > 0)
+ if (result?.Items.Count > 0)
{
var items = new List<BaseItem>();
items.AddRange(result.Items);
@@ -2685,89 +2683,58 @@ namespace Emby.Server.Implementations.Library
};
}
- public IEnumerable<Video> FindTrailers(BaseItem owner, List<FileSystemMetadata> fileSystemChildren, IDirectoryService directoryService)
+ public IEnumerable<Video> FindExtras(BaseItem owner, List<FileSystemMetadata> fileSystemChildren)
{
- var namingOptions = _namingOptions;
-
- var files = owner.IsInMixedFolder ? new List<FileSystemMetadata>() : fileSystemChildren.Where(i => i.IsDirectory)
- .Where(i => string.Equals(i.Name, BaseItem.TrailersFolderName, StringComparison.OrdinalIgnoreCase))
- .SelectMany(i => _fileSystem.GetFiles(i.FullName, namingOptions.VideoFileExtensions, false, false))
- .ToList();
-
- var videos = VideoListResolver.Resolve(fileSystemChildren, namingOptions);
-
- var currentVideo = videos.FirstOrDefault(i => string.Equals(owner.Path, i.Files[0].Path, StringComparison.OrdinalIgnoreCase));
-
- if (currentVideo != null)
+ var ownerVideoInfo = VideoResolver.Resolve(owner.Path, owner.IsFolder, _namingOptions);
+ if (ownerVideoInfo == null)
{
- files.AddRange(currentVideo.Extras.Where(i => i.ExtraType == ExtraType.Trailer).Select(i => _fileSystem.GetFileInfo(i.Path)));
+ yield break;
}
- var resolvers = new IItemResolver[]
+ var count = fileSystemChildren.Count;
+ var files = new List<FileSystemMetadata>();
+ for (var i = 0; i < count; i++)
{
- new GenericVideoResolver<Trailer>(_namingOptions)
- };
-
- return ResolvePaths(files, directoryService, null, new LibraryOptions(), null, resolvers)
- .OfType<Trailer>()
- .Select(video =>
+ var current = fileSystemChildren[i];
+ if (current.IsDirectory && BaseItem.AllExtrasTypesFolderNames.ContainsKey(current.Name))
{
- // Try to retrieve it from the db. If we don't find it, use the resolved version
- if (GetItemById(video.Id) is Trailer dbItem)
- {
- video = dbItem;
- }
-
- video.ParentId = Guid.Empty;
- video.OwnerId = owner.Id;
- video.ExtraType = ExtraType.Trailer;
- video.TrailerTypes = new[] { TrailerType.LocalTrailer };
-
- return video;
-
- // Sort them so that the list can be easily compared for changes
- }).OrderBy(i => i.Path);
- }
-
- public IEnumerable<Video> FindExtras(BaseItem owner, List<FileSystemMetadata> fileSystemChildren, IDirectoryService directoryService)
- {
- var namingOptions = _namingOptions;
-
- var files = owner.IsInMixedFolder ? new List<FileSystemMetadata>() : fileSystemChildren.Where(i => i.IsDirectory)
- .Where(i => BaseItem.AllExtrasTypesFolderNames.ContainsKey(i.Name ?? string.Empty))
- .SelectMany(i => _fileSystem.GetFiles(i.FullName, namingOptions.VideoFileExtensions, false, false))
- .ToList();
-
- var videos = VideoListResolver.Resolve(fileSystemChildren, namingOptions);
-
- var currentVideo = videos.FirstOrDefault(i => string.Equals(owner.Path, i.Files[0].Path, StringComparison.OrdinalIgnoreCase));
+ files.AddRange(_fileSystem.GetFiles(current.FullName, _namingOptions.VideoFileExtensions, false, false));
+ }
+ else if (!current.IsDirectory)
+ {
+ files.Add(current);
+ }
+ }
- if (currentVideo != null)
+ if (files.Count == 0)
{
- files.AddRange(currentVideo.Extras.Where(i => i.ExtraType != ExtraType.Trailer).Select(i => _fileSystem.GetFileInfo(i.Path)));
+ yield break;
}
- return ResolvePaths(files, directoryService, null, new LibraryOptions(), null)
- .OfType<Video>()
- .Select(video =>
- {
- // Try to retrieve it from the db. If we don't find it, use the resolved version
- var dbItem = GetItemById(video.Id) as Video;
+ var videos = VideoListResolver.Resolve(files, _namingOptions);
+ // owner video info cannot be null as that implies it has no path
+ var extras = ExtraResolver.GetExtras(videos, ownerVideoInfo, _namingOptions.VideoFlagDelimiters);
- if (dbItem != null)
- {
- video = dbItem;
- }
-
- video.ParentId = Guid.Empty;
- video.OwnerId = owner.Id;
-
- SetExtraTypeFromFilename(video);
+ for (var i = 0; i < extras.Count; i++)
+ {
+ var currentExtra = extras[i];
+ var resolved = ResolvePath(_fileSystem.GetFileInfo(currentExtra.Path));
+ if (resolved is not Video video)
+ {
+ continue;
+ }
- return video;
+ // Try to retrieve it from the db. If we don't find it, use the resolved version
+ if (GetItemById(resolved.Id) is Video dbItem)
+ {
+ video = dbItem;
+ }
- // Sort them so that the list can be easily compared for changes
- }).OrderBy(i => i.Path);
+ video.ExtraType = currentExtra.ExtraType;
+ video.ParentId = Guid.Empty;
+ video.OwnerId = owner.Id;
+ yield return video;
+ }
}
public string GetPathAfterNetworkSubstitution(string path, BaseItem ownerItem)
@@ -2817,15 +2784,6 @@ namespace Emby.Server.Implementations.Library
return path;
}
- private void SetExtraTypeFromFilename(Video item)
- {
- var resolver = new ExtraResolver(_namingOptions);
-
- var result = resolver.GetExtraInfo(item.Path);
-
- item.ExtraType = result.ExtraType;
- }
-
public List<PersonInfo> GetPeople(InternalPeopleQuery query)
{
return _itemRepository.GetPeople(query);
diff --git a/Emby.Server.Implementations/Library/Resolvers/BaseVideoResolver.cs b/Emby.Server.Implementations/Library/Resolvers/BaseVideoResolver.cs
index 0ebf0e530..9222a9479 100644
--- a/Emby.Server.Implementations/Library/Resolvers/BaseVideoResolver.cs
+++ b/Emby.Server.Implementations/Library/Resolvers/BaseVideoResolver.cs
@@ -49,120 +49,71 @@ namespace Emby.Server.Implementations.Library.Resolvers
protected virtual TVideoType ResolveVideo<TVideoType>(ItemResolveArgs args, bool parseName)
where TVideoType : Video, new()
{
- var namingOptions = NamingOptions;
+ VideoFileInfo videoInfo = null;
+ VideoType? videoType = null;
// If the path is a file check for a matching extensions
if (args.IsDirectory)
{
- TVideoType video = null;
- VideoFileInfo videoInfo = null;
-
// Loop through each child file/folder and see if we find a video
foreach (var child in args.FileSystemChildren)
{
var filename = child.Name;
-
if (child.IsDirectory)
{
if (IsDvdDirectory(child.FullName, filename, args.DirectoryService))
{
- videoInfo = VideoResolver.ResolveDirectory(args.Path, namingOptions);
-
- if (videoInfo == null)
- {
- return null;
- }
-
- video = new TVideoType
- {
- Path = args.Path,
- VideoType = VideoType.Dvd,
- ProductionYear = videoInfo.Year
- };
- break;
+ videoType = VideoType.Dvd;
}
-
- if (IsBluRayDirectory(filename))
+ else if (IsBluRayDirectory(filename))
{
- videoInfo = VideoResolver.ResolveDirectory(args.Path, namingOptions);
-
- if (videoInfo == null)
- {
- return null;
- }
-
- video = new TVideoType
- {
- Path = args.Path,
- VideoType = VideoType.BluRay,
- ProductionYear = videoInfo.Year
- };
- break;
+ videoType = VideoType.BluRay;
}
}
else if (IsDvdFile(filename))
{
- videoInfo = VideoResolver.ResolveDirectory(args.Path, namingOptions);
-
- if (videoInfo == null)
- {
- return null;
- }
-
- video = new TVideoType
- {
- Path = args.Path,
- VideoType = VideoType.Dvd,
- ProductionYear = videoInfo.Year
- };
- break;
+ videoType = VideoType.Dvd;
}
- }
- if (video != null)
- {
- video.Name = parseName ?
- videoInfo.Name :
- Path.GetFileName(args.Path);
+ if (videoType == null)
+ {
+ continue;
+ }
- Set3DFormat(video, videoInfo);
+ videoInfo = VideoResolver.ResolveDirectory(args.Path, NamingOptions, parseName);
+ break;
}
-
- return video;
}
else
{
- var videoInfo = VideoResolver.Resolve(args.Path, false, namingOptions, false);
-
- if (videoInfo == null)
- {
- return null;
- }
-
- if (VideoResolver.IsVideoFile(args.Path, NamingOptions) || videoInfo.IsStub)
- {
- var path = args.Path;
-
- var video = new TVideoType
- {
- Path = path,
- IsInMixedFolder = true,
- ProductionYear = videoInfo.Year
- };
-
- SetVideoType(video, videoInfo);
+ videoInfo = VideoResolver.Resolve(args.Path, false, NamingOptions, parseName);
+ }
- video.Name = parseName ?
- videoInfo.Name :
- Path.GetFileNameWithoutExtension(args.Path);
+ if (videoInfo == null || (!videoInfo.IsStub && !VideoResolver.IsVideoFile(args.Path, NamingOptions)))
+ {
+ return null;
+ }
- Set3DFormat(video, videoInfo);
+ var video = new TVideoType
+ {
+ Name = videoInfo.Name,
+ Path = args.Path,
+ ProductionYear = videoInfo.Year,
+ ExtraType = videoInfo.ExtraType
+ };
- return video;
- }
+ if (videoType.HasValue)
+ {
+ video.VideoType = videoType.Value;
}
+ else
+ {
+ SetVideoType(video, videoInfo);
+ }
+
+ Set3DFormat(video, videoInfo);
- return null;
+ return video;
}
protected void SetVideoType(Video video, VideoFileInfo videoInfo)
@@ -207,8 +158,8 @@ namespace Emby.Server.Implementations.Library.Resolvers
{
// use disc-utils, both DVDs and BDs use UDF filesystem
using (var videoFileStream = File.Open(video.Path, FileMode.Open, FileAccess.Read))
+ using (UdfReader udfReader = new UdfReader(videoFileStream))
{
- UdfReader udfReader = new UdfReader(videoFileStream);
if (udfReader.DirectoryExists("VIDEO_TS"))
{
video.IsoType = IsoType.Dvd;
diff --git a/Emby.Server.Implementations/Library/Resolvers/Movies/MovieResolver.cs b/Emby.Server.Implementations/Library/Resolvers/Movies/MovieResolver.cs
index 732be0fe5..58279af9e 100644
--- a/Emby.Server.Implementations/Library/Resolvers/Movies/MovieResolver.cs
+++ b/Emby.Server.Implementations/Library/Resolvers/Movies/MovieResolver.cs
@@ -26,7 +26,6 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies
public class MovieResolver : BaseVideoResolver<Video>, IMultiItemResolver
{
private readonly IImageProcessor _imageProcessor;
- private readonly StackResolver _stackResolver;
private string[] _validCollectionTypes = new[]
{
@@ -46,7 +45,6 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies
: base(namingOptions)
{
_imageProcessor = imageProcessor;
- _stackResolver = new StackResolver(NamingOptions);
}
/// <summary>
@@ -62,7 +60,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies
string collectionType,
IDirectoryService directoryService)
{
- var result = ResolveMultipleInternal(parent, files, collectionType, directoryService);
+ var result = ResolveMultipleInternal(parent, files, collectionType);
if (result != null)
{
@@ -92,16 +90,17 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies
return null;
}
+ Video movie = null;
var files = args.GetActualFileSystemChildren().ToList();
if (string.Equals(collectionType, CollectionType.MusicVideos, StringComparison.OrdinalIgnoreCase))
{
- return FindMovie<MusicVideo>(args, args.Path, args.Parent, files, args.DirectoryService, collectionType, false);
+ movie = FindMovie<MusicVideo>(args, args.Path, args.Parent, files, args.DirectoryService, collectionType, false);
}
if (string.Equals(collectionType, CollectionType.HomeVideos, StringComparison.OrdinalIgnoreCase))
{
- return FindMovie<Video>(args, args.Path, args.Parent, files, args.DirectoryService, collectionType, false);
+ movie = FindMovie<Video>(args, args.Path, args.Parent, files, args.DirectoryService, collectionType, false);
}
if (string.IsNullOrEmpty(collectionType))
@@ -118,17 +117,16 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies
return null;
}
- {
- return FindMovie<Movie>(args, args.Path, args.Parent, files, args.DirectoryService, collectionType, true);
- }
+ movie = FindMovie<Movie>(args, args.Path, args.Parent, files, args.DirectoryService, collectionType, true);
}
if (string.Equals(collectionType, CollectionType.Movies, StringComparison.OrdinalIgnoreCase))
{
- return FindMovie<Movie>(args, args.Path, args.Parent, files, args.DirectoryService, collectionType, true);
+ movie = FindMovie<Movie>(args, args.Path, args.Parent, files, args.DirectoryService, collectionType, true);
}
- return null;
+ // ignore extras
+ return movie?.ExtraType == null ? movie : null;
}
// Handle owned items
@@ -169,6 +167,12 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies
item = ResolveVideo<Video>(args, false);
}
+ // Ignore extras
+ if (item?.ExtraType != null)
+ {
+ return null;
+ }
+
if (item != null)
{
item.IsInMixedFolder = true;
@@ -180,8 +184,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies
private MultiItemResolverResult ResolveMultipleInternal(
Folder parent,
List<FileSystemMetadata> files,
- string collectionType,
- IDirectoryService directoryService)
+ string collectionType)
{
if (IsInvalid(parent, collectionType))
{
@@ -190,13 +193,13 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies
if (string.Equals(collectionType, CollectionType.MusicVideos, StringComparison.OrdinalIgnoreCase))
{
- return ResolveVideos<MusicVideo>(parent, files, directoryService, true, collectionType, false);
+ return ResolveVideos<MusicVideo>(parent, files, true, collectionType, false);
}
if (string.Equals(collectionType, CollectionType.HomeVideos, StringComparison.OrdinalIgnoreCase) ||
string.Equals(collectionType, CollectionType.Photos, StringComparison.OrdinalIgnoreCase))
{
- return ResolveVideos<Video>(parent, files, directoryService, false, collectionType, false);
+ return ResolveVideos<Video>(parent, files, false, collectionType, false);
}
if (string.IsNullOrEmpty(collectionType))
@@ -204,7 +207,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies
// Owned items should just use the plain video type
if (parent == null)
{
- return ResolveVideos<Video>(parent, files, directoryService, false, collectionType, false);
+ return ResolveVideos<Video>(parent, files, false, collectionType, false);
}
if (parent is Series || parent.GetParents().OfType<Series>().Any())
@@ -212,12 +215,12 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies
return null;
}
- return ResolveVideos<Movie>(parent, files, directoryService, false, collectionType, true);
+ return ResolveVideos<Movie>(parent, files, false, collectionType, true);
}
if (string.Equals(collectionType, CollectionType.Movies, StringComparison.OrdinalIgnoreCase))
{
- return ResolveVideos<Movie>(parent, files, directoryService, true, collectionType, true);
+ return ResolveVideos<Movie>(parent, files, true, collectionType, true);
}
return null;
@@ -226,21 +229,20 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies
private MultiItemResolverResult ResolveVideos<T>(
Folder parent,
IEnumerable<FileSystemMetadata> fileSystemEntries,
- IDirectoryService directoryService,
- bool suppportMultiEditions,
+ bool supportMultiEditions,
string collectionType,
bool parseName)
where T : Video, new()
{
var files = new List<FileSystemMetadata>();
- var videos = new List<BaseItem>();
var leftOver = new List<FileSystemMetadata>();
+ var hasCollectionType = !string.IsNullOrEmpty(collectionType);
// Loop through each child file/folder and see if we find a video
foreach (var child in fileSystemEntries)
{
// This is a hack but currently no better way to resolve a sometimes ambiguous situation
- if (string.IsNullOrEmpty(collectionType))
+ if (!hasCollectionType)
{
if (string.Equals(child.Name, "tvshow.nfo", StringComparison.OrdinalIgnoreCase)
|| string.Equals(child.Name, "season.nfo", StringComparison.OrdinalIgnoreCase))
@@ -259,29 +261,35 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies
}
}
- var resolverResult = VideoListResolver.Resolve(files, NamingOptions, suppportMultiEditions).ToList();
+ var resolverResult = VideoListResolver.Resolve(files, NamingOptions, supportMultiEditions, parseName);
var result = new MultiItemResolverResult
{
- ExtraFiles = leftOver,
- Items = videos
+ ExtraFiles = leftOver
};
- var isInMixedFolder = resolverResult.Count > 1 || (parent != null && parent.IsTopParent);
+ var isInMixedFolder = resolverResult.Count > 1 || parent?.IsTopParent == true;
foreach (var video in resolverResult)
{
var firstVideo = video.Files[0];
+ var path = firstVideo.Path;
+ if (video.ExtraType != null)
+ {
+ // TODO
+ result.ExtraFiles.Add(files.First(f => string.Equals(f.FullName, path, StringComparison.OrdinalIgnoreCase)));
+ continue;
+ }
+
+ var additionalParts = video.Files.Count > 1 ? video.Files.Skip(1).Select(i => i.Path).ToArray() : Array.Empty<string>();
var videoItem = new T
{
- Path = video.Files[0].Path,
+ Path = path,
IsInMixedFolder = isInMixedFolder,
ProductionYear = video.Year,
- Name = parseName ?
- video.Name :
- Path.GetFileNameWithoutExtension(video.Files[0].Path),
- AdditionalParts = video.Files.Skip(1).Select(i => i.Path).ToArray(),
+ Name = parseName ? video.Name : firstVideo.Name,
+ AdditionalParts = additionalParts,
LocalAlternateVersions = video.AlternateVersions.Select(i => i.Path).ToArray()
};
@@ -299,21 +307,34 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies
private static bool IsIgnored(string filename)
{
// Ignore samples
- Match m = Regex.Match(filename, @"\bsample\b", RegexOptions.IgnoreCase);
+ Match m = Regex.Match(filename, @"\bsample\b", RegexOptions.IgnoreCase | RegexOptions.Compiled);
return m.Success;
}
- private bool ContainsFile(List<VideoInfo> result, FileSystemMetadata file)
+ private static bool ContainsFile(IReadOnlyList<VideoInfo> result, FileSystemMetadata file)
{
- return result.Any(i => ContainsFile(i, file));
- }
+ for (var i = 0; i < result.Count; i++)
+ {
+ var current = result[i];
+ for (var j = 0; j < current.Files.Count; j++)
+ {
+ if (ContainsFile(current.Files[j], file))
+ {
+ return true;
+ }
+ }
- private bool ContainsFile(VideoInfo result, FileSystemMetadata file)
- {
- return result.Files.Any(i => ContainsFile(i, file)) ||
- result.AlternateVersions.Any(i => ContainsFile(i, file)) ||
- result.Extras.Any(i => ContainsFile(i, file));
+ for (var j = 0; j < current.AlternateVersions.Count; j++)
+ {
+ if (ContainsFile(current.AlternateVersions[j], file))
+ {
+ return true;
+ }
+ }
+ }
+
+ return false;
}
private static bool ContainsFile(VideoFileInfo result, FileSystemMetadata file)
@@ -431,7 +452,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies
// TODO: Allow GetMultiDiscMovie in here
const bool SupportsMultiVersion = true;
- var result = ResolveVideos<T>(parent, fileSystemEntries, directoryService, SupportsMultiVersion, collectionType, parseName) ??
+ var result = ResolveVideos<T>(parent, fileSystemEntries, SupportsMultiVersion, collectionType, parseName) ??
new MultiItemResolverResult();
if (result.Items.Count == 1)
@@ -510,7 +531,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies
return null;
}
- var result = _stackResolver.ResolveDirectories(folderPaths).ToList();
+ var result = StackResolver.ResolveDirectories(folderPaths, NamingOptions).ToList();
if (result.Count != 1)
{
diff --git a/Emby.Server.Implementations/Library/Resolvers/TV/EpisodeResolver.cs b/Emby.Server.Implementations/Library/Resolvers/TV/EpisodeResolver.cs
index f72da3617..928cd42dd 100644
--- a/Emby.Server.Implementations/Library/Resolvers/TV/EpisodeResolver.cs
+++ b/Emby.Server.Implementations/Library/Resolvers/TV/EpisodeResolver.cs
@@ -45,34 +45,36 @@ namespace Emby.Server.Implementations.Library.Resolvers.TV
// If the parent is a Season or Series and the parent is not an extras folder, then this is an Episode if the VideoResolver returns something
// Also handle flat tv folders
- if ((season != null ||
- string.Equals(args.GetCollectionType(), CollectionType.TvShows, StringComparison.OrdinalIgnoreCase) ||
- args.HasParent<Series>())
- && (parent is Series || !BaseItem.AllExtrasTypesFolderNames.ContainsKey(parent.Name)))
+ if (season != null ||
+ string.Equals(args.GetCollectionType(), CollectionType.TvShows, StringComparison.OrdinalIgnoreCase) ||
+ args.HasParent<Series>())
{
var episode = ResolveVideo<Episode>(args, false);
- if (episode != null)
+ // Ignore extras
+ if (episode == null || episode.ExtraType != null)
{
- var series = parent as Series ?? parent.GetParents().OfType<Series>().FirstOrDefault();
+ return null;
+ }
- if (series != null)
- {
- episode.SeriesId = series.Id;
- episode.SeriesName = series.Name;
- }
+ var series = parent as Series ?? parent.GetParents().OfType<Series>().FirstOrDefault();
- if (season != null)
- {
- episode.SeasonId = season.Id;
- episode.SeasonName = season.Name;
- }
+ if (series != null)
+ {
+ episode.SeriesId = series.Id;
+ episode.SeriesName = series.Name;
+ }
- // Assume season 1 if there's no season folder and a season number could not be determined
- if (season == null && !episode.ParentIndexNumber.HasValue && (episode.IndexNumber.HasValue || episode.PremiereDate.HasValue))
- {
- episode.ParentIndexNumber = 1;
- }
+ if (season != null)
+ {
+ episode.SeasonId = season.Id;
+ episode.SeasonName = season.Name;
+ }
+
+ // Assume season 1 if there's no season folder and a season number could not be determined
+ if (season == null && !episode.ParentIndexNumber.HasValue && (episode.IndexNumber.HasValue || episode.PremiereDate.HasValue))
+ {
+ episode.ParentIndexNumber = 1;
}
return episode;
diff --git a/Jellyfin.Api/Controllers/UserLibraryController.cs b/Jellyfin.Api/Controllers/UserLibraryController.cs
index a33a0826c..249379619 100644
--- a/Jellyfin.Api/Controllers/UserLibraryController.cs
+++ b/Jellyfin.Api/Controllers/UserLibraryController.cs
@@ -213,7 +213,7 @@ namespace Jellyfin.Api.Controllers
if (item is IHasTrailers hasTrailers)
{
- var trailers = hasTrailers.GetTrailers();
+ var trailers = hasTrailers.LocalTrailers;
var dtosTrailers = _dtoService.GetBaseItemDtos(trailers, dtoOptions, user, item);
var allTrailers = new BaseItemDto[dtosExtras.Length + dtosTrailers.Count];
dtosExtras.CopyTo(allTrailers, 0);
diff --git a/MediaBrowser.Controller/Entities/BaseItem.cs b/MediaBrowser.Controller/Entities/BaseItem.cs
index b1ac2fe8e..ad20ea334 100644
--- a/MediaBrowser.Controller/Entities/BaseItem.cs
+++ b/MediaBrowser.Controller/Entities/BaseItem.cs
@@ -40,9 +40,7 @@ namespace MediaBrowser.Controller.Entities
/// </summary>
public abstract class BaseItem : IHasProviderIds, IHasLookupInfo<ItemLookupInfo>, IEquatable<BaseItem>
{
- /// <summary>
- /// The trailer folder name.
- /// </summary>
+ public const string TrailerFileName = "trailer";
public const string TrailersFolderName = "trailers";
public const string ThemeSongsFolderName = "theme-music";
public const string ThemeSongFileName = "theme";
@@ -99,8 +97,6 @@ namespace MediaBrowser.Controller.Entities
};
private string _sortName;
- private Guid[] _themeSongIds;
- private Guid[] _themeVideoIds;
private string _forcedSortName;
@@ -122,40 +118,6 @@ namespace MediaBrowser.Controller.Entities
}
[JsonIgnore]
- public Guid[] ThemeSongIds
- {
- get
- {
- return _themeSongIds ??= GetExtras()
- .Where(extra => extra.ExtraType == Model.Entities.ExtraType.ThemeSong)
- .Select(song => song.Id)
- .ToArray();
- }
-
- private set
- {
- _themeSongIds = value;
- }
- }
-
- [JsonIgnore]
- public Guid[] ThemeVideoIds
- {
- get
- {
- return _themeVideoIds ??= GetExtras()
- .Where(extra => extra.ExtraType == Model.Entities.ExtraType.ThemeVideo)
- .Select(song => song.Id)
- .ToArray();
- }
-
- private set
- {
- _themeVideoIds = value;
- }
- }
-
- [JsonIgnore]
public string PreferredMetadataCountryCode { get; set; }
[JsonIgnore]
@@ -1379,28 +1341,6 @@ namespace MediaBrowser.Controller.Entities
}).OrderBy(i => i.Path).ToArray();
}
- protected virtual BaseItem[] LoadExtras(List<FileSystemMetadata> fileSystemChildren, IDirectoryService directoryService)
- {
- return fileSystemChildren
- .Where(child => child.IsDirectory && AllExtrasTypesFolderNames.ContainsKey(child.Name))
- .SelectMany(folder => LibraryManager
- .ResolvePaths(FileSystem.GetFiles(folder.FullName), directoryService, null, new LibraryOptions())
- .OfType<Video>()
- .Select(video =>
- {
- // Try to retrieve it from the db. If we don't find it, use the resolved version
- if (LibraryManager.GetItemById(video.Id) is Video dbItem)
- {
- video = dbItem;
- }
-
- video.ExtraType = AllExtrasTypesFolderNames[folder.Name];
- return video;
- })
- .OrderBy(video => video.Path)) // Sort them so that the list can be easily compared for changes
- .ToArray();
- }
-
public Task RefreshMetadata(CancellationToken cancellationToken)
{
return RefreshMetadata(new MetadataRefreshOptions(new DirectoryService(FileSystem)), cancellationToken);
@@ -1434,13 +1374,8 @@ namespace MediaBrowser.Controller.Entities
GetFileSystemChildren(options.DirectoryService).ToList() :
new List<FileSystemMetadata>();
- var ownedItemsChanged = await RefreshedOwnedItems(options, files, cancellationToken).ConfigureAwait(false);
+ requiresSave = await RefreshedOwnedItems(options, files, cancellationToken).ConfigureAwait(false);
await LibraryManager.UpdateImagesAsync(this).ConfigureAwait(false); // ensure all image properties in DB are fresh
-
- if (ownedItemsChanged)
- {
- requiresSave = true;
- }
}
catch (Exception ex)
{
@@ -1516,35 +1451,12 @@ namespace MediaBrowser.Controller.Entities
/// <returns><c>true</c> if any items have changed, else <c>false</c>.</returns>
protected virtual async Task<bool> RefreshedOwnedItems(MetadataRefreshOptions options, List<FileSystemMetadata> fileSystemChildren, CancellationToken cancellationToken)
{
- var themeSongsChanged = false;
-
- var themeVideosChanged = false;
-
- var extrasChanged = false;
-
- var localTrailersChanged = false;
-
- if (IsFileProtocol && SupportsOwnedItems)
+ if (!IsFileProtocol || !SupportsOwnedItems || IsInMixedFolder || this is ICollectionFolder)
{
- if (SupportsThemeMedia)
- {
- if (!IsInMixedFolder)
- {
- themeSongsChanged = await RefreshThemeSongs(this, options, fileSystemChildren, cancellationToken).ConfigureAwait(false);
-
- themeVideosChanged = await RefreshThemeVideos(this, options, fileSystemChildren, cancellationToken).ConfigureAwait(false);
-
- extrasChanged = await RefreshExtras(this, options, fileSystemChildren, cancellationToken).ConfigureAwait(false);
- }
- }
-
- if (this is IHasTrailers hasTrailers)
- {
- localTrailersChanged = await RefreshLocalTrailers(hasTrailers, options, fileSystemChildren, cancellationToken).ConfigureAwait(false);
- }
+ return false;
}
- return themeSongsChanged || themeVideosChanged || extrasChanged || localTrailersChanged;
+ return await RefreshExtras(this, options, fileSystemChildren, cancellationToken).ConfigureAwait(false);
}
protected virtual FileSystemMetadata[] GetFileSystemChildren(IDirectoryService directoryService)
@@ -1554,98 +1466,24 @@ namespace MediaBrowser.Controller.Entities
return directoryService.GetFileSystemEntries(path);
}
- private async Task<bool> RefreshLocalTrailers(IHasTrailers item, MetadataRefreshOptions options, List<FileSystemMetadata> fileSystemChildren, CancellationToken cancellationToken)
- {
- var newItems = LibraryManager.FindTrailers(this, fileSystemChildren, options.DirectoryService);
-
- var newItemIds = newItems.Select(i => i.Id);
-
- var itemsChanged = !item.LocalTrailerIds.SequenceEqual(newItemIds);
- var ownerId = item.Id;
-
- var tasks = newItems.Select(i =>
- {
- var subOptions = new MetadataRefreshOptions(options);
-
- if (i.ExtraType != Model.Entities.ExtraType.Trailer ||
- i.OwnerId != ownerId ||
- !i.ParentId.Equals(Guid.Empty))
- {
- i.ExtraType = Model.Entities.ExtraType.Trailer;
- i.OwnerId = ownerId;
- i.ParentId = Guid.Empty;
- subOptions.ForceSave = true;
- }
-
- return RefreshMetadataForOwnedItem(i, true, subOptions, cancellationToken);
- });
-
- await Task.WhenAll(tasks).ConfigureAwait(false);
-
- item.LocalTrailerIds = newItemIds.ToArray();
-
- return itemsChanged;
- }
-
private async Task<bool> RefreshExtras(BaseItem item, MetadataRefreshOptions options, List<FileSystemMetadata> fileSystemChildren, CancellationToken cancellationToken)
{
- var extras = LoadExtras(fileSystemChildren, options.DirectoryService);
- var themeVideos = LoadThemeVideos(fileSystemChildren, options.DirectoryService);
- var themeSongs = LoadThemeSongs(fileSystemChildren, options.DirectoryService);
- var newExtras = new BaseItem[extras.Length + themeVideos.Length + themeSongs.Length];
- extras.CopyTo(newExtras, 0);
- themeVideos.CopyTo(newExtras, extras.Length);
- themeSongs.CopyTo(newExtras, extras.Length + themeVideos.Length);
-
- var newExtraIds = newExtras.Select(i => i.Id).ToArray();
-
+ var extras = LibraryManager.FindExtras(item, fileSystemChildren).ToArray();
+ var newExtraIds = extras.Select(i => i.Id).ToArray();
var extrasChanged = !item.ExtraIds.SequenceEqual(newExtraIds);
- if (extrasChanged)
+ if (!extrasChanged)
{
- var ownerId = item.Id;
-
- var tasks = newExtras.Select(i =>
- {
- var subOptions = new MetadataRefreshOptions(options);
- if (i.OwnerId != ownerId || i.ParentId != Guid.Empty)
- {
- i.OwnerId = ownerId;
- i.ParentId = Guid.Empty;
- subOptions.ForceSave = true;
- }
-
- return RefreshMetadataForOwnedItem(i, true, subOptions, cancellationToken);
- });
-
- await Task.WhenAll(tasks).ConfigureAwait(false);
-
- item.ExtraIds = newExtraIds;
+ return false;
}
- return extrasChanged;
- }
-
- private async Task<bool> RefreshThemeVideos(BaseItem item, MetadataRefreshOptions options, IEnumerable<FileSystemMetadata> fileSystemChildren, CancellationToken cancellationToken)
- {
- var newThemeVideos = LoadThemeVideos(fileSystemChildren, options.DirectoryService);
-
- var newThemeVideoIds = newThemeVideos.Select(i => i.Id).ToArray();
-
- var themeVideosChanged = !item.ThemeVideoIds.SequenceEqual(newThemeVideoIds);
-
var ownerId = item.Id;
- var tasks = newThemeVideos.Select(i =>
+ var tasks = extras.Select(i =>
{
var subOptions = new MetadataRefreshOptions(options);
-
- if (!i.ExtraType.HasValue ||
- i.ExtraType.Value != Model.Entities.ExtraType.ThemeVideo ||
- i.OwnerId != ownerId ||
- !i.ParentId.Equals(Guid.Empty))
+ if (i.OwnerId != ownerId || i.ParentId != Guid.Empty)
{
- i.ExtraType = Model.Entities.ExtraType.ThemeVideo;
i.OwnerId = ownerId;
i.ParentId = Guid.Empty;
subOptions.ForceSave = true;
@@ -1656,48 +1494,9 @@ namespace MediaBrowser.Controller.Entities
await Task.WhenAll(tasks).ConfigureAwait(false);
- // They are expected to be sorted by SortName
- item.ThemeVideoIds = newThemeVideos.OrderBy(i => i.SortName).Select(i => i.Id).ToArray();
-
- return themeVideosChanged;
- }
-
- /// <summary>
- /// Refreshes the theme songs.
- /// </summary>
- private async Task<bool> RefreshThemeSongs(BaseItem item, MetadataRefreshOptions options, List<FileSystemMetadata> fileSystemChildren, CancellationToken cancellationToken)
- {
- var newThemeSongs = LoadThemeSongs(fileSystemChildren, options.DirectoryService);
- var newThemeSongIds = newThemeSongs.Select(i => i.Id).ToArray();
-
- var themeSongsChanged = !item.ThemeSongIds.SequenceEqual(newThemeSongIds);
-
- var ownerId = item.Id;
-
- var tasks = newThemeSongs.Select(i =>
- {
- var subOptions = new MetadataRefreshOptions(options);
+ item.ExtraIds = newExtraIds;
- if (!i.ExtraType.HasValue ||
- i.ExtraType.Value != Model.Entities.ExtraType.ThemeSong ||
- i.OwnerId != ownerId ||
- !i.ParentId.Equals(Guid.Empty))
- {
- i.ExtraType = Model.Entities.ExtraType.ThemeSong;
- i.OwnerId = ownerId;
- i.ParentId = Guid.Empty;
- subOptions.ForceSave = true;
- }
-
- return RefreshMetadataForOwnedItem(i, true, subOptions, cancellationToken);
- });
-
- await Task.WhenAll(tasks).ConfigureAwait(false);
-
- // They are expected to be sorted by SortName
- item.ThemeSongIds = newThemeSongs.OrderBy(i => i.SortName).Select(i => i.Id).ToArray();
-
- return themeSongsChanged;
+ return true;
}
public string GetPresentationUniqueKey()
@@ -2891,14 +2690,14 @@ namespace MediaBrowser.Controller.Entities
StringComparison.OrdinalIgnoreCase);
}
- public IEnumerable<BaseItem> GetThemeSongs()
+ public IReadOnlyList<BaseItem> GetThemeSongs()
{
- return ThemeSongIds.Select(LibraryManager.GetItemById);
+ return GetExtras().Where(e => e.ExtraType == Model.Entities.ExtraType.ThemeSong).ToArray();
}
- public IEnumerable<BaseItem> GetThemeVideos()
+ public IReadOnlyList<BaseItem> GetThemeVideos()
{
- return ThemeVideoIds.Select(LibraryManager.GetItemById);
+ return GetExtras().Where(e => e.ExtraType == Model.Entities.ExtraType.ThemeVideo).ToArray();
}
/// <summary>
diff --git a/MediaBrowser.Controller/Entities/IHasTrailers.cs b/MediaBrowser.Controller/Entities/IHasTrailers.cs
index f4271678d..bb4a6ea94 100644
--- a/MediaBrowser.Controller/Entities/IHasTrailers.cs
+++ b/MediaBrowser.Controller/Entities/IHasTrailers.cs
@@ -2,7 +2,6 @@
#pragma warning disable CS1591
-using System;
using System.Collections.Generic;
using MediaBrowser.Model.Entities;
@@ -17,18 +16,10 @@ namespace MediaBrowser.Controller.Entities
IReadOnlyList<MediaUrl> RemoteTrailers { get; set; }
/// <summary>
- /// Gets or sets the local trailer ids.
+ /// Gets the local trailers.
/// </summary>
- /// <value>The local trailer ids.</value>
- IReadOnlyList<Guid> LocalTrailerIds { get; set; }
-
- /// <summary>
- /// Gets or sets the remote trailer ids.
- /// </summary>
- /// <value>The remote trailer ids.</value>
- IReadOnlyList<Guid> RemoteTrailerIds { get; set; }
-
- Guid Id { get; set; }
+ /// <value>The local trailers.</value>
+ IReadOnlyList<BaseItem> LocalTrailers { get; }
}
/// <summary>
@@ -42,57 +33,6 @@ namespace MediaBrowser.Controller.Entities
/// <param name="item">Media item.</param>
/// <returns><see cref="IReadOnlyList{Guid}" />.</returns>
public static int GetTrailerCount(this IHasTrailers item)
- => item.LocalTrailerIds.Count + item.RemoteTrailerIds.Count;
-
- /// <summary>
- /// Gets the trailer ids.
- /// </summary>
- /// <param name="item">Media item.</param>
- /// <returns><see cref="IReadOnlyList{Guid}" />.</returns>
- public static IReadOnlyList<Guid> GetTrailerIds(this IHasTrailers item)
- {
- var localIds = item.LocalTrailerIds;
- var remoteIds = item.RemoteTrailerIds;
-
- var all = new Guid[localIds.Count + remoteIds.Count];
- var index = 0;
- foreach (var id in localIds)
- {
- all[index++] = id;
- }
-
- foreach (var id in remoteIds)
- {
- all[index++] = id;
- }
-
- return all;
- }
-
- /// <summary>
- /// Gets the trailers.
- /// </summary>
- /// <param name="item">Media item.</param>
- /// <returns><see cref="IReadOnlyList{BaseItem}" />.</returns>
- public static IReadOnlyList<BaseItem> GetTrailers(this IHasTrailers item)
- {
- var localIds = item.LocalTrailerIds;
- var remoteIds = item.RemoteTrailerIds;
- var libraryManager = BaseItem.LibraryManager;
-
- var all = new BaseItem[localIds.Count + remoteIds.Count];
- var index = 0;
- foreach (var id in localIds)
- {
- all[index++] = libraryManager.GetItemById(id);
- }
-
- foreach (var id in remoteIds)
- {
- all[index++] = libraryManager.GetItemById(id);
- }
-
- return all;
- }
+ => item.LocalTrailers.Count + item.RemoteTrailers.Count;
}
}
diff --git a/MediaBrowser.Controller/Entities/Movies/BoxSet.cs b/MediaBrowser.Controller/Entities/Movies/BoxSet.cs
index e46f99cd5..6b93d8d87 100644
--- a/MediaBrowser.Controller/Entities/Movies/BoxSet.cs
+++ b/MediaBrowser.Controller/Entities/Movies/BoxSet.cs
@@ -9,7 +9,6 @@ using System.Text.Json.Serialization;
using Jellyfin.Data.Entities;
using Jellyfin.Data.Enums;
using MediaBrowser.Controller.Providers;
-using MediaBrowser.Model.Entities;
using MediaBrowser.Model.Querying;
namespace MediaBrowser.Controller.Entities.Movies
@@ -21,10 +20,6 @@ namespace MediaBrowser.Controller.Entities.Movies
{
public BoxSet()
{
- RemoteTrailers = Array.Empty<MediaUrl>();
- LocalTrailerIds = Array.Empty<Guid>();
- RemoteTrailerIds = Array.Empty<Guid>();
-
DisplayOrder = ItemSortBy.PremiereDate;
}
@@ -38,10 +33,9 @@ namespace MediaBrowser.Controller.Entities.Movies
public override bool SupportsPeople => true;
/// <inheritdoc />
- public IReadOnlyList<Guid> LocalTrailerIds { get; set; }
-
- /// <inheritdoc />
- public IReadOnlyList<Guid> RemoteTrailerIds { get; set; }
+ public IReadOnlyList<BaseItem> LocalTrailers => GetExtras()
+ .Where(extra => extra.ExtraType == Model.Entities.ExtraType.Trailer)
+ .ToArray();
/// <summary>
/// Gets or sets the display order.
diff --git a/MediaBrowser.Controller/Entities/Movies/Movie.cs b/MediaBrowser.Controller/Entities/Movies/Movie.cs
index b54bbf5eb..6f1a0a8cf 100644
--- a/MediaBrowser.Controller/Entities/Movies/Movie.cs
+++ b/MediaBrowser.Controller/Entities/Movies/Movie.cs
@@ -7,12 +7,9 @@ using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Text.Json.Serialization;
-using System.Threading;
-using System.Threading.Tasks;
using Jellyfin.Data.Enums;
using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.Entities;
-using MediaBrowser.Model.IO;
using MediaBrowser.Model.Providers;
namespace MediaBrowser.Controller.Entities.Movies
@@ -22,22 +19,29 @@ namespace MediaBrowser.Controller.Entities.Movies
/// </summary>
public class Movie : Video, IHasSpecialFeatures, IHasTrailers, IHasLookupInfo<MovieInfo>, ISupportsBoxSetGrouping
{
- public Movie()
- {
- SpecialFeatureIds = Array.Empty<Guid>();
- RemoteTrailers = Array.Empty<MediaUrl>();
- LocalTrailerIds = Array.Empty<Guid>();
- RemoteTrailerIds = Array.Empty<Guid>();
- }
+ private IReadOnlyList<Guid> _specialFeatureIds;
/// <inheritdoc />
- public IReadOnlyList<Guid> SpecialFeatureIds { get; set; }
+ public IReadOnlyList<Guid> SpecialFeatureIds
+ {
+ get
+ {
+ return _specialFeatureIds ??= GetExtras()
+ .Where(extra => extra.ExtraType != Model.Entities.ExtraType.Trailer)
+ .Select(song => song.Id)
+ .ToArray();
+ }
- /// <inheritdoc />
- public IReadOnlyList<Guid> LocalTrailerIds { get; set; }
+ set
+ {
+ _specialFeatureIds = value;
+ }
+ }
/// <inheritdoc />
- public IReadOnlyList<Guid> RemoteTrailerIds { get; set; }
+ public IReadOnlyList<BaseItem> LocalTrailers => GetExtras()
+ .Where(extra => extra.ExtraType == Model.Entities.ExtraType.Trailer)
+ .ToArray();
/// <summary>
/// Gets or sets the name of the TMDB collection.
@@ -66,54 +70,6 @@ namespace MediaBrowser.Controller.Entities.Movies
return 2.0 / 3;
}
- protected override async Task<bool> RefreshedOwnedItems(MetadataRefreshOptions options, List<FileSystemMetadata> fileSystemChildren, CancellationToken cancellationToken)
- {
- var hasChanges = await base.RefreshedOwnedItems(options, fileSystemChildren, cancellationToken).ConfigureAwait(false);
-
- // Must have a parent to have special features
- // In other words, it must be part of the Parent/Child tree
- if (IsFileProtocol && SupportsOwnedItems && !IsInMixedFolder)
- {
- var specialFeaturesChanged = await RefreshSpecialFeatures(options, fileSystemChildren, cancellationToken).ConfigureAwait(false);
-
- if (specialFeaturesChanged)
- {
- hasChanges = true;
- }
- }
-
- return hasChanges;
- }
-
- private async Task<bool> RefreshSpecialFeatures(MetadataRefreshOptions options, List<FileSystemMetadata> fileSystemChildren, CancellationToken cancellationToken)
- {
- var newItems = LibraryManager.FindExtras(this, fileSystemChildren, options.DirectoryService).ToList();
- var newItemIds = newItems.Select(i => i.Id).ToArray();
-
- var itemsChanged = !SpecialFeatureIds.SequenceEqual(newItemIds);
-
- var ownerId = Id;
-
- var tasks = newItems.Select(i =>
- {
- var subOptions = new MetadataRefreshOptions(options);
-
- if (i.OwnerId != ownerId)
- {
- i.OwnerId = ownerId;
- subOptions.ForceSave = true;
- }
-
- return RefreshMetadataForOwnedItem(i, false, subOptions, cancellationToken);
- });
-
- await Task.WhenAll(tasks).ConfigureAwait(false);
-
- SpecialFeatureIds = newItemIds;
-
- return itemsChanged;
- }
-
/// <inheritdoc />
public override UnratedItem GetBlockUnratedType()
{
diff --git a/MediaBrowser.Controller/Entities/TV/Episode.cs b/MediaBrowser.Controller/Entities/TV/Episode.cs
index 27c3ff81b..dcc752f8c 100644
--- a/MediaBrowser.Controller/Entities/TV/Episode.cs
+++ b/MediaBrowser.Controller/Entities/TV/Episode.cs
@@ -20,18 +20,10 @@ namespace MediaBrowser.Controller.Entities.TV
/// </summary>
public class Episode : Video, IHasTrailers, IHasLookupInfo<EpisodeInfo>, IHasSeries
{
- public Episode()
- {
- RemoteTrailers = Array.Empty<MediaUrl>();
- LocalTrailerIds = Array.Empty<Guid>();
- RemoteTrailerIds = Array.Empty<Guid>();
- }
-
- /// <inheritdoc />
- public IReadOnlyList<Guid> LocalTrailerIds { get; set; }
-
/// <inheritdoc />
- public IReadOnlyList<Guid> RemoteTrailerIds { get; set; }
+ public IReadOnlyList<BaseItem> LocalTrailers => GetExtras()
+ .Where(extra => extra.ExtraType == Model.Entities.ExtraType.Trailer)
+ .ToArray();
/// <summary>
/// Gets or sets the season in which it aired.
diff --git a/MediaBrowser.Controller/Entities/TV/Series.cs b/MediaBrowser.Controller/Entities/TV/Series.cs
index e4933e968..90fcffe32 100644
--- a/MediaBrowser.Controller/Entities/TV/Series.cs
+++ b/MediaBrowser.Controller/Entities/TV/Series.cs
@@ -27,9 +27,6 @@ namespace MediaBrowser.Controller.Entities.TV
{
public Series()
{
- RemoteTrailers = Array.Empty<MediaUrl>();
- LocalTrailerIds = Array.Empty<Guid>();
- RemoteTrailerIds = Array.Empty<Guid>();
AirDays = Array.Empty<DayOfWeek>();
}
@@ -53,10 +50,9 @@ namespace MediaBrowser.Controller.Entities.TV
public override bool SupportsPeople => true;
/// <inheritdoc />
- public IReadOnlyList<Guid> LocalTrailerIds { get; set; }
-
- /// <inheritdoc />
- public IReadOnlyList<Guid> RemoteTrailerIds { get; set; }
+ public IReadOnlyList<BaseItem> LocalTrailers => GetExtras()
+ .Where(extra => extra.ExtraType == Model.Entities.ExtraType.Trailer)
+ .ToArray();
/// <summary>
/// Gets or sets the display order.
diff --git a/MediaBrowser.Controller/Entities/UserViewBuilder.cs b/MediaBrowser.Controller/Entities/UserViewBuilder.cs
index 1cff72037..25511f9d9 100644
--- a/MediaBrowser.Controller/Entities/UserViewBuilder.cs
+++ b/MediaBrowser.Controller/Entities/UserViewBuilder.cs
@@ -745,10 +745,9 @@ namespace MediaBrowser.Controller.Entities
var val = query.HasTrailer.Value;
var trailerCount = 0;
- var hasTrailers = item as IHasTrailers;
- if (hasTrailers != null)
+ if (item is IHasTrailers hasTrailers)
{
- trailerCount = hasTrailers.GetTrailerIds().Count;
+ trailerCount = hasTrailers.GetTrailerCount();
}
var ok = val ? trailerCount > 0 : trailerCount == 0;
@@ -763,7 +762,7 @@ namespace MediaBrowser.Controller.Entities
{
var filterValue = query.HasThemeSong.Value;
- var themeCount = item.ThemeSongIds.Length;
+ var themeCount = item.GetThemeSongs().Count;
var ok = filterValue ? themeCount > 0 : themeCount == 0;
if (!ok)
@@ -776,7 +775,7 @@ namespace MediaBrowser.Controller.Entities
{
var filterValue = query.HasThemeVideo.Value;
- var themeCount = item.ThemeVideoIds.Length;
+ var themeCount = item.GetThemeVideos().Count;
var ok = filterValue ? themeCount > 0 : themeCount == 0;
if (!ok)
diff --git a/MediaBrowser.Controller/Library/ILibraryManager.cs b/MediaBrowser.Controller/Library/ILibraryManager.cs
index 1e1e2adb8..1ae28abde 100644
--- a/MediaBrowser.Controller/Library/ILibraryManager.cs
+++ b/MediaBrowser.Controller/Library/ILibraryManager.cs
@@ -6,7 +6,6 @@ using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
-using Emby.Naming.Common;
using Jellyfin.Data.Entities;
using Jellyfin.Data.Enums;
using MediaBrowser.Controller.Dto;
@@ -427,28 +426,14 @@ namespace MediaBrowser.Controller.Library
Guid GetNewItemId(string key, Type type);
/// <summary>
- /// Finds the trailers.
- /// </summary>
- /// <param name="owner">The owner.</param>
- /// <param name="fileSystemChildren">The file system children.</param>
- /// <param name="directoryService">The directory service.</param>
- /// <returns>IEnumerable&lt;Trailer&gt;.</returns>
- IEnumerable<Video> FindTrailers(
- BaseItem owner,
- List<FileSystemMetadata> fileSystemChildren,
- IDirectoryService directoryService);
-
- /// <summary>
/// Finds the extras.
/// </summary>
/// <param name="owner">The owner.</param>
/// <param name="fileSystemChildren">The file system children.</param>
- /// <param name="directoryService">The directory service.</param>
/// <returns>IEnumerable&lt;Video&gt;.</returns>
IEnumerable<Video> FindExtras(
BaseItem owner,
- List<FileSystemMetadata> fileSystemChildren,
- IDirectoryService directoryService);
+ List<FileSystemMetadata> fileSystemChildren);
/// <summary>
/// Gets the collection folders.
diff --git a/MediaBrowser.LocalMetadata/Images/LocalImageProvider.cs b/MediaBrowser.LocalMetadata/Images/LocalImageProvider.cs
index f79147803..46f1fede3 100644
--- a/MediaBrowser.LocalMetadata/Images/LocalImageProvider.cs
+++ b/MediaBrowser.LocalMetadata/Images/LocalImageProvider.cs
@@ -106,7 +106,7 @@ namespace MediaBrowser.LocalMetadata.Images
{
if (!item.IsFileProtocol)
{
- return Enumerable.Empty<FileSystemMetadata>();
+ yield break;
}
var path = item.ContainingFolderPath;
@@ -114,20 +114,21 @@ namespace MediaBrowser.LocalMetadata.Images
// Exit if the cache dir does not exist, alternative solution is to create it, but that's a lot of empty dirs...
if (!Directory.Exists(path))
{
- return Enumerable.Empty<FileSystemMetadata>();
+ yield break;
}
- if (includeDirectories)
+ var files = directoryService.GetFileSystemEntries(path).OrderBy(i => Array.IndexOf(BaseItem.SupportedImageExtensions, i.Extension ?? string.Empty));
+ var count = BaseItem.SupportedImageExtensions.Length;
+ foreach (var file in files)
{
- return directoryService.GetFileSystemEntries(path)
- .Where(i => BaseItem.SupportedImageExtensions.Contains(i.Extension, StringComparer.OrdinalIgnoreCase) || i.IsDirectory)
-
- .OrderBy(i => Array.IndexOf(BaseItem.SupportedImageExtensions, i.Extension ?? string.Empty));
+ for (var i = 0; i < count; i++)
+ {
+ if ((includeDirectories && file.IsDirectory) || string.Equals(BaseItem.SupportedImageExtensions[i], file.Extension, StringComparison.OrdinalIgnoreCase))
+ {
+ yield return file;
+ }
+ }
}
-
- return directoryService.GetFiles(path)
- .Where(i => BaseItem.SupportedImageExtensions.Contains(i.Extension, StringComparer.OrdinalIgnoreCase))
- .OrderBy(i => Array.IndexOf(BaseItem.SupportedImageExtensions, i.Extension ?? string.Empty));
}
/// <inheritdoc />
diff --git a/tests/Jellyfin.Naming.Tests/Video/ExtraTests.cs b/tests/Jellyfin.Naming.Tests/Video/ExtraTests.cs
index d13e89cee..8dd637559 100644
--- a/tests/Jellyfin.Naming.Tests/Video/ExtraTests.cs
+++ b/tests/Jellyfin.Naming.Tests/Video/ExtraTests.cs
@@ -81,9 +81,7 @@ namespace Jellyfin.Naming.Tests.Video
private void Test(string input, ExtraType? expectedType)
{
- var parser = GetExtraTypeParser(_videoOptions);
-
- var extraType = parser.GetExtraInfo(input).ExtraType;
+ var extraType = ExtraResolver.GetExtraInfo(input, _videoOptions).ExtraType;
Assert.Equal(expectedType, extraType);
}
@@ -93,14 +91,9 @@ namespace Jellyfin.Naming.Tests.Video
{
var rule = new ExtraRule(ExtraType.Unknown, ExtraRuleType.Regex, @"([eE]x(tra)?\.\w+)", MediaType.Video);
var options = new NamingOptions { VideoExtraRules = new[] { rule } };
- var res = GetExtraTypeParser(options).GetExtraInfo("extra.mp4");
+ var res = ExtraResolver.GetExtraInfo("extra.mp4", options);
Assert.Equal(rule, res.Rule);
}
-
- private ExtraResolver GetExtraTypeParser(NamingOptions videoOptions)
- {
- return new ExtraResolver(videoOptions);
- }
}
}
diff --git a/tests/Jellyfin.Naming.Tests/Video/MultiVersionTests.cs b/tests/Jellyfin.Naming.Tests/Video/MultiVersionTests.cs
index d02f8ae92..323457d09 100644
--- a/tests/Jellyfin.Naming.Tests/Video/MultiVersionTests.cs
+++ b/tests/Jellyfin.Naming.Tests/Video/MultiVersionTests.cs
@@ -30,8 +30,8 @@ namespace Jellyfin.Naming.Tests.Video
}).ToList(),
_namingOptions).ToList();
- Assert.Single(result);
- Assert.Single(result[0].Extras);
+ Assert.Single(result.Where(v => v.ExtraType == null));
+ Assert.Single(result.Where(v => v.ExtraType != null));
}
[Fact]
@@ -53,8 +53,8 @@ namespace Jellyfin.Naming.Tests.Video
}).ToList(),
_namingOptions).ToList();
- Assert.Single(result);
- Assert.Single(result[0].Extras);
+ Assert.Single(result.Where(v => v.ExtraType == null));
+ Assert.Single(result.Where(v => v.ExtraType != null));
Assert.Equal(2, result[0].AlternateVersions.Count);
}
@@ -102,7 +102,6 @@ namespace Jellyfin.Naming.Tests.Video
_namingOptions).ToList();
Assert.Equal(7, result.Count);
- Assert.Empty(result[0].Extras);
Assert.Empty(result[0].AlternateVersions);
}
@@ -130,7 +129,6 @@ namespace Jellyfin.Naming.Tests.Video
_namingOptions).ToList();
Assert.Single(result);
- Assert.Empty(result[0].Extras);
Assert.Equal(7, result[0].AlternateVersions.Count);
}
@@ -159,7 +157,6 @@ namespace Jellyfin.Naming.Tests.Video
_namingOptions).ToList();
Assert.Equal(9, result.Count);
- Assert.Empty(result[0].Extras);
Assert.Empty(result[0].AlternateVersions);
}
@@ -184,7 +181,6 @@ namespace Jellyfin.Naming.Tests.Video
_namingOptions).ToList();
Assert.Equal(5, result.Count);
- Assert.Empty(result[0].Extras);
Assert.Empty(result[0].AlternateVersions);
}
@@ -211,7 +207,6 @@ namespace Jellyfin.Naming.Tests.Video
_namingOptions).ToList();
Assert.Equal(5, result.Count);
- Assert.Empty(result[0].Extras);
Assert.Empty(result[0].AlternateVersions);
}
@@ -239,7 +234,6 @@ namespace Jellyfin.Naming.Tests.Video
_namingOptions).ToList();
Assert.Single(result);
- Assert.Empty(result[0].Extras);
Assert.Equal(7, result[0].AlternateVersions.Count);
Assert.False(result[0].AlternateVersions[2].Is3D);
Assert.True(result[0].AlternateVersions[3].Is3D);
@@ -270,7 +264,6 @@ namespace Jellyfin.Naming.Tests.Video
_namingOptions).ToList();
Assert.Single(result);
- Assert.Empty(result[0].Extras);
Assert.Equal(7, result[0].AlternateVersions.Count);
Assert.False(result[0].AlternateVersions[3].Is3D);
Assert.True(result[0].AlternateVersions[4].Is3D);
@@ -320,7 +313,6 @@ namespace Jellyfin.Naming.Tests.Video
_namingOptions).ToList();
Assert.Equal(7, result.Count);
- Assert.Empty(result[0].Extras);
Assert.Empty(result[0].AlternateVersions);
}
@@ -347,7 +339,6 @@ namespace Jellyfin.Naming.Tests.Video
_namingOptions).ToList();
Assert.Equal(5, result.Count);
- Assert.Empty(result[0].Extras);
Assert.Empty(result[0].AlternateVersions);
}
@@ -369,7 +360,6 @@ namespace Jellyfin.Naming.Tests.Video
_namingOptions).ToList();
Assert.Single(result);
- Assert.Empty(result[0].Extras);
Assert.Single(result[0].AlternateVersions);
}
@@ -391,7 +381,6 @@ namespace Jellyfin.Naming.Tests.Video
_namingOptions).ToList();
Assert.Single(result);
- Assert.Empty(result[0].Extras);
Assert.Single(result[0].AlternateVersions);
}
@@ -413,7 +402,6 @@ namespace Jellyfin.Naming.Tests.Video
_namingOptions).ToList();
Assert.Single(result);
- Assert.Empty(result[0].Extras);
Assert.Single(result[0].AlternateVersions);
}
diff --git a/tests/Jellyfin.Naming.Tests/Video/StackTests.cs b/tests/Jellyfin.Naming.Tests/Video/StackTests.cs
index 8794d3ebe..41da0e077 100644
--- a/tests/Jellyfin.Naming.Tests/Video/StackTests.cs
+++ b/tests/Jellyfin.Naming.Tests/Video/StackTests.cs
@@ -22,9 +22,7 @@ namespace Jellyfin.Naming.Tests.Video
"Bad Boys (2006)-trailer.mkv"
};
- var resolver = GetResolver();
-
- var result = resolver.ResolveFiles(files).ToList();
+ var result = StackResolver.ResolveFiles(files, _namingOptions).ToList();
Assert.Single(result);
TestStackInfo(result[0], "Bad Boys (2006)", 4);
@@ -39,9 +37,7 @@ namespace Jellyfin.Naming.Tests.Video
"Bad Boys (2007).mkv"
};
- var resolver = GetResolver();
-
- var result = resolver.ResolveFiles(files).ToList();
+ var result = StackResolver.ResolveFiles(files, _namingOptions).ToList();
Assert.Empty(result);
}
@@ -55,9 +51,7 @@ namespace Jellyfin.Naming.Tests.Video
"Bad Boys 2007.mkv"
};
- var resolver = GetResolver();
-
- var result = resolver.ResolveFiles(files).ToList();
+ var result = StackResolver.ResolveFiles(files, _namingOptions).ToList();
Assert.Empty(result);
}
@@ -71,9 +65,7 @@ namespace Jellyfin.Naming.Tests.Video
"300 (2007).mkv"
};
- var resolver = GetResolver();
-
- var result = resolver.ResolveFiles(files).ToList();
+ var result = StackResolver.ResolveFiles(files, _namingOptions).ToList();
Assert.Empty(result);
}
@@ -87,9 +79,7 @@ namespace Jellyfin.Naming.Tests.Video
"300 2007.mkv"
};
- var resolver = GetResolver();
-
- var result = resolver.ResolveFiles(files).ToList();
+ var result = StackResolver.ResolveFiles(files, _namingOptions).ToList();
Assert.Empty(result);
}
@@ -103,9 +93,7 @@ namespace Jellyfin.Naming.Tests.Video
"Star Trek 2- The wrath of khan.mkv"
};
- var resolver = GetResolver();
-
- var result = resolver.ResolveFiles(files).ToList();
+ var result = StackResolver.ResolveFiles(files, _namingOptions).ToList();
Assert.Empty(result);
}
@@ -119,9 +107,7 @@ namespace Jellyfin.Naming.Tests.Video
"Red Riding in the Year of Our Lord 1974 (2009).mkv"
};
- var resolver = GetResolver();
-
- var result = resolver.ResolveFiles(files).ToList();
+ var result = StackResolver.ResolveFiles(files, _namingOptions).ToList();
Assert.Empty(result);
}
@@ -135,9 +121,7 @@ namespace Jellyfin.Naming.Tests.Video
"d:/movies/300 2006 part2.mkv"
};
- var resolver = GetResolver();
-
- var result = resolver.ResolveFiles(files).ToList();
+ var result = StackResolver.ResolveFiles(files, _namingOptions).ToList();
Assert.Single(result);
TestStackInfo(result[0], "300 2006", 2);
@@ -155,9 +139,7 @@ namespace Jellyfin.Naming.Tests.Video
"Bad Boys (2006)-trailer.mkv"
};
- var resolver = GetResolver();
-
- var result = resolver.ResolveFiles(files).ToList();
+ var result = StackResolver.ResolveFiles(files, _namingOptions).ToList();
Assert.Single(result);
TestStackInfo(result[0], "Bad Boys (2006).stv.unrated.multi.1080p.bluray.x264-rough", 4);
@@ -175,9 +157,7 @@ namespace Jellyfin.Naming.Tests.Video
"Bad Boys (2006)-trailer.mkv"
};
- var resolver = GetResolver();
-
- var result = resolver.ResolveFiles(files).ToList();
+ var result = StackResolver.ResolveFiles(files, _namingOptions).ToList();
Assert.Empty(result);
}
@@ -194,9 +174,7 @@ namespace Jellyfin.Naming.Tests.Video
"300 (2006)-trailer.mkv"
};
- var resolver = GetResolver();
-
- var result = resolver.ResolveFiles(files).ToList();
+ var result = StackResolver.ResolveFiles(files, _namingOptions).ToList();
Assert.Single(result);
TestStackInfo(result[0], "300 (2006)", 4);
@@ -214,9 +192,7 @@ namespace Jellyfin.Naming.Tests.Video
"Bad Boys (2006)-trailer.mkv"
};
- var resolver = GetResolver();
-
- var result = resolver.ResolveFiles(files).ToList();
+ var result = StackResolver.ResolveFiles(files, _namingOptions).ToList();
Assert.Single(result);
TestStackInfo(result[0], "Bad Boys (2006)", 3);
@@ -238,9 +214,7 @@ namespace Jellyfin.Naming.Tests.Video
"300 (2006)-trailer.mkv"
};
- var resolver = GetResolver();
-
- var result = resolver.ResolveFiles(files).ToList();
+ var result = StackResolver.ResolveFiles(files, _namingOptions).ToList();
Assert.Equal(2, result.Count);
TestStackInfo(result[1], "Bad Boys (2006)", 4);
@@ -256,9 +230,7 @@ namespace Jellyfin.Naming.Tests.Video
"blah blah - cd 2"
};
- var resolver = GetResolver();
-
- var result = resolver.ResolveDirectories(files).ToList();
+ var result = StackResolver.ResolveDirectories(files, _namingOptions).ToList();
Assert.Single(result);
TestStackInfo(result[0], "blah blah", 2);
@@ -275,9 +247,7 @@ namespace Jellyfin.Naming.Tests.Video
"300-trailer.mkv"
};
- var resolver = GetResolver();
-
- var result = resolver.ResolveFiles(files).ToList();
+ var result = StackResolver.ResolveFiles(files, _namingOptions).ToList();
Assert.Single(result);
@@ -297,9 +267,7 @@ namespace Jellyfin.Naming.Tests.Video
"Avengers part3.mkv"
};
- var resolver = GetResolver();
-
- var result = resolver.ResolveFiles(files).ToList();
+ var result = StackResolver.ResolveFiles(files, _namingOptions).ToList();
Assert.Equal(2, result.Count);
@@ -328,9 +296,7 @@ namespace Jellyfin.Naming.Tests.Video
"300-trailer.mkv"
};
- var resolver = GetResolver();
-
- var result = resolver.ResolveFiles(files).ToList();
+ var result = StackResolver.ResolveFiles(files, _namingOptions).ToList();
Assert.Equal(3, result.Count);
@@ -354,9 +320,7 @@ namespace Jellyfin.Naming.Tests.Video
"300 (2006)-trailer.mkv"
};
- var resolver = GetResolver();
-
- var result = resolver.ResolveFiles(files).ToList();
+ var result = StackResolver.ResolveFiles(files, _namingOptions).ToList();
Assert.Single(result);
@@ -375,9 +339,7 @@ namespace Jellyfin.Naming.Tests.Video
new FileSystemMetadata { FullName = "300 (2006) part1", IsDirectory = true }
};
- var resolver = GetResolver();
-
- var result = resolver.Resolve(files).ToList();
+ var result = StackResolver.Resolve(files, _namingOptions).ToList();
Assert.Equal(2, result.Count);
TestStackInfo(result[0], "300 (2006)", 3);
@@ -397,9 +359,7 @@ namespace Jellyfin.Naming.Tests.Video
"Harry Potter and the Deathly Hallows 4.mkv"
};
- var resolver = GetResolver();
-
- var result = resolver.ResolveFiles(files).ToList();
+ var result = StackResolver.ResolveFiles(files, _namingOptions).ToList();
Assert.Empty(result);
}
@@ -414,9 +374,7 @@ namespace Jellyfin.Naming.Tests.Video
"Neverland (2011)[720p][PG][Voted 6.5][Family-Fantasy]part2.mkv"
};
- var resolver = GetResolver();
-
- var result = resolver.ResolveFiles(files).ToList();
+ var result = StackResolver.ResolveFiles(files, _namingOptions).ToList();
Assert.Single(result);
Assert.Equal(2, result[0].Files.Count);
@@ -432,9 +390,7 @@ namespace Jellyfin.Naming.Tests.Video
@"M:/Movies (DVD)/Movies (Musical)/The Sound of Music/The Sound of Music (1965) (Disc 02)"
};
- var resolver = GetResolver();
-
- var result = resolver.ResolveDirectories(files).ToList();
+ var result = StackResolver.ResolveDirectories(files, _namingOptions).ToList();
Assert.Single(result);
Assert.Equal(2, result[0].Files.Count);
@@ -445,10 +401,5 @@ namespace Jellyfin.Naming.Tests.Video
Assert.Equal(fileCount, stack.Files.Count);
Assert.Equal(name, stack.Name);
}
-
- private StackResolver GetResolver()
- {
- return new StackResolver(_namingOptions);
- }
}
}
diff --git a/tests/Jellyfin.Naming.Tests/Video/VideoListResolverTests.cs b/tests/Jellyfin.Naming.Tests/Video/VideoListResolverTests.cs
index 9e0776c3c..5d9ef1340 100644
--- a/tests/Jellyfin.Naming.Tests/Video/VideoListResolverTests.cs
+++ b/tests/Jellyfin.Naming.Tests/Video/VideoListResolverTests.cs
@@ -2,6 +2,7 @@ using System;
using System.Linq;
using Emby.Naming.Common;
using Emby.Naming.Video;
+using MediaBrowser.Model.Entities;
using MediaBrowser.Model.IO;
using Xunit;
@@ -48,16 +49,25 @@ namespace Jellyfin.Naming.Tests.Video
}).ToList(),
_namingOptions).ToList();
- Assert.Equal(5, result.Count);
+ Assert.Equal(11, result.Count);
var batman = result.FirstOrDefault(x => string.Equals(x.Name, "Batman", StringComparison.Ordinal));
Assert.NotNull(batman);
Assert.Equal(3, batman!.Files.Count);
- Assert.Equal(3, batman!.Extras.Count);
var harry = result.FirstOrDefault(x => string.Equals(x.Name, "Harry Potter and the Deathly Hallows", StringComparison.Ordinal));
Assert.NotNull(harry);
Assert.Equal(4, harry!.Files.Count);
- Assert.Equal(2, harry!.Extras.Count);
+
+ Assert.False(result[2].ExtraType.HasValue);
+
+ Assert.Equal(ExtraType.Trailer, result[3].ExtraType);
+ Assert.Equal(ExtraType.Trailer, result[4].ExtraType);
+ Assert.Equal(ExtraType.DeletedScene, result[5].ExtraType);
+ Assert.Equal(ExtraType.Sample, result[6].ExtraType);
+ Assert.Equal(ExtraType.Trailer, result[7].ExtraType);
+ Assert.Equal(ExtraType.Trailer, result[8].ExtraType);
+ Assert.Equal(ExtraType.Trailer, result[9].ExtraType);
+ Assert.Equal(ExtraType.Trailer, result[10].ExtraType);
}
[Fact]
@@ -97,7 +107,8 @@ namespace Jellyfin.Naming.Tests.Video
}).ToList(),
_namingOptions).ToList();
- Assert.Single(result);
+ Assert.False(result[0].ExtraType.HasValue);
+ Assert.Equal(ExtraType.Trailer, result[1].ExtraType);
}
[Fact]
@@ -117,7 +128,8 @@ namespace Jellyfin.Naming.Tests.Video
}).ToList(),
_namingOptions).ToList();
- Assert.Single(result);
+ Assert.False(result[0].ExtraType.HasValue);
+ Assert.Equal(ExtraType.Trailer, result[1].ExtraType);
}
[Fact]
@@ -138,15 +150,18 @@ namespace Jellyfin.Naming.Tests.Video
}).ToList(),
_namingOptions).ToList();
- Assert.Single(result);
+ Assert.False(result[0].ExtraType.HasValue);
+ Assert.Equal(ExtraType.Trailer, result[1].ExtraType);
+ Assert.Equal(ExtraType.Trailer, result[2].ExtraType);
}
[Fact]
- public void TestDifferentNames()
+ public void Resolve_SameNameAndYear_ReturnsSingleItem()
{
var files = new[]
{
"Looper (2012)-trailer.mkv",
+ "Looper 2012-trailer.mkv",
"Looper.2012.bluray.720p.x264.mkv"
};
@@ -158,7 +173,30 @@ namespace Jellyfin.Naming.Tests.Video
}).ToList(),
_namingOptions).ToList();
- Assert.Single(result);
+ Assert.False(result[0].ExtraType.HasValue);
+ Assert.Equal(ExtraType.Trailer, result[1].ExtraType);
+ Assert.Equal(ExtraType.Trailer, result[2].ExtraType);
+ }
+
+ [Fact]
+ public void Resolve_TrailerMatchesFolderName_ReturnsSingleItem()
+ {
+ var files = new[]
+ {
+ "/movies/Looper (2012)/Looper (2012)-trailer.mkv",
+ "/movies/Looper (2012)/Looper.bluray.720p.x264.mkv"
+ };
+
+ var result = VideoListResolver.Resolve(
+ files.Select(i => new FileSystemMetadata
+ {
+ IsDirectory = false,
+ FullName = i
+ }).ToList(),
+ _namingOptions).ToList();
+
+ Assert.False(result[0].ExtraType.HasValue);
+ Assert.Equal(ExtraType.Trailer, result[1].ExtraType);
}
[Fact]
@@ -233,27 +271,7 @@ namespace Jellyfin.Naming.Tests.Video
{
@"No (2012) part1.mp4",
@"No (2012) part2.mp4",
- @"No (2012) part1-trailer.mp4"
- };
-
- var result = VideoListResolver.Resolve(
- files.Select(i => new FileSystemMetadata
- {
- IsDirectory = false,
- FullName = i
- }).ToList(),
- _namingOptions).ToList();
-
- Assert.Single(result);
- }
-
- [Fact]
- public void TestStackedWithTrailer2()
- {
- var files = new[]
- {
- @"No (2012) part1.mp4",
- @"No (2012) part2.mp4",
+ @"No (2012) part1-trailer.mp4",
@"No (2012)-trailer.mp4"
};
@@ -265,7 +283,10 @@ namespace Jellyfin.Naming.Tests.Video
}).ToList(),
_namingOptions).ToList();
- Assert.Single(result);
+ Assert.Equal(3, result.Count);
+ Assert.False(result[0].ExtraType.HasValue);
+ Assert.Equal(ExtraType.Trailer, result[1].ExtraType);
+ Assert.Equal(ExtraType.Trailer, result[2].ExtraType);
}
[Fact]
@@ -276,7 +297,7 @@ namespace Jellyfin.Naming.Tests.Video
@"/Movies/Top Gun (1984)/movie.mp4",
@"/Movies/Top Gun (1984)/Top Gun (1984)-trailer.mp4",
@"/Movies/Top Gun (1984)/Top Gun (1984)-trailer2.mp4",
- @"trailer.mp4"
+ @"/Movies/trailer.mp4"
};
var result = VideoListResolver.Resolve(
@@ -287,7 +308,10 @@ namespace Jellyfin.Naming.Tests.Video
}).ToList(),
_namingOptions).ToList();
- Assert.Single(result);
+ Assert.False(result[0].ExtraType.HasValue);
+ Assert.Equal(ExtraType.Trailer, result[1].ExtraType);
+ Assert.Equal(ExtraType.Trailer, result[2].ExtraType);
+ Assert.Equal(ExtraType.Trailer, result[3].ExtraType);
}
[Fact]
@@ -396,7 +420,7 @@ namespace Jellyfin.Naming.Tests.Video
var files = new[]
{
@"/Server/Despicable Me/Despicable Me (2010).mkv",
- @"/Server/Despicable Me/movie-trailer.mkv"
+ @"/Server/Despicable Me/trailer.mkv"
};
var result = VideoListResolver.Resolve(
@@ -407,18 +431,17 @@ namespace Jellyfin.Naming.Tests.Video
}).ToList(),
_namingOptions).ToList();
- Assert.Single(result);
+ Assert.False(result[0].ExtraType.HasValue);
+ Assert.Equal(ExtraType.Trailer, result[1].ExtraType);
}
[Fact]
- public void TestTrailerFalsePositives()
+ public void Resolve_TrailerInTrailersFolder_ReturnsCorrectExtraType()
{
var files = new[]
{
- @"/Server/Despicable Me/Skyscraper (2018) - Big Game Spot.mkv",
- @"/Server/Despicable Me/Skyscraper (2018) - Trailer.mkv",
- @"/Server/Despicable Me/Baywatch (2017) - Big Game Spot.mkv",
- @"/Server/Despicable Me/Baywatch (2017) - Trailer.mkv"
+ @"/Server/Despicable Me/Despicable Me (2010).mkv",
+ @"/Server/Despicable Me/trailers/some title.mkv"
};
var result = VideoListResolver.Resolve(
@@ -429,7 +452,8 @@ namespace Jellyfin.Naming.Tests.Video
}).ToList(),
_namingOptions).ToList();
- Assert.Equal(4, result.Count);
+ Assert.False(result[0].ExtraType.HasValue);
+ Assert.Equal(ExtraType.Trailer, result[1].ExtraType);
}
[Fact]
@@ -449,7 +473,8 @@ namespace Jellyfin.Naming.Tests.Video
}).ToList(),
_namingOptions).ToList();
- Assert.Single(result);
+ Assert.False(result[0].ExtraType.HasValue);
+ Assert.Equal(ExtraType.Trailer, result[1].ExtraType);
}
[Fact]
diff --git a/tests/Jellyfin.Server.Implementations.Tests/Library/EpisodeResolverTest.cs b/tests/Jellyfin.Server.Implementations.Tests/Library/EpisodeResolverTest.cs
index a0fe4a5cf..362c3216f 100644
--- a/tests/Jellyfin.Server.Implementations.Tests/Library/EpisodeResolverTest.cs
+++ b/tests/Jellyfin.Server.Implementations.Tests/Library/EpisodeResolverTest.cs
@@ -1,4 +1,5 @@
-using Emby.Server.Implementations.Library.Resolvers.TV;
+using Emby.Naming.Common;
+using Emby.Server.Implementations.Library.Resolvers.TV;
using MediaBrowser.Controller;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Entities.TV;
@@ -13,12 +14,14 @@ namespace Jellyfin.Server.Implementations.Tests.Library
{
public class EpisodeResolverTest
{
+ private static readonly NamingOptions _namingOptions = new ();
+
[Fact]
public void Resolve_GivenVideoInExtrasFolder_DoesNotResolveToEpisode()
{
var parent = new Folder { Name = "extras" };
- var episodeResolver = new EpisodeResolver(null);
+ var episodeResolver = new EpisodeResolver(_namingOptions);
var itemResolveArgs = new ItemResolveArgs(
Mock.Of<IServerApplicationPaths>(),
Mock.Of<IDirectoryService>())
@@ -41,14 +44,14 @@ namespace Jellyfin.Server.Implementations.Tests.Library
// Have to create a mock because of moq proxies not being castable to a concrete implementation
// https://github.com/jellyfin/jellyfin/blob/ab0cff8556403e123642dc9717ba778329554634/Emby.Server.Implementations/Library/Resolvers/BaseVideoResolver.cs#L48
- var episodeResolver = new EpisodeResolverMock();
+ var episodeResolver = new EpisodeResolverMock(_namingOptions);
var itemResolveArgs = new ItemResolveArgs(
Mock.Of<IServerApplicationPaths>(),
Mock.Of<IDirectoryService>())
{
Parent = series,
CollectionType = CollectionType.TvShows,
- FileInfo = new FileSystemMetadata()
+ FileInfo = new FileSystemMetadata
{
FullName = "Extras/Extras S01E01.mkv"
}
@@ -58,7 +61,7 @@ namespace Jellyfin.Server.Implementations.Tests.Library
private class EpisodeResolverMock : EpisodeResolver
{
- public EpisodeResolverMock() : base(null)
+ public EpisodeResolverMock(NamingOptions namingOptions) : base(namingOptions)
{
}
diff --git a/tests/Jellyfin.Server.Implementations.Tests/Library/LibraryManager/FindExtrasTests.cs b/tests/Jellyfin.Server.Implementations.Tests/Library/LibraryManager/FindExtrasTests.cs
new file mode 100644
index 000000000..10afadbef
--- /dev/null
+++ b/tests/Jellyfin.Server.Implementations.Tests/Library/LibraryManager/FindExtrasTests.cs
@@ -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);
+ }
+}