diff options
Diffstat (limited to 'Jellyfin.Server.Implementations')
4 files changed, 131 insertions, 105 deletions
diff --git a/Jellyfin.Server.Implementations/Devices/DeviceManager.cs b/Jellyfin.Server.Implementations/Devices/DeviceManager.cs index 5e5d52b6b..d8d1b6fa8 100644 --- a/Jellyfin.Server.Implementations/Devices/DeviceManager.cs +++ b/Jellyfin.Server.Implementations/Devices/DeviceManager.cs @@ -179,7 +179,7 @@ namespace Jellyfin.Server.Implementations.Devices .SelectMany(d => dbContext.DeviceOptions.Where(o => o.DeviceId == d.DeviceId).DefaultIfEmpty(), (d, o) => new { Device = d, Options = o }) .AsAsyncEnumerable(); - if (userId.HasValue) + if (!userId.IsNullOrEmpty()) { var user = _userManager.GetUserById(userId.Value); if (user is null) diff --git a/Jellyfin.Server.Implementations/Extensions/ServiceCollectionExtensions.cs b/Jellyfin.Server.Implementations/Extensions/ServiceCollectionExtensions.cs index bb8d4dd14..a88989840 100644 --- a/Jellyfin.Server.Implementations/Extensions/ServiceCollectionExtensions.cs +++ b/Jellyfin.Server.Implementations/Extensions/ServiceCollectionExtensions.cs @@ -16,23 +16,28 @@ public static class ServiceCollectionExtensions /// 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> + /// <param name="disableSecondLevelCache">Whether second level cache disabled..</param> /// <returns>The updated service collection.</returns> - public static IServiceCollection AddJellyfinDbContext(this IServiceCollection serviceCollection) + public static IServiceCollection AddJellyfinDbContext(this IServiceCollection serviceCollection, bool disableSecondLevelCache) { - 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 is null || (result.Value is EFTableRows rows && rows.RowsCount == 0))); + if (!disableSecondLevelCache) + { + serviceCollection.AddEFSecondLevelCache(options => + options.UseMemoryCacheProvider() + .CacheAllQueries(CacheExpirationMode.Sliding, TimeSpan.FromMinutes(10)) + .UseCacheKeyPrefix("EF_") + // Don't cache null values. Remove this optional setting if it's not necessary. + .SkipCachingResults(result => result.Value is null or EFTableRows { RowsCount: 0 })); + } serviceCollection.AddPooledDbContextFactory<JellyfinDbContext>((serviceProvider, opt) => { var applicationPaths = serviceProvider.GetRequiredService<IApplicationPaths>(); - opt.UseSqlite($"Filename={Path.Combine(applicationPaths.DataPath, "jellyfin.db")}") - .AddInterceptors(serviceProvider.GetRequiredService<SecondLevelCacheInterceptor>()); + var dbOpt = opt.UseSqlite($"Filename={Path.Combine(applicationPaths.DataPath, "jellyfin.db")}"); + if (!disableSecondLevelCache) + { + dbOpt.AddInterceptors(serviceProvider.GetRequiredService<SecondLevelCacheInterceptor>()); + } }); return serviceCollection; diff --git a/Jellyfin.Server.Implementations/Trickplay/TrickplayManager.cs b/Jellyfin.Server.Implementations/Trickplay/TrickplayManager.cs index fed5dab69..bb32b7c20 100644 --- a/Jellyfin.Server.Implementations/Trickplay/TrickplayManager.cs +++ b/Jellyfin.Server.Implementations/Trickplay/TrickplayManager.cs @@ -81,6 +81,12 @@ public class TrickplayManager : ITrickplayManager _logger.LogDebug("Trickplay refresh for {ItemId} (replace existing: {Replace})", video.Id, replace); var options = _config.Configuration.TrickplayOptions; + if (options.Interval < 1000) + { + _logger.LogWarning("Trickplay image interval {Interval} is too small, reset to the minimum valid value of 1000", options.Interval); + options.Interval = 1000; + } + foreach (var width in options.WidthResolutions) { cancellationToken.ThrowIfCancellationRequested(); @@ -106,18 +112,11 @@ public class TrickplayManager : ITrickplayManager } var imgTempDir = string.Empty; - var outputDir = GetTrickplayDirectory(video, width); using (await _resourcePool.LockAsync(cancellationToken).ConfigureAwait(false)) { try { - if (!replace && Directory.Exists(outputDir) && (await GetTrickplayResolutions(video.Id).ConfigureAwait(false)).ContainsKey(width)) - { - _logger.LogDebug("Found existing trickplay files for {ItemId}. Exiting.", video.Id); - return; - } - // Extract images // Note: Media sources under parent items exist as their own video/item as well. Only use this video stream for trickplay. var mediaSource = video.GetMediaSources(false).Find(source => Guid.Parse(source.Id).Equals(video.Id)); @@ -129,22 +128,47 @@ public class TrickplayManager : ITrickplayManager } var mediaPath = mediaSource.Path; + if (!File.Exists(mediaPath)) + { + _logger.LogWarning("Media not found at {Path} for item {ItemID}", mediaPath, video.Id); + return; + } + + // The width has to be even, otherwise a lot of filters will not be able to sample it + var actualWidth = 2 * (width / 2); + + // Force using the video width when the trickplay setting has a too large width + if (mediaSource.VideoStream.Width is not null && mediaSource.VideoStream.Width < width) + { + _logger.LogWarning("Video width {VideoWidth} is smaller than trickplay setting {TrickPlayWidth}, using video width for thumbnails", mediaSource.VideoStream.Width, width); + actualWidth = 2 * ((int)mediaSource.VideoStream.Width / 2); + } + + var outputDir = GetTrickplayDirectory(video, actualWidth); + + if (!replace && Directory.Exists(outputDir) && (await GetTrickplayResolutions(video.Id).ConfigureAwait(false)).ContainsKey(actualWidth)) + { + _logger.LogDebug("Found existing trickplay files for {ItemId}. Exiting", video.Id); + return; + } + var mediaStream = mediaSource.VideoStream; var container = mediaSource.Container; - _logger.LogInformation("Creating trickplay files at {Width} width, for {Path} [ID: {ItemId}]", width, mediaPath, video.Id); + _logger.LogInformation("Creating trickplay files at {Width} width, for {Path} [ID: {ItemId}]", actualWidth, mediaPath, video.Id); imgTempDir = await _mediaEncoder.ExtractVideoImagesOnIntervalAccelerated( mediaPath, container, mediaSource, mediaStream, - width, + actualWidth, TimeSpan.FromMilliseconds(options.Interval), options.EnableHwAcceleration, options.EnableHwEncoding, options.ProcessThreads, options.Qscale, options.ProcessPriority, + options.EnableKeyFrameOnlyExtraction, _encodingHelper, cancellationToken).ConfigureAwait(false); @@ -159,7 +183,7 @@ public class TrickplayManager : ITrickplayManager .ToList(); // Create tiles - var trickplayInfo = CreateTiles(images, width, options, outputDir); + var trickplayInfo = CreateTiles(images, actualWidth, options, outputDir); // Save tiles info try @@ -207,7 +231,7 @@ public class TrickplayManager : ITrickplayManager throw new ArgumentException("Can't create trickplay from 0 images."); } - var workDir = Path.Combine(_appPaths.TempDirectory, Guid.NewGuid().ToString("N")); + var workDir = Path.Combine(_appPaths.TempDirectory, "trickplay_" + Guid.NewGuid().ToString("N")); Directory.CreateDirectory(workDir); var trickplayInfo = new TrickplayInfo @@ -250,7 +274,7 @@ public class TrickplayManager : ITrickplayManager } // Update bitrate - var bitrate = (int)Math.Ceiling((decimal)new FileInfo(tilePath).Length * 8 / trickplayInfo.TileWidth / trickplayInfo.TileHeight / (trickplayInfo.Interval / 1000)); + var bitrate = (int)Math.Ceiling(new FileInfo(tilePath).Length * 8m / trickplayInfo.TileWidth / trickplayInfo.TileHeight / (trickplayInfo.Interval / 1000m)); trickplayInfo.Bandwidth = Math.Max(trickplayInfo.Bandwidth, bitrate); } diff --git a/Jellyfin.Server.Implementations/Users/UserManager.cs b/Jellyfin.Server.Implementations/Users/UserManager.cs index 41f1ac351..5753e75c9 100644 --- a/Jellyfin.Server.Implementations/Users/UserManager.cs +++ b/Jellyfin.Server.Implementations/Users/UserManager.cs @@ -1,7 +1,7 @@ #pragma warning disable CA1307 -#pragma warning disable CA1309 // Use ordinal string comparison - EF can't translate this using System; +using System.Collections.Concurrent; using System.Collections.Generic; using System.Globalization; using System.Linq; @@ -47,6 +47,8 @@ namespace Jellyfin.Server.Implementations.Users private readonly DefaultPasswordResetProvider _defaultPasswordResetProvider; private readonly IServerConfigurationManager _serverConfigurationManager; + private readonly IDictionary<Guid, User> _users; + /// <summary> /// Initializes a new instance of the <see cref="UserManager"/> class. /// </summary> @@ -84,30 +86,29 @@ namespace Jellyfin.Server.Implementations.Users _invalidAuthProvider = _authenticationProviders.OfType<InvalidAuthProvider>().First(); _defaultAuthenticationProvider = _authenticationProviders.OfType<DefaultAuthenticationProvider>().First(); _defaultPasswordResetProvider = _passwordResetProviders.OfType<DefaultPasswordResetProvider>().First(); + + _users = new ConcurrentDictionary<Guid, User>(); + using var dbContext = _dbProvider.CreateDbContext(); + foreach (var user in dbContext.Users + .AsSplitQuery() + .Include(user => user.Permissions) + .Include(user => user.Preferences) + .Include(user => user.AccessSchedules) + .Include(user => user.ProfileImage) + .AsEnumerable()) + { + _users.Add(user.Id, user); + } } /// <inheritdoc/> public event EventHandler<GenericEventArgs<User>>? OnUserUpdated; /// <inheritdoc/> - public IEnumerable<User> Users - { - get - { - using var dbContext = _dbProvider.CreateDbContext(); - return GetUsersInternal(dbContext).ToList(); - } - } + public IEnumerable<User> Users => _users.Values; /// <inheritdoc/> - public IEnumerable<Guid> UsersIds - { - get - { - using var dbContext = _dbProvider.CreateDbContext(); - return dbContext.Users.Select(u => u.Id).ToList(); - } - } + public IEnumerable<Guid> UsersIds => _users.Keys; // This is some regex that matches only on unicode "word" characters, as well as -, _ and @ // In theory this will cut out most if not all 'control' characters which should help minimize any weirdness @@ -123,8 +124,8 @@ namespace Jellyfin.Server.Implementations.Users throw new ArgumentException("Guid can't be empty", nameof(id)); } - using var dbContext = _dbProvider.CreateDbContext(); - return GetUsersInternal(dbContext).FirstOrDefault(u => u.Id.Equals(id)); + _users.TryGetValue(id, out var user); + return user; } /// <inheritdoc/> @@ -135,9 +136,7 @@ namespace Jellyfin.Server.Implementations.Users throw new ArgumentException("Invalid username", nameof(name)); } - using var dbContext = _dbProvider.CreateDbContext(); - return GetUsersInternal(dbContext) - .FirstOrDefault(u => string.Equals(u.Username, name)); + return _users.Values.FirstOrDefault(u => string.Equals(u.Username, name, StringComparison.OrdinalIgnoreCase)); } /// <inheritdoc/> @@ -202,6 +201,8 @@ namespace Jellyfin.Server.Implementations.Users user.AddDefaultPermissions(); user.AddDefaultPreferences(); + _users.Add(user.Id, user); + return user; } @@ -236,46 +237,40 @@ namespace Jellyfin.Server.Implementations.Users /// <inheritdoc/> public async Task DeleteUserAsync(Guid userId) { - var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false); + if (!_users.TryGetValue(userId, out var user)) + { + throw new ResourceNotFoundException(nameof(userId)); + } - await using (dbContext.ConfigureAwait(false)) + if (_users.Count == 1) { - var user = await dbContext.Users - .AsSingleQuery() - .Include(u => u.Permissions) - .FirstOrDefaultAsync(u => u.Id.Equals(userId)) - .ConfigureAwait(false); - if (user is null) - { - throw new ResourceNotFoundException(nameof(userId)); - } + throw new InvalidOperationException(string.Format( + CultureInfo.InvariantCulture, + "The user '{0}' cannot be deleted because there must be at least one user in the system.", + user.Username)); + } - if (await dbContext.Users.CountAsync().ConfigureAwait(false) == 1) - { - throw new InvalidOperationException(string.Format( + if (user.HasPermission(PermissionKind.IsAdministrator) + && Users.Count(i => i.HasPermission(PermissionKind.IsAdministrator)) == 1) + { + throw new ArgumentException( + string.Format( CultureInfo.InvariantCulture, - "The user '{0}' cannot be deleted because there must be at least one user in the system.", - user.Username)); - } - - if (user.HasPermission(PermissionKind.IsAdministrator) - && await dbContext.Users - .CountAsync(u => u.Permissions.Any(p => p.Kind == PermissionKind.IsAdministrator && p.Value)) - .ConfigureAwait(false) == 1) - { - throw new ArgumentException( - string.Format( - CultureInfo.InvariantCulture, - "The user '{0}' cannot be deleted because there must be at least one admin user in the system.", - user.Username), - nameof(userId)); - } + "The user '{0}' cannot be deleted because there must be at least one admin user in the system.", + user.Username), + nameof(userId)); + } + var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false); + await using (dbContext.ConfigureAwait(false)) + { dbContext.Users.Remove(user); await dbContext.SaveChangesAsync().ConfigureAwait(false); - - await _eventManager.PublishAsync(new UserDeletedEventArgs(user)).ConfigureAwait(false); } + + _users.Remove(userId); + + await _eventManager.PublishAsync(new UserDeletedEventArgs(user)).ConfigureAwait(false); } /// <inheritdoc/> @@ -542,23 +537,23 @@ namespace Jellyfin.Server.Implementations.Users /// <inheritdoc /> public async Task InitializeAsync() { - var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false); - await using (dbContext.ConfigureAwait(false)) + // TODO: Refactor the startup wizard so that it doesn't require a user to already exist. + if (_users.Any()) { - // TODO: Refactor the startup wizard so that it doesn't require a user to already exist. - if (await dbContext.Users.AnyAsync().ConfigureAwait(false)) - { - return; - } + return; + } - var defaultName = Environment.UserName; - if (string.IsNullOrWhiteSpace(defaultName) || !ValidUsernameRegex().IsMatch(defaultName)) - { - defaultName = "MyJellyfinUser"; - } + var defaultName = Environment.UserName; + if (string.IsNullOrWhiteSpace(defaultName) || !ValidUsernameRegex().IsMatch(defaultName)) + { + defaultName = "MyJellyfinUser"; + } - _logger.LogWarning("No users, creating one with username {UserName}", defaultName); + _logger.LogWarning("No users, creating one with username {UserName}", defaultName); + 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); @@ -605,9 +600,12 @@ namespace Jellyfin.Server.Implementations.Users var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false); await using (dbContext.ConfigureAwait(false)) { - var user = await GetUsersInternal(dbContext) - .FirstOrDefaultAsync(u => u.Id.Equals(userId)) - .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; @@ -635,6 +633,7 @@ namespace Jellyfin.Server.Implementations.Users user.SetPreference(PreferenceKind.LatestItemExcludes, config.LatestItemsExcludes); dbContext.Update(user); + _users[user.Id] = user; await dbContext.SaveChangesAsync().ConfigureAwait(false); } } @@ -645,9 +644,12 @@ namespace Jellyfin.Server.Implementations.Users var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false); await using (dbContext.ConfigureAwait(false)) { - var user = await GetUsersInternal(dbContext) - .FirstOrDefaultAsync(u => u.Id.Equals(userId)) - .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" @@ -708,6 +710,7 @@ namespace Jellyfin.Server.Implementations.Users user.SetPreference(PreferenceKind.EnableContentDeletionFromFolders, policy.EnableContentDeletionFromFolders); dbContext.Update(user); + _users[user.Id] = user; await dbContext.SaveChangesAsync().ConfigureAwait(false); } } @@ -728,6 +731,7 @@ namespace Jellyfin.Server.Implementations.Users } user.ProfileImage = null; + _users[user.Id] = user; } internal static void ThrowIfInvalidUsername(string name) @@ -874,15 +878,8 @@ namespace Jellyfin.Server.Implementations.Users private async Task UpdateUserInternalAsync(JellyfinDbContext dbContext, User user) { dbContext.Users.Update(user); + _users[user.Id] = user; await dbContext.SaveChangesAsync().ConfigureAwait(false); } - - private IQueryable<User> GetUsersInternal(JellyfinDbContext dbContext) - => dbContext.Users - .AsSplitQuery() - .Include(user => user.Permissions) - .Include(user => user.Preferences) - .Include(user => user.AccessSchedules) - .Include(user => user.ProfileImage); } } |
