mirror of
				https://github.com/jellyfin/jellyfin.git
				synced 2025-11-03 19:17:24 -05:00 
			
		
		
		
	remove JellyfinDbProvider and add second level caching
This commit is contained in:
		
							parent
							
								
									509c6ec24c
								
							
						
					
					
						commit
						b836fe9685
					
				@ -17,7 +17,7 @@ namespace Emby.Server.Implementations.ScheduledTasks.Tasks
 | 
				
			|||||||
    {
 | 
					    {
 | 
				
			||||||
        private readonly ILogger<OptimizeDatabaseTask> _logger;
 | 
					        private readonly ILogger<OptimizeDatabaseTask> _logger;
 | 
				
			||||||
        private readonly ILocalizationManager _localization;
 | 
					        private readonly ILocalizationManager _localization;
 | 
				
			||||||
        private readonly JellyfinDbProvider _provider;
 | 
					        private readonly IDbContextFactory<JellyfinDb> _provider;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        /// <summary>
 | 
					        /// <summary>
 | 
				
			||||||
        /// Initializes a new instance of the <see cref="OptimizeDatabaseTask" /> class.
 | 
					        /// Initializes a new instance of the <see cref="OptimizeDatabaseTask" /> class.
 | 
				
			||||||
@ -28,7 +28,7 @@ namespace Emby.Server.Implementations.ScheduledTasks.Tasks
 | 
				
			|||||||
        public OptimizeDatabaseTask(
 | 
					        public OptimizeDatabaseTask(
 | 
				
			||||||
            ILogger<OptimizeDatabaseTask> logger,
 | 
					            ILogger<OptimizeDatabaseTask> logger,
 | 
				
			||||||
            ILocalizationManager localization,
 | 
					            ILocalizationManager localization,
 | 
				
			||||||
            JellyfinDbProvider provider)
 | 
					            IDbContextFactory<JellyfinDb> provider)
 | 
				
			||||||
        {
 | 
					        {
 | 
				
			||||||
            _logger = logger;
 | 
					            _logger = logger;
 | 
				
			||||||
            _localization = localization;
 | 
					            _localization = localization;
 | 
				
			||||||
@ -70,30 +70,31 @@ namespace Emby.Server.Implementations.ScheduledTasks.Tasks
 | 
				
			|||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        /// <inheritdoc />
 | 
					        /// <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...");
 | 
					            _logger.LogInformation("Optimizing and vacuuming jellyfin.db...");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            try
 | 
					            try
 | 
				
			||||||
            {
 | 
					            {
 | 
				
			||||||
                using var context = _provider.CreateContext();
 | 
					                var context = await _provider.CreateDbContextAsync(cancellationToken).ConfigureAwait(false);
 | 
				
			||||||
                if (context.Database.IsSqlite())
 | 
					                await using (context.ConfigureAwait(false))
 | 
				
			||||||
                {
 | 
					                {
 | 
				
			||||||
                    context.Database.ExecuteSqlRaw("PRAGMA optimize");
 | 
					                    if (context.Database.IsSqlite())
 | 
				
			||||||
                    context.Database.ExecuteSqlRaw("VACUUM");
 | 
					                    {
 | 
				
			||||||
                    _logger.LogInformation("jellyfin.db optimized successfully!");
 | 
					                        await context.Database.ExecuteSqlRawAsync("PRAGMA optimize", cancellationToken).ConfigureAwait(false);
 | 
				
			||||||
                }
 | 
					                        await context.Database.ExecuteSqlRawAsync("VACUUM", cancellationToken).ConfigureAwait(false);
 | 
				
			||||||
                else
 | 
					                        _logger.LogInformation("jellyfin.db optimized successfully!");
 | 
				
			||||||
                {
 | 
					                    }
 | 
				
			||||||
                    _logger.LogInformation("This database doesn't support optimization");
 | 
					                    else
 | 
				
			||||||
 | 
					                    {
 | 
				
			||||||
 | 
					                        _logger.LogInformation("This database doesn't support optimization");
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
                }
 | 
					                }
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
            catch (Exception e)
 | 
					            catch (Exception e)
 | 
				
			||||||
            {
 | 
					            {
 | 
				
			||||||
                _logger.LogError(e, "Error while optimizing jellyfin.db");
 | 
					                _logger.LogError(e, "Error while optimizing jellyfin.db");
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
 | 
					 | 
				
			||||||
            return Task.CompletedTask;
 | 
					 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
@ -15,13 +15,13 @@ namespace Jellyfin.Server.Implementations.Activity
 | 
				
			|||||||
    /// </summary>
 | 
					    /// </summary>
 | 
				
			||||||
    public class ActivityManager : IActivityManager
 | 
					    public class ActivityManager : IActivityManager
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
        private readonly JellyfinDbProvider _provider;
 | 
					        private readonly IDbContextFactory<JellyfinDb> _provider;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        /// <summary>
 | 
					        /// <summary>
 | 
				
			||||||
        /// Initializes a new instance of the <see cref="ActivityManager"/> class.
 | 
					        /// Initializes a new instance of the <see cref="ActivityManager"/> class.
 | 
				
			||||||
        /// </summary>
 | 
					        /// </summary>
 | 
				
			||||||
        /// <param name="provider">The Jellyfin database provider.</param>
 | 
					        /// <param name="provider">The Jellyfin database provider.</param>
 | 
				
			||||||
        public ActivityManager(JellyfinDbProvider provider)
 | 
					        public ActivityManager(IDbContextFactory<JellyfinDb> provider)
 | 
				
			||||||
        {
 | 
					        {
 | 
				
			||||||
            _provider = provider;
 | 
					            _provider = provider;
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
@ -32,10 +32,12 @@ namespace Jellyfin.Server.Implementations.Activity
 | 
				
			|||||||
        /// <inheritdoc/>
 | 
					        /// <inheritdoc/>
 | 
				
			||||||
        public async Task CreateAsync(ActivityLog entry)
 | 
					        public async Task CreateAsync(ActivityLog entry)
 | 
				
			||||||
        {
 | 
					        {
 | 
				
			||||||
            await using var dbContext = _provider.CreateContext();
 | 
					            var dbContext = await _provider.CreateDbContextAsync().ConfigureAwait(false);
 | 
				
			||||||
 | 
					            await using (dbContext.ConfigureAwait(false))
 | 
				
			||||||
            dbContext.ActivityLogs.Add(entry);
 | 
					            {
 | 
				
			||||||
            await dbContext.SaveChangesAsync().ConfigureAwait(false);
 | 
					                dbContext.ActivityLogs.Add(entry);
 | 
				
			||||||
 | 
					                await dbContext.SaveChangesAsync().ConfigureAwait(false);
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            EntryCreated?.Invoke(this, new GenericEventArgs<ActivityLogEntry>(ConvertToOldModel(entry)));
 | 
					            EntryCreated?.Invoke(this, new GenericEventArgs<ActivityLogEntry>(ConvertToOldModel(entry)));
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
@ -43,44 +45,47 @@ namespace Jellyfin.Server.Implementations.Activity
 | 
				
			|||||||
        /// <inheritdoc/>
 | 
					        /// <inheritdoc/>
 | 
				
			||||||
        public async Task<QueryResult<ActivityLogEntry>> GetPagedResultAsync(ActivityLogQuery query)
 | 
					        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
 | 
					 | 
				
			||||||
                .AsQueryable()
 | 
					 | 
				
			||||||
                .OrderByDescending(entry => entry.DateCreated);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            if (query.MinDate.HasValue)
 | 
					 | 
				
			||||||
            {
 | 
					            {
 | 
				
			||||||
                entries = entries.Where(entry => entry.DateCreated >= query.MinDate);
 | 
					                IQueryable<ActivityLog> entries = dbContext.ActivityLogs
 | 
				
			||||||
            }
 | 
					                    .OrderByDescending(entry => entry.DateCreated);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            if (query.HasUserId.HasValue)
 | 
					                if (query.MinDate.HasValue)
 | 
				
			||||||
            {
 | 
					                {
 | 
				
			||||||
                entries = entries.Where(entry => (!entry.UserId.Equals(default)) == query.HasUserId.Value);
 | 
					                    entries = entries.Where(entry => entry.DateCreated >= query.MinDate);
 | 
				
			||||||
            }
 | 
					                }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            return new QueryResult<ActivityLogEntry>(
 | 
					                if (query.HasUserId.HasValue)
 | 
				
			||||||
                query.Skip,
 | 
					                {
 | 
				
			||||||
                await entries.CountAsync().ConfigureAwait(false),
 | 
					                    entries = entries.Where(entry => (!entry.UserId.Equals(default)) == query.HasUserId.Value);
 | 
				
			||||||
                await entries
 | 
					                }
 | 
				
			||||||
                    .Skip(query.Skip ?? 0)
 | 
					
 | 
				
			||||||
                    .Take(query.Limit ?? 100)
 | 
					                return new QueryResult<ActivityLogEntry>(
 | 
				
			||||||
                    .AsAsyncEnumerable()
 | 
					                    query.Skip,
 | 
				
			||||||
                    .Select(ConvertToOldModel)
 | 
					                    await entries.CountAsync().ConfigureAwait(false),
 | 
				
			||||||
                    .ToListAsync()
 | 
					                    await entries
 | 
				
			||||||
                    .ConfigureAwait(false));
 | 
					                        .Skip(query.Skip ?? 0)
 | 
				
			||||||
 | 
					                        .Take(query.Limit ?? 100)
 | 
				
			||||||
 | 
					                        .AsAsyncEnumerable()
 | 
				
			||||||
 | 
					                        .Select(ConvertToOldModel)
 | 
				
			||||||
 | 
					                        .ToListAsync()
 | 
				
			||||||
 | 
					                        .ConfigureAwait(false));
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        /// <inheritdoc />
 | 
					        /// <inheritdoc />
 | 
				
			||||||
        public async Task CleanAsync(DateTime startDate)
 | 
					        public async Task CleanAsync(DateTime startDate)
 | 
				
			||||||
        {
 | 
					        {
 | 
				
			||||||
            await using var dbContext = _provider.CreateContext();
 | 
					            var dbContext = await _provider.CreateDbContextAsync().ConfigureAwait(false);
 | 
				
			||||||
            var entries = dbContext.ActivityLogs
 | 
					            await using (dbContext.ConfigureAwait(false))
 | 
				
			||||||
                .AsQueryable()
 | 
					            {
 | 
				
			||||||
                .Where(entry => entry.DateCreated <= startDate);
 | 
					                var entries = dbContext.ActivityLogs
 | 
				
			||||||
 | 
					                    .Where(entry => entry.DateCreated <= startDate);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            dbContext.RemoveRange(entries);
 | 
					                dbContext.RemoveRange(entries);
 | 
				
			||||||
            await dbContext.SaveChangesAsync().ConfigureAwait(false);
 | 
					                await dbContext.SaveChangesAsync().ConfigureAwait(false);
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        private static ActivityLogEntry ConvertToOldModel(ActivityLog entry)
 | 
					        private static ActivityLogEntry ConvertToOldModel(ActivityLog entry)
 | 
				
			||||||
 | 
				
			|||||||
@ -1,5 +1,6 @@
 | 
				
			|||||||
using System;
 | 
					using System;
 | 
				
			||||||
using System.Collections.Concurrent;
 | 
					using System.Collections.Concurrent;
 | 
				
			||||||
 | 
					using System.Collections.Generic;
 | 
				
			||||||
using System.Linq;
 | 
					using System.Linq;
 | 
				
			||||||
using System.Threading.Tasks;
 | 
					using System.Threading.Tasks;
 | 
				
			||||||
using Jellyfin.Data.Entities;
 | 
					using Jellyfin.Data.Entities;
 | 
				
			||||||
@ -22,7 +23,7 @@ namespace Jellyfin.Server.Implementations.Devices
 | 
				
			|||||||
    /// </summary>
 | 
					    /// </summary>
 | 
				
			||||||
    public class DeviceManager : IDeviceManager
 | 
					    public class DeviceManager : IDeviceManager
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
        private readonly JellyfinDbProvider _dbProvider;
 | 
					        private readonly IDbContextFactory<JellyfinDb> _dbProvider;
 | 
				
			||||||
        private readonly IUserManager _userManager;
 | 
					        private readonly IUserManager _userManager;
 | 
				
			||||||
        private readonly ConcurrentDictionary<string, ClientCapabilities> _capabilitiesMap = new();
 | 
					        private readonly ConcurrentDictionary<string, ClientCapabilities> _capabilitiesMap = new();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -31,7 +32,7 @@ namespace Jellyfin.Server.Implementations.Devices
 | 
				
			|||||||
        /// </summary>
 | 
					        /// </summary>
 | 
				
			||||||
        /// <param name="dbProvider">The database provider.</param>
 | 
					        /// <param name="dbProvider">The database provider.</param>
 | 
				
			||||||
        /// <param name="userManager">The user manager.</param>
 | 
					        /// <param name="userManager">The user manager.</param>
 | 
				
			||||||
        public DeviceManager(JellyfinDbProvider dbProvider, IUserManager userManager)
 | 
					        public DeviceManager(IDbContextFactory<JellyfinDb> dbProvider, IUserManager userManager)
 | 
				
			||||||
        {
 | 
					        {
 | 
				
			||||||
            _dbProvider = dbProvider;
 | 
					            _dbProvider = dbProvider;
 | 
				
			||||||
            _userManager = userManager;
 | 
					            _userManager = userManager;
 | 
				
			||||||
@ -49,16 +50,20 @@ namespace Jellyfin.Server.Implementations.Devices
 | 
				
			|||||||
        /// <inheritdoc />
 | 
					        /// <inheritdoc />
 | 
				
			||||||
        public async Task UpdateDeviceOptions(string deviceId, string deviceName)
 | 
					        public async Task UpdateDeviceOptions(string deviceId, string deviceName)
 | 
				
			||||||
        {
 | 
					        {
 | 
				
			||||||
            await using var dbContext = _dbProvider.CreateContext();
 | 
					            DeviceOptions? deviceOptions;
 | 
				
			||||||
            var deviceOptions = await dbContext.DeviceOptions.AsQueryable().FirstOrDefaultAsync(dev => dev.DeviceId == deviceId).ConfigureAwait(false);
 | 
					            var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false);
 | 
				
			||||||
            if (deviceOptions == null)
 | 
					            await using (dbContext.ConfigureAwait(false))
 | 
				
			||||||
            {
 | 
					            {
 | 
				
			||||||
                deviceOptions = new DeviceOptions(deviceId);
 | 
					                deviceOptions = await dbContext.DeviceOptions.AsQueryable().FirstOrDefaultAsync(dev => dev.DeviceId == deviceId).ConfigureAwait(false);
 | 
				
			||||||
                dbContext.DeviceOptions.Add(deviceOptions);
 | 
					                if (deviceOptions == null)
 | 
				
			||||||
            }
 | 
					                {
 | 
				
			||||||
 | 
					                    deviceOptions = new DeviceOptions(deviceId);
 | 
				
			||||||
 | 
					                    dbContext.DeviceOptions.Add(deviceOptions);
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            deviceOptions.CustomName = deviceName;
 | 
					                deviceOptions.CustomName = deviceName;
 | 
				
			||||||
            await dbContext.SaveChangesAsync().ConfigureAwait(false);
 | 
					                await dbContext.SaveChangesAsync().ConfigureAwait(false);
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            DeviceOptionsUpdated?.Invoke(this, new GenericEventArgs<Tuple<string, DeviceOptions>>(new Tuple<string, DeviceOptions>(deviceId, deviceOptions)));
 | 
					            DeviceOptionsUpdated?.Invoke(this, new GenericEventArgs<Tuple<string, DeviceOptions>>(new Tuple<string, DeviceOptions>(deviceId, deviceOptions)));
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
@ -66,22 +71,29 @@ namespace Jellyfin.Server.Implementations.Devices
 | 
				
			|||||||
        /// <inheritdoc />
 | 
					        /// <inheritdoc />
 | 
				
			||||||
        public async Task<Device> CreateDevice(Device device)
 | 
					        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;
 | 
					            return device;
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        /// <inheritdoc />
 | 
					        /// <inheritdoc />
 | 
				
			||||||
        public async Task<DeviceOptions> GetDeviceOptions(string deviceId)
 | 
					        public async Task<DeviceOptions> GetDeviceOptions(string deviceId)
 | 
				
			||||||
        {
 | 
					        {
 | 
				
			||||||
            await using var dbContext = _dbProvider.CreateContext();
 | 
					            var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false);
 | 
				
			||||||
            var deviceOptions = await dbContext.DeviceOptions
 | 
					            DeviceOptions? deviceOptions;
 | 
				
			||||||
                .AsQueryable()
 | 
					            await using (dbContext.ConfigureAwait(false))
 | 
				
			||||||
                .FirstOrDefaultAsync(d => d.DeviceId == deviceId)
 | 
					            {
 | 
				
			||||||
                .ConfigureAwait(false);
 | 
					                deviceOptions = await dbContext.DeviceOptions
 | 
				
			||||||
 | 
					                    .AsNoTracking()
 | 
				
			||||||
 | 
					                    .FirstOrDefaultAsync(d => d.DeviceId == deviceId)
 | 
				
			||||||
 | 
					                    .ConfigureAwait(false);
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            return deviceOptions ?? new DeviceOptions(deviceId);
 | 
					            return deviceOptions ?? new DeviceOptions(deviceId);
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
@ -97,14 +109,17 @@ namespace Jellyfin.Server.Implementations.Devices
 | 
				
			|||||||
        /// <inheritdoc />
 | 
					        /// <inheritdoc />
 | 
				
			||||||
        public async Task<DeviceInfo?> GetDevice(string id)
 | 
					        public async Task<DeviceInfo?> GetDevice(string id)
 | 
				
			||||||
        {
 | 
					        {
 | 
				
			||||||
            await using var dbContext = _dbProvider.CreateContext();
 | 
					            Device? device;
 | 
				
			||||||
            var device = await dbContext.Devices
 | 
					            var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false);
 | 
				
			||||||
                .AsQueryable()
 | 
					            await using (dbContext.ConfigureAwait(false))
 | 
				
			||||||
                .Where(d => d.DeviceId == id)
 | 
					            {
 | 
				
			||||||
                .OrderByDescending(d => d.DateLastActivity)
 | 
					                device = await dbContext.Devices
 | 
				
			||||||
                .Include(d => d.User)
 | 
					                    .Where(d => d.DeviceId == id)
 | 
				
			||||||
                .FirstOrDefaultAsync()
 | 
					                    .OrderByDescending(d => d.DateLastActivity)
 | 
				
			||||||
                .ConfigureAwait(false);
 | 
					                    .Include(d => d.User)
 | 
				
			||||||
 | 
					                    .FirstOrDefaultAsync()
 | 
				
			||||||
 | 
					                    .ConfigureAwait(false);
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            var deviceInfo = device == null ? null : ToDeviceInfo(device);
 | 
					            var deviceInfo = device == null ? null : ToDeviceInfo(device);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -114,41 +129,40 @@ namespace Jellyfin.Server.Implementations.Devices
 | 
				
			|||||||
        /// <inheritdoc />
 | 
					        /// <inheritdoc />
 | 
				
			||||||
        public async Task<QueryResult<Device>> GetDevices(DeviceQuery query)
 | 
					        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();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            if (query.UserId.HasValue)
 | 
					 | 
				
			||||||
            {
 | 
					            {
 | 
				
			||||||
                devices = devices.Where(device => device.UserId.Equals(query.UserId.Value));
 | 
					                var devices = dbContext.Devices.AsQueryable();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                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.AccessToken != null)
 | 
				
			||||||
 | 
					                {
 | 
				
			||||||
 | 
					                    devices = devices.Where(device => device.AccessToken == query.AccessToken);
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                var count = await devices.CountAsync().ConfigureAwait(false);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                if (query.Skip.HasValue)
 | 
				
			||||||
 | 
					                {
 | 
				
			||||||
 | 
					                    devices = devices.Skip(query.Skip.Value);
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                if (query.Limit.HasValue)
 | 
				
			||||||
 | 
					                {
 | 
				
			||||||
 | 
					                    devices = devices.Take(query.Limit.Value);
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                return new QueryResult<Device>(query.Skip, count, await devices.ToListAsync().ConfigureAwait(false));
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
 | 
					 | 
				
			||||||
            if (query.DeviceId != null)
 | 
					 | 
				
			||||||
            {
 | 
					 | 
				
			||||||
                devices = devices.Where(device => device.DeviceId == query.DeviceId);
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            if (query.AccessToken != null)
 | 
					 | 
				
			||||||
            {
 | 
					 | 
				
			||||||
                devices = devices.Where(device => device.AccessToken == query.AccessToken);
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            var count = await devices.CountAsync().ConfigureAwait(false);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            if (query.Skip.HasValue)
 | 
					 | 
				
			||||||
            {
 | 
					 | 
				
			||||||
                devices = devices.Skip(query.Skip.Value);
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            if (query.Limit.HasValue)
 | 
					 | 
				
			||||||
            {
 | 
					 | 
				
			||||||
                devices = devices.Take(query.Limit.Value);
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            return new QueryResult<Device>(
 | 
					 | 
				
			||||||
                query.Skip,
 | 
					 | 
				
			||||||
                count,
 | 
					 | 
				
			||||||
                await devices.ToListAsync().ConfigureAwait(false));
 | 
					 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        /// <inheritdoc />
 | 
					        /// <inheritdoc />
 | 
				
			||||||
@ -165,37 +179,43 @@ namespace Jellyfin.Server.Implementations.Devices
 | 
				
			|||||||
        /// <inheritdoc />
 | 
					        /// <inheritdoc />
 | 
				
			||||||
        public async Task<QueryResult<DeviceInfo>> GetDevicesForUser(Guid? userId, bool? supportsSync)
 | 
					        public async Task<QueryResult<DeviceInfo>> GetDevicesForUser(Guid? userId, bool? supportsSync)
 | 
				
			||||||
        {
 | 
					        {
 | 
				
			||||||
            await using var dbContext = _dbProvider.CreateContext();
 | 
					            IAsyncEnumerable<Device> sessions;
 | 
				
			||||||
            var sessions = dbContext.Devices
 | 
					            var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false);
 | 
				
			||||||
                .Include(d => d.User)
 | 
					            await using (dbContext.ConfigureAwait(false))
 | 
				
			||||||
                .AsQueryable()
 | 
					 | 
				
			||||||
                .OrderByDescending(d => d.DateLastActivity)
 | 
					 | 
				
			||||||
                .ThenBy(d => d.DeviceId)
 | 
					 | 
				
			||||||
                .AsAsyncEnumerable();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            if (supportsSync.HasValue)
 | 
					 | 
				
			||||||
            {
 | 
					            {
 | 
				
			||||||
                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 (supportsSync.HasValue)
 | 
				
			||||||
 | 
					                {
 | 
				
			||||||
 | 
					                    sessions = sessions.Where(i => GetCapabilities(i.DeviceId).SupportsSync == supportsSync.Value);
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                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);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                return new QueryResult<DeviceInfo>(array);
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
 | 
					 | 
				
			||||||
            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);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            return new QueryResult<DeviceInfo>(array);
 | 
					 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        /// <inheritdoc />
 | 
					        /// <inheritdoc />
 | 
				
			||||||
        public async Task DeleteDevice(Device device)
 | 
					        public async Task DeleteDevice(Device device)
 | 
				
			||||||
        {
 | 
					        {
 | 
				
			||||||
            await using var dbContext = _dbProvider.CreateContext();
 | 
					            var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false);
 | 
				
			||||||
            dbContext.Devices.Remove(device);
 | 
					            await using (dbContext.ConfigureAwait(false))
 | 
				
			||||||
            await dbContext.SaveChangesAsync().ConfigureAwait(false);
 | 
					            {
 | 
				
			||||||
 | 
					                dbContext.Devices.Remove(device);
 | 
				
			||||||
 | 
					                await dbContext.SaveChangesAsync().ConfigureAwait(false);
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        /// <inheritdoc />
 | 
					        /// <inheritdoc />
 | 
				
			||||||
 | 
				
			|||||||
@ -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;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@ -26,6 +26,7 @@
 | 
				
			|||||||
  </ItemGroup>
 | 
					  </ItemGroup>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  <ItemGroup>
 | 
					  <ItemGroup>
 | 
				
			||||||
 | 
					    <PackageReference Include="EFCoreSecondLevelCacheInterceptor" Version="3.7.3" />
 | 
				
			||||||
    <PackageReference Include="System.Linq.Async" Version="6.0.1" />
 | 
					    <PackageReference Include="System.Linq.Async" Version="6.0.1" />
 | 
				
			||||||
    <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="6.0.9" />
 | 
					    <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="6.0.9" />
 | 
				
			||||||
    <PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="6.0.9" />
 | 
					    <PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="6.0.9" />
 | 
				
			||||||
 | 
				
			|||||||
@ -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);
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
@ -10,13 +10,13 @@ namespace Jellyfin.Server.Implementations.Security
 | 
				
			|||||||
    /// <inheritdoc />
 | 
					    /// <inheritdoc />
 | 
				
			||||||
    public class AuthenticationManager : IAuthenticationManager
 | 
					    public class AuthenticationManager : IAuthenticationManager
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
        private readonly JellyfinDbProvider _dbProvider;
 | 
					        private readonly IDbContextFactory<JellyfinDb> _dbProvider;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        /// <summary>
 | 
					        /// <summary>
 | 
				
			||||||
        /// Initializes a new instance of the <see cref="AuthenticationManager"/> class.
 | 
					        /// Initializes a new instance of the <see cref="AuthenticationManager"/> class.
 | 
				
			||||||
        /// </summary>
 | 
					        /// </summary>
 | 
				
			||||||
        /// <param name="dbProvider">The database provider.</param>
 | 
					        /// <param name="dbProvider">The database provider.</param>
 | 
				
			||||||
        public AuthenticationManager(JellyfinDbProvider dbProvider)
 | 
					        public AuthenticationManager(IDbContextFactory<JellyfinDb> dbProvider)
 | 
				
			||||||
        {
 | 
					        {
 | 
				
			||||||
            _dbProvider = dbProvider;
 | 
					            _dbProvider = dbProvider;
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
@ -24,50 +24,56 @@ namespace Jellyfin.Server.Implementations.Security
 | 
				
			|||||||
        /// <inheritdoc />
 | 
					        /// <inheritdoc />
 | 
				
			||||||
        public async Task CreateApiKey(string name)
 | 
					        public async Task CreateApiKey(string name)
 | 
				
			||||||
        {
 | 
					        {
 | 
				
			||||||
            await using var dbContext = _dbProvider.CreateContext();
 | 
					            var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false);
 | 
				
			||||||
 | 
					            await using (dbContext.ConfigureAwait(false))
 | 
				
			||||||
 | 
					            {
 | 
				
			||||||
 | 
					                dbContext.ApiKeys.Add(new ApiKey(name));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            dbContext.ApiKeys.Add(new ApiKey(name));
 | 
					                await dbContext.SaveChangesAsync().ConfigureAwait(false);
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
            await dbContext.SaveChangesAsync().ConfigureAwait(false);
 | 
					 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        /// <inheritdoc />
 | 
					        /// <inheritdoc />
 | 
				
			||||||
        public async Task<IReadOnlyList<AuthenticationInfo>> GetApiKeys()
 | 
					        public async Task<IReadOnlyList<AuthenticationInfo>> GetApiKeys()
 | 
				
			||||||
        {
 | 
					        {
 | 
				
			||||||
            await using var dbContext = _dbProvider.CreateContext();
 | 
					            var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false);
 | 
				
			||||||
 | 
					            await using (dbContext.ConfigureAwait(false))
 | 
				
			||||||
            return await dbContext.ApiKeys
 | 
					            {
 | 
				
			||||||
                .AsAsyncEnumerable()
 | 
					                return await dbContext.ApiKeys
 | 
				
			||||||
                .Select(key => new AuthenticationInfo
 | 
					                    .AsAsyncEnumerable()
 | 
				
			||||||
                {
 | 
					                    .Select(key => new AuthenticationInfo
 | 
				
			||||||
                    AppName = key.Name,
 | 
					                    {
 | 
				
			||||||
                    AccessToken = key.AccessToken,
 | 
					                        AppName = key.Name,
 | 
				
			||||||
                    DateCreated = key.DateCreated,
 | 
					                        AccessToken = key.AccessToken,
 | 
				
			||||||
                    DeviceId = string.Empty,
 | 
					                        DateCreated = key.DateCreated,
 | 
				
			||||||
                    DeviceName = string.Empty,
 | 
					                        DeviceId = string.Empty,
 | 
				
			||||||
                    AppVersion = string.Empty
 | 
					                        DeviceName = string.Empty,
 | 
				
			||||||
                }).ToListAsync().ConfigureAwait(false);
 | 
					                        AppVersion = string.Empty
 | 
				
			||||||
 | 
					                    }).ToListAsync().ConfigureAwait(false);
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        /// <inheritdoc />
 | 
					        /// <inheritdoc />
 | 
				
			||||||
        public async Task DeleteApiKey(string accessToken)
 | 
					        public async Task DeleteApiKey(string accessToken)
 | 
				
			||||||
        {
 | 
					        {
 | 
				
			||||||
            await using var dbContext = _dbProvider.CreateContext();
 | 
					            var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false);
 | 
				
			||||||
 | 
					            await using (dbContext.ConfigureAwait(false))
 | 
				
			||||||
            var key = await dbContext.ApiKeys
 | 
					 | 
				
			||||||
                .AsQueryable()
 | 
					 | 
				
			||||||
                .Where(apiKey => apiKey.AccessToken == accessToken)
 | 
					 | 
				
			||||||
                .FirstOrDefaultAsync()
 | 
					 | 
				
			||||||
                .ConfigureAwait(false);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            if (key == null)
 | 
					 | 
				
			||||||
            {
 | 
					            {
 | 
				
			||||||
                return;
 | 
					                var key = await dbContext.ApiKeys
 | 
				
			||||||
 | 
					                    .AsQueryable()
 | 
				
			||||||
 | 
					                    .Where(apiKey => apiKey.AccessToken == accessToken)
 | 
				
			||||||
 | 
					                    .FirstOrDefaultAsync()
 | 
				
			||||||
 | 
					                    .ConfigureAwait(false);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                if (key == null)
 | 
				
			||||||
 | 
					                {
 | 
				
			||||||
 | 
					                    return;
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                dbContext.Remove(key);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                await dbContext.SaveChangesAsync().ConfigureAwait(false);
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
 | 
					 | 
				
			||||||
            dbContext.Remove(key);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            await dbContext.SaveChangesAsync().ConfigureAwait(false);
 | 
					 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
@ -4,6 +4,7 @@ using System;
 | 
				
			|||||||
using System.Collections.Generic;
 | 
					using System.Collections.Generic;
 | 
				
			||||||
using System.Net;
 | 
					using System.Net;
 | 
				
			||||||
using System.Threading.Tasks;
 | 
					using System.Threading.Tasks;
 | 
				
			||||||
 | 
					using EFCoreSecondLevelCacheInterceptor;
 | 
				
			||||||
using MediaBrowser.Controller;
 | 
					using MediaBrowser.Controller;
 | 
				
			||||||
using MediaBrowser.Controller.Library;
 | 
					using MediaBrowser.Controller.Library;
 | 
				
			||||||
using MediaBrowser.Controller.Net;
 | 
					using MediaBrowser.Controller.Net;
 | 
				
			||||||
@ -15,12 +16,12 @@ namespace Jellyfin.Server.Implementations.Security
 | 
				
			|||||||
{
 | 
					{
 | 
				
			||||||
    public class AuthorizationContext : IAuthorizationContext
 | 
					    public class AuthorizationContext : IAuthorizationContext
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
        private readonly JellyfinDbProvider _jellyfinDbProvider;
 | 
					        private readonly IDbContextFactory<JellyfinDb> _jellyfinDbProvider;
 | 
				
			||||||
        private readonly IUserManager _userManager;
 | 
					        private readonly IUserManager _userManager;
 | 
				
			||||||
        private readonly IServerApplicationHost _serverApplicationHost;
 | 
					        private readonly IServerApplicationHost _serverApplicationHost;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        public AuthorizationContext(
 | 
					        public AuthorizationContext(
 | 
				
			||||||
            JellyfinDbProvider jellyfinDb,
 | 
					            IDbContextFactory<JellyfinDb> jellyfinDb,
 | 
				
			||||||
            IUserManager userManager,
 | 
					            IUserManager userManager,
 | 
				
			||||||
            IServerApplicationHost serverApplicationHost)
 | 
					            IServerApplicationHost serverApplicationHost)
 | 
				
			||||||
        {
 | 
					        {
 | 
				
			||||||
@ -121,96 +122,99 @@ namespace Jellyfin.Server.Implementations.Security
 | 
				
			|||||||
#pragma warning restore CA1508
 | 
					#pragma warning restore CA1508
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            authInfo.HasToken = true;
 | 
					            authInfo.HasToken = true;
 | 
				
			||||||
            await using var dbContext = _jellyfinDbProvider.CreateContext();
 | 
					            var dbContext = await _jellyfinDbProvider.CreateDbContextAsync().ConfigureAwait(false);
 | 
				
			||||||
            var device = await dbContext.Devices.FirstOrDefaultAsync(d => d.AccessToken == token).ConfigureAwait(false);
 | 
					            await using (dbContext.ConfigureAwait(false))
 | 
				
			||||||
 | 
					 | 
				
			||||||
            if (device != null)
 | 
					 | 
				
			||||||
            {
 | 
					            {
 | 
				
			||||||
                authInfo.IsAuthenticated = true;
 | 
					                var device = await dbContext.Devices.FirstOrDefaultAsync(d => d.AccessToken == token).ConfigureAwait(false);
 | 
				
			||||||
                var updateToken = false;
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
                // TODO: Remove these checks for IsNullOrWhiteSpace
 | 
					                if (device != null)
 | 
				
			||||||
                if (string.IsNullOrWhiteSpace(authInfo.Client))
 | 
					 | 
				
			||||||
                {
 | 
					 | 
				
			||||||
                    authInfo.Client = device.AppName;
 | 
					 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                if (string.IsNullOrWhiteSpace(authInfo.DeviceId))
 | 
					 | 
				
			||||||
                {
 | 
					 | 
				
			||||||
                    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);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                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 (string.IsNullOrWhiteSpace(authInfo.Version))
 | 
					 | 
				
			||||||
                {
 | 
					 | 
				
			||||||
                    authInfo.Version = device.AppVersion;
 | 
					 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
                else if (!string.Equals(authInfo.Version, device.AppVersion, StringComparison.OrdinalIgnoreCase))
 | 
					 | 
				
			||||||
                {
 | 
					 | 
				
			||||||
                    if (allowTokenInfoUpdate)
 | 
					 | 
				
			||||||
                    {
 | 
					 | 
				
			||||||
                        updateToken = true;
 | 
					 | 
				
			||||||
                        device.AppVersion = authInfo.Version;
 | 
					 | 
				
			||||||
                    }
 | 
					 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                if ((DateTime.UtcNow - device.DateLastActivity).TotalMinutes > 3)
 | 
					 | 
				
			||||||
                {
 | 
					 | 
				
			||||||
                    device.DateLastActivity = DateTime.UtcNow;
 | 
					 | 
				
			||||||
                    updateToken = true;
 | 
					 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                authInfo.User = _userManager.GetUserById(device.UserId);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                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.IsAuthenticated = true;
 | 
				
			||||||
                    authInfo.Client = key.Name;
 | 
					                    var updateToken = false;
 | 
				
			||||||
                    authInfo.Token = key.AccessToken;
 | 
					
 | 
				
			||||||
 | 
					                    // TODO: Remove these checks for IsNullOrWhiteSpace
 | 
				
			||||||
 | 
					                    if (string.IsNullOrWhiteSpace(authInfo.Client))
 | 
				
			||||||
 | 
					                    {
 | 
				
			||||||
 | 
					                        authInfo.Client = device.AppName;
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                    if (string.IsNullOrWhiteSpace(authInfo.DeviceId))
 | 
					                    if (string.IsNullOrWhiteSpace(authInfo.DeviceId))
 | 
				
			||||||
                    {
 | 
					                    {
 | 
				
			||||||
                        authInfo.DeviceId = _serverApplicationHost.SystemId;
 | 
					                        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);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                    if (string.IsNullOrWhiteSpace(authInfo.Device))
 | 
					                    if (string.IsNullOrWhiteSpace(authInfo.Device))
 | 
				
			||||||
                    {
 | 
					                    {
 | 
				
			||||||
                        authInfo.Device = _serverApplicationHost.Name;
 | 
					                        authInfo.Device = device.DeviceName;
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                    else if (!string.Equals(authInfo.Device, device.DeviceName, StringComparison.OrdinalIgnoreCase))
 | 
				
			||||||
 | 
					                    {
 | 
				
			||||||
 | 
					                        if (allowTokenInfoUpdate)
 | 
				
			||||||
 | 
					                        {
 | 
				
			||||||
 | 
					                            updateToken = true;
 | 
				
			||||||
 | 
					                            device.DeviceName = authInfo.Device;
 | 
				
			||||||
 | 
					                        }
 | 
				
			||||||
                    }
 | 
					                    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                    if (string.IsNullOrWhiteSpace(authInfo.Version))
 | 
					                    if (string.IsNullOrWhiteSpace(authInfo.Version))
 | 
				
			||||||
                    {
 | 
					                    {
 | 
				
			||||||
                        authInfo.Version = _serverApplicationHost.ApplicationVersionString;
 | 
					                        authInfo.Version = device.AppVersion;
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                    else if (!string.Equals(authInfo.Version, device.AppVersion, StringComparison.OrdinalIgnoreCase))
 | 
				
			||||||
 | 
					                    {
 | 
				
			||||||
 | 
					                        if (allowTokenInfoUpdate)
 | 
				
			||||||
 | 
					                        {
 | 
				
			||||||
 | 
					                            updateToken = true;
 | 
				
			||||||
 | 
					                            device.AppVersion = authInfo.Version;
 | 
				
			||||||
 | 
					                        }
 | 
				
			||||||
                    }
 | 
					                    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                    authInfo.IsApiKey = true;
 | 
					                    if ((DateTime.UtcNow - device.DateLastActivity).TotalMinutes > 3)
 | 
				
			||||||
                }
 | 
					                    {
 | 
				
			||||||
            }
 | 
					                        device.DateLastActivity = DateTime.UtcNow;
 | 
				
			||||||
 | 
					                        updateToken = true;
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            return authInfo;
 | 
					                    authInfo.User = _userManager.GetUserById(device.UserId);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                    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))
 | 
				
			||||||
 | 
					                        {
 | 
				
			||||||
 | 
					                            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;
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                return authInfo;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        /// <summary>
 | 
					        /// <summary>
 | 
				
			||||||
 | 
				
			|||||||
@ -20,10 +20,10 @@ namespace Jellyfin.Server.Implementations.Users
 | 
				
			|||||||
        /// <summary>
 | 
					        /// <summary>
 | 
				
			||||||
        /// Initializes a new instance of the <see cref="DisplayPreferencesManager"/> class.
 | 
					        /// Initializes a new instance of the <see cref="DisplayPreferencesManager"/> class.
 | 
				
			||||||
        /// </summary>
 | 
					        /// </summary>
 | 
				
			||||||
        /// <param name="dbContext">The database context.</param>
 | 
					        /// <param name="dbContextFactory">The database context factory.</param>
 | 
				
			||||||
        public DisplayPreferencesManager(JellyfinDb dbContext)
 | 
					        public DisplayPreferencesManager(IDbContextFactory<JellyfinDb> dbContextFactory)
 | 
				
			||||||
        {
 | 
					        {
 | 
				
			||||||
            _dbContext = dbContext;
 | 
					            _dbContext = dbContextFactory.CreateDbContext();
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        /// <inheritdoc />
 | 
					        /// <inheritdoc />
 | 
				
			||||||
 | 
				
			|||||||
@ -33,7 +33,7 @@ namespace Jellyfin.Server.Implementations.Users
 | 
				
			|||||||
    /// </summary>
 | 
					    /// </summary>
 | 
				
			||||||
    public class UserManager : IUserManager
 | 
					    public class UserManager : IUserManager
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
        private readonly JellyfinDbProvider _dbProvider;
 | 
					        private readonly IDbContextFactory<JellyfinDb> _dbProvider;
 | 
				
			||||||
        private readonly IEventManager _eventManager;
 | 
					        private readonly IEventManager _eventManager;
 | 
				
			||||||
        private readonly ICryptoProvider _cryptoProvider;
 | 
					        private readonly ICryptoProvider _cryptoProvider;
 | 
				
			||||||
        private readonly INetworkManager _networkManager;
 | 
					        private readonly INetworkManager _networkManager;
 | 
				
			||||||
@ -59,7 +59,7 @@ namespace Jellyfin.Server.Implementations.Users
 | 
				
			|||||||
        /// <param name="imageProcessor">The image processor.</param>
 | 
					        /// <param name="imageProcessor">The image processor.</param>
 | 
				
			||||||
        /// <param name="logger">The logger.</param>
 | 
					        /// <param name="logger">The logger.</param>
 | 
				
			||||||
        public UserManager(
 | 
					        public UserManager(
 | 
				
			||||||
            JellyfinDbProvider dbProvider,
 | 
					            IDbContextFactory<JellyfinDb> dbProvider,
 | 
				
			||||||
            IEventManager eventManager,
 | 
					            IEventManager eventManager,
 | 
				
			||||||
            ICryptoProvider cryptoProvider,
 | 
					            ICryptoProvider cryptoProvider,
 | 
				
			||||||
            INetworkManager networkManager,
 | 
					            INetworkManager networkManager,
 | 
				
			||||||
@ -83,7 +83,7 @@ namespace Jellyfin.Server.Implementations.Users
 | 
				
			|||||||
            _defaultPasswordResetProvider = _passwordResetProviders.OfType<DefaultPasswordResetProvider>().First();
 | 
					            _defaultPasswordResetProvider = _passwordResetProviders.OfType<DefaultPasswordResetProvider>().First();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            _users = new ConcurrentDictionary<Guid, User>();
 | 
					            _users = new ConcurrentDictionary<Guid, User>();
 | 
				
			||||||
            using var dbContext = _dbProvider.CreateContext();
 | 
					            using var dbContext = _dbProvider.CreateDbContext();
 | 
				
			||||||
            foreach (var user in dbContext.Users
 | 
					            foreach (var user in dbContext.Users
 | 
				
			||||||
                .Include(user => user.Permissions)
 | 
					                .Include(user => user.Permissions)
 | 
				
			||||||
                .Include(user => user.Preferences)
 | 
					                .Include(user => user.Preferences)
 | 
				
			||||||
@ -139,31 +139,35 @@ namespace Jellyfin.Server.Implementations.Users
 | 
				
			|||||||
                throw new ArgumentException("The new and old names must be different.");
 | 
					                throw new ArgumentException("The new and old names must be different.");
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            await using var dbContext = _dbProvider.CreateContext();
 | 
					            var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false);
 | 
				
			||||||
 | 
					            await using (dbContext.ConfigureAwait(false))
 | 
				
			||||||
            if (await dbContext.Users
 | 
					 | 
				
			||||||
                .AsQueryable()
 | 
					 | 
				
			||||||
                .AnyAsync(u => u.Username == newName && !u.Id.Equals(user.Id))
 | 
					 | 
				
			||||||
                .ConfigureAwait(false))
 | 
					 | 
				
			||||||
            {
 | 
					            {
 | 
				
			||||||
                throw new ArgumentException(string.Format(
 | 
					                if (await dbContext.Users
 | 
				
			||||||
                    CultureInfo.InvariantCulture,
 | 
					                        .AsQueryable()
 | 
				
			||||||
                    "A user with the name '{0}' already exists.",
 | 
					                        .AnyAsync(u => u.Username == newName && !u.Id.Equals(user.Id))
 | 
				
			||||||
                    newName));
 | 
					                        .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));
 | 
					            OnUserUpdated?.Invoke(this, new GenericEventArgs<User>(user));
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        /// <inheritdoc/>
 | 
					        /// <inheritdoc/>
 | 
				
			||||||
        public async Task UpdateUserAsync(User user)
 | 
					        public async Task UpdateUserAsync(User user)
 | 
				
			||||||
        {
 | 
					        {
 | 
				
			||||||
            await using var dbContext = _dbProvider.CreateContext();
 | 
					            var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false);
 | 
				
			||||||
            dbContext.Users.Update(user);
 | 
					            await using (dbContext.ConfigureAwait(false))
 | 
				
			||||||
            _users[user.Id] = user;
 | 
					            {
 | 
				
			||||||
            await dbContext.SaveChangesAsync().ConfigureAwait(false);
 | 
					                await UpdateUserInternalAsync(dbContext, user).ConfigureAwait(false);
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        internal async Task<User> CreateUserInternalAsync(string name, JellyfinDb dbContext)
 | 
					        internal async Task<User> CreateUserInternalAsync(string name, JellyfinDb dbContext)
 | 
				
			||||||
@ -202,12 +206,15 @@ namespace Jellyfin.Server.Implementations.Users
 | 
				
			|||||||
                    name));
 | 
					                    name));
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            await using var dbContext = _dbProvider.CreateContext();
 | 
					            User newUser;
 | 
				
			||||||
 | 
					            var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false);
 | 
				
			||||||
 | 
					            await using (dbContext.ConfigureAwait(false))
 | 
				
			||||||
 | 
					            {
 | 
				
			||||||
 | 
					                newUser = await CreateUserInternalAsync(name, dbContext).ConfigureAwait(false);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            var 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);
 | 
					            await _eventManager.PublishAsync(new UserCreatedEventArgs(newUser)).ConfigureAwait(false);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -241,9 +248,13 @@ namespace Jellyfin.Server.Implementations.Users
 | 
				
			|||||||
                    nameof(userId));
 | 
					                    nameof(userId));
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            await using var dbContext = _dbProvider.CreateContext();
 | 
					            var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false);
 | 
				
			||||||
            dbContext.Users.Remove(user);
 | 
					            await using (dbContext.ConfigureAwait(false))
 | 
				
			||||||
            await dbContext.SaveChangesAsync().ConfigureAwait(false);
 | 
					            {
 | 
				
			||||||
 | 
					                dbContext.Users.Remove(user);
 | 
				
			||||||
 | 
					                await dbContext.SaveChangesAsync().ConfigureAwait(false);
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            _users.Remove(userId);
 | 
					            _users.Remove(userId);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            await _eventManager.PublishAsync(new UserDeletedEventArgs(user)).ConfigureAwait(false);
 | 
					            await _eventManager.PublishAsync(new UserDeletedEventArgs(user)).ConfigureAwait(false);
 | 
				
			||||||
@ -288,7 +299,7 @@ namespace Jellyfin.Server.Implementations.Users
 | 
				
			|||||||
            user.EasyPassword = newPasswordSha1;
 | 
					            user.EasyPassword = newPasswordSha1;
 | 
				
			||||||
            await UpdateUserAsync(user).ConfigureAwait(false);
 | 
					            await UpdateUserAsync(user).ConfigureAwait(false);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            _eventManager.Publish(new UserPasswordChangedEventArgs(user));
 | 
					            await _eventManager.PublishAsync(new UserPasswordChangedEventArgs(user)).ConfigureAwait(false);
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        /// <inheritdoc/>
 | 
					        /// <inheritdoc/>
 | 
				
			||||||
@ -541,14 +552,17 @@ namespace Jellyfin.Server.Implementations.Users
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
            _logger.LogWarning("No users, creating one with username {UserName}", defaultName);
 | 
					            _logger.LogWarning("No users, creating one with username {UserName}", defaultName);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            await using var dbContext = _dbProvider.CreateContext();
 | 
					            var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false);
 | 
				
			||||||
            var newUser = await CreateUserInternalAsync(defaultName, dbContext).ConfigureAwait(false);
 | 
					            await using (dbContext.ConfigureAwait(false))
 | 
				
			||||||
            newUser.SetPermission(PermissionKind.IsAdministrator, true);
 | 
					            {
 | 
				
			||||||
            newUser.SetPermission(PermissionKind.EnableContentDeletion, true);
 | 
					                var newUser = await CreateUserInternalAsync(defaultName, dbContext).ConfigureAwait(false);
 | 
				
			||||||
            newUser.SetPermission(PermissionKind.EnableRemoteControlOfOtherUsers, true);
 | 
					                newUser.SetPermission(PermissionKind.IsAdministrator, true);
 | 
				
			||||||
 | 
					                newUser.SetPermission(PermissionKind.EnableContentDeletion, true);
 | 
				
			||||||
 | 
					                newUser.SetPermission(PermissionKind.EnableRemoteControlOfOtherUsers, true);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            dbContext.Users.Add(newUser);
 | 
					                dbContext.Users.Add(newUser);
 | 
				
			||||||
            await dbContext.SaveChangesAsync().ConfigureAwait(false);
 | 
					                await dbContext.SaveChangesAsync().ConfigureAwait(false);
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        /// <inheritdoc/>
 | 
					        /// <inheritdoc/>
 | 
				
			||||||
@ -584,105 +598,111 @@ namespace Jellyfin.Server.Implementations.Users
 | 
				
			|||||||
        /// <inheritdoc/>
 | 
					        /// <inheritdoc/>
 | 
				
			||||||
        public async Task UpdateConfigurationAsync(Guid userId, UserConfiguration config)
 | 
					        public async Task UpdateConfigurationAsync(Guid userId, UserConfiguration config)
 | 
				
			||||||
        {
 | 
					        {
 | 
				
			||||||
            await using var dbContext = _dbProvider.CreateContext();
 | 
					            var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false);
 | 
				
			||||||
            var user = dbContext.Users
 | 
					            await using (dbContext.ConfigureAwait(false))
 | 
				
			||||||
                           .Include(u => u.Permissions)
 | 
					            {
 | 
				
			||||||
                           .Include(u => u.Preferences)
 | 
					                var user = dbContext.Users
 | 
				
			||||||
                           .Include(u => u.AccessSchedules)
 | 
					                               .Include(u => u.Permissions)
 | 
				
			||||||
                           .Include(u => u.ProfileImage)
 | 
					                               .Include(u => u.Preferences)
 | 
				
			||||||
                           .FirstOrDefault(u => u.Id.Equals(userId))
 | 
					                               .Include(u => u.AccessSchedules)
 | 
				
			||||||
                       ?? throw new ArgumentException("No user exists with given Id!");
 | 
					                               .Include(u => u.ProfileImage)
 | 
				
			||||||
 | 
					                               .FirstOrDefault(u => u.Id.Equals(userId))
 | 
				
			||||||
 | 
					                           ?? throw new ArgumentException("No user exists with given Id!");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            user.SubtitleMode = config.SubtitleMode;
 | 
					                user.SubtitleMode = config.SubtitleMode;
 | 
				
			||||||
            user.HidePlayedInLatest = config.HidePlayedInLatest;
 | 
					                user.HidePlayedInLatest = config.HidePlayedInLatest;
 | 
				
			||||||
            user.EnableLocalPassword = config.EnableLocalPassword;
 | 
					                user.EnableLocalPassword = config.EnableLocalPassword;
 | 
				
			||||||
            user.PlayDefaultAudioTrack = config.PlayDefaultAudioTrack;
 | 
					                user.PlayDefaultAudioTrack = config.PlayDefaultAudioTrack;
 | 
				
			||||||
            user.DisplayCollectionsView = config.DisplayCollectionsView;
 | 
					                user.DisplayCollectionsView = config.DisplayCollectionsView;
 | 
				
			||||||
            user.DisplayMissingEpisodes = config.DisplayMissingEpisodes;
 | 
					                user.DisplayMissingEpisodes = config.DisplayMissingEpisodes;
 | 
				
			||||||
            user.AudioLanguagePreference = config.AudioLanguagePreference;
 | 
					                user.AudioLanguagePreference = config.AudioLanguagePreference;
 | 
				
			||||||
            user.RememberAudioSelections = config.RememberAudioSelections;
 | 
					                user.RememberAudioSelections = config.RememberAudioSelections;
 | 
				
			||||||
            user.EnableNextEpisodeAutoPlay = config.EnableNextEpisodeAutoPlay;
 | 
					                user.EnableNextEpisodeAutoPlay = config.EnableNextEpisodeAutoPlay;
 | 
				
			||||||
            user.RememberSubtitleSelections = config.RememberSubtitleSelections;
 | 
					                user.RememberSubtitleSelections = config.RememberSubtitleSelections;
 | 
				
			||||||
            user.SubtitleLanguagePreference = config.SubtitleLanguagePreference;
 | 
					                user.SubtitleLanguagePreference = config.SubtitleLanguagePreference;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            user.SetPreference(PreferenceKind.OrderedViews, config.OrderedViews);
 | 
					                user.SetPreference(PreferenceKind.OrderedViews, config.OrderedViews);
 | 
				
			||||||
            user.SetPreference(PreferenceKind.GroupedFolders, config.GroupedFolders);
 | 
					                user.SetPreference(PreferenceKind.GroupedFolders, config.GroupedFolders);
 | 
				
			||||||
            user.SetPreference(PreferenceKind.MyMediaExcludes, config.MyMediaExcludes);
 | 
					                user.SetPreference(PreferenceKind.MyMediaExcludes, config.MyMediaExcludes);
 | 
				
			||||||
            user.SetPreference(PreferenceKind.LatestItemExcludes, config.LatestItemsExcludes);
 | 
					                user.SetPreference(PreferenceKind.LatestItemExcludes, config.LatestItemsExcludes);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            dbContext.Update(user);
 | 
					                dbContext.Update(user);
 | 
				
			||||||
            _users[user.Id] = user;
 | 
					                _users[user.Id] = user;
 | 
				
			||||||
            await dbContext.SaveChangesAsync().ConfigureAwait(false);
 | 
					                await dbContext.SaveChangesAsync().ConfigureAwait(false);
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        /// <inheritdoc/>
 | 
					        /// <inheritdoc/>
 | 
				
			||||||
        public async Task UpdatePolicyAsync(Guid userId, UserPolicy policy)
 | 
					        public async Task UpdatePolicyAsync(Guid userId, UserPolicy policy)
 | 
				
			||||||
        {
 | 
					        {
 | 
				
			||||||
            await using var dbContext = _dbProvider.CreateContext();
 | 
					            var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false);
 | 
				
			||||||
            var user = dbContext.Users
 | 
					            await using (dbContext.ConfigureAwait(false))
 | 
				
			||||||
                           .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,
 | 
					                var user = dbContext.Users
 | 
				
			||||||
                0 => 3,
 | 
					                               .Include(u => u.Permissions)
 | 
				
			||||||
                _ => policy.LoginAttemptsBeforeLockout
 | 
					                               .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.MaxParentalAgeRating = policy.MaxParentalRating;
 | 
					                // The default number of login attempts is 3, but for some god forsaken reason it's sent to the server as "0"
 | 
				
			||||||
            user.EnableUserPreferenceAccess = policy.EnableUserPreferenceAccess;
 | 
					                int? maxLoginAttempts = policy.LoginAttemptsBeforeLockout switch
 | 
				
			||||||
            user.RemoteClientBitrateLimit = policy.RemoteClientBitrateLimit;
 | 
					                {
 | 
				
			||||||
            user.AuthenticationProviderId = policy.AuthenticationProviderId;
 | 
					                    -1 => null,
 | 
				
			||||||
            user.PasswordResetProviderId = policy.PasswordResetProviderId;
 | 
					                    0 => 3,
 | 
				
			||||||
            user.InvalidLoginAttemptCount = policy.InvalidLoginAttemptCount;
 | 
					                    _ => policy.LoginAttemptsBeforeLockout
 | 
				
			||||||
            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();
 | 
					                user.MaxParentalAgeRating = policy.MaxParentalRating;
 | 
				
			||||||
            foreach (var policyAccessSchedule in policy.AccessSchedules)
 | 
					                user.EnableUserPreferenceAccess = policy.EnableUserPreferenceAccess;
 | 
				
			||||||
            {
 | 
					                user.RemoteClientBitrateLimit = policy.RemoteClientBitrateLimit;
 | 
				
			||||||
                user.AccessSchedules.Add(policyAccessSchedule);
 | 
					                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);
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
 | 
					 | 
				
			||||||
            // 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/>
 | 
					        /// <inheritdoc/>
 | 
				
			||||||
@ -693,9 +713,13 @@ namespace Jellyfin.Server.Implementations.Users
 | 
				
			|||||||
                return;
 | 
					                return;
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            await using var dbContext = _dbProvider.CreateContext();
 | 
					            var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false);
 | 
				
			||||||
            dbContext.Remove(user.ProfileImage);
 | 
					            await using (dbContext.ConfigureAwait(false))
 | 
				
			||||||
            await dbContext.SaveChangesAsync().ConfigureAwait(false);
 | 
					            {
 | 
				
			||||||
 | 
					                dbContext.Remove(user.ProfileImage);
 | 
				
			||||||
 | 
					                await dbContext.SaveChangesAsync().ConfigureAwait(false);
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            user.ProfileImage = null;
 | 
					            user.ProfileImage = null;
 | 
				
			||||||
            _users[user.Id] = user;
 | 
					            _users[user.Id] = user;
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
@ -859,5 +883,12 @@ namespace Jellyfin.Server.Implementations.Users
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
            await UpdateUserAsync(user).ConfigureAwait(false);
 | 
					            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,6 +1,5 @@
 | 
				
			|||||||
using System;
 | 
					using System;
 | 
				
			||||||
using System.Collections.Generic;
 | 
					using System.Collections.Generic;
 | 
				
			||||||
using System.IO;
 | 
					 | 
				
			||||||
using System.Reflection;
 | 
					using System.Reflection;
 | 
				
			||||||
using Emby.Drawing;
 | 
					using Emby.Drawing;
 | 
				
			||||||
using Emby.Server.Implementations;
 | 
					using Emby.Server.Implementations;
 | 
				
			||||||
@ -71,19 +70,13 @@ namespace Jellyfin.Server
 | 
				
			|||||||
                Logger.LogWarning("Skia not available. Will fallback to {ImageEncoder}.", nameof(NullImageEncoder));
 | 
					                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.AddEventServices();
 | 
				
			||||||
            serviceCollection.AddSingleton<IBaseItemManager, BaseItemManager>();
 | 
					            serviceCollection.AddSingleton<IBaseItemManager, BaseItemManager>();
 | 
				
			||||||
            serviceCollection.AddSingleton<IEventManager, EventManager>();
 | 
					            serviceCollection.AddSingleton<IEventManager, EventManager>();
 | 
				
			||||||
            serviceCollection.AddSingleton<JellyfinDbProvider>();
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
            serviceCollection.AddSingleton<IActivityManager, ActivityManager>();
 | 
					            serviceCollection.AddSingleton<IActivityManager, ActivityManager>();
 | 
				
			||||||
            serviceCollection.AddSingleton<IUserManager, UserManager>();
 | 
					            serviceCollection.AddSingleton<IUserManager, UserManager>();
 | 
				
			||||||
            serviceCollection.AddSingleton<IDisplayPreferencesManager, DisplayPreferencesManager>();
 | 
					            serviceCollection.AddScoped<IDisplayPreferencesManager, DisplayPreferencesManager>();
 | 
				
			||||||
            serviceCollection.AddSingleton<IDeviceManager, DeviceManager>();
 | 
					            serviceCollection.AddSingleton<IDeviceManager, DeviceManager>();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            // TODO search the assemblies instead of adding them manually?
 | 
					            // TODO search the assemblies instead of adding them manually?
 | 
				
			||||||
 | 
				
			|||||||
@ -19,7 +19,7 @@ namespace Jellyfin.Server.Migrations.Routines
 | 
				
			|||||||
        private const string DbFilename = "activitylog.db";
 | 
					        private const string DbFilename = "activitylog.db";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        private readonly ILogger<MigrateActivityLogDb> _logger;
 | 
					        private readonly ILogger<MigrateActivityLogDb> _logger;
 | 
				
			||||||
        private readonly JellyfinDbProvider _provider;
 | 
					        private readonly IDbContextFactory<JellyfinDb> _provider;
 | 
				
			||||||
        private readonly IServerApplicationPaths _paths;
 | 
					        private readonly IServerApplicationPaths _paths;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        /// <summary>
 | 
					        /// <summary>
 | 
				
			||||||
@ -28,7 +28,7 @@ namespace Jellyfin.Server.Migrations.Routines
 | 
				
			|||||||
        /// <param name="logger">The logger.</param>
 | 
					        /// <param name="logger">The logger.</param>
 | 
				
			||||||
        /// <param name="paths">The server application paths.</param>
 | 
					        /// <param name="paths">The server application paths.</param>
 | 
				
			||||||
        /// <param name="provider">The database provider.</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;
 | 
					            _logger = logger;
 | 
				
			||||||
            _provider = provider;
 | 
					            _provider = provider;
 | 
				
			||||||
@ -68,7 +68,7 @@ namespace Jellyfin.Server.Migrations.Routines
 | 
				
			|||||||
            {
 | 
					            {
 | 
				
			||||||
                using var userDbConnection = SQLite3.Open(Path.Combine(dataPath, "users.db"), ConnectionFlags.ReadOnly, null);
 | 
					                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.");
 | 
					                _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");
 | 
					                var queryResult = connection.Query("SELECT * FROM ActivityLog ORDER BY Id");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
@ -6,6 +6,7 @@ using Jellyfin.Data.Entities.Security;
 | 
				
			|||||||
using Jellyfin.Server.Implementations;
 | 
					using Jellyfin.Server.Implementations;
 | 
				
			||||||
using MediaBrowser.Controller;
 | 
					using MediaBrowser.Controller;
 | 
				
			||||||
using MediaBrowser.Controller.Library;
 | 
					using MediaBrowser.Controller.Library;
 | 
				
			||||||
 | 
					using Microsoft.EntityFrameworkCore;
 | 
				
			||||||
using Microsoft.Extensions.Logging;
 | 
					using Microsoft.Extensions.Logging;
 | 
				
			||||||
using SQLitePCL.pretty;
 | 
					using SQLitePCL.pretty;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -19,7 +20,7 @@ namespace Jellyfin.Server.Migrations.Routines
 | 
				
			|||||||
        private const string DbFilename = "authentication.db";
 | 
					        private const string DbFilename = "authentication.db";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        private readonly ILogger<MigrateAuthenticationDb> _logger;
 | 
					        private readonly ILogger<MigrateAuthenticationDb> _logger;
 | 
				
			||||||
        private readonly JellyfinDbProvider _dbProvider;
 | 
					        private readonly IDbContextFactory<JellyfinDb> _dbProvider;
 | 
				
			||||||
        private readonly IServerApplicationPaths _appPaths;
 | 
					        private readonly IServerApplicationPaths _appPaths;
 | 
				
			||||||
        private readonly IUserManager _userManager;
 | 
					        private readonly IUserManager _userManager;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -32,7 +33,7 @@ namespace Jellyfin.Server.Migrations.Routines
 | 
				
			|||||||
        /// <param name="userManager">The user manager.</param>
 | 
					        /// <param name="userManager">The user manager.</param>
 | 
				
			||||||
        public MigrateAuthenticationDb(
 | 
					        public MigrateAuthenticationDb(
 | 
				
			||||||
            ILogger<MigrateAuthenticationDb> logger,
 | 
					            ILogger<MigrateAuthenticationDb> logger,
 | 
				
			||||||
            JellyfinDbProvider dbProvider,
 | 
					            IDbContextFactory<JellyfinDb> dbProvider,
 | 
				
			||||||
            IServerApplicationPaths appPaths,
 | 
					            IServerApplicationPaths appPaths,
 | 
				
			||||||
            IUserManager userManager)
 | 
					            IUserManager userManager)
 | 
				
			||||||
        {
 | 
					        {
 | 
				
			||||||
@ -60,7 +61,7 @@ namespace Jellyfin.Server.Migrations.Routines
 | 
				
			|||||||
                ConnectionFlags.ReadOnly,
 | 
					                ConnectionFlags.ReadOnly,
 | 
				
			||||||
                null))
 | 
					                null))
 | 
				
			||||||
            {
 | 
					            {
 | 
				
			||||||
                using var dbContext = _dbProvider.CreateContext();
 | 
					                using var dbContext = _dbProvider.CreateDbContext();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                var authenticatedDevices = connection.Query("SELECT * FROM Tokens");
 | 
					                var authenticatedDevices = connection.Query("SELECT * FROM Tokens");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
@ -10,6 +10,7 @@ using Jellyfin.Server.Implementations;
 | 
				
			|||||||
using MediaBrowser.Controller;
 | 
					using MediaBrowser.Controller;
 | 
				
			||||||
using MediaBrowser.Controller.Library;
 | 
					using MediaBrowser.Controller.Library;
 | 
				
			||||||
using MediaBrowser.Model.Dto;
 | 
					using MediaBrowser.Model.Dto;
 | 
				
			||||||
 | 
					using Microsoft.EntityFrameworkCore;
 | 
				
			||||||
using Microsoft.Extensions.Logging;
 | 
					using Microsoft.Extensions.Logging;
 | 
				
			||||||
using SQLitePCL.pretty;
 | 
					using SQLitePCL.pretty;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -24,7 +25,7 @@ namespace Jellyfin.Server.Migrations.Routines
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
        private readonly ILogger<MigrateDisplayPreferencesDb> _logger;
 | 
					        private readonly ILogger<MigrateDisplayPreferencesDb> _logger;
 | 
				
			||||||
        private readonly IServerApplicationPaths _paths;
 | 
					        private readonly IServerApplicationPaths _paths;
 | 
				
			||||||
        private readonly JellyfinDbProvider _provider;
 | 
					        private readonly IDbContextFactory<JellyfinDb> _provider;
 | 
				
			||||||
        private readonly JsonSerializerOptions _jsonOptions;
 | 
					        private readonly JsonSerializerOptions _jsonOptions;
 | 
				
			||||||
        private readonly IUserManager _userManager;
 | 
					        private readonly IUserManager _userManager;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -38,7 +39,7 @@ namespace Jellyfin.Server.Migrations.Routines
 | 
				
			|||||||
        public MigrateDisplayPreferencesDb(
 | 
					        public MigrateDisplayPreferencesDb(
 | 
				
			||||||
            ILogger<MigrateDisplayPreferencesDb> logger,
 | 
					            ILogger<MigrateDisplayPreferencesDb> logger,
 | 
				
			||||||
            IServerApplicationPaths paths,
 | 
					            IServerApplicationPaths paths,
 | 
				
			||||||
            JellyfinDbProvider provider,
 | 
					            IDbContextFactory<JellyfinDb> provider,
 | 
				
			||||||
            IUserManager userManager)
 | 
					            IUserManager userManager)
 | 
				
			||||||
        {
 | 
					        {
 | 
				
			||||||
            _logger = logger;
 | 
					            _logger = logger;
 | 
				
			||||||
@ -84,7 +85,7 @@ namespace Jellyfin.Server.Migrations.Routines
 | 
				
			|||||||
            var dbFilePath = Path.Combine(_paths.DataPath, DbFilename);
 | 
					            var dbFilePath = Path.Combine(_paths.DataPath, DbFilename);
 | 
				
			||||||
            using (var connection = SQLite3.Open(dbFilePath, ConnectionFlags.ReadOnly, null))
 | 
					            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");
 | 
					                var results = connection.Query("SELECT * FROM userdisplaypreferences");
 | 
				
			||||||
                foreach (var result in results)
 | 
					                foreach (var result in results)
 | 
				
			||||||
 | 
				
			|||||||
@ -11,6 +11,7 @@ using MediaBrowser.Controller.Entities;
 | 
				
			|||||||
using MediaBrowser.Model.Configuration;
 | 
					using MediaBrowser.Model.Configuration;
 | 
				
			||||||
using MediaBrowser.Model.Serialization;
 | 
					using MediaBrowser.Model.Serialization;
 | 
				
			||||||
using MediaBrowser.Model.Users;
 | 
					using MediaBrowser.Model.Users;
 | 
				
			||||||
 | 
					using Microsoft.EntityFrameworkCore;
 | 
				
			||||||
using Microsoft.Extensions.Logging;
 | 
					using Microsoft.Extensions.Logging;
 | 
				
			||||||
using SQLitePCL.pretty;
 | 
					using SQLitePCL.pretty;
 | 
				
			||||||
using JsonSerializer = System.Text.Json.JsonSerializer;
 | 
					using JsonSerializer = System.Text.Json.JsonSerializer;
 | 
				
			||||||
@ -26,7 +27,7 @@ namespace Jellyfin.Server.Migrations.Routines
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
        private readonly ILogger<MigrateUserDb> _logger;
 | 
					        private readonly ILogger<MigrateUserDb> _logger;
 | 
				
			||||||
        private readonly IServerApplicationPaths _paths;
 | 
					        private readonly IServerApplicationPaths _paths;
 | 
				
			||||||
        private readonly JellyfinDbProvider _provider;
 | 
					        private readonly IDbContextFactory<JellyfinDb> _provider;
 | 
				
			||||||
        private readonly IXmlSerializer _xmlSerializer;
 | 
					        private readonly IXmlSerializer _xmlSerializer;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        /// <summary>
 | 
					        /// <summary>
 | 
				
			||||||
@ -39,7 +40,7 @@ namespace Jellyfin.Server.Migrations.Routines
 | 
				
			|||||||
        public MigrateUserDb(
 | 
					        public MigrateUserDb(
 | 
				
			||||||
            ILogger<MigrateUserDb> logger,
 | 
					            ILogger<MigrateUserDb> logger,
 | 
				
			||||||
            IServerApplicationPaths paths,
 | 
					            IServerApplicationPaths paths,
 | 
				
			||||||
            JellyfinDbProvider provider,
 | 
					            IDbContextFactory<JellyfinDb> provider,
 | 
				
			||||||
            IXmlSerializer xmlSerializer)
 | 
					            IXmlSerializer xmlSerializer)
 | 
				
			||||||
        {
 | 
					        {
 | 
				
			||||||
            _logger = logger;
 | 
					            _logger = logger;
 | 
				
			||||||
@ -65,7 +66,7 @@ namespace Jellyfin.Server.Migrations.Routines
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
            using (var connection = SQLite3.Open(Path.Combine(dataPath, DbFilename), ConnectionFlags.ReadOnly, null))
 | 
					            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");
 | 
					                var queryResult = connection.Query("SELECT * FROM LocalUsersv2");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
@ -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.
 | 
					                // 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;
 | 
					                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);
 | 
					                await appHost.InitializeServices().ConfigureAwait(false);
 | 
				
			||||||
                Migrations.MigrationRunner.Run(appHost, _loggerFactory);
 | 
					                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");
 | 
					                    _logger.LogInformation("Running query planner optimizations in the database... This might take a while");
 | 
				
			||||||
                    // Run before disposing the application
 | 
					                    // Run before disposing the application
 | 
				
			||||||
                    using var context = appHost.Resolve<JellyfinDbProvider>().CreateContext();
 | 
					                    var context = await appHost.ServiceProvider.GetRequiredService<IDbContextFactory<JellyfinDb>>().CreateDbContextAsync().ConfigureAwait(false);
 | 
				
			||||||
                    if (context.Database.IsSqlite())
 | 
					                    await using (context.ConfigureAwait(false))
 | 
				
			||||||
                    {
 | 
					                    {
 | 
				
			||||||
                        context.Database.ExecuteSqlRaw("PRAGMA optimize");
 | 
					                        if (context.Database.IsSqlite())
 | 
				
			||||||
 | 
					                        {
 | 
				
			||||||
 | 
					                            await context.Database.ExecuteSqlRawAsync("PRAGMA optimize").ConfigureAwait(false);
 | 
				
			||||||
 | 
					                        }
 | 
				
			||||||
                    }
 | 
					                    }
 | 
				
			||||||
                }
 | 
					                }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
@ -9,6 +9,7 @@ using Jellyfin.MediaEncoding.Hls.Extensions;
 | 
				
			|||||||
using Jellyfin.Networking.Configuration;
 | 
					using Jellyfin.Networking.Configuration;
 | 
				
			||||||
using Jellyfin.Server.Extensions;
 | 
					using Jellyfin.Server.Extensions;
 | 
				
			||||||
using Jellyfin.Server.Implementations;
 | 
					using Jellyfin.Server.Implementations;
 | 
				
			||||||
 | 
					using Jellyfin.Server.Implementations.Extensions;
 | 
				
			||||||
using Jellyfin.Server.Infrastructure;
 | 
					using Jellyfin.Server.Infrastructure;
 | 
				
			||||||
using Jellyfin.Server.Middleware;
 | 
					using Jellyfin.Server.Middleware;
 | 
				
			||||||
using MediaBrowser.Common.Net;
 | 
					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
 | 
					            // TODO remove once this is fixed upstream https://github.com/dotnet/aspnetcore/issues/34371
 | 
				
			||||||
            services.AddSingleton<IActionResultExecutor<PhysicalFileResult>, SymlinkFollowingPhysicalFileResultExecutor>();
 | 
					            services.AddSingleton<IActionResultExecutor<PhysicalFileResult>, SymlinkFollowingPhysicalFileResultExecutor>();
 | 
				
			||||||
            services.AddJellyfinApi(_serverApplicationHost.GetApiPluginAssemblies(), _serverConfigurationManager.GetNetworkConfiguration());
 | 
					            services.AddJellyfinApi(_serverApplicationHost.GetApiPluginAssemblies(), _serverConfigurationManager.GetNetworkConfiguration());
 | 
				
			||||||
 | 
					            services.AddJellyfinDbContext();
 | 
				
			||||||
            services.AddJellyfinApiSwagger();
 | 
					            services.AddJellyfinApiSwagger();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            // configure custom legacy authentication
 | 
					            // configure custom legacy authentication
 | 
				
			||||||
 | 
				
			|||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user