diff options
| -rw-r--r-- | Emby.Server.Implementations/Library/LibraryManager.cs | 15 | ||||
| -rw-r--r-- | Emby.Server.Implementations/TV/TVSeriesManager.cs | 61 | ||||
| -rw-r--r-- | Jellyfin.Api/Controllers/TvShowsController.cs | 4 | ||||
| -rw-r--r-- | Jellyfin.Server.Implementations/Item/BaseItemRepository.cs | 31 | ||||
| -rw-r--r-- | MediaBrowser.Controller/Library/ILibraryManager.cs | 9 | ||||
| -rw-r--r-- | MediaBrowser.Controller/Persistence/IItemRepository.cs | 8 | ||||
| -rw-r--r-- | MediaBrowser.Model/Querying/NextUpQuery.cs | 113 |
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; } } |
