2
0
Эх сурвалжийг харах

remove JellyfinDbProvider and add second level caching

cvium 2 жил өмнө
parent
commit
b836fe9685

+ 15 - 14
Emby.Server.Implementations/ScheduledTasks/Tasks/OptimizeDatabaseTask.cs

@@ -17,7 +17,7 @@ namespace Emby.Server.Implementations.ScheduledTasks.Tasks
     {
         private readonly ILogger<OptimizeDatabaseTask> _logger;
         private readonly ILocalizationManager _localization;
-        private readonly JellyfinDbProvider _provider;
+        private readonly IDbContextFactory<JellyfinDb> _provider;
 
         /// <summary>
         /// Initializes a new instance of the <see cref="OptimizeDatabaseTask" /> class.
@@ -28,7 +28,7 @@ namespace Emby.Server.Implementations.ScheduledTasks.Tasks
         public OptimizeDatabaseTask(
             ILogger<OptimizeDatabaseTask> logger,
             ILocalizationManager localization,
-            JellyfinDbProvider provider)
+            IDbContextFactory<JellyfinDb> provider)
         {
             _logger = logger;
             _localization = localization;
@@ -70,30 +70,31 @@ namespace Emby.Server.Implementations.ScheduledTasks.Tasks
         }
 
         /// <inheritdoc />
-        public Task ExecuteAsync(IProgress<double> progress, CancellationToken cancellationToken)
+        public async Task ExecuteAsync(IProgress<double> progress, CancellationToken cancellationToken)
         {
             _logger.LogInformation("Optimizing and vacuuming jellyfin.db...");
 
             try
             {
-                using var context = _provider.CreateContext();
-                if (context.Database.IsSqlite())
+                var context = await _provider.CreateDbContextAsync(cancellationToken).ConfigureAwait(false);
+                await using (context.ConfigureAwait(false))
                 {
-                    context.Database.ExecuteSqlRaw("PRAGMA optimize");
-                    context.Database.ExecuteSqlRaw("VACUUM");
-                    _logger.LogInformation("jellyfin.db optimized successfully!");
-                }
-                else
-                {
-                    _logger.LogInformation("This database doesn't support optimization");
+                    if (context.Database.IsSqlite())
+                    {
+                        await context.Database.ExecuteSqlRawAsync("PRAGMA optimize", cancellationToken).ConfigureAwait(false);
+                        await context.Database.ExecuteSqlRawAsync("VACUUM", cancellationToken).ConfigureAwait(false);
+                        _logger.LogInformation("jellyfin.db optimized successfully!");
+                    }
+                    else
+                    {
+                        _logger.LogInformation("This database doesn't support optimization");
+                    }
                 }
             }
             catch (Exception e)
             {
                 _logger.LogError(e, "Error while optimizing jellyfin.db");
             }
-
-            return Task.CompletedTask;
         }
     }
 }

+ 39 - 34
Jellyfin.Server.Implementations/Activity/ActivityManager.cs

