< 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
18%
Covered lines: 19
Uncovered lines: 85
Coverable lines: 104
Total lines: 259
Line coverage: 18.2%
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: 2508/3/2026 - 12:16:46 AM Line coverage: 18.2% (19/104) Branch coverage: 13.8% (5/36) Total lines: 259 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: 2508/3/2026 - 12:16:46 AM Line coverage: 18.2% (19/104) Branch coverage: 13.8% (5/36) Total lines: 259

Coverage delta

Coverage delta 1 -1

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.cctor()100%11100%
GetAllDescendantIds(...)100%210%
GetOwnedDescendantIds(...)100%210%
GetOwnedDescendantIdsBatch(...)50%2287.5%
GetFolderIdsMatching(...)0%4260%
GetMatchingMediaStreamItemIds(...)0%620%
TraverseHierarchyDown(...)0%110100%
TraverseHierarchyDownOwned(...)40%171057.89%
TraverseHierarchyUp(...)0%4260%

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 System.Linq.Expressions;
 5using Jellyfin.Database.Implementations.Entities;
 6using Jellyfin.Database.Implementations.MatchCriteria;
 7
 8namespace Jellyfin.Database.Implementations;
 9
 10/// <summary>
 11/// Provides methods for querying item hierarchies using iterative traversal.
 12/// Uses AncestorIds and LinkedChildren tables for parent-child traversal.
 13/// </summary>
 14public static class DescendantQueryHelper
 15{
 16    /// <summary>
 17    /// Gets the predicate identifying items that count toward played/total aggregation:
 18    /// real leaf media, i.e. neither folders nor virtual items (missing or unaired episodes).
 19    /// Shared by the per-item and batched count paths so they cannot diverge.
 20    /// </summary>
 21    public static Expression<Func<BaseItemEntity, bool>> IsCountableLeaf { get; } =
 122        b => !b.IsFolder && !b.IsVirtualItem;
 23
 24    /// <summary>
 25    /// Gets a queryable of all descendant IDs for a parent item.
 26    /// Traverses AncestorIds and LinkedChildren to find all descendants.
 27    /// </summary>
 28    /// <param name="context">Database context.</param>
 29    /// <param name="parentId">Parent item ID.</param>
 30    /// <returns>Queryable of descendant item IDs.</returns>
 31    public static IQueryable<Guid> GetAllDescendantIds(JellyfinDbContext context, Guid parentId)
 32    {
 033        ArgumentNullException.ThrowIfNull(context);
 34
 035        var descendants = TraverseHierarchyDown(context, [parentId]);
 36
 037        descendants.Remove(parentId);
 38
 039        return descendants.AsQueryable();
 40    }
 41
 42    /// <summary>
 43    /// Gets a queryable of all owned descendant IDs for a parent item.
 44    /// Traverses only AncestorIds (hierarchical ownership), NOT LinkedChildren (associations).
 45    /// Use this for deletion to avoid destroying items that are merely linked (e.g. movies in a BoxSet).
 46    /// </summary>
 47    /// <param name="context">Database context.</param>
 48    /// <param name="parentId">Parent item ID.</param>
 49    /// <returns>Queryable of owned descendant item IDs.</returns>
 50    public static IQueryable<Guid> GetOwnedDescendantIds(JellyfinDbContext context, Guid parentId)
 51    {
 052        ArgumentNullException.ThrowIfNull(context);
 53
 054        var descendants = TraverseHierarchyDownOwned(context, [parentId]);
 55
 056        descendants.Remove(parentId);
 57
 058        return descendants.AsQueryable();
 59    }
 60
 61    /// <summary>
 62    /// Gets all owned descendant IDs for multiple parent items in a single traversal.
 63    /// More efficient than calling <see cref="GetOwnedDescendantIds"/> per parent because
 64    /// it performs one traversal for all seeds instead of N separate traversals.
 65    /// </summary>
 66    /// <param name="context">Database context.</param>
 67    /// <param name="parentIds">Parent item IDs.</param>
 68    /// <returns>Set of all owned descendant item IDs (excluding the parent IDs themselves).</returns>
 69    public static HashSet<Guid> GetOwnedDescendantIdsBatch(JellyfinDbContext context, IReadOnlyList<Guid> parentIds)
 70    {
 171        ArgumentNullException.ThrowIfNull(context);
 172        ArgumentNullException.ThrowIfNull(parentIds);
 73
 174        if (parentIds.Count == 0)
 75        {
 076            return [];
 77        }
 78
 179        var seedSet = new HashSet<Guid>(parentIds);
 180        var descendants = TraverseHierarchyDownOwned(context, seedSet);
 81
 82        // Remove the seed IDs — callers want only descendants
 183        descendants.ExceptWith(seedSet);
 84
 185        return descendants;
 86    }
 87
 88    /// <summary>
 89    /// Gets a queryable of all folder IDs that have any descendant matching the specified criteria.
 90    /// Can be used in LINQ .Contains() expressions.
 91    /// </summary>
 92    /// <param name="context">Database context.</param>
 93    /// <param name="criteria">The matching criteria to apply.</param>
 94    /// <returns>Queryable of folder IDs.</returns>
 95    public static IQueryable<Guid> GetFolderIdsMatching(JellyfinDbContext context, FolderMatchCriteria criteria)
 96    {
 097        ArgumentNullException.ThrowIfNull(context);
 098        ArgumentNullException.ThrowIfNull(criteria);
 099        var matchingItemIds = criteria switch
 0100        {
 0101            HasSubtitles => context.MediaStreamInfos
 0102                .Where(ms => ms.StreamType == MediaStreamTypeEntity.Subtitle)
 0103                .Select(ms => ms.ItemId)
 0104                .Distinct()
 0105                .ToHashSet(),
 0106            HasChapterImages => context.Chapters
 0107                .Where(c => c.ImagePath != null)
 0108                .Select(c => c.ItemId)
 0109                .Distinct()
 0110                .ToHashSet(),
 0111            HasMediaStreamType m => GetMatchingMediaStreamItemIds(context, m),
 0112            _ => throw new ArgumentOutOfRangeException(nameof(criteria), $"Unknown criteria type: {criteria.GetType().Na
 0113        };
 114
 0115        var ancestors = TraverseHierarchyUp(context, matchingItemIds);
 116
 0117        return ancestors.AsQueryable();
 118    }
 119
 120    private static HashSet<Guid> GetMatchingMediaStreamItemIds(JellyfinDbContext context, HasMediaStreamType criteria)
 121    {
 0122        var query = context.MediaStreamInfos
 0123            .Where(ms => ms.StreamType == criteria.StreamType
 0124                   && (criteria.Language.Contains(ms.Language)
 0125                       || (criteria.Language.Contains("und") && string.IsNullOrEmpty(ms.Language)))); // und = undetermi
 126
 0127        if (criteria.IsExternal.HasValue)
 128        {
 0129            var isExternal = criteria.IsExternal.Value;
 0130            query = query.Where(ms => ms.IsExternal == isExternal);
 131        }
 132
 0133        return query.Select(ms => ms.ItemId).Distinct().ToHashSet();
 134    }
 135
 136    /// <summary>
 137    /// Traverses DOWN the hierarchy from parent folders to find all descendants.
 138    /// </summary>
 139    private static HashSet<Guid> TraverseHierarchyDown(JellyfinDbContext context, ICollection<Guid> startIds)
 140    {
 0141        var visited = new HashSet<Guid>(startIds);
 0142        var folderStack = new HashSet<Guid>(startIds);
 143
 0144        while (folderStack.Count != 0)
 145        {
 0146            var currentFolders = folderStack.ToArray();
 0147            folderStack.Clear();
 148
 0149            var directChildren = context.AncestorIds
 0150                .WhereOneOrMany(currentFolders, e => e.ParentItemId)
 0151                .Select(e => e.ItemId)
 0152                .ToArray();
 153
 0154            var linkedChildren = context.LinkedChildren
 0155                .WhereOneOrMany(currentFolders, e => e.ParentId)
 0156                .Select(e => e.ChildId)
 0157                .ToArray();
 158
 0159            var allChildren = directChildren.Concat(linkedChildren).Distinct().ToArray();
 160
 0161            if (allChildren.Length == 0)
 162            {
 163                break;
 164            }
 165
 0166            var childFolders = context.BaseItems
 0167                .WhereOneOrMany(allChildren, e => e.Id)
 0168                .Where(e => e.IsFolder)
 0169                .Select(e => e.Id)
 0170                .ToHashSet();
 171
 0172            foreach (var childId in allChildren)
 173            {
 0174                if (visited.Add(childId) && childFolders.Contains(childId))
 175                {
 0176                    folderStack.Add(childId);
 177                }
 178            }
 179        }
 180
 0181        return visited;
 182    }
 183
 184    /// <summary>
 185    /// Traverses DOWN the hierarchy using only AncestorIds (ownership), not LinkedChildren.
 186    /// </summary>
 187    private static HashSet<Guid> TraverseHierarchyDownOwned(JellyfinDbContext context, ICollection<Guid> startIds)
 188    {
 1189        var visited = new HashSet<Guid>(startIds);
 1190        var folderStack = new HashSet<Guid>(startIds);
 191
 1192        while (folderStack.Count != 0)
 193        {
 1194            var currentFolders = folderStack.ToArray();
 1195            folderStack.Clear();
 196
 1197            var directChildren = context.AncestorIds
 1198                .WhereOneOrMany(currentFolders, e => e.ParentItemId)
 1199                .Select(e => e.ItemId)
 1200                .ToArray();
 201
 1202            if (directChildren.Length == 0)
 203            {
 204                break;
 205            }
 206
 0207            var childFolders = context.BaseItems
 0208                .WhereOneOrMany(directChildren, e => e.Id)
 0209                .Where(e => e.IsFolder)
 0210                .Select(e => e.Id)
 0211                .ToHashSet();
 212
 0213            foreach (var childId in directChildren)
 214            {
 0215                if (visited.Add(childId) && childFolders.Contains(childId))
 216                {
 0217                    folderStack.Add(childId);
 218                }
 219            }
 220        }
 221
 1222        return visited;
 223    }
 224
 225    /// <summary>
 226    /// Traverses UP the hierarchy from items to find all ancestor folders.
 227    /// </summary>
 228    private static HashSet<Guid> TraverseHierarchyUp(JellyfinDbContext context, ICollection<Guid> startIds)
 229    {
 0230        var ancestors = new HashSet<Guid>();
 0231        var itemStack = new HashSet<Guid>(startIds);
 232
 0233        while (itemStack.Count != 0)
 234        {
 0235            var currentItems = itemStack.ToArray();
 0236            itemStack.Clear();
 237
 0238            var ancestorParents = context.AncestorIds
 0239                .WhereOneOrMany(currentItems, e => e.ItemId)
 0240                .Select(e => e.ParentItemId)
 0241                .ToArray();
 242
 0243            var linkedParents = context.LinkedChildren
 0244                .WhereOneOrMany(currentItems, e => e.ChildId)
 0245                .Select(e => e.ParentId)
 0246                .ToArray();
 247
 0248            foreach (var parentId in ancestorParents.Concat(linkedParents))
 249            {
 0250                if (ancestors.Add(parentId))
 251                {
 0252                    itemStack.Add(parentId);
 253                }
 254            }
 255        }
 256
 0257        return ancestors;
 258    }
 259}