aboutsummaryrefslogtreecommitdiff
path: root/Jellyfin.Server.Implementations/Item/BaseItemRepository.TranslateQuery.cs
diff options
context:
space:
mode:
Diffstat (limited to 'Jellyfin.Server.Implementations/Item/BaseItemRepository.TranslateQuery.cs')
-rw-r--r--Jellyfin.Server.Implementations/Item/BaseItemRepository.TranslateQuery.cs73
1 files changed, 44 insertions, 29 deletions
diff --git a/Jellyfin.Server.Implementations/Item/BaseItemRepository.TranslateQuery.cs b/Jellyfin.Server.Implementations/Item/BaseItemRepository.TranslateQuery.cs
index 1e30f0164e..e66f18bc6e 100644
--- a/Jellyfin.Server.Implementations/Item/BaseItemRepository.TranslateQuery.cs
+++ b/Jellyfin.Server.Implementations/Item/BaseItemRepository.TranslateQuery.cs
@@ -38,22 +38,32 @@ public sealed partial class BaseItemRepository
// Shared by the isPlayed filter and the IsPlayed/IsUnplayed ordering so the two cannot disagree.
private Expression<Func<BaseItemEntity, bool>> BuildIsPlayedFilter(JellyfinDbContext context, User user)
{
- var userId = user.Id;
+ // Folders (Series, Seasons, BoxSets, albums, ...) carry no played state of their own and count
+ // as played once no descendant is left unplayed.
+ var unplayedLeafItems = GetAccessFilteredLeafItemsQuery(context, user)
+ .Where(BuildLeafIsPlayedFilter(context, user.Id).Not());
- // Leaf items carry their own played state.
+ return IsFolderFilter.And(BuildHasDescendantFilter(context, unplayedLeafItems).Not())
+ .Or(IsFolderFilter.Not().And(BuildLeafIsPlayedFilter(context, user.Id)));
+ }
+
+ private static Expression<Func<BaseItemEntity, bool>> BuildLeafIsPlayedFilter(JellyfinDbContext context, Guid userId)
+ {
var playedItemIds = context.UserData
.Where(ud => ud.UserId == userId && ud.Played)
.Select(ud => ud.ItemId);
- // Folders (Series, Seasons, BoxSets, albums, ...) have none and count as played once no
- // descendant is left unplayed, matching what the DTO reports for them. This has to key off
- // the item itself rather than off the requested item types: tag and collection listings mix
- // folders and leaf items in a single query.
- var unplayedLeafItems = GetAccessFilteredLeafItemsQuery(context, user)
- .Where(e => !e.UserData!.Any(ud => ud.UserId == userId && ud.Played));
+ // The primaries of every version group holding a played row, whichever version carries it.
+ var playedGroupIds = context.BaseItems
+ .Where(v => v.PrimaryVersionId != null
+ && context.UserData.Any(ud => ud.UserId == userId
+ && ud.Played
+ && (ud.ItemId == v.Id || ud.ItemId == v.PrimaryVersionId)))
+ .Select(v => v.PrimaryVersionId!.Value);
- return IsFolderFilter.And(BuildHasDescendantFilter(context, unplayedLeafItems).Not())
- .Or(IsFolderFilter.Not().And(e => playedItemIds.Contains(e.Id)));
+ return e => playedItemIds.Contains(e.Id)
+ || playedGroupIds.Contains(e.Id)
+ || (e.PrimaryVersionId != null && playedGroupIds.Contains(e.PrimaryVersionId.Value));
}
// "und" is the language filters' stand-in for a track that declares no language at all.
@@ -466,16 +476,8 @@ public sealed partial class BaseItemRepository
if (!string.IsNullOrWhiteSpace(filter.Name))
{
- if (filter.UseRawName == true)
- {
- var nameLower = filter.Name.ToLowerInvariant();
- baseQuery = baseQuery.Where(e => e.Name!.ToLower() == nameLower);
- }
- else
- {
- var cleanName = filter.Name.GetCleanValue();
- baseQuery = baseQuery.Where(e => e.CleanName == cleanName);
- }
+ var cleanName = filter.Name.GetCleanValue();
+ baseQuery = baseQuery.Where(e => e.CleanName == cleanName);
}
var nameContains = filter.NameContains;
@@ -571,8 +573,8 @@ public sealed partial class BaseItemRepository
.ToArray();
var folderIsResumableFilter = IsFolderFilter.And(e => resumableFolderTypes.Contains(e.Type))
.And(BuildHasDescendantFilter(context, inProgressLeafItems)
- .Or(BuildHasDescendantFilter(context, leafItems.Where(e => e.UserData!.Any(ud => ud.UserId == userId && ud.Played)))
- .And(BuildHasDescendantFilter(context, leafItems.Where(e => !e.UserData!.Any(ud => ud.UserId == userId && ud.Played))))));
+ .Or(BuildHasDescendantFilter(context, leafItems.Where(BuildLeafIsPlayedFilter(context, userId)))
+ .And(BuildHasDescendantFilter(context, leafItems.Where(BuildLeafIsPlayedFilter(context, userId).Not())))));
if (isResumable)
{
@@ -797,11 +799,16 @@ public sealed partial class BaseItemRepository
{
// Exclude owned non-extra items from general queries.
// Extras (trailers, etc.) have OwnerId set but also have ExtraType set - keep those.
- // Alternate versions (PrimaryVersionId set) are normally excluded too, but resume queries
- // keep them so the actually-played version can surface instead of collapsing onto the primary.
- baseQuery = filter.IsResumable == true
- ? baseQuery.Where(e => e.OwnerId == null || e.ExtraType != null)
- : baseQuery.Where(e => e.PrimaryVersionId == null && (e.OwnerId == null || e.ExtraType != null));
+ baseQuery = baseQuery.Where(e => e.OwnerId == null || e.ExtraType != null);
+
+ // Alternate versions (PrimaryVersionId set) are normally hidden behind their primary, but
+ // resume queries keep them so the actually-played version can surface instead of collapsing
+ // onto the primary, and the library scan keeps them so a merged version is not mistaken for
+ // a new item.
+ if (filter.IsResumable != true && !filter.IncludeAlternateVersions)
+ {
+ baseQuery = ApplyAlternateVersionFiltering(context, baseQuery);
+ }
}
if (filter.OwnerIds.Length > 0)
@@ -1091,6 +1098,12 @@ public sealed partial class BaseItemRepository
baseQuery = baseQuery.Where(e => e.Parents!.AsQueryable().Any(ancestorFilter));
}
+ if (filter.DescendantOfId.HasValue)
+ {
+ var descendantIds = DescendantQueryHelper.GetAllDescendantIds(context, filter.DescendantOfId.Value);
+ baseQuery = baseQuery.Where(e => descendantIds.Contains(e.Id));
+ }
+
if (filter.LinkedChildAncestorIds.Length > 0)
{
// Keep folder-like items (BoxSets, Playlists) whose linked children descend from any of the requested ancestor ids.
@@ -1119,11 +1132,12 @@ public sealed partial class BaseItemRepository
var blockedTagItemIds = context.ItemValuesMap
.Where(f => f.ItemValue.Type == ItemValueType.Tags && excludedTags.Contains(f.ItemValue.CleanValue))
.Select(f => f.ItemId);
+ var blockedByAncestor = ItemsBelowTaggedAncestor(context, blockedTagItemIds);
baseQuery = baseQuery.Where(e =>
!blockedTagItemIds.Contains(e.Id)
&& !(e.SeriesId.HasValue && blockedTagItemIds.Contains(e.SeriesId.Value))
- && !e.Parents!.Any(p => blockedTagItemIds.Contains(p.ParentItemId))
+ && !blockedByAncestor.Contains(e.Id)
&& !(e.TopParentId.HasValue && blockedTagItemIds.Contains(e.TopParentId.Value)));
}
@@ -1135,11 +1149,12 @@ public sealed partial class BaseItemRepository
var allowedTagItemIds = context.ItemValuesMap
.Where(f => f.ItemValue.Type == ItemValueType.Tags && includeTags.Contains(f.ItemValue.CleanValue))
.Select(f => f.ItemId);
+ var allowedByAncestor = ItemsBelowTaggedAncestor(context, allowedTagItemIds);
baseQuery = baseQuery.Where(e =>
allowedTagItemIds.Contains(e.Id)
|| (e.SeriesId.HasValue && allowedTagItemIds.Contains(e.SeriesId.Value))
- || e.Parents!.Any(p => allowedTagItemIds.Contains(p.ParentItemId))
+ || allowedByAncestor.Contains(e.Id)
|| (e.TopParentId.HasValue && allowedTagItemIds.Contains(e.TopParentId.Value))
// People don't carry the tags of the media they appear in and would never match