@@ -15,13 +15,13 @@ namespace Jellyfin.Server.Implementations.Activity
     /// </summary>
     public class ActivityManager : IActivityManager
     {
-        private readonly JellyfinDbProvider _provider;
+        private readonly IDbContextFactory<JellyfinDb> _provider;
 
         /// <summary>
         /// Initializes a new instance of the <see cref="ActivityManager"/> class.
         /// </summary>
         /// <param name="provider">The Jellyfin database provider.</param>
-        public ActivityManager(JellyfinDbProvider provider)
+        public ActivityManager(IDbContextFactory<JellyfinDb> provider)
         {
             _provider = provider;
         }
@@ -32,10 +32,12 @@ namespace Jellyfin.Server.Implementations.Activity
         /// <inheritdoc/>
         public async Task CreateAsync(ActivityLog entry)
         {
-            await using var dbContext = _provider.CreateContext();
-
-            dbContext.ActivityLogs.Add(entry);
-            await dbContext.SaveChangesAsync().ConfigureAwait(false);
+            var dbContext = await _provider.CreateDbContextAsync().ConfigureAwait(false);
+            await using (dbContext.ConfigureAwait(false))
+            {
+                dbContext.ActivityLogs.Add(entry);
+                await dbContext.SaveChangesAsync().ConfigureAwait(false);
+            }
 
             EntryCreated?.Invoke(this, new GenericEventArgs<ActivityLogEntry>(ConvertToOldModel(entry)));
         }
@@ -43,44 +45,47 @@ namespace Jellyfin.Server.Implementations.Activity
         /// <inheritdoc/>
         public async Task<QueryResult<ActivityLogEntry>> GetPagedResultAsync(ActivityLogQuery query)
         {
-            await using var dbContext = _provider.CreateContext();
+            var dbContext = await _provider.CreateDbContextAsync().ConfigureAwait(false);
+            await using (dbContext.ConfigureAwait(false))
+            {
+                IQueryable<ActivityLog> entries = dbContext.ActivityLogs
+                    .OrderByDescending(entry => entry.DateCreated);
 
-            IQueryable<ActivityLog> entries = dbContext.ActivityLogs
-                .AsQueryable()
-                .OrderByDescending(entry => entry.DateCreated);
+                if (query.MinDate.HasValue)
+                {
+                    entries = entries.Where(entry => entry.DateCreated >= query.MinDate);
+                }
 
-            if (query.MinDate.HasValue)
-            {
-                entries = entries.Where(entry => entry.DateCreated >= query.MinDate);
-            }
+                if (query.HasUserId.HasValue)
+                {
+                    entries = entries.Where(entry => (!entry.UserId.Equals(default)) == query.HasUserId.Value);
+                }
 
-            if (query.HasUserId.HasValue)
-            {
-                entries = entries.Where(entry => (!entry.UserId.Equals(default)) == query.HasUserId.Value);
+                return new QueryResult<ActivityLogEntry>(
+                    query.Skip,
+                    await entries.CountAsync().ConfigureAwait(false),
+                    await entries
+                        .Skip(query.Skip ?? 0)
+                        .Take(query.Limit ?? 100)
+                        .AsAsyncEnumerable()
+                        .Select(ConvertToOldModel)
+                        .ToListAsync()
+                        .ConfigureAwait(false));
             }
-
-            return new QueryResult<ActivityLogEntry>(
-                query.Skip,
-                await entries.CountAsync().ConfigureAwait(false),
-                await entries
-                    .Skip(query.Skip ?? 0)
-                    .Take(query.Limit ?? 100)
-                    .AsAsyncEnumerable()
-                    .Select(ConvertToOldModel)
-                    .ToListAsync()
-                    .ConfigureAwait(false));
         }
 
         /// <inheritdoc />
         public async Task CleanAsync(DateTime startDate)
         {
-            await using var dbContext = _provider.CreateContext();
-            var entries = dbContext.ActivityLogs
-                .AsQueryable()
-                .Where(entry => entry.DateCreated <= startDate);
+            var dbContext = await _provider.CreateDbContextAsync().ConfigureAwait(false);
+            await using (dbContext.ConfigureAwait(false))
+            {
+                var entries = dbContext.ActivityLogs
+                    .Where(entry => entry.DateCreated <= startDate);
 
-            dbContext.RemoveRange(entries);
-            await dbContext.SaveChangesAsync().ConfigureAwait(false);
+                dbContext.RemoveRange(entries);
+                await dbContext.SaveChangesAsync().ConfigureAwait(false);
+            }
         }
 
         private static ActivityLogEntry ConvertToOldModel(ActivityLog entry)

+ 94 - 74
Jellyfin.Server.Implementations/Devices/DeviceManager.cs

@@ -1,5 +1,6 @@
 using System;
 using System.Collections.Concurrent;
+using System.Collections.Generic;
 using System.Linq;
 using System.Threading.Tasks;
 using Jellyfin.Data.Entities;
@@ -22,7 +23,7 @@ namespace Jellyfin.Server.Implementations.Devices
     /// </summary>
     public class DeviceManager : IDeviceManager
     {
-        private readonly JellyfinDbProvider _dbProvider;
+        private readonly IDbContextFactory<JellyfinDb> _dbProvider;
         private readonly IUserManager _userManager;
         private readonly ConcurrentDictionary<string, ClientCapabilities> _capabilitiesMap = new();
 
@@ -31,7 +32,7 @@ namespace Jellyfin.Server.Implementations.Devices
         /// </summary>
         /// <param name="dbProvider">The database provider.</param>
         /// <param name="userManager">The user manager.</param>
-        public DeviceManager(JellyfinDbProvider dbProvider, IUserManager userManager)
+        public DeviceManager(IDbContextFactory<JellyfinDb> dbProvider, IUserManager userManager)
         {
             _dbProvider = dbProvider;
             _userManager = userManager;
@@ -49,39 +50,50 @@ namespace Jellyfin.Server.Implementations.Devices
         /// <inheritdoc />
         public async Task UpdateDeviceOptions(string deviceId, string deviceName)
         {
-            await using var dbContext = _dbProvider.CreateContext();
-            var deviceOptions = await dbContext.DeviceOptions.AsQueryable().FirstOrDefaultAsync(dev => dev.DeviceId == deviceId).ConfigureAwait(false);
-            if (deviceOptions == null)
+            DeviceOptions? deviceOptions;
+            var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false);
+            await using (dbContext.ConfigureAwait(false))
             {
-                deviceOptions = new DeviceOptions(deviceId);
-                dbContext.DeviceOptions.Add(deviceOptions);
+                deviceOptions = await dbContext.DeviceOptions.AsQueryable().FirstOrDefaultAsync(dev => dev.DeviceId == deviceId).ConfigureAwait(false);
+                if (deviceOptions == null)
+                {
+                    deviceOptions = new DeviceOptions(deviceId);
+                    dbContext.DeviceOptions.Add(deviceOptions);
+                }
+
+                deviceOptions.CustomName = deviceName;
+                await dbContext.SaveChangesAsync().ConfigureAwait(false);
             }
 
-            deviceOptions.CustomName = deviceName;
-            await dbContext.SaveChangesAsync().ConfigureAwait(false);
-
             DeviceOptionsUpdated?.Invoke(this, new GenericEventArgs<Tuple<string, DeviceOptions>>(new Tuple<string, DeviceOptions>(deviceId, deviceOptions)));
         }
 
         /// <inheritdoc />
         public async Task<Device> CreateDevice(Device device)
         {
-            await using var dbContext = _dbProvider.CreateContext();
+            var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false);
+            await using (dbContext.ConfigureAwait(false))
+            {
+                dbContext.Devices.Add(device);
 
-            dbContext.Devices.Add(device);
+                await dbContext.SaveChangesAsync().ConfigureAwait(false);
+            }
 
-            await dbContext.SaveChangesAsync().ConfigureAwait(false);
             return device;
         }
 
         /// <inheritdoc />
         public async Task<DeviceOptions> GetDeviceOptions(string deviceId)
         {
-            await using var dbContext = _dbProvider.CreateContext();
-            var deviceOptions = await dbContext.DeviceOptions
-                .AsQueryable()
-                .FirstOrDefaultAsync(d => d.DeviceId == deviceId)
-                .ConfigureAwait(false);
+            var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false);
+            DeviceOptions? deviceOptions;
+            await using (dbContext.ConfigureAwait(false))
+            {
+                deviceOptions = await dbContext.DeviceOptions
+                    .AsNoTracking()
+                    .FirstOrDefaultAsync(d => d.DeviceId == deviceId)
+                    .ConfigureAwait(false);
+            }
 
             return deviceOptions ?? new DeviceOptions(deviceId);
         }
@@ -97,14 +109,17 @@ namespace Jellyfin.Server.Implementations.Devices
         /// <inheritdoc />
         public async Task<DeviceInfo?> GetDevice(string id)
         {
-            await using var dbContext = _dbProvider.CreateContext();
-            var device = await dbContext.Devices
-                .AsQueryable()
-                .Where(d => d.DeviceId == id)
-                .OrderByDescending(d => d.DateLastActivity)
-                .Include(d => d.User)
-                .FirstOrDefaultAsync()
-                .ConfigureAwait(false);
+            Device? device;
+            var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false);
+            await using (dbContext.ConfigureAwait(false))
+            {
+                device = await dbContext.Devices
+                    .Where(d => d.DeviceId == id)
+                    .OrderByDescending(d => d.DateLastActivity)
+                    .Include(d => d.User)
+                    .FirstOrDefaultAsync()
+                    .ConfigureAwait(false);
+            }
 
             var deviceInfo = device == null ? null : ToDeviceInfo(device);
 
@@ -114,41 +129,40 @@ namespace Jellyfin.Server.Implementations.Devices
         /// <inheritdoc />
         public async Task<QueryResult<Device>> GetDevices(DeviceQuery query)
         {
-            await using var dbContext = _dbProvider.CreateContext();
+            var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false);
+            await using (dbContext.ConfigureAwait(false))
+            {
+                var devices = dbContext.Devices.AsQueryable();
 
-            var devices = dbContext.Devices.AsQueryable();
+                if (query.UserId.HasValue)
+                {
+                    devices = devices.Where(device => device.UserId.Equals(query.UserId.Value));
+                }
 
-            if (query.UserId.HasValue)
-            {
-                devices = devices.Where(device => device.UserId.Equals(query.UserId.Value));
-            }
+                if (query.DeviceId != null)
+                {
+                    devices = devices.Where(device => device.DeviceId == query.DeviceId);
+                }
 
-            if (query.DeviceId != null)
-            {
-                devices = devices.Where(device => device.DeviceId == query.DeviceId);
-            }
+                if (query.AccessToken != null)
+                {
+                    devices = devices.Where(device => device.AccessToken == query.AccessToken);
+                }
 
-            if (query.AccessToken != null)
-            {
-                devices = devices.Where(device => device.AccessToken == query.AccessToken);
-            }
+                var count = await devices.CountAsync().ConfigureAwait(false);
 
-            var count = await devices.CountAsync().ConfigureAwait(false);
+                if (query.Skip.HasValue)
+                {
+                    devices = devices.Skip(query.Skip.Value);
+                }
 
-            if (query.Skip.HasValue)
-            {
-                devices = devices.Skip(query.Skip.Value);
-            }
+                if (query.Limit.HasValue)
+                {
+                    devices = devices.Take(query.Limit.Value);
+                }
 
-            if (query.Limit.HasValue)
-            {
-                devices = devices.Take(query.Limit.Value);
+                return new QueryResult<Device>(query.Skip, count, await devices.ToListAsync().ConfigureAwait(false));
             }
-
-            return new QueryResult<Device>(
-                query.Skip,
-                count,
-                await devices.ToListAsync().ConfigureAwait(false));
         }
 
         /// <inheritdoc />
@@ -165,37 +179,43 @@ namespace Jellyfin.Server.Implementations.Devices
         /// <inheritdoc />
         public async Task<QueryResult<DeviceInfo>> GetDevicesForUser(Guid? userId, bool? supportsSync)
         {
-            await using var dbContext = _dbProvider.CreateContext();
-            var sessions = dbContext.Devices
-                .Include(d => d.User)
-                .AsQueryable()
-                .OrderByDescending(d => d.DateLastActivity)
-                .ThenBy(d => d.DeviceId)
-                .AsAsyncEnumerable();
-
-            if (supportsSync.HasValue)
+            IAsyncEnumerable<Device> sessions;
+            var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false);
+            await using (dbContext.ConfigureAwait(false))
             {
-                sessions = sessions.Where(i => GetCapabilities(i.DeviceId).SupportsSync == supportsSync.Value);
-            }
+                sessions = dbContext.Devices
+                    .Include(d => d.User)
+                    .OrderByDescending(d => d.DateLastActivity)
+                    .ThenBy(d => d.DeviceId)
+                    .AsAsyncEnumerable();
 
-            if (userId.HasValue)
-            {
-                var user = _userManager.GetUserById(userId.Value);
+                if (supportsSync.HasValue)
+                {
+                    sessions = sessions.Where(i => GetCapabilities(i.DeviceId).SupportsSync == supportsSync.Value);
+                }
 
-                sessions = sessions.Where(i => CanAccessDevice(user, i.DeviceId));
-            }
+                if (userId.HasValue)
+                {
+                    var user = _userManager.GetUserById(userId.Value);
+
+                    sessions = sessions.Where(i => CanAccessDevice(user, i.DeviceId));
+                }
 
-            var array = await sessions.Select(device => ToDeviceInfo(device)).ToArrayAsync().ConfigureAwait(false);
+                var array = await sessions.Select(device => ToDeviceInfo(device)).ToArrayAsync().ConfigureAwait(false);
 
-            return new QueryResult<DeviceInfo>(array);
+                return new QueryResult<DeviceInfo>(array);
+            }
         }
 
         /// <inheritdoc />
         public async Task DeleteDevice(Device device)
         {
-            await using var dbContext = _dbProvider.CreateContext();
-            dbContext.Devices.Remove(device);
-            await dbContext.SaveChangesAsync().ConfigureAwait(false);
+            var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false);
+            await using (dbContext.ConfigureAwait(false))
+            {
+                dbContext.Devices.Remove(device);
+                await dbContext.SaveChangesAsync().ConfigureAwait(false);
+            }
         }
 
         /// <inheritdoc />

