remove JellyfinDbProvider and add second level caching

This commit is contained in:
cvium
2022-10-21 11:55:32 +02:00
parent 509c6ec24c
commit b836fe9685
17 changed files with 516 additions and 443 deletions

View File

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

View File

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

View File

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

View File

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

View File

@@ -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);
}
}
}

View File

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

View File

@@ -4,6 +4,7 @@ using System;
using System.Collections.Generic;
using System.Net;
using System.Threading.Tasks;
using EFCoreSecondLevelCacheInterceptor;
using MediaBrowser.Controller;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Net;
@@ -15,12 +16,12 @@ namespace Jellyfin.Server.Implementations.Security
{
public class AuthorizationContext : IAuthorizationContext
{
private readonly JellyfinDbProvider _jellyfinDbProvider;
private readonly IDbContextFactory<JellyfinDb> _jellyfinDbProvider;
private readonly IUserManager _userManager;
private readonly IServerApplicationHost _serverApplicationHost;
public AuthorizationContext(
JellyfinDbProvider jellyfinDb,
IDbContextFactory<JellyfinDb> jellyfinDb,
IUserManager userManager,
IServerApplicationHost serverApplicationHost)
{
@@ -121,96 +122,99 @@ namespace Jellyfin.Server.Implementations.Security
#pragma warning restore CA1508
authInfo.HasToken = true;
await using var dbContext = _jellyfinDbProvider.CreateContext();
var device = await dbContext.Devices.FirstOrDefaultAsync(d => d.AccessToken == token).ConfigureAwait(false);
if (device != null)
var dbContext = await _jellyfinDbProvider.CreateDbContextAsync().ConfigureAwait(false);
await using (dbContext.ConfigureAwait(false))
{
authInfo.IsAuthenticated = true;
var updateToken = false;
var device = await dbContext.Devices.FirstOrDefaultAsync(d => d.AccessToken == token).ConfigureAwait(false);
// TODO: Remove these checks for IsNullOrWhiteSpace
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)
if (device != null)
{
authInfo.IsAuthenticated = true;
authInfo.Client = key.Name;
authInfo.Token = key.AccessToken;
var updateToken = false;
// TODO: Remove these checks for IsNullOrWhiteSpace
if (string.IsNullOrWhiteSpace(authInfo.Client))
{
authInfo.Client = device.AppName;
}
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))
{
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))
{
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>

View File

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

View File

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