aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorCody Robibero <cody@robibe.ro>2021-11-03 07:52:06 -0600
committerGitHub <noreply@github.com>2021-11-03 07:52:06 -0600
commit3398f7f953864c47d66b49287fcc2b613b0c03de (patch)
treeb2948acf8016f85e80f0495362a5f49bb079d6f6
parentf444e93a56f6613baa8a722099159f2935d4d942 (diff)
parent45ceba7ad152de85956af51272c53c29cbbd5a70 (diff)
Merge branch 'master' into client-logger
-rw-r--r--Emby.Server.Implementations/ApplicationHost.cs154
-rw-r--r--Emby.Server.Implementations/Localization/countries.json2
-rw-r--r--Emby.Server.Implementations/Localization/iso6392.txt3
-rw-r--r--Jellyfin.Server/CoreAppHost.cs49
-rw-r--r--Jellyfin.Server/Program.cs41
-rw-r--r--MediaBrowser.Common/IApplicationHost.cs4
-rw-r--r--MediaBrowser.Controller/Entities/BaseItem.cs21
-rw-r--r--MediaBrowser.Model/Dlna/StreamBuilder.cs12
-rw-r--r--MediaBrowser.Providers/Manager/ItemImageProvider.cs133
-rw-r--r--tests/Jellyfin.Providers.Tests/Jellyfin.Providers.Tests.csproj6
-rw-r--r--tests/Jellyfin.Providers.Tests/Manager/ItemImageProviderTests.cs606
-rw-r--r--tests/Jellyfin.Providers.Tests/Test Data/Images/blank0.jpg0
-rw-r--r--tests/Jellyfin.Providers.Tests/Test Data/Images/blank1.jpg0
-rw-r--r--tests/Jellyfin.Server.Implementations.Tests/Localization/LocalizationManagerTests.cs2
-rw-r--r--tests/Jellyfin.Server.Integration.Tests/JellyfinApplicationFactory.cs10
-rw-r--r--tests/Jellyfin.Server.Integration.Tests/TestAppHost.cs12
16 files changed, 824 insertions, 231 deletions
diff --git a/Emby.Server.Implementations/ApplicationHost.cs b/Emby.Server.Implementations/ApplicationHost.cs
index 4cfd5cb37..f359ee44c 100644
--- a/Emby.Server.Implementations/ApplicationHost.cs
+++ b/Emby.Server.Implementations/ApplicationHost.cs
@@ -148,25 +148,20 @@ namespace Emby.Server.Implementations
/// <param name="loggerFactory">Instance of the <see cref="ILoggerFactory"/> interface.</param>
/// <param name="options">Instance of the <see cref="IStartupOptions"/> interface.</param>
/// <param name="startupConfig">The <see cref="IConfiguration" /> interface.</param>
- /// <param name="fileSystem">Instance of the <see cref="IFileSystem"/> interface.</param>
- /// <param name="serviceCollection">Instance of the <see cref="IServiceCollection"/> interface.</param>
public ApplicationHost(
IServerApplicationPaths applicationPaths,
ILoggerFactory loggerFactory,
IStartupOptions options,
- IConfiguration startupConfig,
- IFileSystem fileSystem,
- IServiceCollection serviceCollection)
+ IConfiguration startupConfig)
{
ApplicationPaths = applicationPaths;
LoggerFactory = loggerFactory;
_startupOptions = options;
_startupConfig = startupConfig;
- _fileSystemManager = fileSystem;
- ServiceCollection = serviceCollection;
+ _fileSystemManager = new ManagedFileSystem(LoggerFactory.CreateLogger<ManagedFileSystem>(), applicationPaths);
Logger = LoggerFactory.CreateLogger<ApplicationHost>();
- fileSystem.AddShortcutHandler(new MbLinkShortcutHandler(fileSystem));
+ _fileSystemManager.AddShortcutHandler(new MbLinkShortcutHandler(_fileSystemManager));
ApplicationVersion = typeof(ApplicationHost).Assembly.GetName().Version;
ApplicationVersionString = ApplicationVersion.ToString(3);
@@ -231,8 +226,6 @@ namespace Emby.Server.Implementations
/// </summary>
protected ILogger<ApplicationHost> Logger { get; }
- protected IServiceCollection ServiceCollection { get; }
-
/// <summary>
/// Gets the logger factory.
/// </summary>
@@ -522,7 +515,7 @@ namespace Emby.Server.Implementations
}
/// <inheritdoc/>
- public void Init()
+ public void Init(IServiceCollection serviceCollection)
{
DiscoverTypes();
@@ -552,130 +545,129 @@ namespace Emby.Server.Implementations
CertificatePath = networkConfiguration.CertificatePath;
Certificate = GetCertificate(CertificatePath, networkConfiguration.CertificatePassword);
- RegisterServices();
+ RegisterServices(serviceCollection);
- _pluginManager.RegisterServices(ServiceCollection);
+ _pluginManager.RegisterServices(serviceCollection);
}
/// <summary>
/// Registers services/resources with the service collection that will be available via DI.
/// </summary>
- protected virtual void RegisterServices()
+ /// <param name="serviceCollection">Instance of the <see cref="IServiceCollection"/> interface.</param>
+ protected virtual void RegisterServices(IServiceCollection serviceCollection)
{
- ServiceCollection.AddSingleton(_startupOptions);
+ serviceCollection.AddSingleton(_startupOptions);
- ServiceCollection.AddMemoryCache();
+ serviceCollection.AddMemoryCache();
- ServiceCollection.AddSingleton<IServerConfigurationManager>(ConfigurationManager);
- ServiceCollection.AddSingleton<IConfigurationManager>(ConfigurationManager);
- ServiceCollection.AddSingleton<IApplicationHost>(this);
- ServiceCollection.AddSingleton<IPluginManager>(_pluginManager);
- ServiceCollection.AddSingleton<IApplicationPaths>(ApplicationPaths);
+ serviceCollection.AddSingleton<IServerConfigurationManager>(ConfigurationManager);
+ serviceCollection.AddSingleton<IConfigurationManager>(ConfigurationManager);
+ serviceCollection.AddSingleton<IApplicationHost>(this);
+ serviceCollection.AddSingleton<IPluginManager>(_pluginManager);
+ serviceCollection.AddSingleton<IApplicationPaths>(ApplicationPaths);
- ServiceCollection.AddSingleton(_fileSystemManager);
- ServiceCollection.AddSingleton<TmdbClientManager>();
+ serviceCollection.AddSingleton(_fileSystemManager);
+ serviceCollection.AddSingleton<TmdbClientManager>();
- ServiceCollection.AddSingleton(NetManager);
+ serviceCollection.AddSingleton(NetManager);
- ServiceCollection.AddSingleton<ITaskManager, TaskManager>();
+ serviceCollection.AddSingleton<ITaskManager, TaskManager>();
- ServiceCollection.AddSingleton(_xmlSerializer);
+ serviceCollection.AddSingleton(_xmlSerializer);
- ServiceCollection.AddSingleton<IStreamHelper, StreamHelper>();
+ serviceCollection.AddSingleton<IStreamHelper, StreamHelper>();
- ServiceCollection.AddSingleton<ICryptoProvider, CryptographyProvider>();
+ serviceCollection.AddSingleton<ICryptoProvider, CryptographyProvider>();
- ServiceCollection.AddSingleton<ISocketFactory, SocketFactory>();
+ serviceCollection.AddSingleton<ISocketFactory, SocketFactory>();
- ServiceCollection.AddSingleton<IInstallationManager, InstallationManager>();
+ serviceCollection.AddSingleton<IInstallationManager, InstallationManager>();
- ServiceCollection.AddSingleton<IZipClient, ZipClient>();
+ serviceCollection.AddSingleton<IZipClient, ZipClient>();
- ServiceCollection.AddSingleton<IServerApplicationHost>(this);
- ServiceCollection.AddSingleton<IServerApplicationPaths>(ApplicationPaths);
+ serviceCollection.AddSingleton<IServerApplicationHost>(this);
+ serviceCollection.AddSingleton<IServerApplicationPaths>(ApplicationPaths);
- ServiceCollection.AddSingleton<ILocalizationManager, LocalizationManager>();
+ serviceCollection.AddSingleton<ILocalizationManager, LocalizationManager>();
- ServiceCollection.AddSingleton<IBlurayExaminer, BdInfoExaminer>();
+ serviceCollection.AddSingleton<IBlurayExaminer, BdInfoExaminer>();
- ServiceCollection.AddSingleton<IUserDataRepository, SqliteUserDataRepository>();
- ServiceCollection.AddSingleton<IUserDataManager, UserDataManager>();
+ serviceCollection.AddSingleton<IUserDataRepository, SqliteUserDataRepository>();
+ serviceCollection.AddSingleton<IUserDataManager, UserDataManager>();
- ServiceCollection.AddSingleton<IItemRepository, SqliteItemRepository>();
+ serviceCollection.AddSingleton<IItemRepository, SqliteItemRepository>();
- ServiceCollection.AddSingleton<IMediaEncoder, MediaBrowser.MediaEncoding.Encoder.MediaEncoder>();
- ServiceCollection.AddSingleton<EncodingHelper>();
+ serviceCollection.AddSingleton<IMediaEncoder, MediaBrowser.MediaEncoding.Encoder.MediaEncoder>();
+ serviceCollection.AddSingleton<EncodingHelper>();
// TODO: Refactor to eliminate the circular dependencies here so that Lazy<T> isn't required
- ServiceCollection.AddTransient(provider => new Lazy<ILibraryMonitor>(provider.GetRequiredService<ILibraryMonitor>));
- ServiceCollection.AddTransient(provider => new Lazy<IProviderManager>(provider.GetRequiredService<IProviderManager>));
- ServiceCollection.AddTransient(provider => new Lazy<IUserViewManager>(provider.GetRequiredService<IUserViewManager>));
- ServiceCollection.AddSingleton<ILibraryManager, LibraryManager>();
+ serviceCollection.AddTransient(provider => new Lazy<ILibraryMonitor>(provider.GetRequiredService<ILibraryMonitor>));
+ serviceCollection.AddTransient(provider => new Lazy<IProviderManager>(provider.GetRequiredService<IProviderManager>));
+ serviceCollection.AddTransient(provider => new Lazy<IUserViewManager>(provider.GetRequiredService<IUserViewManager>));
+ serviceCollection.AddSingleton<ILibraryManager, LibraryManager>();
- ServiceCollection.AddSingleton<IMusicManager, MusicManager>();
+ serviceCollection.AddSingleton<IMusicManager, MusicManager>();
- ServiceCollection.AddSingleton<ILibraryMonitor, LibraryMonitor>();
+ serviceCollection.AddSingleton<ILibraryMonitor, LibraryMonitor>();
- ServiceCollection.AddSingleton<ISearchEngine, SearchEngine>();
+ serviceCollection.AddSingleton<ISearchEngine, SearchEngine>();
- ServiceCollection.AddSingleton<IWebSocketManager, WebSocketManager>();
+ serviceCollection.AddSingleton<IWebSocketManager, WebSocketManager>();
- ServiceCollection.AddSingleton<IImageProcessor, ImageProcessor>();
+ serviceCollection.AddSingleton<IImageProcessor, ImageProcessor>();
- ServiceCollection.AddSingleton<ITVSeriesManager, TVSeriesManager>();
+ serviceCollection.AddSingleton<ITVSeriesManager, TVSeriesManager>();
- ServiceCollection.AddSingleton<IMediaSourceManager, MediaSourceManager>();
+ serviceCollection.AddSingleton<IMediaSourceManager, MediaSourceManager>();
- ServiceCollection.AddSingleton<ISubtitleManager, SubtitleManager>();
+ serviceCollection.AddSingleton<ISubtitleManager, SubtitleManager>();
- ServiceCollection.AddSingleton<IProviderManager, ProviderManager>();
+ serviceCollection.AddSingleton<IProviderManager, ProviderManager>();
// TODO: Refactor to eliminate the circular dependency here so that Lazy<T> isn't required
- ServiceCollection.AddTransient(provider => new Lazy<ILiveTvManager>(provider.GetRequiredService<ILiveTvManager>));
- ServiceCollection.AddSingleton<IDtoService, DtoService>();
-
- ServiceCollection.AddSingleton<IChannelManager, ChannelManager>();
-
- ServiceCollection.AddSingleton<ISessionManager, SessionManager>();
+ serviceCollection.AddTransient(provider => new Lazy<ILiveTvManager>(provider.GetRequiredService<ILiveTvManager>));
+ serviceCollection.AddSingleton<IDtoService, DtoService>();
- ServiceCollection.AddSingleton<IDlnaManager, DlnaManager>();
+ serviceCollection.AddSingleton<IChannelManager, ChannelManager>();
- ServiceCollection.AddSingleton<ICollectionManager, CollectionManager>();
+ serviceCollection.AddSingleton<ISessionManager, SessionManager>();
- ServiceCollection.AddSingleton<IPlaylistManager, PlaylistManager>();
+ serviceCollection.AddSingleton<IDlnaManager, DlnaManager>();
- ServiceCollection.AddSingleton<ISyncPlayManager, SyncPlayManager>();
+ serviceCollection.AddSingleton<ICollectionManager, CollectionManager>();
- ServiceCollection.AddSingleton<LiveTvDtoService>();
- ServiceCollection.AddSingleton<ILiveTvManager, LiveTvManager>();
+ serviceCollection.AddSingleton<IPlaylistManager, PlaylistManager>();
- ServiceCollection.AddSingleton<IUserViewManager, UserViewManager>();
+ serviceCollection.AddSingleton<ISyncPlayManager, SyncPlayManager>();
- ServiceCollection.AddSingleton<INotificationManager, NotificationManager>();
+ serviceCollection.AddSingleton<LiveTvDtoService>();
+ serviceCollection.AddSingleton<ILiveTvManager, LiveTvManager>();
- ServiceCollection.AddSingleton<IDeviceDiscovery, DeviceDiscovery>();
+ serviceCollection.AddSingleton<IUserViewManager, UserViewManager>();
- ServiceCollection.AddSingleton<IChapterManager, ChapterManager>();
+ serviceCollection.AddSingleton<INotificationManager, NotificationManager>();
- ServiceCollection.AddSingleton<IEncodingManager, MediaEncoder.EncodingManager>();
+ serviceCollection.AddSingleton<IDeviceDiscovery, DeviceDiscovery>();
- ServiceCollection.AddScoped<ISessionContext, SessionContext>();
+ serviceCollection.AddSingleton<IChapterManager, ChapterManager>();
- ServiceCollection.AddSingleton<IAuthService, AuthService>();
- ServiceCollection.AddSingleton<IQuickConnect, QuickConnectManager>();
+ serviceCollection.AddSingleton<IEncodingManager, MediaEncoder.EncodingManager>();
- ServiceCollection.AddSingleton<ISubtitleEncoder, MediaBrowser.MediaEncoding.Subtitles.SubtitleEncoder>();
+ serviceCollection.AddScoped<ISessionContext, SessionContext>();
- ServiceCollection.AddSingleton<IAttachmentExtractor, MediaBrowser.MediaEncoding.Attachments.AttachmentExtractor>();
+ serviceCollection.AddSingleton<IAuthService, AuthService>();
+ serviceCollection.AddSingleton<IQuickConnect, QuickConnectManager>();
- ServiceCollection.AddSingleton<TranscodingJobHelper>();
- ServiceCollection.AddScoped<MediaInfoHelper>();
- ServiceCollection.AddScoped<AudioHelper>();
- ServiceCollection.AddScoped<DynamicHlsHelper>();
+ serviceCollection.AddSingleton<ISubtitleEncoder, MediaBrowser.MediaEncoding.Subtitles.SubtitleEncoder>();
- ServiceCollection.AddScoped<IClientEventLogger, ClientEventLogger>();
+ serviceCollection.AddSingleton<IAttachmentExtractor, MediaBrowser.MediaEncoding.Attachments.AttachmentExtractor>();
- ServiceCollection.AddSingleton<IDirectoryService, DirectoryService>();
+ serviceCollection.AddSingleton<TranscodingJobHelper>();
+ serviceCollection.AddScoped<MediaInfoHelper>();
+ serviceCollection.AddScoped<AudioHelper>();
+ serviceCollection.AddScoped<DynamicHlsHelper>();
+ serviceCollection.AddScoped<IClientEventLogger, ClientEventLogger>();
+ serviceCollection.AddSingleton<IDirectoryService, DirectoryService>();
}
/// <summary>
diff --git a/Emby.Server.Implementations/Localization/countries.json b/Emby.Server.Implementations/Localization/countries.json
index b08a3ae79..22ffc5e09 100644
--- a/Emby.Server.Implementations/Localization/countries.json
+++ b/Emby.Server.Implementations/Localization/countries.json
@@ -630,7 +630,7 @@
"TwoLetterISORegionName": "MD"
},
{
- "DisplayName": "Réunion",
+ "DisplayName": "Réunion",
"Name": "RE",
"ThreeLetterISORegionName": "REU",
"TwoLetterISORegionName": "RE"
diff --git a/Emby.Server.Implementations/Localization/iso6392.txt b/Emby.Server.Implementations/Localization/iso6392.txt
index 488901822..66fba3330 100644
--- a/Emby.Server.Implementations/Localization/iso6392.txt
+++ b/Emby.Server.Implementations/Localization/iso6392.txt
@@ -349,7 +349,8 @@ pli||pi|Pali|pali
pol||pl|Polish|polonais
pon|||Pohnpeian|pohnpei
por||pt|Portuguese|portugais
-pob||pt-br|Portuguese (Brazil)|portugais
+pop||pt-pt|Portuguese (Portugal)|portugais (pt-pt)
+pob||pt-br|Portuguese (Brazil)|portugais (pt-br)
pra|||Prakrit languages|prâkrit, langues
pro|||Provençal, Old (to 1500)|provençal ancien (jusqu'à 1500)
pus||ps|Pushto; Pashto|pachto
diff --git a/Jellyfin.Server/CoreAppHost.cs b/Jellyfin.Server/CoreAppHost.cs
index 21bd9ba01..67e50b92d 100644
--- a/Jellyfin.Server/CoreAppHost.cs
+++ b/Jellyfin.Server/CoreAppHost.cs
@@ -22,7 +22,6 @@ using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Net;
using MediaBrowser.Controller.Security;
using MediaBrowser.Model.Activity;
-using MediaBrowser.Model.IO;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
@@ -42,67 +41,61 @@ namespace Jellyfin.Server
/// <param name="loggerFactory">The <see cref="ILoggerFactory" /> to be used by the <see cref="CoreAppHost" />.</param>
/// <param name="options">The <see cref="StartupOptions" /> to be used by the <see cref="CoreAppHost" />.</param>
/// <param name="startupConfig">The <see cref="IConfiguration" /> to be used by the <see cref="CoreAppHost" />.</param>
- /// <param name="fileSystem">The <see cref="IFileSystem" /> to be used by the <see cref="CoreAppHost" />.</param>
- /// <param name="collection">The <see cref="IServiceCollection"/> to be used by the <see cref="CoreAppHost"/>.</param>
public CoreAppHost(
IServerApplicationPaths applicationPaths,
ILoggerFactory loggerFactory,
IStartupOptions options,
- IConfiguration startupConfig,
- IFileSystem fileSystem,
- IServiceCollection collection)
+ IConfiguration startupConfig)
: base(
applicationPaths,
loggerFactory,
options,
- startupConfig,
- fileSystem,
- collection)
+ startupConfig)
{
}
/// <inheritdoc/>
- protected override void RegisterServices()
+ protected override void RegisterServices(IServiceCollection serviceCollection)
{
// Register an image encoder
bool useSkiaEncoder = SkiaEncoder.IsNativeLibAvailable();
Type imageEncoderType = useSkiaEncoder
? typeof(SkiaEncoder)
: typeof(NullImageEncoder);
- ServiceCollection.AddSingleton(typeof(IImageEncoder), imageEncoderType);
+ serviceCollection.AddSingleton(typeof(IImageEncoder), imageEncoderType);
// Log a warning if the Skia encoder could not be used
if (!useSkiaEncoder)
{
- Logger.LogWarning($"Skia not available. Will fallback to {nameof(NullImageEncoder)}.");
+ Logger.LogWarning("Skia not available. Will fallback to {ImageEncoder}.", nameof(NullImageEncoder));
}
- ServiceCollection.AddDbContextPool<JellyfinDb>(
+ serviceCollection.AddDbContextPool<JellyfinDb>(
options => options
.UseLoggerFactory(LoggerFactory)
.UseSqlite($"Filename={Path.Combine(ApplicationPaths.DataPath, "jellyfin.db")}"));
- ServiceCollection.AddEventServices();
- ServiceCollection.AddSingleton<IBaseItemManager, BaseItemManager>();
- ServiceCollection.AddSingleton<IEventManager, EventManager>();
- ServiceCollection.AddSingleton<JellyfinDbProvider>();
+ serviceCollection.AddEventServices();
+ serviceCollection.AddSingleton<IBaseItemManager, BaseItemManager>();
+ serviceCollection.AddSingleton<IEventManager, EventManager>();
+ serviceCollection.AddSingleton<JellyfinDbProvider>();
- ServiceCollection.AddSingleton<IActivityManager, ActivityManager>();
- ServiceCollection.AddSingleton<IUserManager, UserManager>();
- ServiceCollection.AddSingleton<IDisplayPreferencesManager, DisplayPreferencesManager>();
- ServiceCollection.AddSingleton<IDeviceManager, DeviceManager>();
+ serviceCollection.AddSingleton<IActivityManager, ActivityManager>();
+ serviceCollection.AddSingleton<IUserManager, UserManager>();
+ serviceCollection.AddSingleton<IDisplayPreferencesManager, DisplayPreferencesManager>();
+ serviceCollection.AddSingleton<IDeviceManager, DeviceManager>();
// TODO search the assemblies instead of adding them manually?
- ServiceCollection.AddSingleton<IWebSocketListener, SessionWebSocketListener>();
- ServiceCollection.AddSingleton<IWebSocketListener, ActivityLogWebSocketListener>();
- ServiceCollection.AddSingleton<IWebSocketListener, ScheduledTasksWebSocketListener>();
- ServiceCollection.AddSingleton<IWebSocketListener, SessionInfoWebSocketListener>();
+ serviceCollection.AddSingleton<IWebSocketListener, SessionWebSocketListener>();
+ serviceCollection.AddSingleton<IWebSocketListener, ActivityLogWebSocketListener>();
+ serviceCollection.AddSingleton<IWebSocketListener, ScheduledTasksWebSocketListener>();
+ serviceCollection.AddSingleton<IWebSocketListener, SessionInfoWebSocketListener>();
- ServiceCollection.AddSingleton<IAuthorizationContext, AuthorizationContext>();
+ serviceCollection.AddSingleton<IAuthorizationContext, AuthorizationContext>();
- ServiceCollection.AddScoped<IAuthenticationManager, AuthenticationManager>();
+ serviceCollection.AddScoped<IAuthenticationManager, AuthenticationManager>();
- base.RegisterServices();
+ base.RegisterServices(serviceCollection);
}
/// <inheritdoc />
diff --git a/Jellyfin.Server/Program.cs b/Jellyfin.Server/Program.cs
index 2f8986da8..5c7012d58 100644
--- a/Jellyfin.Server/Program.cs
+++ b/Jellyfin.Server/Program.cs
@@ -10,7 +10,6 @@ using System.Threading;
using System.Threading.Tasks;
using CommandLine;
using Emby.Server.Implementations;
-using Emby.Server.Implementations.IO;
using Jellyfin.Server.Implementations;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Common.Net;
@@ -159,34 +158,36 @@ namespace Jellyfin.Server
ApplicationHost.LogEnvironmentInfo(_logger, appPaths);
+ // If hosting the web client, validate the client content path
+ if (startupConfig.HostWebClient())
+ {
+ string? webContentPath = appPaths.WebPath;
+ if (!Directory.Exists(webContentPath) || !Directory.EnumerateFiles(webContentPath).Any())
+ {
+ _logger.LogError(
+ "The server is expected to host the web client, but the provided content directory is either " +
+ "invalid or empty: {WebContentPath}. If you do not want to host the web client with the " +
+ "server, you may set the '--nowebclient' command line flag, or set" +
+ "'{ConfigKey}=false' in your config settings.",
+ webContentPath,
+ ConfigurationExtensions.HostWebClientKey);
+ Environment.ExitCode = 1;
+ return;
+ }
+ }
+
PerformStaticInitialization();
- var serviceCollection = new ServiceCollection();
var appHost = new CoreAppHost(
appPaths,
_loggerFactory,
options,
- startupConfig,
- new ManagedFileSystem(_loggerFactory.CreateLogger<ManagedFileSystem>(), appPaths),
- serviceCollection);
+ startupConfig);
try
{
- // If hosting the web client, validate the client content path
- if (startupConfig.HostWebClient())
- {
- string? webContentPath = appHost.ConfigurationManager.ApplicationPaths.WebPath;
- if (!Directory.Exists(webContentPath) || Directory.GetFiles(webContentPath).Length == 0)
- {
- throw new InvalidOperationException(
- "The server is expected to host the web client, but the provided content directory is either " +
- $"invalid or empty: {webContentPath}. If you do not want to host the web client with the " +
- "server, you may set the '--nowebclient' command line flag, or set" +
- $"'{ConfigurationExtensions.HostWebClientKey}=false' in your config settings.");
- }
- }
-
- appHost.Init();
+ var serviceCollection = new ServiceCollection();
+ appHost.Init(serviceCollection);
var webHost = new WebHostBuilder().ConfigureWebHostBuilder(appHost, serviceCollection, options, startupConfig, appPaths).Build();
diff --git a/MediaBrowser.Common/IApplicationHost.cs b/MediaBrowser.Common/IApplicationHost.cs
index 192a77611..e49ab41f4 100644
--- a/MediaBrowser.Common/IApplicationHost.cs
+++ b/MediaBrowser.Common/IApplicationHost.cs
@@ -2,6 +2,7 @@ using System;
using System.Collections.Generic;
using System.Reflection;
using System.Threading.Tasks;
+using Microsoft.Extensions.DependencyInjection;
namespace MediaBrowser.Common
{
@@ -137,7 +138,8 @@ namespace MediaBrowser.Common
/// <summary>
/// Initializes this instance.
/// </summary>
- void Init();
+ /// <param name="serviceCollection">Instance of the <see cref="IServiceCollection"/> interface.</param>
+ void Init(IServiceCollection serviceCollection);
/// <summary>
/// Creates the instance.
diff --git a/MediaBrowser.Controller/Entities/BaseItem.cs b/MediaBrowser.Controller/Entities/BaseItem.cs
index 838a9f2f8..02ee97b23 100644
--- a/MediaBrowser.Controller/Entities/BaseItem.cs
+++ b/MediaBrowser.Controller/Entities/BaseItem.cs
@@ -2345,7 +2345,7 @@ namespace MediaBrowser.Controller.Entities
RemoveImages(new List<ItemImageInfo> { image });
}
- public void RemoveImages(List<ItemImageInfo> deletedImages)
+ public void RemoveImages(IEnumerable<ItemImageInfo> deletedImages)
{
ImageInfos = ImageInfos.Except(deletedImages).ToArray();
}
@@ -2495,11 +2495,11 @@ namespace MediaBrowser.Controller.Entities
}
/// <summary>
- /// Adds the images.
+ /// Adds the images, updating metadata if they already are part of this item.
/// </summary>
/// <param name="imageType">Type of the image.</param>
/// <param name="images">The images.</param>
- /// <returns><c>true</c> if XXXX, <c>false</c> otherwise.</returns>
+ /// <returns><c>true</c> if images were added or updated, <c>false</c> otherwise.</returns>
/// <exception cref="ArgumentException">Cannot call AddImages with chapter images.</exception>
public bool AddImages(ImageType imageType, List<FileSystemMetadata> images)
{
@@ -2512,7 +2512,6 @@ namespace MediaBrowser.Controller.Entities
.ToList();
var newImageList = new List<FileSystemMetadata>();
- var imageAdded = false;
var imageUpdated = false;
foreach (var newImage in images)
@@ -2528,7 +2527,6 @@ namespace MediaBrowser.Controller.Entities
if (existing == null)
{
newImageList.Add(newImage);
- imageAdded = true;
}
else
{
@@ -2549,19 +2547,6 @@ namespace MediaBrowser.Controller.Entities
}
}
- if (imageAdded || images.Count != existingImages.Count)
- {
- var newImagePaths = images.Select(i => i.FullName).ToList();
-
- var deleted = existingImages
- .FindAll(i => i.IsLocalFile && !newImagePaths.Contains(i.Path.AsSpan(), StringComparison.OrdinalIgnoreCase) && !File.Exists(i.Path));
-
- if (deleted.Count > 0)
- {
- ImageInfos = ImageInfos.Except(deleted).ToArray();
- }
- }
-
if (newImageList.Count > 0)
{
ImageInfos = ImageInfos.Concat(newImageList.Select(i => GetImageInfo(i, imageType))).ToArray();
diff --git a/MediaBrowser.Model/Dlna/StreamBuilder.cs b/MediaBrowser.Model/Dlna/StreamBuilder.cs
index 84d99d550..322cc367b 100644
--- a/MediaBrowser.Model/Dlna/StreamBuilder.cs
+++ b/MediaBrowser.Model/Dlna/StreamBuilder.cs
@@ -455,7 +455,7 @@ namespace MediaBrowser.Model.Dlna
if (directPlayProfile == null)
{
- _logger.LogInformation(
+ _logger.LogDebug(
"Profile: {0}, No audio direct play profiles found for {1} with codec {2}",
options.Profile.Name ?? "Unknown Profile",
item.Path ?? "Unknown path",
@@ -682,7 +682,7 @@ namespace MediaBrowser.Model.Dlna
bool isEligibleForDirectPlay = options.EnableDirectPlay && (options.ForceDirectPlay || directPlayEligibilityResult.Item1);
bool isEligibleForDirectStream = options.EnableDirectStream && (options.ForceDirectStream || directStreamEligibilityResult.Item1);
- _logger.LogInformation(
+ _logger.LogDebug(
"Profile: {0}, Path: {1}, isEligibleForDirectPlay: {2}, isEligibleForDirectStream: {3}",
options.Profile.Name ?? "Unknown Profile",
item.Path ?? "Unknown path",
@@ -1033,7 +1033,7 @@ namespace MediaBrowser.Model.Dlna
if (directPlay == null)
{
- _logger.LogInformation(
+ _logger.LogDebug(
"Container: {Container}, Video: {Video}, Audio: {Audio} cannot be direct played by profile: {Profile} for path: {Path}",
container,
videoStream?.Codec ?? "no video",
@@ -1198,7 +1198,7 @@ namespace MediaBrowser.Model.Dlna
private void LogConditionFailure(DeviceProfile profile, string type, ProfileCondition condition, MediaSourceInfo mediaSource)
{
- _logger.LogInformation(
+ _logger.LogDebug(
"Profile: {0}, DirectPlay=false. Reason={1}.{2} Condition: {3}. ConditionValue: {4}. IsRequired: {5}. Path: {6}",
type,
profile.Name ?? "Unknown Profile",
@@ -1222,7 +1222,7 @@ namespace MediaBrowser.Model.Dlna
if (subtitleProfile.Method != SubtitleDeliveryMethod.External && subtitleProfile.Method != SubtitleDeliveryMethod.Embed)
{
- _logger.LogInformation("Not eligible for {0} due to unsupported subtitles", playMethod);
+ _logger.LogDebug("Not eligible for {0} due to unsupported subtitles", playMethod);
return (false, TranscodeReason.SubtitleCodecNotSupported);
}
}
@@ -1404,7 +1404,7 @@ namespace MediaBrowser.Model.Dlna
if (itemBitrate > requestedMaxBitrate)
{
- _logger.LogInformation(
+ _logger.LogDebug(
"Bitrate exceeds {PlayBackMethod} limit: media bitrate: {MediaBitrate}, max bitrate: {MaxBitrate}",
playMethod,
itemBitrate,
diff --git a/MediaBrowser.Providers/Manager/ItemImageProvider.cs b/MediaBrowser.Providers/Manager/ItemImageProvider.cs
index 39372acb9..8d5795f8e 100644
--- a/MediaBrowser.Providers/Manager/ItemImageProvider.cs
+++ b/MediaBrowser.Providers/Manager/ItemImageProvider.cs
@@ -1,7 +1,5 @@
#nullable disable
-#pragma warning disable CA1002, CS1591
-
using System;
using System.Collections.Generic;
using System.IO;
@@ -25,6 +23,9 @@ using Microsoft.Extensions.Logging;
namespace MediaBrowser.Providers.Manager
{
+ /// <summary>
+ /// Utilities for managing images attached to items.
+ /// </summary>
public class ItemImageProvider
{
private readonly ILogger _logger;
@@ -47,6 +48,12 @@ namespace MediaBrowser.Providers.Manager
ImageType.Thumb
};
+ /// <summary>
+ /// Initializes a new instance of the <see cref="ItemImageProvider"/> class.
+ /// </summary>
+ /// <param name="logger">The logger.</param>
+ /// <param name="providerManager">The provider manager for interacting with provider image references.</param>
+ /// <param name="fileSystem">The filesystem.</param>
public ItemImageProvider(ILogger logger, IProviderManager providerManager, IFileSystem fileSystem)
{
_logger = logger;
@@ -54,6 +61,13 @@ namespace MediaBrowser.Providers.Manager
_fileSystem = fileSystem;
}
+ /// <summary>
+ /// Verifies existing images have valid paths and adds any new local images provided.
+ /// </summary>
+ /// <param name="item">The <see cref="BaseItem"/> to validate images for.</param>
+ /// <param name="providers">The providers to use, must include <see cref="ILocalImageProvider"/>(s) for local scanning.</param>
+ /// <param name="directoryService">The directory service for <see cref="ILocalImageProvider"/>s to use.</param>
+ /// <returns><c>true</c> if changes were made to the item; otherwise <c>false</c>.</returns>
public bool ValidateImages(BaseItem item, IEnumerable<IImageProvider> providers, IDirectoryService directoryService)
{
var hasChanges = false;
@@ -73,6 +87,15 @@ namespace MediaBrowser.Providers.Manager
return hasChanges;
}
+ /// <summary>
+ /// Refreshes from the providers according to the given options.
+ /// </summary>
+ /// <param name="item">The <see cref="BaseItem"/> to gather images for.</param>
+ /// <param name="libraryOptions">The library options.</param>
+ /// <param name="providers">The providers to query for images.</param>
+ /// <param name="refreshOptions">The refresh options.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <returns>The refresh result.</returns>
public async Task<RefreshResult> RefreshImages(
BaseItem item,
LibraryOptions libraryOptions,
@@ -80,14 +103,16 @@ namespace MediaBrowser.Providers.Manager
ImageRefreshOptions refreshOptions,
CancellationToken cancellationToken)
{
+ var oldBackdropImages = Array.Empty<ItemImageInfo>();
if (refreshOptions.IsReplacingImage(ImageType.Backdrop))
{
- ClearImages(item, ImageType.Backdrop);
+ oldBackdropImages = item.GetImages(ImageType.Backdrop).ToArray();
}
+ var oldScreenshotImages = Array.Empty<ItemImageInfo>();
if (refreshOptions.IsReplacingImage(ImageType.Screenshot))
{
- ClearImages(item, ImageType.Screenshot);
+ oldScreenshotImages = item.GetImages(ImageType.Screenshot).ToArray();
}
var result = new RefreshResult { UpdateType = ItemUpdateType.None };
@@ -95,9 +120,9 @@ namespace MediaBrowser.Providers.Manager
var typeName = item.GetType().Name;
var typeOptions = libraryOptions.GetTypeOptions(typeName) ?? new TypeOptions { Type = typeName };
- // In order to avoid duplicates, only download these if there are none already
- var backdropLimit = typeOptions.GetLimit(ImageType.Backdrop);
- var screenshotLimit = typeOptions.GetLimit(ImageType.Screenshot);
+ // track library limits, adding buffer to allow lazy replacing of current images
+ var backdropLimit = typeOptions.GetLimit(ImageType.Backdrop) + oldBackdropImages.Length;
+ var screenshotLimit = typeOptions.GetLimit(ImageType.Screenshot) + oldScreenshotImages.Length;
var downloadedImages = new List<ImageType>();
foreach (var provider in providers)
@@ -114,11 +139,22 @@ namespace MediaBrowser.Providers.Manager
}
}
+ // only delete existing multi-images if new ones were added
+ if (oldBackdropImages.Length > 0 && oldBackdropImages.Length < item.GetImages(ImageType.Backdrop).Count())
+ {
+ PruneImages(item, oldBackdropImages);
+ }
+
+ if (oldScreenshotImages.Length > 0 && oldScreenshotImages.Length < item.GetImages(ImageType.Screenshot).Count())
+ {
+ PruneImages(item, oldScreenshotImages);
+ }
+
return result;
}
/// <summary>
- /// Refreshes from provider.
+ /// Refreshes from a dynamic provider.
/// </summary>
private async Task RefreshFromProvider(
BaseItem item,
@@ -153,13 +189,14 @@ namespace MediaBrowser.Providers.Manager
if (response.Protocol == MediaProtocol.Http)
{
_logger.LogDebug("Setting image url into item {0}", item.Id);
+ var index = item.AllowsMultipleImages(imageType) ? item.GetImages(imageType).Count() : 0;
item.SetImage(
new ItemImageInfo
{
Path = response.Path,
Type = imageType
},
- 0);
+ index);
}
else
{
@@ -234,7 +271,7 @@ namespace MediaBrowser.Providers.Manager
}
/// <summary>
- /// Refreshes from provider.
+ /// Refreshes from a remote provider.
/// </summary>
/// <param name="item">The item.</param>
/// <param name="provider">The provider.</param>
@@ -305,12 +342,12 @@ namespace MediaBrowser.Providers.Manager
}
minWidth = savedOptions.GetMinWidth(ImageType.Backdrop);
- await DownloadBackdrops(item, ImageType.Backdrop, backdropLimit, provider, result, list, minWidth, cancellationToken).ConfigureAwait(false);
+ await DownloadMultiImages(item, ImageType.Backdrop, refreshOptions, backdropLimit, provider, result, list, minWidth, cancellationToken).ConfigureAwait(false);
- if (item is IHasScreenshots hasScreenshots)
+ if (item is IHasScreenshots)
{
minWidth = savedOptions.GetMinWidth(ImageType.Screenshot);
- await DownloadBackdrops(item, ImageType.Screenshot, screenshotLimit, provider, result, list, minWidth, cancellationToken).ConfigureAwait(false);
+ await DownloadMultiImages(item, ImageType.Screenshot, refreshOptions, screenshotLimit, provider, result, list, minWidth, cancellationToken).ConfigureAwait(false);
}
}
catch (OperationCanceledException)
@@ -329,40 +366,36 @@ namespace MediaBrowser.Providers.Manager
return options.IsEnabled(type);
}
- private void ClearImages(BaseItem item, ImageType type)
+ private void PruneImages(BaseItem item, ItemImageInfo[] images)
{
- var deleted = false;
- var deletedImages = new List<ItemImageInfo>();
-
- foreach (var image in item.GetImages(type))
+ for (var i = 0; i < images.Length; i++)
{
- if (!image.IsLocalFile)
- {
- deletedImages.Add(image);
- continue;
- }
+ var image = images[i];
- try
- {
- _fileSystem.DeleteFile(image.Path);
- deleted = true;
- }
- catch (FileNotFoundException)
+ if (image.IsLocalFile)
{
+ try
+ {
+ _fileSystem.DeleteFile(image.Path);
+ }
+ catch (FileNotFoundException)
+ {
+ }
}
}
- item.RemoveImages(deletedImages);
-
- if (deleted)
- {
- item.ValidateImages(new DirectoryService(_fileSystem));
- }
+ item.RemoveImages(images);
}
+ /// <summary>
+ /// Merges a list of images into the provided item, validating existing images and replacing them or adding new images as necessary.
+ /// </summary>
+ /// <param name="item">The <see cref="BaseItem"/> to modify.</param>
+ /// <param name="images">The new images to place in <c>item</c>.</param>
+ /// <returns><c>true</c> if changes were made to the item; otherwise <c>false</c>.</returns>
public bool MergeImages(BaseItem item, IReadOnlyList<LocalImageInfo> images)
{
- var changed = false;
+ var changed = item.ValidateImages(new DirectoryService(_fileSystem));
for (var i = 0; i < _singularImages.Length; i++)
{
@@ -398,18 +431,6 @@ namespace MediaBrowser.Providers.Manager
currentImage.DateModified = newDateModified;
}
}
- else
- {
- var existing = item.GetImageInfo(type, 0);
- if (existing != null)
- {
- if (existing.IsLocalFile && !File.Exists(existing.Path))
- {
- item.RemoveImage(existing);
- changed = true;
- }
- }
- }
}
if (UpdateMultiImages(item, images, ImageType.Backdrop))
@@ -417,13 +438,9 @@ namespace MediaBrowser.Providers.Manager
changed = true;
}
- var hasScreenshots = item as IHasScreenshots;
- if (hasScreenshots != null)
+ if (item is IHasScreenshots && UpdateMultiImages(item, images, ImageType.Screenshot))
{
- if (UpdateMultiImages(item, images, ImageType.Screenshot))
- {
- changed = true;
- }
+ changed = true;
}
return changed;
@@ -536,7 +553,7 @@ namespace MediaBrowser.Providers.Manager
return true;
}
- if (item is IItemByName && item is not MusicArtist)
+ if (item is IItemByName and not MusicArtist)
{
var hasDualAccess = item as IHasDualAccess;
if (hasDualAccess == null || hasDualAccess.IsAccessedByName)
@@ -569,7 +586,7 @@ namespace MediaBrowser.Providers.Manager
newIndex);
}
- private async Task DownloadBackdrops(BaseItem item, ImageType imageType, int limit, IRemoteImageProvider provider, RefreshResult result, IEnumerable<RemoteImageInfo> images, int minWidth, CancellationToken cancellationToken)
+ private async Task DownloadMultiImages(BaseItem item, ImageType imageType, ImageRefreshOptions refreshOptions, int limit, IRemoteImageProvider provider, RefreshResult result, IEnumerable<RemoteImageInfo> images, int minWidth, CancellationToken cancellationToken)
{
foreach (var image in images.Where(i => i.Type == imageType))
{
@@ -609,8 +626,8 @@ namespace MediaBrowser.Providers.Manager
break;
}
- // If there's already an image of the same size, skip it
- if (response.Content.Headers.ContentLength.HasValue)
+ // If there's already an image of the same file size, skip it unless doing a full refresh
+ if (response.Content.Headers.ContentLength.HasValue && !refreshOptions.IsReplacingImage(imageType))
{
try
{
diff --git a/tests/Jellyfin.Providers.Tests/Jellyfin.Providers.Tests.csproj b/tests/Jellyfin.Providers.Tests/Jellyfin.Providers.Tests.csproj
index bb88ec6a1..10767ae23 100644
--- a/tests/Jellyfin.Providers.Tests/Jellyfin.Providers.Tests.csproj
+++ b/tests/Jellyfin.Providers.Tests/Jellyfin.Providers.Tests.csproj
@@ -7,6 +7,12 @@
</PropertyGroup>
<ItemGroup>
+ <None Include="Test Data\**\*.*">
+ <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+ </None>
+ </ItemGroup>
+
+ <ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.0.0" />
<PackageReference Include="Moq" Version="4.16.1" />
<PackageReference Include="xunit" Version="2.4.1" />
diff --git a/tests/Jellyfin.Providers.Tests/Manager/ItemImageProviderTests.cs b/tests/Jellyfin.Providers.Tests/Manager/ItemImageProviderTests.cs
new file mode 100644
index 000000000..6d65ba2d7
--- /dev/null
+++ b/tests/Jellyfin.Providers.Tests/Manager/ItemImageProviderTests.cs
@@ -0,0 +1,606 @@
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using System.IO;
+using System.Linq;
+using System.Net;
+using System.Net.Http;
+using System.Text;
+using System.Text.RegularExpressions;
+using System.Threading;
+using System.Threading.Tasks;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Entities.Movies;
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Controller.Providers;
+using MediaBrowser.Model.Configuration;
+using MediaBrowser.Model.Drawing;
+using MediaBrowser.Model.Entities;
+using MediaBrowser.Model.IO;
+using MediaBrowser.Model.MediaInfo;
+using MediaBrowser.Model.Providers;
+using MediaBrowser.Providers.Manager;
+using Microsoft.Extensions.Logging.Abstractions;
+using Moq;
+using Xunit;
+
+namespace Jellyfin.Providers.Tests.Manager
+{
+ public class ItemImageProviderTests
+ {
+ private static readonly string TestDataImagePath = "Test Data/Images/blank{0}.jpg";
+
+ [Fact]
+ public void ValidateImages_PhotoEmptyProviders_NoChange()
+ {
+ var itemImageProvider = GetItemImageProvider(null, null);
+ var changed = itemImageProvider.ValidateImages(new Photo(), new List<ILocalImageProvider>(), null);
+
+ Assert.False(changed);
+ }
+
+ [Fact]
+ public void ValidateImages_EmptyItemEmptyProviders_NoChange()
+ {
+ var itemImageProvider = GetItemImageProvider(null, null);
+ var changed = itemImageProvider.ValidateImages(new MovieWithScreenshots(), new List<ILocalImageProvider>(), null);
+
+ Assert.False(changed);
+ }
+
+ private static TheoryData<ImageType, int> GetImageTypesWithCount()
+ {
+ var theoryTypes = new TheoryData<ImageType, int>
+ {
+ // minimal test cases that hit different handling
+ { ImageType.Primary, 1 },
+ { ImageType.Backdrop, 1 },
+ { ImageType.Backdrop, 2 },
+ { ImageType.Screenshot, 1 }
+ };
+
+ return theoryTypes;
+ }
+
+ [Theory]
+ [MemberData(nameof(GetImageTypesWithCount))]
+ public void ValidateImages_EmptyItemAndPopulatedProviders_AddsImages(ImageType imageType, int imageCount)
+ {
+ // Has to exist for querying DateModified time on file, results stored but not checked so not populating
+ BaseItem.FileSystem = Mock.Of<IFileSystem>();
+
+ var item = new MovieWithScreenshots();
+ var imageProvider = GetImageProvider(imageType, imageCount, true);
+
+ var itemImageProvider = GetItemImageProvider(null, null);
+ var changed = itemImageProvider.ValidateImages(item, new List<ILocalImageProvider> { imageProvider }, null);
+
+ Assert.True(changed);
+ Assert.Equal(imageCount, item.GetImages(imageType).Count());
+ }
+
+ [Theory]
+ [MemberData(nameof(GetImageTypesWithCount))]
+ public void ValidateImages_PopulatedItemWithGoodPathsAndEmptyProviders_NoChange(ImageType imageType, int imageCount)
+ {
+ var item = GetItemWithImages(imageType, imageCount, true);
+
+ var itemImageProvider = GetItemImageProvider(null, null);
+ var changed = itemImageProvider.ValidateImages(item, new List<ILocalImageProvider>(), null);
+
+ Assert.False(changed);
+ Assert.Equal(imageCount, item.GetImages(imageType).Count());
+ }
+
+ [Theory]
+ [MemberData(nameof(GetImageTypesWithCount))]
+ public void ValidateImages_PopulatedItemWithBadPathsAndEmptyProviders_RemovesImage(ImageType imageType, int imageCount)
+ {
+ var item = GetItemWithImages(imageType, imageCount, false);
+
+ var itemImageProvider = GetItemImageProvider(null, null);
+ var changed = itemImageProvider.ValidateImages(item, new List<ILocalImageProvider>(), null);
+
+ Assert.True(changed);
+ Assert.Empty(item.GetImages(imageType));
+ }
+
+ [Fact]
+ public void MergeImages_EmptyItemNewImagesEmpty_NoChange()
+ {
+ var itemImageProvider = GetItemImageProvider(null, null);
+ var changed = itemImageProvider.MergeImages(new MovieWithScreenshots(), new List<LocalImageInfo>());
+
+ Assert.False(changed);
+ }
+
+ [Theory]
+ [MemberData(nameof(GetImageTypesWithCount))]
+ public void MergeImages_PopulatedItemWithGoodPathsAndPopulatedNewImages_AddsUpdatesImages(ImageType imageType, int imageCount)
+ {
+ // valid and not valid paths - should replace the valid paths with the invalid ones
+ var item = GetItemWithImages(imageType, imageCount, true);
+ var images = GetImages(imageType, imageCount, false);
+
+ var itemImageProvider = GetItemImageProvider(null, null);
+ var changed = itemImageProvider.MergeImages(item, images);
+
+ Assert.True(changed);
+ // adds for types that allow multiple, replaces singular type images
+ if (item.AllowsMultipleImages(imageType))
+ {
+ Assert.Equal(imageCount * 2, item.GetImages(imageType).Count());
+ }
+ else
+ {
+ Assert.Single(item.GetImages(imageType));
+ Assert.Same(images[0].FileInfo.FullName, item.GetImages(imageType).First().Path);
+ }
+ }
+
+ [Theory]
+ [MemberData(nameof(GetImageTypesWithCount))]
+ public void MergeImages_PopulatedItemWithGoodPathsAndSameNewImages_NoChange(ImageType imageType, int imageCount)
+ {
+ var oldTime = new DateTime(1970, 1, 1);
+
+ // match update time with time added to item images (unix epoch)
+ var fileSystem = new Mock<IFileSystem>();
+ fileSystem.Setup(fs => fs.GetLastWriteTimeUtc(It.IsAny<FileSystemMetadata>()))
+ .Returns(oldTime);
+ BaseItem.FileSystem = fileSystem.Object;
+
+ // all valid paths - matching for strictly updating
+ var item = GetItemWithImages(imageType, imageCount, true);
+ // set size to non-zero to allow for updates to occur
+ foreach (var image in item.GetImages(imageType))
+ {
+ image.DateModified = oldTime;
+ image.Height = 1;
+ image.Width = 1;
+ }
+
+ var images = GetImages(imageType, imageCount, true);
+
+ var itemImageProvider = GetItemImageProvider(null, fileSystem);
+ var changed = itemImageProvider.MergeImages(item, images);
+
+ Assert.False(changed);
+ }
+
+ [Theory]
+ [MemberData(nameof(GetImageTypesWithCount))]
+ public void MergeImages_PopulatedItemWithGoodPathsAndSameNewImagesWithNewTimestamps_ResetsImageSizes(ImageType imageType, int imageCount)
+ {
+ var oldTime = new DateTime(1970, 1, 1);
+ var updatedTime = new DateTime(2021, 1, 1);
+
+ var fileSystem = new Mock<IFileSystem>();
+ fileSystem.Setup(fs => fs.GetLastWriteTimeUtc(It.IsAny<FileSystemMetadata>()))
+ .Returns(updatedTime);
+ BaseItem.FileSystem = fileSystem.Object;
+
+ // all valid paths - matching for strictly updating
+ var item = GetItemWithImages(imageType, imageCount, true);
+ // set size to non-zero to allow for image size reset to occur
+ foreach (var image in item.GetImages(imageType))
+ {
+ image.DateModified = oldTime;
+ image.Height = 1;
+ image.Width = 1;
+ }
+
+ var images = GetImages(imageType, imageCount, true);
+
+ var itemImageProvider = GetItemImageProvider(null, fileSystem);
+ var changed = itemImageProvider.MergeImages(item, images);
+
+ Assert.True(changed);
+ // before and after paths are the same, verify updated by size reset to 0
+ Assert.Equal(imageCount, item.GetImages(imageType).Count());
+ foreach (var image in item.GetImages(imageType))
+ {
+ Assert.Equal(updatedTime, image.DateModified);
+ Assert.Equal(0, image.Height);
+ Assert.Equal(0, image.Width);
+ }
+ }
+
+ [Theory]
+ [InlineData(ImageType.Primary, 1, false)]
+ [InlineData(ImageType.Backdrop, 2, false)]
+ [InlineData(ImageType.Screenshot, 2, false)]
+ [InlineData(ImageType.Primary, 1, true)]
+ [InlineData(ImageType.Backdrop, 2, true)]
+ [InlineData(ImageType.Screenshot, 2, true)]
+ public async void RefreshImages_PopulatedItemPopulatedProviderDynamic_UpdatesImagesIfForced(ImageType imageType, int imageCount, bool forceRefresh)
+ {
+ var item = GetItemWithImages(imageType, imageCount, false);
+
+ var libraryOptions = GetLibraryOptions(item, imageType, imageCount);
+
+ var imageResponse = new DynamicImageResponse
+ {
+ HasImage = true,
+ Format = ImageFormat.Jpg,
+ Path = "url path",
+ Protocol = MediaProtocol.Http
+ };
+
+ var dynamicProvider = new Mock<IDynamicImageProvider>(MockBehavior.Strict);
+ dynamicProvider.Setup(rp => rp.Name).Returns("MockDynamicProvider");
+ dynamicProvider.Setup(rp => rp.GetSupportedImages(item))
+ .Returns(new[] { imageType });
+ dynamicProvider.Setup(rp => rp.GetImage(item, imageType, It.IsAny<CancellationToken>()))
+ .ReturnsAsync(imageResponse);
+
+ var refreshOptions = forceRefresh
+ ? new ImageRefreshOptions(null)
+ {
+ ImageRefreshMode = MetadataRefreshMode.FullRefresh, ReplaceAllImages = true
+ }
+ : new ImageRefreshOptions(null);
+
+ var itemImageProvider = GetItemImageProvider(null, new Mock<IFileSystem>());
+ var result = await itemImageProvider.RefreshImages(item, libraryOptions, new List<IImageProvider> { dynamicProvider.Object }, refreshOptions, CancellationToken.None);
+
+ Assert.Equal(forceRefresh, result.UpdateType.HasFlag(ItemUpdateType.ImageUpdate));
+ if (forceRefresh)
+ {
+ // replaces multi-types
+ Assert.Single(item.GetImages(imageType));
+ }
+ else
+ {
+ // adds to multi-types if room
+ Assert.Equal(imageCount, item.GetImages(imageType).Count());
+ }
+ }
+
+ [Theory]
+ [InlineData(ImageType.Primary, 1, true, MediaProtocol.Http)]
+ [InlineData(ImageType.Backdrop, 2, true, MediaProtocol.Http)]
+ [InlineData(ImageType.Primary, 1, true, MediaProtocol.File)]
+ [InlineData(ImageType.Backdrop, 2, true, MediaProtocol.File)]
+ [InlineData(ImageType.Primary, 1, false, MediaProtocol.File)]
+ [InlineData(ImageType.Backdrop, 2, false, MediaProtocol.File)]
+ public async void RefreshImages_EmptyItemPopulatedProviderDynamic_AddsImages(ImageType imageType, int imageCount, bool responseHasPath, MediaProtocol protocol)
+ {
+ // Has to exist for querying DateModified time on file, results stored but not checked so not populating
+ BaseItem.FileSystem = Mock.Of<IFileSystem>();
+
+ var item = new MovieWithScreenshots();
+
+ var libraryOptions = GetLibraryOptions(item, imageType, imageCount);
+
+ // Path must exist if set: is read in as a stream by AsyncFile.OpenRead
+ var imageResponse = new DynamicImageResponse
+ {
+ HasImage = true,
+ Format = ImageFormat.Jpg,
+ Path = responseHasPath ? string.Format(CultureInfo.InvariantCulture, TestDataImagePath, 0) : null,
+ Protocol = protocol
+ };
+
+ var dynamicProvider = new Mock<IDynamicImageProvider>(MockBehavior.Strict);
+ dynamicProvider.Setup(rp => rp.Name).Returns("MockDynamicProvider");
+ dynamicProvider.Setup(rp => rp.GetSupportedImages(item))
+ .Returns(new[] { imageType });
+ dynamicProvider.Setup(rp => rp.GetImage(item, imageType, It.IsAny<CancellationToken>()))
+ .ReturnsAsync(imageResponse);
+
+ var refreshOptions = new ImageRefreshOptions(null);
+
+ var providerManager = new Mock<IProviderManager>(MockBehavior.Strict);
+ providerManager.Setup(pm => pm.SaveImage(item, It.IsAny<Stream>(), It.IsAny<string>(), imageType, null, It.IsAny<CancellationToken>()))
+ .Callback<BaseItem, Stream, string, ImageType, int?, CancellationToken>((callbackItem, _, _, callbackType, _, _) => callbackItem.SetImagePath(callbackType, 0, new FileSystemMetadata()))
+ .Returns(Task.CompletedTask);
+ var itemImageProvider = GetItemImageProvider(providerManager.Object, null);
+ var result = await itemImageProvider.RefreshImages(item, libraryOptions, new List<IImageProvider> { dynamicProvider.Object }, refreshOptions, CancellationToken.None);
+
+ Assert.True(result.UpdateType.HasFlag(ItemUpdateType.ImageUpdate));
+ // dynamic provider unable to return multiple images
+ Assert.Single(item.GetImages(imageType));
+ if (protocol == MediaProtocol.Http)
+ {
+ Assert.Equal(imageResponse.Path, item.GetImagePath(imageType, 0));
+ }
+ }
+
+ [Theory]
+ [InlineData(ImageType.Primary, 1, false)]
+ [InlineData(ImageType.Backdrop, 1, false)]
+ [InlineData(ImageType.Backdrop, 2, false)]
+ [InlineData(ImageType.Screenshot, 2, false)]
+ [InlineData(ImageType.Primary, 1, true)]
+ [InlineData(ImageType.Backdrop, 1, true)]
+ [InlineData(ImageType.Backdrop, 2, true)]
+ [InlineData(ImageType.Screenshot, 2, true)]
+ public async void RefreshImages_PopulatedItemPopulatedProviderRemote_UpdatesImagesIfForced(ImageType imageType, int imageCount, bool forceRefresh)
+ {
+ var item = GetItemWithImages(imageType, imageCount, false);
+
+ var libraryOptions = GetLibraryOptions(item, imageType, imageCount);
+
+ var remoteProvider = new Mock<IRemoteImageProvider>(MockBehavior.Strict);
+ remoteProvider.Setup(rp => rp.Name).Returns("MockRemoteProvider");
+ remoteProvider.Setup(rp => rp.GetSupportedImages(item))
+ .Returns(new[] { imageType });
+
+ var refreshOptions = forceRefresh
+ ? new ImageRefreshOptions(null)
+ {
+ ImageRefreshMode = MetadataRefreshMode.FullRefresh, ReplaceAllImages = true
+ }
+ : new ImageRefreshOptions(null);
+
+ var remoteInfo = new List<RemoteImageInfo>();
+ for (int i = 0; i < imageCount; i++)
+ {
+ remoteInfo.Add(new RemoteImageInfo
+ {
+ Type = imageType,
+ Url = "image url " + i,
+ Width = 1 // min width is set to 0, this will always pass
+ });
+ }
+
+ var providerManager = new Mock<IProviderManager>(MockBehavior.Strict);
+ providerManager.Setup(pm => pm.GetAvailableRemoteImages(It.IsAny<BaseItem>(), It.IsAny<RemoteImageQuery>(), It.IsAny<CancellationToken>()))
+ .ReturnsAsync(remoteInfo);
+ var itemImageProvider = GetItemImageProvider(providerManager.Object, new Mock<IFileSystem>());
+ var result = await itemImageProvider.RefreshImages(item, libraryOptions, new List<IImageProvider> { remoteProvider.Object }, refreshOptions, CancellationToken.None);
+
+ Assert.Equal(forceRefresh, result.UpdateType.HasFlag(ItemUpdateType.ImageUpdate));
+ Assert.Equal(imageCount, item.GetImages(imageType).Count());
+ foreach (var image in item.GetImages(imageType))
+ {
+ if (forceRefresh)
+ {
+ Assert.Matches(@"image url [0-9]", image.Path);
+ }
+ else
+ {
+ Assert.DoesNotMatch(@"image url [0-9]", image.Path);
+ }
+ }
+ }
+
+ [Theory]
+ [InlineData(ImageType.Primary, 0, false)] // singular type only fetches if type is missing from item, no caching
+ [InlineData(ImageType.Backdrop, 0, false)] // empty item, no cache to check
+ [InlineData(ImageType.Backdrop, 1, false)] // populated item, cached so no download
+ [InlineData(ImageType.Backdrop, 1, true)] // populated item, forced to download
+ public async void RefreshImages_NonStubItemPopulatedProviderRemote_DownloadsIfNecessary(ImageType imageType, int initialImageCount, bool fullRefresh)
+ {
+ var targetImageCount = 1;
+
+ // Set path and media source manager so images will be downloaded (EnableImageStub will return false)
+ var item = GetItemWithImages(imageType, initialImageCount, false);
+ item.Path = "non-empty path";
+ BaseItem.MediaSourceManager = Mock.Of<IMediaSourceManager>();
+
+ // seek 2 so it won't short-circuit out of downloading when populated
+ var libraryOptions = GetLibraryOptions(item, imageType, 2);
+
+ var content = "Content";
+ var remoteProvider = new Mock<IRemoteImageProvider>(MockBehavior.Strict);
+ remoteProvider.Setup(rp => rp.Name).Returns("MockRemoteProvider");
+ remoteProvider.Setup(rp => rp.GetSupportedImages(item))
+ .Returns(new[] { imageType });
+ remoteProvider.Setup(rp => rp.GetImageResponse(It.IsAny<string>(), It.IsAny<CancellationToken>()))
+ .ReturnsAsync((string url, CancellationToken _) => new HttpResponseMessage
+ {
+ ReasonPhrase = url,
+ StatusCode = HttpStatusCode.OK,
+ Content = new StringContent(content, Encoding.UTF8, "image/jpeg")
+ });
+
+ var refreshOptions = fullRefresh
+ ? new ImageRefreshOptions(null)
+ {
+ ImageRefreshMode = MetadataRefreshMode.FullRefresh,
+ ReplaceAllImages = true
+ }
+ : new ImageRefreshOptions(null);
+
+ var remoteInfo = new List<RemoteImageInfo>();
+ for (int i = 0; i < targetImageCount; i++)
+ {
+ remoteInfo.Add(new RemoteImageInfo
+ {
+ Type = imageType,
+ Url = "image url " + i,
+ Width = 1 // min width is set to 0, this will always pass
+ });
+ }
+
+ var providerManager = new Mock<IProviderManager>(MockBehavior.Strict);
+ providerManager.Setup(pm => pm.GetAvailableRemoteImages(It.IsAny<BaseItem>(), It.IsAny<RemoteImageQuery>(), It.IsAny<CancellationToken>()))
+ .ReturnsAsync(remoteInfo);
+ providerManager.Setup(pm => pm.SaveImage(item, It.IsAny<Stream>(), It.IsAny<string>(), imageType, null, It.IsAny<CancellationToken>()))
+ .Callback<BaseItem, Stream, string, ImageType, int?, CancellationToken>((callbackItem, _, _, callbackType, _, _) =>
+ callbackItem.SetImagePath(callbackType, callbackItem.AllowsMultipleImages(callbackType) ? callbackItem.GetImages(callbackType).Count() : 0, new FileSystemMetadata()))
+ .Returns(Task.CompletedTask);
+ var fileSystem = new Mock<IFileSystem>();
+ // match reported file size to image content length - condition for skipping already downloaded multi-images
+ fileSystem.Setup(fs => fs.GetFileInfo(It.IsAny<string>()))
+ .Returns(new FileSystemMetadata { Length = content.Length });
+ var itemImageProvider = GetItemImageProvider(providerManager.Object, fileSystem);
+ var result = await itemImageProvider.RefreshImages(item, libraryOptions, new List<IImageProvider> { remoteProvider.Object }, refreshOptions, CancellationToken.None);
+
+ Assert.Equal(initialImageCount == 0 || fullRefresh, result.UpdateType.HasFlag(ItemUpdateType.ImageUpdate));
+ Assert.Equal(targetImageCount, item.GetImages(imageType).Count());
+ }
+
+ [Theory]
+ [MemberData(nameof(GetImageTypesWithCount))]
+ public async void RefreshImages_EmptyItemPopulatedProviderRemoteExtras_LimitsImages(ImageType imageType, int imageCount)
+ {
+ var item = new MovieWithScreenshots();
+
+ var libraryOptions = GetLibraryOptions(item, imageType, imageCount);
+
+ var remoteProvider = new Mock<IRemoteImageProvider>(MockBehavior.Strict);
+ remoteProvider.Setup(rp => rp.Name).Returns("MockRemoteProvider");
+ remoteProvider.Setup(rp => rp.GetSupportedImages(item))
+ .Returns(new[] { imageType });
+
+ var refreshOptions = new ImageRefreshOptions(null);
+
+ // populate remote with double the required images to verify count is trimmed to the library option count
+ var remoteInfo = new List<RemoteImageInfo>();
+ for (int i = 0; i < imageCount * 2; i++)
+ {
+ remoteInfo.Add(new RemoteImageInfo
+ {
+ Type = imageType,
+ Url = "image url " + i,
+ Width = 1 // min width is set to 0, this will always pass
+ });
+ }
+
+ var providerManager = new Mock<IProviderManager>(MockBehavior.Strict);
+ providerManager.Setup(pm => pm.GetAvailableRemoteImages(It.IsAny<BaseItem>(), It.IsAny<RemoteImageQuery>(), It.IsAny<CancellationToken>()))
+ .ReturnsAsync(remoteInfo);
+ var itemImageProvider = GetItemImageProvider(providerManager.Object, null);
+ var result = await itemImageProvider.RefreshImages(item, libraryOptions, new List<IImageProvider> { remoteProvider.Object }, refreshOptions, CancellationToken.None);
+
+ Assert.True(result.UpdateType.HasFlag(ItemUpdateType.ImageUpdate));
+ var actualImages = item.GetImages(imageType).ToList();
+ Assert.Equal(imageCount, actualImages.Count);
+ // images from the provider manager are sorted by preference (earlier images are higher priority) so we can verify that low url numbers are chosen
+ foreach (var image in actualImages)
+ {
+ var index = int.Parse(Regex.Match(image.Path, @"[0-9]+").Value, NumberStyles.Integer, CultureInfo.InvariantCulture);
+ Assert.True(index < imageCount);
+ }
+ }
+
+ [Theory]
+ [MemberData(nameof(GetImageTypesWithCount))]
+ public async void RefreshImages_PopulatedItemEmptyProviderRemoteFullRefresh_DoesntClearImages(ImageType imageType, int imageCount)
+ {
+ var item = GetItemWithImages(imageType, imageCount, false);
+
+ var libraryOptions = GetLibraryOptions(item, imageType, imageCount);
+
+ var remoteProvider = new Mock<IRemoteImageProvider>(MockBehavior.Strict);
+ remoteProvider.Setup(rp => rp.Name).Returns("MockRemoteProvider");
+ remoteProvider.Setup(rp => rp.GetSupportedImages(item))
+ .Returns(new[] { imageType });
+
+ var refreshOptions = new ImageRefreshOptions(null)
+ {
+ ImageRefreshMode = MetadataRefreshMode.FullRefresh,
+ ReplaceAllImages = true
+ };
+
+ var itemImageProvider = GetItemImageProvider(Mock.Of<IProviderManager>(), null);
+ var result = await itemImageProvider.RefreshImages(item, libraryOptions, new List<IImageProvider> { remoteProvider.Object }, refreshOptions, CancellationToken.None);
+
+ Assert.False(result.UpdateType.HasFlag(ItemUpdateType.ImageUpdate));
+ Assert.Equal(imageCount, item.GetImages(imageType).Count());
+ }
+
+ private static ItemImageProvider GetItemImageProvider(IProviderManager? providerManager, Mock<IFileSystem>? mockFileSystem)
+ {
+ // strict to ensure this isn't accidentally used where a prepared mock is intended
+ providerManager ??= Mock.Of<IProviderManager>(MockBehavior.Strict);
+
+ // BaseItem.ValidateImages depends on the directory service being able to list directory contents, give it the expected valid file paths
+ mockFileSystem ??= new Mock<IFileSystem>(MockBehavior.Strict);
+ mockFileSystem.Setup(fs => fs.GetFilePaths(It.IsAny<string>(), It.IsAny<bool>()))
+ .Returns(new[]
+ {
+ string.Format(CultureInfo.InvariantCulture, TestDataImagePath, 0),
+ string.Format(CultureInfo.InvariantCulture, TestDataImagePath, 1)
+ });
+
+ return new ItemImageProvider(new NullLogger<ItemImageProvider>(), providerManager, mockFileSystem.Object);
+ }
+
+ private static BaseItem GetItemWithImages(ImageType type, int count, bool validPaths)
+ {
+ // Has to exist for querying DateModified time on file, results stored but not checked so not populating
+ BaseItem.FileSystem ??= Mock.Of<IFileSystem>();
+
+ var item = new MovieWithScreenshots();
+
+ var path = validPaths ? TestDataImagePath : "invalid path {0}";
+ for (int i = 0; i < count; i++)
+ {
+ item.SetImagePath(type, i, new FileSystemMetadata
+ {
+ FullName = string.Format(CultureInfo.InvariantCulture, path, i),
+ });
+ }
+
+ return item;
+ }
+
+ private static ILocalImageProvider GetImageProvider(ImageType type, int count, bool validPaths)
+ {
+ var images = GetImages(type, count, validPaths);
+
+ var imageProvider = new Mock<ILocalImageProvider>();
+ imageProvider.Setup(ip => ip.GetImages(It.IsAny<BaseItem>(), It.IsAny<IDirectoryService>()))
+ .Returns(images);
+ return imageProvider.Object;
+ }
+
+ /// <summary>
+ /// Creates a list of <see cref="LocalImageInfo"/> references of the specified type and size, optionally pointing to files that exist.
+ /// </summary>
+ private static List<LocalImageInfo> GetImages(ImageType type, int count, bool validPaths)
+ {
+ var path = validPaths ? TestDataImagePath : "invalid path {0}";
+ var images = new List<LocalImageInfo>(count);
+ for (int i = 0; i < count; i++)
+ {
+ images.Add(new LocalImageInfo
+ {
+ Type = type,
+ FileInfo = new FileSystemMetadata
+ {
+ FullName = string.Format(CultureInfo.InvariantCulture, path, i)
+ }
+ });
+ }
+
+ return images;
+ }
+
+ /// <summary>
+ /// Generates a <see cref="LibraryOptions"/> object that will allow for the requested number of images for the target type.
+ /// </summary>
+ private static LibraryOptions GetLibraryOptions(BaseItem item, ImageType type, int count)
+ {
+ return new LibraryOptions
+ {
+ TypeOptions = new[]
+ {
+ new TypeOptions
+ {
+ Type = item.GetType().Name,
+ ImageOptions = new[]
+ {
+ new ImageOption
+ {
+ Type = type,
+ Limit = count,
+ MinWidth = 0
+ }
+ }
+ }
+ }
+ };
+ }
+
+ // Create a class that implements IHasScreenshots for testing since no BaseItem class is also IHasScreenshots
+ private class MovieWithScreenshots : Movie, IHasScreenshots
+ {
+ // No contents
+ }
+ }
+}
diff --git a/tests/Jellyfin.Providers.Tests/Test Data/Images/blank0.jpg b/tests/Jellyfin.Providers.Tests/Test Data/Images/blank0.jpg
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/tests/Jellyfin.Providers.Tests/Test Data/Images/blank0.jpg
diff --git a/tests/Jellyfin.Providers.Tests/Test Data/Images/blank1.jpg b/tests/Jellyfin.Providers.Tests/Test Data/Images/blank1.jpg
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/tests/Jellyfin.Providers.Tests/Test Data/Images/blank1.jpg
diff --git a/tests/Jellyfin.Server.Implementations.Tests/Localization/LocalizationManagerTests.cs b/tests/Jellyfin.Server.Implementations.Tests/Localization/LocalizationManagerTests.cs
index 143020d43..3e7d6ed1d 100644
--- a/tests/Jellyfin.Server.Implementations.Tests/Localization/LocalizationManagerTests.cs
+++ b/tests/Jellyfin.Server.Implementations.Tests/Localization/LocalizationManagerTests.cs
@@ -40,7 +40,7 @@ namespace Jellyfin.Server.Implementations.Tests.Localization
await localizationManager.LoadAll();
var cultures = localizationManager.GetCultures().ToList();
- Assert.Equal(189, cultures.Count);
+ Assert.Equal(190, cultures.Count);
var germany = cultures.FirstOrDefault(x => x.TwoLetterISOLanguageName.Equals("de", StringComparison.Ordinal));
Assert.NotNull(germany);
diff --git a/tests/Jellyfin.Server.Integration.Tests/JellyfinApplicationFactory.cs b/tests/Jellyfin.Server.Integration.Tests/JellyfinApplicationFactory.cs
index 976e19d46..3d34a18e7 100644
--- a/tests/Jellyfin.Server.Integration.Tests/JellyfinApplicationFactory.cs
+++ b/tests/Jellyfin.Server.Integration.Tests/JellyfinApplicationFactory.cs
@@ -3,7 +3,6 @@ using System.Collections.Concurrent;
using System.IO;
using System.Threading;
using Emby.Server.Implementations;
-using Emby.Server.Implementations.IO;
using MediaBrowser.Common;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Mvc.Testing;
@@ -67,7 +66,7 @@ namespace Jellyfin.Server.Integration.Tests
var startupConfig = Program.CreateAppConfiguration(commandLineOpts, appPaths);
ILoggerFactory loggerFactory = new SerilogLoggerFactory();
- var serviceCollection = new ServiceCollection();
+
_disposableComponents.Add(loggerFactory);
// Create the app host and initialize it
@@ -75,11 +74,10 @@ namespace Jellyfin.Server.Integration.Tests
appPaths,
loggerFactory,
commandLineOpts,
- new ConfigurationBuilder().Build(),
- new ManagedFileSystem(loggerFactory.CreateLogger<ManagedFileSystem>(), appPaths),
- serviceCollection);
+ new ConfigurationBuilder().Build());
_disposableComponents.Add(appHost);
- appHost.Init();
+ var serviceCollection = new ServiceCollection();
+ appHost.Init(serviceCollection);
// Configure the web host builder
Program.ConfigureWebHostBuilder(builder, appHost, serviceCollection, commandLineOpts, startupConfig, appPaths);
diff --git a/tests/Jellyfin.Server.Integration.Tests/TestAppHost.cs b/tests/Jellyfin.Server.Integration.Tests/TestAppHost.cs
index 0a463cfa3..bf74efa09 100644
--- a/tests/Jellyfin.Server.Integration.Tests/TestAppHost.cs
+++ b/tests/Jellyfin.Server.Integration.Tests/TestAppHost.cs
@@ -2,9 +2,7 @@ using System.Collections.Generic;
using System.Reflection;
using Emby.Server.Implementations;
using MediaBrowser.Controller;
-using MediaBrowser.Model.IO;
using Microsoft.Extensions.Configuration;
-using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
namespace Jellyfin.Server.Integration.Tests
@@ -21,22 +19,16 @@ namespace Jellyfin.Server.Integration.Tests
/// <param name="loggerFactory">The <see cref="ILoggerFactory" /> to be used by the <see cref="CoreAppHost" />.</param>
/// <param name="options">The <see cref="StartupOptions" /> to be used by the <see cref="CoreAppHost" />.</param>
/// <param name="startup">The <see cref="IConfiguration" /> to be used by the <see cref="CoreAppHost" />.</param>
- /// <param name="fileSystem">The <see cref="IFileSystem" /> to be used by the <see cref="CoreAppHost" />.</param>
- /// <param name="collection">The <see cref="IServiceCollection"/> to be used by the <see cref="CoreAppHost"/>.</param>
public TestAppHost(
IServerApplicationPaths applicationPaths,
ILoggerFactory loggerFactory,
IStartupOptions options,
- IConfiguration startup,
- IFileSystem fileSystem,
- IServiceCollection collection)
+ IConfiguration startup)
: base(
applicationPaths,
loggerFactory,
options,
- startup,
- fileSystem,
- collection)
+ startup)
{
}