//
// Copyright (c) PlaceholderCompany. All rights reserved.
//
namespace Jellyfin.Server.Implementations.Extensions;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using Jellyfin.Database.Implementations;
using Jellyfin.Database.Implementations.DbConfiguration;
using Jellyfin.Database.Implementations.Locking;
using Jellyfin.Database.Providers.Postgres;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Controller.Configuration;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using JellyfinDbProviderFactory = System.Func;
///
/// Extensions for the interface.
///
public static class ServiceCollectionExtensions
{
///
/// Determines whether detailed EF Core logging should be enabled based on the configured log level.
///
/// The application configuration.
/// True if the log level for Microsoft.EntityFrameworkCore.Database.Command is Debug or lower; otherwise false.
private static bool ShouldEnableDetailedEFCoreLogging(IConfiguration configuration)
{
// Check if detailed EF Core logging is explicitly enabled in configuration
// Read the log level override for Entity Framework Core command logging
var efCoreLogLevelString = configuration["Serilog:MinimumLevel:Override:Microsoft.EntityFrameworkCore.Database.Command"];
if (string.IsNullOrWhiteSpace(efCoreLogLevelString))
{
// If not explicitly set, check the default minimum level
efCoreLogLevelString = configuration["Serilog:MinimumLevel:Default"] ?? "Information";
}
// Parse the log level string to LogLevel enum
if (Enum.TryParse(efCoreLogLevelString, ignoreCase: true, out var logLevel))
{
// Enable detailed logging if the level is Debug or lower (more verbose)
// LogLevel order: Trace=0, Debug=1, Information=2, Warning=3, Error=4, Critical=5, None=6
return logLevel <= LogLevel.Debug;
}
// Default to false if we can't parse the log level
return false;
}
private static IEnumerable DatabaseProviderTypes()
{
yield return typeof(PostgresDatabaseProvider);
// Add additional built-in providers here:
// yield return typeof(MySqlDatabaseProvider);
}
private static IDictionary GetSupportedDbProviders()
{
var items = new Dictionary(StringComparer.InvariantCultureIgnoreCase);
foreach (var providerType in DatabaseProviderTypes())
{
var keyAttribute = providerType.GetCustomAttribute();
if (keyAttribute is null || string.IsNullOrWhiteSpace(keyAttribute.DatabaseProviderKey))
{
continue;
}
var provider = providerType;
items[keyAttribute.DatabaseProviderKey] = (services) => (IJellyfinDatabaseProvider)ActivatorUtilities.CreateInstance(services, providerType);
}
return items;
}
private static JellyfinDbProviderFactory? LoadDatabasePlugin(CustomDatabaseOptions customProviderOptions, IApplicationPaths applicationPaths)
{
var plugin = Directory.EnumerateDirectories(applicationPaths.PluginsPath)
.Where(e => Path.GetFileName(e)!.StartsWith(customProviderOptions.PluginName, StringComparison.OrdinalIgnoreCase))
.Order()
.FirstOrDefault()
?? throw new InvalidOperationException($"The requested custom database plugin with the name '{customProviderOptions.PluginName}' could not been found in '{applicationPaths.PluginsPath}'");
var dbProviderAssembly = Path.Combine(plugin, Path.ChangeExtension(customProviderOptions.PluginAssembly, "dll"));
if (!File.Exists(dbProviderAssembly))
{
throw new InvalidOperationException($"Could not find the requested assembly at '{dbProviderAssembly}'");
}
// we have to load the assembly without proxy to ensure maximum performance for this.
var assembly = Assembly.LoadFrom(dbProviderAssembly);
var dbProviderType = assembly.GetExportedTypes().FirstOrDefault(f => f.IsAssignableTo(typeof(IJellyfinDatabaseProvider)))
?? throw new InvalidOperationException($"Could not find any type implementing the '{nameof(IJellyfinDatabaseProvider)}' interface.");
return (services) => (IJellyfinDatabaseProvider)ActivatorUtilities.CreateInstance(services, dbProviderType);
}
///
/// Adds the interface to the service collection with second level caching enabled.
///
/// An instance of the interface.
/// The server configuration manager.
/// The startup Configuration.
/// The updated service collection.
public static IServiceCollection AddJellyfinDbContext(
this IServiceCollection serviceCollection,
IServerConfigurationManager configurationManager,
IConfiguration configuration)
{
var efCoreConfiguration = configurationManager.GetConfiguration("database");
JellyfinDbProviderFactory? providerFactory = null;
if (efCoreConfiguration?.DatabaseType is null)
{
var cmdMigrationArgument = configuration.GetValue("migration-provider");
if (!string.IsNullOrWhiteSpace(cmdMigrationArgument))
{
efCoreConfiguration = new DatabaseConfigurationOptions()
{
DatabaseType = cmdMigrationArgument,
};
}
else
{
// PostgreSQL-only build: default to PostgreSQL when no database.xml exists.
efCoreConfiguration = new DatabaseConfigurationOptions()
{
DatabaseType = "Jellyfin-PostgreSQL",
LockingBehavior = DatabaseLockingBehaviorTypes.NoLock,
PresetConfigurations =
[
new PresetConfigurationItem
{
Key = "PostgreSQL-Local",
Value = new PresetDatabaseConfiguration
{
DatabaseType = "Jellyfin-PostgreSQL",
LockingBehavior = DatabaseLockingBehaviorTypes.NoLock,
Description = "PostgreSQL database on localhost. Update CustomProviderOptions with your database credentials."
}
}
]
};
configurationManager.SaveConfiguration("database", efCoreConfiguration);
}
}
// Ensure DatabaseType is set (handle corrupted or empty configuration files)
if (string.IsNullOrWhiteSpace(efCoreConfiguration.DatabaseType))
{
efCoreConfiguration.DatabaseType = "Jellyfin-PostgreSQL";
configurationManager.SaveConfiguration("database", efCoreConfiguration);
}
if (efCoreConfiguration.DatabaseType.Equals("PLUGIN_PROVIDER", StringComparison.OrdinalIgnoreCase))
{
if (efCoreConfiguration.CustomProviderOptions is null)
{
throw new InvalidOperationException("The custom database provider must declare the custom provider options to work");
}
providerFactory = LoadDatabasePlugin(efCoreConfiguration.CustomProviderOptions, configurationManager.ApplicationPaths);
}
else
{
var providers = GetSupportedDbProviders();
if (!providers.TryGetValue(efCoreConfiguration.DatabaseType.ToUpperInvariant(), out providerFactory!))
{
throw new InvalidOperationException($"Jellyfin cannot find the database provider of type '{efCoreConfiguration.DatabaseType}'. Supported types are {string.Join(", ", providers.Keys)}");
}
}
serviceCollection.AddSingleton(providerFactory!);
switch (efCoreConfiguration.LockingBehavior)
{
case DatabaseLockingBehaviorTypes.NoLock:
serviceCollection.AddSingleton();
break;
case DatabaseLockingBehaviorTypes.Pessimistic:
serviceCollection.AddSingleton();
break;
case DatabaseLockingBehaviorTypes.Optimistic:
serviceCollection.AddSingleton();
break;
}
serviceCollection.AddPooledDbContextFactory((serviceProvider, opt) =>
{
var provider = serviceProvider.GetRequiredService();
provider.Initialise(opt, efCoreConfiguration);
var lockingBehavior = serviceProvider.GetRequiredService();
lockingBehavior.Initialise(opt);
var loggerFactory = serviceProvider.GetService();
opt.UseLoggerFactory(loggerFactory);
// Only enable sensitive data logging and detailed errors if the EF Core command log level
// is set to Debug or lower, respecting the logging configuration from logging.json
if (ShouldEnableDetailedEFCoreLogging(configuration))
{
opt.EnableSensitiveDataLogging()
.EnableDetailedErrors();
}
});
return serviceCollection;
}
}