Improve code clarity and test documentation

Refactored ImageProcessor to use explicit 'this.' member access for consistency and readability. Enhanced test files with XML documentation, explicit types, modern C# syntax, and clearer Moq setups. Enabled XML doc generation for test project. Updated assembly info and cache files as a result of build changes. No functional or behavioral changes.
This commit is contained in:
2026-02-22 09:38:16 -05:00
parent 48569427a5
commit d5522c6fb3
12 changed files with 252 additions and 189 deletions
@@ -1,6 +1,7 @@
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
// <auto-generated> // <auto-generated>
// This code was generated by a tool. // This code was generated by a tool.
// Runtime Version:4.0.30319.42000
// //
// Changes to this file may cause incorrect behavior and will be lost if // Changes to this file may cause incorrect behavior and will be lost if
// the code is regenerated. // the code is regenerated.
@@ -13,7 +14,7 @@ using System.Reflection;
[assembly: System.Reflection.AssemblyCompanyAttribute("Jellyfin.CodeAnalysis")] [assembly: System.Reflection.AssemblyCompanyAttribute("Jellyfin.CodeAnalysis")]
[assembly: System.Reflection.AssemblyConfigurationAttribute("Debug")] [assembly: System.Reflection.AssemblyConfigurationAttribute("Debug")]
[assembly: System.Reflection.AssemblyFileVersionAttribute("1.0.0.0")] [assembly: System.Reflection.AssemblyFileVersionAttribute("1.0.0.0")]
[assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0.0+e8873563856ad813cb4b7695b0aaa85c32729a4c")] [assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0.0+48569427a5cba735184e017148b7c71f665279bc")]
[assembly: System.Reflection.AssemblyProductAttribute("Jellyfin.CodeAnalysis")] [assembly: System.Reflection.AssemblyProductAttribute("Jellyfin.CodeAnalysis")]
[assembly: System.Reflection.AssemblyTitleAttribute("Jellyfin.CodeAnalysis")] [assembly: System.Reflection.AssemblyTitleAttribute("Jellyfin.CodeAnalysis")]
[assembly: System.Reflection.AssemblyVersionAttribute("1.0.0.0")] [assembly: System.Reflection.AssemblyVersionAttribute("1.0.0.0")]
@@ -1 +1 @@
4aa28b28b5f36d3ba59b76bc7e3a9a39544dcee89f6b8e3632f4df17d7b042d4 5afb18d45c1a1f35abd23adbc2ff716c2f51ec4673ea08b8765ec2e818a59a28
+37 -37
View File
@@ -64,10 +64,10 @@ public sealed class ImageProcessor : IImageProcessor, IDisposable
IImageEncoder imageEncoder, IImageEncoder imageEncoder,
IServerConfigurationManager config) IServerConfigurationManager config)
{ {
_logger = logger; this._logger = logger;
_fileSystem = fileSystem; this._fileSystem = fileSystem;
_imageEncoder = imageEncoder; this._imageEncoder = imageEncoder;
_appPaths = appPaths; this._appPaths = appPaths;
var semaphoreCount = config.Configuration.ParallelImageEncodingLimit; var semaphoreCount = config.Configuration.ParallelImageEncodingLimit;
if (semaphoreCount < 1) if (semaphoreCount < 1)
@@ -75,10 +75,10 @@ public sealed class ImageProcessor : IImageProcessor, IDisposable
semaphoreCount = Environment.ProcessorCount; semaphoreCount = Environment.ProcessorCount;
} }
_parallelEncodingLimit = new(semaphoreCount); this._parallelEncodingLimit = new(semaphoreCount);
} }
private string ResizedImageCachePath => Path.Combine(_appPaths.ImageCachePath, "resized-images"); private string ResizedImageCachePath => Path.Combine(this._appPaths.ImageCachePath, "resized-images");
/// <inheritdoc /> /// <inheritdoc />
public IReadOnlyCollection<string> SupportedInputFormats => public IReadOnlyCollection<string> SupportedInputFormats =>
@@ -113,11 +113,11 @@ public sealed class ImageProcessor : IImageProcessor, IDisposable
}; };
/// <inheritdoc /> /// <inheritdoc />
public bool SupportsImageCollageCreation => _imageEncoder.SupportsImageCollageCreation; public bool SupportsImageCollageCreation => this._imageEncoder.SupportsImageCollageCreation;
/// <inheritdoc /> /// <inheritdoc />
public IReadOnlyCollection<ImageFormat> GetSupportedImageOutputFormats() public IReadOnlyCollection<ImageFormat> GetSupportedImageOutputFormats()
=> _imageEncoder.SupportedOutputFormats; => this._imageEncoder.SupportedOutputFormats;
/// <inheritdoc /> /// <inheritdoc />
public async Task<(string Path, string? MimeType, DateTime DateModified)> ProcessImage(ImageProcessingOptions options) public async Task<(string Path, string? MimeType, DateTime DateModified)> ProcessImage(ImageProcessingOptions options)
@@ -134,12 +134,12 @@ public sealed class ImageProcessor : IImageProcessor, IDisposable
} }
var mimeType = MimeTypes.GetMimeType(originalImagePath); var mimeType = MimeTypes.GetMimeType(originalImagePath);
if (!_imageEncoder.SupportsImageEncoding) if (!this._imageEncoder.SupportsImageEncoding)
{ {
return (originalImagePath, mimeType, dateModified); return (originalImagePath, mimeType, dateModified);
} }
var supportedImageInfo = await GetSupportedImage(originalImagePath, dateModified).ConfigureAwait(false); var supportedImageInfo = await this.GetSupportedImage(originalImagePath, dateModified).ConfigureAwait(false);
originalImagePath = supportedImageInfo.Path; originalImagePath = supportedImageInfo.Path;
// Original file doesn't exist, or original file is gif. // Original file doesn't exist, or original file is gif.
@@ -179,8 +179,8 @@ public sealed class ImageProcessor : IImageProcessor, IDisposable
int quality = options.Quality; int quality = options.Quality;
ImageFormat outputFormat = GetOutputFormat(options.SupportedOutputFormats, requiresTransparency); ImageFormat outputFormat = this.GetOutputFormat(options.SupportedOutputFormats, requiresTransparency);
string cacheFilePath = GetCacheFilePath( string cacheFilePath = this.GetCacheFilePath(
originalImagePath, originalImagePath,
options.Width, options.Width,
options.Height, options.Height,
@@ -204,9 +204,9 @@ public sealed class ImageProcessor : IImageProcessor, IDisposable
string resultPath; string resultPath;
// Limit number of parallel (more precisely: concurrent) image encodings to prevent a high memory usage // Limit number of parallel (more precisely: concurrent) image encodings to prevent a high memory usage
using (await _parallelEncodingLimit.LockAsync().ConfigureAwait(false)) using (await this._parallelEncodingLimit.LockAsync().ConfigureAwait(false))
{ {
resultPath = _imageEncoder.EncodeImage(originalImagePath, dateModified, cacheFilePath, autoOrient, orientation, quality, options, outputFormat); resultPath = this._imageEncoder.EncodeImage(originalImagePath, dateModified, cacheFilePath, autoOrient, orientation, quality, options, outputFormat);
} }
if (string.Equals(resultPath, originalImagePath, StringComparison.OrdinalIgnoreCase)) if (string.Equals(resultPath, originalImagePath, StringComparison.OrdinalIgnoreCase))
@@ -215,19 +215,19 @@ public sealed class ImageProcessor : IImageProcessor, IDisposable
} }
} }
return (cacheFilePath, outputFormat.GetMimeType(), _fileSystem.GetLastWriteTimeUtc(cacheFilePath)); return (cacheFilePath, outputFormat.GetMimeType(), this._fileSystem.GetLastWriteTimeUtc(cacheFilePath));
} }
catch (Exception ex) catch (Exception ex)
{ {
// If it fails for whatever reason, return the original image // If it fails for whatever reason, return the original image
_logger.LogError(ex, "Error encoding image"); this._logger.LogError(ex, "Error encoding image");
return (originalImagePath, MimeTypes.GetMimeType(originalImagePath), dateModified); return (originalImagePath, MimeTypes.GetMimeType(originalImagePath), dateModified);
} }
} }
private ImageFormat GetOutputFormat(IReadOnlyCollection<ImageFormat> clientSupportedFormats, bool requiresTransparency) private ImageFormat GetOutputFormat(IReadOnlyCollection<ImageFormat> clientSupportedFormats, bool requiresTransparency)
{ {
var serverFormats = GetSupportedImageOutputFormats(); var serverFormats = this.GetSupportedImageOutputFormats();
// Client doesn't care about format, so start with webp if supported // Client doesn't care about format, so start with webp if supported
if (serverFormats.Contains(ImageFormat.Webp) && clientSupportedFormats.Contains(ImageFormat.Webp)) if (serverFormats.Contains(ImageFormat.Webp) && clientSupportedFormats.Contains(ImageFormat.Webp))
@@ -354,7 +354,7 @@ public sealed class ImageProcessor : IImageProcessor, IDisposable
filename.Append(",v="); filename.Append(",v=");
filename.Append(Version); filename.Append(Version);
return GetCachePath(ResizedImageCachePath, filename.ToString(), format.GetExtension()); return this.GetCachePath(this.ResizedImageCachePath, filename.ToString(), format.GetExtension());
} }
/// <inheritdoc /> /// <inheritdoc />
@@ -369,9 +369,9 @@ public sealed class ImageProcessor : IImageProcessor, IDisposable
} }
string path = info.Path; string path = info.Path;
_logger.LogDebug("Getting image size for item {ItemType} {Path}", item.GetType().Name, path); this._logger.LogDebug("Getting image size for item {ItemType} {Path}", item.GetType().Name, path);
ImageDimensions size = GetImageDimensions(path); ImageDimensions size = this.GetImageDimensions(path);
info.Width = size.Width; info.Width = size.Width;
info.Height = size.Height; info.Height = size.Height;
@@ -380,13 +380,13 @@ public sealed class ImageProcessor : IImageProcessor, IDisposable
/// <inheritdoc /> /// <inheritdoc />
public ImageDimensions GetImageDimensions(string path) public ImageDimensions GetImageDimensions(string path)
=> _imageEncoder.GetImageSize(path); => this._imageEncoder.GetImageSize(path);
/// <inheritdoc /> /// <inheritdoc />
public string GetImageBlurHash(string path) public string GetImageBlurHash(string path)
{ {
var size = GetImageDimensions(path); var size = this.GetImageDimensions(path);
return GetImageBlurHash(path, size); return this.GetImageBlurHash(path, size);
} }
/// <inheritdoc /> /// <inheritdoc />
@@ -406,7 +406,7 @@ public sealed class ImageProcessor : IImageProcessor, IDisposable
int xComp = Math.Min((int)xCompF + 1, 9); int xComp = Math.Min((int)xCompF + 1, 9);
int yComp = Math.Min((int)yCompF + 1, 9); int yComp = Math.Min((int)yCompF + 1, 9);
return _imageEncoder.GetImageBlurHash(xComp, yComp, path); return this._imageEncoder.GetImageBlurHash(xComp, yComp, path);
} }
/// <inheritdoc /> /// <inheritdoc />
@@ -415,11 +415,11 @@ public sealed class ImageProcessor : IImageProcessor, IDisposable
/// <inheritdoc /> /// <inheritdoc />
public string GetImageCacheTag(BaseItem item, ItemImageInfo image) public string GetImageCacheTag(BaseItem item, ItemImageInfo image)
=> GetImageCacheTag(item.Path, image.DateModified); => this.GetImageCacheTag(item.Path, image.DateModified);
/// <inheritdoc /> /// <inheritdoc />
public string GetImageCacheTag(BaseItemDto item, ItemImageInfo image) public string GetImageCacheTag(BaseItemDto item, ItemImageInfo image)
=> GetImageCacheTag(item.Path, image.DateModified); => this.GetImageCacheTag(item.Path, image.DateModified);
/// <inheritdoc /> /// <inheritdoc />
public string? GetImageCacheTag(BaseItemDto item, ChapterInfo chapter) public string? GetImageCacheTag(BaseItemDto item, ChapterInfo chapter)
@@ -429,7 +429,7 @@ public sealed class ImageProcessor : IImageProcessor, IDisposable
return null; return null;
} }
return GetImageCacheTag(item.Path, chapter.ImageDateModified); return this.GetImageCacheTag(item.Path, chapter.ImageDateModified);
} }
/// <inheritdoc /> /// <inheritdoc />
@@ -440,7 +440,7 @@ public sealed class ImageProcessor : IImageProcessor, IDisposable
return null; return null;
} }
return GetImageCacheTag(item, new ItemImageInfo return this.GetImageCacheTag(item, new ItemImageInfo
{ {
Path = chapter.ImagePath, Path = chapter.ImagePath,
Type = ImageType.Chapter, Type = ImageType.Chapter,
@@ -456,7 +456,7 @@ public sealed class ImageProcessor : IImageProcessor, IDisposable
return null; return null;
} }
return GetImageCacheTag(user.ProfileImage.Path, user.ProfileImage.LastModified); return this.GetImageCacheTag(user.ProfileImage.Path, user.ProfileImage.LastModified);
} }
private Task<(string Path, DateTime DateModified)> GetSupportedImage(string originalImagePath, DateTime dateModified) private Task<(string Path, DateTime DateModified)> GetSupportedImage(string originalImagePath, DateTime dateModified)
@@ -494,7 +494,7 @@ public sealed class ImageProcessor : IImageProcessor, IDisposable
var filename = uniqueName.GetMD5() + fileExtension; var filename = uniqueName.GetMD5() + fileExtension;
return GetCachePath(path, filename); return this.GetCachePath(path, filename);
} }
/// <summary> /// <summary>
@@ -528,28 +528,28 @@ public sealed class ImageProcessor : IImageProcessor, IDisposable
/// <inheritdoc /> /// <inheritdoc />
public void CreateImageCollage(ImageCollageOptions options, string? libraryName) public void CreateImageCollage(ImageCollageOptions options, string? libraryName)
{ {
_logger.LogDebug("Creating image collage and saving to {Path}", options.OutputPath); this._logger.LogDebug("Creating image collage and saving to {Path}", options.OutputPath);
_imageEncoder.CreateImageCollage(options, libraryName); this._imageEncoder.CreateImageCollage(options, libraryName);
_logger.LogDebug("Completed creation of image collage and saved to {Path}", options.OutputPath); this._logger.LogDebug("Completed creation of image collage and saved to {Path}", options.OutputPath);
} }
/// <inheritdoc /> /// <inheritdoc />
public void Dispose() public void Dispose()
{ {
if (_disposed) if (this._disposed)
{ {
return; return;
} }
if (_imageEncoder is IDisposable disposable) if (this._imageEncoder is IDisposable disposable)
{ {
disposable.Dispose(); disposable.Dispose();
} }
_parallelEncodingLimit?.Dispose(); this._parallelEncodingLimit?.Dispose();
_disposed = true; this._disposed = true;
} }
} }
@@ -13,8 +13,17 @@ namespace Jellyfin.Controller.Tests
using Moq; using Moq;
using Xunit; using Xunit;
/// <summary>
/// Tests for <see cref="BaseItemManager"/>.
/// </summary>
public class BaseItemManagerTests public class BaseItemManagerTests
{ {
/// <summary>
/// Tests that IsMetadataFetcherEnabled checks library and server configuration options.
/// </summary>
/// <param name="itemType">The type of item to test.</param>
/// <param name="fetcherName">The name of the metadata fetcher.</param>
/// <param name="expected">The expected result.</param>
[Theory] [Theory]
[InlineData(typeof(Book), "LibraryEnabled", true)] [InlineData(typeof(Book), "LibraryEnabled", true)]
[InlineData(typeof(Book), "LibraryDisabled", false)] [InlineData(typeof(Book), "LibraryDisabled", false)]
@@ -24,30 +33,36 @@ namespace Jellyfin.Controller.Tests
{ {
BaseItem item = (BaseItem)Activator.CreateInstance(itemType)!; BaseItem item = (BaseItem)Activator.CreateInstance(itemType)!;
var libraryTypeOptions = itemType == typeof(Book) TypeOptions? libraryTypeOptions = itemType == typeof(Book)
? new TypeOptions ? new TypeOptions
{ {
Type = "Book", Type = "Book",
MetadataFetchers = new[] { "LibraryEnabled" } MetadataFetchers = ["LibraryEnabled"],
} }
: null; : null;
var serverConfiguration = new ServerConfiguration(); ServerConfiguration serverConfiguration = new();
foreach (var typeConfig in serverConfiguration.MetadataOptions) foreach (MetadataOptions typeConfig in serverConfiguration.MetadataOptions)
{ {
typeConfig.DisabledMetadataFetchers = new[] { "ServerDisabled" }; typeConfig.DisabledMetadataFetchers = ["ServerDisabled"];
} }
var serverConfigurationManager = new Mock<IServerConfigurationManager>(); Mock<IServerConfigurationManager> serverConfigurationManager = new();
serverConfigurationManager.Setup(scm => scm.Configuration) _ = serverConfigurationManager.Setup(scm => scm.Configuration)
.Returns(serverConfiguration); .Returns(serverConfiguration);
var baseItemManager = new BaseItemManager(serverConfigurationManager.Object); BaseItemManager baseItemManager = new(serverConfigurationManager.Object);
var actual = baseItemManager.IsMetadataFetcherEnabled(item, libraryTypeOptions, fetcherName); bool actual = baseItemManager.IsMetadataFetcherEnabled(item, libraryTypeOptions, fetcherName);
Assert.Equal(expected, actual); Assert.Equal(expected, actual);
} }
/// <summary>
/// Tests that IsImageFetcherEnabled checks library and server configuration options.
/// </summary>
/// <param name="itemType">The type of item to test.</param>
/// <param name="fetcherName">The name of the image fetcher.</param>
/// <param name="expected">The expected result.</param>
[Theory] [Theory]
[InlineData(typeof(Book), "LibraryEnabled", true)] [InlineData(typeof(Book), "LibraryEnabled", true)]
[InlineData(typeof(Book), "LibraryDisabled", false)] [InlineData(typeof(Book), "LibraryDisabled", false)]
@@ -57,26 +72,26 @@ namespace Jellyfin.Controller.Tests
{ {
BaseItem item = (BaseItem)Activator.CreateInstance(itemType)!; BaseItem item = (BaseItem)Activator.CreateInstance(itemType)!;
var libraryTypeOptions = itemType == typeof(Book) TypeOptions? libraryTypeOptions = itemType == typeof(Book)
? new TypeOptions ? new TypeOptions
{ {
Type = "Book", Type = "Book",
ImageFetchers = new[] { "LibraryEnabled" } ImageFetchers = ["LibraryEnabled"],
} }
: null; : null;
var serverConfiguration = new ServerConfiguration(); ServerConfiguration serverConfiguration = new();
foreach (var typeConfig in serverConfiguration.MetadataOptions) foreach (MetadataOptions typeConfig in serverConfiguration.MetadataOptions)
{ {
typeConfig.DisabledImageFetchers = new[] { "ServerDisabled" }; typeConfig.DisabledImageFetchers = ["ServerDisabled"];
} }
var serverConfigurationManager = new Mock<IServerConfigurationManager>(); Mock<IServerConfigurationManager> serverConfigurationManager = new();
serverConfigurationManager.Setup(scm => scm.Configuration) _ = serverConfigurationManager.Setup(scm => scm.Configuration)
.Returns(serverConfiguration); .Returns(serverConfiguration);
var baseItemManager = new BaseItemManager(serverConfigurationManager.Object); BaseItemManager baseItemManager = new(serverConfigurationManager.Object);
var actual = baseItemManager.IsImageFetcherEnabled(item, libraryTypeOptions, fetcherName); bool actual = baseItemManager.IsImageFetcherEnabled(item, libraryTypeOptions, fetcherName);
Assert.Equal(expected, actual); Assert.Equal(expected, actual);
} }
@@ -4,124 +4,140 @@
namespace Jellyfin.Controller.Tests namespace Jellyfin.Controller.Tests
{ {
using System.Collections.Generic;
using System.Linq; using System.Linq;
using MediaBrowser.Controller.Providers; using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.IO; using MediaBrowser.Model.IO;
using Moq; using Moq;
using Xunit; using Xunit;
/// <summary>
/// Tests for <see cref="DirectoryService"/>.
/// </summary>
public class DirectoryServiceTests public class DirectoryServiceTests
{ {
private const string LowerCasePath = "/music/someartist"; private const string LowerCasePath = "/music/someartist";
private const string UpperCasePath = "/music/SOMEARTIST"; private const string UpperCasePath = "/music/SOMEARTIST";
private static readonly FileSystemMetadata[] _lowerCaseFileSystemMetadata = private static readonly FileSystemMetadata[] LowerCaseFileSystemMetadata =
{ [
new() new()
{ {
FullName = LowerCasePath + "/Artwork", FullName = LowerCasePath + "/Artwork",
IsDirectory = true IsDirectory = true,
}, },
new() new()
{ {
FullName = LowerCasePath + "/Some Other Folder", FullName = LowerCasePath + "/Some Other Folder",
IsDirectory = true IsDirectory = true,
}, },
new() new()
{ {
FullName = LowerCasePath + "/Song 2.mp3", FullName = LowerCasePath + "/Song 2.mp3",
IsDirectory = false IsDirectory = false,
}, },
new() new()
{ {
FullName = LowerCasePath + "/Song 3.mp3", FullName = LowerCasePath + "/Song 3.mp3",
IsDirectory = false IsDirectory = false,
} },
}; ];
private static readonly FileSystemMetadata[] _upperCaseFileSystemMetadata = private static readonly FileSystemMetadata[] UpperCaseFileSystemMetadata =
{ [
new() new()
{ {
FullName = UpperCasePath + "/Lyrics", FullName = UpperCasePath + "/Lyrics",
IsDirectory = true IsDirectory = true,
}, },
new() new()
{ {
FullName = UpperCasePath + "/Song 1.mp3", FullName = UpperCasePath + "/Song 1.mp3",
IsDirectory = false IsDirectory = false,
} },
}; ];
/// <summary>
/// Tests that GetFileSystemEntries caches entries for paths with different casing.
/// </summary>
[Fact] [Fact]
public void GetFileSystemEntries_GivenPathsWithDifferentCasing_CachesAll() public void GetFileSystemEntries_GivenPathsWithDifferentCasing_CachesAll()
{ {
var fileSystemMock = new Mock<IFileSystem>(); Mock<IFileSystem> fileSystemMock = new();
fileSystemMock.Setup(f => f.GetFileSystemEntries(It.Is<string>(x => x == UpperCasePath), false)).Returns(_upperCaseFileSystemMetadata); _ = fileSystemMock.Setup(f => f.GetFileSystemEntries(It.Is<string>(x => x == UpperCasePath), false)).Returns(UpperCaseFileSystemMetadata);
fileSystemMock.Setup(f => f.GetFileSystemEntries(It.Is<string>(x => x == LowerCasePath), false)).Returns(_lowerCaseFileSystemMetadata); _ = fileSystemMock.Setup(f => f.GetFileSystemEntries(It.Is<string>(x => x == LowerCasePath), false)).Returns(LowerCaseFileSystemMetadata);
var directoryService = new DirectoryService(fileSystemMock.Object); DirectoryService directoryService = new(fileSystemMock.Object);
var upperCaseResult = directoryService.GetFileSystemEntries(UpperCasePath); FileSystemMetadata[] upperCaseResult = directoryService.GetFileSystemEntries(UpperCasePath);
var lowerCaseResult = directoryService.GetFileSystemEntries(LowerCasePath); FileSystemMetadata[] lowerCaseResult = directoryService.GetFileSystemEntries(LowerCasePath);
Assert.Equal(_upperCaseFileSystemMetadata, upperCaseResult); Assert.Equal(UpperCaseFileSystemMetadata, upperCaseResult);
Assert.Equal(_lowerCaseFileSystemMetadata, lowerCaseResult); Assert.Equal(LowerCaseFileSystemMetadata, lowerCaseResult);
} }
/// <summary>
/// Tests that GetFiles returns correct files for paths with different casing.
/// </summary>
[Fact] [Fact]
public void GetFiles_GivenPathsWithDifferentCasing_ReturnsCorrectFiles() public void GetFiles_GivenPathsWithDifferentCasing_ReturnsCorrectFiles()
{ {
var fileSystemMock = new Mock<IFileSystem>(); Mock<IFileSystem> fileSystemMock = new();
fileSystemMock.Setup(f => f.GetFileSystemEntries(It.Is<string>(x => x == UpperCasePath), false)).Returns(_upperCaseFileSystemMetadata); _ = fileSystemMock.Setup(f => f.GetFileSystemEntries(It.Is<string>(x => x == UpperCasePath), false)).Returns(UpperCaseFileSystemMetadata);
fileSystemMock.Setup(f => f.GetFileSystemEntries(It.Is<string>(x => x == LowerCasePath), false)).Returns(_lowerCaseFileSystemMetadata); _ = fileSystemMock.Setup(f => f.GetFileSystemEntries(It.Is<string>(x => x == LowerCasePath), false)).Returns(LowerCaseFileSystemMetadata);
var directoryService = new DirectoryService(fileSystemMock.Object); DirectoryService directoryService = new(fileSystemMock.Object);
var upperCaseResult = directoryService.GetFiles(UpperCasePath); List<FileSystemMetadata> upperCaseResult = directoryService.GetFiles(UpperCasePath);
var lowerCaseResult = directoryService.GetFiles(LowerCasePath); List<FileSystemMetadata> lowerCaseResult = directoryService.GetFiles(LowerCasePath);
Assert.Equal(_upperCaseFileSystemMetadata.Where(f => !f.IsDirectory), upperCaseResult); Assert.Equal(UpperCaseFileSystemMetadata.Where(f => !f.IsDirectory), upperCaseResult);
Assert.Equal(_lowerCaseFileSystemMetadata.Where(f => !f.IsDirectory), lowerCaseResult); Assert.Equal(LowerCaseFileSystemMetadata.Where(f => !f.IsDirectory), lowerCaseResult);
} }
/// <summary>
/// Tests that GetDirectories returns correct directories for paths with different casing.
/// </summary>
[Fact] [Fact]
public void GetDirectories_GivenPathsWithDifferentCasing_ReturnsCorrectDirectories() public void GetDirectories_GivenPathsWithDifferentCasing_ReturnsCorrectDirectories()
{ {
var fileSystemMock = new Mock<IFileSystem>(); Mock<IFileSystem> fileSystemMock = new();
fileSystemMock.Setup(f => f.GetFileSystemEntries(It.Is<string>(x => x == UpperCasePath), false)).Returns(_upperCaseFileSystemMetadata); _ = fileSystemMock.Setup(f => f.GetFileSystemEntries(It.Is<string>(x => x == UpperCasePath), false)).Returns(UpperCaseFileSystemMetadata);
fileSystemMock.Setup(f => f.GetFileSystemEntries(It.Is<string>(x => x == LowerCasePath), false)).Returns(_lowerCaseFileSystemMetadata); _ = fileSystemMock.Setup(f => f.GetFileSystemEntries(It.Is<string>(x => x == LowerCasePath), false)).Returns(LowerCaseFileSystemMetadata);
var directoryService = new DirectoryService(fileSystemMock.Object); DirectoryService directoryService = new(fileSystemMock.Object);
var upperCaseResult = directoryService.GetDirectories(UpperCasePath); List<FileSystemMetadata> upperCaseResult = directoryService.GetDirectories(UpperCasePath);
var lowerCaseResult = directoryService.GetDirectories(LowerCasePath); List<FileSystemMetadata> lowerCaseResult = directoryService.GetDirectories(LowerCasePath);
Assert.Equal(_upperCaseFileSystemMetadata.Where(f => f.IsDirectory), upperCaseResult); Assert.Equal(UpperCaseFileSystemMetadata.Where(f => f.IsDirectory), upperCaseResult);
Assert.Equal(_lowerCaseFileSystemMetadata.Where(f => f.IsDirectory), lowerCaseResult); Assert.Equal(LowerCaseFileSystemMetadata.Where(f => f.IsDirectory), lowerCaseResult);
} }
/// <summary>
/// Tests that GetFile returns correct file for paths with different casing.
/// </summary>
[Fact] [Fact]
public void GetFile_GivenFilePathsWithDifferentCasing_ReturnsCorrectFile() public void GetFile_GivenFilePathsWithDifferentCasing_ReturnsCorrectFile()
{ {
const string lowerCasePath = "/music/someartist/song 1.mp3"; const string lowerCasePath = "/music/someartist/song 1.mp3";
var lowerCaseFileSystemMetadata = new FileSystemMetadata FileSystemMetadata lowerCaseFileSystemMetadata = new()
{ {
FullName = lowerCasePath, FullName = lowerCasePath,
Exists = true Exists = true,
}; };
const string upperCasePath = "/music/SOMEARTIST/SONG 1.mp3"; const string upperCasePath = "/music/SOMEARTIST/SONG 1.mp3";
var upperCaseFileSystemMetadata = new FileSystemMetadata FileSystemMetadata upperCaseFileSystemMetadata = new()
{ {
FullName = upperCasePath, FullName = upperCasePath,
Exists = false Exists = false,
}; };
var fileSystemMock = new Mock<IFileSystem>(); Mock<IFileSystem> fileSystemMock = new();
fileSystemMock.Setup(f => f.GetFileSystemInfo(It.Is<string>(x => x == upperCasePath))).Returns(upperCaseFileSystemMetadata); _ = fileSystemMock.Setup(f => f.GetFileSystemInfo(It.Is<string>(x => x == upperCasePath))).Returns(upperCaseFileSystemMetadata);
fileSystemMock.Setup(f => f.GetFileSystemInfo(It.Is<string>(x => x == lowerCasePath))).Returns(lowerCaseFileSystemMetadata); _ = fileSystemMock.Setup(f => f.GetFileSystemInfo(It.Is<string>(x => x == lowerCasePath))).Returns(lowerCaseFileSystemMetadata);
var directoryService = new DirectoryService(fileSystemMock.Object); DirectoryService directoryService = new(fileSystemMock.Object);
var lowerCaseDirResult = directoryService.GetDirectory(lowerCasePath); FileSystemMetadata? lowerCaseDirResult = directoryService.GetDirectory(lowerCasePath);
var lowerCaseFileResult = directoryService.GetFile(lowerCasePath); FileSystemMetadata? lowerCaseFileResult = directoryService.GetFile(lowerCasePath);
var upperCaseDirResult = directoryService.GetDirectory(upperCasePath); FileSystemMetadata? upperCaseDirResult = directoryService.GetDirectory(upperCasePath);
var upperCaseFileResult = directoryService.GetFile(upperCasePath); FileSystemMetadata? upperCaseFileResult = directoryService.GetFile(upperCasePath);
Assert.Null(lowerCaseDirResult); Assert.Null(lowerCaseDirResult);
Assert.Equal(lowerCaseFileSystemMetadata, lowerCaseFileResult); Assert.Equal(lowerCaseFileSystemMetadata, lowerCaseFileResult);
@@ -129,32 +145,35 @@ namespace Jellyfin.Controller.Tests
Assert.Null(upperCaseFileResult); Assert.Null(upperCaseFileResult);
} }
/// <summary>
/// Tests that GetDirectory returns correct directory for paths with different casing.
/// </summary>
[Fact] [Fact]
public void GetDirectory_GivenFilePathsWithDifferentCasing_ReturnsCorrectDirectory() public void GetDirectory_GivenFilePathsWithDifferentCasing_ReturnsCorrectDirectory()
{ {
const string lowerCasePath = "/music/someartist/Lyrics"; const string lowerCasePath = "/music/someartist/Lyrics";
var lowerCaseFileSystemMetadata = new FileSystemMetadata FileSystemMetadata lowerCaseFileSystemMetadata = new()
{ {
FullName = lowerCasePath, FullName = lowerCasePath,
IsDirectory = true, IsDirectory = true,
Exists = true Exists = true,
}; };
const string upperCasePath = "/music/SOMEARTIST/LYRICS"; const string upperCasePath = "/music/SOMEARTIST/LYRICS";
var upperCaseFileSystemMetadata = new FileSystemMetadata FileSystemMetadata upperCaseFileSystemMetadata = new()
{ {
FullName = upperCasePath, FullName = upperCasePath,
IsDirectory = true, IsDirectory = true,
Exists = false Exists = false,
}; };
var fileSystemMock = new Mock<IFileSystem>(); Mock<IFileSystem> fileSystemMock = new();
fileSystemMock.Setup(f => f.GetFileSystemInfo(It.Is<string>(x => x == upperCasePath))).Returns(upperCaseFileSystemMetadata); _ = fileSystemMock.Setup(f => f.GetFileSystemInfo(It.Is<string>(x => x == upperCasePath))).Returns(upperCaseFileSystemMetadata);
fileSystemMock.Setup(f => f.GetFileSystemInfo(It.Is<string>(x => x == lowerCasePath))).Returns(lowerCaseFileSystemMetadata); _ = fileSystemMock.Setup(f => f.GetFileSystemInfo(It.Is<string>(x => x == lowerCasePath))).Returns(lowerCaseFileSystemMetadata);
var directoryService = new DirectoryService(fileSystemMock.Object); DirectoryService directoryService = new(fileSystemMock.Object);
var lowerCaseDirResult = directoryService.GetDirectory(lowerCasePath); FileSystemMetadata? lowerCaseDirResult = directoryService.GetDirectory(lowerCasePath);
var lowerCaseFileResult = directoryService.GetFile(lowerCasePath); FileSystemMetadata? lowerCaseFileResult = directoryService.GetFile(lowerCasePath);
var upperCaseDirResult = directoryService.GetDirectory(upperCasePath); FileSystemMetadata? upperCaseDirResult = directoryService.GetDirectory(upperCasePath);
var upperCaseFileResult = directoryService.GetFile(upperCasePath); FileSystemMetadata? upperCaseFileResult = directoryService.GetFile(upperCasePath);
Assert.Equal(lowerCaseFileSystemMetadata, lowerCaseDirResult); Assert.Equal(lowerCaseFileSystemMetadata, lowerCaseDirResult);
Assert.Null(lowerCaseFileResult); Assert.Null(lowerCaseFileResult);
@@ -162,92 +181,101 @@ namespace Jellyfin.Controller.Tests
Assert.Null(upperCaseFileResult); Assert.Null(upperCaseFileResult);
} }
/// <summary>
/// Tests that GetFile returns cached file when the path is already cached.
/// </summary>
[Fact] [Fact]
public void GetFile_GivenCachedPath_ReturnsCachedFile() public void GetFile_GivenCachedPath_ReturnsCachedFile()
{ {
const string path = "/music/someartist/song 1.mp3"; const string path = "/music/someartist/song 1.mp3";
var cachedFileSystemMetadata = new FileSystemMetadata FileSystemMetadata cachedFileSystemMetadata = new()
{ {
FullName = path, FullName = path,
Exists = true Exists = true,
}; };
var newFileSystemMetadata = new FileSystemMetadata FileSystemMetadata newFileSystemMetadata = new()
{ {
FullName = "/music/SOMEARTIST/song 1.mp3", FullName = "/music/SOMEARTIST/song 1.mp3",
Exists = true Exists = true,
}; };
var fileSystemMock = new Mock<IFileSystem>(); Mock<IFileSystem> fileSystemMock = new();
fileSystemMock.Setup(f => f.GetFileSystemInfo(It.Is<string>(x => x == path))).Returns(cachedFileSystemMetadata); _ = fileSystemMock.Setup(f => f.GetFileSystemInfo(It.Is<string>(x => x == path))).Returns(cachedFileSystemMetadata);
var directoryService = new DirectoryService(fileSystemMock.Object); DirectoryService directoryService = new(fileSystemMock.Object);
var result = directoryService.GetFile(path); FileSystemMetadata? result = directoryService.GetFile(path);
fileSystemMock.Setup(f => f.GetFileSystemInfo(It.Is<string>(x => x == path))).Returns(newFileSystemMetadata); _ = fileSystemMock.Setup(f => f.GetFileSystemInfo(It.Is<string>(x => x == path))).Returns(newFileSystemMetadata);
var secondResult = directoryService.GetFile(path); FileSystemMetadata? secondResult = directoryService.GetFile(path);
Assert.Equivalent(cachedFileSystemMetadata, result); Assert.Equivalent(cachedFileSystemMetadata, result);
Assert.Equivalent(cachedFileSystemMetadata, secondResult); Assert.Equivalent(cachedFileSystemMetadata, secondResult);
} }
/// <summary>
/// Tests that GetFilePaths returns only cached paths when clear is not called.
/// </summary>
[Fact] [Fact]
public void GetFilePaths_GivenCachedFilePathWithoutClear_ReturnsOnlyCachedPaths() public void GetFilePaths_GivenCachedFilePathWithoutClear_ReturnsOnlyCachedPaths()
{ {
const string path = "/music/someartist"; const string path = "/music/someartist";
var cachedPaths = new[] string[] cachedPaths =
{ [
"/music/someartist/song 1.mp3", "/music/someartist/song 1.mp3",
"/music/someartist/song 2.mp3", "/music/someartist/song 2.mp3",
"/music/someartist/song 3.mp3", "/music/someartist/song 3.mp3",
"/music/someartist/song 4.mp3", "/music/someartist/song 4.mp3",
}; ];
var newPaths = new[] string[] newPaths =
{ [
"/music/someartist/song 5.mp3", "/music/someartist/song 5.mp3",
"/music/someartist/song 6.mp3", "/music/someartist/song 6.mp3",
"/music/someartist/song 7.mp3", "/music/someartist/song 7.mp3",
"/music/someartist/song 8.mp3", "/music/someartist/song 8.mp3",
}; ];
var fileSystemMock = new Mock<IFileSystem>(); Mock<IFileSystem> fileSystemMock = new();
fileSystemMock.Setup(f => f.GetFilePaths(It.Is<string>(x => x == path), false)).Returns(cachedPaths); _ = fileSystemMock.Setup(f => f.GetFilePaths(It.Is<string>(x => x == path), false)).Returns(cachedPaths);
var directoryService = new DirectoryService(fileSystemMock.Object); DirectoryService directoryService = new(fileSystemMock.Object);
var result = directoryService.GetFilePaths(path); IReadOnlyList<string> result = directoryService.GetFilePaths(path);
fileSystemMock.Setup(f => f.GetFilePaths(It.Is<string>(x => x == path), false)).Returns(newPaths); _ = fileSystemMock.Setup(f => f.GetFilePaths(It.Is<string>(x => x == path), false)).Returns(newPaths);
var secondResult = directoryService.GetFilePaths(path); IReadOnlyList<string> secondResult = directoryService.GetFilePaths(path);
Assert.Equal(cachedPaths, result); Assert.Equal(cachedPaths, result);
Assert.Equal(cachedPaths, secondResult); Assert.Equal(cachedPaths, secondResult);
} }
/// <summary>
/// Tests that GetFilePaths returns new paths when clear is called.
/// </summary>
[Fact] [Fact]
public void GetFilePaths_GivenCachedFilePathWithClear_ReturnsNewPaths() public void GetFilePaths_GivenCachedFilePathWithClear_ReturnsNewPaths()
{ {
const string path = "/music/someartist"; const string path = "/music/someartist";
var cachedPaths = new[] string[] cachedPaths =
{ [
"/music/someartist/song 1.mp3", "/music/someartist/song 1.mp3",
"/music/someartist/song 2.mp3", "/music/someartist/song 2.mp3",
"/music/someartist/song 3.mp3", "/music/someartist/song 3.mp3",
"/music/someartist/song 4.mp3", "/music/someartist/song 4.mp3",
}; ];
var newPaths = new[] string[] newPaths =
{ [
"/music/someartist/song 5.mp3", "/music/someartist/song 5.mp3",
"/music/someartist/song 6.mp3", "/music/someartist/song 6.mp3",
"/music/someartist/song 7.mp3", "/music/someartist/song 7.mp3",
"/music/someartist/song 8.mp3", "/music/someartist/song 8.mp3",
}; ];
var fileSystemMock = new Mock<IFileSystem>(); Mock<IFileSystem> fileSystemMock = new();
fileSystemMock.Setup(f => f.GetFilePaths(It.Is<string>(x => x == path), false)).Returns(cachedPaths); _ = fileSystemMock.Setup(f => f.GetFilePaths(It.Is<string>(x => x == path), false)).Returns(cachedPaths);
var directoryService = new DirectoryService(fileSystemMock.Object); DirectoryService directoryService = new(fileSystemMock.Object);
var result = directoryService.GetFilePaths(path); IReadOnlyList<string> result = directoryService.GetFilePaths(path);
fileSystemMock.Setup(f => f.GetFilePaths(It.Is<string>(x => x == path), false)).Returns(newPaths); _ = fileSystemMock.Setup(f => f.GetFilePaths(It.Is<string>(x => x == path), false)).Returns(newPaths);
var secondResult = directoryService.GetFilePaths(path, true); IReadOnlyList<string> secondResult = directoryService.GetFilePaths(path, true);
Assert.Equal(cachedPaths, result); Assert.Equal(cachedPaths, result);
Assert.Equal(newPaths, secondResult); Assert.Equal(newPaths, secondResult);
@@ -2,49 +2,67 @@
// Copyright (c) PlaceholderCompany. All rights reserved. // Copyright (c) PlaceholderCompany. All rights reserved.
// </copyright> // </copyright>
namespace Jellyfin.Controller.Tests.Entities; namespace Jellyfin.Controller.Tests.Entities
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library;
using MediaBrowser.Model.MediaInfo;
using Moq;
using Xunit;
public class BaseItemTests
{ {
[Theory] using MediaBrowser.Controller.Entities;
[InlineData("", "")] using MediaBrowser.Controller.Library;
[InlineData("1", "0000000001")] using MediaBrowser.Model.MediaInfo;
[InlineData("t", "t")] using Moq;
[InlineData("test", "test")] using Xunit;
[InlineData("test1", "test0000000001")]
[InlineData("1test 2", "0000000001test 0000000002")]
public void BaseItem_ModifySortChunks_Valid(string input, string expected)
=> Assert.Equal(expected, BaseItem.ModifySortChunks(input));
[Theory] /// <summary>
[InlineData("/Movies/Ted/Ted.mp4", "/Movies/Ted/Ted - Unrated Edition.mp4", "Ted", "Unrated Edition")] /// Tests for <see cref="BaseItem"/>.
[InlineData("/Movies/Deadpool 2 (2018)/Deadpool 2 (2018).mkv", "/Movies/Deadpool 2 (2018)/Deadpool 2 (2018) - Super Duper Cut.mkv", "Deadpool 2 (2018)", "Super Duper Cut")] /// </summary>
public void GetMediaSourceName_Valid(string primaryPath, string altPath, string name, string altName) public class BaseItemTests
{ {
var mediaSourceManager = new Mock<IMediaSourceManager>(); /// <summary>
mediaSourceManager.Setup(x => x.GetPathProtocol(It.IsAny<string>())) /// Tests that ModifySortChunks correctly modifies sort chunks.
.Returns((string x) => MediaProtocol.File); /// </summary>
BaseItem.MediaSourceManager = mediaSourceManager.Object; /// <param name="input">The input string.</param>
/// <param name="expected">The expected output string.</param>
var video = new Video() [Theory]
[InlineData("", "")]
[InlineData("1", "0000000001")]
[InlineData("t", "t")]
[InlineData("test", "test")]
[InlineData("test1", "test0000000001")]
[InlineData("1test 2", "0000000001test 0000000002")]
public void BaseItem_ModifySortChunks_Valid(string input, string expected)
{ {
Path = primaryPath Assert.Equal(expected, BaseItem.ModifySortChunks(input));
}; }
var videoAlt = new Video() /// <summary>
/// Tests that GetMediaSourceName returns the correct name for media sources.
/// </summary>
/// <param name="primaryPath">The primary path.</param>
/// <param name="altPath">The alternate path.</param>
/// <param name="name">The expected name for the primary path.</param>
/// <param name="altName">The expected name for the alternate path.</param>
[Theory]
[InlineData("/Movies/Ted/Ted.mp4", "/Movies/Ted/Ted - Unrated Edition.mp4", "Ted", "Unrated Edition")]
[InlineData("/Movies/Deadpool 2 (2018)/Deadpool 2 (2018).mkv", "/Movies/Deadpool 2 (2018)/Deadpool 2 (2018) - Super Duper Cut.mkv", "Deadpool 2 (2018)", "Super Duper Cut")]
public void GetMediaSourceName_Valid(string primaryPath, string altPath, string name, string altName)
{ {
Path = altPath, Mock<IMediaSourceManager> mediaSourceManager = new();
}; _ = mediaSourceManager.Setup(x => x.GetPathProtocol(It.IsAny<string>()))
.Returns((string x) => MediaProtocol.File);
BaseItem.MediaSourceManager = mediaSourceManager.Object;
video.LocalAlternateVersions = [videoAlt.Path]; Video video = new()
{
Path = primaryPath,
};
Assert.Equal(name, video.GetMediaSourceName(video)); Video videoAlt = new()
Assert.Equal(altName, video.GetMediaSourceName(videoAlt)); {
Path = altPath,
};
video.LocalAlternateVersions = [videoAlt.Path];
Assert.Equal(name, video.GetMediaSourceName(video));
Assert.Equal(altName, video.GetMediaSourceName(videoAlt));
}
} }
} }
@@ -4,6 +4,7 @@
<PropertyGroup> <PropertyGroup>
<ProjectGuid>{462584F7-5023-4019-9EAC-B98CA458C0A0}</ProjectGuid> <ProjectGuid>{462584F7-5023-4019-9EAC-B98CA458C0A0}</ProjectGuid>
<TargetFramework>net11.0</TargetFramework> <TargetFramework>net11.0</TargetFramework>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>