Compare commits

...

20 Commits

Author SHA1 Message Date
Jellyfin Release Bot
e83a7e62f2 Bump version to 10.11.9 2026-05-20 21:27:36 -04:00
Bond-009
445c6c9448 Merge pull request #16845 from JPVenson/backport/adaptLoggingForUserConcurrency
Update log for user session related concurrency update fails
2026-05-15 10:02:50 +02:00
JPVenson
5f3189af41 readded concurrency exception check 2026-05-14 13:09:00 +00:00
JPVenson
b278dcf475 revert change 2026-05-14 13:08:24 +00:00
JPVenson
f7d80ae9e6 backport changes from #16835 2026-05-14 12:54:35 +00:00
Bond-009
a023b9c88d Fix rate control in av1_amf encoder (#16819)
Only the h26x_amf in legacy driver needs this.

Signed-off-by: nyanmisaka <nst799610810@gmail.com>
2026-05-11 18:25:50 +02:00
nyanmisaka
40f35f6094 Fix rate control in av1_amf encoder
Only the h26x_amf in legacy driver needs this.

Signed-off-by: nyanmisaka <nst799610810@gmail.com>
2026-05-11 01:23:45 +08:00
Niels van Velzen
2b6fc19842 Merge pull request #15368 from JPVenson/bugfix/15153_FixUserConcurrency
Fix UserManager after EFcore refactor
2026-05-05 19:36:58 +02:00
Bond-009
8c29098c8a Use strict QSV CPB size for less powerful H.264 decoder (#16743)
* Fix int32 overflow in QSV rate-control parameter computation (#16376)

Fix int32 overflow in QSV rate-control parameter computation

* Use strict QSV CPB size for less powerful H.264 decoder

Signed-off-by: nyanmisaka <nst799610810@gmail.com>

---------

Signed-off-by: nyanmisaka <nst799610810@gmail.com>
Co-authored-by: scheilch <christian.scheil@icloud.com>
2026-05-03 12:21:33 +02:00
nyanmisaka
758ee0af76 Use strict QSV CPB size for less powerful H.264 decoder
Signed-off-by: nyanmisaka <nst799610810@gmail.com>
2026-05-02 20:14:51 +08:00
scheilch
2e19c247ef Fix int32 overflow in QSV rate-control parameter computation (#16376)
Fix int32 overflow in QSV rate-control parameter computation
2026-05-02 18:45:41 +08:00
JPVenson
511f90d6d3 Update Reset method to not rely on externally available entity 2026-05-01 11:05:33 +00:00
JPVenson
1ae45519d0 Removed obsolete comment 2026-05-01 08:55:15 +00:00
JPVenson
586fa01e46 Apply review comments 2026-04-30 18:08:14 +00:00
JPVenson
2ac0edc052 Refactored all UserManager db access methods
Fixed stale cached entities in UserManager
Fixed wrong state persisting though method calls
2026-04-30 15:42:46 +00:00
JPVenson
b37ebec5f6 Merge remote-tracking branch 'jellyfinorigin/release-10.11.z' into bugfix/15153_FixUserConcurrency 2026-04-30 12:46:39 +00:00
Bond-009
938c043596 Merge pull request #16718 from jellyfin/fix-vpp-hdr10
Allow HDR10 for VPP tonemapping
2026-04-29 21:36:24 +02:00
gnattu
46a53d0605 also allow hdr10plus 2026-04-28 00:09:31 +08:00
gnattu
97f88743b8 Allow HDR10 for VPP tonemapping
It mistakenly required OpenCL tone mapping but VPP can handle this
2026-04-27 20:10:12 +08:00
JPVenson
01ae62aa49 #15153 fixed 2025-11-03 12:47:26 +00:00
17 changed files with 490 additions and 343 deletions

View File

@@ -36,7 +36,7 @@
<PropertyGroup> <PropertyGroup>
<Authors>Jellyfin Contributors</Authors> <Authors>Jellyfin Contributors</Authors>
<PackageId>Jellyfin.Naming</PackageId> <PackageId>Jellyfin.Naming</PackageId>
<VersionPrefix>10.11.8</VersionPrefix> <VersionPrefix>10.11.9</VersionPrefix>
<RepositoryUrl>https://github.com/jellyfin/jellyfin</RepositoryUrl> <RepositoryUrl>https://github.com/jellyfin/jellyfin</RepositoryUrl>
<PackageLicenseExpression>GPL-3.0-only</PackageLicenseExpression> <PackageLicenseExpression>GPL-3.0-only</PackageLicenseExpression>
</PropertyGroup> </PropertyGroup>

View File

@@ -271,9 +271,9 @@ namespace Emby.Server.Implementations.Session
user.LastActivityDate = activityDate; user.LastActivityDate = activityDate;
await _userManager.UpdateUserAsync(user).ConfigureAwait(false); await _userManager.UpdateUserAsync(user).ConfigureAwait(false);
} }
catch (DbUpdateConcurrencyException e) catch (DbUpdateConcurrencyException)
{ {
_logger.LogDebug(e, "Error updating user's last activity date."); _logger.LogDebug("Error updating user's last activity date due to concurrency conflict. This is an expected event.");
} }
} }
} }
@@ -2043,7 +2043,7 @@ namespace Emby.Server.Implementations.Session
{ {
CheckDisposed(); CheckDisposed();
var adminUserIds = _userManager.Users var adminUserIds = _userManager.GetUsers()
.Where(i => i.HasPermission(PermissionKind.IsAdministrator)) .Where(i => i.HasPermission(PermissionKind.IsAdministrator))
.Select(i => i.Id) .Select(i => i.Id)
.ToList(); .ToList();

View File

@@ -1,5 +1,5 @@
using System;
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using Jellyfin.Api.Constants; using Jellyfin.Api.Constants;
using Jellyfin.Api.Models.StartupDtos; 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. // TODO: Remove this method when startup wizard no longer requires an existing user.
await _userManager.InitializeAsync().ConfigureAwait(false); 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 return new StartupUserDto
{ {
Name = user.Username Name = user.Username
@@ -131,7 +131,12 @@ public class StartupController : BaseJellyfinApiController
[ProducesResponseType(StatusCodes.Status204NoContent)] [ProducesResponseType(StatusCodes.Status204NoContent)]
public async Task<ActionResult> UpdateStartupUser([FromBody] StartupUserDto startupUserDto) public async Task<ActionResult> UpdateStartupUser([FromBody] StartupUserDto startupUserDto)
{ {
var user = _userManager.Users.First(); var user = _userManager.GetFirstUser();
if (user is null)
{
return NotFound();
}
if (string.IsNullOrWhiteSpace(startupUserDto.Password)) if (string.IsNullOrWhiteSpace(startupUserDto.Password))
{ {
return BadRequest("Password must not be empty"); return BadRequest("Password must not be empty");
@@ -146,7 +151,7 @@ public class StartupController : BaseJellyfinApiController
if (!string.IsNullOrEmpty(startupUserDto.Password)) if (!string.IsNullOrEmpty(startupUserDto.Password))
{ {
await _userManager.ChangePassword(user, startupUserDto.Password).ConfigureAwait(false); await _userManager.ChangePassword(user.Id, startupUserDto.Password).ConfigureAwait(false);
} }
return NoContent(); return NoContent();

View File

@@ -288,7 +288,7 @@ public class UserController : BaseJellyfinApiController
if (request.ResetPassword) if (request.ResetPassword)
{ {
await _userManager.ResetPassword(user).ConfigureAwait(false); await _userManager.ResetPassword(user.Id).ConfigureAwait(false);
} }
else 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(); var currentToken = User.GetToken();
@@ -392,7 +392,7 @@ public class UserController : BaseJellyfinApiController
if (!string.Equals(user.Username, updateUser.Name, StringComparison.Ordinal)) 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); await _userManager.UpdateConfigurationAsync(requestUserId, updateUser.Configuration).ConfigureAwait(false);
@@ -448,7 +448,7 @@ public class UserController : BaseJellyfinApiController
// If removing admin access // If removing admin access
if (!newPolicy.IsAdministrator && user.HasPermission(PermissionKind.IsAdministrator)) 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."); return StatusCode(StatusCodes.Status403Forbidden, "There must be at least one user in the system with administrative access.");
} }
@@ -463,7 +463,7 @@ public class UserController : BaseJellyfinApiController
// If disabling // If disabling
if (newPolicy.IsDisabled && !user.HasPermission(PermissionKind.IsDisabled)) 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."); return StatusCode(StatusCodes.Status403Forbidden, "There must be at least one enabled user in the system.");
} }
@@ -545,7 +545,7 @@ public class UserController : BaseJellyfinApiController
// no need to authenticate password for new user // no need to authenticate password for new user
if (request.Password is not null) 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()); var result = _userManager.GetUserDto(newUser, HttpContext.GetNormalizedRemoteIP().ToString());
@@ -620,7 +620,7 @@ public class UserController : BaseJellyfinApiController
private IEnumerable<UserDto> Get(bool? isHidden, bool? isDisabled, bool filterByDevice, bool filterByNetwork) private IEnumerable<UserDto> Get(bool? isHidden, bool? isDisabled, bool filterByDevice, bool filterByNetwork)
{ {
var users = _userManager.Users; var users = _userManager.GetUsers();
if (isDisabled.HasValue) if (isDisabled.HasValue)
{ {

View File

@@ -18,7 +18,7 @@
<PropertyGroup> <PropertyGroup>
<Authors>Jellyfin Contributors</Authors> <Authors>Jellyfin Contributors</Authors>
<PackageId>Jellyfin.Data</PackageId> <PackageId>Jellyfin.Data</PackageId>
<VersionPrefix>10.11.8</VersionPrefix> <VersionPrefix>10.11.9</VersionPrefix>
<RepositoryUrl>https://github.com/jellyfin/jellyfin</RepositoryUrl> <RepositoryUrl>https://github.com/jellyfin/jellyfin</RepositoryUrl>
<PackageLicenseExpression>GPL-3.0-only</PackageLicenseExpression> <PackageLicenseExpression>GPL-3.0-only</PackageLicenseExpression>
</PropertyGroup> </PropertyGroup>

View File

@@ -74,7 +74,7 @@ namespace Jellyfin.Server.Implementations.Users
var resetUser = userManager.GetUserByName(spr.UserName) var resetUser = userManager.GetUserByName(spr.UserName)
?? throw new ResourceNotFoundException($"User with a username of {spr.UserName} not found"); ?? 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); usersReset.Add(resetUser.Username);
File.Delete(resetFile); File.Delete(resetFile);
} }

View File

@@ -1,12 +1,14 @@
#pragma warning disable CA1307 #pragma warning disable CA1307
#pragma warning disable RS0030 // Do not use banned APIs
using System; using System;
using System.Collections.Concurrent;
using System.Collections.Generic; using System.Collections.Generic;
using System.Globalization; using System.Globalization;
using System.Linq; using System.Linq;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using AsyncKeyedLock;
using Jellyfin.Data; using Jellyfin.Data;
using Jellyfin.Data.Enums; using Jellyfin.Data.Enums;
using Jellyfin.Data.Events; using Jellyfin.Data.Events;
@@ -35,7 +37,7 @@ namespace Jellyfin.Server.Implementations.Users
/// <summary> /// <summary>
/// Manages the creation and retrieval of <see cref="User"/> instances. /// Manages the creation and retrieval of <see cref="User"/> instances.
/// </summary> /// </summary>
public partial class UserManager : IUserManager public partial class UserManager : IUserManager, IDisposable
{ {
private readonly IDbContextFactory<JellyfinDbContext> _dbProvider; private readonly IDbContextFactory<JellyfinDbContext> _dbProvider;
private readonly IEventManager _eventManager; private readonly IEventManager _eventManager;
@@ -50,7 +52,7 @@ namespace Jellyfin.Server.Implementations.Users
private readonly DefaultPasswordResetProvider _defaultPasswordResetProvider; private readonly DefaultPasswordResetProvider _defaultPasswordResetProvider;
private readonly IServerConfigurationManager _serverConfigurationManager; private readonly IServerConfigurationManager _serverConfigurationManager;
private readonly IDictionary<Guid, User> _users; private readonly AsyncKeyedLocker<Guid> _userLock = new();
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="UserManager"/> class. /// Initializes a new instance of the <see cref="UserManager"/> class.
@@ -89,29 +91,28 @@ namespace Jellyfin.Server.Implementations.Users
_invalidAuthProvider = _authenticationProviders.OfType<InvalidAuthProvider>().First(); _invalidAuthProvider = _authenticationProviders.OfType<InvalidAuthProvider>().First();
_defaultAuthenticationProvider = _authenticationProviders.OfType<DefaultAuthenticationProvider>().First(); _defaultAuthenticationProvider = _authenticationProviders.OfType<DefaultAuthenticationProvider>().First();
_defaultPasswordResetProvider = _passwordResetProviders.OfType<DefaultPasswordResetProvider>().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/> /// <inheritdoc/>
public event EventHandler<GenericEventArgs<User>>? OnUserUpdated; public event EventHandler<GenericEventArgs<User>>? OnUserUpdated;
/// <inheritdoc/> /// <inheritdoc/>
public IEnumerable<User> Users => _users.Values; public IEnumerable<User> GetUsers()
{
using var dbContext = _dbProvider.CreateDbContext();
return UserQuery(dbContext)
.ToArray();
}
/// <inheritdoc/> /// <inheritdoc/>
public IEnumerable<Guid> UsersIds => _users.Keys; public IEnumerable<Guid> 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 @ // 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 // In theory this will cut out most if not all 'control' characters which should help minimize any weirdness
@@ -127,8 +128,28 @@ namespace Jellyfin.Server.Implementations.Users
throw new ArgumentException("Guid can't be empty", nameof(id)); throw new ArgumentException("Guid can't be empty", nameof(id));
} }
_users.TryGetValue(id, out var user); using var dbContext = _dbProvider.CreateDbContext();
return user; return UserQuery(dbContext)
.FirstOrDefault(user => user.Id == id);
}
private static IQueryable<User> UserQuery(JellyfinDbContext dbContext)
{
return dbContext.Users
.AsSingleQuery()
.Include(user => user.Permissions)
.Include(user => user.Preferences)
.Include(user => user.AccessSchedules)
.Include(user => user.ProfileImage)
.AsNoTracking();
}
/// <inheritdoc/>
public User? GetFirstUser()
{
using var dbContext = _dbProvider.CreateDbContext();
return UserQuery(dbContext)
.FirstOrDefault();
} }
/// <inheritdoc/> /// <inheritdoc/>
@@ -139,42 +160,58 @@ namespace Jellyfin.Server.Implementations.Users
throw new ArgumentException("Invalid username", nameof(name)); 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
} }
/// <inheritdoc/> /// <inheritdoc/>
public async Task RenameUser(User user, string newName) public async Task RenameUser(Guid userId, string oldName, string newName)
{ {
ArgumentNullException.ThrowIfNull(user);
ThrowIfInvalidUsername(newName); 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."); throw new ArgumentException("The new and old names must be different.");
} }
var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false); User user = null!; // user is never actually null where its used afterwards so we can just ignore.
await using (dbContext.ConfigureAwait(false)) 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 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 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 #pragma warning disable CA1304 // The behavior of 'string.ToUpper()' could vary based on the current user's locale settings
if (await dbContext.Users if (await dbContext.Users
.AnyAsync(u => u.Username.ToUpper() == newName.ToUpper() && !u.Id.Equals(user.Id)) .AnyAsync(u => u.Username.ToUpper() == newName.ToUpper() && u.Id != userId)
.ConfigureAwait(false)) .ConfigureAwait(false))
{ {
throw new ArgumentException(string.Format( throw new ArgumentException(string.Format(
CultureInfo.InvariantCulture, CultureInfo.InvariantCulture,
"A user with the name '{0}' already exists.", "A user with the name '{0}' already exists.",
newName)); newName));
} }
#pragma warning restore CA1304 // The behavior of 'string.ToUpper()' could vary based on the current user's locale settings #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 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 #pragma warning restore CA1862 // Use the 'StringComparison' method overloads to perform case-insensitive string comparisons
user.Username = newName; user = await UserQuery(dbContext)
await UpdateUserInternalAsync(dbContext, user).ConfigureAwait(false); .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); var eventArgs = new UserUpdatedEventArgs(user);
@@ -185,10 +222,9 @@ namespace Jellyfin.Server.Implementations.Users
/// <inheritdoc/> /// <inheritdoc/>
public async Task UpdateUserAsync(User user) public async Task UpdateUserAsync(User user)
{ {
var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false); using (await _userLock.LockAsync(user.Id).ConfigureAwait(false))
await using (dbContext.ConfigureAwait(false))
{ {
await UpdateUserInternalAsync(dbContext, user).ConfigureAwait(false); await UpdateUserInternalAsync(user).ConfigureAwait(false);
} }
} }
@@ -218,23 +254,30 @@ namespace Jellyfin.Server.Implementations.Users
{ {
ThrowIfInvalidUsername(name); 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; User newUser;
var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false); var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false);
await using (dbContext.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); newUser = await CreateUserInternalAsync(name, dbContext).ConfigureAwait(false);
dbContext.Users.Add(newUser); dbContext.Users.Add(newUser);
await dbContext.SaveChangesAsync().ConfigureAwait(false); await dbContext.SaveChangesAsync().ConfigureAwait(false);
_users.Add(newUser.Id, newUser);
} }
await _eventManager.PublishAsync(new UserCreatedEventArgs(newUser)).ConfigureAwait(false); await _eventManager.PublishAsync(new UserCreatedEventArgs(newUser)).ConfigureAwait(false);
@@ -245,62 +288,84 @@ namespace Jellyfin.Server.Implementations.Users
/// <inheritdoc/> /// <inheritdoc/>
public async Task DeleteUserAsync(Guid userId) 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 (_users.Count == 1) if (user is null)
{ {
throw new InvalidOperationException(string.Format( throw new ResourceNotFoundException(nameof(userId));
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) var userCount = await dbContext.Users.CountAsync().ConfigureAwait(false);
&& Users.Count(i => i.HasPermission(PermissionKind.IsAdministrator)) == 1) if (userCount == 1)
{ {
throw new ArgumentException( throw new InvalidOperationException(string.Format(
string.Format( CultureInfo.InvariantCulture,
CultureInfo.InvariantCulture, "The user '{0}' cannot be deleted because there must be at least one user in the system.",
"The user '{0}' cannot be deleted because there must be at least one admin user in the system.", user.Username));
user.Username), }
nameof(userId));
}
var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false); if (user.HasPermission(PermissionKind.IsAdministrator)
await using (dbContext.ConfigureAwait(false)) && await dbContext.Users
{ .CountAsync(i => i.Permissions.Any(p => p.Kind == PermissionKind.IsAdministrator && p.Value))
dbContext.Users.Attach(user); .ConfigureAwait(false) == 1)
dbContext.Users.Remove(user); {
await dbContext.SaveChangesAsync().ConfigureAwait(false); 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));
}
_users.Remove(userId); dbContext.Users.Remove(user);
await dbContext.SaveChangesAsync().ConfigureAwait(false);
}
}
await _eventManager.PublishAsync(new UserDeletedEventArgs(user)).ConfigureAwait(false); await _eventManager.PublishAsync(new UserDeletedEventArgs(user)).ConfigureAwait(false);
} }
/// <inheritdoc/> /// <inheritdoc/>
public Task ResetPassword(User user) public Task ResetPassword(Guid userId)
{ {
return ChangePassword(user, string.Empty); return ChangePassword(userId, string.Empty);
} }
/// <inheritdoc/> /// <inheritdoc/>
public async Task ChangePassword(User user, string newPassword) public async Task ChangePassword(Guid userId, string newPassword)
{ {
ArgumentNullException.ThrowIfNull(user); User dbUser = null!;
if (user.HasPermission(PermissionKind.IsAdministrator) && string.IsNullOrWhiteSpace(newPassword)) 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(dbUser).ChangePassword(dbUser, newPassword).ConfigureAwait(false);
await dbContext.SaveChangesAsync().ConfigureAwait(false);
}
} }
await GetAuthenticationProvider(user).ChangePassword(user, newPassword).ConfigureAwait(false); await _eventManager.PublishAsync(new UserPasswordChangedEventArgs(dbUser)).ConfigureAwait(false);
await UpdateUserAsync(user).ConfigureAwait(false);
await _eventManager.PublishAsync(new UserPasswordChangedEventArgs(user)).ConfigureAwait(false);
} }
/// <inheritdoc/> /// <inheritdoc/>
@@ -403,102 +468,114 @@ namespace Jellyfin.Server.Implementations.Users
throw new ArgumentNullException(nameof(username)); throw new ArgumentNullException(nameof(username));
} }
var user = Users.FirstOrDefault(i => string.Equals(username, i.Username, StringComparison.OrdinalIgnoreCase)); bool success;
var authResult = await AuthenticateLocalUser(username, password, user) var user = GetUserByName(username);
.ConfigureAwait(false); using (await _userLock.LockAsync(user?.Id ?? Guid.Empty).ConfigureAwait(false))
var authenticationProvider = authResult.AuthenticationProvider;
var success = authResult.Success;
if (user is null)
{ {
string updatedUsername = authResult.Username; // 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
if (success // write (e.g. a concurrent login) incremented RowVersion after our initial load.
&& authenticationProvider is not null if (user is not null)
&& authenticationProvider is not DefaultAuthenticationProvider)
{ {
// Trust the username returned by the authentication provider user = GetUserById(user.Id) ?? user;
username = updatedUsername; }
// Search the database for the user again var authResult = await AuthenticateLocalUser(username, password, user)
// the authentication provider might have created it .ConfigureAwait(false);
user = Users.FirstOrDefault(i => string.Equals(username, i.Username, StringComparison.OrdinalIgnoreCase)); 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) 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; var providerId = authenticationProvider.GetType().FullName;
await UpdateUserAsync(user).ConfigureAwait(false);
}
}
if (user is null) if (providerId is not null && !string.Equals(providerId, user.AuthenticationProviderId, StringComparison.OrdinalIgnoreCase))
{ {
_logger.LogInformation( user.AuthenticationProviderId = providerId;
"Authentication request for {UserName} has been denied (IP: {IP}).", await UpdateUserInternalAsync(user).ConfigureAwait(false);
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.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.");
}
// Update LastActivityDate and LastLoginDate, then save
if (success)
{
if (isUserSession)
{
user.LastActivityDate = user.LastLoginDate = DateTime.UtcNow;
} }
user.InvalidLoginAttemptCount = 0; if (user is null)
await UpdateUserAsync(user).ConfigureAwait(false); {
_logger.LogInformation("Authentication request for {UserName} has succeeded.", user.Username); _logger.LogInformation(
} "Authentication request for {UserName} has been denied (IP: {IP}).",
else username,
{ remoteEndPoint);
await IncrementInvalidLoginAttemptCount(user).ConfigureAwait(false); throw new AuthenticationException("Invalid username or password entered.");
_logger.LogInformation( }
"Authentication request for {UserName} has been denied (IP: {IP}).",
user.Username, if (user.HasPermission(PermissionKind.IsDisabled))
remoteEndPoint); {
_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.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;
}
user.InvalidLoginAttemptCount = 0;
await UpdateUserInternalAsync(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; return success ? user : null;
@@ -542,22 +619,22 @@ namespace Jellyfin.Server.Implementations.Users
public async Task InitializeAsync() public async Task InitializeAsync()
{ {
// TODO: Refactor the startup wizard so that it doesn't require a user to already exist. // TODO: Refactor the startup wizard so that it doesn't require a user to already exist.
if (_users.Any())
{
return;
}
var defaultName = Environment.UserName;
if (string.IsNullOrWhiteSpace(defaultName) || !ValidUsernameRegex().IsMatch(defaultName))
{
defaultName = "MyJellyfinUser";
}
_logger.LogWarning("No users, creating one with username {UserName}", defaultName);
var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false); var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false);
await using (dbContext.ConfigureAwait(false)) await using (dbContext.ConfigureAwait(false))
{ {
if (await dbContext.Users.AnyAsync().ConfigureAwait(false))
{
return;
}
var defaultName = Environment.UserName;
if (string.IsNullOrWhiteSpace(defaultName) || !ValidUsernameRegex().IsMatch(defaultName))
{
defaultName = "MyJellyfinUser";
}
_logger.LogWarning("No users, creating one with username {UserName}", defaultName);
var newUser = await CreateUserInternalAsync(defaultName, dbContext).ConfigureAwait(false); var newUser = await CreateUserInternalAsync(defaultName, dbContext).ConfigureAwait(false);
newUser.SetPermission(PermissionKind.IsAdministrator, true); newUser.SetPermission(PermissionKind.IsAdministrator, true);
newUser.SetPermission(PermissionKind.EnableContentDeletion, true); newUser.SetPermission(PermissionKind.EnableContentDeletion, true);
@@ -565,7 +642,6 @@ namespace Jellyfin.Server.Implementations.Users
dbContext.Users.Add(newUser); dbContext.Users.Add(newUser);
await dbContext.SaveChangesAsync().ConfigureAwait(false); await dbContext.SaveChangesAsync().ConfigureAwait(false);
_users.Add(newUser.Id, newUser);
} }
} }
@@ -602,122 +678,120 @@ namespace Jellyfin.Server.Implementations.Users
/// <inheritdoc/> /// <inheritdoc/>
public async Task UpdateConfigurationAsync(Guid userId, UserConfiguration config) public async Task UpdateConfigurationAsync(Guid userId, UserConfiguration config)
{ {
var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false); using (await _userLock.LockAsync(userId).ConfigureAwait(false))
await using (dbContext.ConfigureAwait(false))
{ {
var user = dbContext.Users var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false);
.Include(u => u.Permissions) await using (dbContext.ConfigureAwait(false))
.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;
// 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; 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);
dbContext.Update(user);
await dbContext.SaveChangesAsync().ConfigureAwait(false);
} }
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/> /// <inheritdoc/>
public async Task UpdatePolicyAsync(Guid userId, UserPolicy policy) public async Task UpdatePolicyAsync(Guid userId, UserPolicy policy)
{ {
var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false); using (await _userLock.LockAsync(userId).ConfigureAwait(false))
await using (dbContext.ConfigureAwait(false))
{ {
var user = dbContext.Users var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false);
.Include(u => u.Permissions) await using (dbContext.ConfigureAwait(false))
.Include(u => u.Preferences)
.Include(u => u.AccessSchedules)
.Include(u => u.ProfileImage)
.FirstOrDefault(u => u.Id.Equals(userId))
?? throw new ArgumentException("No user exists with given Id!");
// The default number of login attempts is 3, but for some god forsaken reason it's sent to the server as "0"
int? maxLoginAttempts = policy.LoginAttemptsBeforeLockout switch
{ {
-1 => null, var user = UserQuery(dbContext)
0 => 3, .AsTracking()
_ => policy.LoginAttemptsBeforeLockout .FirstOrDefault(u => u.Id.Equals(userId))
}; ?? throw new ArgumentException("No user exists with given Id!");
user.MaxParentalRatingScore = policy.MaxParentalRating; // The default number of login attempts is 3, but for some god forsaken reason it's sent to the server as "0"
user.MaxParentalRatingSubScore = policy.MaxParentalSubRating; int? maxLoginAttempts = policy.LoginAttemptsBeforeLockout switch
user.EnableUserPreferenceAccess = policy.EnableUserPreferenceAccess; {
user.RemoteClientBitrateLimit = policy.RemoteClientBitrateLimit; -1 => null,
user.AuthenticationProviderId = policy.AuthenticationProviderId; 0 => 3,
user.PasswordResetProviderId = policy.PasswordResetProviderId; _ => policy.LoginAttemptsBeforeLockout
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(); user.MaxParentalRatingScore = policy.MaxParentalRating;
foreach (var policyAccessSchedule in policy.AccessSchedules) user.MaxParentalRatingSubScore = policy.MaxParentalSubRating;
{ user.EnableUserPreferenceAccess = policy.EnableUserPreferenceAccess;
user.AccessSchedules.Add(policyAccessSchedule); 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<UnratedItem>());
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);
} }
// TODO: fix this at some point
user.SetPreference(PreferenceKind.BlockUnratedItems, policy.BlockUnratedItems ?? Array.Empty<UnratedItem>());
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);
} }
} }
@@ -729,15 +803,17 @@ namespace Jellyfin.Server.Implementations.Users
return; return;
} }
var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false); using (await _userLock.LockAsync(user.Id).ConfigureAwait(false))
await using (dbContext.ConfigureAwait(false))
{ {
dbContext.Remove(user.ProfileImage); var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false);
await dbContext.SaveChangesAsync().ConfigureAwait(false); await using (dbContext.ConfigureAwait(false))
} {
dbContext.Remove(user.ProfileImage);
await dbContext.SaveChangesAsync().ConfigureAwait(false);
}
user.ProfileImage = null; user.ProfileImage = null;
_users[user.Id] = user; }
} }
internal static void ThrowIfInvalidUsername(string name) internal static void ThrowIfInvalidUsername(string name)
@@ -883,15 +959,42 @@ namespace Jellyfin.Server.Implementations.Users
user.InvalidLoginAttemptCount); 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) private async Task UpdateUserInternalAsync(JellyfinDbContext dbContext, User user)
{ {
dbContext.Users.Attach(user); dbContext.Users.Attach(user);
dbContext.Entry(user).State = EntityState.Modified; dbContext.Entry(user).State = EntityState.Modified;
_users[user.Id] = user;
await dbContext.SaveChangesAsync().ConfigureAwait(false); await dbContext.SaveChangesAsync().ConfigureAwait(false);
} }
/// <inheritdoc/>
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
/// <summary>
/// Disposes all members of this class.
/// </summary>
/// <param name="disposing">Defines if the class has been cleaned up by a dispose or finalizer.</param>
protected virtual void Dispose(bool disposing)
{
if (disposing)
{
_userLock.Dispose();
}
}
} }
} }

