Browse Source

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
Cody Robibero 2 tháng trước cách đây
mục cha
commit
85b5bebda4

+ 15 - 0
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)

+ 10 - 51
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)
                     {

+ 2 - 2
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

+ 31 - 0
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

+ 9 - 0
MediaBrowser.Controller/Library/ILibraryManager.cs

@@ -565,6 +565,15 @@ namespace MediaBrowser.Controller.Library
         /// <returns>List of items.</returns>
         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>

+ 8 - 0
MediaBrowser.Controller/Persistence/IItemRepository.cs

@@ -59,6 +59,14 @@ public interface IItemRepository
     /// <returns>List&lt;BaseItem&gt;.</returns>
     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>

+ 53 - 60
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; }
 }