From bb6c3b4eecee46a0a6222ffe17657cabc7da97f4 Mon Sep 17 00:00:00 2001 From: Shadowghost Date: Sat, 7 Feb 2026 21:17:01 +0100 Subject: Fix BoxSet collapse handling and deletion --- .../DescendantQueryHelper.cs | 60 ++++++++++++++++++++++ 1 file changed, 60 insertions(+) (limited to 'src/Jellyfin.Database/Jellyfin.Database.Implementations') diff --git a/src/Jellyfin.Database/Jellyfin.Database.Implementations/DescendantQueryHelper.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/DescendantQueryHelper.cs index e6fa6ca458..3bc36dca7a 100644 --- a/src/Jellyfin.Database/Jellyfin.Database.Implementations/DescendantQueryHelper.cs +++ b/src/Jellyfin.Database/Jellyfin.Database.Implementations/DescendantQueryHelper.cs @@ -30,6 +30,25 @@ public static class DescendantQueryHelper return descendants.AsQueryable(); } + /// + /// Gets a queryable of all owned descendant IDs for a parent item. + /// Traverses only AncestorIds (hierarchical ownership), NOT LinkedChildren (associations). + /// Use this for deletion to avoid destroying items that are merely linked (e.g. movies in a BoxSet). + /// + /// Database context. + /// Parent item ID. + /// Queryable of owned descendant item IDs. + public static IQueryable GetOwnedDescendantIds(JellyfinDbContext context, Guid parentId) + { + ArgumentNullException.ThrowIfNull(context); + + var descendants = TraverseHierarchyDownOwned(context, [parentId]); + + descendants.Remove(parentId); + + return descendants.AsQueryable(); + } + /// /// Gets a queryable of all folder IDs that have any descendant matching the specified criteria. /// Can be used in LINQ .Contains() expressions. @@ -124,6 +143,47 @@ public static class DescendantQueryHelper return visited; } + /// + /// Traverses DOWN the hierarchy using only AncestorIds (ownership), not LinkedChildren. + /// + private static HashSet TraverseHierarchyDownOwned(JellyfinDbContext context, ICollection startIds) + { + var visited = new HashSet(startIds); + var folderStack = new HashSet(startIds); + + while (folderStack.Count != 0) + { + var currentFolders = folderStack.ToArray(); + folderStack.Clear(); + + var directChildren = context.AncestorIds + .WhereOneOrMany(currentFolders, e => e.ParentItemId) + .Select(e => e.ItemId) + .ToArray(); + + if (directChildren.Length == 0) + { + break; + } + + var childFolders = context.BaseItems + .WhereOneOrMany(directChildren, e => e.Id) + .Where(e => e.IsFolder) + .Select(e => e.Id) + .ToHashSet(); + + foreach (var childId in directChildren) + { + if (visited.Add(childId) && childFolders.Contains(childId)) + { + folderStack.Add(childId); + } + } + } + + return visited; + } + /// /// Traverses UP the hierarchy from items to find all ancestor folders. /// -- cgit v1.2.3