aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorShadowghost <Ghost_of_Stone@web.de>2026-05-16 09:44:36 +0200
committerShadowghost <Ghost_of_Stone@web.de>2026-05-16 09:44:36 +0200
commit1fdf58e40f7c8f58377be3716368720923d8d8c0 (patch)
tree7d6311ca8055acb9c898206f49afff82b79d5684
parent97c20e6ac5bf89aa0a29f950b9308036e589de12 (diff)
Address review comments
-rw-r--r--Emby.Server.Implementations/Library/SimilarItems/MovieSimilarItemsProvider.cs1
-rw-r--r--Emby.Server.Implementations/Library/SimilarItems/SimilarItemsManager.cs205
-rw-r--r--Jellyfin.Api/Controllers/MoviesController.cs257
-rw-r--r--MediaBrowser.Controller/Library/ISimilarItemsManager.cs24
-rw-r--r--MediaBrowser.Controller/Library/SimilarItemsRecommendation.cs32
5 files changed, 258 insertions, 261 deletions
diff --git a/Emby.Server.Implementations/Library/SimilarItems/MovieSimilarItemsProvider.cs b/Emby.Server.Implementations/Library/SimilarItems/MovieSimilarItemsProvider.cs
index 5660cf30a8..54466a6ad9 100644
--- a/Emby.Server.Implementations/Library/SimilarItems/MovieSimilarItemsProvider.cs
+++ b/Emby.Server.Implementations/Library/SimilarItems/MovieSimilarItemsProvider.cs
@@ -188,7 +188,6 @@ public sealed class MovieSimilarItemsProvider : ILocalSimilarItemsProvider<Movie
var entitiesById = _queryHelpers.ApplyNavigations(
context.BaseItems.AsNoTracking().Where(e => allOrderedIdsList.Contains(e.Id)),
filter)
- .AsSplitQuery()
.AsEnumerable()
.Select(e => _queryHelpers.DeserializeBaseItem(e, filter.SkipDeserialization))
.Where(dto => dto is not null)
diff --git a/Emby.Server.Implementations/Library/SimilarItems/SimilarItemsManager.cs b/Emby.Server.Implementations/Library/SimilarItems/SimilarItemsManager.cs
index d4ee643f95..fc83817015 100644
--- a/Emby.Server.Implementations/Library/SimilarItems/SimilarItemsManager.cs
+++ b/Emby.Server.Implementations/Library/SimilarItems/SimilarItemsManager.cs
@@ -8,12 +8,16 @@ using System.Threading;
using System.Threading.Tasks;
using Jellyfin.Data.Enums;
using Jellyfin.Database.Implementations.Entities;
+using Jellyfin.Database.Implementations.Enums;
using Jellyfin.Extensions.Json;
+using MediaBrowser.Common.Extensions;
using MediaBrowser.Controller;
+using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Dto;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library;
using MediaBrowser.Model.Configuration;
+using MediaBrowser.Model.Dto;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.IO;
using MediaBrowser.Model.Querying;
@@ -30,6 +34,7 @@ public class SimilarItemsManager : ISimilarItemsManager
private readonly IServerApplicationPaths _appPaths;
private readonly ILibraryManager _libraryManager;
private readonly IFileSystem _fileSystem;
+ private readonly IServerConfigurationManager _serverConfigurationManager;
private ISimilarItemsProvider[] _similarItemsProviders = [];
/// <summary>
@@ -39,16 +44,19 @@ public class SimilarItemsManager : ISimilarItemsManager
/// <param name="appPaths">The server application paths.</param>
/// <param name="libraryManager">The library manager.</param>
/// <param name="fileSystem">The file system.</param>
+ /// <param name="serverConfigurationManager">The server configuration manager.</param>
public SimilarItemsManager(
ILogger<SimilarItemsManager> logger,
IServerApplicationPaths appPaths,
ILibraryManager libraryManager,
- IFileSystem fileSystem)
+ IFileSystem fileSystem,
+ IServerConfigurationManager serverConfigurationManager)
{
_logger = logger;
_appPaths = appPaths;
_libraryManager = libraryManager;
_fileSystem = fileSystem;
+ _serverConfigurationManager = serverConfigurationManager;
}
/// <inheritdoc/>
@@ -226,20 +234,205 @@ public class SimilarItemsManager : ISimilarItemsManager
}
/// <inheritdoc/>
- public Task<Dictionary<Guid, IReadOnlyList<BaseItem>>> GetBatchSimilarItemsAsync(
- IReadOnlyList<BaseItem> sourceItems,
+ public async Task<IReadOnlyList<SimilarItemsRecommendation>> GetMovieRecommendationsAsync(
+ User? user,
+ Guid parentId,
+ int categoryLimit,
+ int itemLimit,
+ DtoOptions dtoOptions,
+ CancellationToken cancellationToken)
+ {
+ ArgumentNullException.ThrowIfNull(dtoOptions);
+
+ var recentlyPlayedMovies = _libraryManager.GetItemList(new InternalItemsQuery(user)
+ {
+ IncludeItemTypes = [BaseItemKind.Movie],
+ OrderBy = [(ItemSortBy.DatePlayed, SortOrder.Descending), (ItemSortBy.Random, SortOrder.Descending)],
+ Limit = 7,
+ ParentId = parentId,
+ Recursive = true,
+ IsPlayed = true,
+ EnableGroupByMetadataKey = true,
+ DtoOptions = dtoOptions
+ });
+
+ var itemTypes = new List<BaseItemKind> { BaseItemKind.Movie };
+ if (_serverConfigurationManager.Configuration.EnableExternalContentInSuggestions)
+ {
+ itemTypes.Add(BaseItemKind.Trailer);
+ itemTypes.Add(BaseItemKind.LiveTvProgram);
+ }
+
+ var likedMovies = _libraryManager.GetItemList(new InternalItemsQuery(user)
+ {
+ IncludeItemTypes = itemTypes.ToArray(),
+ IsMovie = true,
+ OrderBy = [(ItemSortBy.Random, SortOrder.Descending)],
+ Limit = 10,
+ IsFavoriteOrLiked = true,
+ ExcludeItemIds = recentlyPlayedMovies.Select(i => i.Id).ToArray(),
+ EnableGroupByMetadataKey = true,
+ ParentId = parentId,
+ Recursive = true,
+ DtoOptions = dtoOptions
+ });
+
+ var mostRecentMovies = recentlyPlayedMovies.Take(Math.Min(recentlyPlayedMovies.Count, 6)).ToList();
+ var recentDirectors = GetPeopleNames(mostRecentMovies, [PersonType.Director]);
+ var recentActors = GetPeopleNames(mostRecentMovies, [PersonType.Actor, PersonType.GuestStar]);
+
+ // Cap baseline items to categoryLimit - the round-robin can't use more categories than that.
+ var recentlyPlayedBaseline = recentlyPlayedMovies.Count > categoryLimit
+ ? recentlyPlayedMovies.Take(categoryLimit).ToList()
+ : recentlyPlayedMovies;
+ var likedBaseline = likedMovies.Count > categoryLimit
+ ? likedMovies.Take(categoryLimit).ToList()
+ : likedMovies;
+
+ var batchQuery = new SimilarItemsQuery
+ {
+ User = user,
+ Limit = itemLimit,
+ DtoOptions = dtoOptions
+ };
+
+ var similarToRecentlyPlayed = await GetSimilarItemsRecommendationsAsync(
+ recentlyPlayedBaseline,
+ RecommendationType.SimilarToRecentlyPlayed,
+ batchQuery).ConfigureAwait(false);
+
+ var similarToLiked = await GetSimilarItemsRecommendationsAsync(
+ likedBaseline,
+ RecommendationType.SimilarToLikedItem,
+ batchQuery).ConfigureAwait(false);
+
+ var hasDirectorFromRecentlyPlayed = GetPersonRecommendations(user, recentDirectors, itemLimit, dtoOptions, RecommendationType.HasDirectorFromRecentlyPlayed, itemTypes);
+ var hasActorFromRecentlyPlayed = GetPersonRecommendations(user, recentActors, itemLimit, dtoOptions, RecommendationType.HasActorFromRecentlyPlayed, itemTypes);
+
+ // Use a single enumerator per list, listed twice so MoveNext advances it
+ // twice per round-robin pass (giving these categories double weight).
+ // IMPORTANT: Declare as IEnumerator<T> to box the List<T>.Enumerator struct once;
+ // using var would box separately per list insertion, creating independent copies.
+ IEnumerator<SimilarItemsRecommendation> similarToRecentlyPlayedEnum = similarToRecentlyPlayed.GetEnumerator();
+ IEnumerator<SimilarItemsRecommendation> similarToLikedEnum = similarToLiked.GetEnumerator();
+
+ var categoryTypes = new List<IEnumerator<SimilarItemsRecommendation>>
+ {
+ similarToRecentlyPlayedEnum,
+ similarToRecentlyPlayedEnum,
+ similarToLikedEnum,
+ similarToLikedEnum,
+ hasDirectorFromRecentlyPlayed.GetEnumerator(),
+ hasActorFromRecentlyPlayed.GetEnumerator()
+ };
+
+ var categories = new List<SimilarItemsRecommendation>();
+ while (categories.Count < categoryLimit)
+ {
+ var allEmpty = true;
+ foreach (var category in categoryTypes)
+ {
+ if (category.MoveNext())
+ {
+ categories.Add(category.Current);
+ allEmpty = false;
+
+ if (categories.Count >= categoryLimit)
+ {
+ break;
+ }
+ }
+ }
+
+ if (allEmpty)
+ {
+ break;
+ }
+ }
+
+ return [.. categories.OrderBy(i => i.RecommendationType)];
+ }
+
+ private async Task<IReadOnlyList<SimilarItemsRecommendation>> GetSimilarItemsRecommendationsAsync(
+ IReadOnlyList<BaseItem> baselineItems,
+ RecommendationType recommendationType,
SimilarItemsQuery query)
{
var batchProvider = _similarItemsProviders
.OfType<IBatchLocalSimilarItemsProvider>()
.FirstOrDefault();
- if (batchProvider is null)
+ if (batchProvider is null || baselineItems.Count == 0)
{
- return Task.FromResult(new Dictionary<Guid, IReadOnlyList<BaseItem>>());
+ return [];
}
- return batchProvider.GetBatchSimilarItemsAsync(sourceItems, query);
+ var batchResults = await batchProvider.GetBatchSimilarItemsAsync(baselineItems, query).ConfigureAwait(false);
+
+ var recommendations = new List<SimilarItemsRecommendation>(baselineItems.Count);
+ foreach (var baseline in baselineItems)
+ {
+ if (batchResults.TryGetValue(baseline.Id, out var similar) && similar.Count > 0)
+ {
+ recommendations.Add(new SimilarItemsRecommendation
+ {
+ BaselineItemName = baseline.Name,
+ CategoryId = baseline.Id,
+ RecommendationType = recommendationType,
+ Items = similar
+ });
+ }
+ }
+
+ return recommendations;
+ }
+
+ private IEnumerable<SimilarItemsRecommendation> GetPersonRecommendations(
+ User? user,
+ IReadOnlyList<string> names,
+ int itemLimit,
+ DtoOptions dtoOptions,
+ RecommendationType type,
+ IReadOnlyList<BaseItemKind> itemTypes)
+ {
+ var personTypes = type == RecommendationType.HasDirectorFromRecentlyPlayed
+ ? [PersonType.Director]
+ : Array.Empty<string>();
+
+ foreach (var name in names)
+ {
+ var items = _libraryManager.GetItemList(new InternalItemsQuery(user)
+ {
+ Person = name,
+ Limit = itemLimit + 2,
+ PersonTypes = personTypes,
+ IncludeItemTypes = itemTypes.ToArray(),
+ IsMovie = true,
+ IsPlayed = false,
+ EnableGroupByMetadataKey = true,
+ DtoOptions = dtoOptions
+ })
+ .DistinctBy(i => i.GetProviderId(MetadataProvider.Imdb) ?? Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture))
+ .Take(itemLimit)
+ .ToList();
+
+ if (items.Count > 0)
+ {
+ yield return new SimilarItemsRecommendation
+ {
+ BaselineItemName = name,
+ CategoryId = name.GetMD5(),
+ RecommendationType = type,
+ Items = items
+ };
+ }
+ }
+ }
+
+ private IReadOnlyList<string> GetPeopleNames(IReadOnlyList<BaseItem> items, IReadOnlyList<string> personTypes)
+ {
+ var itemIds = items.Select(i => i.Id).ToArray();
+ return _libraryManager.GetPeopleNamesByItems(itemIds, personTypes, limit: 0);
}
private List<(BaseItem Item, float Score)> ResolveRemoteReferences(
diff --git a/Jellyfin.Api/Controllers/MoviesController.cs b/Jellyfin.Api/Controllers/MoviesController.cs
index 9c0e216f12..a1f2fe7ce7 100644
--- a/Jellyfin.Api/Controllers/MoviesController.cs
+++ b/Jellyfin.Api/Controllers/MoviesController.cs
@@ -1,20 +1,13 @@
using System;
using System.Collections.Generic;
-using System.Globalization;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Jellyfin.Api.Extensions;
using Jellyfin.Api.Helpers;
using Jellyfin.Api.ModelBinders;
-using Jellyfin.Data.Enums;
-using Jellyfin.Database.Implementations.Entities;
-using Jellyfin.Database.Implementations.Enums;
using Jellyfin.Extensions;
-using MediaBrowser.Common.Extensions;
-using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Dto;
-using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library;
using MediaBrowser.Model.Dto;
using MediaBrowser.Model.Entities;
@@ -33,30 +26,22 @@ namespace Jellyfin.Api.Controllers;
public class MoviesController : BaseJellyfinApiController
{
private readonly IUserManager _userManager;
- private readonly ILibraryManager _libraryManager;
private readonly IDtoService _dtoService;
- private readonly IServerConfigurationManager _serverConfigurationManager;
private readonly ISimilarItemsManager _similarItemsManager;
/// <summary>
/// Initializes a new instance of the <see cref="MoviesController"/> class.
/// </summary>
/// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param>
- /// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
/// <param name="dtoService">Instance of the <see cref="IDtoService"/> interface.</param>
- /// <param name="serverConfigurationManager">Instance of the <see cref="IServerConfigurationManager"/> interface.</param>
/// <param name="similarItemsManager">Instance of the <see cref="ISimilarItemsManager"/> interface.</param>
public MoviesController(
IUserManager userManager,
- ILibraryManager libraryManager,
IDtoService dtoService,
- IServerConfigurationManager serverConfigurationManager,
ISimilarItemsManager similarItemsManager)
{
_userManager = userManager;
- _libraryManager = libraryManager;
_dtoService = dtoService;
- _serverConfigurationManager = serverConfigurationManager;
_similarItemsManager = similarItemsManager;
}
@@ -72,7 +57,7 @@ public class MoviesController : BaseJellyfinApiController
/// <response code="200">Movie recommendations returned.</response>
/// <returns>The list of movie recommendations.</returns>
[HttpGet("Recommendations")]
- public Task<ActionResult<IEnumerable<RecommendationDto>>> GetMovieRecommendations(
+ public async Task<ActionResult<IEnumerable<RecommendationDto>>> GetMovieRecommendations(
[FromQuery] Guid? userId,
[FromQuery] Guid? parentId,
[FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] ItemFields[] fields,
@@ -86,238 +71,16 @@ public class MoviesController : BaseJellyfinApiController
: _userManager.GetUserById(userId.Value);
var dtoOptions = new DtoOptions { Fields = fields };
- var categories = new List<RecommendationDto>();
+ var recommendations = await _similarItemsManager
+ .GetMovieRecommendationsAsync(user, parentId ?? Guid.Empty, categoryLimit, itemLimit, dtoOptions, cancellationToken)
+ .ConfigureAwait(false);
- var parentIdGuid = parentId ?? Guid.Empty;
-
- var query = new InternalItemsQuery(user)
- {
- IncludeItemTypes = new[]
- {
- BaseItemKind.Movie,
- },
- OrderBy = new[] { (ItemSortBy.DatePlayed, SortOrder.Descending), (ItemSortBy.Random, SortOrder.Descending) },
- Limit = 7,
- ParentId = parentIdGuid,
- Recursive = true,
- IsPlayed = true,
- EnableGroupByMetadataKey = true,
- DtoOptions = dtoOptions
- };
-
- var recentlyPlayedMovies = _libraryManager.GetItemList(query);
-
- var itemTypes = new List<BaseItemKind> { BaseItemKind.Movie };
- if (_serverConfigurationManager.Configuration.EnableExternalContentInSuggestions)
- {
- itemTypes.Add(BaseItemKind.Trailer);
- itemTypes.Add(BaseItemKind.LiveTvProgram);
- }
-
- var likedMovies = _libraryManager.GetItemList(new InternalItemsQuery(user)
- {
- IncludeItemTypes = itemTypes.ToArray(),
- IsMovie = true,
- OrderBy = new[] { (ItemSortBy.Random, SortOrder.Descending) },
- Limit = 10,
- IsFavoriteOrLiked = true,
- ExcludeItemIds = recentlyPlayedMovies.Select(i => i.Id).ToArray(),
- EnableGroupByMetadataKey = true,
- ParentId = parentIdGuid,
- Recursive = true,
- DtoOptions = dtoOptions
- });
-
- var mostRecentMovies = recentlyPlayedMovies.Take(Math.Min(recentlyPlayedMovies.Count, 6)).ToList();
- var recentDirectors = GetDirectors(mostRecentMovies).ToList();
- var recentActors = GetActors(mostRecentMovies).ToList();
-
- // Cap baseline items to categoryLimit - the round-robin can't use more categories than that.
- var recentlyPlayedBaseline = recentlyPlayedMovies.Count > categoryLimit
- ? recentlyPlayedMovies.Take(categoryLimit).ToList()
- : recentlyPlayedMovies;
- var likedBaseline = likedMovies.Count > categoryLimit
- ? likedMovies.Take(categoryLimit).ToList()
- : likedMovies;
-
- var batchQuery = new SimilarItemsQuery
- {
- User = user,
- Limit = itemLimit,
- DtoOptions = dtoOptions
- };
-
- var similarToRecentlyPlayed = BuildPendingFromBatch(
- _similarItemsManager.GetBatchSimilarItemsAsync(recentlyPlayedBaseline, batchQuery),
- recentlyPlayedBaseline,
- RecommendationType.SimilarToRecentlyPlayed);
-
- var similarToLiked = BuildPendingFromBatch(
- _similarItemsManager.GetBatchSimilarItemsAsync(likedBaseline, batchQuery),
- likedBaseline,
- RecommendationType.SimilarToLikedItem);
-
- var hasDirectorFromRecentlyPlayed = GetWithPerson(user, recentDirectors, itemLimit, dtoOptions, RecommendationType.HasDirectorFromRecentlyPlayed);
- var hasActorFromRecentlyPlayed = GetWithPerson(user, recentActors, itemLimit, dtoOptions, RecommendationType.HasActorFromRecentlyPlayed);
-
- // Use a single enumerator per list, listed twice so MoveNext advances it
- // twice per round-robin pass (giving these categories double weight).
- // IMPORTANT: Declare as IEnumerator<T> to box the List<T>.Enumerator struct once;
- // using var would box separately per list insertion, creating independent copies.
- IEnumerator<PendingRecommendation> similarToRecentlyPlayedEnum = similarToRecentlyPlayed.GetEnumerator();
- IEnumerator<PendingRecommendation> similarToLikedEnum = similarToLiked.GetEnumerator();
-
- var categoryTypes = new List<IEnumerator<PendingRecommendation>>
- {
- similarToRecentlyPlayedEnum,
- similarToRecentlyPlayedEnum,
- similarToLikedEnum,
- similarToLikedEnum,
- hasDirectorFromRecentlyPlayed.GetEnumerator(),
- hasActorFromRecentlyPlayed.GetEnumerator()
- };
-
- while (categories.Count < categoryLimit)
+ return Ok(recommendations.Select(r => new RecommendationDto
{
- var allEmpty = true;
-
- foreach (var category in categoryTypes)
- {
- if (category.MoveNext())
- {
- var pending = category.Current;
- var returnItems = _dtoService.GetBaseItemDtos(pending.Items, dtoOptions, user);
-
- categories.Add(new RecommendationDto
- {
- BaselineItemName = pending.BaselineItemName,
- CategoryId = pending.CategoryId,
- RecommendationType = pending.RecommendationType,
- Items = returnItems
- });
-
- allEmpty = false;
-
- if (categories.Count >= categoryLimit)
- {
- break;
- }
- }
- }
-
- if (allEmpty)
- {
- break;
- }
- }
-
- return Task.FromResult<ActionResult<IEnumerable<RecommendationDto>>>(
- Ok(categories.OrderBy(i => i.RecommendationType).AsEnumerable()));
- }
-
- private static List<PendingRecommendation> BuildPendingFromBatch(
- Task<Dictionary<Guid, IReadOnlyList<BaseItem>>> batchTask,
- IReadOnlyList<BaseItem> baselineItems,
- RecommendationType type)
- {
- var batchResults = batchTask.GetAwaiter().GetResult();
- var results = new List<PendingRecommendation>();
-
- foreach (var item in baselineItems)
- {
- if (batchResults.TryGetValue(item.Id, out var similar) && similar.Count > 0)
- {
- results.Add(new PendingRecommendation
- {
- BaselineItemName = item.Name,
- CategoryId = item.Id,
- RecommendationType = type,
- Items = similar
- });
- }
- }
-
- return results;
- }
-
- private IEnumerable<PendingRecommendation> GetWithPerson(
- User? user,
- IEnumerable<string> names,
- int itemLimit,
- DtoOptions dtoOptions,
- RecommendationType type)
- {
- var itemTypes = new List<BaseItemKind> { BaseItemKind.Movie };
- if (_serverConfigurationManager.Configuration.EnableExternalContentInSuggestions)
- {
- itemTypes.Add(BaseItemKind.Trailer);
- itemTypes.Add(BaseItemKind.LiveTvProgram);
- }
-
- var personTypes = type == RecommendationType.HasDirectorFromRecentlyPlayed
- ? [PersonType.Director]
- : Array.Empty<string>();
-
- foreach (var name in names)
- {
- var items = _libraryManager.GetItemList(
- new InternalItemsQuery(user)
- {
- Person = name,
- Limit = itemLimit + 2,
- PersonTypes = personTypes,
- IncludeItemTypes = itemTypes.ToArray(),
- IsMovie = true,
- IsPlayed = false,
- EnableGroupByMetadataKey = true,
- DtoOptions = dtoOptions
- }).DistinctBy(i => i.GetProviderId(MediaBrowser.Model.Entities.MetadataProvider.Imdb) ?? Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture))
- .Take(itemLimit)
- .ToList();
-
- if (items.Count > 0)
- {
- yield return new PendingRecommendation
- {
- BaselineItemName = name,
- CategoryId = name.GetMD5(),
- RecommendationType = type,
- Items = items
- };
- }
- }
- }
-
- private IReadOnlyList<string> GetActors(IReadOnlyList<BaseItem> items)
- {
- var itemIds = items.Select(i => i.Id).ToArray();
- return _libraryManager.GetPeopleNamesByItems(
- itemIds,
- new[] { PersonType.Actor, PersonType.GuestStar },
- limit: 0);
- }
-
- private IReadOnlyList<string> GetDirectors(IReadOnlyList<BaseItem> items)
- {
- var itemIds = items.Select(i => i.Id).ToArray();
- return _libraryManager.GetPeopleNamesByItems(
- itemIds,
- [PersonType.Director],
- limit: 0);
- }
-
- /// <summary>
- /// Holds a recommendation category's BaseItems before DTO conversion.
- /// DTO conversion is deferred until the round-robin actually selects the category.
- /// </summary>
- private sealed class PendingRecommendation
- {
- public required string BaselineItemName { get; init; }
-
- public required Guid CategoryId { get; init; }
-
- public required RecommendationType RecommendationType { get; init; }
-
- public required IReadOnlyList<BaseItem> Items { get; init; }
+ BaselineItemName = r.BaselineItemName,
+ CategoryId = r.CategoryId,
+ RecommendationType = r.RecommendationType,
+ Items = _dtoService.GetBaseItemDtos(r.Items, dtoOptions, user)
+ }));
}
}
diff --git a/MediaBrowser.Controller/Library/ISimilarItemsManager.cs b/MediaBrowser.Controller/Library/ISimilarItemsManager.cs
index 1c826ea780..36fa547eeb 100644
--- a/MediaBrowser.Controller/Library/ISimilarItemsManager.cs
+++ b/MediaBrowser.Controller/Library/ISimilarItemsManager.cs
@@ -6,6 +6,7 @@ using Jellyfin.Database.Implementations.Entities;
using MediaBrowser.Controller.Dto;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Model.Configuration;
+using MediaBrowser.Model.Dto;
namespace MediaBrowser.Controller.Library;
@@ -49,12 +50,21 @@ public interface ISimilarItemsManager
CancellationToken cancellationToken);
/// <summary>
- /// Gets similar items for multiple source items in a single batch.
+ /// Builds movie recommendations for a user: a mix of similar-items and person-based categories,
+ /// scheduled round-robin and capped to <paramref name="categoryLimit"/>.
/// </summary>
- /// <param name="sourceItems">The source items to find similar items for.</param>
- /// <param name="query">The query options.</param>
- /// <returns>Per-source-item results keyed by source item ID.</returns>
- Task<Dictionary<Guid, IReadOnlyList<BaseItem>>> GetBatchSimilarItemsAsync(
- IReadOnlyList<BaseItem> sourceItems,
- SimilarItemsQuery query);
+ /// <param name="user">The user the recommendations are for. May be <see langword="null"/> for anonymous access.</param>
+ /// <param name="parentId">The library/folder to localize the search to. Pass <see cref="Guid.Empty"/> to use the root.</param>
+ /// <param name="categoryLimit">Maximum number of recommendation categories to return.</param>
+ /// <param name="itemLimit">Maximum number of items per category.</param>
+ /// <param name="dtoOptions">DTO options used when querying the library.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <returns>The list of recommendation categories, ordered by <see cref="RecommendationType"/>.</returns>
+ Task<IReadOnlyList<SimilarItemsRecommendation>> GetMovieRecommendationsAsync(
+ User? user,
+ Guid parentId,
+ int categoryLimit,
+ int itemLimit,
+ DtoOptions dtoOptions,
+ CancellationToken cancellationToken);
}
diff --git a/MediaBrowser.Controller/Library/SimilarItemsRecommendation.cs b/MediaBrowser.Controller/Library/SimilarItemsRecommendation.cs
new file mode 100644
index 0000000000..71346fcadf
--- /dev/null
+++ b/MediaBrowser.Controller/Library/SimilarItemsRecommendation.cs
@@ -0,0 +1,32 @@
+using System;
+using System.Collections.Generic;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Model.Dto;
+
+namespace MediaBrowser.Controller.Library;
+
+/// <summary>
+/// A recommendation category derived from a baseline item, holding similar items prior to DTO conversion.
+/// </summary>
+public sealed class SimilarItemsRecommendation
+{
+ /// <summary>
+ /// Gets the display name of the baseline item the recommendation is based on.
+ /// </summary>
+ public required string BaselineItemName { get; init; }
+
+ /// <summary>
+ /// Gets an identifier for the recommendation category.
+ /// </summary>
+ public required Guid CategoryId { get; init; }
+
+ /// <summary>
+ /// Gets the recommendation type.
+ /// </summary>
+ public required RecommendationType RecommendationType { get; init; }
+
+ /// <summary>
+ /// Gets the similar items for the baseline, ordered by relevance.
+ /// </summary>
+ public required IReadOnlyList<BaseItem> Items { get; init; }
+}