From 3a4dff8cc475e53691466dde2db9828b4d1b66aa Mon Sep 17 00:00:00 2001 From: WizardOfYendor1 Date: Sat, 28 Feb 2026 11:21:53 -0500 Subject: Fix live stream consumer leak when PositionTicks is negative When a stalled HLS client sends ReportPlaybackStopped with a negative PositionTicks (e.g. HLS.js reporting position on buffer stall), the ArgumentOutOfRangeException was thrown before GetSession was called, causing CloseLiveStreamIfNeededAsync to never run. This left the ILiveStream.ConsumerCount permanently incremented, permanently holding a tuner open with no active viewer. Fix by acquiring the session and performing live stream cleanup before throwing the validation exception, so tuner resources are always freed even when playback reporting fails with invalid position data. Co-Authored-By: Claude Opus 4.6 --- Emby.Server.Implementations/Session/SessionManager.cs | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) (limited to 'Emby.Server.Implementations/Session') diff --git a/Emby.Server.Implementations/Session/SessionManager.cs b/Emby.Server.Implementations/Session/SessionManager.cs index 8e14f5bdf4..7e13e17eb6 100644 --- a/Emby.Server.Implementations/Session/SessionManager.cs +++ b/Emby.Server.Implementations/Session/SessionManager.cs @@ -1025,15 +1025,22 @@ namespace Emby.Server.Implementations.Session ArgumentNullException.ThrowIfNull(info); + var session = GetSession(info.SessionId); + + session.StopAutomaticProgress(); + if (info.PositionTicks.HasValue && info.PositionTicks.Value < 0) { + // Ensure live stream is cleaned up before throwing, to prevent tuner + // resource leaks when stalled clients report a negative PositionTicks. + if (!string.IsNullOrEmpty(info.LiveStreamId)) + { + await CloseLiveStreamIfNeededAsync(info.LiveStreamId, session.Id).ConfigureAwait(false); + } + throw new ArgumentOutOfRangeException(nameof(info), "The PlaybackStopInfo's PositionTicks was negative."); } - var session = GetSession(info.SessionId); - - session.StopAutomaticProgress(); - var libraryItem = info.ItemId.IsEmpty() ? null : GetNowPlayingItem(session, info.ItemId); -- cgit v1.2.3 From 6ea2f05497efa6e8fce8e88be12f5385c1e2600d Mon Sep 17 00:00:00 2001 From: llaforest Date: Mon, 4 May 2026 13:24:43 -0400 Subject: Guard against null-overwrite of saved audio/subtitle track selections Some clients omit AudioStreamIndex or SubtitleStreamIndex in playback progress reports and it causes previously saved track selections to be erased. Add .HasValue checks so only explicit track changes are persisted. --- Emby.Server.Implementations/Session/SessionManager.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'Emby.Server.Implementations/Session') diff --git a/Emby.Server.Implementations/Session/SessionManager.cs b/Emby.Server.Implementations/Session/SessionManager.cs index e2ddf86c7a..5652d0c9b5 100644 --- a/Emby.Server.Implementations/Session/SessionManager.cs +++ b/Emby.Server.Implementations/Session/SessionManager.cs @@ -973,7 +973,7 @@ namespace Emby.Server.Implementations.Session if (user.RememberAudioSelections) { - if (data.AudioStreamIndex != info.AudioStreamIndex) + if (info.AudioStreamIndex.HasValue && data.AudioStreamIndex != info.AudioStreamIndex) { data.AudioStreamIndex = info.AudioStreamIndex; changed = true; @@ -990,7 +990,7 @@ namespace Emby.Server.Implementations.Session if (user.RememberSubtitleSelections) { - if (data.SubtitleStreamIndex != info.SubtitleStreamIndex) + if (info.SubtitleStreamIndex.HasValue && data.SubtitleStreamIndex != info.SubtitleStreamIndex) { data.SubtitleStreamIndex = info.SubtitleStreamIndex; changed = true; -- cgit v1.2.3 From ec054f6a345ab6407f39a95c3404b3d7651b2993 Mon Sep 17 00:00:00 2001 From: JPVenson Date: Tue, 5 May 2026 17:57:27 +0000 Subject: Backport changes from #15368 --- .../Session/SessionManager.cs | 2 +- Jellyfin.Api/Controllers/StartupController.cs | 13 +- Jellyfin.Api/Controllers/UserController.cs | 14 +- .../Users/DefaultPasswordResetProvider.cs | 2 +- .../Users/UserManager.cs | 681 ++++++++++++--------- MediaBrowser.Controller/Library/IUserManager.cs | 27 +- src/Jellyfin.LiveTv/LiveTvManager.cs | 4 +- .../Recordings/RecordingNotifier.cs | 2 +- 8 files changed, 427 insertions(+), 318 deletions(-) (limited to 'Emby.Server.Implementations/Session') diff --git a/Emby.Server.Implementations/Session/SessionManager.cs b/Emby.Server.Implementations/Session/SessionManager.cs index 5652d0c9b5..582e8ce8dc 100644 --- a/Emby.Server.Implementations/Session/SessionManager.cs +++ b/Emby.Server.Implementations/Session/SessionManager.cs @@ -2049,7 +2049,7 @@ namespace Emby.Server.Implementations.Session { CheckDisposed(); - var adminUserIds = _userManager.Users + var adminUserIds = _userManager.GetUsers() .Where(i => i.HasPermission(PermissionKind.IsAdministrator)) .Select(i => i.Id) .ToList(); diff --git a/Jellyfin.Api/Controllers/StartupController.cs b/Jellyfin.Api/Controllers/StartupController.cs index 09f20558fe..9378cfedb6 100644 --- a/Jellyfin.Api/Controllers/StartupController.cs +++ b/Jellyfin.Api/Controllers/StartupController.cs @@ -1,5 +1,5 @@ +using System; using System.ComponentModel.DataAnnotations; -using System.Linq; using System.Threading.Tasks; using Jellyfin.Api.Constants; using Jellyfin.Api.Models.StartupDtos; @@ -111,7 +111,7 @@ public class StartupController : BaseJellyfinApiController { // TODO: Remove this method when startup wizard no longer requires an existing user. await _userManager.InitializeAsync().ConfigureAwait(false); - var user = _userManager.Users.First(); + var user = _userManager.GetFirstUser() ?? throw new InvalidOperationException("No user exists after initialization."); return new StartupUserDto { Name = user.Username @@ -131,7 +131,12 @@ public class StartupController : BaseJellyfinApiController [ProducesResponseType(StatusCodes.Status204NoContent)] public async Task UpdateStartupUser([FromBody] StartupUserDto startupUserDto) { - var user = _userManager.Users.First(); + var user = _userManager.GetFirstUser(); + if (user is null) + { + return NotFound(); + } + if (string.IsNullOrWhiteSpace(startupUserDto.Password)) { return BadRequest("Password must not be empty"); @@ -146,7 +151,7 @@ public class StartupController : BaseJellyfinApiController if (!string.IsNullOrEmpty(startupUserDto.Password)) { - await _userManager.ChangePassword(user, startupUserDto.Password).ConfigureAwait(false); + await _userManager.ChangePassword(user.Id, startupUserDto.Password).ConfigureAwait(false); } return NoContent(); diff --git a/Jellyfin.Api/Controllers/UserController.cs b/Jellyfin.Api/Controllers/UserController.cs index 536b95dbb5..55cc66f79f 100644 --- a/Jellyfin.Api/Controllers/UserController.cs +++ b/Jellyfin.Api/Controllers/UserController.cs @@ -288,7 +288,7 @@ public class UserController : BaseJellyfinApiController if (request.ResetPassword) { - await _userManager.ResetPassword(user).ConfigureAwait(false); + await _userManager.ResetPassword(user.Id).ConfigureAwait(false); } else { @@ -306,7 +306,7 @@ public class UserController : BaseJellyfinApiController } } - await _userManager.ChangePassword(user, request.NewPw ?? string.Empty).ConfigureAwait(false); + await _userManager.ChangePassword(user.Id, request.NewPw ?? string.Empty).ConfigureAwait(false); var currentToken = User.GetToken(); @@ -369,7 +369,7 @@ public class UserController : BaseJellyfinApiController if (!string.Equals(user.Username, updateUser.Name, StringComparison.Ordinal)) { - await _userManager.RenameUser(user, updateUser.Name).ConfigureAwait(false); + await _userManager.RenameUser(user.Id, user.Username, updateUser.Name).ConfigureAwait(false); } await _userManager.UpdateConfigurationAsync(requestUserId, updateUser.Configuration).ConfigureAwait(false); @@ -425,7 +425,7 @@ public class UserController : BaseJellyfinApiController // If removing admin access if (!newPolicy.IsAdministrator && user.HasPermission(PermissionKind.IsAdministrator)) { - if (_userManager.Users.Count(i => i.HasPermission(PermissionKind.IsAdministrator)) == 1) + if (_userManager.GetUsers().Count(i => i.HasPermission(PermissionKind.IsAdministrator)) == 1) { return StatusCode(StatusCodes.Status403Forbidden, "There must be at least one user in the system with administrative access."); } @@ -440,7 +440,7 @@ public class UserController : BaseJellyfinApiController // If disabling if (newPolicy.IsDisabled && !user.HasPermission(PermissionKind.IsDisabled)) { - if (_userManager.Users.Count(i => !i.HasPermission(PermissionKind.IsDisabled)) == 1) + if (_userManager.GetUsers().Count(i => !i.HasPermission(PermissionKind.IsDisabled)) == 1) { return StatusCode(StatusCodes.Status403Forbidden, "There must be at least one enabled user in the system."); } @@ -522,7 +522,7 @@ public class UserController : BaseJellyfinApiController // no need to authenticate password for new user if (request.Password is not null) { - await _userManager.ChangePassword(newUser, request.Password).ConfigureAwait(false); + await _userManager.ChangePassword(newUser.Id, request.Password).ConfigureAwait(false); } var result = _userManager.GetUserDto(newUser, HttpContext.GetNormalizedRemoteIP().ToString()); @@ -597,7 +597,7 @@ public class UserController : BaseJellyfinApiController private IEnumerable Get(bool? isHidden, bool? isDisabled, bool filterByDevice, bool filterByNetwork) { - var users = _userManager.Users; + var users = _userManager.GetUsers(); if (isDisabled.HasValue) { diff --git a/Jellyfin.Server.Implementations/Users/DefaultPasswordResetProvider.cs b/Jellyfin.Server.Implementations/Users/DefaultPasswordResetProvider.cs index 49a9fda943..7371545914 100644 --- a/Jellyfin.Server.Implementations/Users/DefaultPasswordResetProvider.cs +++ b/Jellyfin.Server.Implementations/Users/DefaultPasswordResetProvider.cs @@ -74,7 +74,7 @@ namespace Jellyfin.Server.Implementations.Users var resetUser = userManager.GetUserByName(spr.UserName) ?? throw new ResourceNotFoundException($"User with a username of {spr.UserName} not found"); - await userManager.ChangePassword(resetUser, pin).ConfigureAwait(false); + await userManager.ChangePassword(resetUser.Id, pin).ConfigureAwait(false); usersReset.Add(resetUser.Username); File.Delete(resetFile); } diff --git a/Jellyfin.Server.Implementations/Users/UserManager.cs b/Jellyfin.Server.Implementations/Users/UserManager.cs index 7292e9c7a9..8c0cbbd448 100644 --- a/Jellyfin.Server.Implementations/Users/UserManager.cs +++ b/Jellyfin.Server.Implementations/Users/UserManager.cs @@ -1,12 +1,14 @@ #pragma warning disable CA1307 +#pragma warning disable RS0030 // Do not use banned APIs using System; -using System.Collections.Concurrent; using System.Collections.Generic; using System.Globalization; using System.Linq; using System.Text.RegularExpressions; +using System.Threading; using System.Threading.Tasks; +using AsyncKeyedLock; using Jellyfin.Data; using Jellyfin.Data.Enums; using Jellyfin.Data.Events; @@ -35,7 +37,7 @@ namespace Jellyfin.Server.Implementations.Users /// /// Manages the creation and retrieval of instances. /// - public partial class UserManager : IUserManager + public partial class UserManager : IUserManager, IDisposable { private readonly IDbContextFactory _dbProvider; private readonly IEventManager _eventManager; @@ -50,7 +52,7 @@ namespace Jellyfin.Server.Implementations.Users private readonly DefaultPasswordResetProvider _defaultPasswordResetProvider; private readonly IServerConfigurationManager _serverConfigurationManager; - private readonly IDictionary _users; + private readonly AsyncKeyedLocker _userLock = new(); /// /// Initializes a new instance of the class. @@ -89,29 +91,28 @@ namespace Jellyfin.Server.Implementations.Users _invalidAuthProvider = _authenticationProviders.OfType().First(); _defaultAuthenticationProvider = _authenticationProviders.OfType().First(); _defaultPasswordResetProvider = _passwordResetProviders.OfType().First(); - - _users = new ConcurrentDictionary(); - using var dbContext = _dbProvider.CreateDbContext(); - foreach (var user in dbContext.Users - .AsSingleQuery() - .Include(user => user.Permissions) - .Include(user => user.Preferences) - .Include(user => user.AccessSchedules) - .Include(user => user.ProfileImage) - .AsEnumerable()) - { - _users.Add(user.Id, user); - } } /// public event EventHandler>? OnUserUpdated; /// - public IEnumerable Users => _users.Values; + public IEnumerable GetUsers() + { + using var dbContext = _dbProvider.CreateDbContext(); + return UserQuery(dbContext) + .ToArray(); + } /// - public IEnumerable UsersIds => _users.Keys; + public IEnumerable GetUsersIds() + { + using var dbContext = _dbProvider.CreateDbContext(); + return dbContext.Users + .AsNoTracking() + .Select(user => user.Id) + .ToArray(); + } // 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 @@ -127,8 +128,27 @@ namespace Jellyfin.Server.Implementations.Users throw new ArgumentException("Guid can't be empty", nameof(id)); } - _users.TryGetValue(id, out var user); - return user; + using var dbContext = _dbProvider.CreateDbContext(); + return UserQuery(dbContext) + .FirstOrDefault(user => user.Id == id); + } + + private static IQueryable UserQuery(JellyfinDbContext dbContext) + { + return dbContext.Users + .AsSingleQuery() + .Include(user => user.Permissions) + .Include(user => user.Preferences) + .Include(user => user.AccessSchedules) + .Include(user => user.ProfileImage) + .AsNoTracking(); + } + + /// + public User? GetFirstUser() + { + using var dbContext = _dbProvider.CreateDbContext(); + return UserQuery(dbContext).FirstOrDefault(); } /// @@ -139,42 +159,57 @@ namespace Jellyfin.Server.Implementations.Users throw new ArgumentException("Invalid username", nameof(name)); } - return _users.Values.FirstOrDefault(u => string.Equals(u.Username, name, StringComparison.OrdinalIgnoreCase)); + using var dbContext = _dbProvider.CreateDbContext(); +#pragma warning disable CA1862 // Use the 'StringComparison' method overloads to perform case-insensitive string comparisons +#pragma warning disable CA1311 // Specify a culture or use an invariant version to avoid implicit dependency on current culture +#pragma warning disable CA1304 // The behavior of 'string.ToUpper()' could vary based on the current user's locale settings + return UserQuery(dbContext) + .FirstOrDefault(u => u.Username.ToUpper() == name.ToUpper()); +#pragma warning restore CA1304 // The behavior of 'string.ToUpper()' could vary based on the current user's locale settings +#pragma warning restore CA1311 // Specify a culture or use an invariant version to avoid implicit dependency on current culture +#pragma warning restore CA1862 // Use the 'StringComparison' method overloads to perform case-insensitive string comparisons } /// - public async Task RenameUser(User user, string newName) + public async Task RenameUser(Guid userId, string oldName, string newName) { - ArgumentNullException.ThrowIfNull(user); - ThrowIfInvalidUsername(newName); - if (user.Username.Equals(newName, StringComparison.Ordinal)) + if (oldName.Equals(newName, StringComparison.OrdinalIgnoreCase)) { throw new ArgumentException("The new and old names must be different."); } - var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false); - await using (dbContext.ConfigureAwait(false)) + User user = null!; // user is never actually null where its used afterwards so we can just ignore. + using (await _userLock.LockAsync(userId).ConfigureAwait(false)) { + var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false); + await using (dbContext.ConfigureAwait(false)) + { #pragma warning disable CA1862 // Use the 'StringComparison' method overloads to perform case-insensitive string comparisons #pragma warning disable CA1311 // Specify a culture or use an invariant version to avoid implicit dependency on current culture #pragma warning disable CA1304 // The behavior of 'string.ToUpper()' could vary based on the current user's locale settings - if (await dbContext.Users - .AnyAsync(u => u.Username.ToUpper() == newName.ToUpper() && !u.Id.Equals(user.Id)) - .ConfigureAwait(false)) - { - throw new ArgumentException(string.Format( - CultureInfo.InvariantCulture, - "A user with the name '{0}' already exists.", - newName)); - } + if (await dbContext.Users + .AnyAsync(u => u.Username.ToUpper() == newName.ToUpper() && u.Id != userId) + .ConfigureAwait(false)) + { + throw new ArgumentException(string.Format( + CultureInfo.InvariantCulture, + "A user with the name '{0}' already exists.", + newName)); + } #pragma warning restore CA1304 // The behavior of 'string.ToUpper()' could vary based on the current user's locale settings #pragma warning restore CA1311 // Specify a culture or use an invariant version to avoid implicit dependency on current culture #pragma warning restore CA1862 // Use the 'StringComparison' method overloads to perform case-insensitive string comparisons - user.Username = newName; - await UpdateUserInternalAsync(dbContext, user).ConfigureAwait(false); + user = await UserQuery(dbContext) + .AsTracking() + .FirstOrDefaultAsync(u => u.Id == userId) + .ConfigureAwait(false) + ?? throw new ResourceNotFoundException(nameof(userId)); + user.Username = newName; + await UpdateUserInternalAsync(dbContext, user).ConfigureAwait(false); + } } var eventArgs = new UserUpdatedEventArgs(user); @@ -185,10 +220,9 @@ namespace Jellyfin.Server.Implementations.Users /// public async Task UpdateUserAsync(User user) { - var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false); - await using (dbContext.ConfigureAwait(false)) + using (await _userLock.LockAsync(user.Id).ConfigureAwait(false)) { - await UpdateUserInternalAsync(dbContext, user).ConfigureAwait(false); + await UpdateUserInternalAsync(user).ConfigureAwait(false); } } @@ -218,23 +252,30 @@ namespace Jellyfin.Server.Implementations.Users { ThrowIfInvalidUsername(name); - if (Users.Any(u => u.Username.Equals(name, StringComparison.OrdinalIgnoreCase))) - { - throw new ArgumentException(string.Format( - CultureInfo.InvariantCulture, - "A user with the name '{0}' already exists.", - name)); - } - User newUser; var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false); await using (dbContext.ConfigureAwait(false)) { +#pragma warning disable CA1862 // Use the 'StringComparison' method overloads to perform case-insensitive string comparisons +#pragma warning disable CA1311 // Specify a culture or use an invariant version to avoid implicit dependency on current culture +#pragma warning disable CA1304 // The behavior of 'string.ToUpper()' could vary based on the current user's locale settings + if (await dbContext.Users + .AnyAsync(u => u.Username.ToUpper() == name.ToUpper()) + .ConfigureAwait(false)) + { + throw new ArgumentException(string.Format( + CultureInfo.InvariantCulture, + "A user with the name '{0}' already exists.", + name)); + } +#pragma warning restore CA1304 // The behavior of 'string.ToUpper()' could vary based on the current user's locale settings +#pragma warning restore CA1311 // Specify a culture or use an invariant version to avoid implicit dependency on current culture +#pragma warning restore CA1862 // Use the 'StringComparison' method overloads to perform case-insensitive string comparisons + newUser = await CreateUserInternalAsync(name, dbContext).ConfigureAwait(false); dbContext.Users.Add(newUser); await dbContext.SaveChangesAsync().ConfigureAwait(false); - _users.Add(newUser.Id, newUser); } await _eventManager.PublishAsync(new UserCreatedEventArgs(newUser)).ConfigureAwait(false); @@ -245,62 +286,82 @@ namespace Jellyfin.Server.Implementations.Users /// public async Task DeleteUserAsync(Guid userId) { - if (!_users.TryGetValue(userId, out var user)) + User? user; + using (await _userLock.LockAsync(userId).ConfigureAwait(false)) { - throw new ResourceNotFoundException(nameof(userId)); - } + var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false); + await using (dbContext.ConfigureAwait(false)) + { + user = await dbContext.Users + .Include(u => u.Permissions) + .FirstOrDefaultAsync(u => u.Id.Equals(userId)) + .ConfigureAwait(false); + if (user is null) + { + throw new ResourceNotFoundException(nameof(userId)); + } - if (_users.Count == 1) - { - 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)); - } + var userCount = await dbContext.Users.CountAsync().ConfigureAwait(false); + if (userCount == 1) + { + 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 (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 admin user in the system.", - user.Username), - nameof(userId)); - } + if (user.HasPermission(PermissionKind.IsAdministrator) + && await dbContext.Users + .CountAsync(i => i.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)); + } - var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false); - await using (dbContext.ConfigureAwait(false)) - { - dbContext.Users.Attach(user); - dbContext.Users.Remove(user); - await dbContext.SaveChangesAsync().ConfigureAwait(false); + dbContext.Users.Remove(user); + await dbContext.SaveChangesAsync().ConfigureAwait(false); + } } - _users.Remove(userId); - await _eventManager.PublishAsync(new UserDeletedEventArgs(user)).ConfigureAwait(false); } /// - public Task ResetPassword(User user) + public Task ResetPassword(Guid userId) { - return ChangePassword(user, string.Empty); + return ChangePassword(userId, string.Empty); } /// - public async Task ChangePassword(User user, string newPassword) + public async Task ChangePassword(Guid userId, string newPassword) { - ArgumentNullException.ThrowIfNull(user); - if (user.HasPermission(PermissionKind.IsAdministrator) && string.IsNullOrWhiteSpace(newPassword)) + User dbUser = null!; + using (await _userLock.LockAsync(userId).ConfigureAwait(false)) { - throw new ArgumentException("Admin user passwords must not be empty", nameof(newPassword)); - } + var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false); + await using (dbContext.ConfigureAwait(false)) + { + dbUser = await UserQuery(dbContext) + .AsTracking() + .FirstOrDefaultAsync(u => u.Id == userId) + .ConfigureAwait(false) + ?? throw new ResourceNotFoundException(nameof(userId)); + if (dbUser.HasPermission(PermissionKind.IsAdministrator) && string.IsNullOrWhiteSpace(newPassword)) + { + throw new ArgumentException("Admin user passwords must not be empty", nameof(newPassword)); + } - await GetAuthenticationProvider(user).ChangePassword(user, newPassword).ConfigureAwait(false); - await UpdateUserAsync(user).ConfigureAwait(false); + await GetAuthenticationProvider(dbUser).ChangePassword(dbUser, newPassword).ConfigureAwait(false); + await dbContext.SaveChangesAsync().ConfigureAwait(false); + } + } - await _eventManager.PublishAsync(new UserPasswordChangedEventArgs(user)).ConfigureAwait(false); + await _eventManager.PublishAsync(new UserPasswordChangedEventArgs(dbUser)).ConfigureAwait(false); } /// @@ -400,102 +461,114 @@ namespace Jellyfin.Server.Implementations.Users throw new ArgumentNullException(nameof(username)); } - var user = Users.FirstOrDefault(i => string.Equals(username, i.Username, StringComparison.OrdinalIgnoreCase)); - var authResult = await AuthenticateLocalUser(username, password, user) - .ConfigureAwait(false); - var authenticationProvider = authResult.AuthenticationProvider; - var success = authResult.Success; - - if (user is null) + bool success; + var user = GetUserByName(username); + using (await _userLock.LockAsync(user?.Id ?? Guid.Empty).ConfigureAwait(false)) { - string updatedUsername = authResult.Username; - - if (success - && authenticationProvider is not null - && authenticationProvider is not DefaultAuthenticationProvider) + // Reload the user now that we hold the lock so the RowVersion is current. + // GetUserByName uses AsNoTracking and the snapshot may be stale if another + // write (e.g. a concurrent login) incremented RowVersion after our initial load. + if (user is not null) { - // Trust the username returned by the authentication provider - username = updatedUsername; + user = GetUserById(user.Id) ?? user; + } - // Search the database for the user again - // the authentication provider might have created it - user = Users.FirstOrDefault(i => string.Equals(username, i.Username, StringComparison.OrdinalIgnoreCase)); + var authResult = await AuthenticateLocalUser(username, password, user) + .ConfigureAwait(false); + var authenticationProvider = authResult.AuthenticationProvider; + success = authResult.Success; - if (authenticationProvider is IHasNewUserPolicy hasNewUserPolicy && user is not null) + if (user is null) + { + string updatedUsername = authResult.Username; + + if (success + && authenticationProvider is not null + && authenticationProvider is not DefaultAuthenticationProvider) { - await UpdatePolicyAsync(user.Id, hasNewUserPolicy.GetNewUserPolicy()).ConfigureAwait(false); + // Trust the username returned by the authentication provider + username = updatedUsername; + + // Search the database for the user again + // the authentication provider might have created it + user = GetUserByName(username); + + if (authenticationProvider is IHasNewUserPolicy hasNewUserPolicy && user is not null) + { + await UpdatePolicyAsync(user.Id, hasNewUserPolicy.GetNewUserPolicy()).ConfigureAwait(false); + } } } - } - if (success && user is not null && authenticationProvider is not null) - { - var providerId = authenticationProvider.GetType().FullName; + if (success && user is not null && authenticationProvider is not null) + { + var providerId = authenticationProvider.GetType().FullName; + + if (providerId is not null && !string.Equals(providerId, user.AuthenticationProviderId, StringComparison.OrdinalIgnoreCase)) + { + user.AuthenticationProviderId = providerId; + await UpdateUserInternalAsync(user).ConfigureAwait(false); + } + } - if (providerId is not null && !string.Equals(providerId, user.AuthenticationProviderId, StringComparison.OrdinalIgnoreCase)) + if (user is null) { - user.AuthenticationProviderId = providerId; - await UpdateUserAsync(user).ConfigureAwait(false); + _logger.LogInformation( + "Authentication request for {UserName} has been denied (IP: {IP}).", + username, + remoteEndPoint); + throw new AuthenticationException("Invalid username or password entered."); } - } - if (user is null) - { - _logger.LogInformation( - "Authentication request for {UserName} has been denied (IP: {IP}).", - username, - remoteEndPoint); - throw new AuthenticationException("Invalid username or password entered."); - } + if (user.HasPermission(PermissionKind.IsDisabled)) + { + _logger.LogInformation( + "Authentication request for {UserName} has been denied because this account is currently disabled (IP: {IP}).", + username, + remoteEndPoint); + throw new SecurityException( + $"The {user.Username} account is currently disabled. Please consult with your administrator."); + } - if (user.HasPermission(PermissionKind.IsDisabled)) - { - _logger.LogInformation( - "Authentication request for {UserName} has been denied because this account is currently disabled (IP: {IP}).", - username, - remoteEndPoint); - throw new SecurityException( - $"The {user.Username} account is currently disabled. Please consult with your administrator."); - } + if (!user.HasPermission(PermissionKind.EnableRemoteAccess) && + !_networkManager.IsInLocalNetwork(remoteEndPoint)) + { + _logger.LogInformation( + "Authentication request for {UserName} forbidden: remote access disabled and user not in local network (IP: {IP}).", + username, + remoteEndPoint); + throw new SecurityException("Forbidden."); + } - if (!user.HasPermission(PermissionKind.EnableRemoteAccess) && - !_networkManager.IsInLocalNetwork(remoteEndPoint)) - { - _logger.LogInformation( - "Authentication request for {UserName} forbidden: remote access disabled and user not in local network (IP: {IP}).", - username, - remoteEndPoint); - throw new SecurityException("Forbidden."); - } + if (!user.IsParentalScheduleAllowed()) + { + _logger.LogInformation( + "Authentication request for {UserName} is not allowed at this time due parental restrictions (IP: {IP}).", + username, + remoteEndPoint); + throw new SecurityException("User is not allowed access at this time."); + } - if (!user.IsParentalScheduleAllowed()) - { - _logger.LogInformation( - "Authentication request for {UserName} is not allowed at this time due parental restrictions (IP: {IP}).", - username, - remoteEndPoint); - throw new SecurityException("User is not allowed access at this time."); - } + // Update LastActivityDate and LastLoginDate, then save + if (success) + { + if (isUserSession) + { + user.LastActivityDate = user.LastLoginDate = DateTime.UtcNow; + } - // Update LastActivityDate and LastLoginDate, then save - if (success) - { - if (isUserSession) + user.InvalidLoginAttemptCount = 0; + await UpdateUserInternalAsync(user).ConfigureAwait(false); + _logger.LogInformation("Authentication request for {UserName} has succeeded.", user.Username); + } + else { - user.LastActivityDate = user.LastLoginDate = DateTime.UtcNow; + await IncrementInvalidLoginAttemptCount(user).ConfigureAwait(false); + _logger.LogInformation( + "Authentication request for {UserName} has been denied (IP: {IP}).", + user.Username, + remoteEndPoint); } - - user.InvalidLoginAttemptCount = 0; - await UpdateUserAsync(user).ConfigureAwait(false); - _logger.LogInformation("Authentication request for {UserName} has succeeded.", user.Username); - } - else - { - await IncrementInvalidLoginAttemptCount(user).ConfigureAwait(false); - _logger.LogInformation( - "Authentication request for {UserName} has been denied (IP: {IP}).", - user.Username, - remoteEndPoint); } return success ? user : null; @@ -539,22 +612,22 @@ namespace Jellyfin.Server.Implementations.Users public async Task InitializeAsync() { // TODO: Refactor the startup wizard so that it doesn't require a user to already exist. - if (_users.Any()) + var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false); + await using (dbContext.ConfigureAwait(false)) { - return; - } + if (await dbContext.Users.AnyAsync().ConfigureAwait(false)) + { + 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); @@ -562,7 +635,6 @@ namespace Jellyfin.Server.Implementations.Users dbContext.Users.Add(newUser); await dbContext.SaveChangesAsync().ConfigureAwait(false); - _users.Add(newUser.Id, newUser); } } @@ -599,124 +671,120 @@ namespace Jellyfin.Server.Implementations.Users /// public async Task UpdateConfigurationAsync(Guid userId, UserConfiguration config) { - var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false); - await using (dbContext.ConfigureAwait(false)) + using (await _userLock.LockAsync(userId).ConfigureAwait(false)) { - var user = dbContext.Users - .Include(u => u.Permissions) - .Include(u => u.Preferences) - .Include(u => u.AccessSchedules) - .Include(u => u.ProfileImage) - .AsSingleQuery() - .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; - - // Only set cast receiver id if it is passed in and it exists in the server config. - if (!string.IsNullOrEmpty(config.CastReceiverId) - && _serverConfigurationManager.Configuration.CastReceiverApplications.Any(c => string.Equals(c.Id, config.CastReceiverId, StringComparison.Ordinal))) + var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false); + await using (dbContext.ConfigureAwait(false)) { - user.CastReceiverId = config.CastReceiverId; - } + var user = UserQuery(dbContext) + .AsTracking() + .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; + + // Only set cast receiver id if it is passed in and it exists in the server config. + if (!string.IsNullOrEmpty(config.CastReceiverId) + && _serverConfigurationManager.Configuration.CastReceiverApplications.Any(c => string.Equals(c.Id, config.CastReceiverId, StringComparison.Ordinal))) + { + user.CastReceiverId = config.CastReceiverId; + } - 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); + await dbContext.SaveChangesAsync().ConfigureAwait(false); + } } } /// public async Task UpdatePolicyAsync(Guid userId, UserPolicy policy) { - var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false); - await using (dbContext.ConfigureAwait(false)) + using (await _userLock.LockAsync(userId).ConfigureAwait(false)) { - var user = dbContext.Users - .Include(u => u.Permissions) - .Include(u => u.Preferences) - .Include(u => u.AccessSchedules) - .Include(u => u.ProfileImage) - .AsSingleQuery() - .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 = UserQuery(dbContext) + .AsTracking() + .FirstOrDefault(u => u.Id.Equals(userId)) + ?? throw new ArgumentException("No user exists with given Id!"); - user.MaxParentalRatingScore = policy.MaxParentalRating; - user.MaxParentalRatingSubScore = policy.MaxParentalSubRating; - 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.EnableCollectionManagement, policy.EnableCollectionManagement); - user.SetPermission(PermissionKind.EnableSubtitleManagement, policy.EnableSubtitleManagement); - user.SetPermission(PermissionKind.EnableLyricManagement, policy.EnableLyricManagement); - 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); - } + // 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.MaxParentalRatingScore = policy.MaxParentalRating; + user.MaxParentalRatingSubScore = policy.MaxParentalSubRating; + 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.EnableCollectionManagement, policy.EnableCollectionManagement); + user.SetPermission(PermissionKind.EnableSubtitleManagement, policy.EnableSubtitleManagement); + user.SetPermission(PermissionKind.EnableLyricManagement, policy.EnableLyricManagement); + 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()); - user.SetPreference(PreferenceKind.BlockedTags, policy.BlockedTags); - user.SetPreference(PreferenceKind.AllowedTags, policy.AllowedTags); - 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()); + user.SetPreference(PreferenceKind.BlockedTags, policy.BlockedTags); + user.SetPreference(PreferenceKind.AllowedTags, policy.AllowedTags); + 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); + await dbContext.SaveChangesAsync().ConfigureAwait(false); + } } } @@ -728,15 +796,17 @@ namespace Jellyfin.Server.Implementations.Users return; } - var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false); - await using (dbContext.ConfigureAwait(false)) + using (await _userLock.LockAsync(user.Id).ConfigureAwait(false)) { - 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; + user.ProfileImage = null; + } } internal static void ThrowIfInvalidUsername(string name) @@ -882,15 +952,42 @@ namespace Jellyfin.Server.Implementations.Users user.InvalidLoginAttemptCount); } - await UpdateUserAsync(user).ConfigureAwait(false); + await UpdateUserInternalAsync(user).ConfigureAwait(false); + } + + private async Task UpdateUserInternalAsync(User user) + { + var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false); + await using (dbContext.ConfigureAwait(false)) + { + await UpdateUserInternalAsync(dbContext, user).ConfigureAwait(false); + } } private async Task UpdateUserInternalAsync(JellyfinDbContext dbContext, User user) { dbContext.Users.Attach(user); dbContext.Entry(user).State = EntityState.Modified; - _users[user.Id] = user; await dbContext.SaveChangesAsync().ConfigureAwait(false); } + + /// + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + /// + /// Disposes all members of this class. + /// + /// Defines if the class has been cleaned up by a dispose or finalizer. + protected virtual void Dispose(bool disposing) + { + if (disposing) + { + _userLock.Dispose(); + } + } } } diff --git a/MediaBrowser.Controller/Library/IUserManager.cs b/MediaBrowser.Controller/Library/IUserManager.cs index 0109cf4b7d..e2b54ea7a7 100644 --- a/MediaBrowser.Controller/Library/IUserManager.cs +++ b/MediaBrowser.Controller/Library/IUserManager.cs @@ -24,14 +24,14 @@ namespace MediaBrowser.Controller.Library /// /// Gets the users. /// - /// The users. - IEnumerable Users { get; } + /// The users. + IEnumerable GetUsers(); /// /// Gets the user ids. /// - /// The users ids. - IEnumerable UsersIds { get; } + /// The users ids. + IEnumerable GetUsersIds(); /// /// Initializes the user manager and ensures that a user exists. @@ -47,6 +47,12 @@ namespace MediaBrowser.Controller.Library /// id is an empty Guid. User? GetUserById(Guid id); + /// + /// Gets the first available user. + /// + /// The first user, or null if no users exist. + User? GetFirstUser(); + /// /// Gets the name of the user by. /// @@ -57,12 +63,13 @@ namespace MediaBrowser.Controller.Library /// /// Renames the user. /// - /// The user. + /// The UserId to change. + /// The old Username. /// The new name. /// Task. /// If user is null. /// If the provided user doesn't exist. - Task RenameUser(User user, string newName); + Task RenameUser(Guid userId, string oldName, string newName); /// /// Updates the user. @@ -92,17 +99,17 @@ namespace MediaBrowser.Controller.Library /// /// Resets the password. /// - /// The user. + /// The users Id. /// Task. - Task ResetPassword(User user); + Task ResetPassword(Guid userId); /// /// Changes the password. /// - /// The user. + /// The users id. /// New password to use. /// Awaitable task. - Task ChangePassword(User user, string newPassword); + Task ChangePassword(Guid userId, string newPassword); /// /// Gets the user dto. diff --git a/src/Jellyfin.LiveTv/LiveTvManager.cs b/src/Jellyfin.LiveTv/LiveTvManager.cs index 1d18ade9dc..2abc8a8c09 100644 --- a/src/Jellyfin.LiveTv/LiveTvManager.cs +++ b/src/Jellyfin.LiveTv/LiveTvManager.cs @@ -1204,7 +1204,7 @@ namespace Jellyfin.LiveTv { Services = services, IsEnabled = services.Length > 0, - EnabledUsers = _userManager.Users + EnabledUsers = _userManager.GetUsers() .Where(IsLiveTvEnabled) .Select(i => i.Id.ToString("N", CultureInfo.InvariantCulture)) .ToArray() @@ -1220,7 +1220,7 @@ namespace Jellyfin.LiveTv public IEnumerable GetEnabledUsers() { - return _userManager.Users + return _userManager.GetUsers() .Where(IsLiveTvEnabled); } diff --git a/src/Jellyfin.LiveTv/Recordings/RecordingNotifier.cs b/src/Jellyfin.LiveTv/Recordings/RecordingNotifier.cs index a5d186ce18..4b0f63b041 100644 --- a/src/Jellyfin.LiveTv/Recordings/RecordingNotifier.cs +++ b/src/Jellyfin.LiveTv/Recordings/RecordingNotifier.cs @@ -79,7 +79,7 @@ namespace Jellyfin.LiveTv.Recordings private async Task SendMessage(SessionMessageType name, TimerEventInfo info) { - var users = _userManager.Users + var users = _userManager.GetUsers() .Where(i => i.HasPermission(PermissionKind.EnableLiveTvAccess)) .Select(i => i.Id) .ToList(); -- cgit v1.2.3