+ 45 - 0
Jellyfin.Server.Implementations/Extensions/ServiceCollectionExtensions.cs

@@ -0,0 +1,45 @@
+using System;
+using System.IO;
+using EFCoreSecondLevelCacheInterceptor;
+using MediaBrowser.Common.Configuration;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Logging;
+
+namespace Jellyfin.Server.Implementations.Extensions;
+
+/// <summary>
+/// Extensions for the <see cref="IServiceCollection"/> interface.
+/// </summary>
+public static class ServiceCollectionExtensions
+{
+    /// <summary>
+    /// Adds the <see cref="IDbContextFactory{TContext}"/> interface to the service collection with second level caching enabled.
+    /// </summary>
+    /// <param name="serviceCollection">An instance of the <see cref="IServiceCollection"/> interface.</param>
+    /// <returns>The updated service collection.</returns>
+    public static IServiceCollection AddJellyfinDbContext(this IServiceCollection serviceCollection)
+    {
+        serviceCollection.AddEFSecondLevelCache(options =>
+            options.UseMemoryCacheProvider()
+                .CacheAllQueries(CacheExpirationMode.Sliding, TimeSpan.FromMinutes(10))
+                .DisableLogging(true)
+                .UseCacheKeyPrefix("EF_")
+                .SkipCachingCommands(commandText =>
+                    commandText.Contains("NEWID()", StringComparison.InvariantCultureIgnoreCase))
+                // Don't cache null values. Remove this optional setting if it's not necessary.
+                .SkipCachingResults(result =>
+                    result.Value == null || (result.Value is EFTableRows rows && rows.RowsCount == 0)));
+
+        serviceCollection.AddPooledDbContextFactory<JellyfinDb>((serviceProvider, opt) =>
+        {
+            var applicationPaths = serviceProvider.GetRequiredService<IApplicationPaths>();
+            var loggerFactory = serviceProvider.GetRequiredService<ILoggerFactory>();
+            opt.UseSqlite($"Filename={Path.Combine(applicationPaths.DataPath, "jellyfin.db")}")
+                .AddInterceptors(serviceProvider.GetRequiredService<SecondLevelCacheInterceptor>())
+                .UseLoggerFactory(loggerFactory);
+        });
+
+        return serviceCollection;
+    }
+}

+ 1 - 0
Jellyfin.Server.Implementations/Jellyfin.Server.Implementations.csproj

@@ -26,6 +26,7 @@
   </ItemGroup>
 
   <ItemGroup>
+    <PackageReference Include="EFCoreSecondLevelCacheInterceptor" Version="3.7.3" />
     <PackageReference Include="System.Linq.Async" Version="6.0.1" />
     <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="6.0.9" />
     <PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="6.0.9" />

+ 0 - 51
Jellyfin.Server.Implementations/JellyfinDbProvider.cs

@@ -1,51 +0,0 @@
-using System;
-using System.IO;
-using System.Linq;
-using MediaBrowser.Common.Configuration;
-using Microsoft.EntityFrameworkCore;
-using Microsoft.Extensions.DependencyInjection;
-using Microsoft.Extensions.Logging;
-
-namespace Jellyfin.Server.Implementations
-{
-    /// <summary>
-    /// Factory class for generating new <see cref="JellyfinDb"/> instances.
-    /// </summary>
-    public class JellyfinDbProvider
-    {
-        private readonly IServiceProvider _serviceProvider;
-        private readonly IApplicationPaths _appPaths;
-        private readonly ILogger<JellyfinDbProvider> _logger;
-
-        /// <summary>
-        /// Initializes a new instance of the <see cref="JellyfinDbProvider"/> class.
-        /// </summary>
-        /// <param name="serviceProvider">The application's service provider.</param>
-        /// <param name="appPaths">The application paths.</param>
-        /// <param name="logger">The logger.</param>
-        public JellyfinDbProvider(IServiceProvider serviceProvider, IApplicationPaths appPaths, ILogger<JellyfinDbProvider> logger)
-        {
-            _serviceProvider = serviceProvider;
-            _appPaths = appPaths;
-            _logger = logger;
-
-            using var jellyfinDb = CreateContext();
-            if (jellyfinDb.Database.GetPendingMigrations().Any())
-            {
-                _logger.LogInformation("There are pending EFCore migrations in the database. Applying... (This may take a while, do not stop Jellyfin)");
-                jellyfinDb.Database.Migrate();
-                _logger.LogInformation("EFCore migrations applied successfully");
-            }
-        }
-
-        /// <summary>
-        /// Creates a new <see cref="JellyfinDb"/> context.
-        /// </summary>
-        /// <returns>The newly created context.</returns>
-        public JellyfinDb CreateContext()
-        {
-            var contextOptions = new DbContextOptionsBuilder<JellyfinDb>().UseSqlite($"Filename={Path.Combine(_appPaths.DataPath, "jellyfin.db")}");
-            return ActivatorUtilities.CreateInstance<JellyfinDb>(_serviceProvider, contextOptions.Options);
-        }
-    }
-}

+ 38 - 32
Jellyfin.Server.Implementations/Security/AuthenticationManager.cs

@@ -10,13 +10,13 @@ namespace Jellyfin.Server.Implementations.Security
     /// <inheritdoc />
     public class AuthenticationManager : IAuthenticationManager
     {
-        private readonly JellyfinDbProvider _dbProvider;
+        private readonly IDbContextFactory<JellyfinDb> _dbProvider;
 
         /// <summary>
         /// Initializes a new instance of the <see cref="AuthenticationManager"/> class.
         /// </summary>
         /// <param name="dbProvider">The database provider.</param>
-        public AuthenticationManager(JellyfinDbProvider dbProvider)
+        public AuthenticationManager(IDbContextFactory<JellyfinDb> dbProvider)
         {
             _dbProvider = dbProvider;
         }
@@ -24,50 +24,56 @@ namespace Jellyfin.Server.Implementations.Security
         /// <inheritdoc />
         public async Task CreateApiKey(string name)
         {
-            await using var dbContext = _dbProvider.CreateContext();
-
-            dbContext.ApiKeys.Add(new ApiKey(name));
+            var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false);
+            await using (dbContext.ConfigureAwait(false))
+            {
+                dbContext.ApiKeys.Add(new ApiKey(name));
 
-            await dbContext.SaveChangesAsync().ConfigureAwait(false);
+                await dbContext.SaveChangesAsync().ConfigureAwait(false);
+            }
         }
 
         /// <inheritdoc />
         public async Task<IReadOnlyList<AuthenticationInfo>> GetApiKeys()
         {
-            await using var dbContext = _dbProvider.CreateContext();
-
-            return await dbContext.ApiKeys
-                .AsAsyncEnumerable()
-                .Select(key => new AuthenticationInfo
-                {
-                    AppName = key.Name,
-                    AccessToken = key.AccessToken,
-                    DateCreated = key.DateCreated,
-                    DeviceId = string.Empty,
-                    DeviceName = string.Empty,
-                    AppVersion = string.Empty
-                }).ToListAsync().ConfigureAwait(false);
+            var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false);
+            await using (dbContext.ConfigureAwait(false))
+            {
+                return await dbContext.ApiKeys
+                    .AsAsyncEnumerable()
+                    .Select(key => new AuthenticationInfo
+                    {
+                        AppName = key.Name,
+                        AccessToken = key.AccessToken,
+                        DateCreated = key.DateCreated,
+                        DeviceId = string.Empty,
+                        DeviceName = string.Empty,
+                        AppVersion = string.Empty
+                    }).ToListAsync().ConfigureAwait(false);
+            }
         }
 
         /// <inheritdoc />
         public async Task DeleteApiKey(string accessToken)
         {
-            await using var dbContext = _dbProvider.CreateContext();
-
-            var key = await dbContext.ApiKeys
-                .AsQueryable()
-                .Where(apiKey => apiKey.AccessToken == accessToken)
-                .FirstOrDefaultAsync()
-                .ConfigureAwait(false);
-
-            if (key == null)
+            var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false);
+            await using (dbContext.ConfigureAwait(false))
             {
-                return;
-            }
+                var key = await dbContext.ApiKeys
+                    .AsQueryable()
+                    .Where(apiKey => apiKey.AccessToken == accessToken)
+                    .FirstOrDefaultAsync()
+                    .ConfigureAwait(false);
 
