diff options
| author | Shadowghost <Ghost_of_Stone@web.de> | 2026-08-14 07:39:49 +0200 |
|---|---|---|
| committer | Shadowghost <Ghost_of_Stone@web.de> | 2026-08-14 07:39:49 +0200 |
| commit | c77649d21e6bd53a662b217c6431b7d123d656b9 (patch) | |
| tree | 7b8663fdf4719ae1c53d9f8ebd2900a93ef8537d | |
| parent | 7e6709f023bab9421a1219e5a59f34e1b8208147 (diff) | |
Skip alternate version links when resolving link parents
| -rw-r--r-- | src/Jellyfin.Database/Jellyfin.Database.Implementations/DescendantQueryHelper.cs | 16 | ||||
| -rw-r--r-- | tests/Jellyfin.Server.Implementations.Tests/Item/DescendantQueryHelperTests.cs | 42 |
2 files changed, 52 insertions, 6 deletions
diff --git a/src/Jellyfin.Database/Jellyfin.Database.Implementations/DescendantQueryHelper.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/DescendantQueryHelper.cs index 92adc37ccc..1e1c8780e8 100644 --- a/src/Jellyfin.Database/Jellyfin.Database.Implementations/DescendantQueryHelper.cs +++ b/src/Jellyfin.Database/Jellyfin.Database.Implementations/DescendantQueryHelper.cs @@ -177,9 +177,17 @@ public static class DescendantQueryHelper // Resolves the folders whose linked children lead, at any depth, to a matching item. private static List<Guid> ResolveLinkParents(JellyfinDbContext context, IQueryable<Guid> matchingItemIds, IQueryable<Guid> ancestorsOfMatches) { + // An alternate version is a second file for the item that links it, not a child of it, so that + // edge is not walked. It is also the one link a non-folder owns, and there is one per remuxed + // movie: walking it would swell this list from the BoxSet and Playlist count to the item count, + // and the list is bound into every statement the returned queryable is embedded in. + var containerLinks = context.LinkedChildren + .Where(e => e.ChildType != LinkedChildType.LocalAlternateVersion + && e.ChildType != LinkedChildType.LinkedAlternateVersion); + // A link sits above the closure and above another link alike, so the hop repeats until nothing - // new turns up. Only link owners are collected, which bounds it by BoxSets and Playlists. - var resolved = context.LinkedChildren + // new turns up. + var resolved = containerLinks .Where(e => matchingItemIds.Contains(e.ChildId) || ancestorsOfMatches.Contains(e.ChildId)) .Select(e => e.ParentId) .Distinct() @@ -193,11 +201,11 @@ public static class DescendantQueryHelper .WhereOneOrMany(frontier, e => e.ItemId) .Select(e => e.ParentItemId); - var directLinkParents = context.LinkedChildren + var directLinkParents = containerLinks .WhereOneOrMany(frontier, e => e.ChildId) .Select(e => e.ParentId); - var indirectLinkParents = context.LinkedChildren + var indirectLinkParents = containerLinks .Where(e => containingFolders.Contains(e.ChildId)) .Select(e => e.ParentId); diff --git a/tests/Jellyfin.Server.Implementations.Tests/Item/DescendantQueryHelperTests.cs b/tests/Jellyfin.Server.Implementations.Tests/Item/DescendantQueryHelperTests.cs index bb14c3897c..f2ecfadd50 100644 --- a/tests/Jellyfin.Server.Implementations.Tests/Item/DescendantQueryHelperTests.cs +++ b/tests/Jellyfin.Server.Implementations.Tests/Item/DescendantQueryHelperTests.cs @@ -364,6 +364,44 @@ public sealed class DescendantQueryHelperTests : SqliteDbTestFixture } [Fact] + public void GetFolderIdsMatching_AlternateVersionLinks_AreNotWalked() + { + var collections = Guid.NewGuid(); + var boxSet = Guid.NewGuid(); + var library = Guid.NewGuid(); + var movie = Guid.NewGuid(); + var alternateVersion = Guid.NewGuid(); + + using (var ctx = CreateDbContext()) + { + AddFolder(ctx, collections); + AddItem(ctx, boxSet, BoxSetType, isFolder: true); + AddFolder(ctx, library); + AddItem(ctx, movie, MovieType); + AddItem(ctx, alternateVersion, MovieType); + + AddAncestors(ctx, boxSet, collections); + AddAncestors(ctx, movie, library); + AddAncestors(ctx, alternateVersion, library); + // Only the second file carries the subtitles, and it hangs off the movie by an alternate + // version link. The movie is not a folder, so that link is not a parent-child edge. + AddLink(ctx, movie, alternateVersion, LinkedChildType.LocalAlternateVersion); + AddLink(ctx, boxSet, movie); + AddStream(ctx, alternateVersion, MediaStreamTypeEntity.Subtitle); + ctx.SaveChanges(); + } + + using (var ctx = CreateDbContext()) + { + var folders = DescendantQueryHelper.GetFolderIdsMatching(ctx, new HasSubtitles()).ToHashSet(); + + // The library still matches: the alternate version carries its own closure. The box set does + // not, matching the descendant side, which does not follow a non-folder's links either. + Assert.Equal([library], folders); + } + } + + [Fact] public void GetOwnedDescendantIds_IgnoresLinkedChildren() { var boxSet = Guid.NewGuid(); @@ -472,7 +510,7 @@ public sealed class DescendantQueryHelperTests : SqliteDbTestFixture } // LinkedChildren is keyed on (ParentId, SortOrder), so every link of a parent needs its own slot. - private void AddLink(JellyfinDbContext context, Guid parentId, Guid childId) + private void AddLink(JellyfinDbContext context, Guid parentId, Guid childId, LinkedChildType childType = LinkedChildType.Manual) { _linkCounters.TryGetValue(parentId, out var sortOrder); _linkCounters[parentId] = sortOrder + 1; @@ -481,7 +519,7 @@ public sealed class DescendantQueryHelperTests : SqliteDbTestFixture { ParentId = parentId, ChildId = childId, - ChildType = LinkedChildType.Manual, + ChildType = childType, SortOrder = sortOrder }); } |
