From 5e621d0e3f2102177210f162e72a5d73378063fb Mon Sep 17 00:00:00 2001 From: Piotr Niełacny Date: Tue, 25 Aug 2026 15:19:38 +0200 Subject: Order IsPlayed and IsUnplayed by the played state the filter reports Ordering mapped both keys to the item's own stored UserData row. Folders do not have one: a series, season or box set counts as played when no descendant is left unplayed, which is what the isPlayed filter and the DTO both report. A mixed library therefore sorted every series and box set into the unplayed group, and a query could filter and sort by two different notions of "played". Extract the filter's predicate into BuildIsPlayedFilter and route both sort keys through it so the two cannot drift apart again. --- .../Item/BaseItemRepository.QueryBuilding.cs | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) (limited to 'Jellyfin.Server.Implementations/Item/BaseItemRepository.QueryBuilding.cs') diff --git a/Jellyfin.Server.Implementations/Item/BaseItemRepository.QueryBuilding.cs b/Jellyfin.Server.Implementations/Item/BaseItemRepository.QueryBuilding.cs index 05ff720ddf..c0067d8392 100644 --- a/Jellyfin.Server.Implementations/Item/BaseItemRepository.QueryBuilding.cs +++ b/Jellyfin.Server.Implementations/Item/BaseItemRepository.QueryBuilding.cs @@ -12,6 +12,7 @@ using Jellyfin.Database.Implementations; using Jellyfin.Database.Implementations.Entities; using Jellyfin.Database.Implementations.Enums; using Jellyfin.Extensions; +using Jellyfin.Server.Implementations.Extensions; using MediaBrowser.Controller.Entities; using MediaBrowser.Model.Entities; using MediaBrowser.Model.Querying; @@ -323,10 +324,21 @@ public sealed partial class BaseItemRepository orderedQuery = query.OrderBy(relevanceExpression); } + // Folders carry no played flag of their own, so these two keys go through the same predicate + // the isPlayed filter uses rather than through the stored-column lookup in OrderMapper. + Expression> MapOrderByField(ItemSortBy sortBy) => sortBy switch + { + ItemSortBy.IsPlayed when filter.User is not null + => AsOrderKey(BuildIsPlayedFilter(context, filter.User)), + ItemSortBy.IsUnplayed when filter.User is not null + => AsOrderKey(BuildIsPlayedFilter(context, filter.User).Not()), + _ => OrderMapper.MapOrderByField(sortBy, filter, context) + }; + if (orderBy.Length > 0) { var firstOrdering = orderBy[0]; - var expression = OrderMapper.MapOrderByField(firstOrdering.OrderBy, filter, context); + var expression = MapOrderByField(firstOrdering.OrderBy); if (orderedQuery is null) { @@ -350,7 +362,7 @@ public sealed partial class BaseItemRepository foreach (var item in orderBy.Skip(1)) { - expression = OrderMapper.MapOrderByField(item.OrderBy, filter, context); + expression = MapOrderByField(item.OrderBy); orderedQuery = item.SortOrder == SortOrder.Ascending ? orderedQuery.ThenBy(expression) : orderedQuery.ThenByDescending(expression); @@ -666,6 +678,9 @@ public sealed partial class BaseItemRepository return ApplyAccessFiltering(context, leafItems, new InternalItemsQuery(user) { IncludeOwnedItems = includeOwnedItems }); } + private static Expression> AsOrderKey(Expression> predicate) + => Expression.Lambda>(Expression.Convert(predicate.Body, typeof(object)), predicate.Parameters); + /// public Expression> BuildHasDescendantFilter(JellyfinDbContext context, IQueryable descendants) { -- cgit v1.2.3