diff options
| author | Bond-009 <bond.009@outlook.com> | 2021-06-12 00:20:54 +0200 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2021-06-12 00:20:54 +0200 |
| commit | cfad97ff28339088ab86c40661bb407024285a7c (patch) | |
| tree | 152c04363e23e233229a7b40080d388a13046657 | |
| parent | adf406b1821800bbf1b214c4ac673defa362e4f4 (diff) | |
| parent | 66de8428960ec751521386c27e37909cf73add9e (diff) | |
Merge pull request #6096 from cvium/saving_private_ram
44 files changed, 1124 insertions, 888 deletions
diff --git a/Emby.Naming/TV/EpisodeResolver.cs b/Emby.Naming/TV/EpisodeResolver.cs index c63aec64e..5e952e47b 100644 --- a/Emby.Naming/TV/EpisodeResolver.cs +++ b/Emby.Naming/TV/EpisodeResolver.cs @@ -16,7 +16,7 @@ namespace Emby.Naming.TV /// <summary> /// Initializes a new instance of the <see cref="EpisodeResolver"/> class. /// </summary> - /// <param name="options"><see cref="NamingOptions"/> object containing VideoFileExtensions and passed to <see cref="StubResolver"/>, <see cref="FlagParser"/>, <see cref="Format3DParser"/> and <see cref="EpisodePathParser"/>.</param> + /// <param name="options"><see cref="NamingOptions"/> object containing VideoFileExtensions and passed to <see cref="StubResolver"/>, <see cref="Format3DParser"/> and <see cref="EpisodePathParser"/>.</param> public EpisodeResolver(NamingOptions options) { _options = options; @@ -62,8 +62,7 @@ namespace Emby.Naming.TV container = extension.TrimStart('.'); } - var flags = new FlagParser(_options).GetFlags(path); - var format3DResult = new Format3DParser(_options).Parse(flags); + var format3DResult = Format3DParser.Parse(path, _options); var parsingResult = new EpisodePathParser(_options) .Parse(path, isDirectory, isNamed, isOptimistic, supportsAbsoluteNumbers, fillExtendedInfo); diff --git a/Emby.Naming/Video/ExtraResolver.cs b/Emby.Naming/Video/ExtraResolver.cs index f9d06c09b..1fade985b 100644 --- a/Emby.Naming/Video/ExtraResolver.cs +++ b/Emby.Naming/Video/ExtraResolver.cs @@ -44,7 +44,7 @@ namespace Emby.Naming.Video } else if (rule.MediaType == MediaType.Video) { - if (!new VideoResolver(_options).IsVideoFile(path)) + if (!VideoResolver.IsVideoFile(path, _options)) { continue; } diff --git a/Emby.Naming/Video/FlagParser.cs b/Emby.Naming/Video/FlagParser.cs deleted file mode 100644 index 439de1813..000000000 --- a/Emby.Naming/Video/FlagParser.cs +++ /dev/null @@ -1,53 +0,0 @@ -using System; -using System.IO; -using Emby.Naming.Common; - -namespace Emby.Naming.Video -{ - /// <summary> - /// Parses list of flags from filename based on delimiters. - /// </summary> - public class FlagParser - { - private readonly NamingOptions _options; - - /// <summary> - /// Initializes a new instance of the <see cref="FlagParser"/> class. - /// </summary> - /// <param name="options"><see cref="NamingOptions"/> object containing VideoFlagDelimiters.</param> - public FlagParser(NamingOptions options) - { - _options = options; - } - - /// <summary> - /// Calls GetFlags function with _options.VideoFlagDelimiters parameter. - /// </summary> - /// <param name="path">Path to file.</param> - /// <returns>List of found flags.</returns> - public string[] GetFlags(string path) - { - return GetFlags(path, _options.VideoFlagDelimiters); - } - - /// <summary> - /// Parses flags from filename based on delimiters. - /// </summary> - /// <param name="path">Path to file.</param> - /// <param name="delimiters">Delimiters used to extract flags.</param> - /// <returns>List of found flags.</returns> - public string[] GetFlags(string path, char[] delimiters) - { - if (string.IsNullOrEmpty(path)) - { - return Array.Empty<string>(); - } - - // Note: the tags need be be surrounded be either a space ( ), hyphen -, dot . or underscore _. - - var file = Path.GetFileName(path); - - return file.Split(delimiters, StringSplitOptions.RemoveEmptyEntries); - } - } -} diff --git a/Emby.Naming/Video/Format3DParser.cs b/Emby.Naming/Video/Format3DParser.cs index 4fd5d78ba..089089989 100644 --- a/Emby.Naming/Video/Format3DParser.cs +++ b/Emby.Naming/Video/Format3DParser.cs @@ -1,45 +1,37 @@ using System; -using System.Linq; using Emby.Naming.Common; namespace Emby.Naming.Video { /// <summary> - /// Parste 3D format related flags. + /// Parse 3D format related flags. /// </summary> - public class Format3DParser + public static class Format3DParser { - private readonly NamingOptions _options; - - /// <summary> - /// Initializes a new instance of the <see cref="Format3DParser"/> class. - /// </summary> - /// <param name="options"><see cref="NamingOptions"/> object containing VideoFlagDelimiters and passes options to <see cref="FlagParser"/>.</param> - public Format3DParser(NamingOptions options) - { - _options = options; - } + // Static default result to save on allocation costs. + private static readonly Format3DResult _defaultResult = new (false, null); /// <summary> /// Parse 3D format related flags. /// </summary> /// <param name="path">Path to file.</param> + /// <param name="namingOptions">The naming options.</param> /// <returns>Returns <see cref="Format3DResult"/> object.</returns> - public Format3DResult Parse(string path) + public static Format3DResult Parse(ReadOnlySpan<char> path, NamingOptions namingOptions) { - int oldLen = _options.VideoFlagDelimiters.Length; - var delimiters = new char[oldLen + 1]; - _options.VideoFlagDelimiters.CopyTo(delimiters, 0); + int oldLen = namingOptions.VideoFlagDelimiters.Length; + Span<char> delimiters = stackalloc char[oldLen + 1]; + namingOptions.VideoFlagDelimiters.AsSpan().CopyTo(delimiters); delimiters[oldLen] = ' '; - return Parse(new FlagParser(_options).GetFlags(path, delimiters)); + return Parse(path, delimiters, namingOptions); } - internal Format3DResult Parse(string[] videoFlags) + private static Format3DResult Parse(ReadOnlySpan<char> path, ReadOnlySpan<char> delimiters, NamingOptions namingOptions) { - foreach (var rule in _options.Format3DRules) + foreach (var rule in namingOptions.Format3DRules) { - var result = Parse(videoFlags, rule); + var result = Parse(path, rule, delimiters); if (result.Is3D) { @@ -47,51 +39,43 @@ namespace Emby.Naming.Video } } - return new Format3DResult(); + return _defaultResult; } - private static Format3DResult Parse(string[] videoFlags, Format3DRule rule) + private static Format3DResult Parse(ReadOnlySpan<char> path, Format3DRule rule, ReadOnlySpan<char> delimiters) { - var result = new Format3DResult(); + bool is3D = false; + string? format3D = null; - if (string.IsNullOrEmpty(rule.PrecedingToken)) + // If there's no preceding token we just consider it found + var foundPrefix = string.IsNullOrEmpty(rule.PrecedingToken); + while (path.Length > 0) { - result.Format3D = new[] { rule.Token }.FirstOrDefault(i => videoFlags.Contains(i, StringComparer.OrdinalIgnoreCase)); - result.Is3D = !string.IsNullOrEmpty(result.Format3D); - - if (result.Is3D) + var index = path.IndexOfAny(delimiters); + if (index == -1) { - result.Tokens.Add(rule.Token); + index = path.Length - 1; } - } - else - { - var foundPrefix = false; - string? format = null; - foreach (var flag in videoFlags) - { - if (foundPrefix) - { - result.Tokens.Add(rule.PrecedingToken); + var currentSlice = path[..index]; + path = path[(index + 1)..]; - if (string.Equals(rule.Token, flag, StringComparison.OrdinalIgnoreCase)) - { - format = flag; - result.Tokens.Add(rule.Token); - } + if (!foundPrefix) + { + foundPrefix = currentSlice.Equals(rule.PrecedingToken, StringComparison.OrdinalIgnoreCase); + continue; + } - break; - } + is3D = foundPrefix && currentSlice.Equals(rule.Token, StringComparison.OrdinalIgnoreCase); - foundPrefix = string.Equals(flag, rule.PrecedingToken, StringComparison.OrdinalIgnoreCase); + if (is3D) + { + format3D = rule.Token; + break; } - - result.Is3D = foundPrefix && !string.IsNullOrEmpty(format); - result.Format3D = format; } - return result; + return is3D ? new Format3DResult(true, format3D) : _defaultResult; } } } diff --git a/Emby.Naming/Video/Format3DResult.cs b/Emby.Naming/Video/Format3DResult.cs index ac935f203..aac959c13 100644 --- a/Emby.Naming/Video/Format3DResult.cs +++ b/Emby.Naming/Video/Format3DResult.cs @@ -1,5 +1,3 @@ -using System.Collections.Generic; - namespace Emby.Naming.Video { /// <summary> @@ -10,27 +8,24 @@ namespace Emby.Naming.Video /// <summary> /// Initializes a new instance of the <see cref="Format3DResult"/> class. /// </summary> - public Format3DResult() + /// <param name="is3D">A value indicating whether the parsed string contains 3D tokens.</param> + /// <param name="format3D">The 3D format. Value might be null if [is3D] is <c>false</c>.</param> + public Format3DResult(bool is3D, string? format3D) { - Tokens = new List<string>(); + Is3D = is3D; + Format3D = format3D; } /// <summary> - /// Gets or sets a value indicating whether [is3 d]. + /// Gets a value indicating whether [is3 d]. /// </summary> /// <value><c>true</c> if [is3 d]; otherwise, <c>false</c>.</value> - public bool Is3D { get; set; } + public bool Is3D { get; } /// <summary> - /// Gets or sets the format3 d. + /// Gets the format3 d. /// </summary> /// <value>The format3 d.</value> - public string? Format3D { get; set; } - - /// <summary> - /// Gets or sets the tokens. - /// </summary> - /// <value>The tokens.</value> - public List<string> Tokens { get; set; } + public string? Format3D { get; } } } diff --git a/Emby.Naming/Video/StackResolver.cs b/Emby.Naming/Video/StackResolver.cs index 550c42961..36f65a562 100644 --- a/Emby.Naming/Video/StackResolver.cs +++ b/Emby.Naming/Video/StackResolver.cs @@ -85,10 +85,8 @@ namespace Emby.Naming.Video /// <returns>Enumerable <see cref="FileStack"/> of videos.</returns> public IEnumerable<FileStack> Resolve(IEnumerable<FileSystemMetadata> files) { - var resolver = new VideoResolver(_options); - var list = files - .Where(i => i.IsDirectory || resolver.IsVideoFile(i.FullName) || resolver.IsStubFile(i.FullName)) + .Where(i => i.IsDirectory || VideoResolver.IsVideoFile(i.FullName, _options) || VideoResolver.IsStubFile(i.FullName, _options)) .OrderBy(i => i.FullName) .ToList(); diff --git a/Emby.Naming/Video/VideoFileInfo.cs b/Emby.Naming/Video/VideoFileInfo.cs index 1457db737..481773ff6 100644 --- a/Emby.Naming/Video/VideoFileInfo.cs +++ b/Emby.Naming/Video/VideoFileInfo.cs @@ -1,3 +1,4 @@ +using System; using MediaBrowser.Model.Entities; namespace Emby.Naming.Video @@ -106,9 +107,9 @@ namespace Emby.Naming.Video /// Gets the file name without extension. /// </summary> /// <value>The file name without extension.</value> - public string FileNameWithoutExtension => !IsDirectory - ? System.IO.Path.GetFileNameWithoutExtension(Path) - : System.IO.Path.GetFileName(Path); + public ReadOnlySpan<char> FileNameWithoutExtension => !IsDirectory + ? System.IO.Path.GetFileNameWithoutExtension(Path.AsSpan()) + : System.IO.Path.GetFileName(Path.AsSpan()); /// <inheritdoc /> public override string ToString() diff --git a/Emby.Naming/Video/VideoListResolver.cs b/Emby.Naming/Video/VideoListResolver.cs index 7b6a1705b..7da2dcd7a 100644 --- a/Emby.Naming/Video/VideoListResolver.cs +++ b/Emby.Naming/Video/VideoListResolver.cs @@ -12,31 +12,19 @@ namespace Emby.Naming.Video /// <summary> /// Resolves alternative versions and extras from list of video files. /// </summary> - public class VideoListResolver + public static class VideoListResolver { - private readonly NamingOptions _options; - - /// <summary> - /// Initializes a new instance of the <see cref="VideoListResolver"/> class. - /// </summary> - /// <param name="options"><see cref="NamingOptions"/> object containing CleanStringRegexes and VideoFlagDelimiters and passes options to <see cref="StackResolver"/> and <see cref="VideoResolver"/>.</param> - public VideoListResolver(NamingOptions options) - { - _options = options; - } - /// <summary> /// Resolves alternative versions and extras from list of video files. /// </summary> /// <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> /// <returns>Returns enumerable of <see cref="VideoInfo"/> which groups files together when related.</returns> - public IEnumerable<VideoInfo> Resolve(List<FileSystemMetadata> files, bool supportMultiVersion = true) + public static IEnumerable<VideoInfo> Resolve(List<FileSystemMetadata> files, NamingOptions namingOptions, bool supportMultiVersion = true) { - var videoResolver = new VideoResolver(_options); - var videoInfos = files - .Select(i => videoResolver.Resolve(i.FullName, i.IsDirectory)) + .Select(i => VideoResolver.Resolve(i.FullName, i.IsDirectory, namingOptions)) .OfType<VideoFileInfo>() .ToList(); @@ -46,7 +34,7 @@ namespace Emby.Naming.Video .Where(i => i.ExtraType == null) .Select(i => new FileSystemMetadata { FullName = i.Path, IsDirectory = i.IsDirectory }); - var stackResult = new StackResolver(_options) + var stackResult = new StackResolver(namingOptions) .Resolve(nonExtras).ToList(); var remainingFiles = videoInfos @@ -59,23 +47,17 @@ namespace Emby.Naming.Video { var info = new VideoInfo(stack.Name) { - Files = stack.Files.Select(i => videoResolver.Resolve(i, stack.IsDirectoryStack)) + Files = stack.Files.Select(i => VideoResolver.Resolve(i, stack.IsDirectoryStack, namingOptions)) .OfType<VideoFileInfo>() .ToList() }; info.Year = info.Files[0].Year; - var extraBaseNames = new List<string> { stack.Name, Path.GetFileNameWithoutExtension(stack.Files[0]) }; - - var extras = GetExtras(remainingFiles, extraBaseNames); + var extras = ExtractExtras(remainingFiles, stack.Name, Path.GetFileNameWithoutExtension(stack.Files[0].AsSpan()), namingOptions.VideoFlagDelimiters); if (extras.Count > 0) { - remainingFiles = remainingFiles - .Except(extras) - .ToList(); - info.Extras = extras; } @@ -88,15 +70,12 @@ namespace Emby.Naming.Video foreach (var media in standaloneMedia) { - var info = new VideoInfo(media.Name) { Files = new List<VideoFileInfo> { media } }; + var info = new VideoInfo(media.Name) { Files = new[] { media } }; info.Year = info.Files[0].Year; - var extras = GetExtras(remainingFiles, new List<string> { media.FileNameWithoutExtension }); - - remainingFiles = remainingFiles - .Except(extras.Concat(new[] { media })) - .ToList(); + remainingFiles.Remove(media); + var extras = ExtractExtras(remainingFiles, media.FileNameWithoutExtension, namingOptions.VideoFlagDelimiters); info.Extras = extras; @@ -105,8 +84,7 @@ namespace Emby.Naming.Video if (supportMultiVersion) { - list = GetVideosGroupedByVersion(list) - .ToList(); + list = GetVideosGroupedByVersion(list, namingOptions); } // If there's only one resolved video, use the folder name as well to find extras @@ -114,19 +92,14 @@ namespace Emby.Naming.Video { var info = list[0]; var videoPath = list[0].Files[0].Path; - var parentPath = Path.GetDirectoryName(videoPath); + var parentPath = Path.GetDirectoryName(videoPath.AsSpan()); - if (!string.IsNullOrEmpty(parentPath)) + if (!parentPath.IsEmpty) { var folderName = Path.GetFileName(parentPath); - if (!string.IsNullOrEmpty(folderName)) + if (!folderName.IsEmpty) { - var extras = GetExtras(remainingFiles, new List<string> { folderName }); - - remainingFiles = remainingFiles - .Except(extras) - .ToList(); - + var extras = ExtractExtras(remainingFiles, folderName, namingOptions.VideoFlagDelimiters); extras.AddRange(info.Extras); info.Extras = extras; } @@ -164,96 +137,168 @@ namespace Emby.Naming.Video // Whatever files are left, just add them list.AddRange(remainingFiles.Select(i => new VideoInfo(i.Name) { - Files = new List<VideoFileInfo> { i }, + Files = new[] { i }, Year = i.Year })); return list; } - private IEnumerable<VideoInfo> GetVideosGroupedByVersion(List<VideoInfo> videos) + private static List<VideoInfo> GetVideosGroupedByVersion(List<VideoInfo> videos, NamingOptions namingOptions) { if (videos.Count == 0) { return videos; } - var list = new List<VideoInfo>(); - - var folderName = Path.GetFileName(Path.GetDirectoryName(videos[0].Files[0].Path)); + var folderName = Path.GetFileName(Path.GetDirectoryName(videos[0].Files[0].Path.AsSpan())); - if (!string.IsNullOrEmpty(folderName) - && folderName.Length > 1 - && videos.All(i => i.Files.Count == 1 - && IsEligibleForMultiVersion(folderName, i.Files[0].Path)) - && HaveSameYear(videos)) + if (folderName.Length <= 1 || !HaveSameYear(videos)) { - var ordered = videos.OrderBy(i => i.Name).ToList(); - - list.Add(ordered[0]); + return videos; + } - var alternateVersionsLen = ordered.Count - 1; - var alternateVersions = new VideoFileInfo[alternateVersionsLen]; - for (int i = 0; i < alternateVersionsLen; i++) + // Cannot use Span inside local functions and delegates thus we cannot use LINQ here nor merge with the above [if] + for (var i = 0; i < videos.Count; i++) + { + var video = videos[i]; + if (!IsEligibleForMultiVersion(folderName, video.Files[0].Path, namingOptions)) { - alternateVersions[i] = ordered[i + 1].Files[0]; + return videos; } + } + + // The list is created and overwritten in the caller, so we are allowed to do in-place sorting + videos.Sort((x, y) => string.Compare(x.Name, y.Name, StringComparison.Ordinal)); - list[0].AlternateVersions = alternateVersions; - list[0].Name = folderName; - var extras = ordered.Skip(1).SelectMany(i => i.Extras).ToList(); - extras.AddRange(list[0].Extras); - list[0].Extras = extras; + var list = new List<VideoInfo> + { + videos[0] + }; - return list; + 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); } - return videos; - } + list[0].AlternateVersions = alternateVersions; + list[0].Name = folderName.ToString(); + list[0].Extras = extras; - private bool HaveSameYear(List<VideoInfo> videos) - { - return videos.Select(i => i.Year ?? -1).Distinct().Count() < 2; + return list; } - private bool IsEligibleForMultiVersion(string folderName, string testFilePath) + private static bool HaveSameYear(IReadOnlyList<VideoInfo> videos) { - string testFilename = Path.GetFileNameWithoutExtension(testFilePath); - if (testFilename.StartsWith(folderName, StringComparison.OrdinalIgnoreCase)) + if (videos.Count == 1) { - // Remove the folder name before cleaning as we don't care about cleaning that part - if (folderName.Length <= testFilename.Length) - { - testFilename = testFilename.Substring(folderName.Length).Trim(); - } + return true; + } - if (CleanStringParser.TryClean(testFilename, _options.CleanStringRegexes, out var cleanName)) + var firstYear = videos[0].Year ?? -1; + for (var i = 1; i < videos.Count; i++) + { + if ((videos[i].Year ?? -1) != firstYear) { - testFilename = cleanName.Trim().ToString(); + return false; } + } - // The CleanStringParser should have removed common keywords etc. - return string.IsNullOrEmpty(testFilename) - || testFilename[0] == '-' - || Regex.IsMatch(testFilename, @"^\[([^]]*)\]"); + return true; + } + + private static bool IsEligibleForMultiVersion(ReadOnlySpan<char> folderName, string testFilePath, NamingOptions namingOptions) + { + var testFilename = Path.GetFileNameWithoutExtension(testFilePath.AsSpan()); + if (!testFilename.StartsWith(folderName, StringComparison.OrdinalIgnoreCase)) + { + return false; } - return false; + // Remove the folder name before cleaning as we don't care about cleaning that part + if (folderName.Length <= testFilename.Length) + { + testFilename = testFilename[folderName.Length..].Trim(); + } + + // There are no span overloads for regex unfortunately + var tmpTestFilename = testFilename.ToString(); + if (CleanStringParser.TryClean(tmpTestFilename, namingOptions.CleanStringRegexes, out var cleanName)) + { + tmpTestFilename = cleanName.Trim().ToString(); + } + + // The CleanStringParser should have removed common keywords etc. + return string.IsNullOrEmpty(tmpTestFilename) + || 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 List<VideoFileInfo> GetExtras(IEnumerable<VideoFileInfo> remainingFiles, List<string> baseNames) + private static bool StartsWith(ReadOnlySpan<char> fileName, ReadOnlySpan<char> baseName, ReadOnlySpan<char> trimmedBaseName) { - foreach (var name in baseNames.ToList()) + if (baseName.IsEmpty) { - var trimmedName = name.TrimEnd().TrimEnd(_options.VideoFlagDelimiters).TrimEnd(); - baseNames.Add(trimmedName); + return false; } - return remainingFiles - .Where(i => i.ExtraType != null) - .Where(i => baseNames.Any(b => - i.FileNameWithoutExtension.StartsWith(b, StringComparison.OrdinalIgnoreCase))) - .ToList(); + 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 27e73208c..c4ac5fdc6 100644 --- a/Emby.Naming/Video/VideoResolver.cs +++ b/Emby.Naming/Video/VideoResolver.cs @@ -9,38 +9,28 @@ namespace Emby.Naming.Video /// <summary> /// Resolves <see cref="VideoFileInfo"/> from file path. /// </summary> - public class VideoResolver + public static class VideoResolver { - private readonly NamingOptions _options; - - /// <summary> - /// Initializes a new instance of the <see cref="VideoResolver"/> class. - /// </summary> - /// <param name="options"><see cref="NamingOptions"/> object containing VideoFileExtensions, StubFileExtensions, CleanStringRegexes and CleanDateTimeRegexes - /// and passes options in <see cref="StubResolver"/>, <see cref="FlagParser"/>, <see cref="Format3DParser"/> and <see cref="ExtraResolver"/>.</param> - public VideoResolver(NamingOptions options) - { - _options = options; - } - /// <summary> /// Resolves the directory. /// </summary> /// <param name="path">The path.</param> + /// <param name="namingOptions">The naming options.</param> /// <returns>VideoFileInfo.</returns> - public VideoFileInfo? ResolveDirectory(string? path) + public static VideoFileInfo? ResolveDirectory(string? path, NamingOptions namingOptions) { - return Resolve(path, true); + return Resolve(path, true, namingOptions); } /// <summary> /// Resolves the file. /// </summary> /// <param name="path">The path.</param> + /// <param name="namingOptions">The naming options.</param> /// <returns>VideoFileInfo.</returns> - public VideoFileInfo? ResolveFile(string? path) + public static VideoFileInfo? ResolveFile(string? path, NamingOptions namingOptions) { - return Resolve(path, false); + return Resolve(path, false, namingOptions); } /// <summary> @@ -48,10 +38,11 @@ namespace Emby.Naming.Video /// </summary> /// <param name="path">The path.</param> /// <param name="isDirectory">if set to <c>true</c> [is folder].</param> + /// <param name="namingOptions">The naming options.</param> /// <param name="parseName">Whether or not the name should be parsed for info.</param> /// <returns>VideoFileInfo.</returns> /// <exception cref="ArgumentNullException"><c>path</c> is <c>null</c>.</exception> - public VideoFileInfo? Resolve(string? path, bool isDirectory, bool parseName = true) + public static VideoFileInfo? Resolve(string? path, bool isDirectory, NamingOptions namingOptions, bool parseName = true) { if (string.IsNullOrEmpty(path)) { @@ -67,10 +58,10 @@ namespace Emby.Naming.Video var extension = Path.GetExtension(path.AsSpan()); // Check supported extensions - if (!_options.VideoFileExtensions.Contains(extension, StringComparison.OrdinalIgnoreCase)) + if (!namingOptions.VideoFileExtensions.Contains(extension, StringComparison.OrdinalIgnoreCase)) { // It's not supported. Check stub extensions - if (!StubResolver.TryResolveFile(path, _options, out stubType)) + if (!StubResolver.TryResolveFile(path, namingOptions, out stubType)) { return null; } @@ -81,10 +72,9 @@ namespace Emby.Naming.Video container = extension.TrimStart('.'); } - var flags = new FlagParser(_options).GetFlags(path); - var format3DResult = new Format3DParser(_options).Parse(flags); + var format3DResult = Format3DParser.Parse(path, namingOptions); - var extraResult = new ExtraResolver(_options).GetExtraInfo(path); + var extraResult = new ExtraResolver(namingOptions).GetExtraInfo(path); var name = Path.GetFileNameWithoutExtension(path); @@ -92,12 +82,12 @@ namespace Emby.Naming.Video if (parseName) { - var cleanDateTimeResult = CleanDateTime(name); + var cleanDateTimeResult = CleanDateTime(name, namingOptions); name = cleanDateTimeResult.Name; year = cleanDateTimeResult.Year; if (extraResult.ExtraType == null - && TryCleanString(name, out ReadOnlySpan<char> newName)) + && TryCleanString(name, namingOptions, out ReadOnlySpan<char> newName)) { name = newName.ToString(); } @@ -121,43 +111,47 @@ namespace Emby.Naming.Video /// Determines if path is video file based on extension. /// </summary> /// <param name="path">Path to file.</param> + /// <param name="namingOptions">The naming options.</param> /// <returns>True if is video file.</returns> - public bool IsVideoFile(string path) + public static bool IsVideoFile(string path, NamingOptions namingOptions) { var extension = Path.GetExtension(path.AsSpan()); - return _options.VideoFileExtensions.Contains(extension, StringComparison.OrdinalIgnoreCase); + return namingOptions.VideoFileExtensions.Contains(extension, StringComparison.OrdinalIgnoreCase); } /// <summary> /// Determines if path is video file stub based on extension. /// </summary> /// <param name="path">Path to file.</param> + /// <param name="namingOptions">The naming options.</param> /// <returns>True if is video file stub.</returns> - public bool IsStubFile(string path) + public static bool IsStubFile(string path, NamingOptions namingOptions) { var extension = Path.GetExtension(path.AsSpan()); - return _options.StubFileExtensions.Contains(extension, StringComparison.OrdinalIgnoreCase); + return namingOptions.StubFileExtensions.Contains(extension, StringComparison.OrdinalIgnoreCase); } /// <summary> /// Tries to clean name of clutter. /// </summary> /// <param name="name">Raw name.</param> + /// <param name="namingOptions">The naming options.</param> /// <param name="newName">Clean name.</param> /// <returns>True if cleaning of name was successful.</returns> - public bool TryCleanString([NotNullWhen(true)] string? name, out ReadOnlySpan<char> newName) + public static bool TryCleanString([NotNullWhen(true)] string? name, NamingOptions namingOptions, out ReadOnlySpan<char> newName) { - return CleanStringParser.TryClean(name, _options.CleanStringRegexes, out newName); + return CleanStringParser.TryClean(name, namingOptions.CleanStringRegexes, out newName); } /// <summary> /// Tries to get name and year from raw name. /// </summary> /// <param name="name">Raw name.</param> + /// <param name="namingOptions">The naming options.</param> /// <returns>Returns <see cref="CleanDateTimeResult"/> with name and optional year.</returns> - public CleanDateTimeResult CleanDateTime(string name) + public static CleanDateTimeResult CleanDateTime(string name, NamingOptions namingOptions) { - return CleanDateTimeParser.Clean(name, _options.CleanDateTimeRegexes); + return CleanDateTimeParser.Clean(name, namingOptions.CleanDateTimeRegexes); } } } diff --git a/Emby.Server.Implementations/AppBase/BaseConfigurationManager.cs b/Emby.Server.Implementations/AppBase/BaseConfigurationManager.cs index 4c442a473..d38535634 100644 --- a/Emby.Server.Implementations/AppBase/BaseConfigurationManager.cs +++ b/Emby.Server.Implementations/AppBase/BaseConfigurationManager.cs @@ -299,25 +299,29 @@ namespace Emby.Server.Implementations.AppBase /// <inheritdoc /> public object GetConfiguration(string key) { - return _configurations.GetOrAdd(key, k => - { - var file = GetConfigurationFile(key); + return _configurations.GetOrAdd( + key, + (k, configurationManager) => + { + var file = configurationManager.GetConfigurationFile(k); - var configurationInfo = _configurationStores - .FirstOrDefault(i => string.Equals(i.Key, key, StringComparison.OrdinalIgnoreCase)); + var configurationInfo = Array.Find( + configurationManager._configurationStores, + i => string.Equals(i.Key, k, StringComparison.OrdinalIgnoreCase)); - if (configurationInfo == null) - { - throw new ResourceNotFoundException("Configuration with key " + key + " not found."); - } + if (configurationInfo == null) + { + throw new ResourceNotFoundException("Configuration with key " + k + " not found."); + } - var configurationType = configurationInfo.ConfigurationType; + var configurationType = configurationInfo.ConfigurationType; - lock (_configurationSyncLock) - { - return LoadConfiguration(file, configurationType); - } - }); + lock (configurationManager._configurationSyncLock) + { + return configurationManager.LoadConfiguration(file, configurationType); + } + }, + this); } private object LoadConfiguration(string path, Type configurationType) diff --git a/Emby.Server.Implementations/Data/SqliteItemRepository.cs b/Emby.Server.Implementations/Data/SqliteItemRepository.cs index 2d060dd65..3ae28fadf 100644 --- a/Emby.Server.Implementations/Data/SqliteItemRepository.cs +++ b/Emby.Server.Implementations/Data/SqliteItemRepository.cs @@ -43,6 +43,7 @@ namespace Emby.Server.Implementations.Data /// </summary> public class SqliteItemRepository : BaseSqliteRepository, IItemRepository { + private const string FromText = " from TypedBaseItems A"; private const string ChaptersTableName = "Chapters2"; private readonly IServerConfigurationManager _config; @@ -1045,18 +1046,34 @@ namespace Emby.Server.Implementations.Data return Array.Empty<ItemImageInfo>(); } - var list = new List<ItemImageInfo>(); - foreach (var part in value.SpanSplit('|')) + // TODO The following is an ugly performance optimization, but it's extremely unlikely that the data in the database would be malformed + var valueSpan = value.AsSpan(); + var count = valueSpan.CountOccurrences('|') + 1; + + var position = 0; + var result = new ItemImageInfo[count]; + foreach (var part in valueSpan.Split('|')) { var image = ItemImageInfoFromValueString(part); if (image != null) { - list.Add(image); + result[position++] = image; } } - return list.ToArray(); + if (position == count) + { + return result; + } + + if (position == 0) + { + return Array.Empty<ItemImageInfo>(); + } + + // Extremely unlikely, but somehow one or more of the image strings were malformed. Cut the array. + return result[..position]; } private void AppendItemImageInfo(StringBuilder bldr, ItemImageInfo image) @@ -2250,10 +2267,8 @@ namespace Emby.Server.Implementations.Data return query.IncludeItemTypes.Any(x => _seriesTypes.Contains(x)); } - private List<string> GetFinalColumnsToSelect(InternalItemsQuery query, IEnumerable<string> startColumns) + private void SetFinalColumnsToSelect(InternalItemsQuery query, List<string> columns) { - var list = startColumns.ToList(); - foreach (var field in _allFields) { if (!HasField(query, field)) @@ -2261,28 +2276,28 @@ namespace Emby.Server.Implementations.Data switch (field) { case ItemFields.Settings: - list.Remove("IsLocked"); - list.Remove("PreferredMetadataCountryCode"); - list.Remove("PreferredMetadataLanguage"); - list.Remove("LockedFields"); + columns.Remove("IsLocked"); + columns.Remove("PreferredMetadataCountryCode"); + columns.Remove("PreferredMetadataLanguage"); + columns.Remove("LockedFields"); break; case ItemFields.ServiceName: - list.Remove("ExternalServiceId"); + columns.Remove("ExternalServiceId"); break; case ItemFields.SortName: - list.Remove("ForcedSortName"); + columns.Remove("ForcedSortName"); break; case ItemFields.Taglines: - list.Remove("Tagline"); + columns.Remove("Tagline"); break; case ItemFields.Tags: - list.Remove("Tags"); + columns.Remove("Tags"); break; case ItemFields.IsHD: // do nothing break; default: - list.Remove(field.ToString()); + columns.Remove(field.ToString()); break; } } @@ -2290,60 +2305,60 @@ namespace Emby.Server.Implementations.Data if (!HasProgramAttributes(query)) { - list.Remove("IsMovie"); - list.Remove("IsSeries"); - list.Remove("EpisodeTitle"); - list.Remove("IsRepeat"); - list.Remove("ShowId"); + columns.Remove("IsMovie"); + columns.Remove("IsSeries"); + columns.Remove("EpisodeTitle"); + columns.Remove("IsRepeat"); + columns.Remove("ShowId"); } if (!HasEpisodeAttributes(query)) { - list.Remove("SeasonName"); - list.Remove("SeasonId"); + columns.Remove("SeasonName"); + columns.Remove("SeasonId"); } if (!HasStartDate(query)) { - list.Remove("StartDate"); + columns.Remove("StartDate"); } if (!HasTrailerTypes(query)) { - list.Remove("TrailerTypes"); + columns.Remove("TrailerTypes"); } if (!HasArtistFields(query)) { - list.Remove("AlbumArtists"); - list.Remove("Artists"); + columns.Remove("AlbumArtists"); + columns.Remove("Artists"); } if (!HasSeriesFields(query)) { - list.Remove("SeriesId"); + columns.Remove("SeriesId"); } if (!HasEpisodeAttributes(query)) { - list.Remove("SeasonName"); - list.Remove("SeasonId"); + columns.Remove("SeasonName"); + columns.Remove("SeasonId"); } if (!query.DtoOptions.EnableImages) { - list.Remove("Images"); + columns.Remove("Images"); } if (EnableJoinUserData(query)) { - list.Add("UserDatas.UserId"); - list.Add("UserDatas.lastPlayedDate"); - list.Add("UserDatas.playbackPositionTicks"); - list.Add("UserDatas.playcount"); - list.Add("UserDatas.isFavorite"); - list.Add("UserDatas.played"); - list.Add("UserDatas.rating"); + columns.Add("UserDatas.UserId"); + columns.Add("UserDatas.lastPlayedDate"); + columns.Add("UserDatas.playbackPositionTicks"); + columns.Add("UserDatas.playcount"); + columns.Add("UserDatas.isFavorite"); + columns.Add("UserDatas.played"); + columns.Add("UserDatas.rating"); } if (query.SimilarTo != null) @@ -2391,7 +2406,7 @@ namespace Emby.Server.Implementations.Data builder.Append(") as SimilarityScore"); - list.Add(builder.ToString()); + columns.Add(builder.ToString()); var oldLen = query.ExcludeItemIds.Length; var newLen = oldLen + item.ExtraIds.Length + 1; @@ -2418,10 +2433,8 @@ namespace Emby.Server.Implementations.Data builder.Append(") as SearchScore"); - list.Add(builder.ToString()); + columns.Add(builder.ToString()); } - - return list; } private void BindSearchParams(InternalItemsQuery query, IStatement statement) @@ -2487,31 +2500,25 @@ namespace Emby.Server.Implementations.Data private string GetGroupBy(InternalItemsQuery query) { - var groups = new List<string>(); - - if (EnableGroupByPresentationUniqueKey(query)) + var enableGroupByPresentationUniqueKey = EnableGroupByPresentationUniqueKey(query); + if (enableGroupByPresentationUniqueKey && query.GroupBySeriesPresentationUniqueKey) { - groups.Add("PresentationUniqueKey"); + return " Group by PresentationUniqueKey, SeriesPresentationUniqueKey"; } - if (query.GroupBySeriesPresentationUniqueKey) + if (enableGroupByPresentationUniqueKey) { - groups.Add("SeriesPresentationUniqueKey"); + return " Group by PresentationUniqueKey"; } - if (groups.Count > 0) + if (query.GroupBySeriesPresentationUniqueKey) { - return " Group by " + string.Join(',', groups); + return " Group by SeriesPresentationUniqueKey"; } return string.Empty; } - private string GetFromText(string alias = "A") - { - return " from TypedBaseItems " + alias; - } - public int GetCount(InternalItemsQuery query) { if (query == null) @@ -2529,17 +2536,21 @@ namespace Emby.Server.Implementations.Data query.Limit = query.Limit.Value + 4; } - var commandText = "select " - + string.Join(',', GetFinalColumnsToSelect(query, new[] { "count(distinct PresentationUniqueKey)" })) - + GetFromText() - + GetJoinUserDataText(query); + var columns = new List<string> { "count(distinct PresentationUniqueKey)" }; + SetFinalColumnsToSelect(query, columns); + var commandTextBuilder = new StringBuilder("select ", 256) + .AppendJoin(',', columns) + .Append(FromText) + .Append(GetJoinUserDataText(query)); var whereClauses = GetWhereClauses(query, null); if (whereClauses.Count != 0) { - commandText += " where " + string.Join(" AND ", whereClauses); + commandTextBuilder.Append(" where ") + .AppendJoin(" AND ", whereClauses); } + var commandText = commandTextBuilder.ToString(); int count; using (var connection = GetConnection(true)) { @@ -2581,20 +2592,23 @@ namespace Emby.Server.Implementations.Data query.Limit = query.Limit.Value + 4; } - var commandText = "select " - + string.Join(',', GetFinalColumnsToSelect(query, _retriveItemColumns)) - + GetFromText() - + GetJoinUserDataText(query); + var columns = _retriveItemColumns.ToList(); + SetFinalColumnsToSelect(query, columns); + var commandTextBuilder = new StringBuilder("select ", 1024) + .AppendJoin(',', columns) + .Append(FromText) + .Append(GetJoinUserDataText(query)); var whereClauses = GetWhereClauses(query, null); if (whereClauses.Count != 0) { - commandText += " where " + string.Join(" AND ", whereClauses); + commandTextBuilder.Append(" where ") + .AppendJoin(" AND ", whereClauses); } - commandText += GetGroupBy(query) - + GetOrderByText(query); + commandTextBuilder.Append(GetGroupBy(query)) + .Append(GetOrderByText(query)); if (query.Limit.HasValue || query.StartIndex.HasValue) { @@ -2602,15 +2616,18 @@ namespace Emby.Server.Implementations.Data if (query.Limit.HasValue || offset > 0) { - commandText += " LIMIT " + (query.Limit ?? int.MaxValue).ToString(CultureInfo.InvariantCulture); + commandTextBuilder.Append(" LIMIT ") + .Append(query.Limit ?? int.MaxValue); } if (offset > 0) { - commandText += " OFFSET " + offset.ToString(CultureInfo.InvariantCulture); + commandTextBuilder.Append(" OFFSET ") + .Append(offset); } } + var commandText = commandTextBuilder.ToString(); var items = new List<BaseItem>(); using (var connection = GetConnection(true)) { @@ -2766,20 +2783,27 @@ namespace Emby.Server.Implementations.Data query.Limit = query.Limit.Value + 4; } - var commandText = "select " - + string.Join(',', GetFinalColumnsToSelect(query, _retriveItemColumns)) - + GetFromText() - + GetJoinUserDataText(query); + var columns = _retriveItemColumns.ToList(); + SetFinalColumnsToSelect(query, columns); + var commandTextBuilder = new StringBuilder("select ", 512) + .AppendJoin(',', columns) + .Append(FromText) + .Append(GetJoinUserDataText(query)); var whereClauses = GetWhereClauses(query, null); var whereText = whereClauses.Count == 0 ? string.Empty : - " where " + string.Join(" AND ", whereClauses); + string.Join(" AND ", whereClauses); - commandText += whereText - + GetGroupBy(query) - + GetOrderByText(query); + if (!string.IsNullOrEmpty(whereText)) + { + commandTextBuilder.Append(" where ") + .Append(whereText); + } + + commandTextBuilder.Append(GetGroupBy(query)) + .Append(GetOrderByText(query)); if (query.Limit.HasValue || query.StartIndex.HasValue) { @@ -2787,43 +2811,58 @@ namespace Emby.Server.Implementations.Data if (query.Limit.HasValue || offset > 0) { - commandText += " LIMIT " + (query.Limit ?? int.MaxValue).ToString(CultureInfo.InvariantCulture); + commandTextBuilder.Append(" LIMIT ") + .Append(query.Limit ?? int.MaxValue); } if (offset > 0) { - commandText += " OFFSET " + offset.ToString(CultureInfo.InvariantCulture); + commandTextBuilder.Append(" OFFSET ") + .Append(offset); } } var isReturningZeroItems = query.Limit.HasValue && query.Limit <= 0; - var statementTexts = new List<string>(); + var itemQuery = string.Empty; + var totalRecordCountQuery = string.Empty; if (!isReturningZeroItems) { - statementTexts.Add(commandText); + itemQuery = commandTextBuilder.ToString(); } if (query.EnableTotalRecordCount) { - commandText = string.Empty; + commandTextBuilder.Clear(); + + commandTextBuilder.Append(" select "); + List<string> columnsToSelect; if (EnableGroupByPresentationUniqueKey(query)) { - commandText += " select " + string.Join(',', GetFinalColumnsToSelect(query, new[] { "count (distinct PresentationUniqueKey)" })) + GetFromText(); + columnsToSelect = new List<string> { "count (distinct PresentationUniqueKey)" }; } else if (query.GroupBySeriesPresentationUniqueKey) { - commandText += " select " + string.Join(',', GetFinalColumnsToSelect(query, new[] { "count (distinct SeriesPresentationUniqueKey)" })) + GetFromText(); + columnsToSelect = new List<string> { "count (distinct SeriesPresentationUniqueKey)" }; } else { - commandText += " select " + string.Join(',', GetFinalColumnsToSelect(query, new[] { "count (guid)" })) + GetFromText(); + columnsToSelect = new List<string> { "count (guid)" }; } - commandText += GetJoinUserDataText(query) - + whereText; - statementTexts.Add(commandText); + SetFinalColumnsToSelect(query, columnsToSelect); + + commandTextBuilder.AppendJoin(',', columnsToSelect) + .Append(FromText) + .Append(GetJoinUserDataText(query)); + if (!string.IsNullOrEmpty(whereText)) + { + commandTextBuilder.Append(" where ") + .Append(whereText); + } + + totalRecordCountQuery = commandTextBuilder.ToString(); } var list = new List<BaseItem>(); @@ -2833,11 +2872,12 @@ namespace Emby.Server.Implementations.Data connection.RunInTransaction( db => { - var statements = PrepareAll(db, statementTexts); + var itemQueryStatement = PrepareStatement(db, itemQuery); + var totalRecordCountQueryStatement = PrepareStatement(db, totalRecordCountQuery); if (!isReturningZeroItems) { - using (var statement = statements[0]) + using (var statement = itemQueryStatement) { if (EnableJoinUserData(query)) { @@ -2867,11 +2907,14 @@ namespace Emby.Server.Implementations.Data } } } + + LogQueryTime("GetItems.ItemQuery", itemQuery, now); } + now = DateTime.UtcNow; if (query.EnableTotalRecordCount) { - using (var statement = statements[statements.Length - 1]) + using (var statement = totalRecordCountQueryStatement) { if (EnableJoinUserData(query)) { @@ -2886,11 +2929,12 @@ namespace Emby.Server.Implementations.Data result.TotalRecordCount = statement.ExecuteQuery().SelectScalarInt().First(); } + + LogQueryTime("GetItems.TotalRecordCount", totalRecordCountQuery, now); } }, ReadTransactionMode); } - LogQueryTime("GetItems", commandText, now); result.Items = list; return result; } @@ -3023,19 +3067,22 @@ namespace Emby.Server.Implementations.Data var now = DateTime.UtcNow; - var commandText = "select " - + string.Join(',', GetFinalColumnsToSelect(query, new[] { "guid" })) - + GetFromText() - + GetJoinUserDataText(query); + var columns = new List<string> { "guid" }; + SetFinalColumnsToSelect(query, columns); + var commandTextBuilder = new StringBuilder("select ", 256) + .AppendJoin(',', columns) + .Append(FromText) + .Append(GetJoinUserDataText(query)); var whereClauses = GetWhereClauses(query, null); if (whereClauses.Count != 0) { - commandText += " where " + string.Join(" AND ", whereClauses); + commandTextBuilder.Append(" where ") + .AppendJoin(" AND ", whereClauses); } - commandText += GetGroupBy(query) - + GetOrderByText(query); + commandTextBuilder.Append(GetGroupBy(query)) + .Append(GetOrderByText(query)); if (query.Limit.HasValue || query.StartIndex.HasValue) { @@ -3043,15 +3090,18 @@ namespace Emby.Server.Implementations.Data if (query.Limit.HasValue || offset > 0) { - commandText += " LIMIT " + (query.Limit ?? int.MaxValue).ToString(CultureInfo.InvariantCulture); + commandTextBuilder.Append(" LIMIT ") + .Append(query.Limit ?? int.MaxValue); } if (offset > 0) { - commandText += " OFFSET " + offset.ToString(CultureInfo.InvariantCulture); + commandTextBuilder.Append(" OFFSET ") + .Append(offset); } } + var commandText = commandTextBuilder.ToString(); var list = new List<Guid>(); using (var connection = GetConnection(true)) { @@ -3090,7 +3140,9 @@ namespace Emby.Server.Implementations.Data var now = DateTime.UtcNow; - var commandText = "select " + string.Join(',', GetFinalColumnsToSelect(query, new[] { "guid", "path" })) + GetFromText(); + var columns = new List<string> { "guid", "path" }; + SetFinalColumnsToSelect(query, columns); + var commandText = "select " + string.Join(',', columns) + FromText; var whereClauses = GetWhereClauses(query, null); if (whereClauses.Count != 0) @@ -3166,9 +3218,11 @@ namespace Emby.Server.Implementations.Data var now = DateTime.UtcNow; + var columns = new List<string> { "guid" }; + SetFinalColumnsToSelect(query, columns); var commandText = "select " - + string.Join(',', GetFinalColumnsToSelect(query, new[] { "guid" })) - + GetFromText() + + string.Join(',', columns) + + FromText + GetJoinUserDataText(query); var whereClauses = GetWhereClauses(query, null); @@ -3208,19 +3262,23 @@ namespace Emby.Server.Implementations.Data { commandText = string.Empty; + List<string> columnsToSelect; if (EnableGroupByPresentationUniqueKey(query)) { - commandText += " select " + string.Join(',', GetFinalColumnsToSelect(query, new[] { "count (distinct PresentationUniqueKey)" })) + GetFromText(); + columnsToSelect = new List<string> { "count (distinct PresentationUniqueKey)" }; } else if (query.GroupBySeriesPresentationUniqueKey) { - commandText += " select " + string.Join(',', GetFinalColumnsToSelect(query, new[] { "count (distinct SeriesPresentationUniqueKey)" })) + GetFromText(); + columnsToSelect = new List<string> { "count (distinct SeriesPresentationUniqueKey)" }; } else { - commandText += " select " + string.Join(',', GetFinalColumnsToSelect(query, new[] { "count (guid)" })) + GetFromText(); + columnsToSelect = new List<string> { "count (guid)" }; } + SetFinalColumnsToSelect(query, columnsToSelect); + commandText += " select " + string.Join(',', columnsToSelect) + FromText; + commandText += GetJoinUserDataText(query) + whereText; statementTexts.Add(commandText); @@ -4415,56 +4473,50 @@ namespace Emby.Server.Implementations.Data whereClauses.Add(GetProviderIdClause(query.HasTvdbId.Value, "tvdb")); } - var includedItemByNameTypes = GetItemByNameTypesInQuery(query); - var enableItemsByName = (query.IncludeItemsByName ?? false) && includedItemByNameTypes.Count > 0; - var queryTopParentIds = query.TopParentIds; - if (queryTopParentIds.Length == 1) + if (queryTopParentIds.Length > 0) { - if (enableItemsByName && includedItemByNameTypes.Count == 1) + var includedItemByNameTypes = GetItemByNameTypesInQuery(query); + var enableItemsByName = (query.IncludeItemsByName ?? false) && includedItemByNameTypes.Count > 0; + + if (queryTopParentIds.Length == 1) { - whereClauses.Add("(TopParentId=@TopParentId or Type=@IncludedItemByNameType)"); - if (statement != null) + if (enableItemsByName && includedItemByNameTypes.Count == 1) { - statement.TryBind("@IncludedItemByNameType", includedItemByNameTypes[0]); + whereClauses.Add("(TopParentId=@TopParentId or Type=@IncludedItemByNameType)"); + statement?.TryBind("@IncludedItemByNameType", includedItemByNameTypes[0]); + } + else if (enableItemsByName && includedItemByNameTypes.Count > 1) + { + var itemByNameTypeVal = string.Join(',', includedItemByNameTypes.Select(i => "'" + i + "'")); + whereClauses.Add("(TopParentId=@TopParentId or Type in (" + itemByNameTypeVal + "))"); + } + else + { + whereClauses.Add("(TopParentId=@TopParentId)"); } - } - else if (enableItemsByName && includedItemByNameTypes.Count > 1) - { - var itemByNameTypeVal = string.Join(',', includedItemByNameTypes.Select(i => "'" + i + "'")); - whereClauses.Add("(TopParentId=@TopParentId or Type in (" + itemByNameTypeVal + "))"); - } - else - { - whereClauses.Add("(TopParentId=@TopParentId)"); - } - if (statement != null) - { - statement.TryBind("@TopParentId", queryTopParentIds[0].ToString("N", CultureInfo.InvariantCulture)); + statement?.TryBind("@TopParentId", queryTopParentIds[0].ToString("N", CultureInfo.InvariantCulture)); } - } - else if (queryTopParentIds.Length > 1) - { - var val = string.Join(',', queryTopParentIds.Select(i => "'" + i.ToString("N", CultureInfo.InvariantCulture) + "'")); - - if (enableItemsByName && includedItemByNameTypes.Count == 1) + else if (queryTopParentIds.Length > 1) { - whereClauses.Add("(Type=@IncludedItemByNameType or TopParentId in (" + val + "))"); - if (statement != null) + var val = string.Join(',', queryTopParentIds.Select(i => "'" + i.ToString("N", CultureInfo.InvariantCulture) + "'")); + + if (enableItemsByName && includedItemByNameTypes.Count == 1) { - statement.TryBind("@IncludedItemByNameType", includedItemByNameTypes[0]); + whereClauses.Add("(Type=@IncludedItemByNameType or TopParentId in (" + val + "))"); + statement?.TryBind("@IncludedItemByNameType", includedItemByNameTypes[0]); + } + else if (enableItemsByName && includedItemByNameTypes.Count > 1) + { + var itemByNameTypeVal = string.Join(',', includedItemByNameTypes.Select(i => "'" + i + "'")); + whereClauses.Add("(Type in (" + itemByNameTypeVal + ") or TopParentId in (" + val + "))"); + } + else + { + whereClauses.Add("TopParentId in (" + val + ")"); } - } - else if (enableItemsByName && includedItemByNameTypes.Count > 1) - { - var itemByNameTypeVal = string.Join(',', includedItemByNameTypes.Select(i => "'" + i + "'")); - whereClauses.Add("(Type in (" + itemByNameTypeVal + ") or TopParentId in (" + val + "))"); - } - else - { - whereClauses.Add("TopParentId in (" + val + ")"); } } @@ -4746,17 +4798,12 @@ namespace Emby.Server.Implementations.Data return true; } - var types = new[] - { - nameof(Episode), - nameof(Video), - nameof(Movie), - nameof(MusicVideo), - nameof(Series), - nameof(Season) - }; - - if (types.Any(i => query.IncludeItemTypes.Contains(i, StringComparer.OrdinalIgnoreCase))) + if (query.IncludeItemTypes.Contains(nameof(Episode), StringComparer.OrdinalIgnoreCase) + || query.IncludeItemTypes.Contains(nameof(Video), StringComparer.OrdinalIgnoreCase) + || query.IncludeItemTypes.Contains(nameof(Movie), StringComparer.OrdinalIgnoreCase) + || query.IncludeItemTypes.Contains(nameof(MusicVideo), StringComparer.OrdinalIgnoreCase) + || query.IncludeItemTypes.Contains(nameof(Series), StringComparer.OrdinalIgnoreCase) + || query.IncludeItemTypes.Contains(nameof(Season), StringComparer.OrdinalIgnoreCase)) { return true; } @@ -5200,37 +5247,45 @@ AND Type = @InternalPersonType)"); var now = DateTime.UtcNow; - var typeClause = itemValueTypes.Length == 1 ? - ("Type=" + itemValueTypes[0].ToString(CultureInfo.InvariantCulture)) : - ("Type in (" + string.Join(',', itemValueTypes.Select(i => i.ToString(CultureInfo.InvariantCulture))) + ")"); - - var commandText = "Select Value From ItemValues where " + typeClause; + var stringBuilder = new StringBuilder("Select Value From ItemValues where Type", 128); + if (itemValueTypes.Length == 1) + { + stringBuilder.Append('=') + .Append(itemValueTypes[0]); + } + else + { + stringBuilder.Append(" in (") + .AppendJoin(',', itemValueTypes) + .Append(')'); + } if (withItemTypes.Count > 0) { - var typeString = string.Join(',', withItemTypes.Select(i => "'" + i + "'")); - commandText += " AND ItemId In (select guid from typedbaseitems where type in (" + typeString + "))"; + stringBuilder.Append(" AND ItemId In (select guid from typedbaseitems where type in (") + .AppendJoinInSingleQuotes(',', withItemTypes) + .Append("))"); } if (excludeItemTypes.Count > 0) { - var typeString = string.Join(',', excludeItemTypes.Select(i => "'" + i + "'")); - commandText += " AND ItemId not In (select guid from typedbaseitems where type in (" + typeString + "))"; + stringBuilder.Append(" AND ItemId not In (select guid from typedbaseitems where type in (") + .AppendJoinInSingleQuotes(',', excludeItemTypes) + .Append("))"); } - commandText += " Group By CleanValue"; + stringBuilder.Append(" Group By CleanValue"); + var commandText = stringBuilder.ToString(); var list = new List<string>(); using (var connection = GetConnection(true)) + using (var statement = PrepareStatement(connection, commandText)) { - using (var statement = PrepareStatement(connection, commandText)) + foreach (var row in statement.ExecuteQuery()) { - foreach (var row in statement.ExecuteQuery()) + if (row.TryGetString(0, out var result)) { - if (row.TryGetString(0, out var result)) - { - list.Add(result); - } + list.Add(result); } } } @@ -5256,18 +5311,19 @@ AND Type = @InternalPersonType)"); var now = DateTime.UtcNow; var typeClause = itemValueTypes.Length == 1 ? - ("Type=" + itemValueTypes[0].ToString(CultureInfo.InvariantCulture)) : - ("Type in (" + string.Join(',', itemValueTypes.Select(i => i.ToString(CultureInfo.InvariantCulture))) + ")"); + ("Type=" + itemValueTypes[0]) : + ("Type in (" + string.Join(',', itemValueTypes) + ")"); InternalItemsQuery typeSubQuery = null; - Dictionary<string, string> itemCountColumns = null; + string itemCountColumns = null; + var stringBuilder = new StringBuilder(1024); var typesToCount = query.IncludeItemTypes; if (typesToCount.Length > 0) { - var itemCountColumnQuery = "select group_concat(type, '|')" + GetFromText("B"); + stringBuilder.Append("(select group_concat(type, '|') from TypedBaseItems B"); typeSubQuery = new InternalItemsQuery(query.User) { @@ -5283,20 +5339,22 @@ AND Type = @InternalPersonType)"); }; var whereClauses = GetWhereClauses(typeSubQuery, null); - whereClauses.Add("guid in (select ItemId from ItemValues where ItemValues.CleanValue=A.CleanName AND " + typeClause + ")"); + stringBuilder.Append(" where ") + .AppendJoin(" AND ", whereClauses) + .Append(" AND ") + .Append("guid in (select ItemId from ItemValues where ItemValues.CleanValue=A.CleanName AND ") + .Append(typeClause) + .Append(")) as itemTypes"); - itemCountColumnQuery += " where " + string.Join(" AND ", whereClauses); - - itemCountColumns = new Dictionary<string, string>() - { - { "itemTypes", "(" + itemCountColumnQuery + ") as itemTypes" } - }; + itemCountColumns = stringBuilder.ToString(); + stringBuilder.Clear(); } List<string> columns = _retriveItemColumns.ToList(); - if (itemCountColumns != null) + // Unfortunately we need to add it to columns to ensure the order of the columns in the select + if (!string.IsNullOrEmpty(itemCountColumns)) { - columns.AddRange(itemCountColumns.Values); + columns.Add(itemCountColumns); } // do this first before calling GetFinalColumnsToSelect, otherwise ExcludeItemIds will be set by SimilarTo @@ -5317,20 +5375,20 @@ AND Type = @InternalPersonType)"); IsSeries = query.IsSeries }; - columns = GetFinalColumnsToSelect(query, columns); - - var commandText = "select " - + string.Join(',', columns) - + GetFromText() - + GetJoinUserDataText(query); + SetFinalColumnsToSelect(query, columns); var innerWhereClauses = GetWhereClauses(innerQuery, null); - var innerWhereText = innerWhereClauses.Count == 0 ? - string.Empty : - " where " + string.Join(" AND ", innerWhereClauses); + stringBuilder.Append(" where Type=@SelectType And CleanName In (Select CleanValue from ItemValues where ") + .Append(typeClause) + .Append(" AND ItemId in (select guid from TypedBaseItems"); + if (innerWhereClauses.Count > 0) + { + stringBuilder.Append(" where ") + .AppendJoin(" AND ", innerWhereClauses); + } - var whereText = " where Type=@SelectType And CleanName In (Select CleanValue from ItemValues where " + typeClause + " AND ItemId in (select guid from TypedBaseItems" + innerWhereText + "))"; + stringBuilder.Append("))"); var outerQuery = new InternalItemsQuery(query.User) { @@ -5355,23 +5413,31 @@ AND Type = @InternalPersonType)"); }; var outerWhereClauses = GetWhereClauses(outerQuery, null); - if (outerWhereClauses.Count != 0) { - whereText += " AND " + string.Join(" AND ", outerWhereClauses); + stringBuilder.Append(" AND ") + .AppendJoin(" AND ", outerWhereClauses); } - commandText += whereText + " group by PresentationUniqueKey"; + var whereText = stringBuilder.ToString(); + stringBuilder.Clear(); + + stringBuilder.Append("select ") + .AppendJoin(',', columns) + .Append(FromText) + .Append(GetJoinUserDataText(query)) + .Append(whereText) + .Append(" group by PresentationUniqueKey"); if (query.OrderBy.Count != 0 || query.SimilarTo != null || !string.IsNullOrEmpty(query.SearchTerm)) { - commandText += GetOrderByText(query); + stringBuilder.Append(GetOrderByText(query)); } else { - commandText += " order by SortName"; + stringBuilder.Append(" order by SortName"); } if (query.Limit.HasValue || query.StartIndex.HasValue) @@ -5380,32 +5446,39 @@ AND Type = @InternalPersonType)"); if (query.Limit.HasValue || offset > 0) { - commandText += " LIMIT " + (query.Limit ?? int.MaxValue).ToString(CultureInfo.InvariantCulture); + stringBuilder.Append(" LIMIT ") + .Append(query.Limit ?? int.MaxValue); } if (offset > 0) { - commandText += " OFFSET " + offset.ToString(CultureInfo.InvariantCulture); + stringBuilder.Append(" OFFSET ") + .Append(offset); } } var isReturningZeroItems = query.Limit.HasValue && query.Limit <= 0; - var statementTexts = new List<string>(); + string commandText = string.Empty; + if (!isReturningZeroItems) { - statementTexts.Add(commandText); + commandText = stringBuilder.ToString(); } + string countText = string.Empty; if (query.EnableTotalRecordCount) { - var countText = "select " - + string.Join(',', GetFinalColumnsToSelect(query, new[] { "count (distinct PresentationUniqueKey)" })) - + GetFromText() - + GetJoinUserDataText(query) - + whereText; + stringBuilder.Clear(); + var columnsToSelect = new List<string> { "count (distinct PresentationUniqueKey)" }; + SetFinalColumnsToSelect(query, columnsToSelect); + stringBuilder.Append("select ") + .AppendJoin(',', columnsToSelect) + .Append(FromText) + .Append(GetJoinUserDataText(query)) + .Append(whereText); - statementTexts.Add(countText); + countText = stringBuilder.ToString(); } var list = new List<(BaseItem, ItemCounts)>(); @@ -5415,11 +5488,9 @@ AND Type = @InternalPersonType)"); connection.RunInTransaction( db => { - var statements = PrepareAll(db, statementTexts); - if (!isReturningZeroItems) { - using (var statement = statements[0]) + using (var statement = PrepareStatement(db, commandText)) { statement.TryBind("@SelectType", returnType); if (EnableJoinUserData(query)) @@ -5460,13 +5531,7 @@ AND Type = @InternalPersonType)"); if (query.EnableTotalRecordCount) { - commandText = "select " - + string.Join(',', GetFinalColumnsToSelect(query, new[] { "count (distinct PresentationUniqueKey)" })) - + GetFromText() - + GetJoinUserDataText(query) - + whereText; - - using (var statement = statements[statements.Length - 1]) + using (var statement = PrepareStatement(db, countText)) { statement.TryBind("@SelectType", returnType); if (EnableJoinUserData(query)) diff --git a/Emby.Server.Implementations/Data/TypeMapper.cs b/Emby.Server.Implementations/Data/TypeMapper.cs index 7f1306d15..064664e1f 100644 --- a/Emby.Server.Implementations/Data/TypeMapper.cs +++ b/Emby.Server.Implementations/Data/TypeMapper.cs @@ -28,19 +28,9 @@ namespace Emby.Server.Implementations.Data throw new ArgumentNullException(nameof(typeName)); } - return _typeMap.GetOrAdd(typeName, LookupType); - } - - /// <summary> - /// Lookups the type. - /// </summary> - /// <param name="typeName">Name of the type.</param> - /// <returns>Type.</returns> - private Type? LookupType(string typeName) - { - return AppDomain.CurrentDomain.GetAssemblies() - .Select(a => a.GetType(typeName)) - .FirstOrDefault(t => t != null); + return _typeMap.GetOrAdd(typeName, k => AppDomain.CurrentDomain.GetAssemblies() + .Select(a => a.GetType(k)) + .FirstOrDefault(t => t != null)); } } } diff --git a/Emby.Server.Implementations/IO/ManagedFileSystem.cs b/Emby.Server.Implementations/IO/ManagedFileSystem.cs index 6a554e68a..64d802457 100644 --- a/Emby.Server.Implementations/IO/ManagedFileSystem.cs +++ b/Emby.Server.Implementations/IO/ManagedFileSystem.cs @@ -7,6 +7,7 @@ using System.IO; using System.Linq; using System.Runtime.InteropServices; using MediaBrowser.Common.Configuration; +using MediaBrowser.Common.Extensions; using MediaBrowser.Model.IO; using MediaBrowser.Model.System; using Microsoft.Extensions.Logging; @@ -243,8 +244,8 @@ namespace Emby.Server.Implementations.IO { result.Length = fileInfo.Length; - // Issue #2354 get the size of files behind symbolic links - if (fileInfo.Attributes.HasFlag(FileAttributes.ReparsePoint)) + // Issue #2354 get the size of files behind symbolic links. Also Enum.HasFlag is bad as it boxes! + if ((fileInfo.Attributes & FileAttributes.ReparsePoint) == FileAttributes.ReparsePoint) { try { @@ -618,13 +619,13 @@ namespace Emby.Server.Implementations.IO { files = files.Where(i => { - var ext = i.Extension; - if (ext == null) + var ext = i.Extension.AsSpan(); + if (ext.IsEmpty) { return false; } - return extensions.Contains(ext, StringComparer.OrdinalIgnoreCase); + return extensions.Contains(ext, StringComparison.OrdinalIgnoreCase); }); } @@ -636,8 +637,7 @@ namespace Emby.Server.Implementations.IO var directoryInfo = new DirectoryInfo(path); var enumerationOptions = GetEnumerationOptions(recursive); - return ToMetadata(directoryInfo.EnumerateDirectories("*", enumerationOptions)) - .Concat(ToMetadata(directoryInfo.EnumerateFiles("*", enumerationOptions))); + return ToMetadata(directoryInfo.EnumerateFileSystemInfos("*", enumerationOptions)); } private IEnumerable<FileSystemMetadata> ToMetadata(IEnumerable<FileSystemInfo> infos) @@ -672,13 +672,13 @@ namespace Emby.Server.Implementations.IO { files = files.Where(i => { - var ext = Path.GetExtension(i); - if (ext == null) + var ext = Path.GetExtension(i.AsSpan()); + if (ext.IsEmpty) { return false; } - return extensions.Contains(ext, StringComparer.OrdinalIgnoreCase); + return extensions.Contains(ext, StringComparison.OrdinalIgnoreCase); }); } diff --git a/Emby.Server.Implementations/Library/CoreResolutionIgnoreRule.cs b/Emby.Server.Implementations/Library/CoreResolutionIgnoreRule.cs index 3380e29d4..c7d113963 100644 --- a/Emby.Server.Implementations/Library/CoreResolutionIgnoreRule.cs +++ b/Emby.Server.Implementations/Library/CoreResolutionIgnoreRule.cs @@ -77,7 +77,7 @@ namespace Emby.Server.Implementations.Library if (parent != null) { // Don't resolve these into audio files - if (string.Equals(Path.GetFileNameWithoutExtension(filename), BaseItem.ThemeSongFilename, StringComparison.Ordinal) + if (Path.GetFileNameWithoutExtension(filename.AsSpan()).Equals(BaseItem.ThemeSongFilename, StringComparison.Ordinal) && _libraryManager.IsAudioFile(filename)) { return true; diff --git a/Emby.Server.Implementations/Library/LibraryManager.cs b/Emby.Server.Implementations/Library/LibraryManager.cs index 4af1f10f0..028673529 100644 --- a/Emby.Server.Implementations/Library/LibraryManager.cs +++ b/Emby.Server.Implementations/Library/LibraryManager.cs @@ -696,25 +696,32 @@ namespace Emby.Server.Implementations.Library } private IEnumerable<BaseItem> ResolveFileList( - IEnumerable<FileSystemMetadata> fileList, + IReadOnlyList<FileSystemMetadata> fileList, IDirectoryService directoryService, Folder parent, string collectionType, IItemResolver[] resolvers, LibraryOptions libraryOptions) { - return fileList.Select(f => + // Given that fileList is a list we can save enumerator allocations by indexing + for (var i = 0; i < fileList.Count; i++) { + var file = fileList[i]; + BaseItem result = null; try { - return ResolvePath(f, directoryService, resolvers, parent, collectionType, libraryOptions); + result = ResolvePath(file, directoryService, resolvers, parent, collectionType, libraryOptions); } catch (Exception ex) { - _logger.LogError(ex, "Error resolving path {path}", f.FullName); - return null; + _logger.LogError(ex, "Error resolving path {Path}", file.FullName); } - }).Where(i => i != null); + + if (result != null) + { + yield return result; + } + } } /// <summary> @@ -2076,7 +2083,7 @@ namespace Emby.Server.Implementations.Library return new List<Folder>(); } - return GetCollectionFoldersInternal(item, GetUserRootFolder().Children.OfType<Folder>().ToList()); + return GetCollectionFoldersInternal(item, GetUserRootFolder().Children.OfType<Folder>()); } public List<Folder> GetCollectionFolders(BaseItem item, List<Folder> allUserRootChildren) @@ -2101,10 +2108,10 @@ namespace Emby.Server.Implementations.Library return GetCollectionFoldersInternal(item, allUserRootChildren); } - private static List<Folder> GetCollectionFoldersInternal(BaseItem item, List<Folder> allUserRootChildren) + private static List<Folder> GetCollectionFoldersInternal(BaseItem item, IEnumerable<Folder> allUserRootChildren) { return allUserRootChildren - .Where(i => string.Equals(i.Path, item.Path, StringComparison.OrdinalIgnoreCase) || i.PhysicalLocations.Contains(item.Path, StringComparer.OrdinalIgnoreCase)) + .Where(i => string.Equals(i.Path, item.Path, StringComparison.OrdinalIgnoreCase) || i.PhysicalLocations.Contains(item.Path.AsSpan(), StringComparison.OrdinalIgnoreCase)) .ToList(); } @@ -2112,9 +2119,9 @@ namespace Emby.Server.Implementations.Library { if (!(item is CollectionFolder collectionFolder)) { + // List.Find is more performant than FirstOrDefault due to enumerator allocation collectionFolder = GetCollectionFolders(item) - .OfType<CollectionFolder>() - .FirstOrDefault(); + .Find(folder => folder is CollectionFolder) as CollectionFolder; } return collectionFolder == null ? new LibraryOptions() : collectionFolder.GetLibraryOptions(); @@ -2500,8 +2507,7 @@ namespace Emby.Server.Implementations.Library /// <inheritdoc /> public bool IsVideoFile(string path) { - var resolver = new VideoResolver(GetNamingOptions()); - return resolver.IsVideoFile(path); + return VideoResolver.IsVideoFile(path, GetNamingOptions()); } /// <inheritdoc /> @@ -2679,6 +2685,7 @@ namespace Emby.Server.Implementations.Library return changed; } + /// <inheritdoc /> public NamingOptions GetNamingOptions() { if (_namingOptions == null) @@ -2692,13 +2699,12 @@ namespace Emby.Server.Implementations.Library public ItemLookupInfo ParseName(string name) { - var resolver = new VideoResolver(GetNamingOptions()); - - var result = resolver.CleanDateTime(name); + var namingOptions = GetNamingOptions(); + var result = VideoResolver.CleanDateTime(name, namingOptions); return new ItemLookupInfo { - Name = resolver.TryCleanString(result.Name, out var newName) ? newName.ToString() : result.Name, + Name = VideoResolver.TryCleanString(result.Name, namingOptions, out var newName) ? newName.ToString() : result.Name, Year = result.Year }; } @@ -2712,9 +2718,7 @@ namespace Emby.Server.Implementations.Library .SelectMany(i => _fileSystem.GetFiles(i.FullName, _videoFileExtensions, false, false)) .ToList(); - var videoListResolver = new VideoListResolver(namingOptions); - - var videos = videoListResolver.Resolve(fileSystemChildren); + var videos = VideoListResolver.Resolve(fileSystemChildren, namingOptions); var currentVideo = videos.FirstOrDefault(i => string.Equals(owner.Path, i.Files[0].Path, StringComparison.OrdinalIgnoreCase)); @@ -2758,9 +2762,7 @@ namespace Emby.Server.Implementations.Library .SelectMany(i => _fileSystem.GetFiles(i.FullName, _videoFileExtensions, false, false)) .ToList(); - var videoListResolver = new VideoListResolver(namingOptions); - - var videos = videoListResolver.Resolve(fileSystemChildren); + var videos = VideoListResolver.Resolve(fileSystemChildren, namingOptions); var currentVideo = videos.FirstOrDefault(i => string.Equals(owner.Path, i.Files[0].Path, StringComparison.OrdinalIgnoreCase)); diff --git a/Emby.Server.Implementations/Library/MediaSourceManager.cs b/Emby.Server.Implementations/Library/MediaSourceManager.cs index 38e81d14c..b812b6b61 100644 --- a/Emby.Server.Implementations/Library/MediaSourceManager.cs +++ b/Emby.Server.Implementations/Library/MediaSourceManager.cs @@ -352,7 +352,7 @@ namespace Emby.Server.Implementations.Library private string[] NormalizeLanguage(string language) { - if (language == null) + if (string.IsNullOrEmpty(language)) { return Array.Empty<string>(); } @@ -381,8 +381,7 @@ namespace Emby.Server.Implementations.Library } } - var preferredSubs = string.IsNullOrEmpty(user.SubtitleLanguagePreference) - ? Array.Empty<string>() : NormalizeLanguage(user.SubtitleLanguagePreference); + var preferredSubs = NormalizeLanguage(user.SubtitleLanguagePreference); var defaultAudioIndex = source.DefaultAudioStreamIndex; var audioLangage = defaultAudioIndex == null @@ -411,9 +410,7 @@ namespace Emby.Server.Implementations.Library } } - var preferredAudio = string.IsNullOrEmpty(user.AudioLanguagePreference) - ? Array.Empty<string>() - : NormalizeLanguage(user.AudioLanguagePreference); + var preferredAudio = NormalizeLanguage(user.AudioLanguagePreference); source.DefaultAudioStreamIndex = MediaStreamSelector.GetDefaultAudioStreamIndex(source.MediaStreams, preferredAudio, user.PlayDefaultAudioTrack); } diff --git a/Emby.Server.Implementations/Library/Resolvers/BaseVideoResolver.cs b/Emby.Server.Implementations/Library/Resolvers/BaseVideoResolver.cs index a3dcdc944..cdb492022 100644 --- a/Emby.Server.Implementations/Library/Resolvers/BaseVideoResolver.cs +++ b/Emby.Server.Implementations/Library/Resolvers/BaseVideoResolver.cs @@ -47,11 +47,9 @@ namespace Emby.Server.Implementations.Library.Resolvers protected virtual TVideoType ResolveVideo<TVideoType>(ItemResolveArgs args, bool parseName) where TVideoType : Video, new() { - var namingOptions = ((LibraryManager)LibraryManager).GetNamingOptions(); + var namingOptions = LibraryManager.GetNamingOptions(); // If the path is a file check for a matching extensions - var parser = new VideoResolver(namingOptions); - if (args.IsDirectory) { TVideoType video = null; @@ -66,7 +64,7 @@ namespace Emby.Server.Implementations.Library.Resolvers { if (IsDvdDirectory(child.FullName, filename, args.DirectoryService)) { - videoInfo = parser.ResolveDirectory(args.Path); + videoInfo = VideoResolver.ResolveDirectory(args.Path, namingOptions); if (videoInfo == null) { @@ -84,7 +82,7 @@ namespace Emby.Server.Implementations.Library.Resolvers if (IsBluRayDirectory(child.FullName, filename, args.DirectoryService)) { - videoInfo = parser.ResolveDirectory(args.Path); + videoInfo = VideoResolver.ResolveDirectory(args.Path, namingOptions); if (videoInfo == null) { @@ -102,7 +100,7 @@ namespace Emby.Server.Implementations.Library.Resolvers } else if (IsDvdFile(filename)) { - videoInfo = parser.ResolveDirectory(args.Path); + videoInfo = VideoResolver.ResolveDirectory(args.Path, namingOptions); if (videoInfo == null) { @@ -132,7 +130,7 @@ namespace Emby.Server.Implementations.Library.Resolvers } else { - var videoInfo = parser.Resolve(args.Path, false, false); + var videoInfo = VideoResolver.Resolve(args.Path, false, namingOptions, false); if (videoInfo == null) { @@ -252,10 +250,7 @@ namespace Emby.Server.Implementations.Library.Resolvers protected void Set3DFormat(Video video) { - var namingOptions = ((LibraryManager)LibraryManager).GetNamingOptions(); - - var resolver = new Format3DParser(namingOptions); - var result = resolver.Parse(video.Path); + var result = Format3DParser.Parse(video.Path, LibraryManager.GetNamingOptions()); Set3DFormat(video, result.Is3D, result.Format3D); } diff --git a/Emby.Server.Implementations/Library/Resolvers/Movies/MovieResolver.cs b/Emby.Server.Implementations/Library/Resolvers/Movies/MovieResolver.cs index 02c528764..97f96f746 100644 --- a/Emby.Server.Implementations/Library/Resolvers/Movies/MovieResolver.cs +++ b/Emby.Server.Implementations/Library/Resolvers/Movies/MovieResolver.cs @@ -6,6 +6,7 @@ using System.IO; using System.Linq; using System.Text.RegularExpressions; using Emby.Naming.Video; +using MediaBrowser.Common.Extensions; using MediaBrowser.Controller.Drawing; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities.Movies; @@ -257,10 +258,9 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies } } - var namingOptions = ((LibraryManager)LibraryManager).GetNamingOptions(); + var namingOptions = LibraryManager.GetNamingOptions(); - var resolver = new VideoListResolver(namingOptions); - var resolverResult = resolver.Resolve(files, suppportMultiEditions).ToList(); + var resolverResult = VideoListResolver.Resolve(files, namingOptions, suppportMultiEditions).ToList(); var result = new MultiItemResolverResult { @@ -537,7 +537,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies return returnVideo; } - private bool IsInvalid(Folder parent, string collectionType) + private bool IsInvalid(Folder parent, ReadOnlySpan<char> collectionType) { if (parent != null) { @@ -547,12 +547,12 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies } } - if (string.IsNullOrEmpty(collectionType)) + if (collectionType.IsEmpty) { return false; } - return !_validCollectionTypes.Contains(collectionType, StringComparer.OrdinalIgnoreCase); + return !_validCollectionTypes.Contains(collectionType, StringComparison.OrdinalIgnoreCase); } } } diff --git a/Emby.Server.Implementations/Localization/LocalizationManager.cs b/Emby.Server.Implementations/Localization/LocalizationManager.cs index dd5dee1d1..b1ff28c2c 100644 --- a/Emby.Server.Implementations/Localization/LocalizationManager.cs +++ b/Emby.Server.Implementations/Localization/LocalizationManager.cs @@ -5,7 +5,6 @@ using System.Collections.Concurrent; using System.Collections.Generic; using System.Globalization; using System.IO; -using System.Linq; using System.Reflection; using System.Text.Json; using System.Threading.Tasks; @@ -169,12 +168,22 @@ namespace Emby.Server.Implementations.Localization /// <inheritdoc /> public CultureDto FindLanguageInfo(string language) - => GetCultures() - .FirstOrDefault(i => - string.Equals(i.DisplayName, language, StringComparison.OrdinalIgnoreCase) - || string.Equals(i.Name, language, StringComparison.OrdinalIgnoreCase) - || i.ThreeLetterISOLanguageNames.Contains(language, StringComparer.OrdinalIgnoreCase) - || string.Equals(i.TwoLetterISOLanguageName, language, StringComparison.OrdinalIgnoreCase)); + { + // TODO language should ideally be a ReadOnlySpan but moq cannot mock ref structs + for (var i = 0; i < _cultures.Count; i++) + { + var culture = _cultures[i]; + if (language.Equals(culture.DisplayName, StringComparison.OrdinalIgnoreCase) + || language.Equals(culture.Name, StringComparison.OrdinalIgnoreCase) + || culture.ThreeLetterISOLanguageNames.Contains(language, StringComparison.OrdinalIgnoreCase) + || language.Equals(culture.TwoLetterISOLanguageName, StringComparison.OrdinalIgnoreCase)) + { + return culture; + } + } + + return default; + } /// <inheritdoc /> public IEnumerable<CountryInfo> GetCountries() @@ -224,7 +233,7 @@ namespace Emby.Server.Implementations.Localization throw new ArgumentNullException(nameof(rating)); } - if (_unratedValues.Contains(rating, StringComparer.OrdinalIgnoreCase)) + if (_unratedValues.Contains(rating.AsSpan(), StringComparison.OrdinalIgnoreCase)) { return null; } @@ -252,11 +261,11 @@ namespace Emby.Server.Implementations.Localization var index = rating.IndexOf(':', StringComparison.Ordinal); if (index != -1) { - rating = rating.Substring(index).TrimStart(':').Trim(); + var trimmedRating = rating.AsSpan(index).TrimStart(':').Trim(); - if (!string.IsNullOrWhiteSpace(rating)) + if (!trimmedRating.IsEmpty) { - return GetRatingLevel(rating); + return GetRatingLevel(trimmedRating.ToString()); } } @@ -318,7 +327,8 @@ namespace Emby.Server.Implementations.Localization return _dictionaries.GetOrAdd( culture, - f => GetDictionary(Prefix, culture, DefaultCulture + ".json").GetAwaiter().GetResult()); + (key, localizationManager) => localizationManager.GetDictionary(Prefix, key, DefaultCulture + ".json").GetAwaiter().GetResult(), + this); } private async Task<Dictionary<string, string>> GetDictionary(string prefix, string culture, string baseFilename) diff --git a/Emby.Server.Implementations/Serialization/MyXmlSerializer.cs b/Emby.Server.Implementations/Serialization/MyXmlSerializer.cs index 8d8b82f0a..5ff73de81 100644 --- a/Emby.Server.Implementations/Serialization/MyXmlSerializer.cs +++ b/Emby.Server.Implementations/Serialization/MyXmlSerializer.cs @@ -21,7 +21,8 @@ namespace Emby.Server.Implementations.Serialization private static XmlSerializer GetSerializer(Type type) => _serializers.GetOrAdd( type.FullName ?? throw new ArgumentException($"Invalid type {type}."), - _ => new XmlSerializer(type)); + (_, t) => new XmlSerializer(t), + type); /// <summary> /// Serializes to writer. diff --git a/Emby.Server.Implementations/ServerApplicationPaths.cs b/Emby.Server.Implementations/ServerApplicationPaths.cs index ac589b03c..6cf9a8f71 100644 --- a/Emby.Server.Implementations/ServerApplicationPaths.cs +++ b/Emby.Server.Implementations/ServerApplicationPaths.cs @@ -25,6 +25,10 @@ namespace Emby.Server.Implementations cacheDirectoryPath, webDirectoryPath) { + // ProgramDataPath cannot change when the server is running, so cache these to avoid allocations. + RootFolderPath = Path.Join(ProgramDataPath, "root"); + DefaultUserViewsPath = Path.Combine(RootFolderPath, "default"); + DefaultInternalMetadataPath = Path.Combine(ProgramDataPath, "metadata"); InternalMetadataPath = DefaultInternalMetadataPath; } @@ -32,13 +36,13 @@ namespace Emby.Server.Implementations /// Gets the path to the base root media directory. /// </summary> /// <value>The root folder path.</value> - public string RootFolderPath => Path.Combine(ProgramDataPath, "root"); + public string RootFolderPath { get; } /// <summary> /// Gets the path to the default user view directory. Used if no specific user view is defined. /// </summary> /// <value>The default user views path.</value> - public string DefaultUserViewsPath => Path.Combine(RootFolderPath, "default"); + public string DefaultUserViewsPath { get; } /// <summary> /// Gets the path to the People directory. @@ -98,7 +102,7 @@ namespace Emby.Server.Implementations public string UserConfigurationDirectoryPath => Path.Combine(ConfigurationDirectoryPath, "users"); /// <inheritdoc/> - public string DefaultInternalMetadataPath => Path.Combine(ProgramDataPath, "metadata"); + public string DefaultInternalMetadataPath { get; } /// <inheritdoc /> public string InternalMetadataPath { get; set; } diff --git a/MediaBrowser.Common/Extensions/StringBuilderExtensions.cs b/MediaBrowser.Common/Extensions/StringBuilderExtensions.cs new file mode 100644 index 000000000..75d654f23 --- /dev/null +++ b/MediaBrowser.Common/Extensions/StringBuilderExtensions.cs @@ -0,0 +1,35 @@ +using System.Collections.Generic; +using System.Text; + +namespace MediaBrowser.Common.Extensions +{ + /// <summary> + /// Extension methods for the <see cref="StringBuilder"/> class. + /// </summary> + public static class StringBuilderExtensions + { + /// <summary> + /// Concatenates and appends the members of a collection in single quotes using the specified delimiter. + /// </summary> + /// <param name="builder">The string builder.</param> + /// <param name="delimiter">The character delimiter.</param> + /// <param name="values">The collection of strings to concatenate.</param> + /// <returns>The updated string builder.</returns> + public static StringBuilder AppendJoinInSingleQuotes(this StringBuilder builder, char delimiter, IReadOnlyList<string> values) + { + var len = values.Count; + for (var i = 0; i < len; i++) + { + builder.Append('\'') + .Append(values[i]) + .Append('\'') + .Append(delimiter); + } + + // remove last , + builder.Length--; + + return builder; + } + } +} diff --git a/MediaBrowser.Controller/BaseItemManager/BaseItemManager.cs b/MediaBrowser.Controller/BaseItemManager/BaseItemManager.cs index 68119cfed..ffc274c5d 100644 --- a/MediaBrowser.Controller/BaseItemManager/BaseItemManager.cs +++ b/MediaBrowser.Controller/BaseItemManager/BaseItemManager.cs @@ -3,6 +3,7 @@ using System; using System.Linq; using System.Threading; +using MediaBrowser.Common.Extensions; using MediaBrowser.Controller.Channels; using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Entities; @@ -52,7 +53,7 @@ namespace MediaBrowser.Controller.BaseItemManager var typeOptions = libraryOptions.GetTypeOptions(baseItem.GetType().Name); if (typeOptions != null) { - return typeOptions.MetadataFetchers.Contains(name, StringComparer.OrdinalIgnoreCase); + return typeOptions.MetadataFetchers.Contains(name.AsSpan(), StringComparison.OrdinalIgnoreCase); } if (!libraryOptions.EnableInternetProviders) @@ -62,7 +63,7 @@ namespace MediaBrowser.Controller.BaseItemManager var itemConfig = _serverConfigurationManager.Configuration.MetadataOptions.FirstOrDefault(i => string.Equals(i.ItemType, GetType().Name, StringComparison.OrdinalIgnoreCase)); - return itemConfig == null || !itemConfig.DisabledMetadataFetchers.Contains(name, StringComparer.OrdinalIgnoreCase); + return itemConfig == null || !itemConfig.DisabledMetadataFetchers.Contains(name.AsSpan(), StringComparison.OrdinalIgnoreCase); } /// <inheritdoc /> @@ -83,7 +84,7 @@ namespace MediaBrowser.Controller.BaseItemManager var typeOptions = libraryOptions.GetTypeOptions(baseItem.GetType().Name); if (typeOptions != null) { - return typeOptions.ImageFetchers.Contains(name, StringComparer.OrdinalIgnoreCase); + return typeOptions.ImageFetchers.Contains(name.AsSpan(), StringComparison.OrdinalIgnoreCase); } if (!libraryOptions.EnableInternetProviders) @@ -93,7 +94,7 @@ namespace MediaBrowser.Controller.BaseItemManager var itemConfig = _serverConfigurationManager.Configuration.MetadataOptions.FirstOrDefault(i => string.Equals(i.ItemType, GetType().Name, StringComparison.OrdinalIgnoreCase)); - return itemConfig == null || !itemConfig.DisabledImageFetchers.Contains(name, StringComparer.OrdinalIgnoreCase); + return itemConfig == null || !itemConfig.DisabledImageFetchers.Contains(name.AsSpan(), StringComparison.OrdinalIgnoreCase); } /// <summary> diff --git a/MediaBrowser.Controller/Entities/BaseItem.cs b/MediaBrowser.Controller/Entities/BaseItem.cs index e879968e1..6137ddbf7 100644 --- a/MediaBrowser.Controller/Entities/BaseItem.cs +++ b/MediaBrowser.Controller/Entities/BaseItem.cs @@ -670,14 +670,12 @@ namespace MediaBrowser.Controller.Entities { if (SourceType == SourceType.Channel) { - return System.IO.Path.Combine(basePath, "channels", ChannelId.ToString("N", CultureInfo.InvariantCulture), Id.ToString("N", CultureInfo.InvariantCulture)); + return System.IO.Path.Join(basePath, "channels", ChannelId.ToString("N", CultureInfo.InvariantCulture), Id.ToString("N", CultureInfo.InvariantCulture)); } ReadOnlySpan<char> idString = Id.ToString("N", CultureInfo.InvariantCulture); - basePath = System.IO.Path.Combine(basePath, "library"); - - return System.IO.Path.Join(basePath, idString.Slice(0, 2), idString); + return System.IO.Path.Join(basePath, "library", idString.Slice(0, 2), idString); } /// <summary> @@ -1262,7 +1260,7 @@ namespace MediaBrowser.Controller.Entities // Support plex/xbmc convention files.AddRange(fileSystemChildren - .Where(i => !i.IsDirectory && string.Equals(FileSystem.GetFileNameWithoutExtension(i), ThemeSongFilename, StringComparison.OrdinalIgnoreCase))); + .Where(i => !i.IsDirectory && System.IO.Path.GetFileNameWithoutExtension(i.FullName.AsSpan()).Equals(ThemeSongFilename, StringComparison.OrdinalIgnoreCase))); return LibraryManager.ResolvePaths(files, directoryService, null, new LibraryOptions()) .OfType<Audio.Audio>() @@ -1323,14 +1321,16 @@ namespace MediaBrowser.Controller.Entities { var extras = new List<Video>(); - var folders = fileSystemChildren.Where(i => i.IsDirectory).ToArray(); + var libraryOptions = new LibraryOptions(); + var folders = fileSystemChildren.Where(i => i.IsDirectory).ToList(); foreach (var extraFolderName in AllExtrasTypesFolderNames) { var files = folders .Where(i => string.Equals(i.Name, extraFolderName, StringComparison.OrdinalIgnoreCase)) .SelectMany(i => FileSystem.GetFiles(i.FullName)); - extras.AddRange(LibraryManager.ResolvePaths(files, directoryService, null, new LibraryOptions()) + // Re-using the same instance of LibraryOptions since it looks like it's never being altered. + extras.AddRange(LibraryManager.ResolvePaths(files, directoryService, null, libraryOptions) .OfType<Video>() .Select(item => { @@ -2327,7 +2327,7 @@ namespace MediaBrowser.Controller.Entities .Where(i => i.IsLocalFile) .Select(i => System.IO.Path.GetDirectoryName(i.Path)) .Distinct(StringComparer.OrdinalIgnoreCase) - .SelectMany(directoryService.GetFilePaths) + .SelectMany(path => directoryService.GetFilePaths(path)) .ToList(); var deletedImages = ImageInfos @@ -2436,7 +2436,15 @@ namespace MediaBrowser.Controller.Entities throw new ArgumentException("No image info for chapter images"); } - return ImageInfos.Where(i => i.Type == imageType); + // Yield return is more performant than LINQ Where on an Array + for (var i = 0; i < ImageInfos.Length; i++) + { + var imageInfo = ImageInfos[i]; + if (imageInfo.Type == imageType) + { + yield return imageInfo; + } + } } /// <summary> @@ -2468,7 +2476,7 @@ namespace MediaBrowser.Controller.Entities } var existing = existingImages - .FirstOrDefault(i => string.Equals(i.Path, newImage.FullName, StringComparison.OrdinalIgnoreCase)); + .Find(i => string.Equals(i.Path, newImage.FullName, StringComparison.OrdinalIgnoreCase)); if (existing == null) { @@ -2499,8 +2507,7 @@ namespace MediaBrowser.Controller.Entities var newImagePaths = images.Select(i => i.FullName).ToList(); var deleted = existingImages - .Where(i => i.IsLocalFile && !newImagePaths.Contains(i.Path, StringComparer.OrdinalIgnoreCase) && !File.Exists(i.Path)) - .ToList(); + .FindAll(i => i.IsLocalFile && !newImagePaths.Contains(i.Path.AsSpan(), StringComparison.OrdinalIgnoreCase) && !File.Exists(i.Path)); if (deleted.Count > 0) { diff --git a/MediaBrowser.Controller/Extensions/StringExtensions.cs b/MediaBrowser.Controller/Extensions/StringExtensions.cs index 1853896ee..291485e75 100644 --- a/MediaBrowser.Controller/Extensions/StringExtensions.cs +++ b/MediaBrowser.Controller/Extensions/StringExtensions.cs @@ -22,6 +22,27 @@ namespace MediaBrowser.Controller.Extensions return Normalize(string.Concat(chars), NormalizationForm.FormC); } + /// <summary> + /// Counts the number of occurrences of [needle] in the string. + /// </summary> + /// <param name="value">The haystack to search in.</param> + /// <param name="needle">The character to search for.</param> + /// <returns>The number of occurrences of the [needle] character.</returns> + public static int CountOccurrences(this ReadOnlySpan<char> value, char needle) + { + var count = 0; + var length = value.Length; + for (var i = 0; i < length; i++) + { + if (value[i] == needle) + { + count++; + } + } + + return count; + } + private static string Normalize(string text, NormalizationForm form, bool stripStringOnFailure = true) { if (stripStringOnFailure) diff --git a/MediaBrowser.Controller/Library/ILibraryManager.cs b/MediaBrowser.Controller/Library/ILibraryManager.cs index 0593e65f5..7a4ba6a24 100644 --- a/MediaBrowser.Controller/Library/ILibraryManager.cs +++ b/MediaBrowser.Controller/Library/ILibraryManager.cs @@ -6,6 +6,7 @@ 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; @@ -595,5 +596,11 @@ namespace MediaBrowser.Controller.Library BaseItem GetParentItem(string parentId, Guid? userId); BaseItem GetParentItem(Guid? parentId, Guid? userId); + + /// <summary> + /// Gets or creates a static instance of <see cref="NamingOptions"/>. + /// </summary> + /// <returns>An instance of the <see cref="NamingOptions"/> class.</returns> + NamingOptions GetNamingOptions(); } } diff --git a/MediaBrowser.Controller/MediaBrowser.Controller.csproj b/MediaBrowser.Controller/MediaBrowser.Controller.csproj index 3f3a505ea..ee76ff080 100644 --- a/MediaBrowser.Controller/MediaBrowser.Controller.csproj +++ b/MediaBrowser.Controller/MediaBrowser.Controller.csproj @@ -21,8 +21,9 @@ </ItemGroup> <ItemGroup> - <ProjectReference Include="..\MediaBrowser.Model\MediaBrowser.Model.csproj" /> - <ProjectReference Include="..\MediaBrowser.Common\MediaBrowser.Common.csproj" /> + <ProjectReference Include="../Emby.Naming/Emby.Naming.csproj" /> + <ProjectReference Include="../MediaBrowser.Model/MediaBrowser.Model.csproj" /> + <ProjectReference Include="../MediaBrowser.Common/MediaBrowser.Common.csproj" /> </ItemGroup> <ItemGroup> diff --git a/MediaBrowser.Controller/Providers/DirectoryService.cs b/MediaBrowser.Controller/Providers/DirectoryService.cs index 291a26883..b31270270 100644 --- a/MediaBrowser.Controller/Providers/DirectoryService.cs +++ b/MediaBrowser.Controller/Providers/DirectoryService.cs @@ -25,15 +25,16 @@ namespace MediaBrowser.Controller.Providers public FileSystemMetadata[] GetFileSystemEntries(string path) { - return _cache.GetOrAdd(path, p => _fileSystem.GetFileSystemEntries(p).ToArray()); + return _cache.GetOrAdd(path, (p, fileSystem) => fileSystem.GetFileSystemEntries(p).ToArray(), _fileSystem); } public List<FileSystemMetadata> GetFiles(string path) { var list = new List<FileSystemMetadata>(); var items = GetFileSystemEntries(path); - foreach (var item in items) + for (var i = 0; i < items.Length; i++) { + var item = items[i]; if (!item.IsDirectory) { list.Add(item); @@ -48,10 +49,9 @@ namespace MediaBrowser.Controller.Providers if (!_fileCache.TryGetValue(path, out var result)) { var file = _fileSystem.GetFileInfo(path); - var res = file != null && file.Exists ? file : null; - if (res != null) + if (file.Exists) { - result = res; + result = file; _fileCache.TryAdd(path, result); } } @@ -62,14 +62,21 @@ namespace MediaBrowser.Controller.Providers public IReadOnlyList<string> GetFilePaths(string path) => GetFilePaths(path, false); - public IReadOnlyList<string> GetFilePaths(string path, bool clearCache) + public IReadOnlyList<string> GetFilePaths(string path, bool clearCache, bool sort = false) { if (clearCache) { _filePathCache.TryRemove(path, out _); } - return _filePathCache.GetOrAdd(path, p => _fileSystem.GetFilePaths(p).ToList()); + var filePaths = _filePathCache.GetOrAdd(path, (p, fileSystem) => fileSystem.GetFilePaths(p).ToList(), _fileSystem); + + if (sort) + { + filePaths.Sort(); + } + + return filePaths; } } } diff --git a/MediaBrowser.Controller/Providers/IDirectoryService.cs b/MediaBrowser.Controller/Providers/IDirectoryService.cs index 9cee06a4c..b1a36e102 100644 --- a/MediaBrowser.Controller/Providers/IDirectoryService.cs +++ b/MediaBrowser.Controller/Providers/IDirectoryService.cs @@ -15,6 +15,6 @@ namespace MediaBrowser.Controller.Providers IReadOnlyList<string> GetFilePaths(string path); - IReadOnlyList<string> GetFilePaths(string path, bool clearCache); + IReadOnlyList<string> GetFilePaths(string path, bool clearCache, bool sort = false); } } diff --git a/MediaBrowser.Controller/Providers/MetadataResult.cs b/MediaBrowser.Controller/Providers/MetadataResult.cs index 8b0967a6e..7ec1eefcd 100644 --- a/MediaBrowser.Controller/Providers/MetadataResult.cs +++ b/MediaBrowser.Controller/Providers/MetadataResult.cs @@ -12,16 +12,26 @@ namespace MediaBrowser.Controller.Providers { public class MetadataResult<T> { + // Images aren't always used so the allocation is a waste a lot of the time + private List<LocalImageInfo> _images; + private List<(string url, ImageType type)> _remoteImages; + public MetadataResult() { - Images = new List<LocalImageInfo>(); - RemoteImages = new List<(string url, ImageType type)>(); ResultLanguage = "en"; } - public List<LocalImageInfo> Images { get; set; } + public List<LocalImageInfo> Images + { + get => _images ??= new List<LocalImageInfo>(); + set => _images = value; + } - public List<(string url, ImageType type)> RemoteImages { get; set; } + public List<(string url, ImageType type)> RemoteImages + { + get => _remoteImages ??= new List<(string url, ImageType type)>(); + set => _remoteImages = value; + } public List<UserItemData> UserDataList { get; set; } diff --git a/MediaBrowser.LocalMetadata/Images/EpisodeLocalImageProvider.cs b/MediaBrowser.LocalMetadata/Images/EpisodeLocalImageProvider.cs index bc62ca4d5..fdba64c4a 100644 --- a/MediaBrowser.LocalMetadata/Images/EpisodeLocalImageProvider.cs +++ b/MediaBrowser.LocalMetadata/Images/EpisodeLocalImageProvider.cs @@ -2,6 +2,7 @@ using System; using System.Collections.Generic; using System.IO; using System.Linq; +using MediaBrowser.Common.Extensions; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities.TV; using MediaBrowser.Controller.Providers; @@ -15,17 +16,6 @@ namespace MediaBrowser.LocalMetadata.Images /// </summary> public class EpisodeLocalImageProvider : ILocalImageProvider, IHasOrder { - private readonly IFileSystem _fileSystem; - - /// <summary> - /// Initializes a new instance of the <see cref="EpisodeLocalImageProvider"/> class. - /// </summary> - /// <param name="fileSystem">Instance of the <see cref="IFileSystem"/> interface.</param> - public EpisodeLocalImageProvider(IFileSystem fileSystem) - { - _fileSystem = fileSystem; - } - /// <inheritdoc /> public string Name => "Local Images"; @@ -49,14 +39,14 @@ namespace MediaBrowser.LocalMetadata.Images var parentPathFiles = directoryService.GetFiles(parentPath); - var nameWithoutExtension = Path.GetFileNameWithoutExtension(item.Path); + var nameWithoutExtension = Path.GetFileNameWithoutExtension(item.Path.AsSpan()); return GetFilesFromParentFolder(nameWithoutExtension, parentPathFiles); } - private List<LocalImageInfo> GetFilesFromParentFolder(string filenameWithoutExtension, List<FileSystemMetadata> parentPathFiles) + private List<LocalImageInfo> GetFilesFromParentFolder(ReadOnlySpan<char> filenameWithoutExtension, List<FileSystemMetadata> parentPathFiles) { - var thumbName = filenameWithoutExtension + "-thumb"; + var thumbName = string.Concat(filenameWithoutExtension, "-thumb"); var list = new List<LocalImageInfo>(1); @@ -67,15 +57,15 @@ namespace MediaBrowser.LocalMetadata.Images continue; } - if (BaseItem.SupportedImageExtensions.Contains(i.Extension, StringComparer.OrdinalIgnoreCase)) + if (BaseItem.SupportedImageExtensions.Contains(i.Extension.AsSpan(), StringComparison.OrdinalIgnoreCase)) { - var currentNameWithoutExtension = _fileSystem.GetFileNameWithoutExtension(i); + var currentNameWithoutExtension = Path.GetFileNameWithoutExtension(i.FullName.AsSpan()); - if (string.Equals(filenameWithoutExtension, currentNameWithoutExtension, StringComparison.OrdinalIgnoreCase)) + if (filenameWithoutExtension.Equals(currentNameWithoutExtension, StringComparison.OrdinalIgnoreCase)) { list.Add(new LocalImageInfo { FileInfo = i, Type = ImageType.Primary }); } - else if (string.Equals(thumbName, currentNameWithoutExtension, StringComparison.OrdinalIgnoreCase)) + else if (currentNameWithoutExtension.Equals(thumbName, StringComparison.OrdinalIgnoreCase)) { list.Add(new LocalImageInfo { FileInfo = i, Type = ImageType.Primary }); } diff --git a/MediaBrowser.Providers/Manager/ItemImageProvider.cs b/MediaBrowser.Providers/Manager/ItemImageProvider.cs index 4471a25b2..966a3d822 100644 --- a/MediaBrowser.Providers/Manager/ItemImageProvider.cs +++ b/MediaBrowser.Providers/Manager/ItemImageProvider.cs @@ -32,7 +32,7 @@ namespace MediaBrowser.Providers.Manager /// <summary> /// Image types that are only one per item. /// </summary> - private readonly ImageType[] _singularImages = + private static readonly ImageType[] _singularImages = { ImageType.Primary, ImageType.Art, @@ -208,9 +208,14 @@ namespace MediaBrowser.Providers.Manager /// <returns><c>true</c> if the specified item contains images; otherwise, <c>false</c>.</returns> private bool ContainsImages(BaseItem item, List<ImageType> images, TypeOptions savedOptions, int backdropLimit, int screenshotLimit) { - if (_singularImages.Any(i => images.Contains(i) && !HasImage(item, i) && savedOptions.GetLimit(i) > 0)) + // Using .Any causes the creation of a DisplayClass aka. variable capture + for (var i = 0; i < _singularImages.Length; i++) { - return false; + var type = _singularImages[i]; + if (images.Contains(type) && !HasImage(item, type) && savedOptions.GetLimit(type) > 0) + { + return false; + } } if (images.Contains(ImageType.Backdrop) && item.GetImages(ImageType.Backdrop).Count() < backdropLimit) @@ -329,7 +334,7 @@ namespace MediaBrowser.Providers.Manager var deleted = false; var deletedImages = new List<ItemImageInfo>(); - foreach (var image in item.GetImages(type).ToList()) + foreach (var image in item.GetImages(type)) { if (!image.IsLocalFile) { @@ -359,9 +364,10 @@ namespace MediaBrowser.Providers.Manager { var changed = false; - foreach (var type in _singularImages) + for (var i = 0; i < _singularImages.Length; i++) { - var image = images.FirstOrDefault(i => i.Type == type); + var type = _singularImages[i]; + var image = GetFirstLocalImageInfoByType(images, type); if (image != null) { @@ -423,15 +429,29 @@ namespace MediaBrowser.Providers.Manager return changed; } + private static LocalImageInfo GetFirstLocalImageInfoByType(IReadOnlyList<LocalImageInfo> images, ImageType type) + { + var len = images.Count; + for (var i = 0; i < len; i++) + { + var image = images[i]; + if (image.Type == type) + { + return image; + } + } + + return null; + } + private bool UpdateMultiImages(BaseItem item, List<LocalImageInfo> images, ImageType type) { var changed = false; - var newImages = images.Where(i => i.Type == type).ToList(); - - var newImageFileInfos = newImages - .Select(i => i.FileInfo) - .ToList(); + var newImageFileInfos = images + .FindAll(i => i.Type == type) + .Select(i => i.FileInfo) + .ToList(); if (item.AddImages(type, newImageFileInfos)) { diff --git a/MediaBrowser.Providers/Manager/MetadataService.cs b/MediaBrowser.Providers/Manager/MetadataService.cs index 401c7e99f..826b7941a 100644 --- a/MediaBrowser.Providers/Manager/MetadataService.cs +++ b/MediaBrowser.Providers/Manager/MetadataService.cs @@ -28,8 +28,11 @@ namespace MediaBrowser.Providers.Manager ProviderManager = providerManager; FileSystem = fileSystem; LibraryManager = libraryManager; + ImageProvider = new ItemImageProvider(Logger, ProviderManager, FileSystem); } + protected ItemImageProvider ImageProvider { get; } + protected IServerConfigurationManager ServerConfigurationManager { get; } protected ILogger<MetadataService<TItemType, TIdType>> Logger { get; } @@ -88,7 +91,6 @@ namespace MediaBrowser.Providers.Manager } } - var itemImageProvider = new ItemImageProvider(Logger, ProviderManager, FileSystem); var localImagesFailed = false; var allImageProviders = ((ProviderManager)ProviderManager).GetImageProviders(item, refreshOptions).ToList(); @@ -97,7 +99,7 @@ namespace MediaBrowser.Providers.Manager try { // Always validate images and check for new locally stored ones. - if (itemImageProvider.ValidateImages(item, allImageProviders.OfType<ILocalImageProvider>(), refreshOptions.DirectoryService)) + if (ImageProvider.ValidateImages(item, allImageProviders.OfType<ILocalImageProvider>(), refreshOptions.DirectoryService)) { updateType |= ItemUpdateType.ImageUpdate; } @@ -143,7 +145,7 @@ namespace MediaBrowser.Providers.Manager // await FindIdentities(id, cancellationToken).ConfigureAwait(false); id.IsAutomated = refreshOptions.IsAutomated; - var result = await RefreshWithProviders(metadataResult, id, refreshOptions, providers, itemImageProvider, cancellationToken).ConfigureAwait(false); + var result = await RefreshWithProviders(metadataResult, id, refreshOptions, providers, ImageProvider, cancellationToken).ConfigureAwait(false); updateType |= result.UpdateType; if (result.Failures > 0) @@ -160,7 +162,7 @@ namespace MediaBrowser.Providers.Manager if (providers.Count > 0) { - var result = await itemImageProvider.RefreshImages(itemOfType, libraryOptions, providers, refreshOptions, cancellationToken).ConfigureAwait(false); + var result = await ImageProvider.RefreshImages(itemOfType, libraryOptions, providers, refreshOptions, cancellationToken).ConfigureAwait(false); updateType |= result.UpdateType; if (result.Failures > 0) @@ -563,7 +565,7 @@ namespace MediaBrowser.Providers.Manager protected virtual IEnumerable<IImageProvider> GetNonLocalImageProviders(BaseItem item, IEnumerable<IImageProvider> allImageProviders, ImageRefreshOptions options) { // Get providers to refresh - var providers = allImageProviders.Where(i => !(i is ILocalImageProvider)).ToList(); + var providers = allImageProviders.Where(i => !(i is ILocalImageProvider)); var dateLastImageRefresh = item.DateLastRefreshed; @@ -575,15 +577,13 @@ namespace MediaBrowser.Providers.Manager providers = providers .Where(i => { - var hasFileChangeMonitor = i as IHasItemChangeMonitor; - if (hasFileChangeMonitor != null) + if (i is IHasItemChangeMonitor hasFileChangeMonitor) { return HasChanged(item, hasFileChangeMonitor, options.DirectoryService); } return false; - }) - .ToList(); + }); } return providers; diff --git a/MediaBrowser.Providers/MediaInfo/SubtitleResolver.cs b/MediaBrowser.Providers/MediaInfo/SubtitleResolver.cs index b086ef07b..3cd7ec772 100644 --- a/MediaBrowser.Providers/MediaInfo/SubtitleResolver.cs +++ b/MediaBrowser.Providers/MediaInfo/SubtitleResolver.cs @@ -3,7 +3,6 @@ using System; using System.Collections.Generic; using System.IO; -using System.Linq; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Providers; using MediaBrowser.Model.Entities; @@ -55,38 +54,35 @@ namespace MediaBrowser.Providers.MediaInfo return streams; } - public List<string> GetExternalSubtitleFiles( + public IEnumerable<string> GetExternalSubtitleFiles( Video video, IDirectoryService directoryService, bool clearCache) { - var list = new List<string>(); - if (!video.IsFileProtocol) { - return list; + yield break; } var streams = GetExternalSubtitleStreams(video, 0, directoryService, clearCache); foreach (var stream in streams) { - list.Add(stream.Path); + yield return stream.Path; } - - return list; } public void AddExternalSubtitleStreams( List<MediaStream> streams, string videoPath, int startIndex, - string[] files) + IReadOnlyList<string> files) { var videoFileNameWithoutExtension = NormalizeFilenameForSubtitleComparison(videoPath); - foreach (var fullName in files) + for (var i = 0; i < files.Count; i++) { + var fullName = files[i]; var extension = Path.GetExtension(fullName.AsSpan()); if (!IsSubtitleExtension(extension)) { @@ -135,15 +131,12 @@ namespace MediaBrowser.Providers.MediaInfo break; } - var language = languageSpan.ToString(); // Try to translate to three character code // Be flexible and check against both the full and three character versions + var language = languageSpan.ToString(); var culture = _localization.FindLanguageInfo(language); - if (culture != null) - { - language = culture.ThreeLetterISOLanguageName; - } + language = culture == null ? language : culture.ThreeLetterISOLanguageName; mediaStream = new MediaStream { @@ -194,7 +187,7 @@ namespace MediaBrowser.Providers.MediaInfo IDirectoryService directoryService, bool clearCache) { - var files = directoryService.GetFilePaths(folder, clearCache).OrderBy(i => i).ToArray(); + var files = directoryService.GetFilePaths(folder, clearCache, true); AddExternalSubtitleStreams(streams, videoPath, startIndex, files); } diff --git a/RSSDP/SsdpCommunicationsServer.cs b/RSSDP/SsdpCommunicationsServer.cs index f448ad38b..e49c0e77b 100644 --- a/RSSDP/SsdpCommunicationsServer.cs +++ b/RSSDP/SsdpCommunicationsServer.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Diagnostics; using System.Linq; using System.Net; using System.Net.Http; @@ -42,7 +43,7 @@ namespace Rssdp.Infrastructure private HttpResponseParser _ResponseParser; private readonly ILogger _logger; private ISocketFactory _SocketFactory; - private readonly INetworkManager _networkManager; + private readonly INetworkManager _networkManager; private int _LocalPort; private int _MulticastTtl; @@ -68,7 +69,7 @@ namespace Rssdp.Infrastructure INetworkManager networkManager, ILogger logger, bool enableMultiSocketBinding) : this(socketFactory, 0, SsdpConstants.SsdpDefaultMulticastTimeToLive, networkManager, logger, enableMultiSocketBinding) { - + } /// <summary> @@ -358,7 +359,7 @@ namespace Rssdp.Infrastructure { // Not support IPv6 right now continue; - } + } try { diff --git a/tests/Jellyfin.Naming.Tests/Video/CleanDateTimeTests.cs b/tests/Jellyfin.Naming.Tests/Video/CleanDateTimeTests.cs index 950899d7e..b1141df47 100644 --- a/tests/Jellyfin.Naming.Tests/Video/CleanDateTimeTests.cs +++ b/tests/Jellyfin.Naming.Tests/Video/CleanDateTimeTests.cs @@ -58,7 +58,7 @@ namespace Jellyfin.Naming.Tests.Video { input = Path.GetFileName(input); - var result = new VideoResolver(_namingOptions).CleanDateTime(input); + var result = VideoResolver.CleanDateTime(input, _namingOptions); Assert.Equal(expectedName, result.Name, true); Assert.Equal(expectedYear, result.Year); diff --git a/tests/Jellyfin.Naming.Tests/Video/CleanStringTests.cs b/tests/Jellyfin.Naming.Tests/Video/CleanStringTests.cs index a720bdade..fb050cf5a 100644 --- a/tests/Jellyfin.Naming.Tests/Video/CleanStringTests.cs +++ b/tests/Jellyfin.Naming.Tests/Video/CleanStringTests.cs @@ -7,7 +7,7 @@ namespace Jellyfin.Naming.Tests.Video { public sealed class CleanStringTests { - private readonly VideoResolver _videoResolver = new VideoResolver(new NamingOptions()); + private readonly NamingOptions _namingOptions = new NamingOptions(); [Theory] [InlineData("Super movie 480p.mp4", "Super movie")] @@ -26,7 +26,7 @@ namespace Jellyfin.Naming.Tests.Video // FIXME: [InlineData("After The Sunset - [0004].mkv", "After The Sunset")] public void CleanStringTest_NeedsCleaning_Success(string input, string expectedName) { - Assert.True(_videoResolver.TryCleanString(input, out ReadOnlySpan<char> newName)); + Assert.True(VideoResolver.TryCleanString(input, _namingOptions, out ReadOnlySpan<char> newName)); // TODO: compare spans when XUnit supports it Assert.Equal(expectedName, newName.ToString()); } @@ -41,7 +41,7 @@ namespace Jellyfin.Naming.Tests.Video [InlineData("Run lola run (lola rennt) (2009).mp4")] public void CleanStringTest_DoesntNeedCleaning_False(string? input) { - Assert.False(_videoResolver.TryCleanString(input, out ReadOnlySpan<char> newName)); + Assert.False(VideoResolver.TryCleanString(input, _namingOptions, out ReadOnlySpan<char> newName)); Assert.True(newName.IsEmpty); } } diff --git a/tests/Jellyfin.Naming.Tests/Video/ExtraTests.cs b/tests/Jellyfin.Naming.Tests/Video/ExtraTests.cs index 2f173b0ce..f872f94f8 100644 --- a/tests/Jellyfin.Naming.Tests/Video/ExtraTests.cs +++ b/tests/Jellyfin.Naming.Tests/Video/ExtraTests.cs @@ -104,13 +104,6 @@ namespace Jellyfin.Naming.Tests.Video Assert.Equal(rule, res.Rule); } - [Fact] - public void TestFlagsParser() - { - var flags = new FlagParser(_videoOptions).GetFlags(string.Empty); - Assert.Empty(flags); - } - private ExtraResolver GetExtraTypeParser(NamingOptions videoOptions) { return new ExtraResolver(videoOptions); diff --git a/tests/Jellyfin.Naming.Tests/Video/Format3DTests.cs b/tests/Jellyfin.Naming.Tests/Video/Format3DTests.cs index 69de96a47..1762b91b9 100644 --- a/tests/Jellyfin.Naming.Tests/Video/Format3DTests.cs +++ b/tests/Jellyfin.Naming.Tests/Video/Format3DTests.cs @@ -22,8 +22,7 @@ namespace Jellyfin.Naming.Tests.Video [Fact] public void Test3DName() { - var result = - new VideoResolver(_namingOptions).ResolveFile(@"C:/Users/media/Desktop/Video Test/Movies/Oblivion/Oblivion.3d.hsbs.mkv"); + var result = VideoResolver.ResolveFile(@"C:/Users/media/Desktop/Video Test/Movies/Oblivion/Oblivion.3d.hsbs.mkv", _namingOptions); Assert.Equal("hsbs", result?.Format3D); Assert.Equal("Oblivion", result?.Name); @@ -58,15 +57,13 @@ namespace Jellyfin.Naming.Tests.Video private void Test(string input, bool is3D, string? format3D) { - var parser = new Format3DParser(_namingOptions); - - var result = parser.Parse(input); + var result = Format3DParser.Parse(input, _namingOptions); Assert.Equal(is3D, result.Is3D); if (format3D == null) { - Assert.Null(result.Format3D); + Assert.Null(result?.Format3D); } else { diff --git a/tests/Jellyfin.Naming.Tests/Video/MultiVersionTests.cs b/tests/Jellyfin.Naming.Tests/Video/MultiVersionTests.cs index 6e803593e..d02f8ae92 100644 --- a/tests/Jellyfin.Naming.Tests/Video/MultiVersionTests.cs +++ b/tests/Jellyfin.Naming.Tests/Video/MultiVersionTests.cs @@ -9,7 +9,7 @@ namespace Jellyfin.Naming.Tests.Video { public class MultiVersionTests { - private readonly VideoListResolver _videoListResolver = new VideoListResolver(new NamingOptions()); + private readonly NamingOptions _namingOptions = new NamingOptions(); [Fact] public void TestMultiEdition1() @@ -22,11 +22,13 @@ namespace Jellyfin.Naming.Tests.Video @"/movies/X-Men Days of Future Past/X-Men Days of Future Past [hsbs].mkv" }; - var result = _videoListResolver.Resolve(files.Select(i => new FileSystemMetadata - { - IsDirectory = false, - FullName = i - }).ToList()).ToList(); + var result = VideoListResolver.Resolve( + files.Select(i => new FileSystemMetadata + { + IsDirectory = false, + FullName = i + }).ToList(), + _namingOptions).ToList(); Assert.Single(result); Assert.Single(result[0].Extras); @@ -43,11 +45,13 @@ namespace Jellyfin.Naming.Tests.Video @"/movies/X-Men Days of Future Past/X-Men Days of Future Past [banana].mp4" }; - var result = _videoListResolver.Resolve(files.Select(i => new FileSystemMetadata - { - IsDirectory = false, - FullName = i - }).ToList()).ToList(); + var result = VideoListResolver.Resolve( + files.Select(i => new FileSystemMetadata + { + IsDirectory = false, + FullName = i + }).ToList(), + _namingOptions).ToList(); Assert.Single(result); Assert.Single(result[0].Extras); @@ -63,11 +67,13 @@ namespace Jellyfin.Naming.Tests.Video @"/movies/The Phantom of the Opera (1925)/The Phantom of the Opera (1925) - 1929 version.mkv" }; - var result = _videoListResolver.Resolve(files.Select(i => new FileSystemMetadata - { - IsDirectory = false, - FullName = i - }).ToList()).ToList(); + var result = VideoListResolver.Resolve( + files.Select(i => new FileSystemMetadata + { + IsDirectory = false, + FullName = i + }).ToList(), + _namingOptions).ToList(); Assert.Single(result); Assert.Single(result[0].AlternateVersions); @@ -87,11 +93,13 @@ namespace Jellyfin.Naming.Tests.Video @"/movies/M/Movie 7.mkv" }; - var result = _videoListResolver.Resolve(files.Select(i => new FileSystemMetadata - { - IsDirectory = false, - FullName = i - }).ToList()).ToList(); + var result = VideoListResolver.Resolve( + files.Select(i => new FileSystemMetadata + { + IsDirectory = false, + FullName = i + }).ToList(), + _namingOptions).ToList(); Assert.Equal(7, result.Count); Assert.Empty(result[0].Extras); @@ -113,11 +121,13 @@ namespace Jellyfin.Naming.Tests.Video @"/movies/Movie/Movie-8.mkv" }; - var result = _videoListResolver.Resolve(files.Select(i => new FileSystemMetadata - { - IsDirectory = false, - FullName = i - }).ToList()).ToList(); + var result = VideoListResolver.Resolve( + files.Select(i => new FileSystemMetadata + { + IsDirectory = false, + FullName = i + }).ToList(), + _namingOptions).ToList(); Assert.Single(result); Assert.Empty(result[0].Extras); @@ -140,11 +150,13 @@ namespace Jellyfin.Naming.Tests.Video @"/movies/Mo/Movie 9.mkv" }; - var result = _videoListResolver.Resolve(files.Select(i => new FileSystemMetadata - { - IsDirectory = false, - FullName = i - }).ToList()).ToList(); + var result = VideoListResolver.Resolve( + files.Select(i => new FileSystemMetadata + { + IsDirectory = false, + FullName = i + }).ToList(), + _namingOptions).ToList(); Assert.Equal(9, result.Count); Assert.Empty(result[0].Extras); @@ -163,11 +175,13 @@ namespace Jellyfin.Naming.Tests.Video @"/movies/Movie/Movie 5.mkv" }; - var result = _videoListResolver.Resolve(files.Select(i => new FileSystemMetadata - { - IsDirectory = false, - FullName = i - }).ToList()).ToList(); + var result = VideoListResolver.Resolve( + files.Select(i => new FileSystemMetadata + { + IsDirectory = false, + FullName = i + }).ToList(), + _namingOptions).ToList(); Assert.Equal(5, result.Count); Assert.Empty(result[0].Extras); @@ -188,11 +202,13 @@ namespace Jellyfin.Naming.Tests.Video @"/movies/Iron Man/Iron Man (2011).mkv" }; - var result = _videoListResolver.Resolve(files.Select(i => new FileSystemMetadata - { - IsDirectory = false, - FullName = i - }).ToList()).ToList(); + var result = VideoListResolver.Resolve( + files.Select(i => new FileSystemMetadata + { + IsDirectory = false, + FullName = i + }).ToList(), + _namingOptions).ToList(); Assert.Equal(5, result.Count); Assert.Empty(result[0].Extras); @@ -214,11 +230,13 @@ namespace Jellyfin.Naming.Tests.Video @"/movies/Iron Man/Iron Man[test].mkv", }; - var result = _videoListResolver.Resolve(files.Select(i => new FileSystemMetadata - { - IsDirectory = false, - FullName = i - }).ToList()).ToList(); + var result = VideoListResolver.Resolve( + files.Select(i => new FileSystemMetadata + { + IsDirectory = false, + FullName = i + }).ToList(), + _namingOptions).ToList(); Assert.Single(result); Assert.Empty(result[0].Extras); @@ -243,11 +261,13 @@ namespace Jellyfin.Naming.Tests.Video @"/movies/Iron Man/Iron Man [test].mkv" }; - var result = _videoListResolver.Resolve(files.Select(i => new FileSystemMetadata - { - IsDirectory = false, - FullName = i - }).ToList()).ToList(); + var result = VideoListResolver.Resolve( + files.Select(i => new FileSystemMetadata + { + IsDirectory = false, + FullName = i + }).ToList(), + _namingOptions).ToList(); Assert.Single(result); Assert.Empty(result[0].Extras); @@ -266,11 +286,13 @@ namespace Jellyfin.Naming.Tests.Video @"/movies/Iron Man/Iron Man - C (2007).mkv" }; - var result = _videoListResolver.Resolve(files.Select(i => new FileSystemMetadata - { - IsDirectory = false, - FullName = i - }).ToList()).ToList(); + var result = VideoListResolver.Resolve( + files.Select(i => new FileSystemMetadata + { + IsDirectory = false, + FullName = i + }).ToList(), + _namingOptions).ToList(); Assert.Equal(2, result.Count); } @@ -289,11 +311,13 @@ namespace Jellyfin.Naming.Tests.Video @"/movies/Iron Man/Iron Man_3d.hsbs.mkv" }; - var result = _videoListResolver.Resolve(files.Select(i => new FileSystemMetadata - { - IsDirectory = false, - FullName = i - }).ToList()).ToList(); + var result = VideoListResolver.Resolve( + files.Select(i => new FileSystemMetadata + { + IsDirectory = false, + FullName = i + }).ToList(), + _namingOptions).ToList(); Assert.Equal(7, result.Count); Assert.Empty(result[0].Extras); @@ -314,11 +338,13 @@ namespace Jellyfin.Naming.Tests.Video @"/movies/Iron Man/Iron Man (2011).mkv" }; - var result = _videoListResolver.Resolve(files.Select(i => new FileSystemMetadata - { - IsDirectory = false, - FullName = i - }).ToList()).ToList(); + var result = VideoListResolver.Resolve( + files.Select(i => new FileSystemMetadata + { + IsDirectory = false, + FullName = i + }).ToList(), + _namingOptions).ToList(); Assert.Equal(5, result.Count); Assert.Empty(result[0].Extras); @@ -334,11 +360,13 @@ namespace Jellyfin.Naming.Tests.Video @"/movies/Blade Runner (1982)/Blade Runner (1982) [EE by ADM] [480p HEVC AAC,AAC,AAC].mkv" }; - var result = _videoListResolver.Resolve(files.Select(i => new FileSystemMetadata - { - IsDirectory = false, - FullName = i - }).ToList()).ToList(); + var result = VideoListResolver.Resolve( + files.Select(i => new FileSystemMetadata + { + IsDirectory = false, + FullName = i + }).ToList(), + _namingOptions).ToList(); Assert.Single(result); Assert.Empty(result[0].Extras); @@ -354,11 +382,13 @@ namespace Jellyfin.Naming.Tests.Video @"/movies/X-Men Apocalypse (2016)/X-Men Apocalypse (2016) [2160p] Blu-ray.x265.AAC.mkv" }; - var result = _videoListResolver.Resolve(files.Select(i => new FileSystemMetadata - { - IsDirectory = false, - FullName = i - }).ToList()).ToList(); + var result = VideoListResolver.Resolve( + files.Select(i => new FileSystemMetadata + { + IsDirectory = false, + FullName = i + }).ToList(), + _namingOptions).ToList(); Assert.Single(result); Assert.Empty(result[0].Extras); @@ -374,11 +404,13 @@ namespace Jellyfin.Naming.Tests.Video @"/movies/John Wick - Kapitel 3 (2019) [imdbid=tt6146586]/John Wick - Kapitel 3 (2019) [imdbid=tt6146586] - Version 2.mkv" }; - var result = _videoListResolver.Resolve(files.Select(i => new FileSystemMetadata - { - IsDirectory = false, - FullName = i - }).ToList()).ToList(); + var result = VideoListResolver.Resolve( + files.Select(i => new FileSystemMetadata + { + IsDirectory = false, + FullName = i + }).ToList(), + _namingOptions).ToList(); Assert.Single(result); Assert.Empty(result[0].Extras); @@ -394,11 +426,13 @@ namespace Jellyfin.Naming.Tests.Video @"/movies/John Wick - Chapter 3 (2019)/John Wick - Chapter 3 (2019) [Version 2.mkv" }; - var result = _videoListResolver.Resolve(files.Select(i => new FileSystemMetadata - { - IsDirectory = false, - FullName = i - }).ToList()).ToList(); + var result = VideoListResolver.Resolve( + files.Select(i => new FileSystemMetadata + { + IsDirectory = false, + FullName = i + }).ToList(), + _namingOptions).ToList(); Assert.Equal(2, result.Count); } @@ -406,7 +440,7 @@ namespace Jellyfin.Naming.Tests.Video [Fact] public void TestEmptyList() { - var result = _videoListResolver.Resolve(new List<FileSystemMetadata>()).ToList(); + var result = VideoListResolver.Resolve(new List<FileSystemMetadata>(), _namingOptions).ToList(); Assert.Empty(result); } diff --git a/tests/Jellyfin.Naming.Tests/Video/StubTests.cs b/tests/Jellyfin.Naming.Tests/Video/StubTests.cs index 6e759c6d6..1d50df7a6 100644 --- a/tests/Jellyfin.Naming.Tests/Video/StubTests.cs +++ b/tests/Jellyfin.Naming.Tests/Video/StubTests.cs @@ -29,8 +29,7 @@ namespace Jellyfin.Naming.Tests.Video [Fact] public void TestStubName() { - var result = - new VideoResolver(_namingOptions).ResolveFile(@"C:/Users/media/Desktop/Video Test/Movies/Oblivion/Oblivion.dvd.disc"); + var result = VideoResolver.ResolveFile(@"C:/Users/media/Desktop/Video Test/Movies/Oblivion/Oblivion.dvd.disc", _namingOptions); Assert.Equal("Oblivion", result?.Name); } diff --git a/tests/Jellyfin.Naming.Tests/Video/VideoListResolverTests.cs b/tests/Jellyfin.Naming.Tests/Video/VideoListResolverTests.cs index 08af76669..9e0776c3c 100644 --- a/tests/Jellyfin.Naming.Tests/Video/VideoListResolverTests.cs +++ b/tests/Jellyfin.Naming.Tests/Video/VideoListResolverTests.cs @@ -9,7 +9,7 @@ namespace Jellyfin.Naming.Tests.Video { public class VideoListResolverTests { - private readonly VideoListResolver _videoListResolver = new VideoListResolver(new NamingOptions()); + private readonly NamingOptions _namingOptions = new NamingOptions(); [Fact] public void TestStackAndExtras() @@ -40,11 +40,13 @@ namespace Jellyfin.Naming.Tests.Video "WillyWonka-trailer.mkv" }; - var result = _videoListResolver.Resolve(files.Select(i => new FileSystemMetadata - { - IsDirectory = false, - FullName = i - }).ToList()).ToList(); + var result = VideoListResolver.Resolve( + files.Select(i => new FileSystemMetadata + { + IsDirectory = false, + FullName = i + }).ToList(), + _namingOptions).ToList(); Assert.Equal(5, result.Count); var batman = result.FirstOrDefault(x => string.Equals(x.Name, "Batman", StringComparison.Ordinal)); @@ -67,11 +69,13 @@ namespace Jellyfin.Naming.Tests.Video "300.nfo" }; - var result = _videoListResolver.Resolve(files.Select(i => new FileSystemMetadata - { - IsDirectory = false, - FullName = i - }).ToList()).ToList(); + var result = VideoListResolver.Resolve( + files.Select(i => new FileSystemMetadata + { + IsDirectory = false, + FullName = i + }).ToList(), + _namingOptions).ToList(); Assert.Single(result); } @@ -85,11 +89,13 @@ namespace Jellyfin.Naming.Tests.Video "300 trailer.mkv" }; - var result = _videoListResolver.Resolve(files.Select(i => new FileSystemMetadata - { - IsDirectory = false, - FullName = i - }).ToList()).ToList(); + var result = VideoListResolver.Resolve( + files.Select(i => new FileSystemMetadata + { + IsDirectory = false, + FullName = i + }).ToList(), + _namingOptions).ToList(); Assert.Single(result); } @@ -103,11 +109,13 @@ namespace Jellyfin.Naming.Tests.Video "X-Men Days of Future Past-trailer.mp4" }; - var result = _videoListResolver.Resolve(files.Select(i => new FileSystemMetadata - { - IsDirectory = false, - FullName = i - }).ToList()).ToList(); + var result = VideoListResolver.Resolve( + files.Select(i => new FileSystemMetadata + { + IsDirectory = false, + FullName = i + }).ToList(), + _namingOptions).ToList(); Assert.Single(result); } @@ -122,11 +130,13 @@ namespace Jellyfin.Naming.Tests.Video "X-Men Days of Future Past-trailer2.mp4" }; - var result = _videoListResolver.Resolve(files.Select(i => new FileSystemMetadata - { - IsDirectory = false, - FullName = i - }).ToList()).ToList(); + var result = VideoListResolver.Resolve( + files.Select(i => new FileSystemMetadata + { + IsDirectory = false, + FullName = i + }).ToList(), + _namingOptions).ToList(); Assert.Single(result); } @@ -140,11 +150,13 @@ namespace Jellyfin.Naming.Tests.Video "Looper.2012.bluray.720p.x264.mkv" }; - var result = _videoListResolver.Resolve(files.Select(i => new FileSystemMetadata - { - IsDirectory = false, - FullName = i - }).ToList()).ToList(); + var result = VideoListResolver.Resolve( + files.Select(i => new FileSystemMetadata + { + IsDirectory = false, + FullName = i + }).ToList(), + _namingOptions).ToList(); Assert.Single(result); } @@ -162,11 +174,13 @@ namespace Jellyfin.Naming.Tests.Video "My video 5.mkv" }; - var result = _videoListResolver.Resolve(files.Select(i => new FileSystemMetadata - { - IsDirectory = false, - FullName = i - }).ToList()).ToList(); + var result = VideoListResolver.Resolve( + files.Select(i => new FileSystemMetadata + { + IsDirectory = false, + FullName = i + }).ToList(), + _namingOptions).ToList(); Assert.Equal(5, result.Count); } @@ -180,11 +194,13 @@ namespace Jellyfin.Naming.Tests.Video @"M:/Movies (DVD)/Movies (Musical)/Sound of Music (1965)/Sound of Music Disc 2" }; - var result = _videoListResolver.Resolve(files.Select(i => new FileSystemMetadata - { - IsDirectory = true, - FullName = i - }).ToList()).ToList(); + var result = VideoListResolver.Resolve( + files.Select(i => new FileSystemMetadata + { + IsDirectory = true, + FullName = i + }).ToList(), + _namingOptions).ToList(); Assert.Single(result); } @@ -199,11 +215,13 @@ namespace Jellyfin.Naming.Tests.Video @"My movie #2.mp4" }; - var result = _videoListResolver.Resolve(files.Select(i => new FileSystemMetadata - { - IsDirectory = true, - FullName = i - }).ToList()).ToList(); + var result = VideoListResolver.Resolve( + files.Select(i => new FileSystemMetadata + { + IsDirectory = true, + FullName = i + }).ToList(), + _namingOptions).ToList(); Assert.Equal(2, result.Count); } @@ -218,11 +236,13 @@ namespace Jellyfin.Naming.Tests.Video @"No (2012) part1-trailer.mp4" }; - var result = _videoListResolver.Resolve(files.Select(i => new FileSystemMetadata - { - IsDirectory = false, - FullName = i - }).ToList()).ToList(); + var result = VideoListResolver.Resolve( + files.Select(i => new FileSystemMetadata + { + IsDirectory = false, + FullName = i + }).ToList(), + _namingOptions).ToList(); Assert.Single(result); } @@ -237,11 +257,13 @@ namespace Jellyfin.Naming.Tests.Video @"No (2012)-trailer.mp4" }; - var result = _videoListResolver.Resolve(files.Select(i => new FileSystemMetadata - { - IsDirectory = false, - FullName = i - }).ToList()).ToList(); + var result = VideoListResolver.Resolve( + files.Select(i => new FileSystemMetadata + { + IsDirectory = false, + FullName = i + }).ToList(), + _namingOptions).ToList(); Assert.Single(result); } @@ -257,11 +279,13 @@ namespace Jellyfin.Naming.Tests.Video @"trailer.mp4" }; - var result = _videoListResolver.Resolve(files.Select(i => new FileSystemMetadata - { - IsDirectory = false, - FullName = i - }).ToList()).ToList(); + var result = VideoListResolver.Resolve( + files.Select(i => new FileSystemMetadata + { + IsDirectory = false, + FullName = i + }).ToList(), + _namingOptions).ToList(); Assert.Single(result); } @@ -277,11 +301,13 @@ namespace Jellyfin.Naming.Tests.Video @"/MCFAMILY-PC/Private3$/Heterosexual/Breast In Class 2 Counterfeit Racks (2011)/Breast In Class 2 Disc 2 cd2.avi" }; - var result = _videoListResolver.Resolve(files.Select(i => new FileSystemMetadata - { - IsDirectory = false, - FullName = i - }).ToList()).ToList(); + var result = VideoListResolver.Resolve( + files.Select(i => new FileSystemMetadata + { + IsDirectory = false, + FullName = i + }).ToList(), + _namingOptions).ToList(); Assert.Equal(2, result.Count); } @@ -294,11 +320,13 @@ namespace Jellyfin.Naming.Tests.Video @"/nas-markrobbo78/Videos/INDEX HTPC/Movies/Watched/3 - ACTION/Argo (2012)/movie.mkv" }; - var result = _videoListResolver.Resolve(files.Select(i => new FileSystemMetadata - { - IsDirectory = false, - FullName = i - }).ToList()).ToList(); + var result = VideoListResolver.Resolve( + files.Select(i => new FileSystemMetadata + { + IsDirectory = false, + FullName = i + }).ToList(), + _namingOptions).ToList(); Assert.Single(result); } @@ -311,11 +339,13 @@ namespace Jellyfin.Naming.Tests.Video @"The Colony.mkv" }; - var result = _videoListResolver.Resolve(files.Select(i => new FileSystemMetadata - { - IsDirectory = false, - FullName = i - }).ToList()).ToList(); + var result = VideoListResolver.Resolve( + files.Select(i => new FileSystemMetadata + { + IsDirectory = false, + FullName = i + }).ToList(), + _namingOptions).ToList(); Assert.Single(result); } @@ -329,11 +359,13 @@ namespace Jellyfin.Naming.Tests.Video @"Four Sisters and a Wedding - B.avi" }; - var result = _videoListResolver.Resolve(files.Select(i => new FileSystemMetadata - { - IsDirectory = false, - FullName = i - }).ToList()).ToList(); + var result = VideoListResolver.Resolve( + files.Select(i => new FileSystemMetadata + { + IsDirectory = false, + FullName = i + }).ToList(), + _namingOptions).ToList(); Assert.Single(result); } @@ -347,11 +379,13 @@ namespace Jellyfin.Naming.Tests.Video @"Four Rooms - A.mp4" }; - var result = _videoListResolver.Resolve(files.Select(i => new FileSystemMetadata - { - IsDirectory = false, - FullName = i - }).ToList()).ToList(); + var result = VideoListResolver.Resolve( + files.Select(i => new FileSystemMetadata + { + IsDirectory = false, + FullName = i + }).ToList(), + _namingOptions).ToList(); Assert.Equal(2, result.Count); } @@ -365,11 +399,13 @@ namespace Jellyfin.Naming.Tests.Video @"/Server/Despicable Me/movie-trailer.mkv" }; - var result = _videoListResolver.Resolve(files.Select(i => new FileSystemMetadata - { - IsDirectory = false, - FullName = i - }).ToList()).ToList(); + var result = VideoListResolver.Resolve( + files.Select(i => new FileSystemMetadata + { + IsDirectory = false, + FullName = i + }).ToList(), + _namingOptions).ToList(); Assert.Single(result); } @@ -385,11 +421,13 @@ namespace Jellyfin.Naming.Tests.Video @"/Server/Despicable Me/Baywatch (2017) - Trailer.mkv" }; - var result = _videoListResolver.Resolve(files.Select(i => new FileSystemMetadata - { - IsDirectory = false, - FullName = i - }).ToList()).ToList(); + var result = VideoListResolver.Resolve( + files.Select(i => new FileSystemMetadata + { + IsDirectory = false, + FullName = i + }).ToList(), + _namingOptions).ToList(); Assert.Equal(4, result.Count); } @@ -403,11 +441,13 @@ namespace Jellyfin.Naming.Tests.Video @"/Movies/Despicable Me/trailers/trailer.mkv" }; - var result = _videoListResolver.Resolve(files.Select(i => new FileSystemMetadata - { - IsDirectory = false, - FullName = i - }).ToList()).ToList(); + var result = VideoListResolver.Resolve( + files.Select(i => new FileSystemMetadata + { + IsDirectory = false, + FullName = i + }).ToList(), + _namingOptions).ToList(); Assert.Single(result); } diff --git a/tests/Jellyfin.Naming.Tests/Video/VideoResolverTests.cs b/tests/Jellyfin.Naming.Tests/Video/VideoResolverTests.cs index c56046f03..ac5a7a21e 100644 --- a/tests/Jellyfin.Naming.Tests/Video/VideoResolverTests.cs +++ b/tests/Jellyfin.Naming.Tests/Video/VideoResolverTests.cs @@ -9,7 +9,7 @@ namespace Jellyfin.Naming.Tests.Video { public class VideoResolverTests { - private readonly VideoResolver _videoResolver = new VideoResolver(new NamingOptions()); + private static NamingOptions _namingOptions = new NamingOptions(); public static IEnumerable<object[]> ResolveFile_ValidFileNameTestData() { @@ -159,27 +159,27 @@ namespace Jellyfin.Naming.Tests.Video [MemberData(nameof(ResolveFile_ValidFileNameTestData))] public void ResolveFile_ValidFileName_Success(VideoFileInfo expectedResult) { - var result = _videoResolver.ResolveFile(expectedResult.Path); + var result = VideoResolver.ResolveFile(expectedResult.Path, _namingOptions); Assert.NotNull(result); - Assert.Equal(result?.Path, expectedResult.Path); - Assert.Equal(result?.Container, expectedResult.Container); - Assert.Equal(result?.Name, expectedResult.Name); - Assert.Equal(result?.Year, expectedResult.Year); - Assert.Equal(result?.ExtraType, expectedResult.ExtraType); - Assert.Equal(result?.Format3D, expectedResult.Format3D); - Assert.Equal(result?.Is3D, expectedResult.Is3D); - Assert.Equal(result?.IsStub, expectedResult.IsStub); - Assert.Equal(result?.StubType, expectedResult.StubType); - Assert.Equal(result?.IsDirectory, expectedResult.IsDirectory); - Assert.Equal(result?.FileNameWithoutExtension, expectedResult.FileNameWithoutExtension); - Assert.Equal(result?.ToString(), expectedResult.ToString()); + Assert.Equal(result!.Path, expectedResult.Path); + Assert.Equal(result.Container, expectedResult.Container); + Assert.Equal(result.Name, expectedResult.Name); + Assert.Equal(result.Year, expectedResult.Year); + Assert.Equal(result.ExtraType, expectedResult.ExtraType); + Assert.Equal(result.Format3D, expectedResult.Format3D); + Assert.Equal(result.Is3D, expectedResult.Is3D); + Assert.Equal(result.IsStub, expectedResult.IsStub); + Assert.Equal(result.StubType, expectedResult.StubType); + Assert.Equal(result.IsDirectory, expectedResult.IsDirectory); + Assert.Equal(result.FileNameWithoutExtension.ToString(), expectedResult.FileNameWithoutExtension.ToString()); + Assert.Equal(result.ToString(), expectedResult.ToString()); } [Fact] public void ResolveFile_EmptyPath() { - var result = _videoResolver.ResolveFile(string.Empty); + var result = VideoResolver.ResolveFile(string.Empty, _namingOptions); Assert.Null(result); } @@ -194,7 +194,7 @@ namespace Jellyfin.Naming.Tests.Video string.Empty }; - var results = paths.Select(path => _videoResolver.ResolveDirectory(path)).ToList(); + var results = paths.Select(path => VideoResolver.ResolveDirectory(path, _namingOptions)).ToList(); Assert.Equal(3, results.Count); Assert.NotNull(results[0]); diff --git a/tests/Jellyfin.Server.Implementations.Tests/Data/SqliteItemRepositoryTests.cs b/tests/Jellyfin.Server.Implementations.Tests/Data/SqliteItemRepositoryTests.cs index 71f8c5181..f312933fb 100644 --- a/tests/Jellyfin.Server.Implementations.Tests/Data/SqliteItemRepositoryTests.cs +++ b/tests/Jellyfin.Server.Implementations.Tests/Data/SqliteItemRepositoryTests.cs @@ -166,6 +166,38 @@ namespace Jellyfin.Server.Implementations.Tests.Data }; } + public static IEnumerable<object[]> DeserializeImages_ValidAndInvalid_TestData() + { + yield return new object[] + { + string.Empty, + Array.Empty<ItemImageInfo>() + }; + + yield return new object[] + { + "/mnt/series/Family Guy/Season 1/Family Guy - S01E01-thumb.jpg*637452096478512963*Primary*1920*1080*WjQbtJtSO8nhNZ%L_Io#R/oaS6o}-;adXAoIn7j[%hW9s:WGw[nN|test|1234||ss", + new ItemImageInfo[] + { + new () + { + Path = "/mnt/series/Family Guy/Season 1/Family Guy - S01E01-thumb.jpg", + Type = ImageType.Primary, + DateModified = new DateTime(637452096478512963, DateTimeKind.Utc), + Width = 1920, + Height = 1080, + BlurHash = "WjQbtJtSO8nhNZ%L_Io#R*oaS6o}-;adXAoIn7j[%hW9s:WGw[nN" + } + } + }; + + yield return new object[] + { + "|", + Array.Empty<ItemImageInfo>() + }; + } + [Theory] [MemberData(nameof(DeserializeImages_Valid_TestData))] public void DeserializeImages_Valid_Success(string value, ItemImageInfo[] expected) @@ -184,6 +216,23 @@ namespace Jellyfin.Server.Implementations.Tests.Data } [Theory] + [MemberData(nameof(DeserializeImages_ValidAndInvalid_TestData))] + public void DeserializeImages_ValidAndInvalid_Success(string value, ItemImageInfo[] expected) + { + var result = _sqliteItemRepository.DeserializeImages(value); + Assert.Equal(expected.Length, result.Length); + for (int i = 0; i < expected.Length; i++) + { + Assert.Equal(expected[i].Path, result[i].Path); + Assert.Equal(expected[i].Type, result[i].Type); + Assert.Equal(expected[i].DateModified, result[i].DateModified); + Assert.Equal(expected[i].Width, result[i].Width); + Assert.Equal(expected[i].Height, result[i].Height); + Assert.Equal(expected[i].BlurHash, result[i].BlurHash); + } + } + + [Theory] [MemberData(nameof(DeserializeImages_Valid_TestData))] public void SerializeImages_Valid_Success(string expected, ItemImageInfo[] value) { |
