mirror of
https://github.com/jellyfin/jellyfin.git
synced 2026-06-09 01:08:45 +01:00
Fix version-aware resume
This commit is contained in:
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -136,6 +136,22 @@ public class BaseItemTests
|
||||
Assert.Null(primary.GetAlternateVersion(Guid.NewGuid()));
|
||||
}
|
||||
|
||||
[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()
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user