diff options
Diffstat (limited to 'Jellyfin.Server.Implementations')
| -rw-r--r-- | Jellyfin.Server.Implementations/Item/BaseItemRepository.TranslateQuery.cs | 59 | ||||
| -rw-r--r-- | Jellyfin.Server.Implementations/Item/BaseItemRepository.cs | 8 |
2 files changed, 40 insertions, 27 deletions
diff --git a/Jellyfin.Server.Implementations/Item/BaseItemRepository.TranslateQuery.cs b/Jellyfin.Server.Implementations/Item/BaseItemRepository.TranslateQuery.cs index 96d6d2eaff..c234f333af 100644 --- a/Jellyfin.Server.Implementations/Item/BaseItemRepository.TranslateQuery.cs +++ b/Jellyfin.Server.Implementations/Item/BaseItemRepository.TranslateQuery.cs @@ -515,15 +515,15 @@ public sealed partial class BaseItemRepository var hasSeries = filter.IncludeItemTypes.Contains(BaseItemKind.Series); var userId = filter.User!.Id; var isResumable = filter.IsResumable.Value; + var seriesTypeName = _itemTypeLookup.BaseItemKindNames[BaseItemKind.Series]; // In-progress user data rows; alternate versions track their own progress. var inProgress = context.UserData .Where(ud => ud.UserId == userId && ud.PlaybackPositionTicks > 0); + IQueryable<Guid>? resumableSeriesIds = null; if (hasSeries) { - var seriesTypeName = _itemTypeLookup.BaseItemKindNames[BaseItemKind.Series]; - // Aggregate per series in a single GROUP BY pass, instead of three full scans. var seriesEpisodeStats = context.BaseItems .AsNoTracking() @@ -539,39 +539,41 @@ public sealed partial class BaseItemRepository // A series is resumable if it has an in-progress episode, // or if it has both played and unplayed episodes (partially watched). - var resumableSeriesIds = seriesEpisodeStats + resumableSeriesIds = seriesEpisodeStats .Where(s => s.HasInProgress || (s.HasPlayed && s.HasUnplayed)) .Select(s => s.SeriesId); - - // Non-series items: resumable if the item or any of its alternate versions has - // PlaybackPositionTicks > 0. Alternate versions (PrimaryVersionId set) are excluded - // from the base query, so coalesce their progress onto the primary's id. - var resumableMovieIds = inProgress - .Join(context.BaseItems, ud => ud.ItemId, bi => bi.Id, (ud, bi) => bi.PrimaryVersionId ?? bi.Id); - - baseQuery = baseQuery.Where(e => - (e.Type == seriesTypeName && resumableSeriesIds.Contains(e.Id) == isResumable) - || (e.Type != seriesTypeName && resumableMovieIds.Contains(e.Id) == isResumable)); - } - else - { - // Resumable if the item or any of its alternate versions has PlaybackPositionTicks > 0. - // Alternate versions (PrimaryVersionId set) are excluded from the base query, so - // coalesce their progress onto the primary's id. - var resumableMovieIds = inProgress - .Join(context.BaseItems, ud => ud.ItemId, bi => bi.Id, (ud, bi) => bi.PrimaryVersionId ?? bi.Id); - baseQuery = baseQuery.Where(e => resumableMovieIds.Contains(e.Id) == isResumable); } if (isResumable) { - // Multi-version items surface as the version that was actually played. + // Resume queries surface the version that was actually played, which may be an alternate. + // Match each version on its own progress rather than coalescing onto the primary. + var inProgressIds = inProgress.Select(ud => ud.ItemId); + + baseQuery = hasSeries + ? baseQuery.Where(e => + (e.Type == seriesTypeName && resumableSeriesIds!.Contains(e.Id)) + || (e.Type != seriesTypeName && inProgressIds.Contains(e.Id))) + : baseQuery.Where(e => inProgressIds.Contains(e.Id)); + // When several versions of the same item are in progress, keep only the most recently played one. - baseQuery = baseQuery.Where(e => !context.BaseItems + baseQuery = baseQuery.Where(e => e.Type == seriesTypeName || !context.BaseItems .Where(s => s.Id != e.Id && (s.PrimaryVersionId ?? s.Id) == (e.PrimaryVersionId ?? e.Id)) .Any(s => inProgress.Where(su => su.ItemId == s.Id).Max(su => su.LastPlayedDate) > inProgress.Where(eu => eu.ItemId == e.Id).Max(eu => eu.LastPlayedDate))); } + else + { + // Not-resumable queries operate on primaries only. + var resumableMovieIds = inProgress + .Join(context.BaseItems, ud => ud.ItemId, bi => bi.Id, (ud, bi) => bi.PrimaryVersionId ?? bi.Id); + + baseQuery = hasSeries + ? baseQuery.Where(e => + (e.Type == seriesTypeName && !resumableSeriesIds!.Contains(e.Id)) + || (e.Type != seriesTypeName && !resumableMovieIds.Contains(e.Id))) + : baseQuery.Where(e => !resumableMovieIds.Contains(e.Id)); + } } if (filter.ArtistIds.Length > 0) @@ -758,10 +760,13 @@ public sealed partial class BaseItemRepository } else if (filter.OwnerIds.Length == 0 && filter.ExtraTypes.Length == 0 && !filter.IncludeOwnedItems) { - // Exclude alternate versions and owned non-extra items from general queries. - // Alternate versions have PrimaryVersionId set (pointing to their primary). + // Exclude owned non-extra items from general queries. // Extras (trailers, etc.) have OwnerId set but also have ExtraType set - keep those. - baseQuery = baseQuery.Where(e => e.PrimaryVersionId == null && (e.OwnerId == null || e.ExtraType != null)); + // 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)); } if (filter.OwnerIds.Length > 0) diff --git a/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs b/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs index 94dedaeba8..57041276b7 100644 --- a/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs +++ b/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs @@ -167,6 +167,14 @@ public sealed partial class BaseItemRepository return false; } + // Resume queries surface the actually-played version (which may be an alternate sharing the + // primary's presentation key). The resumable filter already keeps one version per group, so + // presentation-key grouping must not collapse the surfaced version back onto the primary. + if (query.IsResumable == true) + { + return false; + } + if (query.GroupBySeriesPresentationUniqueKey) { return false; |