-            dbContext.Remove(key);
+                if (key == null)
+                {
+                    return;
+                }
+
+                dbContext.Remove(key);
 
-            await dbContext.SaveChangesAsync().ConfigureAwait(false);
+                await dbContext.SaveChangesAsync().ConfigureAwait(false);
+            }
         }
     }
 }

+ 73 - 69
Jellyfin.Server.Implementations/Security/AuthorizationContext.cs

@@ -4,6 +4,7 @@ using System;
 using System.Collections.Generic;
 using System.Net;
 using System.Threading.Tasks;
+using EFCoreSecondLevelCacheInterceptor;
 using MediaBrowser.Controller;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.Net;
@@ -15,12 +16,12 @@ namespace Jellyfin.Server.Implementations.Security
 {
     public class AuthorizationContext : IAuthorizationContext
     {
-        private readonly JellyfinDbProvider _jellyfinDbProvider;
+        private readonly IDbContextFactory<JellyfinDb> _jellyfinDbProvider;
         private readonly IUserManager _userManager;
         private readonly IServerApplicationHost _serverApplicationHost;
 
         public AuthorizationContext(
-            JellyfinDbProvider jellyfinDb,
+            IDbContextFactory<JellyfinDb> jellyfinDb,
             IUserManager userManager,
             IServerApplicationHost serverApplicationHost)
         {
@@ -121,96 +122,99 @@ namespace Jellyfin.Server.Implementations.Security
 #pragma warning restore CA1508
 
             authInfo.HasToken = true;
-            await using var dbContext = _jellyfinDbProvider.CreateContext();
-            var device = await dbContext.Devices.FirstOrDefaultAsync(d => d.AccessToken == token).ConfigureAwait(false);
-
-            if (device != null)
+            var dbContext = await _jellyfinDbProvider.CreateDbContextAsync().ConfigureAwait(false);
+            await using (dbContext.ConfigureAwait(false))
             {
-                authInfo.IsAuthenticated = true;
-                var updateToken = false;
-
-                // TODO: Remove these checks for IsNullOrWhiteSpace
-                if (string.IsNullOrWhiteSpace(authInfo.Client))
-                {
-                    authInfo.Client = device.AppName;
-                }
+                var device = await dbContext.Devices.FirstOrDefaultAsync(d => d.AccessToken == token).ConfigureAwait(false);
 
-                if (string.IsNullOrWhiteSpace(authInfo.DeviceId))
+                if (device != null)
                 {
-                    authInfo.DeviceId = device.DeviceId;
-                }
-
-                // Temporary. TODO - allow clients to specify that the token has been shared with a casting device
-                var allowTokenInfoUpdate = !authInfo.Client.Contains("chromecast", StringComparison.OrdinalIgnoreCase);
+                    authInfo.IsAuthenticated = true;
+                    var updateToken = false;
 
-                if (string.IsNullOrWhiteSpace(authInfo.Device))
-                {
-                    authInfo.Device = device.DeviceName;
-                }
-                else if (!string.Equals(authInfo.Device, device.DeviceName, StringComparison.OrdinalIgnoreCase))
-                {
-                    if (allowTokenInfoUpdate)
+                    // TODO: Remove these checks for IsNullOrWhiteSpace
+                    if (string.IsNullOrWhiteSpace(authInfo.Client))
                     {
-                        updateToken = true;
-                        device.DeviceName = authInfo.Device;
+                        authInfo.Client = device.AppName;
                     }
-                }
 
-                if (string.IsNullOrWhiteSpace(authInfo.Version))
-                {
-                    authInfo.Version = device.AppVersion;
-                }
-                else if (!string.Equals(authInfo.Version, device.AppVersion, StringComparison.OrdinalIgnoreCase))
-                {
-                    if (allowTokenInfoUpdate)
+                    if (string.IsNullOrWhiteSpace(authInfo.DeviceId))
                     {
-                        updateToken = true;
-                        device.AppVersion = authInfo.Version;
+                        authInfo.DeviceId = device.DeviceId;
                     }
-                }
 
-                if ((DateTime.UtcNow - device.DateLastActivity).TotalMinutes > 3)
-                {
-                    device.DateLastActivity = DateTime.UtcNow;
-                    updateToken = true;
-                }
+                    // Temporary. TODO - allow clients to specify that the token has been shared with a casting device
+                    var allowTokenInfoUpdate = !authInfo.Client.Contains("chromecast", StringComparison.OrdinalIgnoreCase);
 
-                authInfo.User = _userManager.GetUserById(device.UserId);
+                    if (string.IsNullOrWhiteSpace(authInfo.Device))
+                    {
+                        authInfo.Device = device.DeviceName;
+                    }
+                    else if (!string.Equals(authInfo.Device, device.DeviceName, StringComparison.OrdinalIgnoreCase))
+                    {
+                        if (allowTokenInfoUpdate)
+                        {
+                            updateToken = true;
+                            device.DeviceName = authInfo.Device;
+                        }
+                    }
 
-                if (updateToken)
-                {
-                    dbContext.Devices.Update(device);
-                    await dbContext.SaveChangesAsync().ConfigureAwait(false);
-                }
-            }
-            else
-            {
-                var key = await dbContext.ApiKeys.FirstOrDefaultAsync(apiKey => apiKey.AccessToken == token).ConfigureAwait(false);
-                if (key != null)
-                {
-                    authInfo.IsAuthenticated = true;
-                    authInfo.Client = key.Name;
-                    authInfo.Token = key.AccessToken;
-                    if (string.IsNullOrWhiteSpace(authInfo.DeviceId))
+                    if (string.IsNullOrWhiteSpace(authInfo.Version))
                     {
-                        authInfo.DeviceId = _serverApplicationHost.SystemId;
+                        authInfo.Version = device.AppVersion;
+                    }
+                    else if (!string.Equals(authInfo.Version, device.AppVersion, StringComparison.OrdinalIgnoreCase))
+                    {
+                        if (allowTokenInfoUpdate)
+                        {
+                            updateToken = true;
+                            device.AppVersion = authInfo.Version;
+                        }
                     }
 
-                    if (string.IsNullOrWhiteSpace(authInfo.Device))
+                    if ((DateTime.UtcNow - device.DateLastActivity).TotalMinutes > 3)
                     {
-                        authInfo.Device = _serverApplicationHost.Name;
+                        device.DateLastActivity = DateTime.UtcNow;
+                        updateToken = true;
                     }
 
-                    if (string.IsNullOrWhiteSpace(authInfo.Version))
+                    authInfo.User = _userManager.GetUserById(device.UserId);
+
+                    if (updateToken)
                     {
-                        authInfo.Version = _serverApplicationHost.ApplicationVersionString;
+                        dbContext.Devices.Update(device);
+                        await dbContext.SaveChangesAsync().ConfigureAwait(false);
                     }
+                }
+                else
+                {
+                    var key = await dbContext.ApiKeys.FirstOrDefaultAsync(apiKey => apiKey.AccessToken == token).ConfigureAwait(false);
+                    if (key != null)
+                    {
+                        authInfo.IsAuthenticated = true;
+                        authInfo.Client = key.Name;
+                        authInfo.Token = key.AccessToken;
+                        if (string.IsNullOrWhiteSpace(authInfo.DeviceId))
+                        {
+                            authInfo.DeviceId = _serverApplicationHost.SystemId;
+                        }
+
+                        if (string.IsNullOrWhiteSpace(authInfo.Device))
+                        {
+                            authInfo.Device = _serverApplicationHost.Name;
+                        }
+
+                        if (string.IsNullOrWhiteSpace(authInfo.Version))
+                        {
+                            authInfo.Version = _serverApplicationHost.ApplicationVersionString;
+                        }
 
-                    authInfo.IsApiKey = true;
+                        authInfo.IsApiKey = true;
+                    }
                 }
-            }
 
-            return authInfo;
+                return authInfo;
+            }
         }
 
         /// <summary>

+ 3 - 3
Jellyfin.Server.Implementations/Users/DisplayPreferencesManager.cs

@@ -20,10 +20,10 @@ namespace Jellyfin.Server.Implementations.Users
         /// <summary>
         /// Initializes a new instance of the <see cref="DisplayPreferencesManager"/> class.
         /// </summary>
-        /// <param name="dbContext">The database context.</param>
-        public DisplayPreferencesManager(JellyfinDb dbContext)
+        /// <param name="dbContextFactory">The database context factory.</param>
+        public DisplayPreferencesManager(IDbContextFactory<JellyfinDb> dbContextFactory)
         {
-            _dbContext = dbContext;
+            _dbContext = dbContextFactory.CreateDbContext();
         }
 
         /// <inheritdoc />

+ 162 - 131
Jellyfin.Server.Implementations/Users/UserManager.cs

@@ -33,7 +33,7 @@ namespace Jellyfin.Server.Implementations.Users
     /// </summary>
     public class UserManager : IUserManager
     {
-        private readonly JellyfinDbProvider _dbProvider;
+        private readonly IDbContextFactory<JellyfinDb> _dbProvider;
         private readonly IEventManager _eventManager;
         private readonly ICryptoProvider _cryptoProvider;
         private readonly INetworkManager _networkManager;
@@ -59,7 +59,7 @@ namespace Jellyfin.Server.Implementations.Users
         /// <param name="imageProcessor">The image processor.</param>
         /// <param name="logger">The logger.</param>
         public UserManager(
-            JellyfinDbProvider dbProvider,
+            IDbContextFactory<JellyfinDb> dbProvider,
             IEventManager eventManager,
             ICryptoProvider cryptoProvider,
             INetworkManager networkManager,
@@ -83,7 +83,7 @@ namespace Jellyfin.Server.Implementations.Users
             _defaultPasswordResetProvider = _passwordResetProviders.OfType<DefaultPasswordResetProvider>().First();
 
             _users = new ConcurrentDictionary<Guid, User>();
-            using var dbContext = _dbProvider.CreateContext();
+            using var dbContext = _dbProvider.CreateDbContext();
             foreach (var user in dbContext.Users
                 .Include(user => user.Permissions)
                 .Include(user => user.Preferences)
@@ -139,31 +139,35 @@ namespace Jellyfin.Server.Implementations.Users
                 throw new ArgumentException("The new and old names must be different.");
             }
 
-            await using var dbContext = _dbProvider.CreateContext();
-
-            if (await dbContext.Users
-                .AsQueryable()
-                .AnyAsync(u => u.Username == newName && !u.Id.Equals(user.Id))
-                .ConfigureAwait(false))
+            var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false);
+            await using (dbContext.ConfigureAwait(false))
             {
-                throw new ArgumentException(string.Format(
-                    CultureInfo.InvariantCulture,
-                    "A user with the name '{0}' already exists.",
-                    newName));
+                if (await dbContext.Users
+                        .AsQueryable()
+                        .AnyAsync(u => u.Username == newName && !u.Id.Equals(user.Id))
+                        .ConfigureAwait(false))
+                {
+                    throw new ArgumentException(string.Format(
+                        CultureInfo.InvariantCulture,
+                        "A user with the name '{0}' already exists.",
+                        newName));
+                }
+
+                user.Username = newName;
+                await UpdateUserInternalAsync(dbContext, user).ConfigureAwait(false);
             }
 
-            user.Username = newName;
-            await UpdateUserAsync(user).ConfigureAwait(false);
             OnUserUpdated?.Invoke(this, new GenericEventArgs<User>(user));
         }
 
         /// <inheritdoc/>
         public async Task UpdateUserAsync(User user)
         {
-            await using var dbContext = _dbProvider.CreateContext();
-            dbContext.Users.Update(user);
-            _users[user.Id] = user;
-            await dbContext.SaveChangesAsync().ConfigureAwait(false);
+            var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false);
+            await using (dbContext.ConfigureAwait(false))
+            {
+                await UpdateUserInternalAsync(dbContext, user).ConfigureAwait(false);
+            }
         }
 
         internal async Task<User> CreateUserInternalAsync(string name, JellyfinDb dbContext)
