Merge pull request #17395 from paoloantinori/fix/userdata-null-user-nre-master

Avoid NRE when sorting by user-dependent keys without a user
This commit is contained in:
Cody Robibero
2026-07-25 08:38:43 -04:00
committed by GitHub
5 changed files with 103 additions and 22 deletions
@@ -2315,9 +2315,13 @@ namespace Emby.Server.Implementations.Library
{
var comparer = Comparers.FirstOrDefault(c => name == c.Type);
// If it requires a user, create a new one, and assign the user
if (comparer is IUserBaseItemComparer)
{
if (user is null)
{
throw new ArgumentException($"Sort key '{name}' requires a user, but none was provided.");
}
var userComparer = (IUserBaseItemComparer)Activator.CreateInstance(comparer.GetType())!; // only null for Nullable<T> instances
userComparer.User = user;
@@ -352,6 +352,7 @@ namespace Emby.Server.Implementations.Library
/// <inheritdoc />
public UserItemData? GetUserData(User user, BaseItem item)
{
ArgumentNullException.ThrowIfNull(user);
var row = ResolveUserDataRow(item, item.UserData?.Where(e => e.UserId.Equals(user.Id)));
return row is not null ? Map(row) : new UserItemData()
{
@@ -3,34 +3,14 @@
using System;
using Jellyfin.Data.Enums;
using Jellyfin.Database.Implementations.Entities;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Sorting;
using MediaBrowser.Model.Querying;
namespace Emby.Server.Implementations.Sorting
{
public class DateLastMediaAddedComparer : IUserBaseItemComparer
public class DateLastMediaAddedComparer : IBaseItemComparer
{
/// <summary>
/// Gets or sets the user.
/// </summary>
/// <value>The user.</value>
public User User { get; set; }
/// <summary>
/// Gets or sets the user manager.
/// </summary>
/// <value>The user manager.</value>
public IUserManager UserManager { get; set; }
/// <summary>
/// Gets or sets the user data manager.
/// </summary>
/// <value>The user data manager.</value>
public IUserDataManager UserDataManager { get; set; }
/// <summary>
/// Gets the name.
/// </summary>
@@ -0,0 +1,89 @@
using System;
using System.Collections.Generic;
using System.Linq;
using AutoFixture;
using AutoFixture.AutoMoq;
using Emby.Naming.Common;
using Emby.Server.Implementations.Library;
using Emby.Server.Implementations.Sorting;
using Jellyfin.Data.Enums;
using Jellyfin.Database.Implementations.Enums;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Entities.Audio;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Persistence;
using MediaBrowser.Controller.Resolvers;
using MediaBrowser.Controller.Sorting;
using MediaBrowser.Model.IO;
using Moq;
using Xunit;
using BaseItem = MediaBrowser.Controller.Entities.BaseItem;
namespace Jellyfin.Server.Implementations.Tests.Library;
public class LibraryManagerSortTests
{
[Fact]
public void Sort_UserDependentKey_NullUser_ThrowsArgumentException()
{
var libraryManager = CreateLibraryManager(
new IBaseItemComparer[] { new PlayCountComparer(), new SortNameComparer() });
BaseItem[] items =
{
new Audio { Name = "Zulu", SortName = "Zulu", Id = Guid.NewGuid() },
new Audio { Name = "Alpha", SortName = "Alpha", Id = Guid.NewGuid() },
};
Assert.Throws<ArgumentException>(() => libraryManager.Sort(
items,
user: null,
new[] { (ItemSortBy.PlayCount, SortOrder.Descending) }).ToArray());
}
[Fact]
public void Sort_DateLastContentAdded_NullUser_OrdersByDateNotSortName()
{
var libraryManager = CreateLibraryManager(
new IBaseItemComparer[] { new DateLastMediaAddedComparer(), new SortNameComparer() });
BaseItem[] items =
{
MakeFolder("Alpha", new DateTime(2026, 1, 1)),
MakeFolder("Mike", new DateTime(2025, 1, 1)),
MakeFolder("Zulu", new DateTime(2024, 1, 1))
};
var sorted = libraryManager.Sort(
items,
user: null,
new[] { (ItemSortBy.DateLastContentAdded, SortOrder.Descending) }).ToArray();
Assert.Equal(new[] { "Alpha", "Mike", "Zulu" }, sorted.Select(i => i.Name));
}
private static Folder MakeFolder(string name, DateTime dateLastMediaAdded)
=> new() { Name = name, Id = Guid.NewGuid(), DateLastMediaAdded = dateLastMediaAdded };
private static Emby.Server.Implementations.Library.LibraryManager CreateLibraryManager(IReadOnlyCollection<IBaseItemComparer> comparers)
{
var fixture = new Fixture().Customize(new AutoMoqCustomization());
fixture.Register(() => new NamingOptions());
var configMock = fixture.Freeze<Mock<IServerConfigurationManager>>();
configMock.Setup(c => c.ApplicationPaths.ProgramDataPath).Returns("/data");
BaseItem.ConfigurationManager ??= configMock.Object;
var itemRepository = fixture.Freeze<Mock<IItemRepository>>();
itemRepository.Setup(i => i.RetrieveItem(It.IsAny<Guid>())).Returns<BaseItem>(null);
var fileSystemMock = fixture.Freeze<Mock<IFileSystem>>();
fileSystemMock.Setup(f => f.GetFileInfo(It.IsAny<string>())).Returns<string>(path => new FileSystemMetadata { FullName = path });
return fixture.Build<Emby.Server.Implementations.Library.LibraryManager>().Do(s => s.AddParts(
fixture.Create<IEnumerable<IResolverIgnoreRule>>(),
fixture.Create<IEnumerable<IItemResolver>>(),
fixture.Create<IEnumerable<IIntroProvider>>(),
comparers,
fixture.Create<IEnumerable<ILibraryPostScanTask>>()))
.Create();
}
}
@@ -206,4 +206,11 @@ public sealed class UserDataManagerTests : IDisposable
Assert.Equal(222, result[fossilItem.Id].PlaybackPositionTicks);
Assert.Equal(333, result[retiredItem.Id].PlaybackPositionTicks);
}
[Fact]
public void GetUserData_NullUser_ThrowsArgumentNullException()
{
var item = CreateAudioBook();
Assert.Throws<ArgumentNullException>(() => _userDataManager.GetUserData(null!, item));
}
}