< Summary - Jellyfin

Information
Class: Jellyfin.Database.Implementations.DescendantQueryHelper
Assembly: Jellyfin.Database.Implementations
File(s): /srv/git/jellyfin/src/Jellyfin.Database/Jellyfin.Database.Implementations/DescendantQueryHelper.cs
Line coverage
17%
Covered lines: 18
Uncovered lines: 85
Coverable lines: 103
Total lines: 250
Line coverage: 17.4%
Branch coverage
13%
Covered branches: 5
Total branches: 36
Branch coverage: 13.8%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Coverage history

Coverage history 0 25 50 75 100 5/4/2026 - 12:15:16 AM Line coverage: 17.8% (18/101) Branch coverage: 13.8% (5/36) Total lines: 2485/16/2026 - 12:15:55 AM Line coverage: 17.4% (18/103) Branch coverage: 13.8% (5/36) Total lines: 250 5/4/2026 - 12:15:16 AM Line coverage: 17.8% (18/101) Branch coverage: 13.8% (5/36) Total lines: 2485/16/2026 - 12:15:55 AM Line coverage: 17.4% (18/103) Branch coverage: 13.8% (5/36) Total lines: 250

Coverage delta

Coverage delta 1 -1

Metrics

File(s)

/srv/git/jellyfin/src/Jellyfin.Database/Jellyfin.Database.Implementations/DescendantQueryHelper.cs