View File

@@ -8,7 +8,7 @@
<PropertyGroup> <PropertyGroup>
<Authors>Jellyfin Contributors</Authors> <Authors>Jellyfin Contributors</Authors>
<PackageId>Jellyfin.Common</PackageId> <PackageId>Jellyfin.Common</PackageId>
<VersionPrefix>10.11.8</VersionPrefix> <VersionPrefix>10.11.9</VersionPrefix>
<RepositoryUrl>https://github.com/jellyfin/jellyfin</RepositoryUrl> <RepositoryUrl>https://github.com/jellyfin/jellyfin</RepositoryUrl>
<PackageLicenseExpression>GPL-3.0-only</PackageLicenseExpression> <PackageLicenseExpression>GPL-3.0-only</PackageLicenseExpression>
</PropertyGroup> </PropertyGroup>

View File

@@ -24,14 +24,14 @@ namespace MediaBrowser.Controller.Library
/// <summary> /// <summary>
/// Gets the users. /// Gets the users.
/// </summary> /// </summary>
/// <value>The users.</value> /// <returns>The users.</returns>
IEnumerable<User> Users { get; } IEnumerable<User> GetUsers();
/// <summary> /// <summary>
/// Gets the user ids. /// Gets the user ids.
/// </summary> /// </summary>
/// <value>The users ids.</value> /// <returns>The users ids.</returns>
IEnumerable<Guid> UsersIds { get; } IEnumerable<Guid> GetUsersIds();
/// <summary> /// <summary>
/// Initializes the user manager and ensures that a user exists. /// Initializes the user manager and ensures that a user exists.
@@ -47,6 +47,12 @@ namespace MediaBrowser.Controller.Library
/// <exception cref="ArgumentException"><c>id</c> is an empty Guid.</exception> /// <exception cref="ArgumentException"><c>id</c> is an empty Guid.</exception>
User? GetUserById(Guid id); User? GetUserById(Guid id);
/// <summary>
/// Gets the first available user.
/// </summary>
/// <returns>The first user, or <c>null</c> if no users exist.</returns>
User? GetFirstUser();
/// <summary> /// <summary>
/// Gets the name of the user by. /// Gets the name of the user by.
/// </summary> /// </summary>
@@ -57,12 +63,13 @@ namespace MediaBrowser.Controller.Library
/// <summary> /// <summary>
/// Renames the user. /// Renames the user.
/// </summary> /// </summary>
/// <param name="user">The user.</param> /// <param name="userId">The UserId to change.</param>
/// <param name="oldName">The old Username.</param>
/// <param name="newName">The new name.</param> /// <param name="newName">The new name.</param>
/// <returns>Task.</returns> /// <returns>Task.</returns>
/// <exception cref="ArgumentNullException">If user is <c>null</c>.</exception> /// <exception cref="ArgumentNullException">If user is <c>null</c>.</exception>
/// <exception cref="ArgumentException">If the provided user doesn't exist.</exception> /// <exception cref="ArgumentException">If the provided user doesn't exist.</exception>
Task RenameUser(User user, string newName); Task RenameUser(Guid userId, string oldName, string newName);
/// <summary> /// <summary>
/// Updates the user. /// Updates the user.
@@ -92,17 +99,17 @@ namespace MediaBrowser.Controller.Library
/// <summary> /// <summary>
/// Resets the password. /// Resets the password.
/// </summary> /// </summary>
/// <param name="user">The user.</param> /// <param name="userId">The users Id.</param>
/// <returns>Task.</returns> /// <returns>Task.</returns>
Task ResetPassword(User user); Task ResetPassword(Guid userId);
/// <summary> /// <summary>
/// Changes the password. /// Changes the password.
/// </summary> /// </summary>
/// <param name="user">The user.</param> /// <param name="userId">The users id.</param>
/// <param name="newPassword">New password to use.</param> /// <param name="newPassword">New password to use.</param>
/// <returns>Awaitable task.</returns> /// <returns>Awaitable task.</returns>
Task ChangePassword(User user, string newPassword); Task ChangePassword(Guid userId, string newPassword);
/// <summary> /// <summary>
/// Gets the user dto. /// Gets the user dto.

