mirror of
https://github.com/jellyfin/jellyfin.git
synced 2026-07-06 14:22:55 +01:00
Coalesce alternate-version progress onto primary in resume filter
This commit is contained in:
@@ -167,9 +167,13 @@ namespace Emby.Server.Implementations.Dto
|
||||
|
||||
// Batch-fetch user data for all items
|
||||
Dictionary<Guid, UserItemData>? userDataBatch = null;
|
||||
IReadOnlyDictionary<Guid, VersionResumeData>? resumeDataBatch = null;
|
||||
if (user is not null && options.EnableUserData)
|
||||
{
|
||||
userDataBatch = _userDataRepository.GetUserDataBatch(accessibleItems, user);
|
||||
|
||||
// For items with alternate versions, the most recently played version drives resume.
|
||||
resumeDataBatch = _userDataRepository.GetResumeUserDataBatch(accessibleItems, user);
|
||||
}
|
||||
|
||||
// Pre-compute collection folders once to avoid N+1 queries in CanDelete
|
||||
@@ -248,7 +252,8 @@ namespace Emby.Server.Implementations.Dto
|
||||
allCollectionFolders,
|
||||
childCountBatch,
|
||||
playedCountBatch,
|
||||
artistsBatch);
|
||||
artistsBatch,
|
||||
resumeDataBatch?.GetValueOrDefault(item.Id));
|
||||
|
||||
if (item is LiveTvChannel tvChannel)
|
||||
{
|
||||
@@ -309,7 +314,8 @@ namespace Emby.Server.Implementations.Dto
|
||||
List<Folder>? allCollectionFolders = null,
|
||||
Dictionary<Guid, int>? childCountBatch = null,
|
||||
Dictionary<Guid, (int Played, int Total)>? playedCountBatch = null,
|
||||
IReadOnlyDictionary<string, MusicArtist[]>? artistsBatch = null)
|
||||
IReadOnlyDictionary<string, MusicArtist[]>? artistsBatch = null,
|
||||
VersionResumeData? resumeData = null)
|
||||
{
|
||||
var dto = new BaseItemDto
|
||||
{
|
||||
@@ -353,7 +359,8 @@ namespace Emby.Server.Implementations.Dto
|
||||
options,
|
||||
userData,
|
||||
childCountBatch,
|
||||
playedCountBatch);
|
||||
playedCountBatch,
|
||||
resumeData);
|
||||
}
|
||||
|
||||
if (item is IHasMediaSources
|
||||
@@ -538,7 +545,8 @@ namespace Emby.Server.Implementations.Dto
|
||||
DtoOptions options,
|
||||
UserItemData? userData = null,
|
||||
Dictionary<Guid, int>? childCountBatch = null,
|
||||
Dictionary<Guid, (int Played, int Total)>? playedCountBatch = null)
|
||||
Dictionary<Guid, (int Played, int Total)>? playedCountBatch = null,
|
||||
VersionResumeData? resumeData = null)
|
||||
{
|
||||
if (item.IsFolder)
|
||||
{
|
||||
@@ -600,6 +608,9 @@ namespace Emby.Server.Implementations.Dto
|
||||
// Use pre-fetched user data
|
||||
dto.UserData = GetUserItemDataDto(userData, item.Id);
|
||||
item.FillUserDataDtoValues(dto.UserData, userData, dto, user, options);
|
||||
|
||||
// For items with alternate versions, the most recently played version drives resume.
|
||||
resumeData?.ApplyTo(dto.UserData);
|
||||
}
|
||||
else
|
||||
{
|
||||
|
||||
@@ -229,7 +229,11 @@ namespace Emby.Server.Implementations.Library
|
||||
list.Add(source);
|
||||
}
|
||||
|
||||
return SortMediaSources(list).ToArray();
|
||||
var preferredId = mediaSources.Count > 0 && Guid.TryParse(mediaSources[0].Id, out var topSourceId)
|
||||
? topSourceId
|
||||
: item.Id;
|
||||
|
||||
return SortMediaSources(list, preferredId).ToArray();
|
||||
}
|
||||
|
||||
/// <inheritdoc />>
|
||||
@@ -400,6 +404,72 @@ namespace Emby.Server.Implementations.Library
|
||||
source.SupportsDirectStream = user.HasPermission(PermissionKind.EnablePlaybackRemuxing);
|
||||
}
|
||||
}
|
||||
|
||||
sources = SetAlternateVersionResumeStates(item, sources, user);
|
||||
}
|
||||
|
||||
return sources;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Populates each source's own playback position for the user and, when the queried item is a
|
||||
/// primary, moves the most recently played version to the front so that resuming without an
|
||||
/// explicit source selection plays the version that was last watched. A directly queried
|
||||
/// alternate version keeps its own source first.
|
||||
/// </summary>
|
||||
/// <param name="item">The queried item.</param>
|
||||
/// <param name="sources">The item's media sources.</param>
|
||||
/// <param name="user">The user.</param>
|
||||
/// <returns>The media sources, reordered when a version drives resume.</returns>
|
||||
private IReadOnlyList<MediaSourceInfo> SetAlternateVersionResumeStates(BaseItem item, IReadOnlyList<MediaSourceInfo> sources, User user)
|
||||
{
|
||||
// For a video, multiple sources means alternate versions.
|
||||
if (item is not Video video || sources.Count < 2)
|
||||
{
|
||||
return sources;
|
||||
}
|
||||
|
||||
var versions = video.GetAllVersions();
|
||||
if (versions.Count < 2)
|
||||
{
|
||||
return sources;
|
||||
}
|
||||
|
||||
var userDataByVersion = _userDataManager.GetUserDataBatch(versions, user);
|
||||
var dataBySourceId = new Dictionary<string, UserItemData>(versions.Count, StringComparer.OrdinalIgnoreCase);
|
||||
foreach (var version in versions)
|
||||
{
|
||||
if (userDataByVersion.TryGetValue(version.Id, out var data))
|
||||
{
|
||||
dataBySourceId[version.Id.ToString("N", CultureInfo.InvariantCulture)] = data;
|
||||
}
|
||||
}
|
||||
|
||||
MediaSourceInfo resumeSource = null;
|
||||
UserItemData resumeData = null;
|
||||
foreach (var source in sources)
|
||||
{
|
||||
if (source.Id is null
|
||||
|| !dataBySourceId.TryGetValue(source.Id, out var data)
|
||||
|| data.PlaybackPositionTicks <= 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
source.PlaybackPositionTicks = data.PlaybackPositionTicks;
|
||||
|
||||
if (resumeData is null || (data.LastPlayedDate ?? DateTime.MinValue) > (resumeData.LastPlayedDate ?? DateTime.MinValue))
|
||||
{
|
||||
resumeSource = source;
|
||||
resumeData = data;
|
||||
}
|
||||
}
|
||||
|
||||
if (resumeSource is not null && !video.PrimaryVersionId.HasValue && !ReferenceEquals(sources[0], resumeSource))
|
||||
{
|
||||
var reordered = new List<MediaSourceInfo>(sources.Count) { resumeSource };
|
||||
reordered.AddRange(sources.Where(s => !ReferenceEquals(s, resumeSource)));
|
||||
return reordered;
|
||||
}
|
||||
|
||||
return sources;
|
||||
|
||||
@@ -247,6 +247,72 @@ namespace Emby.Server.Implementations.Library
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public VersionResumeData? GetResumeUserData(User user, BaseItem item)
|
||||
{
|
||||
return GetResumeUserDataBatch([item], user).GetValueOrDefault(item.Id);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public IReadOnlyDictionary<Guid, VersionResumeData> GetResumeUserDataBatch(IReadOnlyList<BaseItem> items, User user)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(user);
|
||||
|
||||
var result = new Dictionary<Guid, VersionResumeData>();
|
||||
List<(Guid PrimaryId, IReadOnlyList<Video> Versions)>? versionGroups = null;
|
||||
List<BaseItem>? allVersions = null;
|
||||
|
||||
foreach (var item in items)
|
||||
{
|
||||
// Only primary items aggregate over their versions; a directly queried version keeps its own data.
|
||||
if (item is not Video video
|
||||
|| video.PrimaryVersionId.HasValue
|
||||
|| (video.LinkedAlternateVersions.Length == 0 && !video.HasLocalAlternateVersions))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var versions = video.GetAllVersions();
|
||||
if (versions.Count < 2)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
(versionGroups ??= []).Add((item.Id, versions));
|
||||
(allVersions ??= []).AddRange(versions);
|
||||
}
|
||||
|
||||
if (versionGroups is null)
|
||||
{
|
||||
return result;
|
||||
}
|
||||
|
||||
var userDataByVersion = GetUserDataBatch(allVersions!.DistinctBy(i => i.Id).ToList(), user);
|
||||
|
||||
foreach (var (primaryId, versions) in versionGroups)
|
||||
{
|
||||
Video? resumeVersion = null;
|
||||
UserItemData? resumeData = null;
|
||||
foreach (var version in versions)
|
||||
{
|
||||
if (userDataByVersion.TryGetValue(version.Id, out var data)
|
||||
&& data.PlaybackPositionTicks > 0
|
||||
&& (resumeData is null || (data.LastPlayedDate ?? DateTime.MinValue) > (resumeData.LastPlayedDate ?? DateTime.MinValue)))
|
||||
{
|
||||
resumeVersion = version;
|
||||
resumeData = data;
|
||||
}
|
||||
}
|
||||
|
||||
if (resumeData is not null)
|
||||
{
|
||||
result[primaryId] = new VersionResumeData(resumeData, resumeVersion!.RunTimeTicks);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the internal key.
|
||||
/// </summary>
|
||||
@@ -281,6 +347,10 @@ namespace Emby.Server.Implementations.Library
|
||||
var dto = GetUserItemDataDto(userData, item.Id);
|
||||
|
||||
item.FillUserDataDtoValues(dto, userData, itemDto, user, options);
|
||||
|
||||
// For an item with alternate versions, surface the most recently played version's resume point.
|
||||
GetResumeUserData(user, item)?.ApplyTo(dto);
|
||||
|
||||
return dto;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user