mirror of
https://github.com/jellyfin/jellyfin.git
synced 2026-07-13 01:33:04 +01:00
Address review comments
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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)
|
||||
return Ok(recommendations.Select(r => new RecommendationDto
|
||||
{
|
||||
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)
|
||||
{
|
||||
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)
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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; }
|
||||
}
|
||||
Reference in New Issue
Block a user