aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorShadowghost <Ghost_of_Stone@web.de>2026-08-14 07:39:49 +0200
committerShadowghost <Ghost_of_Stone@web.de>2026-08-14 07:39:49 +0200
commitc77649d21e6bd53a662b217c6431b7d123d656b9 (patch)
tree7b8663fdf4719ae1c53d9f8ebd2900a93ef8537d
parent7e6709f023bab9421a1219e5a59f34e1b8208147 (diff)
Skip alternate version links when resolving link parents
-rw-r--r--src/Jellyfin.Database/Jellyfin.Database.Implementations/DescendantQueryHelper.cs16
-rw-r--r--tests/Jellyfin.Server.Implementations.Tests/Item/DescendantQueryHelperTests.cs42
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
});
}