diff options
| author | Shadowghost <Shadowghost@users.noreply.github.com> | 2026-09-15 11:15:46 -0400 |
|---|---|---|
| committer | Cody Robibero <cody@robibe.ro> | 2026-09-15 11:15:46 -0400 |
| commit | 006ecadbe041ea422b34adf6a3fcf6638d189a70 (patch) | |
| tree | c845d30aafd6cb23bf2e41dd955712db0f641ce4 /tests/Jellyfin.Server.Implementations.Tests/Item | |
| parent | 65bc888b07a25d1883d4d0a2f55a7f3e1ac35c87 (diff) | |
Backport pull request #17881 from jellyfin/release-12.z
Fix /UserViews exhausting memory and reporting random child counts
Original-merge: a838a06aa51eac388a76e9e6421f1a80873c417b
Merged-by: crobibero <cody@robibe.ro>
Backported-by: Cody Robibero <cody@robibe.ro>
Diffstat (limited to 'tests/Jellyfin.Server.Implementations.Tests/Item')
| -rw-r--r-- | tests/Jellyfin.Server.Implementations.Tests/Item/ItemCountServiceTests.cs | 195 |
1 files changed, 195 insertions, 0 deletions
diff --git a/tests/Jellyfin.Server.Implementations.Tests/Item/ItemCountServiceTests.cs b/tests/Jellyfin.Server.Implementations.Tests/Item/ItemCountServiceTests.cs index ff683dc57a..787bb24150 100644 --- a/tests/Jellyfin.Server.Implementations.Tests/Item/ItemCountServiceTests.cs +++ b/tests/Jellyfin.Server.Implementations.Tests/Item/ItemCountServiceTests.cs @@ -201,6 +201,201 @@ public sealed class ItemCountServiceTests : IDisposable } [Fact] + public void GetCounts_PlayedAlternateVersion_CountThePrimaryAsPlayed() + { + var user = new User("alt-version-test", "provider", "reset"); + var seriesId = Guid.NewGuid(); + var primaryId = Guid.NewGuid(); + var alternateId = Guid.NewGuid(); + + using (var context = CreateDbContext()) + { + context.Users.Add(user); + + var series = CreateItem(seriesId); + series.PresentationUniqueKey = "alt-version-series"; + context.BaseItems.Add(series); + + context.BaseItems.Add(CreateLeaf(primaryId)); + var alternate = CreateLeaf(alternateId); + alternate.PrimaryVersionId = primaryId; + context.BaseItems.Add(alternate); + context.SaveChanges(); + + // Only the primary is counted as a leaf, as ApplyAccessFiltering leaves it in production. + AddAncestor(context, primaryId, seriesId); + + context.LinkedChildren.Add(new LinkedChildEntity + { + ParentId = primaryId, + ChildId = alternateId, + ChildType = LinkedChildType.LocalAlternateVersion, + SortOrder = 0 + }); + + // The file that was watched is the alternate, so the primary carries no played row. + context.UserData.Add(new UserData + { + ItemId = alternateId, + UserId = user.Id, + CustomDataKey = string.Empty, + Played = true, + Item = null, + User = null + }); + + context.SaveChanges(); + } + + var filter = new InternalItemsQuery(user); + + // The per-item paths have to agree with the batch one, which the DTO uses interchangeably. + Assert.Equal(1, _service.GetPlayedCount(filter, seriesId)); + Assert.Equal((1, 1), _service.GetPlayedAndTotalCount(filter, seriesId)); + Assert.Equal((1, 1), _service.GetPlayedAndTotalCountBatch([seriesId], user)[seriesId]); + } + + [Fact] + public void GetCounts_MultiVersionMovie_CountPlaybackOfAnyVersion() + { + // Two movies held as two files each: the primary the collection links, and an alternate version + // linked to it. One movie was watched on its alternate, which is where playback of a second cut + // lands; the other was not watched at all. + var user = new User("alt-version-test", "provider", "reset"); + var boxSetId = Guid.NewGuid(); + var libraryId = Guid.NewGuid(); + var watchedPrimaryId = Guid.NewGuid(); + var watchedAlternateId = Guid.NewGuid(); + var unwatchedPrimaryId = Guid.NewGuid(); + var unwatchedAlternateId = Guid.NewGuid(); + + using (var context = CreateDbContext()) + { + context.Users.Add(user); + + var boxSet = CreateItem(boxSetId); + boxSet.PresentationUniqueKey = "alt-version-box-set"; + context.BaseItems.Add(boxSet); + + var library = CreateItem(libraryId); + library.PresentationUniqueKey = "alt-version-library"; + context.BaseItems.Add(library); + + foreach (var (primaryId, alternateId) in + new[] { (watchedPrimaryId, watchedAlternateId), (unwatchedPrimaryId, unwatchedAlternateId) }) + { + context.BaseItems.Add(CreateLeaf(primaryId)); + + var alternate = CreateLeaf(alternateId); + alternate.PrimaryVersionId = primaryId; + context.BaseItems.Add(alternate); + } + + context.SaveChanges(); + + context.LinkedChildren.AddRange( + new LinkedChildEntity + { + ParentId = boxSetId, + ChildId = watchedPrimaryId, + ChildType = LinkedChildType.Manual, + SortOrder = 0 + }, + new LinkedChildEntity + { + ParentId = boxSetId, + ChildId = unwatchedPrimaryId, + ChildType = LinkedChildType.Manual, + SortOrder = 1 + }, + new LinkedChildEntity + { + ParentId = watchedPrimaryId, + ChildId = watchedAlternateId, + ChildType = LinkedChildType.LocalAlternateVersion, + SortOrder = 0 + }, + new LinkedChildEntity + { + ParentId = unwatchedPrimaryId, + ChildId = unwatchedAlternateId, + ChildType = LinkedChildType.LocalAlternateVersion, + SortOrder = 0 + }); + + AddAncestor(context, watchedPrimaryId, libraryId); + AddAncestor(context, unwatchedPrimaryId, libraryId); + + context.UserData.Add(new UserData + { + ItemId = watchedAlternateId, + UserId = user.Id, + CustomDataKey = string.Empty, + Played = true, + Item = null, + User = null + }); + + context.SaveChanges(); + } + + var filter = new InternalItemsQuery(user); + + // A version group is one item to count, and the alternate's playback makes that item played - + // as it already does for the played flag the primary itself reports. + Assert.Equal((1, 2), _service.GetPlayedAndTotalCountFromLinkedChildren(filter, boxSetId)); + Assert.Equal((1, 2), _service.GetPlayedAndTotalCountBatch([boxSetId], user)[boxSetId]); + + // The ancestor-based paths answer the same for the library the primaries sit in. + Assert.Equal(1, _service.GetPlayedCount(filter, libraryId)); + Assert.Equal((1, 2), _service.GetPlayedAndTotalCount(filter, libraryId)); + } + + [Fact] + public void GetChildCountBatch_NoUser_StillCollapsesAlternateVersions() + { + // Both files of a merged movie sit in the folder. With a user it is access filtering that + // drops the alternate; with no user nothing else would, and the folder would report two + // children for the one title a viewer sees. + var folderId = Guid.NewGuid(); + var primaryId = Guid.NewGuid(); + var alternateId = Guid.NewGuid(); + var extraId = Guid.NewGuid(); + var ownedId = Guid.NewGuid(); + + using (var context = CreateDbContext()) + { + context.BaseItems.Add(CreateItem(folderId)); + + var primary = CreateLeaf(primaryId); + primary.ParentId = folderId; + context.BaseItems.Add(primary); + + var alternate = CreateLeaf(alternateId); + alternate.ParentId = folderId; + alternate.PrimaryVersionId = primaryId; + context.BaseItems.Add(alternate); + + // An extra carries an owner and an extra type, and stays a child of its own. + var extra = CreateLeaf(extraId); + extra.ParentId = folderId; + extra.OwnerId = primaryId; + extra.ExtraType = BaseItemExtraType.Trailer; + context.BaseItems.Add(extra); + + // An owned item that is not an extra belongs to its owner, not to the folder. + var owned = CreateLeaf(ownedId); + owned.ParentId = folderId; + owned.OwnerId = primaryId; + context.BaseItems.Add(owned); + + context.SaveChanges(); + } + + Assert.Equal(2, _service.GetChildCountBatch([folderId], null)[folderId]); + } + + [Fact] public void GetChildCountBatch_MergedFolders_CountsDistinctChildKeys() { var seriesA = Guid.NewGuid(); |
