< 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: 83
Coverable lines: 101
Total lines: 248
Line coverage: 17.8%
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: 248 5/4/2026 - 12:15:16 AM Line coverage: 17.8% (18/101) Branch coverage: 13.8% (5/36) Total lines: 248

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 && ms.Language == criteria.Language);
 115
 0116        if (criteria.IsExternal.HasValue)
 117        {
 0118            var isExternal = criteria.IsExternal.Value;
 0119            query = query.Where(ms => ms.IsExternal == isExternal);
 120        }
 121
 0122        return query.Select(ms => ms.ItemId).Distinct().ToHashSet();
 123    }
 124
 125    /// <summary>
 126    /// Traverses DOWN the hierarchy from parent folders to find all descendants.
 127    /// </summary>
 128    private static HashSet<Guid> TraverseHierarchyDown(JellyfinDbContext context, ICollection<Guid> startIds)
 129    {
 0130        var visited = new HashSet<Guid>(startIds);
 0131        var folderStack = new HashSet<Guid>(startIds);
 132
 0133        while (folderStack.Count != 0)
 134        {
 0135            var currentFolders = folderStack.ToArray();
 0136            folderStack.Clear();
 137
 0138            var directChildren = context.AncestorIds
 0139                .WhereOneOrMany(currentFolders, e => e.ParentItemId)
 0140                .Select(e => e.ItemId)
 0141                .ToArray();
 142
 0143            var linkedChildren = context.LinkedChildren
 0144                .WhereOneOrMany(currentFolders, e => e.ParentId)
 0145                .Select(e => e.ChildId)
 0146                .ToArray();
 147
 0148            var allChildren = directChildren.Concat(linkedChildren).Distinct().ToArray();
 149
 0150            if (allChildren.Length == 0)
 151            {
 152                break;
 153            }
 154
 0155            var childFolders = context.BaseItems
 0156                .WhereOneOrMany(allChildren, e => e.Id)
 0157                .Where(e => e.IsFolder)
 0158                .Select(e => e.Id)
 0159                .ToHashSet();
 160
 0161            foreach (var childId in allChildren)
 162            {
 0163                if (visited.Add(childId) && childFolders.Contains(childId))
 164                {
 0165                    folderStack.Add(childId);
 166                }
 167            }
 168        }
 169
 0170        return visited;
 171    }
 172
 173    /// <summary>
 174    /// Traverses DOWN the hierarchy using only AncestorIds (ownership), not LinkedChildren.
 175    /// </summary>
 176    private static HashSet<Guid> TraverseHierarchyDownOwned(JellyfinDbContext context, ICollection<Guid> startIds)
 177    {
 1178        var visited = new HashSet<Guid>(startIds);
 1179        var folderStack = new HashSet<Guid>(startIds);
 180
 1181        while (folderStack.Count != 0)
 182        {
 1183            var currentFolders = folderStack.ToArray();
 1184            folderStack.Clear();
 185
 1186            var directChildren = context.AncestorIds
 1187                .WhereOneOrMany(currentFolders, e => e.ParentItemId)
 1188                .Select(e => e.ItemId)
 1189                .ToArray();
 190
 1191            if (directChildren.Length == 0)
 192            {
 193                break;
 194            }
 195
 0196            var childFolders = context.BaseItems
 0197                .WhereOneOrMany(directChildren, e => e.Id)
 0198                .Where(e => e.IsFolder)
 0199                .Select(e => e.Id)
 0200                .ToHashSet();
 201
 0202            foreach (var childId in directChildren)
 203            {
 0204                if (visited.Add(childId) && childFolders.Contains(childId))
 205                {
 0206                    folderStack.Add(childId);
 207                }
 208            }
 209        }
 210
 1211        return visited;
 212    }
 213
 214    /// <summary>
 215    /// Traverses UP the hierarchy from items to find all ancestor folders.
 216    /// </summary>
 217    private static HashSet<Guid> TraverseHierarchyUp(JellyfinDbContext context, ICollection<Guid> startIds)
 218    {
 0219        var ancestors = new HashSet<Guid>();
 0220        var itemStack = new HashSet<Guid>(startIds);
 221
 0222        while (itemStack.Count != 0)
 223        {
 0224            var currentItems = itemStack.ToArray();
 0225            itemStack.Clear();
 226
 0227            var ancestorParents = context.AncestorIds
 0228                .WhereOneOrMany(currentItems, e => e.ItemId)
 0229                .Select(e => e.ParentItemId)
 0230                .ToArray();
 231
 0232            var linkedParents = context.LinkedChildren
 0233                .WhereOneOrMany(currentItems, e => e.ChildId)
 0234                .Select(e => e.ParentId)
 0235                .ToArray();
 236
 0237            foreach (var parentId in ancestorParents.Concat(linkedParents))
 238            {
 0239                if (ancestors.Add(parentId))
 240                {
 0241                    itemStack.Add(parentId);
 242                }
 243            }
 244        }
 245
 0246        return ancestors;
 247    }
 248}