From fd016cfb13266dc67f0ce5982e6d4965d8fbd273 Mon Sep 17 00:00:00 2001 From: Shadowghost Date: Sun, 6 Sep 2026 17:19:36 +0200 Subject: Share played state across alternate versions --- .../Item/BaseItemRepository.TranslateQuery.cs | 30 ++++++++++++++-------- 1 file changed, 20 insertions(+), 10 deletions(-) (limited to 'Jellyfin.Server.Implementations') diff --git a/Jellyfin.Server.Implementations/Item/BaseItemRepository.TranslateQuery.cs b/Jellyfin.Server.Implementations/Item/BaseItemRepository.TranslateQuery.cs index d635b38df5..a745c3309f 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> 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()); + + return IsFolderFilter.And(BuildHasDescendantFilter(context, unplayedLeafItems).Not()) + .Or(IsFolderFilter.Not().And(BuildLeafIsPlayedFilter(context, user.Id))); + } - // Leaf items carry their own played state. + private static Expression> 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. -- cgit v1.2.3