diff --git a/Jellyfin.Server.Implementations/Item/FolderAwareFilterExtensions.cs b/Jellyfin.Server.Implementations/Item/FolderAwareFilterExtensions.cs
new file mode 100644
index 0000000000..c63d99d54d
--- /dev/null
+++ b/Jellyfin.Server.Implementations/Item/FolderAwareFilterExtensions.cs
@@ -0,0 +1,65 @@
+using System;
+using System.Linq;
+using System.Linq.Expressions;
+using Jellyfin.Database.Implementations;
+using Jellyfin.Database.Implementations.Entities;
+
+namespace Jellyfin.Server.Implementations.Item;
+
+///
+/// Extension methods for applying folder-aware filters that check items and their descendants.
+///
+internal static class FolderAwareFilterExtensions
+{
+ ///
+ /// Filters items where either the item matches the condition (for non-folders)
+ /// or any descendant matches (for folders). Uses reverse traversal through AncestorIds.
+ ///
+ /// The query to filter.
+ /// The database context.
+ /// The condition to check on BaseItemEntity.
+ /// Filtered query.
+ public static IQueryable WhereItemOrDescendantMatches(
+ this IQueryable query,
+ JellyfinDbContext context,
+ Expression> condition)
+ {
+ var matchingIds = context.BaseItems.Where(condition).Select(b => b.Id);
+ var foldersWithMatchingDescendants = context.AncestorIds
+ .Where(a => matchingIds.Contains(a.ItemId))
+ .Select(a => a.ParentItemId)
+ .Union(context.LinkedChildren
+ .Where(lc => matchingIds.Contains(lc.ChildId))
+ .Select(lc => lc.ParentId));
+
+ return query.Where(e =>
+ matchingIds.Contains(e.Id)
+ || foldersWithMatchingDescendants.Contains(e.Id));
+ }
+
+ ///
+ /// Filters items where neither the item matches the condition (for non-folders)
+ /// nor any descendant matches (for folders). Uses reverse traversal for infinite depth.
+ ///
+ /// The query to filter.
+ /// The database context.
+ /// The condition that should NOT match.
+ /// Filtered query.
+ public static IQueryable WhereNeitherItemNorDescendantMatches(
+ this IQueryable query,
+ JellyfinDbContext context,
+ Expression> condition)
+ {
+ var matchingIds = context.BaseItems.Where(condition).Select(b => b.Id);
+ var foldersWithMatchingDescendants = context.AncestorIds
+ .Where(a => matchingIds.Contains(a.ItemId))
+ .Select(a => a.ParentItemId)
+ .Union(context.LinkedChildren
+ .Where(lc => matchingIds.Contains(lc.ChildId))
+ .Select(lc => lc.ParentId));
+
+ return query.Where(e =>
+ !matchingIds.Contains(e.Id)
+ && !foldersWithMatchingDescendants.Contains(e.Id));
+ }
+}
diff --git a/src/Jellyfin.Database/Jellyfin.Database.Implementations/IDescendantQueryProvider.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/IDescendantQueryProvider.cs
new file mode 100644
index 0000000000..9e3d510b9c
--- /dev/null
+++ b/src/Jellyfin.Database/Jellyfin.Database.Implementations/IDescendantQueryProvider.cs
@@ -0,0 +1,30 @@
+using System;
+using System.Linq;
+using Jellyfin.Database.Implementations.MatchCriteria;
+
+namespace Jellyfin.Database.Implementations;
+
+///
+/// Provider interface for descendant queries using recursive CTEs.
+/// Each database provider implements this with provider-specific SQL.
+///
+public interface IDescendantQueryProvider
+{
+ ///
+ /// Gets a queryable of all descendant IDs for a parent item.
+ /// Uses recursive CTE to traverse AncestorIds and LinkedChildren infinitely.
+ ///
+ /// Database context.
+ /// Parent item ID.
+ /// Queryable of descendant item IDs.
+ IQueryable GetAllDescendantIds(JellyfinDbContext context, Guid parentId);
+
+ ///
+ /// Gets a queryable of all folder IDs that have any descendant matching the specified criteria.
+ /// Uses recursive CTE for infinite depth traversal. Can be used in LINQ .Contains() expressions.
+ ///
+ /// Database context.
+ /// The matching criteria to apply.
+ /// Queryable of folder IDs.
+ IQueryable GetFolderIdsMatching(JellyfinDbContext context, FolderMatchCriteria criteria);
+}
diff --git a/src/Jellyfin.Database/Jellyfin.Database.Implementations/MatchCriteria/FolderMatchCriteria.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/MatchCriteria/FolderMatchCriteria.cs
new file mode 100644
index 0000000000..d9f2d91806
--- /dev/null
+++ b/src/Jellyfin.Database/Jellyfin.Database.Implementations/MatchCriteria/FolderMatchCriteria.cs
@@ -0,0 +1,6 @@
+namespace Jellyfin.Database.Implementations.MatchCriteria;
+
+///
+/// Base type for folder matching criteria using discriminated union pattern.
+///
+public abstract record FolderMatchCriteria;
diff --git a/src/Jellyfin.Database/Jellyfin.Database.Implementations/MatchCriteria/HasChapterImages.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/MatchCriteria/HasChapterImages.cs
new file mode 100644
index 0000000000..3dd84bbd27
--- /dev/null
+++ b/src/Jellyfin.Database/Jellyfin.Database.Implementations/MatchCriteria/HasChapterImages.cs
@@ -0,0 +1,6 @@
+namespace Jellyfin.Database.Implementations.MatchCriteria;
+
+///
+/// Matches folders containing descendants with chapter images.
+///
+public sealed record HasChapterImages : FolderMatchCriteria;
diff --git a/src/Jellyfin.Database/Jellyfin.Database.Implementations/MatchCriteria/HasMediaStreamType.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/MatchCriteria/HasMediaStreamType.cs
new file mode 100644
index 0000000000..68f2ca2786
--- /dev/null
+++ b/src/Jellyfin.Database/Jellyfin.Database.Implementations/MatchCriteria/HasMediaStreamType.cs
@@ -0,0 +1,14 @@
+using Jellyfin.Database.Implementations.Entities;
+
+namespace Jellyfin.Database.Implementations.MatchCriteria;
+
+///
+/// Matches folders containing descendants with a specific media stream type and language.
+///
+/// The type of media stream to match (Audio, Subtitle, etc.).
+/// The language to match.
+/// If not null, filters by internal (false) or external (true) streams. Only applicable to subtitles.
+public sealed record HasMediaStreamType(
+ MediaStreamTypeEntity StreamType,
+ string Language,
+ bool? IsExternal = null) : FolderMatchCriteria;
diff --git a/src/Jellyfin.Database/Jellyfin.Database.Implementations/MatchCriteria/HasSubtitles.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/MatchCriteria/HasSubtitles.cs
new file mode 100644
index 0000000000..e50b9f3e12
--- /dev/null
+++ b/src/Jellyfin.Database/Jellyfin.Database.Implementations/MatchCriteria/HasSubtitles.cs
@@ -0,0 +1,6 @@
+namespace Jellyfin.Database.Implementations.MatchCriteria;
+
+///
+/// Matches folders containing descendants with subtitles.
+///
+public sealed record HasSubtitles : FolderMatchCriteria;
diff --git a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/SqliteDescendantQueryProvider.cs b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/SqliteDescendantQueryProvider.cs
new file mode 100644
index 0000000000..756f750bf9
--- /dev/null
+++ b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/SqliteDescendantQueryProvider.cs
@@ -0,0 +1,129 @@
+using System;
+using System.Linq;
+using Jellyfin.Database.Implementations;
+using Jellyfin.Database.Implementations.Entities;
+using Jellyfin.Database.Implementations.MatchCriteria;
+using Microsoft.EntityFrameworkCore;
+
+namespace Jellyfin.Database.Providers.Sqlite;
+
+///
+/// SQLite implementation of descendant queries using optimized ancestor lookups.
+/// Uses AncestorIds and LinkedChildren tables for efficient parent-child traversal.
+///
+public class SqliteDescendantQueryProvider : IDescendantQueryProvider
+{
+ ///
+ /// Recursive CTE fragment that traverses UP the tree from matching items to find all ancestor folders.
+ /// Expects a preceding CTE named "MatchingItems" with an ItemId column.
+ ///
+ private const string AllAncestorsCte = """
+ AllAncestors AS (
+ SELECT a.ParentItemId AS AncestorId
+ FROM AncestorIds a
+ WHERE a.ItemId IN (SELECT ItemId FROM MatchingItems)
+ UNION
+ SELECT lc.ParentId AS AncestorId
+ FROM LinkedChildren lc
+ WHERE lc.ChildId IN (SELECT ItemId FROM MatchingItems)
+ UNION
+ SELECT a.ParentItemId AS AncestorId
+ FROM AllAncestors aa
+ INNER JOIN AncestorIds a ON a.ItemId = aa.AncestorId
+ UNION
+ SELECT lc.ParentId AS AncestorId
+ FROM AllAncestors aa
+ INNER JOIN LinkedChildren lc ON lc.ChildId = aa.AncestorId
+ )
+ SELECT DISTINCT AncestorId AS Value FROM AllAncestors
+ """;
+
+ ///
+ public IQueryable GetAllDescendantIds(JellyfinDbContext context, Guid parentId)
+ {
+ ArgumentNullException.ThrowIfNull(context);
+
+ var sql = """
+ WITH RECURSIVE AllDescendants AS (
+ SELECT ItemId FROM AncestorIds WHERE ParentItemId = {0}
+ UNION
+ SELECT ChildId AS ItemId FROM LinkedChildren WHERE ParentId = {0}
+ UNION ALL
+ SELECT a.ItemId
+ FROM AllDescendants d
+ INNER JOIN BaseItems b ON b.Id = d.ItemId AND b.IsFolder = 1
+ INNER JOIN AncestorIds a ON a.ParentItemId = d.ItemId
+ UNION ALL
+ SELECT lc.ChildId AS ItemId
+ FROM AllDescendants d
+ INNER JOIN BaseItems b ON b.Id = d.ItemId AND b.IsFolder = 1
+ INNER JOIN LinkedChildren lc ON lc.ParentId = d.ItemId
+ )
+ SELECT DISTINCT ItemId AS Value FROM AllDescendants
+ """;
+
+ return context.Database.SqlQueryRaw(sql, parentId);
+ }
+
+ ///
+ public IQueryable GetFolderIdsMatching(JellyfinDbContext context, FolderMatchCriteria criteria)
+ {
+ ArgumentNullException.ThrowIfNull(context);
+ ArgumentNullException.ThrowIfNull(criteria);
+
+ return criteria switch
+ {
+ HasSubtitles => GetFolderIdsWithSubtitles(context),
+ HasChapterImages => GetFolderIdsWithChapterImages(context),
+ HasMediaStreamType m => GetFolderIdsWithMediaStream(context, m.StreamType, m.Language, m.IsExternal),
+ _ => throw new ArgumentOutOfRangeException(nameof(criteria), $"Unknown criteria type: {criteria.GetType().Name}")
+ };
+ }
+
+ private IQueryable GetFolderIdsWithSubtitles(JellyfinDbContext context)
+ {
+ var sql = $"""
+ WITH RECURSIVE MatchingItems AS (
+ SELECT DISTINCT ms.ItemId FROM MediaStreamInfos ms WHERE ms.StreamType = 2
+ ),
+ {AllAncestorsCte}
+ """;
+
+ return context.Database.SqlQueryRaw(sql);
+ }
+
+ private IQueryable GetFolderIdsWithChapterImages(JellyfinDbContext context)
+ {
+ var sql = $"""
+ WITH RECURSIVE MatchingItems AS (
+ SELECT DISTINCT c.ItemId FROM Chapters c WHERE c.ImagePath IS NOT NULL
+ ),
+ {AllAncestorsCte}
+ """;
+
+ return context.Database.SqlQueryRaw(sql);
+ }
+
+ private IQueryable GetFolderIdsWithMediaStream(JellyfinDbContext context, MediaStreamTypeEntity streamType, string language, bool? isExternal)
+ {
+ ArgumentNullException.ThrowIfNull(language);
+
+ var streamTypeInt = (int)streamType;
+ var externalCondition = isExternal switch
+ {
+ true => " AND ms.IsExternal = 1",
+ false => " AND ms.IsExternal = 0",
+ null => string.Empty
+ };
+
+ var sql = $$"""
+ WITH RECURSIVE MatchingItems AS (
+ SELECT DISTINCT ms.ItemId FROM MediaStreamInfos ms
+ WHERE ms.StreamType = {0} AND ms.Language = {1}{{externalCondition}}
+ ),
+ {{AllAncestorsCte}}
+ """;
+
+ return context.Database.SqlQueryRaw(sql, streamTypeInt, language);
+ }
+}