Fix: Fetch the correct row matching the most up to date file

This commit is contained in:
Jordan Rushing
2026-07-13 15:40:40 -05:00
parent e31f168e9b
commit fcce108948
2 changed files with 180 additions and 2 deletions
@@ -192,7 +192,8 @@ namespace Emby.Server.Implementations.Library
}
else
{
var userData = item.UserData?.Where(e => e.UserId.Equals(user.Id)).Select(Map).FirstOrDefault();
var userDataRow = ResolveUserDataRow(item, item.UserData?.Where(e => e.UserId.Equals(user.Id)));
var userData = userDataRow is not null ? Map(userDataRow) : null;
if (userData is not null)
{
result[item.Id] = userData;
@@ -356,12 +357,40 @@ namespace Emby.Server.Implementations.Library
/// <inheritdoc />
public UserItemData? GetUserData(User user, BaseItem item)
{
return item.UserData?.Where(e => e.UserId.Equals(user.Id)).Select(Map).FirstOrDefault() ?? new UserItemData()
var row = ResolveUserDataRow(item, item.UserData?.Where(e => e.UserId.Equals(user.Id)));
return row is not null ? Map(row) : new UserItemData()
{
Key = item.GetUserDataKeys()[0],
};
}
/// <summary>
/// Picks the row matching the item's current user data keys, in key order, so rows left behind
/// under keys from older metadata don't take priority over the rows the write path updates.
/// </summary>
/// <param name="item">The item whose keys to match.</param>
/// <param name="rows">The candidate user data rows for a single user.</param>
/// <returns>The best matching row, or <c>null</c> when there are none.</returns>
private static UserData? ResolveUserDataRow(BaseItem item, IEnumerable<UserData>? rows)
{
var candidates = rows?.ToList();
if (candidates is null || candidates.Count == 0)
{
return null;
}
foreach (var key in item.GetUserDataKeys())
{
var match = candidates.Find(e => string.Equals(e.CustomDataKey, key, StringComparison.Ordinal));
if (match is not null)
{
return match;
}
}
return candidates[0];
}
/// <inheritdoc />
public UserItemDataDto? GetUserDataDto(BaseItem item, User user)
=> GetUserDataDto(item, null, user, new DtoOptions());
@@ -0,0 +1,149 @@
using System;
using System.Collections.Generic;
using Emby.Server.Implementations.Library;
using Jellyfin.Database.Implementations;
using Jellyfin.Database.Implementations.Entities;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Model.Configuration;
using Microsoft.EntityFrameworkCore;
using Moq;
using Xunit;
using AudioBook = MediaBrowser.Controller.Entities.AudioBook;
namespace Jellyfin.Server.Implementations.Tests.Library;
public class UserDataManagerTests
{
private readonly UserDataManager _userDataManager;
private readonly User _user;
public UserDataManagerTests()
{
var config = new Mock<IServerConfigurationManager>();
config.SetupGet(c => c.Configuration).Returns(new ServerConfiguration());
var repository = Mock.Of<IDbContextFactory<JellyfinDbContext>>();
_userDataManager = new UserDataManager(config.Object, repository);
_user = new User("user", "auth-provider", "reset-provider")
{
Id = Guid.NewGuid()
};
}
private AudioBook CreateAudioBook()
{
// GetUserDataKeys(): ["Author-Series-0001Book Title", "<item id N>"]
return new AudioBook
{
Id = Guid.NewGuid(),
Name = "Book Title",
Album = "Series",
AlbumArtists = new[] { "Author" },
IndexNumber = 1
};
}
private UserData CreateUserDataRow(AudioBook item, string key, long positionTicks)
{
return new UserData
{
ItemId = item.Id,
Item = null,
UserId = _user.Id,
User = null,
CustomDataKey = key,
PlaybackPositionTicks = positionTicks
};
}
[Fact]
public void GetUserData_RowsUnderCurrentAndRetiredKeys_PrefersCurrentKeyRow()
{
var item = CreateAudioBook();
var currentKey = item.GetUserDataKeys()[0];
// the retired-key row comes first to ensure selection is by key, not row order
item.UserData = new List<UserData>
{
CreateUserDataRow(item, "Author-Old Album-0001Old File Name", 111),
CreateUserDataRow(item, currentKey, 222)
};
var userData = _userDataManager.GetUserData(_user, item);
Assert.NotNull(userData);
Assert.Equal(currentKey, userData.Key);
Assert.Equal(222, userData.PlaybackPositionTicks);
}
[Fact]
public void GetUserData_NoPrimaryKeyRow_UsesNextCurrentKeyRow()
{
var item = CreateAudioBook();
var idKey = item.GetUserDataKeys()[1];
item.UserData = new List<UserData>
{
CreateUserDataRow(item, "Author-Old Album-0001Old File Name", 111),
CreateUserDataRow(item, idKey, 333)
};
var userData = _userDataManager.GetUserData(_user, item);
Assert.NotNull(userData);
Assert.Equal(idKey, userData.Key);
Assert.Equal(333, userData.PlaybackPositionTicks);
}
[Fact]
public void GetUserData_OnlyRetiredKeyRows_ReturnsRetiredKeyRow()
{
var item = CreateAudioBook();
item.UserData = new List<UserData>
{
CreateUserDataRow(item, "Author-Old Album-0001Old File Name", 111)
};
var userData = _userDataManager.GetUserData(_user, item);
Assert.NotNull(userData);
Assert.Equal(111, userData.PlaybackPositionTicks);
}
[Fact]
public void GetUserData_NoRows_ReturnsDefaultWithPrimaryKey()
{
var item = CreateAudioBook();
item.UserData = new List<UserData>();
var userData = _userDataManager.GetUserData(_user, item);
Assert.NotNull(userData);
Assert.Equal(item.GetUserDataKeys()[0], userData.Key);
Assert.Equal(0, userData.PlaybackPositionTicks);
}
[Fact]
public void GetUserData_RowsForOtherUsers_AreIgnored()
{
var item = CreateAudioBook();
var currentKey = item.GetUserDataKeys()[0];
var otherUserRow = CreateUserDataRow(item, currentKey, 999);
otherUserRow.UserId = Guid.NewGuid();
item.UserData = new List<UserData>
{
otherUserRow,
CreateUserDataRow(item, currentKey, 222)
};
var userData = _userDataManager.GetUserData(_user, item);
Assert.NotNull(userData);
Assert.Equal(222, userData.PlaybackPositionTicks);
}
}