View File

@@ -8,7 +8,7 @@
<PropertyGroup> <PropertyGroup>
<Authors>Jellyfin Contributors</Authors> <Authors>Jellyfin Contributors</Authors>
<PackageId>Jellyfin.Controller</PackageId> <PackageId>Jellyfin.Controller</PackageId>
<VersionPrefix>10.11.8</VersionPrefix> <VersionPrefix>10.11.9</VersionPrefix>
<RepositoryUrl>https://github.com/jellyfin/jellyfin</RepositoryUrl> <RepositoryUrl>https://github.com/jellyfin/jellyfin</RepositoryUrl>
<PackageLicenseExpression>GPL-3.0-only</PackageLicenseExpression> <PackageLicenseExpression>GPL-3.0-only</PackageLicenseExpression>
</PropertyGroup> </PropertyGroup>

View File

@@ -413,7 +413,9 @@ namespace MediaBrowser.Controller.MediaEncoding
} }
return state.VideoStream.VideoRange == VideoRange.HDR return state.VideoStream.VideoRange == VideoRange.HDR
&& IsDoviWithHdr10Bl(state.VideoStream); && (state.VideoStream.VideoRangeType == VideoRangeType.HDR10
|| IsHdr10Plus(state.VideoStream)
|| IsDoviWithHdr10Bl(state.VideoStream));
} }
private bool IsVideoToolboxTonemapAvailable(EncodingJobInfo state, EncodingOptions options) private bool IsVideoToolboxTonemapAvailable(EncodingJobInfo state, EncodingOptions options)
@@ -428,8 +430,10 @@ namespace MediaBrowser.Controller.MediaEncoding
// Certain DV profile 5 video works in Safari with direct playing, but the VideoToolBox does not produce correct mapping results with transcoding. // Certain DV profile 5 video works in Safari with direct playing, but the VideoToolBox does not produce correct mapping results with transcoding.
// All other HDR formats working. // All other HDR formats working.
return state.VideoStream.VideoRange == VideoRange.HDR return state.VideoStream.VideoRange == VideoRange.HDR
&& (IsDoviWithHdr10Bl(state.VideoStream) && (state.VideoStream.VideoRangeType == VideoRangeType.HDR10
|| state.VideoStream.VideoRangeType is VideoRangeType.HLG); || IsHdr10Plus(state.VideoStream)
|| IsDoviWithHdr10Bl(state.VideoStream)
|| state.VideoStream.VideoRangeType == VideoRangeType.HLG);
} }
private bool IsVideoStreamHevcRext(EncodingJobInfo state) private bool IsVideoStreamHevcRext(EncodingJobInfo state)
@@ -1574,14 +1578,15 @@ namespace MediaBrowser.Controller.MediaEncoding
int bitrate = state.OutputVideoBitrate.Value; int bitrate = state.OutputVideoBitrate.Value;
// Bit rate under 1000k is not allowed in h264_qsv // Bit rate under 1000k is not allowed in h264_qsv.
if (string.Equals(videoCodec, "h264_qsv", StringComparison.OrdinalIgnoreCase)) if (string.Equals(videoCodec, "h264_qsv", StringComparison.OrdinalIgnoreCase))
{ {
bitrate = Math.Max(bitrate, 1000); bitrate = Math.Max(bitrate, 1000);
} }
// Currently use the same buffer size for all encoders // Currently use the same buffer size for all non-QSV encoders.
int bufsize = bitrate * 2; // Use long arithmetic to prevent int32 overflow for very high bitrate values.
int bufsize = (int)Math.Min((long)bitrate * 2, int.MaxValue);
if (string.Equals(videoCodec, "libsvtav1", StringComparison.OrdinalIgnoreCase)) if (string.Equals(videoCodec, "libsvtav1", StringComparison.OrdinalIgnoreCase))
{ {
@@ -1609,16 +1614,33 @@ namespace MediaBrowser.Controller.MediaEncoding
mbbrcOpt = " -mbbrc 1"; mbbrcOpt = " -mbbrc 1";
} }
// Some less powerful H.264 HW decoders require strict CPB size
// So bufsize optimizations should not be applied to them
int factor = 2;
var codec = state.ActualOutputVideoCodec;
var level = state.GetRequestedLevel(codec);
if (string.Equals(codec, "h264", StringComparison.OrdinalIgnoreCase)
&& double.TryParse(level, CultureInfo.InvariantCulture, out double requestedLevel)
&& requestedLevel < 51)
{
factor = 1;
}
// Set (maxrate == bitrate + 1) to trigger VBR for better bitrate allocation // Set (maxrate == bitrate + 1) to trigger VBR for better bitrate allocation
// Set (rc_init_occupancy == 2 * bitrate) and (bufsize == 4 * bitrate) to deal with drastic scene changes // Set (rc_init_occupancy == 2 * bitrate) and (bufsize == 4 * bitrate) to deal with drastic scene changes
return FormattableString.Invariant($"{mbbrcOpt} -b:v {bitrate} -maxrate {bitrate + 1} -rc_init_occupancy {bitrate * 2} -bufsize {bitrate * 4}"); // Use long arithmetic and clamp to int.MaxValue to prevent int32 overflow
// (e.g. bitrate * 4 wraps to a negative value for bitrates above ~537 million)
int qsvMaxrate = (int)Math.Min((long)bitrate + 1, int.MaxValue);
int qsvInitOcc = (int)Math.Min((long)bitrate * 1 * factor, int.MaxValue);
int qsvBufsize = (int)Math.Min((long)bitrate * 2 * factor, int.MaxValue);
return FormattableString.Invariant($"{mbbrcOpt} -b:v {bitrate} -maxrate {qsvMaxrate} -rc_init_occupancy {qsvInitOcc} -bufsize {qsvBufsize}");
} }
if (string.Equals(videoCodec, "h264_amf", StringComparison.OrdinalIgnoreCase) if (string.Equals(videoCodec, "h264_amf", StringComparison.OrdinalIgnoreCase)
|| string.Equals(videoCodec, "hevc_amf", StringComparison.OrdinalIgnoreCase) || string.Equals(videoCodec, "hevc_amf", StringComparison.OrdinalIgnoreCase))
|| string.Equals(videoCodec, "av1_amf", StringComparison.OrdinalIgnoreCase))
{ {
// Override the too high default qmin 18 in transcoding preset // Override the too high default qmin 18 in transcoding preset in legacy h26x_amf
return FormattableString.Invariant($" -rc cbr -qmin 0 -qmax 32 -b:v {bitrate} -maxrate {bitrate} -bufsize {bufsize}"); return FormattableString.Invariant($" -rc cbr -qmin 0 -qmax 32 -b:v {bitrate} -maxrate {bitrate} -bufsize {bufsize}");
} }

