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.Entities; 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(() => 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)); } [Fact] public void Sort_ComparerThatIgnoresPrefetchedUserData_StillSortsFromLiveReads() { var alpha = new Audio { Name = "Alpha", SortName = "Alpha", Id = Guid.NewGuid() }; var zulu = new Audio { Name = "Zulu", SortName = "Zulu", Id = Guid.NewGuid() }; var playCounts = new Dictionary { [alpha.Id] = 1, [zulu.Id] = 9 }; var userDataManager = new Mock(); userDataManager .Setup(u => u.GetUserData(It.IsAny(), It.IsAny())) .Returns((_, item) => new UserItemData { Key = item.Id.ToString("N"), PlayCount = playCounts[item.Id] }); userDataManager .Setup(u => u.GetUserDataBatch(It.IsAny>(), It.IsAny())) .Returns(new Dictionary()); var libraryManager = CreateLibraryManager( new IBaseItemComparer[] { new PluginPlayCountComparer() }, userDataManager); var sorted = libraryManager.Sort( new BaseItem[] { alpha, zulu }, new User("sorter", "provider", "provider"), new[] { (ItemSortBy.PlayCount, SortOrder.Descending) }).ToArray(); Assert.Equal(new[] { "Zulu", "Alpha" }, sorted.Select(i => i.Name)); userDataManager.Verify(u => u.GetUserData(It.IsAny(), It.IsAny()), Times.AtLeastOnce); } 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 comparers, Mock? userDataManager = null) { var fixture = new Fixture().Customize(new AutoMoqCustomization()); fixture.Register(() => new NamingOptions()); if (userDataManager is not null) { fixture.Inject(userDataManager.Object); } var configMock = fixture.Freeze>(); configMock.Setup(c => c.ApplicationPaths.ProgramDataPath).Returns("/data"); BaseItem.ConfigurationManager ??= configMock.Object; var itemRepository = fixture.Freeze>(); itemRepository.Setup(i => i.RetrieveItem(It.IsAny())).Returns(null); var fileSystemMock = fixture.Freeze>(); fileSystemMock.Setup(f => f.GetFileInfo(It.IsAny())).Returns(path => new FileSystemMetadata { FullName = path }); return fixture.Build().Do(s => s.AddParts( fixture.Create>(), fixture.Create>(), fixture.Create>(), comparers, fixture.Create>())) .Create(); } /// /// A comparer of the shape a third-party plugin ships: it implements /// without ever mentioning PrefetchedUserData. /// public sealed class PluginPlayCountComparer : IUserBaseItemComparer { public User User { get; set; } = null!; public IUserManager UserManager { get; set; } = null!; public IUserDataManager UserDataManager { get; set; } = null!; public ItemSortBy Type => ItemSortBy.PlayCount; public int Compare(BaseItem? x, BaseItem? y) => UserDataManager.GetUserData(User, x!)!.PlayCount.CompareTo(UserDataManager.GetUserData(User, y!)!.PlayCount); } }