Fix series merging leaking across libraries and under-counting merged children
This commit is contained in:
@@ -7,6 +7,7 @@ using Jellyfin.Database.Implementations.Locking;
|
||||
using Jellyfin.Database.Providers.Sqlite;
|
||||
using Jellyfin.Server.Implementations.Item;
|
||||
using MediaBrowser.Common.Configuration;
|
||||
using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using MediaBrowser.Controller.Persistence;
|
||||
using Microsoft.Data.Sqlite;
|
||||
@@ -14,6 +15,7 @@ using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using Moq;
|
||||
using Xunit;
|
||||
using LinkedChildType = Jellyfin.Database.Implementations.Entities.LinkedChildType;
|
||||
|
||||
namespace Jellyfin.Server.Implementations.Tests.Item;
|
||||
|
||||
@@ -43,10 +45,18 @@ public sealed class ItemCountServiceTests : IDisposable
|
||||
var factory = new Mock<IDbContextFactory<JellyfinDbContext>>();
|
||||
factory.Setup(f => f.CreateDbContext()).Returns(CreateDbContext);
|
||||
|
||||
var queryHelpers = new Mock<IItemQueryHelpers>();
|
||||
queryHelpers
|
||||
.Setup(h => h.ApplyAccessFiltering(
|
||||
It.IsAny<JellyfinDbContext>(),
|
||||
It.IsAny<IQueryable<BaseItemEntity>>(),
|
||||
It.IsAny<InternalItemsQuery>()))
|
||||
.Returns((JellyfinDbContext _, IQueryable<BaseItemEntity> query, InternalItemsQuery _) => query);
|
||||
|
||||
_service = new ItemCountService(
|
||||
factory.Object,
|
||||
new Mock<IItemTypeLookup>().Object,
|
||||
new Mock<IItemQueryHelpers>().Object);
|
||||
queryHelpers.Object);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
@@ -106,6 +116,153 @@ public sealed class ItemCountServiceTests : IDisposable
|
||||
Assert.Equal(parentIds.Count, result.Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetCounts_MergedFolders_CountLeavesOfEveryFolderInTheGroup()
|
||||
{
|
||||
// Two folder-items of one merged series: same presentation key, a leaf each, one of them played.
|
||||
var (user, seriesA, seriesB) = SeedMergedSeries(out var playedLeafId);
|
||||
|
||||
var filter = new InternalItemsQuery(user);
|
||||
|
||||
// Either folder-item stands for the whole merged series, so both must report the group.
|
||||
foreach (var seriesId in new[] { seriesA, seriesB })
|
||||
{
|
||||
Assert.Equal(2, _service.GetTotalCount(filter, seriesId));
|
||||
Assert.Equal(1, _service.GetPlayedCount(filter, seriesId));
|
||||
Assert.Equal((1, 2), _service.GetPlayedAndTotalCount(filter, seriesId));
|
||||
}
|
||||
|
||||
var batch = _service.GetPlayedAndTotalCountBatch([seriesA], user);
|
||||
Assert.Equal((1, 2), batch[seriesA]);
|
||||
|
||||
Assert.NotEqual(Guid.Empty, playedLeafId);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetCounts_UnmergedFolder_CountsOnlyItsOwnLeaves()
|
||||
{
|
||||
var (user, _, _) = SeedMergedSeries(out _);
|
||||
|
||||
// A folder with a key of its own must not pick up anything from the merged pair.
|
||||
var loneSeriesId = Guid.NewGuid();
|
||||
var loneLeafId = Guid.NewGuid();
|
||||
|
||||
using (var context = CreateDbContext())
|
||||
{
|
||||
var lone = CreateItem(loneSeriesId);
|
||||
lone.PresentationUniqueKey = "lone-series";
|
||||
context.BaseItems.Add(lone);
|
||||
context.BaseItems.Add(CreateLeaf(loneLeafId));
|
||||
context.SaveChanges();
|
||||
AddAncestor(context, loneLeafId, loneSeriesId);
|
||||
context.SaveChanges();
|
||||
}
|
||||
|
||||
var filter = new InternalItemsQuery(user);
|
||||
|
||||
Assert.Equal(1, _service.GetTotalCount(filter, loneSeriesId));
|
||||
Assert.Equal(0, _service.GetPlayedCount(filter, loneSeriesId));
|
||||
Assert.Equal((0, 1), _service.GetPlayedAndTotalCount(filter, loneSeriesId));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetChildCountBatch_MergedFolders_CountsDistinctChildKeys()
|
||||
{
|
||||
var seriesA = Guid.NewGuid();
|
||||
var seriesB = Guid.NewGuid();
|
||||
|
||||
using (var context = CreateDbContext())
|
||||
{
|
||||
foreach (var id in new[] { seriesA, seriesB })
|
||||
{
|
||||
var series = CreateItem(id);
|
||||
series.PresentationUniqueKey = "merged-series";
|
||||
context.BaseItems.Add(series);
|
||||
}
|
||||
|
||||
// Each folder-item holds a "Season 1"; those two share a key and are one season to the user.
|
||||
var sharedSeasonA = CreateItem(Guid.NewGuid(), seriesA);
|
||||
sharedSeasonA.PresentationUniqueKey = "merged-series-001";
|
||||
var sharedSeasonB = CreateItem(Guid.NewGuid(), seriesB);
|
||||
sharedSeasonB.PresentationUniqueKey = "merged-series-001";
|
||||
var ownSeason = CreateItem(Guid.NewGuid(), seriesB);
|
||||
ownSeason.PresentationUniqueKey = "merged-series-002";
|
||||
|
||||
context.BaseItems.AddRange(sharedSeasonA, sharedSeasonB, ownSeason);
|
||||
context.SaveChanges();
|
||||
}
|
||||
|
||||
var result = _service.GetChildCountBatch([seriesA, seriesB], null);
|
||||
|
||||
Assert.Equal(2, result[seriesA]);
|
||||
Assert.Equal(2, result[seriesB]);
|
||||
}
|
||||
|
||||
private (User User, Guid SeriesA, Guid SeriesB) SeedMergedSeries(out Guid playedLeafId)
|
||||
{
|
||||
var user = new User("count-test", "provider", "reset");
|
||||
var seriesA = Guid.NewGuid();
|
||||
var seriesB = Guid.NewGuid();
|
||||
var leafA = Guid.NewGuid();
|
||||
var leafB = Guid.NewGuid();
|
||||
playedLeafId = leafA;
|
||||
|
||||
using (var context = CreateDbContext())
|
||||
{
|
||||
context.Users.Add(user);
|
||||
|
||||
foreach (var id in new[] { seriesA, seriesB })
|
||||
{
|
||||
var series = CreateItem(id);
|
||||
series.PresentationUniqueKey = "merged-series";
|
||||
context.BaseItems.Add(series);
|
||||
}
|
||||
|
||||
context.BaseItems.AddRange(CreateLeaf(leafA), CreateLeaf(leafB));
|
||||
context.SaveChanges();
|
||||
|
||||
AddAncestor(context, leafA, seriesA);
|
||||
AddAncestor(context, leafB, seriesB);
|
||||
|
||||
context.UserData.Add(new UserData
|
||||
{
|
||||
ItemId = leafA,
|
||||
UserId = user.Id,
|
||||
CustomDataKey = string.Empty,
|
||||
Played = true,
|
||||
Item = null,
|
||||
User = null
|
||||
});
|
||||
|
||||
context.SaveChanges();
|
||||
}
|
||||
|
||||
return (user, seriesA, seriesB);
|
||||
}
|
||||
|
||||
private static void AddAncestor(JellyfinDbContext context, Guid itemId, Guid parentItemId)
|
||||
{
|
||||
context.AncestorIds.Add(new AncestorId
|
||||
{
|
||||
ItemId = itemId,
|
||||
ParentItemId = parentItemId,
|
||||
Item = null!,
|
||||
ParentItem = null!
|
||||
});
|
||||
}
|
||||
|
||||
private static BaseItemEntity CreateLeaf(Guid id)
|
||||
{
|
||||
return new BaseItemEntity
|
||||
{
|
||||
Id = id,
|
||||
Type = "Episode",
|
||||
IsFolder = false,
|
||||
IsVirtualItem = false,
|
||||
PresentationUniqueKey = id.ToString("N")
|
||||
};
|
||||
}
|
||||
|
||||
private static BaseItemEntity CreateItem(Guid id, Guid? parentId = null)
|
||||
{
|
||||
return new BaseItemEntity
|
||||
|
||||
Reference in New Issue
Block a user