mirror of
https://github.com/jellyfin/jellyfin.git
synced 2026-05-04 07:46:32 +01:00
Complete LinkedChildren integration and batch DTO optimizations
This commit integrates remaining performance changes: - Add batch user data fetching in DtoService to reduce N+1 queries - Add GetNextUpEpisodesBatch in TVSeriesManager for efficient batch retrieval - Update Video/Movie/BoxSet to use LibraryManager for alternate versions - Transition LinkedChild to use ItemId instead of Path (obsolete Path/LibraryItemId) - Update providers and controllers for LinkedChildren-based references - Add NextUpEpisodeBatchResult for batched episode queries - Integrate IDescendantQueryProvider in SqliteDatabaseProvider
This commit is contained in:
@@ -272,7 +272,7 @@ namespace Emby.Server.Implementations.Collections
|
||||
{
|
||||
var childItem = _libraryManager.GetItemById(guidId);
|
||||
|
||||
var child = collection.LinkedChildren.FirstOrDefault(i => (i.ItemId.HasValue && i.ItemId.Value.Equals(guidId)) || (childItem is not null && string.Equals(childItem.Path, i.Path, StringComparison.OrdinalIgnoreCase)));
|
||||
var child = collection.LinkedChildren.FirstOrDefault(i => i.ItemId.HasValue && i.ItemId.Value.Equals(guidId));
|
||||
|
||||
if (child is null)
|
||||
{
|
||||
@@ -342,7 +342,7 @@ namespace Emby.Server.Implementations.Collections
|
||||
// this is kind of a performance hack because only Video has alternate versions that should be in a box set?
|
||||
if (item is Video video)
|
||||
{
|
||||
foreach (var childId in video.GetLocalAlternateVersionIds())
|
||||
foreach (var childId in _libraryManager.GetLocalAlternateVersionIds(video))
|
||||
{
|
||||
if (!results.ContainsKey(childId))
|
||||
{
|
||||
|
||||
@@ -153,17 +153,42 @@ namespace Emby.Server.Implementations.Dto
|
||||
private ILiveTvManager LivetvManager => _livetvManagerFactory.Value;
|
||||
|
||||
/// <inheritdoc />
|
||||
public IReadOnlyList<BaseItemDto> GetBaseItemDtos(IReadOnlyList<BaseItem> items, DtoOptions options, User? user = null, BaseItem? owner = null)
|
||||
public IReadOnlyList<BaseItemDto> GetBaseItemDtos(IReadOnlyList<BaseItem> items, DtoOptions options, User? user = null, BaseItem? owner = null, bool skipVisibilityCheck = false)
|
||||
{
|
||||
var accessibleItems = user is null ? items : items.Where(x => x.IsVisible(user)).ToList();
|
||||
var accessibleItems = skipVisibilityCheck || user is null ? items : items.Where(x => x.IsVisible(user)).ToList();
|
||||
var returnItems = new BaseItemDto[accessibleItems.Count];
|
||||
List<(BaseItem, BaseItemDto)>? programTuples = null;
|
||||
List<(BaseItemDto, LiveTvChannel)>? channelTuples = null;
|
||||
|
||||
// Batch-fetch user data for all items
|
||||
Dictionary<Guid, UserItemData>? userDataBatch = null;
|
||||
if (user is not null && options.EnableUserData)
|
||||
{
|
||||
userDataBatch = _userDataRepository.GetUserDataBatch(accessibleItems, user);
|
||||
}
|
||||
|
||||
// Pre-compute collection folders once to avoid N+1 queries in CanDelete
|
||||
List<Folder>? allCollectionFolders = null;
|
||||
if (user is not null && options.ContainsField(ItemFields.CanDelete))
|
||||
{
|
||||
allCollectionFolders = _libraryManager.GetUserRootFolder().Children.OfType<Folder>().ToList();
|
||||
}
|
||||
|
||||
// Batch-fetch child counts for all folders to avoid N+1 queries
|
||||
Dictionary<Guid, int>? childCountBatch = null;
|
||||
if (options.ContainsField(ItemFields.ChildCount))
|
||||
{
|
||||
var folderIds = accessibleItems.OfType<Folder>().Select(f => f.Id).ToList();
|
||||
if (folderIds.Count > 0)
|
||||
{
|
||||
childCountBatch = _libraryManager.GetChildCountBatch(folderIds, user?.Id);
|
||||
}
|
||||
}
|
||||
|
||||
for (int index = 0; index < accessibleItems.Count; index++)
|
||||
{
|
||||
var item = accessibleItems[index];
|
||||
var dto = GetBaseItemDtoInternal(item, options, user, owner);
|
||||
var dto = GetBaseItemDtoInternal(item, options, user, owner, userDataBatch?.GetValueOrDefault(item.Id), allCollectionFolders, childCountBatch);
|
||||
|
||||
if (item is LiveTvChannel tvChannel)
|
||||
{
|
||||
@@ -197,7 +222,7 @@ namespace Emby.Server.Implementations.Dto
|
||||
|
||||
public BaseItemDto GetBaseItemDto(BaseItem item, DtoOptions options, User? user = null, BaseItem? owner = null)
|
||||
{
|
||||
var dto = GetBaseItemDtoInternal(item, options, user, owner);
|
||||
var dto = GetBaseItemDtoInternal(item, options, user, owner, null);
|
||||
if (item is LiveTvChannel tvChannel)
|
||||
{
|
||||
LivetvManager.AddChannelInfo(new[] { (dto, tvChannel) }, options, user);
|
||||
@@ -215,7 +240,7 @@ namespace Emby.Server.Implementations.Dto
|
||||
return dto;
|
||||
}
|
||||
|
||||
private BaseItemDto GetBaseItemDtoInternal(BaseItem item, DtoOptions options, User? user = null, BaseItem? owner = null)
|
||||
private BaseItemDto GetBaseItemDtoInternal(BaseItem item, DtoOptions options, User? user = null, BaseItem? owner = null, UserItemData? userData = null, List<Folder>? allCollectionFolders = null, Dictionary<Guid, int>? childCountBatch = null)
|
||||
{
|
||||
var dto = new BaseItemDto
|
||||
{
|
||||
@@ -252,7 +277,7 @@ namespace Emby.Server.Implementations.Dto
|
||||
|
||||
if (user is not null)
|
||||
{
|
||||
AttachUserSpecificInfo(dto, item, user, options);
|
||||
AttachUserSpecificInfo(dto, item, user, options, userData, childCountBatch);
|
||||
}
|
||||
|
||||
if (item is IHasMediaSources
|
||||
@@ -274,7 +299,9 @@ namespace Emby.Server.Implementations.Dto
|
||||
{
|
||||
dto.CanDelete = user is null
|
||||
? item.CanDelete()
|
||||
: item.CanDelete(user);
|
||||
: allCollectionFolders is not null
|
||||
? item.CanDelete(user, allCollectionFolders)
|
||||
: item.CanDelete(user);
|
||||
}
|
||||
|
||||
if (options.ContainsField(ItemFields.CanDownload))
|
||||
@@ -458,7 +485,7 @@ namespace Emby.Server.Implementations.Dto
|
||||
/// <summary>
|
||||
/// Attaches the user specific info.
|
||||
/// </summary>
|
||||
private void AttachUserSpecificInfo(BaseItemDto dto, BaseItem item, User user, DtoOptions options)
|
||||
private void AttachUserSpecificInfo(BaseItemDto dto, BaseItem item, User user, DtoOptions options, UserItemData? userData = null, Dictionary<Guid, int>? childCountBatch = null)
|
||||
{
|
||||
if (item.IsFolder)
|
||||
{
|
||||
@@ -466,7 +493,17 @@ namespace Emby.Server.Implementations.Dto
|
||||
|
||||
if (options.EnableUserData)
|
||||
{
|
||||
dto.UserData = _userDataRepository.GetUserDataDto(item, dto, user, options);
|
||||
if (userData is not null)
|
||||
{
|
||||
// Use pre-fetched user data
|
||||
dto.UserData = GetUserItemDataDto(userData, item.Id);
|
||||
item.FillUserDataDtoValues(dto.UserData, userData, dto, user, options);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Fall back to individual fetch
|
||||
dto.UserData = _userDataRepository.GetUserDataDto(item, dto, user, options);
|
||||
}
|
||||
}
|
||||
|
||||
if (!dto.ChildCount.HasValue && item.SourceType == SourceType.Library)
|
||||
@@ -485,7 +522,7 @@ namespace Emby.Server.Implementations.Dto
|
||||
|
||||
if (options.ContainsField(ItemFields.ChildCount))
|
||||
{
|
||||
dto.ChildCount ??= GetChildCount(folder, user);
|
||||
dto.ChildCount ??= GetChildCount(folder, user, childCountBatch);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -503,7 +540,17 @@ namespace Emby.Server.Implementations.Dto
|
||||
{
|
||||
if (options.EnableUserData)
|
||||
{
|
||||
dto.UserData = _userDataRepository.GetUserDataDto(item, user);
|
||||
if (userData is not null)
|
||||
{
|
||||
// Use pre-fetched user data
|
||||
dto.UserData = GetUserItemDataDto(userData, item.Id);
|
||||
item.FillUserDataDtoValues(dto.UserData, userData, dto, user, options);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Fall back to individual fetch
|
||||
dto.UserData = _userDataRepository.GetUserDataDto(item, user);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -513,7 +560,25 @@ namespace Emby.Server.Implementations.Dto
|
||||
}
|
||||
}
|
||||
|
||||
private static int GetChildCount(Folder folder, User user)
|
||||
private static UserItemDataDto GetUserItemDataDto(UserItemData data, Guid itemId)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(data);
|
||||
|
||||
return new UserItemDataDto
|
||||
{
|
||||
IsFavorite = data.IsFavorite,
|
||||
Likes = data.Likes,
|
||||
PlaybackPositionTicks = data.PlaybackPositionTicks,
|
||||
PlayCount = data.PlayCount,
|
||||
Rating = data.Rating,
|
||||
Played = data.Played,
|
||||
LastPlayedDate = data.LastPlayedDate,
|
||||
ItemId = itemId,
|
||||
Key = data.Key
|
||||
};
|
||||
}
|
||||
|
||||
private static int GetChildCount(Folder folder, User user, Dictionary<Guid, int>? childCountBatch)
|
||||
{
|
||||
// Right now this is too slow to calculate for top level folders on a per-user basis
|
||||
// Just return something so that apps that are expecting a value won't think the folders are empty
|
||||
@@ -522,6 +587,13 @@ namespace Emby.Server.Implementations.Dto
|
||||
return Random.Shared.Next(1, 10);
|
||||
}
|
||||
|
||||
// Use pre-fetched batch data if available
|
||||
if (childCountBatch is not null && childCountBatch.TryGetValue(folder.Id, out var count))
|
||||
{
|
||||
return count;
|
||||
}
|
||||
|
||||
// Fall back to individual query for special cases (Series, Season, etc.)
|
||||
return folder.GetChildCount(user);
|
||||
}
|
||||
|
||||
|
||||
@@ -406,6 +406,37 @@ namespace Emby.Server.Implementations.Library
|
||||
item.Id);
|
||||
}
|
||||
|
||||
// If deleting a primary version video, clear PrimaryVersionId from alternate versions
|
||||
if (item is Video video && string.IsNullOrEmpty(video.PrimaryVersionId))
|
||||
{
|
||||
var alternateVersions = GetLocalAlternateVersionIds(video)
|
||||
.Concat(GetLinkedAlternateVersions(video).Select(v => v.Id))
|
||||
.Distinct()
|
||||
.Select(id => GetItemById(id))
|
||||
.OfType<Video>()
|
||||
.ToList();
|
||||
|
||||
if (alternateVersions.Count > 0)
|
||||
{
|
||||
_logger.LogInformation(
|
||||
"Clearing PrimaryVersionId from {Count} alternate versions of {Name}",
|
||||
alternateVersions.Count,
|
||||
item.Name ?? "Unknown name");
|
||||
|
||||
// Promote the first alternate version to be the new primary
|
||||
var newPrimary = alternateVersions[0];
|
||||
newPrimary.SetPrimaryVersionId(null);
|
||||
newPrimary.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, CancellationToken.None).GetAwaiter().GetResult();
|
||||
|
||||
// Update remaining alternates to point to new primary
|
||||
foreach (var alternate in alternateVersions.Skip(1))
|
||||
{
|
||||
alternate.SetPrimaryVersionId(newPrimary.Id.ToString("N", CultureInfo.InvariantCulture));
|
||||
alternate.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, CancellationToken.None).GetAwaiter().GetResult();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var children = item.IsFolder
|
||||
? ((Folder)item).GetRecursiveChildren(false)
|
||||
: [];
|
||||
@@ -576,6 +607,9 @@ namespace Emby.Server.Implementations.Library
|
||||
// Trickplay
|
||||
list.Add(_pathManager.GetTrickplayDirectory(video));
|
||||
|
||||
// Chapter Images
|
||||
list.Add(_pathManager.GetChapterImageFolderPath(video));
|
||||
|
||||
// Subtitles and attachments
|
||||
foreach (var mediaSource in item.GetMediaSources(false))
|
||||
{
|
||||
@@ -1421,14 +1455,7 @@ namespace Emby.Server.Implementations.Library
|
||||
AddUserToQuery(query, query.User, allowExternalContent);
|
||||
}
|
||||
|
||||
var itemList = _itemRepository.GetItemList(query);
|
||||
var user = query.User;
|
||||
if (user is not null)
|
||||
{
|
||||
return itemList.Where(i => i.IsVisible(user)).ToList();
|
||||
}
|
||||
|
||||
return itemList;
|
||||
return _itemRepository.GetItemList(query);
|
||||
}
|
||||
|
||||
public IReadOnlyList<BaseItem> GetItemList(InternalItemsQuery query)
|
||||
@@ -1474,6 +1501,11 @@ namespace Emby.Server.Implementations.Library
|
||||
return _itemRepository.GetItemCounts(query);
|
||||
}
|
||||
|
||||
public Dictionary<Guid, int> GetChildCountBatch(IReadOnlyList<Guid> parentIds, Guid? userId)
|
||||
{
|
||||
return _itemRepository.GetChildCountBatch(parentIds, userId);
|
||||
}
|
||||
|
||||
public IReadOnlyList<BaseItem> GetItemList(InternalItemsQuery query, List<BaseItem> parents)
|
||||
{
|
||||
SetTopParentIdsOrAncestors(query, parents);
|
||||
@@ -1519,6 +1551,16 @@ namespace Emby.Server.Implementations.Library
|
||||
return _itemRepository.GetNextUpSeriesKeys(query, dateCutoff);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public IReadOnlyDictionary<string, MediaBrowser.Controller.Persistence.NextUpEpisodeBatchResult> GetNextUpEpisodesBatch(
|
||||
InternalItemsQuery query,
|
||||
IReadOnlyList<string> seriesKeys,
|
||||
bool includeSpecials,
|
||||
bool includeWatchedForRewatching)
|
||||
{
|
||||
return _itemRepository.GetNextUpEpisodesBatch(query, seriesKeys, includeSpecials, includeWatchedForRewatching);
|
||||
}
|
||||
|
||||
public QueryResult<BaseItem> QueryItems(InternalItemsQuery query)
|
||||
{
|
||||
if (query.User is not null)
|
||||
@@ -1700,6 +1742,11 @@ namespace Emby.Server.Implementations.Library
|
||||
|
||||
private void AddUserToQuery(InternalItemsQuery query, User user, bool allowExternalContent = true)
|
||||
{
|
||||
if (query.User is null)
|
||||
{
|
||||
query.SetUser(user);
|
||||
}
|
||||
|
||||
if (query.AncestorIds.Length == 0 &&
|
||||
query.ParentId.IsEmpty() &&
|
||||
query.ChannelIds.Count == 0 &&
|
||||
@@ -1725,6 +1772,15 @@ namespace Emby.Server.Implementations.Library
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void ConfigureUserAccess(InternalItemsQuery query, User user)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(query);
|
||||
ArgumentNullException.ThrowIfNull(user);
|
||||
|
||||
AddUserToQuery(query, user);
|
||||
}
|
||||
|
||||
private IEnumerable<Guid> GetTopParentIdsForQuery(BaseItem item, User? user)
|
||||
{
|
||||
if (item is UserView view)
|
||||
@@ -1889,6 +1945,38 @@ namespace Emby.Server.Implementations.Library
|
||||
return video;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public IEnumerable<Guid> GetLocalAlternateVersionIds(Video video)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(video);
|
||||
|
||||
var linkedIds = _itemRepository.GetLinkedChildrenIds(video.Id, (int)MediaBrowser.Controller.Entities.LinkedChildType.LocalAlternateVersion);
|
||||
if (linkedIds.Count > 0)
|
||||
{
|
||||
return linkedIds;
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public IEnumerable<Video> GetLinkedAlternateVersions(Video video)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(video);
|
||||
|
||||
var linkedIds = _itemRepository.GetLinkedChildrenIds(video.Id, (int)MediaBrowser.Controller.Entities.LinkedChildType.LinkedAlternateVersion);
|
||||
if (linkedIds.Count > 0)
|
||||
{
|
||||
return linkedIds
|
||||
.Select(id => GetItemById(id))
|
||||
.Where(i => i is not null)
|
||||
.OfType<Video>()
|
||||
.OrderBy(i => i.SortName);
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public IEnumerable<BaseItem> Sort(IEnumerable<BaseItem> items, User? user, IEnumerable<ItemSortBy> sortBy, SortOrder sortOrder)
|
||||
{
|
||||
@@ -2896,10 +2984,17 @@ namespace Emby.Server.Implementations.Library
|
||||
extra.ExtraType = extraType;
|
||||
}
|
||||
|
||||
extra.ParentId = Guid.Empty;
|
||||
extra.OwnerId = owner.Id;
|
||||
extra.IsInMixedFolder = isInMixedFolder;
|
||||
return extra;
|
||||
// Only set OwnerId if this is actually an extra (not Unknown or null)
|
||||
if (extra.ExtraType is not null)
|
||||
{
|
||||
extra.ParentId = Guid.Empty;
|
||||
extra.OwnerId = owner.Id;
|
||||
extra.IsInMixedFolder = isInMixedFolder;
|
||||
|
||||
return extra;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Jellyfin.Extensions;
|
||||
using MediaBrowser.Controller.Collections;
|
||||
using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Controller.Entities.Movies;
|
||||
@@ -11,7 +11,6 @@ using MediaBrowser.Controller.Library;
|
||||
using MediaBrowser.Controller.Playlists;
|
||||
using MediaBrowser.Controller.Providers;
|
||||
using MediaBrowser.Model.Globalization;
|
||||
using MediaBrowser.Model.IO;
|
||||
using MediaBrowser.Model.Tasks;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
@@ -27,6 +26,7 @@ public class CleanupCollectionAndPlaylistPathsTask : IScheduledTask
|
||||
private readonly IPlaylistManager _playlistManager;
|
||||
private readonly ILogger<CleanupCollectionAndPlaylistPathsTask> _logger;
|
||||
private readonly IProviderManager _providerManager;
|
||||
private readonly ILibraryManager _libraryManager;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="CleanupCollectionAndPlaylistPathsTask"/> class.
|
||||
@@ -36,18 +36,21 @@ public class CleanupCollectionAndPlaylistPathsTask : IScheduledTask
|
||||
/// <param name="playlistManager">Instance of the <see cref="IPlaylistManager"/> interface.</param>
|
||||
/// <param name="logger">Instance of the <see cref="ILogger"/> interface.</param>
|
||||
/// <param name="providerManager">Instance of the <see cref="IProviderManager"/> interface.</param>
|
||||
/// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
|
||||
public CleanupCollectionAndPlaylistPathsTask(
|
||||
ILocalizationManager localization,
|
||||
ICollectionManager collectionManager,
|
||||
IPlaylistManager playlistManager,
|
||||
ILogger<CleanupCollectionAndPlaylistPathsTask> logger,
|
||||
IProviderManager providerManager)
|
||||
IProviderManager providerManager,
|
||||
ILibraryManager libraryManager)
|
||||
{
|
||||
_localization = localization;
|
||||
_collectionManager = collectionManager;
|
||||
_playlistManager = playlistManager;
|
||||
_logger = logger;
|
||||
_providerManager = providerManager;
|
||||
_libraryManager = libraryManager;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
@@ -111,12 +114,15 @@ public class CleanupCollectionAndPlaylistPathsTask : IScheduledTask
|
||||
List<LinkedChild>? itemsToRemove = null;
|
||||
foreach (var linkedChild in folder.LinkedChildren)
|
||||
{
|
||||
var path = linkedChild.Path;
|
||||
if (!File.Exists(path) && !Directory.Exists(path))
|
||||
if (linkedChild.ItemId.HasValue
|
||||
&& !linkedChild.ItemId.Value.IsEmpty()
|
||||
&& _libraryManager.GetItemById(linkedChild.ItemId.Value) is not null)
|
||||
{
|
||||
_logger.LogInformation("Item in {FolderName} cannot be found at {ItemPath}", folder.Name, path);
|
||||
(itemsToRemove ??= new List<LinkedChild>()).Add(linkedChild);
|
||||
continue;
|
||||
}
|
||||
|
||||
_logger.LogInformation("Item in {FolderName} with ItemId {ItemId} no longer exists in library", folder.Name, linkedChild.ItemId);
|
||||
(itemsToRemove ??= []).Add(linkedChild);
|
||||
}
|
||||
|
||||
if (itemsToRemove is not null)
|
||||
|
||||
@@ -1820,7 +1820,6 @@ namespace Emby.Server.Implementations.Session
|
||||
fields.Remove(ItemFields.Settings);
|
||||
fields.Remove(ItemFields.SortName);
|
||||
fields.Remove(ItemFields.Tags);
|
||||
fields.Remove(ItemFields.ExtraIds);
|
||||
|
||||
dtoOptions.Fields = fields.ToArray();
|
||||
|
||||
|
||||
@@ -47,7 +47,7 @@ namespace Emby.Server.Implementations.TV
|
||||
|
||||
if (!string.IsNullOrEmpty(presentationUniqueKey))
|
||||
{
|
||||
return GetResult(GetNextUpEpisodes(query, user, new[] { presentationUniqueKey }, options), query);
|
||||
return GetNextUpBatched(query, user, [presentationUniqueKey], options);
|
||||
}
|
||||
|
||||
BaseItem[] parents;
|
||||
@@ -58,11 +58,11 @@ namespace Emby.Server.Implementations.TV
|
||||
|
||||
if (parent is not null)
|
||||
{
|
||||
parents = new[] { parent };
|
||||
parents = [parent];
|
||||
}
|
||||
else
|
||||
{
|
||||
parents = Array.Empty<BaseItem>();
|
||||
parents = [];
|
||||
}
|
||||
}
|
||||
else
|
||||
@@ -93,7 +93,7 @@ namespace Emby.Server.Implementations.TV
|
||||
|
||||
if (!string.IsNullOrEmpty(presentationUniqueKey))
|
||||
{
|
||||
return GetResult(GetNextUpEpisodes(request, user, [presentationUniqueKey], options), request);
|
||||
return GetNextUpBatched(request, user, [presentationUniqueKey], options);
|
||||
}
|
||||
|
||||
if (limit.HasValue)
|
||||
@@ -103,25 +103,133 @@ namespace Emby.Server.Implementations.TV
|
||||
|
||||
var nextUpSeriesKeys = _libraryManager.GetNextUpSeriesKeys(new InternalItemsQuery(user) { Limit = limit }, parentsFolders, request.NextUpDateCutoff);
|
||||
|
||||
var episodes = GetNextUpEpisodes(request, user, nextUpSeriesKeys, options);
|
||||
|
||||
return GetResult(episodes, request);
|
||||
return GetNextUpBatched(request, user, nextUpSeriesKeys, options);
|
||||
}
|
||||
|
||||
private IEnumerable<Episode> GetNextUpEpisodes(NextUpQuery request, User user, IReadOnlyList<string> seriesKeys, DtoOptions dtoOptions)
|
||||
private QueryResult<BaseItem> GetNextUpBatched(NextUpQuery request, User user, IReadOnlyList<string> seriesKeys, DtoOptions dtoOptions)
|
||||
{
|
||||
var allNextUp = seriesKeys.Select(i => GetNextUp(i, user, dtoOptions, request.EnableResumable, false));
|
||||
|
||||
if (request.EnableRewatching)
|
||||
if (seriesKeys.Count == 0)
|
||||
{
|
||||
allNextUp = allNextUp
|
||||
.Concat(seriesKeys.Select(i => GetNextUp(i, user, dtoOptions, false, true)))
|
||||
.OrderByDescending(i => i.LastWatchedDate);
|
||||
return new QueryResult<BaseItem>();
|
||||
}
|
||||
|
||||
return allNextUp
|
||||
.Select(i => i.GetEpisodeFunction())
|
||||
.Where(i => i is not null)!;
|
||||
var includeSpecials = _configurationManager.Configuration.DisplaySpecialsWithinSeasons;
|
||||
var includeRewatching = request.EnableRewatching;
|
||||
|
||||
var query = new InternalItemsQuery(user)
|
||||
{
|
||||
DtoOptions = dtoOptions
|
||||
};
|
||||
|
||||
var batchResult = _libraryManager.GetNextUpEpisodesBatch(query, seriesKeys, includeSpecials, includeRewatching);
|
||||
|
||||
var nextUpList = new List<(DateTime LastWatchedDate, Episode Episode)>();
|
||||
|
||||
foreach (var seriesKey in seriesKeys)
|
||||
{
|
||||
if (!batchResult.TryGetValue(seriesKey, out var result))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var nextEpisode = DetermineNextEpisode(result, user, includeSpecials, request.EnableResumable, false);
|
||||
|
||||
if (nextEpisode is not null)
|
||||
{
|
||||
DateTime lastWatchedDate = DateTime.MinValue;
|
||||
if (result.LastWatched is not null)
|
||||
{
|
||||
var userData = _userDataManager.GetUserData(user, result.LastWatched);
|
||||
lastWatchedDate = userData?.LastPlayedDate ?? DateTime.MinValue.AddDays(1);
|
||||
}
|
||||
|
||||
nextUpList.Add((lastWatchedDate, nextEpisode));
|
||||
}
|
||||
|
||||
if (includeRewatching)
|
||||
{
|
||||
var nextPlayedEpisode = DetermineNextEpisodeForRewatching(result, user, includeSpecials);
|
||||
|
||||
if (nextPlayedEpisode is not null)
|
||||
{
|
||||
DateTime rewatchLastWatchedDate = DateTime.MinValue;
|
||||
if (result.LastWatchedForRewatching is not null)
|
||||
{
|
||||
var userData = _userDataManager.GetUserData(user, result.LastWatchedForRewatching);
|
||||
rewatchLastWatchedDate = userData?.LastPlayedDate ?? DateTime.MinValue.AddDays(1);
|
||||
}
|
||||
|
||||
nextUpList.Add((rewatchLastWatchedDate, nextPlayedEpisode));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var sortedEpisodes = nextUpList
|
||||
.OrderByDescending(x => x.LastWatchedDate)
|
||||
.Select(x => (BaseItem)x.Episode);
|
||||
|
||||
return GetResult(sortedEpisodes, request);
|
||||
}
|
||||
|
||||
private Episode? DetermineNextEpisode(
|
||||
MediaBrowser.Controller.Persistence.NextUpEpisodeBatchResult result,
|
||||
User user,
|
||||
bool includeSpecials,
|
||||
bool includeResumable,
|
||||
bool includePlayed)
|
||||
{
|
||||
var nextEpisode = (includePlayed ? result.NextPlayedForRewatching : result.NextUp) as Episode;
|
||||
var lastWatchedEpisode = (includePlayed ? result.LastWatchedForRewatching : result.LastWatched) as Episode;
|
||||
|
||||
if (includeSpecials && result.Specials?.Count > 0)
|
||||
{
|
||||
var consideredEpisodes = result.Specials
|
||||
.Cast<Episode>()
|
||||
.Where(episode => episode.AirsBeforeSeasonNumber is not null || episode.AirsAfterSeasonNumber is not null)
|
||||
.ToList();
|
||||
|
||||
if (lastWatchedEpisode is not null)
|
||||
{
|
||||
consideredEpisodes.Add(lastWatchedEpisode);
|
||||
}
|
||||
|
||||
if (nextEpisode is not null)
|
||||
{
|
||||
consideredEpisodes.Add(nextEpisode);
|
||||
}
|
||||
|
||||
if (consideredEpisodes.Count > 0)
|
||||
{
|
||||
var sortedEpisodes = _libraryManager.Sort(consideredEpisodes, user, [(ItemSortBy.AiredEpisodeOrder, SortOrder.Ascending)])
|
||||
.Cast<Episode>();
|
||||
|
||||
if (lastWatchedEpisode is not null)
|
||||
{
|
||||
sortedEpisodes = sortedEpisodes.SkipWhile(episode => !episode.Id.Equals(lastWatchedEpisode.Id)).Skip(1);
|
||||
}
|
||||
|
||||
nextEpisode = sortedEpisodes.FirstOrDefault();
|
||||
}
|
||||
}
|
||||
|
||||
if (nextEpisode is not null && !includeResumable)
|
||||
{
|
||||
var userData = _userDataManager.GetUserData(user, nextEpisode);
|
||||
if (userData?.PlaybackPositionTicks > 0)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
return nextEpisode;
|
||||
}
|
||||
|
||||
private Episode? DetermineNextEpisodeForRewatching(
|
||||
MediaBrowser.Controller.Persistence.NextUpEpisodeBatchResult result,
|
||||
User user,
|
||||
bool includeSpecials)
|
||||
{
|
||||
return DetermineNextEpisode(result, user, includeSpecials, includeResumable: false, includePlayed: true);
|
||||
}
|
||||
|
||||
private static string GetUniqueSeriesKey(Series series)
|
||||
@@ -129,127 +237,6 @@ namespace Emby.Server.Implementations.TV
|
||||
return series.GetPresentationUniqueKey();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the next up.
|
||||
/// </summary>
|
||||
/// <returns>Task{Episode}.</returns>
|
||||
private (DateTime LastWatchedDate, Func<Episode?> GetEpisodeFunction) GetNextUp(string seriesKey, User user, DtoOptions dtoOptions, bool includeResumable, bool includePlayed)
|
||||
{
|
||||
var lastQuery = new InternalItemsQuery(user)
|
||||
{
|
||||
AncestorWithPresentationUniqueKey = null,
|
||||
SeriesPresentationUniqueKey = seriesKey,
|
||||
IncludeItemTypes = [BaseItemKind.Episode],
|
||||
IsPlayed = true,
|
||||
Limit = 1,
|
||||
ParentIndexNumberNotEquals = 0,
|
||||
DtoOptions = new DtoOptions
|
||||
{
|
||||
Fields = [ItemFields.SortName],
|
||||
EnableImages = false
|
||||
}
|
||||
};
|
||||
|
||||
// If including played results, sort first by date played and then by season and episode numbers
|
||||
lastQuery.OrderBy = includePlayed
|
||||
? new[] { (ItemSortBy.DatePlayed, SortOrder.Descending), (ItemSortBy.ParentIndexNumber, SortOrder.Descending), (ItemSortBy.IndexNumber, SortOrder.Descending) }
|
||||
: new[] { (ItemSortBy.ParentIndexNumber, SortOrder.Descending), (ItemSortBy.IndexNumber, SortOrder.Descending) };
|
||||
|
||||
var lastWatchedEpisode = _libraryManager.GetItemList(lastQuery).Cast<Episode>().FirstOrDefault();
|
||||
|
||||
Episode? GetEpisode()
|
||||
{
|
||||
var nextQuery = new InternalItemsQuery(user)
|
||||
{
|
||||
AncestorWithPresentationUniqueKey = null,
|
||||
SeriesPresentationUniqueKey = seriesKey,
|
||||
IncludeItemTypes = [BaseItemKind.Episode],
|
||||
OrderBy = [(ItemSortBy.ParentIndexNumber, SortOrder.Ascending), (ItemSortBy.IndexNumber, SortOrder.Ascending)],
|
||||
Limit = 1,
|
||||
IsPlayed = includePlayed,
|
||||
IsVirtualItem = false,
|
||||
ParentIndexNumberNotEquals = 0,
|
||||
DtoOptions = dtoOptions
|
||||
};
|
||||
|
||||
// Locate the next up episode based on the last watched episode's season and episode number
|
||||
var lastWatchedParentIndexNumber = lastWatchedEpisode?.ParentIndexNumber;
|
||||
var lastWatchedIndexNumber = lastWatchedEpisode?.IndexNumberEnd ?? lastWatchedEpisode?.IndexNumber;
|
||||
if (lastWatchedParentIndexNumber.HasValue && lastWatchedIndexNumber.HasValue)
|
||||
{
|
||||
nextQuery.MinParentAndIndexNumber = (lastWatchedParentIndexNumber.Value, lastWatchedIndexNumber.Value + 1);
|
||||
}
|
||||
|
||||
var nextEpisode = _libraryManager.GetItemList(nextQuery).Cast<Episode>().FirstOrDefault();
|
||||
|
||||
if (_configurationManager.Configuration.DisplaySpecialsWithinSeasons)
|
||||
{
|
||||
var consideredEpisodes = _libraryManager.GetItemList(new InternalItemsQuery(user)
|
||||
{
|
||||
AncestorWithPresentationUniqueKey = null,
|
||||
SeriesPresentationUniqueKey = seriesKey,
|
||||
ParentIndexNumber = 0,
|
||||
IncludeItemTypes = [BaseItemKind.Episode],
|
||||
IsPlayed = includePlayed,
|
||||
IsVirtualItem = false,
|
||||
DtoOptions = dtoOptions
|
||||
})
|
||||
.Cast<Episode>()
|
||||
.Where(episode => episode.AirsBeforeSeasonNumber is not null || episode.AirsAfterSeasonNumber is not null)
|
||||
.ToList();
|
||||
|
||||
if (lastWatchedEpisode is not null)
|
||||
{
|
||||
// Last watched episode is added, because there could be specials that aired before the last watched episode
|
||||
consideredEpisodes.Add(lastWatchedEpisode);
|
||||
}
|
||||
|
||||
if (nextEpisode is not null)
|
||||
{
|
||||
consideredEpisodes.Add(nextEpisode);
|
||||
}
|
||||
|
||||
var sortedConsideredEpisodes = _libraryManager.Sort(consideredEpisodes, user, [(ItemSortBy.AiredEpisodeOrder, SortOrder.Ascending)])
|
||||
.Cast<Episode>();
|
||||
if (lastWatchedEpisode is not null)
|
||||
{
|
||||
sortedConsideredEpisodes = sortedConsideredEpisodes.SkipWhile(episode => !episode.Id.Equals(lastWatchedEpisode.Id)).Skip(1);
|
||||
}
|
||||
|
||||
nextEpisode = sortedConsideredEpisodes.FirstOrDefault();
|
||||
}
|
||||
|
||||
if (nextEpisode is not null && !includeResumable)
|
||||
{
|
||||
var userData = _userDataManager.GetUserData(user, nextEpisode);
|
||||
|
||||
if (userData?.PlaybackPositionTicks > 0)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
return nextEpisode;
|
||||
}
|
||||
|
||||
if (lastWatchedEpisode is not null)
|
||||
{
|
||||
var userData = _userDataManager.GetUserData(user, lastWatchedEpisode);
|
||||
|
||||
if (userData is null)
|
||||
{
|
||||
return (DateTime.MinValue, GetEpisode);
|
||||
}
|
||||
|
||||
var lastWatchedDate = userData.LastPlayedDate ?? DateTime.MinValue.AddDays(1);
|
||||
|
||||
return (lastWatchedDate, GetEpisode);
|
||||
}
|
||||
|
||||
// Return the first episode
|
||||
return (DateTime.MinValue, GetEpisode);
|
||||
}
|
||||
|
||||
private static QueryResult<BaseItem> GetResult(IEnumerable<BaseItem> items, NextUpQuery query)
|
||||
{
|
||||
int totalCount = 0;
|
||||
|
||||
Reference in New Issue
Block a user