aboutsummaryrefslogtreecommitdiff
path: root/Jellyfin.Server.Implementations/Item/BaseItemRepository.TranslateQuery.cs
diff options
context:
space:
mode:
authorPiotr Niełacny <piotr.nielacny@gmail.com>2026-08-25 15:19:38 +0200
committerPiotr Niełacny <piotr.nielacny@gmail.com>2026-08-25 15:19:38 +0200
commit5e621d0e3f2102177210f162e72a5d73378063fb (patch)
tree0dc45a48ce637dde27d1c118d798eeae8ea092ca /Jellyfin.Server.Implementations/Item/BaseItemRepository.TranslateQuery.cs
parent422b2bb3d9193bfe0813d3d654919151eb355d82 (diff)
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.
Diffstat (limited to 'Jellyfin.Server.Implementations/Item/BaseItemRepository.TranslateQuery.cs')
-rw-r--r--Jellyfin.Server.Implementations/Item/BaseItemRepository.TranslateQuery.cs38
1 files changed, 22 insertions, 16 deletions
diff --git a/Jellyfin.Server.Implementations/Item/BaseItemRepository.TranslateQuery.cs b/Jellyfin.Server.Implementations/Item/BaseItemRepository.TranslateQuery.cs
index 623c1ea0ab..1e30f0164e 100644
--- a/Jellyfin.Server.Implementations/Item/BaseItemRepository.TranslateQuery.cs
+++ b/Jellyfin.Server.Implementations/Item/BaseItemRepository.TranslateQuery.cs
@@ -35,6 +35,27 @@ public sealed partial class BaseItemRepository
// instance across several lambdas, and this filter is combined into a tree more than once.
private static Expression<Func<BaseItemEntity, bool>> IsFolderFilter => e => e.IsFolder;
+ // 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;
+
+ // Leaf items carry their own played state.
+ 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));
+
+ return IsFolderFilter.And(BuildHasDescendantFilter(context, unplayedLeafItems).Not())
+ .Or(IsFolderFilter.Not().And(e => playedItemIds.Contains(e.Id)));
+ }
+
// "und" is the language filters' stand-in for a track that declares no language at all.
private static string NormalizeLanguage(string language)
=> string.Equals(language, "und", StringComparison.OrdinalIgnoreCase) ? "und" : language;
@@ -523,22 +544,7 @@ public sealed partial class BaseItemRepository
if (filter.IsPlayed.HasValue)
{
- var userId = filter.User!.Id;
-
- // Leaf items carry their own played state.
- 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, filter.User!)
- .Where(e => !e.UserData!.Any(ud => ud.UserId == userId && ud.Played));
-
- var isPlayedFilter = IsFolderFilter.And(BuildHasDescendantFilter(context, unplayedLeafItems).Not())
- .Or(IsFolderFilter.Not().And(e => playedItemIds.Contains(e.Id)));
+ var isPlayedFilter = BuildIsPlayedFilter(context, filter.User!);
baseQuery = baseQuery.Where(filter.IsPlayed.Value ? isPlayedFilter : isPlayedFilter.Not());
}