aboutsummaryrefslogtreecommitdiff
path: root/Emby.Server.Implementations
diff options
context:
space:
mode:
authorShadowghost <Ghost_of_Stone@web.de>2026-01-17 17:10:07 +0100
committerShadowghost <Ghost_of_Stone@web.de>2026-01-18 19:48:46 +0100
commit5996c4afce11249804d24f1caa3a99b390543c4d (patch)
treed84b98428d95c801492b1354571e2ab3fc0cc99b /Emby.Server.Implementations
parentdfa78590c2899c7e74b142ebbced4140a354aed0 (diff)
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
Diffstat (limited to 'Emby.Server.Implementations')
-rw-r--r--Emby.Server.Implementations/Collections/CollectionManager.cs4
-rw-r--r--Emby.Server.Implementations/Dto/DtoService.cs96
-rw-r--r--Emby.Server.Implementations/Library/LibraryManager.cs119
-rw-r--r--Emby.Server.Implementations/ScheduledTasks/Tasks/CleanupCollectionAndPlaylistPathsTask.cs20
-rw-r--r--Emby.Server.Implementations/Session/SessionManager.cs1
-rw-r--r--Emby.Server.Implementations/TV/TVSeriesManager.cs205
6 files changed, 302 insertions, 143 deletions
diff --git a/Emby.Server.Implementations/Collections/CollectionManager.cs b/Emby.Server.Implementations/Collections/CollectionManager.cs
index a320a774c6..0ede5665f9 100644
--- a/Emby.Server.Implementations/Collections/CollectionManager.cs
+++ b/Emby.Server.Implementations/Collections/CollectionManager.cs
@@ -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))
{
diff --git a/Emby.Server.Implementations/Dto/DtoService.cs b/Emby.Server.Implementations/Dto/DtoService.cs
index c5dc3b054c..236b3fabe4 100644
--- a/Emby.Server.Implementations/Dto/DtoService.cs
+++ b/Emby.Server.Implementations/Dto/DtoService.cs
@@ -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);
}
diff --git a/Emby.Server.Implementations/Library/LibraryManager.cs b/Emby.Server.Implementations/Library/LibraryManager.cs
index f7f5c387e1..2acfd68c36 100644
--- a/Emby.Server.Implementations/Library/LibraryManager.cs
+++ b/Emby.Server.Implementations/Library/LibraryManager.cs
@@ -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)
@@ -1890,6 +1946,38 @@ namespace Emby.Server.Implementations.Library
}
/// <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)
{
IOrderedEnumerable<BaseItem>? orderedItems = null;
@@ -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;
}
}
diff --git a/Emby.Server.Implementations/ScheduledTasks/Tasks/CleanupCollectionAndPlaylistPathsTask.cs b/Emby.Server.Implementations/ScheduledTasks/Tasks/CleanupCollectionAndPlaylistPathsTask.cs
index 7f68f7701e..1c2038d839 100644
--- a/Emby.Server.Implementations/ScheduledTasks/Tasks/CleanupCollectionAndPlaylistPathsTask.cs
+++ b/Emby.Server.Implementations/ScheduledTasks/Tasks/CleanupCollectionAndPlaylistPathsTask.cs
@@ -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)
diff --git a/Emby.Server.Implementations/Session/SessionManager.cs b/Emby.Server.Implementations/Session/SessionManager.cs
index cf2ca047cf..6f576146e4 100644
--- a/Emby.Server.Implementations/Session/SessionManager.cs
+++ b/Emby.Server.Implementations/Session/SessionManager.cs
@@ -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();
diff --git a/Emby.Server.Implementations/TV/TVSeriesManager.cs b/Emby.Server.Implementations/TV/TVSeriesManager.cs
index cd98dbe86e..ebabb4ca2f 100644
--- a/Emby.Server.Implementations/TV/TVSeriesManager.cs
+++ b/Emby.Server.Implementations/TV/TVSeriesManager.cs
@@ -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,151 +103,138 @@ 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)!;
- }
-
- private static string GetUniqueSeriesKey(Series series)
- {
- return series.GetPresentationUniqueKey();
- }
+ var includeSpecials = _configurationManager.Configuration.DisplaySpecialsWithinSeasons;
+ var includeRewatching = request.EnableRewatching;
- /// <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)
+ var query = new InternalItemsQuery(user)
{
- AncestorWithPresentationUniqueKey = null,
- SeriesPresentationUniqueKey = seriesKey,
- IncludeItemTypes = [BaseItemKind.Episode],
- IsPlayed = true,
- Limit = 1,
- ParentIndexNumberNotEquals = 0,
- DtoOptions = new DtoOptions
- {
- Fields = [ItemFields.SortName],
- EnableImages = false
- }
+ DtoOptions = dtoOptions
};
- // 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 batchResult = _libraryManager.GetNextUpEpisodesBatch(query, seriesKeys, includeSpecials, includeRewatching);
- var lastWatchedEpisode = _libraryManager.GetItemList(lastQuery).Cast<Episode>().FirstOrDefault();
+ var nextUpList = new List<(DateTime LastWatchedDate, Episode Episode)>();
- Episode? GetEpisode()
+ foreach (var seriesKey in seriesKeys)
{
- var nextQuery = new InternalItemsQuery(user)
+ if (!batchResult.TryGetValue(seriesKey, out var result))
{
- 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);
+ continue;
}
- var nextEpisode = _libraryManager.GetItemList(nextQuery).Cast<Episode>().FirstOrDefault();
+ var nextEpisode = DetermineNextEpisode(result, user, includeSpecials, request.EnableResumable, false);
- if (_configurationManager.Configuration.DisplaySpecialsWithinSeasons)
+ if (nextEpisode is not null)
{
- var consideredEpisodes = _libraryManager.GetItemList(new InternalItemsQuery(user)
+ DateTime lastWatchedDate = DateTime.MinValue;
+ if (result.LastWatched is not null)
{
- 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);
+ var userData = _userDataManager.GetUserData(user, result.LastWatched);
+ lastWatchedDate = userData?.LastPlayedDate ?? DateTime.MinValue.AddDays(1);
}
- if (nextEpisode is not null)
- {
- consideredEpisodes.Add(nextEpisode);
- }
+ nextUpList.Add((lastWatchedDate, nextEpisode));
+ }
- var sortedConsideredEpisodes = _libraryManager.Sort(consideredEpisodes, user, [(ItemSortBy.AiredEpisodeOrder, SortOrder.Ascending)])
- .Cast<Episode>();
- if (lastWatchedEpisode is not null)
+ if (includeRewatching)
+ {
+ var nextPlayedEpisode = DetermineNextEpisodeForRewatching(result, user, includeSpecials);
+
+ if (nextPlayedEpisode is not null)
{
- sortedConsideredEpisodes = sortedConsideredEpisodes.SkipWhile(episode => !episode.Id.Equals(lastWatchedEpisode.Id)).Skip(1);
+ 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));
}
+ }
+ }
- nextEpisode = sortedConsideredEpisodes.FirstOrDefault();
+ 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 && !includeResumable)
+ if (nextEpisode is not null)
{
- var userData = _userDataManager.GetUserData(user, nextEpisode);
+ consideredEpisodes.Add(nextEpisode);
+ }
- if (userData?.PlaybackPositionTicks > 0)
+ if (consideredEpisodes.Count > 0)
+ {
+ var sortedEpisodes = _libraryManager.Sort(consideredEpisodes, user, [(ItemSortBy.AiredEpisodeOrder, SortOrder.Ascending)])
+ .Cast<Episode>();
+
+ if (lastWatchedEpisode is not null)
{
- return null;
+ sortedEpisodes = sortedEpisodes.SkipWhile(episode => !episode.Id.Equals(lastWatchedEpisode.Id)).Skip(1);
}
- }
- return nextEpisode;
+ nextEpisode = sortedEpisodes.FirstOrDefault();
+ }
}
- if (lastWatchedEpisode is not null)
+ if (nextEpisode is not null && !includeResumable)
{
- var userData = _userDataManager.GetUserData(user, lastWatchedEpisode);
-
- if (userData is null)
+ var userData = _userDataManager.GetUserData(user, nextEpisode);
+ if (userData?.PlaybackPositionTicks > 0)
{
- return (DateTime.MinValue, GetEpisode);
+ return null;
}
+ }
- var lastWatchedDate = userData.LastPlayedDate ?? DateTime.MinValue.AddDays(1);
+ return nextEpisode;
+ }
- return (lastWatchedDate, GetEpisode);
- }
+ private Episode? DetermineNextEpisodeForRewatching(
+ MediaBrowser.Controller.Persistence.NextUpEpisodeBatchResult result,
+ User user,
+ bool includeSpecials)
+ {
+ return DetermineNextEpisode(result, user, includeSpecials, includeResumable: false, includePlayed: true);
+ }
- // Return the first episode
- return (DateTime.MinValue, GetEpisode);
+ private static string GetUniqueSeriesKey(Series series)
+ {
+ return series.GetPresentationUniqueKey();
}
private static QueryResult<BaseItem> GetResult(IEnumerable<BaseItem> items, NextUpQuery query)