View File

@@ -8,7 +8,7 @@
<PropertyGroup> <PropertyGroup>
<Authors>Jellyfin Contributors</Authors> <Authors>Jellyfin Contributors</Authors>
<PackageId>Jellyfin.Model</PackageId> <PackageId>Jellyfin.Model</PackageId>
<VersionPrefix>10.11.8</VersionPrefix> <VersionPrefix>10.11.9</VersionPrefix>
<RepositoryUrl>https://github.com/jellyfin/jellyfin</RepositoryUrl> <RepositoryUrl>https://github.com/jellyfin/jellyfin</RepositoryUrl>
<PackageLicenseExpression>GPL-3.0-only</PackageLicenseExpression> <PackageLicenseExpression>GPL-3.0-only</PackageLicenseExpression>
</PropertyGroup> </PropertyGroup>

View File

@@ -1,4 +1,4 @@
using System.Reflection; using System.Reflection;
[assembly: AssemblyVersion("10.11.8")] [assembly: AssemblyVersion("10.11.9")]
[assembly: AssemblyFileVersion("10.11.8")] [assembly: AssemblyFileVersion("10.11.9")]

View File

@@ -268,6 +268,11 @@ public class JellyfinDbContext(DbContextOptions<JellyfinDbContext> options, ILog
}).ConfigureAwait(false); }).ConfigureAwait(false);
return result; return result;
} }
catch (DbUpdateConcurrencyException)
{
// a concurrency exception is supposed to be always handled by the invoker of the method, logging it here is only causing log bloat.
throw;
}
catch (Exception e) catch (Exception e)
{ {
logger.LogError(e, "Error trying to save changes."); logger.LogError(e, "Error trying to save changes.");
@@ -289,6 +294,11 @@ public class JellyfinDbContext(DbContextOptions<JellyfinDbContext> options, ILog
}); });
return result; return result;
} }
catch (DbUpdateConcurrencyException)
{
// a concurrency exception is supposed to be always handled by the invoker of the method, logging it here is only causing log bloat.
throw;
}
catch (Exception e) catch (Exception e)
{ {
logger.LogError(e, "Error trying to save changes."); logger.LogError(e, "Error trying to save changes.");

View File

@@ -15,7 +15,7 @@
<PropertyGroup> <PropertyGroup>
<Authors>Jellyfin Contributors</Authors> <Authors>Jellyfin Contributors</Authors>
<PackageId>Jellyfin.Extensions</PackageId> <PackageId>Jellyfin.Extensions</PackageId>
<VersionPrefix>10.11.8</VersionPrefix> <VersionPrefix>10.11.9</VersionPrefix>
<RepositoryUrl>https://github.com/jellyfin/jellyfin</RepositoryUrl> <RepositoryUrl>https://github.com/jellyfin/jellyfin</RepositoryUrl>
<PackageLicenseExpression>GPL-3.0-only</PackageLicenseExpression> <PackageLicenseExpression>GPL-3.0-only</PackageLicenseExpression>
</PropertyGroup> </PropertyGroup>

View File

@@ -1204,7 +1204,7 @@ namespace Jellyfin.LiveTv
{ {
Services = services, Services = services,
IsEnabled = services.Length > 0, IsEnabled = services.Length > 0,
EnabledUsers = _userManager.Users EnabledUsers = _userManager.GetUsers()
.Where(IsLiveTvEnabled) .Where(IsLiveTvEnabled)
.Select(i => i.Id.ToString("N", CultureInfo.InvariantCulture)) .Select(i => i.Id.ToString("N", CultureInfo.InvariantCulture))
.ToArray() .ToArray()
@@ -1220,7 +1220,7 @@ namespace Jellyfin.LiveTv
public IEnumerable<User> GetEnabledUsers() public IEnumerable<User> GetEnabledUsers()
{ {
return _userManager.Users return _userManager.GetUsers()
.Where(IsLiveTvEnabled); .Where(IsLiveTvEnabled);
} }

View File

@@ -79,7 +79,7 @@ namespace Jellyfin.LiveTv.Recordings
private async Task SendMessage(SessionMessageType name, TimerEventInfo info) private async Task SendMessage(SessionMessageType name, TimerEventInfo info)
{ {
var users = _userManager.Users var users = _userManager.GetUsers()
.Where(i => i.HasPermission(PermissionKind.EnableLiveTvAccess)) .Where(i => i.HasPermission(PermissionKind.EnableLiveTvAccess))
.Select(i => i.Id) .Select(i => i.Id)
.ToList(); .ToList();