diff options
Diffstat (limited to 'Jellyfin.Server.Implementations')
14 files changed, 1186 insertions, 430 deletions
diff --git a/Jellyfin.Server.Implementations/Activity/ActivityManager.cs b/Jellyfin.Server.Implementations/Activity/ActivityManager.cs index 592c53fe5..9d6ca6aab 100644 --- a/Jellyfin.Server.Implementations/Activity/ActivityManager.cs +++ b/Jellyfin.Server.Implementations/Activity/ActivityManager.cs @@ -15,13 +15,13 @@ namespace Jellyfin.Server.Implementations.Activity /// </summary> public class ActivityManager : IActivityManager { - private readonly JellyfinDbProvider _provider; + private readonly IDbContextFactory<JellyfinDb> _provider; /// <summary> /// Initializes a new instance of the <see cref="ActivityManager"/> class. /// </summary> /// <param name="provider">The Jellyfin database provider.</param> - public ActivityManager(JellyfinDbProvider provider) + public ActivityManager(IDbContextFactory<JellyfinDb> provider) { _provider = provider; } @@ -32,10 +32,12 @@ namespace Jellyfin.Server.Implementations.Activity /// <inheritdoc/> public async Task CreateAsync(ActivityLog entry) { - await using var dbContext = _provider.CreateContext(); - - dbContext.ActivityLogs.Add(entry); - await dbContext.SaveChangesAsync().ConfigureAwait(false); + var dbContext = await _provider.CreateDbContextAsync().ConfigureAwait(false); + await using (dbContext.ConfigureAwait(false)) + { + dbContext.ActivityLogs.Add(entry); + await dbContext.SaveChangesAsync().ConfigureAwait(false); + } EntryCreated?.Invoke(this, new GenericEventArgs<ActivityLogEntry>(ConvertToOldModel(entry))); } @@ -43,44 +45,47 @@ namespace Jellyfin.Server.Implementations.Activity /// <inheritdoc/> public async Task<QueryResult<ActivityLogEntry>> GetPagedResultAsync(ActivityLogQuery query) { - await using var dbContext = _provider.CreateContext(); + var dbContext = await _provider.CreateDbContextAsync().ConfigureAwait(false); + await using (dbContext.ConfigureAwait(false)) + { + IQueryable<ActivityLog> entries = dbContext.ActivityLogs + .OrderByDescending(entry => entry.DateCreated); - IQueryable<ActivityLog> entries = dbContext.ActivityLogs - .AsQueryable() - .OrderByDescending(entry => entry.DateCreated); + if (query.MinDate.HasValue) + { + entries = entries.Where(entry => entry.DateCreated >= query.MinDate); + } - if (query.MinDate.HasValue) - { - entries = entries.Where(entry => entry.DateCreated >= query.MinDate); - } + if (query.HasUserId.HasValue) + { + entries = entries.Where(entry => (!entry.UserId.Equals(default)) == query.HasUserId.Value); + } - if (query.HasUserId.HasValue) - { - entries = entries.Where(entry => (!entry.UserId.Equals(default)) == query.HasUserId.Value); + return new QueryResult<ActivityLogEntry>( + query.Skip, + await entries.CountAsync().ConfigureAwait(false), + await entries + .Skip(query.Skip ?? 0) + .Take(query.Limit ?? 100) + .AsAsyncEnumerable() + .Select(ConvertToOldModel) + .ToListAsync() + .ConfigureAwait(false)); } - - return new QueryResult<ActivityLogEntry>( - query.Skip, - await entries.CountAsync().ConfigureAwait(false), - await entries - .Skip(query.Skip ?? 0) - .Take(query.Limit ?? 100) - .AsAsyncEnumerable() - .Select(ConvertToOldModel) - .ToListAsync() - .ConfigureAwait(false)); } /// <inheritdoc /> public async Task CleanAsync(DateTime startDate) { - await using var dbContext = _provider.CreateContext(); - var entries = dbContext.ActivityLogs - .AsQueryable() - .Where(entry => entry.DateCreated <= startDate); + var dbContext = await _provider.CreateDbContextAsync().ConfigureAwait(false); + await using (dbContext.ConfigureAwait(false)) + { + var entries = dbContext.ActivityLogs + .Where(entry => entry.DateCreated <= startDate); - dbContext.RemoveRange(entries); - await dbContext.SaveChangesAsync().ConfigureAwait(false); + dbContext.RemoveRange(entries); + await dbContext.SaveChangesAsync().ConfigureAwait(false); + } } private static ActivityLogEntry ConvertToOldModel(ActivityLog entry) diff --git a/Jellyfin.Server.Implementations/Devices/DeviceManager.cs b/Jellyfin.Server.Implementations/Devices/DeviceManager.cs index 3203bed18..eeb958c62 100644 --- a/Jellyfin.Server.Implementations/Devices/DeviceManager.cs +++ b/Jellyfin.Server.Implementations/Devices/DeviceManager.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Concurrent; +using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using Jellyfin.Data.Entities; @@ -22,7 +23,7 @@ namespace Jellyfin.Server.Implementations.Devices /// </summary> public class DeviceManager : IDeviceManager { - private readonly JellyfinDbProvider _dbProvider; + private readonly IDbContextFactory<JellyfinDb> _dbProvider; private readonly IUserManager _userManager; private readonly ConcurrentDictionary<string, ClientCapabilities> _capabilitiesMap = new(); @@ -31,7 +32,7 @@ namespace Jellyfin.Server.Implementations.Devices /// </summary> /// <param name="dbProvider">The database provider.</param> /// <param name="userManager">The user manager.</param> - public DeviceManager(JellyfinDbProvider dbProvider, IUserManager userManager) + public DeviceManager(IDbContextFactory<JellyfinDb> dbProvider, IUserManager userManager) { _dbProvider = dbProvider; _userManager = userManager; @@ -49,39 +50,50 @@ namespace Jellyfin.Server.Implementations.Devices /// <inheritdoc /> public async Task UpdateDeviceOptions(string deviceId, string deviceName) { - await using var dbContext = _dbProvider.CreateContext(); - var deviceOptions = await dbContext.DeviceOptions.AsQueryable().FirstOrDefaultAsync(dev => dev.DeviceId == deviceId).ConfigureAwait(false); - if (deviceOptions == null) + DeviceOptions? deviceOptions; + var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false); + await using (dbContext.ConfigureAwait(false)) { - deviceOptions = new DeviceOptions(deviceId); - dbContext.DeviceOptions.Add(deviceOptions); + deviceOptions = await dbContext.DeviceOptions.AsQueryable().FirstOrDefaultAsync(dev => dev.DeviceId == deviceId).ConfigureAwait(false); + if (deviceOptions == null) + { + deviceOptions = new DeviceOptions(deviceId); + dbContext.DeviceOptions.Add(deviceOptions); + } + + deviceOptions.CustomName = deviceName; + await dbContext.SaveChangesAsync().ConfigureAwait(false); } - deviceOptions.CustomName = deviceName; - await dbContext.SaveChangesAsync().ConfigureAwait(false); - DeviceOptionsUpdated?.Invoke(this, new GenericEventArgs<Tuple<string, DeviceOptions>>(new Tuple<string, DeviceOptions>(deviceId, deviceOptions))); } /// <inheritdoc /> public async Task<Device> CreateDevice(Device device) { - await using var dbContext = _dbProvider.CreateContext(); + var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false); + await using (dbContext.ConfigureAwait(false)) + { + dbContext.Devices.Add(device); - dbContext.Devices.Add(device); + await dbContext.SaveChangesAsync().ConfigureAwait(false); + } - await dbContext.SaveChangesAsync().ConfigureAwait(false); return device; } /// <inheritdoc /> public async Task<DeviceOptions> GetDeviceOptions(string deviceId) { - await using var dbContext = _dbProvider.CreateContext(); - var deviceOptions = await dbContext.DeviceOptions - .AsQueryable() - .FirstOrDefaultAsync(d => d.DeviceId == deviceId) - .ConfigureAwait(false); + var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false); + DeviceOptions? deviceOptions; + await using (dbContext.ConfigureAwait(false)) + { + deviceOptions = await dbContext.DeviceOptions + .AsNoTracking() + .FirstOrDefaultAsync(d => d.DeviceId == deviceId) + .ConfigureAwait(false); + } return deviceOptions ?? new DeviceOptions(deviceId); } @@ -97,14 +109,17 @@ namespace Jellyfin.Server.Implementations.Devices /// <inheritdoc /> public async Task<DeviceInfo?> GetDevice(string id) { - await using var dbContext = _dbProvider.CreateContext(); - var device = await dbContext.Devices - .AsQueryable() - .Where(d => d.DeviceId == id) - .OrderByDescending(d => d.DateLastActivity) - .Include(d => d.User) - .FirstOrDefaultAsync() - .ConfigureAwait(false); + Device? device; + var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false); + await using (dbContext.ConfigureAwait(false)) + { + device = await dbContext.Devices + .Where(d => d.DeviceId == id) + .OrderByDescending(d => d.DateLastActivity) + .Include(d => d.User) + .FirstOrDefaultAsync() + .ConfigureAwait(false); + } var deviceInfo = device == null ? null : ToDeviceInfo(device); @@ -114,41 +129,40 @@ namespace Jellyfin.Server.Implementations.Devices /// <inheritdoc /> public async Task<QueryResult<Device>> GetDevices(DeviceQuery query) { - await using var dbContext = _dbProvider.CreateContext(); + var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false); + await using (dbContext.ConfigureAwait(false)) + { + var devices = dbContext.Devices.AsQueryable(); - var devices = dbContext.Devices.AsQueryable(); + if (query.UserId.HasValue) + { + devices = devices.Where(device => device.UserId.Equals(query.UserId.Value)); + } - if (query.UserId.HasValue) - { - devices = devices.Where(device => device.UserId.Equals(query.UserId.Value)); - } + if (query.DeviceId != null) + { + devices = devices.Where(device => device.DeviceId == query.DeviceId); + } - if (query.DeviceId != null) - { - devices = devices.Where(device => device.DeviceId == query.DeviceId); - } + if (query.AccessToken != null) + { + devices = devices.Where(device => device.AccessToken == query.AccessToken); + } - if (query.AccessToken != null) - { - devices = devices.Where(device => device.AccessToken == query.AccessToken); - } + var count = await devices.CountAsync().ConfigureAwait(false); - var count = await devices.CountAsync().ConfigureAwait(false); + if (query.Skip.HasValue) + { + devices = devices.Skip(query.Skip.Value); + } - if (query.Skip.HasValue) - { - devices = devices.Skip(query.Skip.Value); - } + if (query.Limit.HasValue) + { + devices = devices.Take(query.Limit.Value); + } - if (query.Limit.HasValue) - { - devices = devices.Take(query.Limit.Value); + return new QueryResult<Device>(query.Skip, count, await devices.ToListAsync().ConfigureAwait(false)); } - - return new QueryResult<Device>( - query.Skip, - count, - await devices.ToListAsync().ConfigureAwait(false)); } /// <inheritdoc /> @@ -165,46 +179,49 @@ namespace Jellyfin.Server.Implementations.Devices /// <inheritdoc /> public async Task<QueryResult<DeviceInfo>> GetDevicesForUser(Guid? userId, bool? supportsSync) { - await using var dbContext = _dbProvider.CreateContext(); - var sessions = dbContext.Devices - .Include(d => d.User) - .AsQueryable() - .OrderByDescending(d => d.DateLastActivity) - .ThenBy(d => d.DeviceId) - .AsAsyncEnumerable(); - - if (supportsSync.HasValue) + IAsyncEnumerable<Device> sessions; + var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false); + await using (dbContext.ConfigureAwait(false)) { - sessions = sessions.Where(i => GetCapabilities(i.DeviceId).SupportsSync == supportsSync.Value); - } + sessions = dbContext.Devices + .Include(d => d.User) + .OrderByDescending(d => d.DateLastActivity) + .ThenBy(d => d.DeviceId) + .AsAsyncEnumerable(); - if (userId.HasValue) - { - var user = _userManager.GetUserById(userId.Value); + if (supportsSync.HasValue) + { + sessions = sessions.Where(i => GetCapabilities(i.DeviceId).SupportsSync == supportsSync.Value); + } - sessions = sessions.Where(i => CanAccessDevice(user, i.DeviceId)); - } + if (userId.HasValue) + { + var user = _userManager.GetUserById(userId.Value); - var array = await sessions.Select(device => ToDeviceInfo(device)).ToArrayAsync().ConfigureAwait(false); + sessions = sessions.Where(i => CanAccessDevice(user, i.DeviceId)); + } - return new QueryResult<DeviceInfo>(array); + 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 /> public bool CanAccessDevice(User user, string deviceId) { - if (user == null) - { - throw new ArgumentNullException(nameof(user)); - } + ArgumentNullException.ThrowIfNull(user); if (string.IsNullOrEmpty(deviceId)) { diff --git a/Jellyfin.Server.Implementations/Extensions/ServiceCollectionExtensions.cs b/Jellyfin.Server.Implementations/Extensions/ServiceCollectionExtensions.cs new file mode 100644 index 000000000..f98a0aede --- /dev/null +++ b/Jellyfin.Server.Implementations/Extensions/ServiceCollectionExtensions.cs @@ -0,0 +1,43 @@ +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_") + // 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; + } +} diff --git a/Jellyfin.Server.Implementations/Jellyfin.Server.Implementations.csproj b/Jellyfin.Server.Implementations/Jellyfin.Server.Implementations.csproj index 678f96083..5caac4523 100644 --- a/Jellyfin.Server.Implementations/Jellyfin.Server.Implementations.csproj +++ b/Jellyfin.Server.Implementations/Jellyfin.Server.Implementations.csproj @@ -26,14 +26,15 @@ </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.8" /> - <PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="6.0.8" /> - <PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="6.0.8"> + <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="6.0.11" /> + <PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="6.0.11" /> + <PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="6.0.11"> <PrivateAssets>all</PrivateAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> </PackageReference> - <PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="6.0.8"> + <PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="6.0.11"> <PrivateAssets>all</PrivateAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> </PackageReference> diff --git a/Jellyfin.Server.Implementations/JellyfinDbProvider.cs b/Jellyfin.Server.Implementations/JellyfinDbProvider.cs deleted file mode 100644 index c2c5198d1..000000000 --- a/Jellyfin.Server.Implementations/JellyfinDbProvider.cs +++ /dev/null @@ -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); - } - } -} diff --git a/Jellyfin.Server.Implementations/Migrations/20221022080052_AddIndexActivityLogsDateCreated.Designer.cs b/Jellyfin.Server.Implementations/Migrations/20221022080052_AddIndexActivityLogsDateCreated.Designer.cs new file mode 100644 index 000000000..03e3f3c92 --- /dev/null +++ b/Jellyfin.Server.Implementations/Migrations/20221022080052_AddIndexActivityLogsDateCreated.Designer.cs @@ -0,0 +1,657 @@ +#pragma warning disable CS1591 + +// <auto-generated /> +using System; +using Jellyfin.Server.Implementations; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace Jellyfin.Server.Implementations.Migrations +{ + [DbContext(typeof(JellyfinDb))] + [Migration("20221022080052_AddIndexActivityLogsDateCreated")] + partial class AddIndexActivityLogsDateCreated + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasDefaultSchema("jellyfin") + .HasAnnotation("ProductVersion", "6.0.9"); + + modelBuilder.Entity("Jellyfin.Data.Entities.AccessSchedule", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property<int>("DayOfWeek") + .HasColumnType("INTEGER"); + + b.Property<double>("EndHour") + .HasColumnType("REAL"); + + b.Property<double>("StartHour") + .HasColumnType("REAL"); + + b.Property<Guid>("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("AccessSchedules", "jellyfin"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.ActivityLog", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property<DateTime>("DateCreated") + .HasColumnType("TEXT"); + + b.Property<string>("ItemId") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property<int>("LogSeverity") + .HasColumnType("INTEGER"); + + b.Property<string>("Name") + .IsRequired() + .HasMaxLength(512) + .HasColumnType("TEXT"); + + b.Property<string>("Overview") + .HasMaxLength(512) + .HasColumnType("TEXT"); + + b.Property<uint>("RowVersion") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.Property<string>("ShortOverview") + .HasMaxLength(512) + .HasColumnType("TEXT"); + + b.Property<string>("Type") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property<Guid>("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("DateCreated"); + + b.ToTable("ActivityLogs", "jellyfin"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.CustomItemDisplayPreferences", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property<string>("Client") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("TEXT"); + + b.Property<Guid>("ItemId") + .HasColumnType("TEXT"); + + b.Property<string>("Key") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property<Guid>("UserId") + .HasColumnType("TEXT"); + + b.Property<string>("Value") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("UserId", "ItemId", "Client", "Key") + .IsUnique(); + + b.ToTable("CustomItemDisplayPreferences", "jellyfin"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.DisplayPreferences", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property<int>("ChromecastVersion") + .HasColumnType("INTEGER"); + + b.Property<string>("Client") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("TEXT"); + + b.Property<string>("DashboardTheme") + .HasMaxLength(32) + .HasColumnType("TEXT"); + + b.Property<bool>("EnableNextVideoInfoOverlay") + .HasColumnType("INTEGER"); + + b.Property<int?>("IndexBy") + .HasColumnType("INTEGER"); + + b.Property<Guid>("ItemId") + .HasColumnType("TEXT"); + + b.Property<int>("ScrollDirection") + .HasColumnType("INTEGER"); + + b.Property<bool>("ShowBackdrop") + .HasColumnType("INTEGER"); + + b.Property<bool>("ShowSidebar") + .HasColumnType("INTEGER"); + + b.Property<int>("SkipBackwardLength") + .HasColumnType("INTEGER"); + + b.Property<int>("SkipForwardLength") + .HasColumnType("INTEGER"); + + b.Property<string>("TvHome") + .HasMaxLength(32) + .HasColumnType("TEXT"); + + b.Property<Guid>("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("UserId", "ItemId", "Client") + .IsUnique(); + + b.ToTable("DisplayPreferences", "jellyfin"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.HomeSection", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property<int>("DisplayPreferencesId") + .HasColumnType("INTEGER"); + + b.Property<int>("Order") + .HasColumnType("INTEGER"); + + b.Property<int>("Type") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("DisplayPreferencesId"); + + b.ToTable("HomeSection", "jellyfin"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.ImageInfo", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property<DateTime>("LastModified") + .HasColumnType("TEXT"); + + b.Property<string>("Path") + .IsRequired() + .HasMaxLength(512) + .HasColumnType("TEXT"); + + b.Property<Guid?>("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("UserId") + .IsUnique(); + + b.ToTable("ImageInfos", "jellyfin"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.ItemDisplayPreferences", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property<string>("Client") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("TEXT"); + + b.Property<int?>("IndexBy") + .HasColumnType("INTEGER"); + + b.Property<Guid>("ItemId") + .HasColumnType("TEXT"); + + b.Property<bool>("RememberIndexing") + .HasColumnType("INTEGER"); + + b.Property<bool>("RememberSorting") + .HasColumnType("INTEGER"); + + b.Property<string>("SortBy") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("TEXT"); + + b.Property<int>("SortOrder") + .HasColumnType("INTEGER"); + + b.Property<Guid>("UserId") + .HasColumnType("TEXT"); + + b.Property<int>("ViewType") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("ItemDisplayPreferences", "jellyfin"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.Permission", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property<int>("Kind") + .HasColumnType("INTEGER"); + + b.Property<Guid?>("Permission_Permissions_Guid") + .HasColumnType("TEXT"); + + b.Property<uint>("RowVersion") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.Property<Guid?>("UserId") + .HasColumnType("TEXT"); + + b.Property<bool>("Value") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("UserId", "Kind") + .IsUnique() + .HasFilter("[UserId] IS NOT NULL"); + + b.ToTable("Permissions", "jellyfin"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.Preference", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property<int>("Kind") + .HasColumnType("INTEGER"); + + b.Property<Guid?>("Preference_Preferences_Guid") + .HasColumnType("TEXT"); + + b.Property<uint>("RowVersion") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.Property<Guid?>("UserId") + .HasColumnType("TEXT"); + + b.Property<string>("Value") + .IsRequired() + .HasMaxLength(65535) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("UserId", "Kind") + .IsUnique() + .HasFilter("[UserId] IS NOT NULL"); + + b.ToTable("Preferences", "jellyfin"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.Security.ApiKey", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property<string>("AccessToken") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property<DateTime>("DateCreated") + .HasColumnType("TEXT"); + + b.Property<DateTime>("DateLastActivity") + .HasColumnType("TEXT"); + + b.Property<string>("Name") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("AccessToken") + .IsUnique(); + + b.ToTable("ApiKeys", "jellyfin"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.Security.Device", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property<string>("AccessToken") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property<string>("AppName") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("TEXT"); + + b.Property<string>("AppVersion") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("TEXT"); + + b.Property<DateTime>("DateCreated") + .HasColumnType("TEXT"); + + b.Property<DateTime>("DateLastActivity") + .HasColumnType("TEXT"); + + b.Property<DateTime>("DateModified") + .HasColumnType("TEXT"); + + b.Property<string>("DeviceId") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property<string>("DeviceName") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("TEXT"); + + b.Property<bool>("IsActive") + .HasColumnType("INTEGER"); + + b.Property<Guid>("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("DeviceId"); + + b.HasIndex("AccessToken", "DateLastActivity"); + + b.HasIndex("DeviceId", "DateLastActivity"); + + b.HasIndex("UserId", "DeviceId"); + + b.ToTable("Devices", "jellyfin"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.Security.DeviceOptions", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property<string>("CustomName") + .HasColumnType("TEXT"); + + b.Property<string>("DeviceId") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("DeviceId") + .IsUnique(); + + b.ToTable("DeviceOptions", "jellyfin"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.User", b => + { + b.Property<Guid>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property<string>("AudioLanguagePreference") + .HasMaxLength(255) + .HasColumnType("TEXT"); + + b.Property<string>("AuthenticationProviderId") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("TEXT"); + + b.Property<bool>("DisplayCollectionsView") + .HasColumnType("INTEGER"); + + b.Property<bool>("DisplayMissingEpisodes") + .HasColumnType("INTEGER"); + + b.Property<string>("EasyPassword") + .HasMaxLength(65535) + .HasColumnType("TEXT"); + + b.Property<bool>("EnableAutoLogin") + .HasColumnType("INTEGER"); + + b.Property<bool>("EnableLocalPassword") + .HasColumnType("INTEGER"); + + b.Property<bool>("EnableNextEpisodeAutoPlay") + .HasColumnType("INTEGER"); + + b.Property<bool>("EnableUserPreferenceAccess") + .HasColumnType("INTEGER"); + + b.Property<bool>("HidePlayedInLatest") + .HasColumnType("INTEGER"); + + b.Property<long>("InternalId") + .HasColumnType("INTEGER"); + + b.Property<int>("InvalidLoginAttemptCount") + .HasColumnType("INTEGER"); + + b.Property<DateTime?>("LastActivityDate") + .HasColumnType("TEXT"); + + b.Property<DateTime?>("LastLoginDate") + .HasColumnType("TEXT"); + + b.Property<int?>("LoginAttemptsBeforeLockout") + .HasColumnType("INTEGER"); + + b.Property<int>("MaxActiveSessions") + .HasColumnType("INTEGER"); + + b.Property<int?>("MaxParentalAgeRating") + .HasColumnType("INTEGER"); + + b.Property<bool>("MustUpdatePassword") + .HasColumnType("INTEGER"); + + b.Property<string>("Password") + .HasMaxLength(65535) + .HasColumnType("TEXT"); + + b.Property<string>("PasswordResetProviderId") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("TEXT"); + + b.Property<bool>("PlayDefaultAudioTrack") + .HasColumnType("INTEGER"); + + b.Property<bool>("RememberAudioSelections") + .HasColumnType("INTEGER"); + + b.Property<bool>("RememberSubtitleSelections") + .HasColumnType("INTEGER"); + + b.Property<int?>("RemoteClientBitrateLimit") + .HasColumnType("INTEGER"); + + b.Property<uint>("RowVersion") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.Property<string>("SubtitleLanguagePreference") + .HasMaxLength(255) + .HasColumnType("TEXT"); + + b.Property<int>("SubtitleMode") + .HasColumnType("INTEGER"); + + b.Property<int>("SyncPlayAccess") + .HasColumnType("INTEGER"); + + b.Property<string>("Username") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("TEXT") + .UseCollation("NOCASE"); + + b.HasKey("Id"); + + b.HasIndex("Username") + .IsUnique(); + + b.ToTable("Users", "jellyfin"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.AccessSchedule", b => + { + b.HasOne("Jellyfin.Data.Entities.User", null) + .WithMany("AccessSchedules") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.DisplayPreferences", b => + { + b.HasOne("Jellyfin.Data.Entities.User", null) + .WithMany("DisplayPreferences") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.HomeSection", b => + { + b.HasOne("Jellyfin.Data.Entities.DisplayPreferences", null) + .WithMany("HomeSections") + .HasForeignKey("DisplayPreferencesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.ImageInfo", b => + { + b.HasOne("Jellyfin.Data.Entities.User", null) + .WithOne("ProfileImage") + .HasForeignKey("Jellyfin.Data.Entities.ImageInfo", "UserId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.ItemDisplayPreferences", b => + { + b.HasOne("Jellyfin.Data.Entities.User", null) + .WithMany("ItemDisplayPreferences") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.Permission", b => + { + b.HasOne("Jellyfin.Data.Entities.User", null) + .WithMany("Permissions") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.Preference", b => + { + b.HasOne("Jellyfin.Data.Entities.User", null) + .WithMany("Preferences") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.Security.Device", b => + { + b.HasOne("Jellyfin.Data.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.DisplayPreferences", b => + { + b.Navigation("HomeSections"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.User", b => + { + b.Navigation("AccessSchedules"); + + b.Navigation("DisplayPreferences"); + + b.Navigation("ItemDisplayPreferences"); + + b.Navigation("Permissions"); + + b.Navigation("Preferences"); + + b.Navigation("ProfileImage"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Jellyfin.Server.Implementations/Migrations/20221022080052_AddIndexActivityLogsDateCreated.cs b/Jellyfin.Server.Implementations/Migrations/20221022080052_AddIndexActivityLogsDateCreated.cs new file mode 100644 index 000000000..f09ad2709 --- /dev/null +++ b/Jellyfin.Server.Implementations/Migrations/20221022080052_AddIndexActivityLogsDateCreated.cs @@ -0,0 +1,28 @@ +#pragma warning disable CS1591, SA1601 + +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Jellyfin.Server.Implementations.Migrations +{ + public partial class AddIndexActivityLogsDateCreated : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateIndex( + name: "IX_ActivityLogs_DateCreated", + schema: "jellyfin", + table: "ActivityLogs", + column: "DateCreated"); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropIndex( + name: "IX_ActivityLogs_DateCreated", + schema: "jellyfin", + table: "ActivityLogs"); + } + } +} diff --git a/Jellyfin.Server.Implementations/Migrations/JellyfinDbModelSnapshot.cs b/Jellyfin.Server.Implementations/Migrations/JellyfinDbModelSnapshot.cs index fcc360e26..2dd7b094a 100644 --- a/Jellyfin.Server.Implementations/Migrations/JellyfinDbModelSnapshot.cs +++ b/Jellyfin.Server.Implementations/Migrations/JellyfinDbModelSnapshot.cs @@ -5,6 +5,8 @@ using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +#nullable disable + namespace Jellyfin.Server.Implementations.Migrations { [DbContext(typeof(JellyfinDb))] @@ -15,7 +17,7 @@ namespace Jellyfin.Server.Implementations.Migrations #pragma warning disable 612, 618 modelBuilder .HasDefaultSchema("jellyfin") - .HasAnnotation("ProductVersion", "5.0.7"); + .HasAnnotation("ProductVersion", "6.0.9"); modelBuilder.Entity("Jellyfin.Data.Entities.AccessSchedule", b => { @@ -39,7 +41,7 @@ namespace Jellyfin.Server.Implementations.Migrations b.HasIndex("UserId"); - b.ToTable("AccessSchedules"); + b.ToTable("AccessSchedules", "jellyfin"); }); modelBuilder.Entity("Jellyfin.Data.Entities.ActivityLog", b => @@ -85,7 +87,9 @@ namespace Jellyfin.Server.Implementations.Migrations b.HasKey("Id"); - b.ToTable("ActivityLogs"); + b.HasIndex("DateCreated"); + + b.ToTable("ActivityLogs", "jellyfin"); }); modelBuilder.Entity("Jellyfin.Data.Entities.CustomItemDisplayPreferences", b => @@ -117,7 +121,7 @@ namespace Jellyfin.Server.Implementations.Migrations b.HasIndex("UserId", "ItemId", "Client", "Key") .IsUnique(); - b.ToTable("CustomItemDisplayPreferences"); + b.ToTable("CustomItemDisplayPreferences", "jellyfin"); }); modelBuilder.Entity("Jellyfin.Data.Entities.DisplayPreferences", b => @@ -174,7 +178,7 @@ namespace Jellyfin.Server.Implementations.Migrations b.HasIndex("UserId", "ItemId", "Client") .IsUnique(); - b.ToTable("DisplayPreferences"); + b.ToTable("DisplayPreferences", "jellyfin"); }); modelBuilder.Entity("Jellyfin.Data.Entities.HomeSection", b => @@ -196,7 +200,7 @@ namespace Jellyfin.Server.Implementations.Migrations b.HasIndex("DisplayPreferencesId"); - b.ToTable("HomeSection"); + b.ToTable("HomeSection", "jellyfin"); }); modelBuilder.Entity("Jellyfin.Data.Entities.ImageInfo", b => @@ -221,7 +225,7 @@ namespace Jellyfin.Server.Implementations.Migrations b.HasIndex("UserId") .IsUnique(); - b.ToTable("ImageInfos"); + b.ToTable("ImageInfos", "jellyfin"); }); modelBuilder.Entity("Jellyfin.Data.Entities.ItemDisplayPreferences", b => @@ -265,7 +269,7 @@ namespace Jellyfin.Server.Implementations.Migrations b.HasIndex("UserId"); - b.ToTable("ItemDisplayPreferences"); + b.ToTable("ItemDisplayPreferences", "jellyfin"); }); modelBuilder.Entity("Jellyfin.Data.Entities.Permission", b => @@ -296,7 +300,7 @@ namespace Jellyfin.Server.Implementations.Migrations .IsUnique() .HasFilter("[UserId] IS NOT NULL"); - b.ToTable("Permissions"); + b.ToTable("Permissions", "jellyfin"); }); modelBuilder.Entity("Jellyfin.Data.Entities.Preference", b => @@ -329,7 +333,7 @@ namespace Jellyfin.Server.Implementations.Migrations .IsUnique() .HasFilter("[UserId] IS NOT NULL"); - b.ToTable("Preferences"); + b.ToTable("Preferences", "jellyfin"); }); modelBuilder.Entity("Jellyfin.Data.Entities.Security.ApiKey", b => @@ -358,7 +362,7 @@ namespace Jellyfin.Server.Implementations.Migrations b.HasIndex("AccessToken") .IsUnique(); - b.ToTable("ApiKeys"); + b.ToTable("ApiKeys", "jellyfin"); }); modelBuilder.Entity("Jellyfin.Data.Entities.Security.Device", b => @@ -416,7 +420,7 @@ namespace Jellyfin.Server.Implementations.Migrations b.HasIndex("UserId", "DeviceId"); - b.ToTable("Devices"); + b.ToTable("Devices", "jellyfin"); }); modelBuilder.Entity("Jellyfin.Data.Entities.Security.DeviceOptions", b => @@ -437,7 +441,7 @@ namespace Jellyfin.Server.Implementations.Migrations b.HasIndex("DeviceId") .IsUnique(); - b.ToTable("DeviceOptions"); + b.ToTable("DeviceOptions", "jellyfin"); }); modelBuilder.Entity("Jellyfin.Data.Entities.User", b => @@ -550,7 +554,7 @@ namespace Jellyfin.Server.Implementations.Migrations b.HasIndex("Username") .IsUnique(); - b.ToTable("Users"); + b.ToTable("Users", "jellyfin"); }); modelBuilder.Entity("Jellyfin.Data.Entities.AccessSchedule", b => diff --git a/Jellyfin.Server.Implementations/ModelConfiguration/ActivityLogConfiguration.cs b/Jellyfin.Server.Implementations/ModelConfiguration/ActivityLogConfiguration.cs new file mode 100644 index 000000000..9a63ed9f2 --- /dev/null +++ b/Jellyfin.Server.Implementations/ModelConfiguration/ActivityLogConfiguration.cs @@ -0,0 +1,17 @@ +using Jellyfin.Data.Entities; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; + +namespace Jellyfin.Server.Implementations.ModelConfiguration; + +/// <summary> +/// FluentAPI configuration for the ActivityLog entity. +/// </summary> +public class ActivityLogConfiguration : IEntityTypeConfiguration<ActivityLog> +{ + /// <inheritdoc/> + public void Configure(EntityTypeBuilder<ActivityLog> builder) + { + builder.HasIndex(entity => entity.DateCreated); + } +} diff --git a/Jellyfin.Server.Implementations/Security/AuthenticationManager.cs b/Jellyfin.Server.Implementations/Security/AuthenticationManager.cs index b79e46469..33c08c8c2 100644 --- a/Jellyfin.Server.Implementations/Security/AuthenticationManager.cs +++ b/Jellyfin.Server.Implementations/Security/AuthenticationManager.cs @@ -10,13 +10,13 @@ namespace Jellyfin.Server.Implementations.Security /// <inheritdoc /> public class AuthenticationManager : IAuthenticationManager { - private readonly JellyfinDbProvider _dbProvider; + private readonly IDbContextFactory<JellyfinDb> _dbProvider; /// <summary> /// Initializes a new instance of the <see cref="AuthenticationManager"/> class. /// </summary> /// <param name="dbProvider">The database provider.</param> - public AuthenticationManager(JellyfinDbProvider dbProvider) + public AuthenticationManager(IDbContextFactory<JellyfinDb> dbProvider) { _dbProvider = dbProvider; } @@ -24,50 +24,56 @@ namespace Jellyfin.Server.Implementations.Security /// <inheritdoc /> public async Task CreateApiKey(string name) { - await using var dbContext = _dbProvider.CreateContext(); - - dbContext.ApiKeys.Add(new ApiKey(name)); + var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false); + await using (dbContext.ConfigureAwait(false)) + { + dbContext.ApiKeys.Add(new ApiKey(name)); - await dbContext.SaveChangesAsync().ConfigureAwait(false); + await dbContext.SaveChangesAsync().ConfigureAwait(false); + } } /// <inheritdoc /> public async Task<IReadOnlyList<AuthenticationInfo>> GetApiKeys() { - await using var dbContext = _dbProvider.CreateContext(); - - return await dbContext.ApiKeys - .AsAsyncEnumerable() - .Select(key => new AuthenticationInfo - { - AppName = key.Name, - AccessToken = key.AccessToken, - DateCreated = key.DateCreated, - DeviceId = string.Empty, - DeviceName = string.Empty, - AppVersion = string.Empty - }).ToListAsync().ConfigureAwait(false); + var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false); + await using (dbContext.ConfigureAwait(false)) + { + return await dbContext.ApiKeys + .AsAsyncEnumerable() + .Select(key => new AuthenticationInfo + { + AppName = key.Name, + AccessToken = key.AccessToken, + DateCreated = key.DateCreated, + DeviceId = string.Empty, + DeviceName = string.Empty, + AppVersion = string.Empty + }).ToListAsync().ConfigureAwait(false); + } } /// <inheritdoc /> public async Task DeleteApiKey(string accessToken) { - await using var dbContext = _dbProvider.CreateContext(); - - var key = await dbContext.ApiKeys - .AsQueryable() - .Where(apiKey => apiKey.AccessToken == accessToken) - .FirstOrDefaultAsync() - .ConfigureAwait(false); - - if (key == null) + var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false); + await using (dbContext.ConfigureAwait(false)) { - return; - } + var key = await dbContext.ApiKeys + .AsQueryable() + .Where(apiKey => apiKey.AccessToken == accessToken) + .FirstOrDefaultAsync() + .ConfigureAwait(false); - dbContext.Remove(key); + if (key == null) + { + return; + } + + dbContext.Remove(key); - await dbContext.SaveChangesAsync().ConfigureAwait(false); + await dbContext.SaveChangesAsync().ConfigureAwait(false); + } } } } diff --git a/Jellyfin.Server.Implementations/Security/AuthorizationContext.cs b/Jellyfin.Server.Implementations/Security/AuthorizationContext.cs index 9f813f532..4d1a1b3cf 100644 --- a/Jellyfin.Server.Implementations/Security/AuthorizationContext.cs +++ b/Jellyfin.Server.Implementations/Security/AuthorizationContext.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; using System.Net; using System.Threading.Tasks; +using EFCoreSecondLevelCacheInterceptor; using MediaBrowser.Controller; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Net; @@ -15,12 +16,12 @@ namespace Jellyfin.Server.Implementations.Security { public class AuthorizationContext : IAuthorizationContext { - private readonly JellyfinDbProvider _jellyfinDbProvider; + private readonly IDbContextFactory<JellyfinDb> _jellyfinDbProvider; private readonly IUserManager _userManager; private readonly IServerApplicationHost _serverApplicationHost; public AuthorizationContext( - JellyfinDbProvider jellyfinDb, + IDbContextFactory<JellyfinDb> jellyfinDb, IUserManager userManager, IServerApplicationHost serverApplicationHost) { @@ -121,96 +122,99 @@ namespace Jellyfin.Server.Implementations.Security #pragma warning restore CA1508 authInfo.HasToken = true; - await using var dbContext = _jellyfinDbProvider.CreateContext(); - var device = await dbContext.Devices.FirstOrDefaultAsync(d => d.AccessToken == token).ConfigureAwait(false); - - if (device != null) + var dbContext = await _jellyfinDbProvider.CreateDbContextAsync().ConfigureAwait(false); + await using (dbContext.ConfigureAwait(false)) { - authInfo.IsAuthenticated = true; - var updateToken = false; - - // TODO: Remove these checks for IsNullOrWhiteSpace - if (string.IsNullOrWhiteSpace(authInfo.Client)) - { - authInfo.Client = device.AppName; - } + var device = await dbContext.Devices.FirstOrDefaultAsync(d => d.AccessToken == token).ConfigureAwait(false); - if (string.IsNullOrWhiteSpace(authInfo.DeviceId)) + if (device != null) { - authInfo.DeviceId = device.DeviceId; - } - - // Temporary. TODO - allow clients to specify that the token has been shared with a casting device - var allowTokenInfoUpdate = !authInfo.Client.Contains("chromecast", StringComparison.OrdinalIgnoreCase); + authInfo.IsAuthenticated = true; + var updateToken = false; - if (string.IsNullOrWhiteSpace(authInfo.Device)) - { - authInfo.Device = device.DeviceName; - } - else if (!string.Equals(authInfo.Device, device.DeviceName, StringComparison.OrdinalIgnoreCase)) - { - if (allowTokenInfoUpdate) + // TODO: Remove these checks for IsNullOrWhiteSpace + if (string.IsNullOrWhiteSpace(authInfo.Client)) { - updateToken = true; - device.DeviceName = authInfo.Device; + authInfo.Client = device.AppName; } - } - if (string.IsNullOrWhiteSpace(authInfo.Version)) - { - authInfo.Version = device.AppVersion; - } - else if (!string.Equals(authInfo.Version, device.AppVersion, StringComparison.OrdinalIgnoreCase)) - { - if (allowTokenInfoUpdate) + if (string.IsNullOrWhiteSpace(authInfo.DeviceId)) { - updateToken = true; - device.AppVersion = authInfo.Version; + authInfo.DeviceId = device.DeviceId; } - } - if ((DateTime.UtcNow - device.DateLastActivity).TotalMinutes > 3) - { - device.DateLastActivity = DateTime.UtcNow; - updateToken = true; - } + // Temporary. TODO - allow clients to specify that the token has been shared with a casting device + var allowTokenInfoUpdate = !authInfo.Client.Contains("chromecast", StringComparison.OrdinalIgnoreCase); - authInfo.User = _userManager.GetUserById(device.UserId); + if (string.IsNullOrWhiteSpace(authInfo.Device)) + { + authInfo.Device = device.DeviceName; + } + else if (!string.Equals(authInfo.Device, device.DeviceName, StringComparison.OrdinalIgnoreCase)) + { + if (allowTokenInfoUpdate) + { + updateToken = true; + device.DeviceName = authInfo.Device; + } + } - if (updateToken) - { - dbContext.Devices.Update(device); - await dbContext.SaveChangesAsync().ConfigureAwait(false); - } - } - else - { - var key = await dbContext.ApiKeys.FirstOrDefaultAsync(apiKey => apiKey.AccessToken == token).ConfigureAwait(false); - if (key != null) - { - authInfo.IsAuthenticated = true; - authInfo.Client = key.Name; - authInfo.Token = key.AccessToken; - if (string.IsNullOrWhiteSpace(authInfo.DeviceId)) + if (string.IsNullOrWhiteSpace(authInfo.Version)) { - authInfo.DeviceId = _serverApplicationHost.SystemId; + authInfo.Version = device.AppVersion; + } + else if (!string.Equals(authInfo.Version, device.AppVersion, StringComparison.OrdinalIgnoreCase)) + { + if (allowTokenInfoUpdate) + { + updateToken = true; + device.AppVersion = authInfo.Version; + } } - if (string.IsNullOrWhiteSpace(authInfo.Device)) + if ((DateTime.UtcNow - device.DateLastActivity).TotalMinutes > 3) { - authInfo.Device = _serverApplicationHost.Name; + device.DateLastActivity = DateTime.UtcNow; + updateToken = true; } - if (string.IsNullOrWhiteSpace(authInfo.Version)) + authInfo.User = _userManager.GetUserById(device.UserId); + + if (updateToken) { - authInfo.Version = _serverApplicationHost.ApplicationVersionString; + dbContext.Devices.Update(device); + await dbContext.SaveChangesAsync().ConfigureAwait(false); } + } + else + { + var key = await dbContext.ApiKeys.FirstOrDefaultAsync(apiKey => apiKey.AccessToken == token).ConfigureAwait(false); + if (key != null) + { + authInfo.IsAuthenticated = true; + authInfo.Client = key.Name; + authInfo.Token = key.AccessToken; + if (string.IsNullOrWhiteSpace(authInfo.DeviceId)) + { + authInfo.DeviceId = _serverApplicationHost.SystemId; + } + + if (string.IsNullOrWhiteSpace(authInfo.Device)) + { + authInfo.Device = _serverApplicationHost.Name; + } + + if (string.IsNullOrWhiteSpace(authInfo.Version)) + { + authInfo.Version = _serverApplicationHost.ApplicationVersionString; + } - authInfo.IsApiKey = true; + authInfo.IsApiKey = true; + } } - } - return authInfo; + return authInfo; + } } /// <summary> diff --git a/Jellyfin.Server.Implementations/Users/DefaultPasswordResetProvider.cs b/Jellyfin.Server.Implementations/Users/DefaultPasswordResetProvider.cs index 5e84255f9..4fda8f5a4 100644 --- a/Jellyfin.Server.Implementations/Users/DefaultPasswordResetProvider.cs +++ b/Jellyfin.Server.Implementations/Users/DefaultPasswordResetProvider.cs @@ -67,7 +67,7 @@ namespace Jellyfin.Server.Implementations.Users else if (string.Equals( spr.Pin.Replace("-", string.Empty, StringComparison.Ordinal), pin.Replace("-", string.Empty, StringComparison.Ordinal), - StringComparison.OrdinalIgnoreCase)) + StringComparison.Ordinal)) { var resetUser = userManager.GetUserByName(spr.UserName) ?? throw new ResourceNotFoundException($"User with a username of {spr.UserName} not found"); diff --git a/Jellyfin.Server.Implementations/Users/DisplayPreferencesManager.cs b/Jellyfin.Server.Implementations/Users/DisplayPreferencesManager.cs index f5d38db20..87babc05c 100644 --- a/Jellyfin.Server.Implementations/Users/DisplayPreferencesManager.cs +++ b/Jellyfin.Server.Implementations/Users/DisplayPreferencesManager.cs @@ -20,10 +20,10 @@ namespace Jellyfin.Server.Implementations.Users /// <summary> /// Initializes a new instance of the <see cref="DisplayPreferencesManager"/> class. /// </summary> - /// <param name="dbContext">The database context.</param> - public DisplayPreferencesManager(JellyfinDb dbContext) + /// <param name="dbContextFactory">The database context factory.</param> + public DisplayPreferencesManager(IDbContextFactory<JellyfinDb> dbContextFactory) { - _dbContext = dbContext; + _dbContext = dbContextFactory.CreateDbContext(); } /// <inheritdoc /> @@ -79,7 +79,7 @@ namespace Jellyfin.Server.Implementations.Users } /// <inheritdoc /> - public void SetCustomItemDisplayPreferences(Guid userId, Guid itemId, string client, Dictionary<string, string> customPreferences) + public void SetCustomItemDisplayPreferences(Guid userId, Guid itemId, string client, Dictionary<string, string?> customPreferences) { var existingPrefs = _dbContext.CustomItemDisplayPreferences .AsQueryable() diff --git a/Jellyfin.Server.Implementations/Users/UserManager.cs b/Jellyfin.Server.Implementations/Users/UserManager.cs index 2100fa6d5..25560707a 100644 --- a/Jellyfin.Server.Implementations/Users/UserManager.cs +++ b/Jellyfin.Server.Implementations/Users/UserManager.cs @@ -33,7 +33,7 @@ namespace Jellyfin.Server.Implementations.Users /// </summary> public class UserManager : IUserManager { - private readonly JellyfinDbProvider _dbProvider; + private readonly IDbContextFactory<JellyfinDb> _dbProvider; private readonly IEventManager _eventManager; private readonly ICryptoProvider _cryptoProvider; private readonly INetworkManager _networkManager; @@ -59,7 +59,7 @@ namespace Jellyfin.Server.Implementations.Users /// <param name="imageProcessor">The image processor.</param> /// <param name="logger">The logger.</param> public UserManager( - JellyfinDbProvider dbProvider, + IDbContextFactory<JellyfinDb> dbProvider, IEventManager eventManager, ICryptoProvider cryptoProvider, INetworkManager networkManager, @@ -83,7 +83,7 @@ namespace Jellyfin.Server.Implementations.Users _defaultPasswordResetProvider = _passwordResetProviders.OfType<DefaultPasswordResetProvider>().First(); _users = new ConcurrentDictionary<Guid, User>(); - using var dbContext = _dbProvider.CreateContext(); + using var dbContext = _dbProvider.CreateDbContext(); foreach (var user in dbContext.Users .Include(user => user.Permissions) .Include(user => user.Preferences) @@ -130,10 +130,7 @@ namespace Jellyfin.Server.Implementations.Users /// <inheritdoc/> public async Task RenameUser(User user, string newName) { - if (user == null) - { - throw new ArgumentNullException(nameof(user)); - } + ArgumentNullException.ThrowIfNull(user); ThrowIfInvalidUsername(newName); @@ -142,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) @@ -205,12 +206,15 @@ namespace Jellyfin.Server.Implementations.Users name)); } - await using var dbContext = _dbProvider.CreateContext(); - - var newUser = await CreateUserInternalAsync(name, dbContext).ConfigureAwait(false); + User newUser; + var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false); + await using (dbContext.ConfigureAwait(false)) + { + newUser = await CreateUserInternalAsync(name, dbContext).ConfigureAwait(false); - dbContext.Users.Add(newUser); - await dbContext.SaveChangesAsync().ConfigureAwait(false); + dbContext.Users.Add(newUser); + await dbContext.SaveChangesAsync().ConfigureAwait(false); + } await _eventManager.PublishAsync(new UserCreatedEventArgs(newUser)).ConfigureAwait(false); @@ -244,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); @@ -267,10 +275,7 @@ namespace Jellyfin.Server.Implementations.Users /// <inheritdoc/> public async Task ChangePassword(User user, string newPassword) { - if (user == null) - { - throw new ArgumentNullException(nameof(user)); - } + ArgumentNullException.ThrowIfNull(user); await GetAuthenticationProvider(user).ChangePassword(user, newPassword).ConfigureAwait(false); await UpdateUserAsync(user).ConfigureAwait(false); @@ -294,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/> @@ -326,10 +331,10 @@ namespace Jellyfin.Server.Implementations.Users EnableNextEpisodeAutoPlay = user.EnableNextEpisodeAutoPlay, RememberSubtitleSelections = user.RememberSubtitleSelections, SubtitleLanguagePreference = user.SubtitleLanguagePreference ?? string.Empty, - OrderedViews = user.GetPreference(PreferenceKind.OrderedViews), - GroupedFolders = user.GetPreference(PreferenceKind.GroupedFolders), - MyMediaExcludes = user.GetPreference(PreferenceKind.MyMediaExcludes), - LatestItemsExcludes = user.GetPreference(PreferenceKind.LatestItemExcludes) + OrderedViews = user.GetPreferenceValues<Guid>(PreferenceKind.OrderedViews), + GroupedFolders = user.GetPreferenceValues<Guid>(PreferenceKind.GroupedFolders), + MyMediaExcludes = user.GetPreferenceValues<Guid>(PreferenceKind.MyMediaExcludes), + LatestItemsExcludes = user.GetPreferenceValues<Guid>(PreferenceKind.LatestItemExcludes) }, Policy = new UserPolicy { @@ -547,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/> @@ -590,105 +598,111 @@ namespace Jellyfin.Server.Implementations.Users /// <inheritdoc/> public async Task UpdateConfigurationAsync(Guid userId, UserConfiguration config) { - await using var dbContext = _dbProvider.CreateContext(); - var user = dbContext.Users - .Include(u => u.Permissions) - .Include(u => u.Preferences) - .Include(u => u.AccessSchedules) - .Include(u => u.ProfileImage) - .FirstOrDefault(u => u.Id.Equals(userId)) - ?? throw new ArgumentException("No user exists with given Id!"); - - user.SubtitleMode = config.SubtitleMode; - user.HidePlayedInLatest = config.HidePlayedInLatest; - user.EnableLocalPassword = config.EnableLocalPassword; - user.PlayDefaultAudioTrack = config.PlayDefaultAudioTrack; - user.DisplayCollectionsView = config.DisplayCollectionsView; - user.DisplayMissingEpisodes = config.DisplayMissingEpisodes; - user.AudioLanguagePreference = config.AudioLanguagePreference; - user.RememberAudioSelections = config.RememberAudioSelections; - user.EnableNextEpisodeAutoPlay = config.EnableNextEpisodeAutoPlay; - user.RememberSubtitleSelections = config.RememberSubtitleSelections; - user.SubtitleLanguagePreference = config.SubtitleLanguagePreference; - - user.SetPreference(PreferenceKind.OrderedViews, config.OrderedViews); - user.SetPreference(PreferenceKind.GroupedFolders, config.GroupedFolders); - user.SetPreference(PreferenceKind.MyMediaExcludes, config.MyMediaExcludes); - user.SetPreference(PreferenceKind.LatestItemExcludes, config.LatestItemsExcludes); - - dbContext.Update(user); - _users[user.Id] = user; - await dbContext.SaveChangesAsync().ConfigureAwait(false); + var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false); + await using (dbContext.ConfigureAwait(false)) + { + var user = dbContext.Users + .Include(u => u.Permissions) + .Include(u => u.Preferences) + .Include(u => u.AccessSchedules) + .Include(u => u.ProfileImage) + .FirstOrDefault(u => u.Id.Equals(userId)) + ?? throw new ArgumentException("No user exists with given Id!"); + + user.SubtitleMode = config.SubtitleMode; + user.HidePlayedInLatest = config.HidePlayedInLatest; + user.EnableLocalPassword = config.EnableLocalPassword; + user.PlayDefaultAudioTrack = config.PlayDefaultAudioTrack; + user.DisplayCollectionsView = config.DisplayCollectionsView; + user.DisplayMissingEpisodes = config.DisplayMissingEpisodes; + user.AudioLanguagePreference = config.AudioLanguagePreference; + user.RememberAudioSelections = config.RememberAudioSelections; + user.EnableNextEpisodeAutoPlay = config.EnableNextEpisodeAutoPlay; + user.RememberSubtitleSelections = config.RememberSubtitleSelections; + user.SubtitleLanguagePreference = config.SubtitleLanguagePreference; + + user.SetPreference(PreferenceKind.OrderedViews, config.OrderedViews); + user.SetPreference(PreferenceKind.GroupedFolders, config.GroupedFolders); + user.SetPreference(PreferenceKind.MyMediaExcludes, config.MyMediaExcludes); + user.SetPreference(PreferenceKind.LatestItemExcludes, config.LatestItemsExcludes); + + dbContext.Update(user); + _users[user.Id] = user; + await dbContext.SaveChangesAsync().ConfigureAwait(false); + } } /// <inheritdoc/> public async Task UpdatePolicyAsync(Guid userId, UserPolicy policy) { - await using var dbContext = _dbProvider.CreateContext(); - var user = dbContext.Users - .Include(u => u.Permissions) - .Include(u => u.Preferences) - .Include(u => u.AccessSchedules) - .Include(u => u.ProfileImage) - .FirstOrDefault(u => u.Id.Equals(userId)) - ?? throw new ArgumentException("No user exists with given Id!"); - - // The default number of login attempts is 3, but for some god forsaken reason it's sent to the server as "0" - int? maxLoginAttempts = policy.LoginAttemptsBeforeLockout switch - { - -1 => null, - 0 => 3, - _ => policy.LoginAttemptsBeforeLockout - }; + var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false); + await using (dbContext.ConfigureAwait(false)) + { + var user = dbContext.Users + .Include(u => u.Permissions) + .Include(u => u.Preferences) + .Include(u => u.AccessSchedules) + .Include(u => u.ProfileImage) + .FirstOrDefault(u => u.Id.Equals(userId)) + ?? throw new ArgumentException("No user exists with given Id!"); + + // The default number of login attempts is 3, but for some god forsaken reason it's sent to the server as "0" + int? maxLoginAttempts = policy.LoginAttemptsBeforeLockout switch + { + -1 => null, + 0 => 3, + _ => policy.LoginAttemptsBeforeLockout + }; - user.MaxParentalAgeRating = policy.MaxParentalRating; - user.EnableUserPreferenceAccess = policy.EnableUserPreferenceAccess; - user.RemoteClientBitrateLimit = policy.RemoteClientBitrateLimit; - user.AuthenticationProviderId = policy.AuthenticationProviderId; - user.PasswordResetProviderId = policy.PasswordResetProviderId; - user.InvalidLoginAttemptCount = policy.InvalidLoginAttemptCount; - user.LoginAttemptsBeforeLockout = maxLoginAttempts; - user.MaxActiveSessions = policy.MaxActiveSessions; - user.SyncPlayAccess = policy.SyncPlayAccess; - user.SetPermission(PermissionKind.IsAdministrator, policy.IsAdministrator); - user.SetPermission(PermissionKind.IsHidden, policy.IsHidden); - user.SetPermission(PermissionKind.IsDisabled, policy.IsDisabled); - user.SetPermission(PermissionKind.EnableSharedDeviceControl, policy.EnableSharedDeviceControl); - user.SetPermission(PermissionKind.EnableRemoteAccess, policy.EnableRemoteAccess); - user.SetPermission(PermissionKind.EnableLiveTvManagement, policy.EnableLiveTvManagement); - user.SetPermission(PermissionKind.EnableLiveTvAccess, policy.EnableLiveTvAccess); - user.SetPermission(PermissionKind.EnableMediaPlayback, policy.EnableMediaPlayback); - user.SetPermission(PermissionKind.EnableAudioPlaybackTranscoding, policy.EnableAudioPlaybackTranscoding); - user.SetPermission(PermissionKind.EnableVideoPlaybackTranscoding, policy.EnableVideoPlaybackTranscoding); - user.SetPermission(PermissionKind.EnableContentDeletion, policy.EnableContentDeletion); - user.SetPermission(PermissionKind.EnableContentDownloading, policy.EnableContentDownloading); - user.SetPermission(PermissionKind.EnableSyncTranscoding, policy.EnableSyncTranscoding); - user.SetPermission(PermissionKind.EnableMediaConversion, policy.EnableMediaConversion); - user.SetPermission(PermissionKind.EnableAllChannels, policy.EnableAllChannels); - user.SetPermission(PermissionKind.EnableAllDevices, policy.EnableAllDevices); - user.SetPermission(PermissionKind.EnableAllFolders, policy.EnableAllFolders); - user.SetPermission(PermissionKind.EnableRemoteControlOfOtherUsers, policy.EnableRemoteControlOfOtherUsers); - user.SetPermission(PermissionKind.EnablePlaybackRemuxing, policy.EnablePlaybackRemuxing); - user.SetPermission(PermissionKind.ForceRemoteSourceTranscoding, policy.ForceRemoteSourceTranscoding); - user.SetPermission(PermissionKind.EnablePublicSharing, policy.EnablePublicSharing); - - user.AccessSchedules.Clear(); - foreach (var policyAccessSchedule in policy.AccessSchedules) - { - user.AccessSchedules.Add(policyAccessSchedule); - } - - // TODO: fix this at some point - user.SetPreference(PreferenceKind.BlockUnratedItems, policy.BlockUnratedItems ?? Array.Empty<UnratedItem>()); - user.SetPreference(PreferenceKind.BlockedTags, policy.BlockedTags); - user.SetPreference(PreferenceKind.EnabledChannels, policy.EnabledChannels); - user.SetPreference(PreferenceKind.EnabledDevices, policy.EnabledDevices); - user.SetPreference(PreferenceKind.EnabledFolders, policy.EnabledFolders); - user.SetPreference(PreferenceKind.EnableContentDeletionFromFolders, policy.EnableContentDeletionFromFolders); - - dbContext.Update(user); - _users[user.Id] = user; - await dbContext.SaveChangesAsync().ConfigureAwait(false); + user.MaxParentalAgeRating = policy.MaxParentalRating; + user.EnableUserPreferenceAccess = policy.EnableUserPreferenceAccess; + user.RemoteClientBitrateLimit = policy.RemoteClientBitrateLimit; + user.AuthenticationProviderId = policy.AuthenticationProviderId; + user.PasswordResetProviderId = policy.PasswordResetProviderId; + user.InvalidLoginAttemptCount = policy.InvalidLoginAttemptCount; + user.LoginAttemptsBeforeLockout = maxLoginAttempts; + user.MaxActiveSessions = policy.MaxActiveSessions; + user.SyncPlayAccess = policy.SyncPlayAccess; + user.SetPermission(PermissionKind.IsAdministrator, policy.IsAdministrator); + user.SetPermission(PermissionKind.IsHidden, policy.IsHidden); + user.SetPermission(PermissionKind.IsDisabled, policy.IsDisabled); + user.SetPermission(PermissionKind.EnableSharedDeviceControl, policy.EnableSharedDeviceControl); + user.SetPermission(PermissionKind.EnableRemoteAccess, policy.EnableRemoteAccess); + user.SetPermission(PermissionKind.EnableLiveTvManagement, policy.EnableLiveTvManagement); + user.SetPermission(PermissionKind.EnableLiveTvAccess, policy.EnableLiveTvAccess); + user.SetPermission(PermissionKind.EnableMediaPlayback, policy.EnableMediaPlayback); + user.SetPermission(PermissionKind.EnableAudioPlaybackTranscoding, policy.EnableAudioPlaybackTranscoding); + user.SetPermission(PermissionKind.EnableVideoPlaybackTranscoding, policy.EnableVideoPlaybackTranscoding); + user.SetPermission(PermissionKind.EnableContentDeletion, policy.EnableContentDeletion); + user.SetPermission(PermissionKind.EnableContentDownloading, policy.EnableContentDownloading); + user.SetPermission(PermissionKind.EnableSyncTranscoding, policy.EnableSyncTranscoding); + user.SetPermission(PermissionKind.EnableMediaConversion, policy.EnableMediaConversion); + user.SetPermission(PermissionKind.EnableAllChannels, policy.EnableAllChannels); + user.SetPermission(PermissionKind.EnableAllDevices, policy.EnableAllDevices); + user.SetPermission(PermissionKind.EnableAllFolders, policy.EnableAllFolders); + user.SetPermission(PermissionKind.EnableRemoteControlOfOtherUsers, policy.EnableRemoteControlOfOtherUsers); + user.SetPermission(PermissionKind.EnablePlaybackRemuxing, policy.EnablePlaybackRemuxing); + user.SetPermission(PermissionKind.ForceRemoteSourceTranscoding, policy.ForceRemoteSourceTranscoding); + user.SetPermission(PermissionKind.EnablePublicSharing, policy.EnablePublicSharing); + + user.AccessSchedules.Clear(); + foreach (var policyAccessSchedule in policy.AccessSchedules) + { + user.AccessSchedules.Add(policyAccessSchedule); + } + + // TODO: fix this at some point + user.SetPreference(PreferenceKind.BlockUnratedItems, policy.BlockUnratedItems ?? Array.Empty<UnratedItem>()); + user.SetPreference(PreferenceKind.BlockedTags, policy.BlockedTags); + user.SetPreference(PreferenceKind.EnabledChannels, policy.EnabledChannels); + user.SetPreference(PreferenceKind.EnabledDevices, policy.EnabledDevices); + user.SetPreference(PreferenceKind.EnabledFolders, policy.EnabledFolders); + user.SetPreference(PreferenceKind.EnableContentDeletionFromFolders, policy.EnableContentDeletionFromFolders); + + dbContext.Update(user); + _users[user.Id] = user; + await dbContext.SaveChangesAsync().ConfigureAwait(false); + } } /// <inheritdoc/> @@ -699,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; } @@ -865,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); + } } } |
