aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorCody Robibero <cody@robibe.ro>2025-03-18 17:37:04 -0600
committerGitHub <noreply@github.com>2025-03-18 17:37:04 -0600
commit85b5bebda4a887bad03a114e727d9ee5d87961cc (patch)
treed880ec88af19d1586b9c660e813a724823ce89e3
parente1392ca1b62355d9ce16177b9f69d2bd56c1e0d0 (diff)
Add fast-path to getting just the SeriesPresentationUniqueKey for NextUp (#13687)
* Add more optimized query to calculate series that should be processed for next up * Filter series based on last watched date
-rw-r--r--Emby.Server.Implementations/Library/LibraryManager.cs15
-rw-r--r--Emby.Server.Implementations/TV/TVSeriesManager.cs61
-rw-r--r--Jellyfin.Api/Controllers/TvShowsController.cs4
-rw-r--r--Jellyfin.Server.Implementations/Item/BaseItemRepository.cs31
-rw-r--r--MediaBrowser.Controller/Library/ILibraryManager.cs9
-rw-r--r--MediaBrowser.Controller/Persistence/IItemRepository.cs8
-rw-r--r--MediaBrowser.Model/Querying/NextUpQuery.cs113
7 files changed, 128 insertions, 113 deletions
diff --git a/Emby.Server.Implementations/Library/LibraryManager.cs b/Emby.Server.Implementations/Library/LibraryManager.cs
index cc2092e21..7b3a54039 100644
--- a/Emby.Server.Implementations/Library/LibraryManager.cs
+++ b/Emby.Server.Implementations/Library/LibraryManager.cs
@@ -1344,6 +1344,21 @@ namespace Emby.Server.Implementations.Library
return _itemRepository.GetItemList(query);
}
+ public IReadOnlyList<string> GetNextUpSeriesKeys(InternalItemsQuery query, IReadOnlyCollection<BaseItem> parents, DateTime dateCutoff)
+ {
+ SetTopParentIdsOrAncestors(query, parents);
+
+ if (query.AncestorIds.Length == 0 && query.TopParentIds.Length == 0)
+ {
+ if (query.User is not null)
+ {
+ AddUserToQuery(query, query.User);
+ }
+ }
+
+ return _itemRepository.GetNextUpSeriesKeys(query, dateCutoff);
+ }
+
public QueryResult<BaseItem> QueryItems(InternalItemsQuery query)
{
if (query.User is not null)
diff --git a/Emby.Server.Implementations/TV/TVSeriesManager.cs b/Emby.Server.Implementations/TV/TVSeriesManager.cs
index f8ce473da..10d27498b 100644
--- a/Emby.Server.Implementations/TV/TVSeriesManager.cs
+++ b/Emby.Server.Implementations/TV/TVSeriesManager.cs
@@ -91,7 +91,7 @@ namespace Emby.Server.Implementations.TV
if (!string.IsNullOrEmpty(presentationUniqueKey))
{
- return GetResult(GetNextUpEpisodes(request, user, new[] { presentationUniqueKey }, options), request);
+ return GetResult(GetNextUpEpisodes(request, user, [presentationUniqueKey], options), request);
}
if (limit.HasValue)
@@ -99,25 +99,9 @@ namespace Emby.Server.Implementations.TV
limit = limit.Value + 10;
}
- var items = _libraryManager
- .GetItemList(
- new InternalItemsQuery(user)
- {
- IncludeItemTypes = new[] { BaseItemKind.Episode },
- OrderBy = new[] { (ItemSortBy.DatePlayed, SortOrder.Descending) },
- SeriesPresentationUniqueKey = presentationUniqueKey,
- Limit = limit,
- DtoOptions = new DtoOptions { Fields = new[] { ItemFields.SeriesPresentationUniqueKey }, EnableImages = false },
- GroupBySeriesPresentationUniqueKey = true
- },
- parentsFolders.ToList())
- .Cast<Episode>()
- .Where(episode => !string.IsNullOrEmpty(episode.SeriesPresentationUniqueKey))
- .Select(GetUniqueSeriesKey)
- .ToList();
-
- // Avoid implicitly captured closure
- var episodes = GetNextUpEpisodes(request, user, items.Distinct().ToArray(), options);
+ var nextUpSeriesKeys = _libraryManager.GetNextUpSeriesKeys(new InternalItemsQuery(user) { Limit = limit }, parentsFolders, request.NextUpDateCutoff);
+
+ var episodes = GetNextUpEpisodes(request, user, nextUpSeriesKeys, options);
return GetResult(episodes, request);
}
@@ -133,36 +117,11 @@ namespace Emby.Server.Implementations.TV
.OrderByDescending(i => i.LastWatchedDate);
}
- // If viewing all next up for all series, remove first episodes
- // But if that returns empty, keep those first episodes (avoid completely empty view)
- var alwaysEnableFirstEpisode = !request.SeriesId.IsNullOrEmpty();
- var anyFound = false;
-
return allNextUp
- .Where(i =>
- {
- if (request.DisableFirstEpisode)
- {
- return i.LastWatchedDate != DateTime.MinValue;
- }
-
- if (alwaysEnableFirstEpisode || (i.LastWatchedDate != DateTime.MinValue && i.LastWatchedDate.Date >= request.NextUpDateCutoff))
- {
- anyFound = true;
- return true;
- }
-
- return !anyFound && i.LastWatchedDate == DateTime.MinValue;
- })
.Select(i => i.GetEpisodeFunction())
.Where(i => i is not null)!;
}
- private static string GetUniqueSeriesKey(Episode episode)
- {
- return episode.SeriesPresentationUniqueKey;
- }
-
private static string GetUniqueSeriesKey(Series series)
{
return series.GetPresentationUniqueKey();
@@ -178,13 +137,13 @@ namespace Emby.Server.Implementations.TV
{
AncestorWithPresentationUniqueKey = null,
SeriesPresentationUniqueKey = seriesKey,
- IncludeItemTypes = new[] { BaseItemKind.Episode },
+ IncludeItemTypes = [BaseItemKind.Episode],
IsPlayed = true,
Limit = 1,
ParentIndexNumberNotEquals = 0,
DtoOptions = new DtoOptions
{
- Fields = new[] { ItemFields.SortName },
+ Fields = [ItemFields.SortName],
EnableImages = false
}
};
@@ -202,8 +161,8 @@ namespace Emby.Server.Implementations.TV
{
AncestorWithPresentationUniqueKey = null,
SeriesPresentationUniqueKey = seriesKey,
- IncludeItemTypes = new[] { BaseItemKind.Episode },
- OrderBy = new[] { (ItemSortBy.ParentIndexNumber, SortOrder.Ascending), (ItemSortBy.IndexNumber, SortOrder.Ascending) },
+ IncludeItemTypes = [BaseItemKind.Episode],
+ OrderBy = [(ItemSortBy.ParentIndexNumber, SortOrder.Ascending), (ItemSortBy.IndexNumber, SortOrder.Ascending)],
Limit = 1,
IsPlayed = includePlayed,
IsVirtualItem = false,
@@ -228,7 +187,7 @@ namespace Emby.Server.Implementations.TV
AncestorWithPresentationUniqueKey = null,
SeriesPresentationUniqueKey = seriesKey,
ParentIndexNumber = 0,
- IncludeItemTypes = new[] { BaseItemKind.Episode },
+ IncludeItemTypes = [BaseItemKind.Episode],
IsPlayed = includePlayed,
IsVirtualItem = false,
DtoOptions = dtoOptions
@@ -248,7 +207,7 @@ namespace Emby.Server.Implementations.TV
consideredEpisodes.Add(nextEpisode);
}
- var sortedConsideredEpisodes = _libraryManager.Sort(consideredEpisodes, user, new[] { (ItemSortBy.AiredEpisodeOrder, SortOrder.Ascending) })
+ var sortedConsideredEpisodes = _libraryManager.Sort(consideredEpisodes, user, [(ItemSortBy.AiredEpisodeOrder, SortOrder.Ascending)])
.Cast<Episode>();
if (lastWatchedEpisode is not null)
{
diff --git a/Jellyfin.Api/Controllers/TvShowsController.cs b/Jellyfin.Api/Controllers/TvShowsController.cs
index df46c2dac..cc070244b 100644
--- a/Jellyfin.Api/Controllers/TvShowsController.cs
+++ b/Jellyfin.Api/Controllers/TvShowsController.cs
@@ -2,6 +2,7 @@ using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
+using Jellyfin.Api.Attributes;
using Jellyfin.Api.Extensions;
using Jellyfin.Api.Helpers;
using Jellyfin.Api.ModelBinders;
@@ -86,7 +87,7 @@ public class TvShowsController : BaseJellyfinApiController
[FromQuery] bool? enableUserData,
[FromQuery] DateTime? nextUpDateCutoff,
[FromQuery] bool enableTotalRecordCount = true,
- [FromQuery] bool disableFirstEpisode = false,
+ [FromQuery][ParameterObsolete] bool disableFirstEpisode = false,
[FromQuery] bool enableResumable = true,
[FromQuery] bool enableRewatching = false)
{
@@ -109,7 +110,6 @@ public class TvShowsController : BaseJellyfinApiController
StartIndex = startIndex,
User = user,
EnableTotalRecordCount = enableTotalRecordCount,
- DisableFirstEpisode = disableFirstEpisode,
NextUpDateCutoff = nextUpDateCutoff ?? DateTime.MinValue,
EnableResumable = enableResumable,
EnableRewatching = enableRewatching
diff --git a/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs b/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs
index 392b7de74..e20ad79ad 100644
--- a/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs
+++ b/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs
@@ -255,6 +255,37 @@ public sealed class BaseItemRepository
return dbQuery.AsEnumerable().Where(e => e is not null).Select(w => DeserialiseBaseItem(w, filter.SkipDeserialization)).ToArray();
}
+ /// <inheritdoc />
+ public IReadOnlyList<string> GetNextUpSeriesKeys(InternalItemsQuery filter, DateTime dateCutoff)
+ {
+ ArgumentNullException.ThrowIfNull(filter);
+ ArgumentNullException.ThrowIfNull(filter.User);
+
+ using var context = _dbProvider.CreateDbContext();
+
+ var query = context.BaseItems
+ .AsNoTracking()
+ .Where(i => filter.TopParentIds.Contains(i.TopParentId!.Value))
+ .Where(i => i.Type == _itemTypeLookup.BaseItemKindNames[BaseItemKind.Episode])
+ .Join(
+ context.UserData.AsNoTracking(),
+ i => new { UserId = filter.User.Id, ItemId = i.Id },
+ u => new { UserId = u.UserId, ItemId = u.ItemId },
+ (entity, data) => new { Item = entity, UserData = data })
+ .GroupBy(g => g.Item.SeriesPresentationUniqueKey)
+ .Select(g => new { g.Key, LastPlayedDate = g.Max(u => u.UserData.LastPlayedDate) })
+ .Where(g => g.Key != null && g.LastPlayedDate != null && g.LastPlayedDate >= dateCutoff)
+ .OrderByDescending(g => g.LastPlayedDate)
+ .Select(g => g.Key!);
+
+ if (filter.Limit.HasValue)
+ {
+ query = query.Take(filter.Limit.Value);
+ }
+
+ return query.ToArray();
+ }
+
private IQueryable<BaseItemEntity> ApplyGroupingFilter(IQueryable<BaseItemEntity> dbQuery, InternalItemsQuery filter)
{
// This whole block is needed to filter duplicate entries on request
diff --git a/MediaBrowser.Controller/Library/ILibraryManager.cs b/MediaBrowser.Controller/Library/ILibraryManager.cs
index 47b1cb16e..03a28fd8c 100644
--- a/MediaBrowser.Controller/Library/ILibraryManager.cs
+++ b/MediaBrowser.Controller/Library/ILibraryManager.cs
@@ -566,6 +566,15 @@ namespace MediaBrowser.Controller.Library
IReadOnlyList<BaseItem> GetItemList(InternalItemsQuery query, List<BaseItem> parents);
/// <summary>
+ /// Gets the list of series presentation keys for next up.
+ /// </summary>
+ /// <param name="query">The query to use.</param>
+ /// <param name="parents">Items to use for query.</param>
+ /// <param name="dateCutoff">The minimum date for a series to have been most recently watched.</param>
+ /// <returns>List of series presentation keys.</returns>
+ IReadOnlyList<string> GetNextUpSeriesKeys(InternalItemsQuery query, IReadOnlyCollection<BaseItem> parents, DateTime dateCutoff);
+
+ /// <summary>
/// Gets the items result.
/// </summary>
/// <param name="query">The query.</param>
diff --git a/MediaBrowser.Controller/Persistence/IItemRepository.cs b/MediaBrowser.Controller/Persistence/IItemRepository.cs
index afe2d833d..f1ed4fe27 100644
--- a/MediaBrowser.Controller/Persistence/IItemRepository.cs
+++ b/MediaBrowser.Controller/Persistence/IItemRepository.cs
@@ -60,6 +60,14 @@ public interface IItemRepository
IReadOnlyList<BaseItem> GetItemList(InternalItemsQuery filter);
/// <summary>
+ /// Gets the list of series presentation keys for next up.
+ /// </summary>
+ /// <param name="filter">The query.</param>
+ /// <param name="dateCutoff">The minimum date for a series to have been most recently watched.</param>
+ /// <returns>The list of keys.</returns>
+ IReadOnlyList<string> GetNextUpSeriesKeys(InternalItemsQuery filter, DateTime dateCutoff);
+
+ /// <summary>
/// Updates the inherited values.
/// </summary>
void UpdateInheritedValues();
diff --git a/MediaBrowser.Model/Querying/NextUpQuery.cs b/MediaBrowser.Model/Querying/NextUpQuery.cs
index 8dece28a0..aee720aa7 100644
--- a/MediaBrowser.Model/Querying/NextUpQuery.cs
+++ b/MediaBrowser.Model/Querying/NextUpQuery.cs
@@ -4,76 +4,69 @@ using System;
using Jellyfin.Data.Entities;
using MediaBrowser.Model.Entities;
-namespace MediaBrowser.Model.Querying
+namespace MediaBrowser.Model.Querying;
+
+public class NextUpQuery
{
- public class NextUpQuery
+ public NextUpQuery()
{
- public NextUpQuery()
- {
- EnableImageTypes = Array.Empty<ImageType>();
- EnableTotalRecordCount = true;
- DisableFirstEpisode = false;
- NextUpDateCutoff = DateTime.MinValue;
- EnableResumable = false;
- EnableRewatching = false;
- }
-
- /// <summary>
- /// Gets or sets the user.
- /// </summary>
- /// <value>The user.</value>
- public required User User { get; set; }
+ EnableImageTypes = Array.Empty<ImageType>();
+ EnableTotalRecordCount = true;
+ NextUpDateCutoff = DateTime.MinValue;
+ EnableResumable = false;
+ EnableRewatching = false;
+ }
- /// <summary>
- /// Gets or sets the parent identifier.
- /// </summary>
- /// <value>The parent identifier.</value>
- public Guid? ParentId { get; set; }
+ /// <summary>
+ /// Gets or sets the user.
+ /// </summary>
+ /// <value>The user.</value>
+ public required User User { get; set; }
- /// <summary>
- /// Gets or sets the series id.
- /// </summary>
- /// <value>The series id.</value>
- public Guid? SeriesId { get; set; }
+ /// <summary>
+ /// Gets or sets the parent identifier.
+ /// </summary>
+ /// <value>The parent identifier.</value>
+ public Guid? ParentId { get; set; }
- /// <summary>
- /// Gets or sets the start index. Use for paging.
- /// </summary>
- /// <value>The start index.</value>
- public int? StartIndex { get; set; }
+ /// <summary>
+ /// Gets or sets the series id.
+ /// </summary>
+ /// <value>The series id.</value>
+ public Guid? SeriesId { get; set; }
- /// <summary>
- /// Gets or sets the maximum number of items to return.
- /// </summary>
- /// <value>The limit.</value>
- public int? Limit { get; set; }
+ /// <summary>
+ /// Gets or sets the start index. Use for paging.
+ /// </summary>
+ /// <value>The start index.</value>
+ public int? StartIndex { get; set; }
- /// <summary>
- /// Gets or sets the enable image types.
- /// </summary>
- /// <value>The enable image types.</value>
- public ImageType[] EnableImageTypes { get; set; }
+ /// <summary>
+ /// Gets or sets the maximum number of items to return.
+ /// </summary>
+ /// <value>The limit.</value>
+ public int? Limit { get; set; }
- public bool EnableTotalRecordCount { get; set; }
+ /// <summary>
+ /// Gets or sets the enable image types.
+ /// </summary>
+ /// <value>The enable image types.</value>
+ public ImageType[] EnableImageTypes { get; set; }
- /// <summary>
- /// Gets or sets a value indicating whether do disable sending first episode as next up.
- /// </summary>
- public bool DisableFirstEpisode { get; set; }
+ public bool EnableTotalRecordCount { get; set; }
- /// <summary>
- /// Gets or sets a value indicating the oldest date for a show to appear in Next Up.
- /// </summary>
- public DateTime NextUpDateCutoff { get; set; }
+ /// <summary>
+ /// Gets or sets a value indicating the oldest date for a show to appear in Next Up.
+ /// </summary>
+ public DateTime NextUpDateCutoff { get; set; }
- /// <summary>
- /// Gets or sets a value indicating whether to include resumable episodes as next up.
- /// </summary>
- public bool EnableResumable { get; set; }
+ /// <summary>
+ /// Gets or sets a value indicating whether to include resumable episodes as next up.
+ /// </summary>
+ public bool EnableResumable { get; set; }
- /// <summary>
- /// Gets or sets a value indicating whether getting rewatching next up list.
- /// </summary>
- public bool EnableRewatching { get; set; }
- }
+ /// <summary>
+ /// Gets or sets a value indicating whether getting rewatching next up list.
+ /// </summary>
+ public bool EnableRewatching { get; set; }
}