@@ -202,12 +206,15 @@ namespace Jellyfin.Server.Implementations.Users
                     name));
             }
 
-            await using var dbContext = _dbProvider.CreateContext();
-
-            var newUser = await CreateUserInternalAsync(name, dbContext).ConfigureAwait(false);
+            User newUser;
+            var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false);
+            await using (dbContext.ConfigureAwait(false))
+            {
+                newUser = await CreateUserInternalAsync(name, dbContext).ConfigureAwait(false);
 
-            dbContext.Users.Add(newUser);
-            await dbContext.SaveChangesAsync().ConfigureAwait(false);
+                dbContext.Users.Add(newUser);
+                await dbContext.SaveChangesAsync().ConfigureAwait(false);
+            }
 
             await _eventManager.PublishAsync(new UserCreatedEventArgs(newUser)).ConfigureAwait(false);
 
@@ -241,9 +248,13 @@ namespace Jellyfin.Server.Implementations.Users
                     nameof(userId));
             }
 
-            await using var dbContext = _dbProvider.CreateContext();
-            dbContext.Users.Remove(user);
-            await dbContext.SaveChangesAsync().ConfigureAwait(false);
+            var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false);
+            await using (dbContext.ConfigureAwait(false))
+            {
+                dbContext.Users.Remove(user);
+                await dbContext.SaveChangesAsync().ConfigureAwait(false);
+            }
+
             _users.Remove(userId);
 
             await _eventManager.PublishAsync(new UserDeletedEventArgs(user)).ConfigureAwait(false);
@@ -288,7 +299,7 @@ namespace Jellyfin.Server.Implementations.Users
             user.EasyPassword = newPasswordSha1;
             await UpdateUserAsync(user).ConfigureAwait(false);
 
-            _eventManager.Publish(new UserPasswordChangedEventArgs(user));
+            await _eventManager.PublishAsync(new UserPasswordChangedEventArgs(user)).ConfigureAwait(false);
         }
 
         /// <inheritdoc/>
@@ -541,14 +552,17 @@ namespace Jellyfin.Server.Implementations.Users
 
             _logger.LogWarning("No users, creating one with username {UserName}", defaultName);
 
-            await using var dbContext = _dbProvider.CreateContext();
-            var newUser = await CreateUserInternalAsync(defaultName, dbContext).ConfigureAwait(false);
-            newUser.SetPermission(PermissionKind.IsAdministrator, true);
-            newUser.SetPermission(PermissionKind.EnableContentDeletion, true);
-            newUser.SetPermission(PermissionKind.EnableRemoteControlOfOtherUsers, true);
+            var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false);
+            await using (dbContext.ConfigureAwait(false))
+            {
+                var newUser = await CreateUserInternalAsync(defaultName, dbContext).ConfigureAwait(false);
+                newUser.SetPermission(PermissionKind.IsAdministrator, true);
+                newUser.SetPermission(PermissionKind.EnableContentDeletion, true);
+                newUser.SetPermission(PermissionKind.EnableRemoteControlOfOtherUsers, true);
 
-            dbContext.Users.Add(newUser);
-            await dbContext.SaveChangesAsync().ConfigureAwait(false);
+                dbContext.Users.Add(newUser);
+                await dbContext.SaveChangesAsync().ConfigureAwait(false);
+            }
         }
 
         /// <inheritdoc/>
