diff options
| author | Shadowghost <Ghost_of_Stone@web.de> | 2026-06-07 22:37:34 +0200 |
|---|---|---|
| committer | Shadowghost <Ghost_of_Stone@web.de> | 2026-06-07 23:06:14 +0200 |
| commit | 9ea3f458863f1f1db1a2eb56d2a5799834a64ec9 (patch) | |
| tree | bc0827443cae86c5655fe4da22ed9a1c0f9bc1f3 | |
| parent | e64cc73f881fc9def52f9e362a5094dbdbbcb7fb (diff) | |
Make resume queries version-aware
4 files changed, 45 insertions, 13 deletions
diff --git a/Jellyfin.Api/Controllers/TvShowsController.cs b/Jellyfin.Api/Controllers/TvShowsController.cs index 340a54e13b..6b0f10e02a 100644 --- a/Jellyfin.Api/Controllers/TvShowsController.cs +++ b/Jellyfin.Api/Controllers/TvShowsController.cs @@ -277,8 +277,15 @@ public class TvShowsController : BaseJellyfinApiController if (startItemId.HasValue) { + // The start item may be an alternate version, which is not part of the episode listing; start from its primary episode instead. + var startId = startItemId.Value; + if (_libraryManager.GetItemById<Video>(startId)?.PrimaryVersionId is { } primaryVersionId) + { + startId = primaryVersionId; + } + episodes = episodes - .SkipWhile(i => !startItemId.Value.Equals(i.Id)) + .SkipWhile(i => !startId.Equals(i.Id)) .ToList(); } diff --git a/Jellyfin.Server.Implementations/Item/BaseItemRepository.TranslateQuery.cs b/Jellyfin.Server.Implementations/Item/BaseItemRepository.TranslateQuery.cs index d905775aef..22f7cc3550 100644 --- a/Jellyfin.Server.Implementations/Item/BaseItemRepository.TranslateQuery.cs +++ b/Jellyfin.Server.Implementations/Item/BaseItemRepository.TranslateQuery.cs @@ -513,12 +513,17 @@ public sealed partial class BaseItemRepository if (filter.IsResumable.HasValue) { var hasSeries = filter.IncludeItemTypes.Contains(BaseItemKind.Series); + var userId = filter.User!.Id; + var isResumable = filter.IsResumable.Value; + + // In-progress user data rows; alternate versions track their own progress. + var inProgress = context.UserData + .Where(ud => ud.UserId == userId && ud.PlaybackPositionTicks > 0); + var resumableItemIds = inProgress.Select(ud => ud.ItemId); if (hasSeries) { - var userId = filter.User!.Id; var seriesTypeName = _itemTypeLookup.BaseItemKindNames[BaseItemKind.Series]; - var isResumable = filter.IsResumable.Value; // Aggregate per series in a single GROUP BY pass, instead of three full scans. var seriesEpisodeStats = context.BaseItems @@ -539,23 +544,24 @@ public sealed partial class BaseItemRepository .Where(s => s.HasInProgress || (s.HasPlayed && s.HasUnplayed)) .Select(s => s.SeriesId); - // Non-series items: resumable if PlaybackPositionTicks > 0 - var resumableItemIds = context.UserData - .Where(ud => ud.UserId == userId && ud.PlaybackPositionTicks > 0) - .Select(ud => ud.ItemId); - baseQuery = baseQuery.Where(e => (e.Type == seriesTypeName && resumableSeriesIds.Contains(e.Id) == isResumable) || (e.Type != seriesTypeName && resumableItemIds.Contains(e.Id) == isResumable)); } else { - var resumableItemIds = context.UserData - .Where(ud => ud.UserId == filter.User!.Id && ud.PlaybackPositionTicks > 0) - .Select(ud => ud.ItemId); - var isResumable = filter.IsResumable.Value; baseQuery = baseQuery.Where(e => resumableItemIds.Contains(e.Id) == isResumable); } + + if (isResumable) + { + // Multi-version items surface as the version that was actually played. + // When several versions of the same item are in progress, keep only the most recently played one. + baseQuery = baseQuery.Where(e => !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))); + } } if (filter.ArtistIds.Length > 0) diff --git a/Jellyfin.Server.Implementations/Item/OrderMapper.cs b/Jellyfin.Server.Implementations/Item/OrderMapper.cs index d327b218a9..eeeeda8193 100644 --- a/Jellyfin.Server.Implementations/Item/OrderMapper.cs +++ b/Jellyfin.Server.Implementations/Item/OrderMapper.cs @@ -34,7 +34,10 @@ public static class OrderMapper (ItemSortBy.AirTime, _) => e => e.SortName, (ItemSortBy.Runtime, _) => e => e.RunTimeTicks, (ItemSortBy.Random, _) => e => EF.Functions.Random(), - (ItemSortBy.DatePlayed, _) => e => e.UserData!.Where(f => f.UserId.Equals(query.User!.Id)).OrderBy(f => f.CustomDataKey).FirstOrDefault()!.LastPlayedDate, + (ItemSortBy.DatePlayed, not null) => e => + jellyfinDbContext.UserData + .Where(w => w.UserId == query.User.Id && (w.ItemId == e.Id || w.Item!.PrimaryVersionId == e.Id)) + .Max(f => f.LastPlayedDate), (ItemSortBy.PlayCount, _) => e => e.UserData!.Where(f => f.UserId.Equals(query.User!.Id)).OrderBy(f => f.CustomDataKey).FirstOrDefault()!.PlayCount, (ItemSortBy.IsFavoriteOrLiked, _) => e => e.UserData!.Where(f => f.UserId.Equals(query.User!.Id)).OrderBy(f => f.CustomDataKey).Select(f => (bool?)f.IsFavorite).FirstOrDefault() ?? false, (ItemSortBy.IsFolder, _) => e => e.IsFolder, diff --git a/tests/Jellyfin.Controller.Tests/Entities/BaseItemTests.cs b/tests/Jellyfin.Controller.Tests/Entities/BaseItemTests.cs index 664821815b..c0a2b0ecca 100644 --- a/tests/Jellyfin.Controller.Tests/Entities/BaseItemTests.cs +++ b/tests/Jellyfin.Controller.Tests/Entities/BaseItemTests.cs @@ -137,6 +137,22 @@ public class BaseItemTests } [Fact] + public void GetAllVersions_FromAnyVersion_ReturnsEveryVersionOnce() + { + var (primary, alt1, alt2) = SetupVersionGroup(); + + foreach (var source in new[] { primary, alt1, alt2 }) + { + var versions = source.GetAllVersions(); + + Assert.Equal(3, versions.Count); + Assert.Contains(versions, v => v.Id.Equals(primary.Id)); + Assert.Contains(versions, v => v.Id.Equals(alt1.Id)); + Assert.Contains(versions, v => v.Id.Equals(alt2.Id)); + } + } + + [Fact] public void PropagatePlayedState_MarksAlternateVersions_AndResetsPositionByDefault() { var (primary, alt1, alt2) = SetupVersionGroup(); |
