// // 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; } }