#LineLine coverage
 1using System;
 2using System.Collections.Generic;
 3using System.Linq;
 4using Jellyfin.Database.Implementations.Entities;
 5using Jellyfin.Database.Implementations.MatchCriteria;
 6
 7namespace Jellyfin.Database.Implementations;
 8
 9/// <summary>
 10/// Provides methods for querying item hierarchies using iterative traversal.
 11/// Uses AncestorIds and LinkedChildren tables for parent-child traversal.
 12/// </summary>
 13public static class DescendantQueryHelper
 14{
 15    /// <summary>
 16    /// Gets a queryable of all descendant IDs for a parent item.
 17    /// Traverses AncestorIds and LinkedChildren to find all descendants.
 18    /// </summary>
 19    /// <param name="context">Database context.</param>
 20    /// <param name="parentId">Parent item ID.</param>
 21    /// <returns>Queryable of descendant item IDs.</returns>
 22    public static IQueryable<Guid> GetAllDescendantIds(JellyfinDbContext context, Guid parentId)
 23    {
 024        ArgumentNullException.ThrowIfNull(context);
 25
 026        var descendants = TraverseHierarchyDown(context, [parentId]);
 27
 028        descendants.Remove(parentId);
 29
 030        return descendants.AsQueryable();
 31    }
 32
 33    /// <summary>
 34    /// Gets a queryable of all owned descendant IDs for a parent item.
 35    /// Traverses only AncestorIds (hierarchical ownership), NOT LinkedChildren (associations).
 36    /// Use this for deletion to avoid destroying items that are merely linked (e.g. movies in a BoxSet).
 37    /// </summary>
 38    /// <param name="context">Database context.</param>
 39    /// <param name="parentId">Parent item ID.</param>
 40    /// <returns>Queryable of owned descendant item IDs.</returns>
 41    public static IQueryable<Guid> GetOwnedDescendantIds(JellyfinDbContext context, Guid parentId)
 42    {
 043        ArgumentNullException.ThrowIfNull(context);
 44
 045        var descendants = TraverseHierarchyDownOwned(context, [parentId]);
 46
 047        descendants.Remove(parentId);
 48
 049        return descendants.AsQueryable();
 50    }
 51
 52    /// <summary>
 53    /// Gets all owned descendant IDs for multiple parent items in a single traversal.
 54    /// More efficient than calling <see cref="GetOwnedDescendantIds"/> per parent because
 55    /// it performs one traversal for all seeds instead of N separate traversals.
 56    /// </summary>
 57    /// <param name="context">Database context.</param>
 58    /// <param name="parentIds">Parent item IDs.</param>
 59    /// <returns>Set of all owned descendant item IDs (excluding the parent IDs themselves).</returns>
 60    public static HashSet<Guid> GetOwnedDescendantIdsBatch(JellyfinDbContext context, IReadOnlyList<Guid> parentIds)
 61    {
 162        ArgumentNullException.ThrowIfNull(context);
 163        ArgumentNullException.ThrowIfNull(parentIds);
 64
 165        if (parentIds.Count == 0)
 66        {
 067            return [];
 68        }
 69
 170        var seedSet = new HashSet<Guid>(parentIds);
 171        var descendants = TraverseHierarchyDownOwned(context, seedSet);
 72
 73        // Remove the seed IDs — callers want only descendants
 174        descendants.ExceptWith(seedSet);
 75
 176        return descendants;
 77    }
 78
 79    /// <summary>
 80    /// Gets a queryable of all folder IDs that have any descendant matching the specified criteria.
 81    /// Can be used in LINQ .Contains() expressions.
 82    /// </summary>
 83    /// <param name="context">Database context.</param>
 84    /// <param name="criteria">The matching criteria to apply.</param>
 85    /// <returns>Queryable of folder IDs.</returns>
 86    public static IQueryable<Guid> GetFolderIdsMatching(JellyfinDbContext context, FolderMatchCriteria criteria)
 87    {
 088        ArgumentNullException.ThrowIfNull(context);
 089        ArgumentNullException.ThrowIfNull(criteria);
 090        var matchingItemIds = criteria switch
 091        {
 092            HasSubtitles => context.MediaStreamInfos
 093                .Where(ms => ms.StreamType == MediaStreamTypeEntity.Subtitle)
 094                .Select(ms => ms.ItemId)
 095                .Distinct()
 096                .ToHashSet(),
 097            HasChapterImages => context.Chapters
 098                .Where(c => c.ImagePath != null)
 099                .Select(c => c.ItemId)
 0100                .Distinct()
 0101                .ToHashSet(),
 0102            HasMediaStreamType m => GetMatchingMediaStreamItemIds(context, m),
 0103            _ => throw new ArgumentOutOfRangeException(nameof(criteria), $"Unknown criteria type: {criteria.GetType().Na
 0104        };
 105
 0106        var ancestors = TraverseHierarchyUp(context, matchingItemIds);
 107
 0108        return ancestors.AsQueryable();
 109    }
 110
 111    private static HashSet<Guid> GetMatchingMediaStreamItemIds(JellyfinDbContext context, HasMediaStreamType criteria)
 112    {
 0113        var query = context.MediaStreamInfos
 0114            .Where(ms => ms.StreamType == criteria.StreamType
 0115                   && (criteria.Language.Contains(ms.Language)
 0116                       || (criteria.Language.Contains("und") && string.IsNullOrEmpty(ms.Language)))); // und = undetermi
 117
 0118        if (criteria.IsExternal.HasValue)
 119        {
 0120            var isExternal = criteria.IsExternal.Value;
 0121            query = query.Where(ms => ms.IsExternal == isExternal);
 122        }
 123
 0124        return query.Select(ms => ms.ItemId).Distinct().ToHashSet();
 125    }
 126
 127    /// <summary>
 128    /// Traverses DOWN the hierarchy from parent folders to find all descendants.
 129    /// </summary>
 130    private static HashSet<Guid> TraverseHierarchyDown(JellyfinDbContext context, ICollection<Guid> startIds)
 131    {
 0132        var visited = new HashSet<Guid>(startIds);
 0133        var folderStack = new HashSet<Guid>(startIds);
 134
 0135        while (folderStack.Count != 0)
 136        {
 0137            var currentFolders = folderStack.ToArray();
 0138            folderStack.Clear();
 139
 0140            var directChildren = context.AncestorIds
 0141                .WhereOneOrMany(currentFolders, e => e.ParentItemId)
 0142                .Select(e => e.ItemId)
 0143                .ToArray();
 144
 0145            var linkedChildren = context.LinkedChildren
 0146                .WhereOneOrMany(currentFolders, e => e.ParentId)
 0147                .Select(e => e.ChildId)
 0148                .ToArray();
 149
 0150            var allChildren = directChildren.Concat(linkedChildren).Distinct().ToArray();
 151
 0152            if (allChildren.Length == 0)
 153            {
 154                break;
 155            }
 156
 0157            var childFolders = context.BaseItems
 0158                .WhereOneOrMany(allChildren, e => e.Id)
 0159                .Where(e => e.IsFolder)
 0160                .Select(e => e.Id)
 0161                .ToHashSet();
 162
 0163            foreach (var childId in allChildren)
 164            {
 0165                if (visited.Add(childId) && childFolders.Contains(childId))
 166                {
 0167                    folderStack.Add(childId);
 168                }
 169            }
 170        }
 171
 0172        return visited;
 173    }
 174
 175    /// <summary>
 176    /// Traverses DOWN the hierarchy using only AncestorIds (ownership), not LinkedChildren.
 177    /// </summary>
 178    private static HashSet<Guid> TraverseHierarchyDownOwned(JellyfinDbContext context, ICollection<Guid> startIds)
 179    {
 1180        var visited = new HashSet<Guid>(startIds);
 1181        var folderStack = new HashSet<Guid>(startIds);
 182
 1183        while (folderStack.Count != 0)
 184        {
 1185            var currentFolders = folderStack.ToArray();
 1186            folderStack.Clear();
 187
 1188            var directChildren = context.AncestorIds
 1189                .WhereOneOrMany(currentFolders, e => e.ParentItemId)
 1190                .Select(e => e.ItemId)
 1191                .ToArray();
 192
 1193            if (directChildren.Length == 0)
 194            {
 195                break;
 196            }
 197
 0198            var childFolders = context.BaseItems
 0199                .WhereOneOrMany(directChildren, e => e.Id)
 0200                .Where(e => e.IsFolder)
 0201                .Select(e => e.Id)
 0202                .ToHashSet();
 203
 0204            foreach (var childId in directChildren)
 205            {
 0206                if (visited.Add(childId) && childFolders.Contains(childId))
 207                {
 0208                    folderStack.Add(childId);
 209                }
 210            }
 211        }
 212
 1213        return visited;
 214    }
 215
 216    /// <summary>
 217    /// Traverses UP the hierarchy from items to find all ancestor folders.
 218    /// </summary>
 219    private static HashSet<Guid> TraverseHierarchyUp(JellyfinDbContext context, ICollection<Guid> startIds)
 220    {
 0221        var ancestors = new HashSet<Guid>();
 0222        var itemStack = new HashSet<Guid>(startIds);
 223
 0224        while (itemStack.Count != 0)
 225        {
 0226            var currentItems = itemStack.ToArray();
 0227            itemStack.Clear();
 228
 0229            var ancestorParents = context.AncestorIds
 0230                .WhereOneOrMany(currentItems, e => e.ItemId)
 0231                .Select(e => e.ParentItemId)
 0232                .ToArray();
 233
 0234            var linkedParents = context.LinkedChildren
 0235                .WhereOneOrMany(currentItems, e => e.ChildId)
 0236                .Select(e => e.ParentId)
 0237                .ToArray();
 238
 0239            foreach (var parentId in ancestorParents.Concat(linkedParents))
 240            {
 0241                if (ancestors.Add(parentId))
 242                {
 0243                    itemStack.Add(parentId);
 244                }
 245            }
 246        }
 247
 0248        return ancestors;
 249    }
 250}