@@ -584,105 +598,111 @@ namespace Jellyfin.Server.Implementations.Users
         /// <inheritdoc/>
         public async Task UpdateConfigurationAsync(Guid userId, UserConfiguration config)
         {
-            await using var dbContext = _dbProvider.CreateContext();
-            var user = dbContext.Users
-                           .Include(u => u.Permissions)
-                           .Include(u => u.Preferences)
-                           .Include(u => u.AccessSchedules)
-                           .Include(u => u.ProfileImage)
-                           .FirstOrDefault(u => u.Id.Equals(userId))
-                       ?? throw new ArgumentException("No user exists with given Id!");
-
-            user.SubtitleMode = config.SubtitleMode;
-            user.HidePlayedInLatest = config.HidePlayedInLatest;
-            user.EnableLocalPassword = config.EnableLocalPassword;
-            user.PlayDefaultAudioTrack = config.PlayDefaultAudioTrack;
-            user.DisplayCollectionsView = config.DisplayCollectionsView;
-            user.DisplayMissingEpisodes = config.DisplayMissingEpisodes;
-            user.AudioLanguagePreference = config.AudioLanguagePreference;
-            user.RememberAudioSelections = config.RememberAudioSelections;
-            user.EnableNextEpisodeAutoPlay = config.EnableNextEpisodeAutoPlay;
-            user.RememberSubtitleSelections = config.RememberSubtitleSelections;
-            user.SubtitleLanguagePreference = config.SubtitleLanguagePreference;
-
-            user.SetPreference(PreferenceKind.OrderedViews, config.OrderedViews);
-            user.SetPreference(PreferenceKind.GroupedFolders, config.GroupedFolders);
-            user.SetPreference(PreferenceKind.MyMediaExcludes, config.MyMediaExcludes);
-            user.SetPreference(PreferenceKind.LatestItemExcludes, config.LatestItemsExcludes);
-
-            dbContext.Update(user);
-            _users[user.Id] = user;
-            await dbContext.SaveChangesAsync().ConfigureAwait(false);
+            var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false);
+            await using (dbContext.ConfigureAwait(false))
+            {
+                var user = dbContext.Users
+                               .Include(u => u.Permissions)
+                               .Include(u => u.Preferences)
+                               .Include(u => u.AccessSchedules)
+                               .Include(u => u.ProfileImage)
+                               .FirstOrDefault(u => u.Id.Equals(userId))
+                           ?? throw new ArgumentException("No user exists with given Id!");
+
+                user.SubtitleMode = config.SubtitleMode;
+                user.HidePlayedInLatest = config.HidePlayedInLatest;
+                user.EnableLocalPassword = config.EnableLocalPassword;
+                user.PlayDefaultAudioTrack = config.PlayDefaultAudioTrack;
+                user.DisplayCollectionsView = config.DisplayCollectionsView;
+                user.DisplayMissingEpisodes = config.DisplayMissingEpisodes;
+                user.AudioLanguagePreference = config.AudioLanguagePreference;
+                user.RememberAudioSelections = config.RememberAudioSelections;
+                user.EnableNextEpisodeAutoPlay = config.EnableNextEpisodeAutoPlay;
+                user.RememberSubtitleSelections = config.RememberSubtitleSelections;
+                user.SubtitleLanguagePreference = config.SubtitleLanguagePreference;
+
+                user.SetPreference(PreferenceKind.OrderedViews, config.OrderedViews);
+                user.SetPreference(PreferenceKind.GroupedFolders, config.GroupedFolders);
+                user.SetPreference(PreferenceKind.MyMediaExcludes, config.MyMediaExcludes);
+                user.SetPreference(PreferenceKind.LatestItemExcludes, config.LatestItemsExcludes);
+
+                dbContext.Update(user);
+                _users[user.Id] = user;
+                await dbContext.SaveChangesAsync().ConfigureAwait(false);
+            }
         }
 
         /// <inheritdoc/>
         public async Task UpdatePolicyAsync(Guid userId, UserPolicy policy)
         {
-            await using var dbContext = _dbProvider.CreateContext();
-            var user = dbContext.Users
-                           .Include(u => u.Permissions)
-                           .Include(u => u.Preferences)
-                           .Include(u => u.AccessSchedules)
-                           .Include(u => u.ProfileImage)
-                           .FirstOrDefault(u => u.Id.Equals(userId))
-                       ?? throw new ArgumentException("No user exists with given Id!");
-
-            // The default number of login attempts is 3, but for some god forsaken reason it's sent to the server as "0"
-            int? maxLoginAttempts = policy.LoginAttemptsBeforeLockout switch
-            {
-                -1 => null,
-                0 => 3,
-                _ => policy.LoginAttemptsBeforeLockout
-            };
+            var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false);
+            await using (dbContext.ConfigureAwait(false))
+            {
+                var user = dbContext.Users
+                               .Include(u => u.Permissions)
+                               .Include(u => u.Preferences)
+                               .Include(u => u.AccessSchedules)
+                               .Include(u => u.ProfileImage)
+                               .FirstOrDefault(u => u.Id.Equals(userId))
+                           ?? throw new ArgumentException("No user exists with given Id!");
+
+                // The default number of login attempts is 3, but for some god forsaken reason it's sent to the server as "0"
+                int? maxLoginAttempts = policy.LoginAttemptsBeforeLockout switch
+                {
+                    -1 => null,
+                    0 => 3,
+                    _ => policy.LoginAttemptsBeforeLockout
+                };
 
-            user.MaxParentalAgeRating = policy.MaxParentalRating;
-            user.EnableUserPreferenceAccess = policy.EnableUserPreferenceAccess;
-            user.RemoteClientBitrateLimit = policy.RemoteClientBitrateLimit;
-            user.AuthenticationProviderId = policy.AuthenticationProviderId;
-            user.PasswordResetProviderId = policy.PasswordResetProviderId;
-            user.InvalidLoginAttemptCount = policy.InvalidLoginAttemptCount;
-            user.LoginAttemptsBeforeLockout = maxLoginAttempts;
-            user.MaxActiveSessions = policy.MaxActiveSessions;
-            user.SyncPlayAccess = policy.SyncPlayAccess;
-            user.SetPermission(PermissionKind.IsAdministrator, policy.IsAdministrator);
-            user.SetPermission(PermissionKind.IsHidden, policy.IsHidden);
-            user.SetPermission(PermissionKind.IsDisabled, policy.IsDisabled);
-            user.SetPermission(PermissionKind.EnableSharedDeviceControl, policy.EnableSharedDeviceControl);
-            user.SetPermission(PermissionKind.EnableRemoteAccess, policy.EnableRemoteAccess);
-            user.SetPermission(PermissionKind.EnableLiveTvManagement, policy.EnableLiveTvManagement);
-            user.SetPermission(PermissionKind.EnableLiveTvAccess, policy.EnableLiveTvAccess);
-            user.SetPermission(PermissionKind.EnableMediaPlayback, policy.EnableMediaPlayback);
-            user.SetPermission(PermissionKind.EnableAudioPlaybackTranscoding, policy.EnableAudioPlaybackTranscoding);
-            user.SetPermission(PermissionKind.EnableVideoPlaybackTranscoding, policy.EnableVideoPlaybackTranscoding);
-            user.SetPermission(PermissionKind.EnableContentDeletion, policy.EnableContentDeletion);
-            user.SetPermission(PermissionKind.EnableContentDownloading, policy.EnableContentDownloading);
-            user.SetPermission(PermissionKind.EnableSyncTranscoding, policy.EnableSyncTranscoding);
-            user.SetPermission(PermissionKind.EnableMediaConversion, policy.EnableMediaConversion);
-            user.SetPermission(PermissionKind.EnableAllChannels, policy.EnableAllChannels);
-            user.SetPermission(PermissionKind.EnableAllDevices, policy.EnableAllDevices);
-            user.SetPermission(PermissionKind.EnableAllFolders, policy.EnableAllFolders);
-            user.SetPermission(PermissionKind.EnableRemoteControlOfOtherUsers, policy.EnableRemoteControlOfOtherUsers);
-            user.SetPermission(PermissionKind.EnablePlaybackRemuxing, policy.EnablePlaybackRemuxing);
-            user.SetPermission(PermissionKind.ForceRemoteSourceTranscoding, policy.ForceRemoteSourceTranscoding);
-            user.SetPermission(PermissionKind.EnablePublicSharing, policy.EnablePublicSharing);
-
-            user.AccessSchedules.Clear();
-            foreach (var policyAccessSchedule in policy.AccessSchedules)
-            {
-                user.AccessSchedules.Add(policyAccessSchedule);
-            }
-
-            // TODO: fix this at some point
-            user.SetPreference(PreferenceKind.BlockUnratedItems, policy.BlockUnratedItems ?? Array.Empty<UnratedItem>());
-            user.SetPreference(PreferenceKind.BlockedTags, policy.BlockedTags);
-            user.SetPreference(PreferenceKind.EnabledChannels, policy.EnabledChannels);
-            user.SetPreference(PreferenceKind.EnabledDevices, policy.EnabledDevices);
-            user.SetPreference(PreferenceKind.EnabledFolders, policy.EnabledFolders);
-            user.SetPreference(PreferenceKind.EnableContentDeletionFromFolders, policy.EnableContentDeletionFromFolders);
-
-            dbContext.Update(user);
-            _users[user.Id] = user;
-            await dbContext.SaveChangesAsync().ConfigureAwait(false);
+                user.MaxParentalAgeRating = policy.MaxParentalRating;
+                user.EnableUserPreferenceAccess = policy.EnableUserPreferenceAccess;
+                user.RemoteClientBitrateLimit = policy.RemoteClientBitrateLimit;
+                user.AuthenticationProviderId = policy.AuthenticationProviderId;
+                user.PasswordResetProviderId = policy.PasswordResetProviderId;
+                user.InvalidLoginAttemptCount = policy.InvalidLoginAttemptCount;
+                user.LoginAttemptsBeforeLockout = maxLoginAttempts;
+                user.MaxActiveSessions = policy.MaxActiveSessions;
+                user.SyncPlayAccess = policy.SyncPlayAccess;
+                user.SetPermission(PermissionKind.IsAdministrator, policy.IsAdministrator);
+                user.SetPermission(PermissionKind.IsHidden, policy.IsHidden);
+                user.SetPermission(PermissionKind.IsDisabled, policy.IsDisabled);
+                user.SetPermission(PermissionKind.EnableSharedDeviceControl, policy.EnableSharedDeviceControl);
+                user.SetPermission(PermissionKind.EnableRemoteAccess, policy.EnableRemoteAccess);
+                user.SetPermission(PermissionKind.EnableLiveTvManagement, policy.EnableLiveTvManagement);
+                user.SetPermission(PermissionKind.EnableLiveTvAccess, policy.EnableLiveTvAccess);
+                user.SetPermission(PermissionKind.EnableMediaPlayback, policy.EnableMediaPlayback);
+                user.SetPermission(PermissionKind.EnableAudioPlaybackTranscoding, policy.EnableAudioPlaybackTranscoding);
+                user.SetPermission(PermissionKind.EnableVideoPlaybackTranscoding, policy.EnableVideoPlaybackTranscoding);
+                user.SetPermission(PermissionKind.EnableContentDeletion, policy.EnableContentDeletion);
+                user.SetPermission(PermissionKind.EnableContentDownloading, policy.EnableContentDownloading);
+                user.SetPermission(PermissionKind.EnableSyncTranscoding, policy.EnableSyncTranscoding);
+                user.SetPermission(PermissionKind.EnableMediaConversion, policy.EnableMediaConversion);
+                user.SetPermission(PermissionKind.EnableAllChannels, policy.EnableAllChannels);
+                user.SetPermission(PermissionKind.EnableAllDevices, policy.EnableAllDevices);
+                user.SetPermission(PermissionKind.EnableAllFolders, policy.EnableAllFolders);
+                user.SetPermission(PermissionKind.EnableRemoteControlOfOtherUsers, policy.EnableRemoteControlOfOtherUsers);
+                user.SetPermission(PermissionKind.EnablePlaybackRemuxing, policy.EnablePlaybackRemuxing);
+                user.SetPermission(PermissionKind.ForceRemoteSourceTranscoding, policy.ForceRemoteSourceTranscoding);
+                user.SetPermission(PermissionKind.EnablePublicSharing, policy.EnablePublicSharing);
+
+                user.AccessSchedules.Clear();
+                foreach (var policyAccessSchedule in policy.AccessSchedules)
+                {
+                    user.AccessSchedules.Add(policyAccessSchedule);
+                }
+
+                // TODO: fix this at some point
+                user.SetPreference(PreferenceKind.BlockUnratedItems, policy.BlockUnratedItems ?? Array.Empty<UnratedItem>());
+                user.SetPreference(PreferenceKind.BlockedTags, policy.BlockedTags);
+                user.SetPreference(PreferenceKind.EnabledChannels, policy.EnabledChannels);
+                user.SetPreference(PreferenceKind.EnabledDevices, policy.EnabledDevices);
+                user.SetPreference(PreferenceKind.EnabledFolders, policy.EnabledFolders);
+                user.SetPreference(PreferenceKind.EnableContentDeletionFromFolders, policy.EnableContentDeletionFromFolders);
+
+                dbContext.Update(user);
+                _users[user.Id] = user;
+                await dbContext.SaveChangesAsync().ConfigureAwait(false);
+            }
         }
 
         /// <inheritdoc/>
