Skip alternate version links when resolving link parents

This commit is contained in:
Shadowghost
2026-08-14 07:39:49 +02:00
parent 7e6709f023
commit c77649d21e
2 changed files with 52 additions and 6 deletions
@@ -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);
@@ -363,6 +363,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()
{
@@ -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
});
}