//
// 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 JellyfinDbProviderFactory = System.Func;
///
/// Extensions for the interface.
///
public static class ServiceCollectionExtensions
{
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)
.EnableSensitiveDataLogging()
.EnableDetailedErrors();
});
return serviceCollection;
}
}