@@ -693,9 +713,13 @@ namespace Jellyfin.Server.Implementations.Users
                 return;
             }
 
-            await using var dbContext = _dbProvider.CreateContext();
-            dbContext.Remove(user.ProfileImage);
-            await dbContext.SaveChangesAsync().ConfigureAwait(false);
+            var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false);
+            await using (dbContext.ConfigureAwait(false))
+            {
+                dbContext.Remove(user.ProfileImage);
+                await dbContext.SaveChangesAsync().ConfigureAwait(false);
+            }
+
             user.ProfileImage = null;
             _users[user.Id] = user;
         }
@@ -859,5 +883,12 @@ namespace Jellyfin.Server.Implementations.Users
 
             await UpdateUserAsync(user).ConfigureAwait(false);
         }
+
+        private async Task UpdateUserInternalAsync(JellyfinDb dbContext, User user)
+        {
+            dbContext.Users.Update(user);
+            _users[user.Id] = user;
+            await dbContext.SaveChangesAsync().ConfigureAwait(false);
+        }
     }
 }

+ 1 - 8
Jellyfin.Server/CoreAppHost.cs

@@ -1,6 +1,5 @@
 using System;
 using System.Collections.Generic;
-using System.IO;
 using System.Reflection;
 using Emby.Drawing;
 using Emby.Server.Implementations;
@@ -71,19 +70,13 @@ namespace Jellyfin.Server
                 Logger.LogWarning("Skia not available. Will fallback to {ImageEncoder}.", nameof(NullImageEncoder));
             }
 
-            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.AddSingleton<IActivityManager, ActivityManager>();
             serviceCollection.AddSingleton<IUserManager, UserManager>();
-            serviceCollection.AddSingleton<IDisplayPreferencesManager, DisplayPreferencesManager>();
+            serviceCollection.AddScoped<IDisplayPreferencesManager, DisplayPreferencesManager>();
             serviceCollection.AddSingleton<IDeviceManager, DeviceManager>();
 
             // TODO search the assemblies instead of adding them manually?

+ 3 - 3
Jellyfin.Server/Migrations/Routines/MigrateActivityLogDb.cs

@@ -19,7 +19,7 @@ namespace Jellyfin.Server.Migrations.Routines
         private const string DbFilename = "activitylog.db";
 
         private readonly ILogger<MigrateActivityLogDb> _logger;
-        private readonly JellyfinDbProvider _provider;
+        private readonly IDbContextFactory<JellyfinDb> _provider;
         private readonly IServerApplicationPaths _paths;
 
         /// <summary>
@@ -28,7 +28,7 @@ namespace Jellyfin.Server.Migrations.Routines
         /// <param name="logger">The logger.</param>
         /// <param name="paths">The server application paths.</param>
         /// <param name="provider">The database provider.</param>
