diff options
| author | Shadowghost <Ghost_of_Stone@web.de> | 2026-08-22 07:42:56 +0200 |
|---|---|---|
| committer | Shadowghost <Ghost_of_Stone@web.de> | 2026-08-22 07:42:56 +0200 |
| commit | 587f06dccc67a6ad67b43cf8889b6b7605950bcf (patch) | |
| tree | 66deb4bc69c5b6a271ed35f80cdc0afa54dbd145 /src/Jellyfin.Database | |
| parent | 7ad5ff6aa7804855e6805495f6c9ebcf6d459a11 (diff) | |
Multiple fixes and improvements
Co-Authored-By: Cody Robibero <cody@robibe.ro>
Diffstat (limited to 'src/Jellyfin.Database')
| -rw-r--r-- | src/Jellyfin.Database/Jellyfin.Database.Implementations/DescendantQueryHelper.cs | 88 |
1 files changed, 49 insertions, 39 deletions
diff --git a/src/Jellyfin.Database/Jellyfin.Database.Implementations/DescendantQueryHelper.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/DescendantQueryHelper.cs index 5a17a46d9e..6b08f8dd7e 100644 --- a/src/Jellyfin.Database/Jellyfin.Database.Implementations/DescendantQueryHelper.cs +++ b/src/Jellyfin.Database/Jellyfin.Database.Implementations/DescendantQueryHelper.cs @@ -102,20 +102,25 @@ public static class DescendantQueryHelper ArgumentNullException.ThrowIfNull(context); ArgumentNullException.ThrowIfNull(criteria); - var matchingItemIds = GetItemIdsMatching(context, criteria); + // Both sides of a version group can hold a folder a caller would see as matching: the + // alternate carries its own AncestorIds rows and may sit in a different library than the + // primary it is reported against, and the primary is the item that becomes visible. + var reportedItemIds = MatchingMediaOwnerIds(context, criteria) + .Concat(GetPrimaryVersionIdsMatching(context, criteria)) + .Distinct(); // One hop up the closure covers every ancestor level. var hierarchyAncestors = context.AncestorIds - .Where(e => matchingItemIds.Contains(e.ItemId)) + .Where(e => reportedItemIds.Contains(e.ItemId)) .Select(e => e.ParentItemId); - var linkParents = ResolveLinkParents(context, matchingItemIds, hierarchyAncestors); + var linkParents = ResolveLinkParents(context, reportedItemIds, hierarchyAncestors); - // Read back as a sub-select so the result stays composable. LinkedChildren is the cheapest - // source: owning a link is what put an id in the set, and ParentId is its leading key. - var linkedParents = context.LinkedChildren - .WhereOneOrMany(linkParents, e => e.ParentId) - .Select(e => e.ParentId); + // Read back as a sub-select so the result stays composable. Off the primary key, which is one + // row per id: LinkedChildren would yield one row per link and lean on the outer Distinct. + var linkedParents = context.BaseItems + .WhereOneOrMany(linkParents, e => e.Id) + .Select(e => e.Id); var linkedParentAncestors = context.AncestorIds .WhereOneOrMany(linkParents, e => e.ItemId) @@ -135,25 +140,6 @@ public static class DescendantQueryHelper } /// <summary> - /// Gets a queryable of the IDs of the items whose media matches the criteria. - /// </summary> - /// <param name="context">Database context.</param> - /// <param name="criteria">The matching criteria to apply.</param> - /// <returns>Queryable of item IDs.</returns> - /// <remarks> - /// An alternate version is a second file for its primary version and is never listed on its own, so a - /// track only that file carries is reported against the primary: the item a caller can actually see. - /// </remarks> - public static IQueryable<Guid> GetItemIdsMatching(JellyfinDbContext context, FolderMatchCriteria criteria) - { - ArgumentNullException.ThrowIfNull(context); - ArgumentNullException.ThrowIfNull(criteria); - - return MatchingMediaOwners(context, criteria) - .Select(e => e.PrimaryVersionId ?? e.Id); - } - - /// <summary> /// Gets a queryable of the IDs of the primary versions whose alternate version's media matches the /// criteria. /// </summary> @@ -170,23 +156,47 @@ public static class DescendantQueryHelper ArgumentNullException.ThrowIfNull(context); ArgumentNullException.ThrowIfNull(criteria); - return MatchingMediaOwners(context, criteria) - .Where(e => e.PrimaryVersionId.HasValue) - .Select(e => e.PrimaryVersionId!.Value); + // Anchored on the alternates rather than on the matches: "has a primary version" is served by + // the partial PrimaryVersionId index, which holds only the few items that are second files, so + // this costs a seek each into the stream index instead of a second pass over every stream row. + var alternates = context.BaseItems.Where(v => v.PrimaryVersionId.HasValue); + + if (criteria is HasChapterImages) + { + return alternates + .Where(v => context.Chapters.Any(c => c.ItemId.Equals(v.Id) && c.ImagePath != null)) + .Select(v => v.PrimaryVersionId!.Value); + } + + var matchingStreams = MatchingMediaStreams(context, criteria); + + return alternates + .Where(v => matchingStreams.Any(ms => ms.ItemId.Equals(v.Id))) + .Select(v => v.PrimaryVersionId!.Value); } - // The items whose own media matches, as their BaseItems rows so the version group can be read off - // them. One definition of "matches" per criteria, so the projections above cannot drift apart. - private static IQueryable<BaseItemEntity> MatchingMediaOwners(JellyfinDbContext context, FolderMatchCriteria criteria) + // The ids of the items whose own media matches. Kept to the stream and chapter tables so their + // covering indexes answer this outright: projecting the BaseItems navigation instead would add a + // primary-key lookup per stream row rather than one per matching item, and the leading key of both + // indexes leaves the ids already grouped, so the Distinct costs no sort. + private static IQueryable<Guid> MatchingMediaOwnerIds(JellyfinDbContext context, FolderMatchCriteria criteria) + => criteria is HasChapterImages + ? context.Chapters + .Where(c => c.ImagePath != null) + .Select(c => c.ItemId) + .Distinct() + : MatchingMediaStreams(context, criteria) + .Select(ms => ms.ItemId) + .Distinct(); + + // The stream rows a criteria matches. One definition, so the owner projection and the alternate + // projection cannot drift apart despite reading it from opposite ends. + private static IQueryable<MediaStreamInfo> MatchingMediaStreams(JellyfinDbContext context, FolderMatchCriteria criteria) => criteria switch { HasSubtitles => context.MediaStreamInfos - .Where(ms => ms.StreamType == MediaStreamTypeEntity.Subtitle) - .Select(ms => ms.Item), - HasChapterImages => context.Chapters - .Where(c => c.ImagePath != null) - .Select(c => c.Item), - HasMediaStreamType m => GetMatchingMediaStreams(context, m).Select(ms => ms.Item), + .Where(ms => ms.StreamType == MediaStreamTypeEntity.Subtitle), + HasMediaStreamType m => GetMatchingMediaStreams(context, m), _ => throw new ArgumentOutOfRangeException(nameof(criteria), $"Unknown criteria type: {criteria.GetType().Name}") }; |