-        public MigrateActivityLogDb(ILogger<MigrateActivityLogDb> logger, IServerApplicationPaths paths, JellyfinDbProvider provider)
+        public MigrateActivityLogDb(ILogger<MigrateActivityLogDb> logger, IServerApplicationPaths paths, IDbContextFactory<JellyfinDb> provider)
         {
             _logger = logger;
             _provider = provider;
@@ -68,7 +68,7 @@ namespace Jellyfin.Server.Migrations.Routines
             {
                 using var userDbConnection = SQLite3.Open(Path.Combine(dataPath, "users.db"), ConnectionFlags.ReadOnly, null);
                 _logger.LogWarning("Migrating the activity database may take a while, do not stop Jellyfin.");
-                using var dbContext = _provider.CreateContext();
+                using var dbContext = _provider.CreateDbContext();
 
                 var queryResult = connection.Query("SELECT * FROM ActivityLog ORDER BY Id");
 

+ 4 - 3
Jellyfin.Server/Migrations/Routines/MigrateAuthenticationDb.cs

@@ -6,6 +6,7 @@ using Jellyfin.Data.Entities.Security;
 using Jellyfin.Server.Implementations;
 using MediaBrowser.Controller;
 using MediaBrowser.Controller.Library;
+using Microsoft.EntityFrameworkCore;
 using Microsoft.Extensions.Logging;
 using SQLitePCL.pretty;
 
@@ -19,7 +20,7 @@ namespace Jellyfin.Server.Migrations.Routines
         private const string DbFilename = "authentication.db";
 
         private readonly ILogger<MigrateAuthenticationDb> _logger;
-        private readonly JellyfinDbProvider _dbProvider;
+        private readonly IDbContextFactory<JellyfinDb> _dbProvider;
         private readonly IServerApplicationPaths _appPaths;
         private readonly IUserManager _userManager;
 
@@ -32,7 +33,7 @@ namespace Jellyfin.Server.Migrations.Routines
         /// <param name="userManager">The user manager.</param>
         public MigrateAuthenticationDb(
             ILogger<MigrateAuthenticationDb> logger,
-            JellyfinDbProvider dbProvider,
+            IDbContextFactory<JellyfinDb> dbProvider,
             IServerApplicationPaths appPaths,
             IUserManager userManager)
         {
@@ -60,7 +61,7 @@ namespace Jellyfin.Server.Migrations.Routines
                 ConnectionFlags.ReadOnly,
                 null))
             {
-                using var dbContext = _dbProvider.CreateContext();
+                using var dbContext = _dbProvider.CreateDbContext();
 
                 var authenticatedDevices = connection.Query("SELECT * FROM Tokens");
 

+ 4 - 3
Jellyfin.Server/Migrations/Routines/MigrateDisplayPreferencesDb.cs

@@ -10,6 +10,7 @@ using Jellyfin.Server.Implementations;
 using MediaBrowser.Controller;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Model.Dto;
+using Microsoft.EntityFrameworkCore;
 using Microsoft.Extensions.Logging;
 using SQLitePCL.pretty;
 
@@ -24,7 +25,7 @@ namespace Jellyfin.Server.Migrations.Routines
 
         private readonly ILogger<MigrateDisplayPreferencesDb> _logger;
         private readonly IServerApplicationPaths _paths;
-        private readonly JellyfinDbProvider _provider;
+        private readonly IDbContextFactory<JellyfinDb> _provider;
         private readonly JsonSerializerOptions _jsonOptions;
         private readonly IUserManager _userManager;
 
@@ -38,7 +39,7 @@ namespace Jellyfin.Server.Migrations.Routines
         public MigrateDisplayPreferencesDb(
             ILogger<MigrateDisplayPreferencesDb> logger,
             IServerApplicationPaths paths,
-            JellyfinDbProvider provider,
+            IDbContextFactory<JellyfinDb> provider,
             IUserManager userManager)
         {
             _logger = logger;
@@ -84,7 +85,7 @@ namespace Jellyfin.Server.Migrations.Routines
             var dbFilePath = Path.Combine(_paths.DataPath, DbFilename);
             using (var connection = SQLite3.Open(dbFilePath, ConnectionFlags.ReadOnly, null))
             {
-                using var dbContext = _provider.CreateContext();
+                using var dbContext = _provider.CreateDbContext();
 
                 var results = connection.Query("SELECT * FROM userdisplaypreferences");
                 foreach (var result in results)

+ 4 - 3
Jellyfin.Server/Migrations/Routines/MigrateUserDb.cs

@@ -11,6 +11,7 @@ using MediaBrowser.Controller.Entities;
 using MediaBrowser.Model.Configuration;
 using MediaBrowser.Model.Serialization;
 using MediaBrowser.Model.Users;
+using Microsoft.EntityFrameworkCore;
 using Microsoft.Extensions.Logging;
 using SQLitePCL.pretty;
 using JsonSerializer = System.Text.Json.JsonSerializer;
@@ -26,7 +27,7 @@ namespace Jellyfin.Server.Migrations.Routines
 
         private readonly ILogger<MigrateUserDb> _logger;
         private readonly IServerApplicationPaths _paths;
-        private readonly JellyfinDbProvider _provider;
+        private readonly IDbContextFactory<JellyfinDb> _provider;
         private readonly IXmlSerializer _xmlSerializer;
 
         /// <summary>
@@ -39,7 +40,7 @@ namespace Jellyfin.Server.Migrations.Routines
         public MigrateUserDb(
             ILogger<MigrateUserDb> logger,
             IServerApplicationPaths paths,
-            JellyfinDbProvider provider,
+            IDbContextFactory<JellyfinDb> provider,
             IXmlSerializer xmlSerializer)
         {
             _logger = logger;
@@ -65,7 +66,7 @@ namespace Jellyfin.Server.Migrations.Routines
 
             using (var connection = SQLite3.Open(Path.Combine(dataPath, DbFilename), ConnectionFlags.ReadOnly, null))
             {
-                var dbContext = _provider.CreateContext();
+                var dbContext = _provider.CreateDbContext();
 
                 var queryResult = connection.Query("SELECT * FROM LocalUsersv2");
 

+ 17 - 3
Jellyfin.Server/Program.cs

@@ -192,6 +192,17 @@ namespace Jellyfin.Server
 
                 // Re-use the web host service provider in the app host since ASP.NET doesn't allow a custom service collection.
                 appHost.ServiceProvider = webHost.Services;
+                var jellyfinDb = await appHost.ServiceProvider.GetRequiredService<IDbContextFactory<JellyfinDb>>().CreateDbContextAsync().ConfigureAwait(false);
+                await using (jellyfinDb.ConfigureAwait(false))
+                {
+                    if ((await jellyfinDb.Database.GetPendingMigrationsAsync().ConfigureAwait(false)).Any())
+                    {
+                        _logger.LogInformation("There are pending EFCore migrations in the database. Applying... (This may take a while, do not stop Jellyfin)");
+                        await jellyfinDb.Database.MigrateAsync().ConfigureAwait(false);
+                        _logger.LogInformation("EFCore migrations applied successfully");
+                    }
+                }
+
                 await appHost.InitializeServices().ConfigureAwait(false);
                 Migrations.MigrationRunner.Run(appHost, _loggerFactory);
 
@@ -236,10 +247,13 @@ namespace Jellyfin.Server
                 {
                     _logger.LogInformation("Running query planner optimizations in the database... This might take a while");
                     // Run before disposing the application
-                    using var context = appHost.Resolve<JellyfinDbProvider>().CreateContext();
-                    if (context.Database.IsSqlite())
+                    var context = await appHost.ServiceProvider.GetRequiredService<IDbContextFactory<JellyfinDb>>().CreateDbContextAsync().ConfigureAwait(false);
+                    await using (context.ConfigureAwait(false))
                     {
-                        context.Database.ExecuteSqlRaw("PRAGMA optimize");
+                        if (context.Database.IsSqlite())
+                        {
+                            await context.Database.ExecuteSqlRawAsync("PRAGMA optimize").ConfigureAwait(false);
+                        }
                     }
                 }
 

+ 2 - 1
Jellyfin.Server/Startup.cs

@@ -9,6 +9,7 @@ using Jellyfin.MediaEncoding.Hls.Extensions;
 using Jellyfin.Networking.Configuration;
 using Jellyfin.Server.Extensions;
 using Jellyfin.Server.Implementations;
+using Jellyfin.Server.Implementations.Extensions;
 using Jellyfin.Server.Infrastructure;
 using Jellyfin.Server.Middleware;
 using MediaBrowser.Common.Net;
@@ -65,7 +66,7 @@ namespace Jellyfin.Server
             // TODO remove once this is fixed upstream https://github.com/dotnet/aspnetcore/issues/34371
             services.AddSingleton<IActionResultExecutor<PhysicalFileResult>, SymlinkFollowingPhysicalFileResultExecutor>();
             services.AddJellyfinApi(_serverApplicationHost.GetApiPluginAssemblies(), _serverConfigurationManager.GetNetworkConfiguration());
-
+            services.AddJellyfinDbContext();
             services.AddJellyfinApiSwagger();
 
             // configure custom legacy authentication