327 lines
12 KiB
C#
327 lines
12 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Globalization;
|
|
using System.Linq;
|
|
using Jellyfin.Api.Extensions;
|
|
using Jellyfin.Api.Helpers;
|
|
using Jellyfin.Api.ModelBinders;
|
|
using Jellyfin.Data.Entities;
|
|
using Jellyfin.Data.Enums;
|
|
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;
|
|
using MediaBrowser.Model.Querying;
|
|
using Microsoft.AspNetCore.Authorization;
|
|
using Microsoft.AspNetCore.Mvc;
|
|
|
|
namespace Jellyfin.Api.Controllers;
|
|
|
|
/// <summary>
|
|
/// Movies controller.
|
|
/// </summary>
|
|
[Authorize]
|
|
public class MoviesController : BaseJellyfinApiController
|
|
{
|
|
private readonly IUserManager _userManager;
|
|
private readonly ILibraryManager _libraryManager;
|
|
private readonly IDtoService _dtoService;
|
|
private readonly IServerConfigurationManager _serverConfigurationManager;
|
|
|
|
/// <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>
|
|
public MoviesController(
|
|
IUserManager userManager,
|
|
ILibraryManager libraryManager,
|
|
IDtoService dtoService,
|
|
IServerConfigurationManager serverConfigurationManager)
|
|
{
|
|
_userManager = userManager;
|
|
_libraryManager = libraryManager;
|
|
_dtoService = dtoService;
|
|
_serverConfigurationManager = serverConfigurationManager;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets movie recommendations.
|
|
/// </summary>
|
|
/// <param name="userId">Optional. Filter by user id, and attach user data.</param>
|
|
/// <param name="parentId">Specify this to localize the search to a specific item or folder. Omit to use the root.</param>
|
|
/// <param name="fields">Optional. The fields to return.</param>
|
|
/// <param name="categoryLimit">The max number of categories to return.</param>
|
|
/// <param name="itemLimit">The max number of items to return per category.</param>
|
|
/// <response code="200">Movie recommendations returned.</response>
|
|
/// <returns>The list of movie recommendations.</returns>
|
|
[HttpGet("Recommendations")]
|
|
public ActionResult<IEnumerable<RecommendationDto>> GetMovieRecommendations(
|
|
[FromQuery] Guid? userId,
|
|
[FromQuery] Guid? parentId,
|
|
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
|
|
[FromQuery] int categoryLimit = 5,
|
|
[FromQuery] int itemLimit = 8)
|
|
{
|
|
userId = RequestHelpers.GetUserId(User, userId);
|
|
var user = userId.Value.Equals(default)
|
|
? null
|
|
: _userManager.GetUserById(userId.Value);
|
|
var dtoOptions = new DtoOptions { Fields = fields }
|
|
.AddClientFields(User);
|
|
|
|
var categories = new List<RecommendationDto>();
|
|
|
|
var parentIdGuid = parentId ?? Guid.Empty;
|
|
|
|
var query = new InternalItemsQuery(user)
|
|
{
|
|
IncludeItemTypes = new[]
|
|
{
|
|
BaseItemKind.Movie,
|
|
// nameof(Trailer),
|
|
// nameof(LiveTvProgram)
|
|
},
|
|
// IsMovie = true
|
|
OrderBy = new[] { (ItemSortBy.DatePlayed, SortOrder.Descending), (ItemSortBy.Random, SortOrder.Descending) },
|
|
Limit = 7,
|
|
ParentId = parentIdGuid,
|
|
Recursive = true,
|
|
IsPlayed = 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.GetRange(0, Math.Min(recentlyPlayedMovies.Count, 6));
|
|
// Get recently played directors
|
|
var recentDirectors = GetDirectors(mostRecentMovies)
|
|
.ToList();
|
|
|
|
// Get recently played actors
|
|
var recentActors = GetActors(mostRecentMovies)
|
|
.ToList();
|
|
|
|
var similarToRecentlyPlayed = GetSimilarTo(user, recentlyPlayedMovies, itemLimit, dtoOptions, RecommendationType.SimilarToRecentlyPlayed).GetEnumerator();
|
|
var similarToLiked = GetSimilarTo(user, likedMovies, itemLimit, dtoOptions, RecommendationType.SimilarToLikedItem).GetEnumerator();
|
|
|
|
var hasDirectorFromRecentlyPlayed = GetWithDirector(user, recentDirectors, itemLimit, dtoOptions, RecommendationType.HasDirectorFromRecentlyPlayed).GetEnumerator();
|
|
var hasActorFromRecentlyPlayed = GetWithActor(user, recentActors, itemLimit, dtoOptions, RecommendationType.HasActorFromRecentlyPlayed).GetEnumerator();
|
|
|
|
var categoryTypes = new List<IEnumerator<RecommendationDto>>
|
|
{
|
|
// Give this extra weight
|
|
similarToRecentlyPlayed,
|
|
similarToRecentlyPlayed,
|
|
|
|
// Give this extra weight
|
|
similarToLiked,
|
|
similarToLiked,
|
|
hasDirectorFromRecentlyPlayed,
|
|
hasActorFromRecentlyPlayed
|
|
};
|
|
|
|
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 Ok(categories.OrderBy(i => i.RecommendationType).AsEnumerable());
|
|
}
|
|
|
|
private IEnumerable<RecommendationDto> GetWithDirector(
|
|
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);
|
|
}
|
|
|
|
foreach (var name in names)
|
|
{
|
|
var items = _libraryManager.GetItemList(
|
|
new InternalItemsQuery(user)
|
|
{
|
|
Person = name,
|
|
// Account for duplicates by IMDb id, since the database doesn't support this yet
|
|
Limit = itemLimit + 2,
|
|
PersonTypes = new[] { PersonType.Director },
|
|
IncludeItemTypes = itemTypes.ToArray(),
|
|
IsMovie = true,
|
|
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)
|
|
{
|
|
var returnItems = _dtoService.GetBaseItemDtos(items, dtoOptions, user);
|
|
|
|
yield return new RecommendationDto
|
|
{
|
|
BaselineItemName = name,
|
|
CategoryId = name.GetMD5(),
|
|
RecommendationType = type,
|
|
Items = returnItems
|
|
};
|
|
}
|
|
}
|
|
}
|
|
|
|
private IEnumerable<RecommendationDto> GetWithActor(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);
|
|
}
|
|
|
|
foreach (var name in names)
|
|
{
|
|
var items = _libraryManager.GetItemList(new InternalItemsQuery(user)
|
|
{
|
|
Person = name,
|
|
// Account for duplicates by IMDb id, since the database doesn't support this yet
|
|
Limit = itemLimit + 2,
|
|
IncludeItemTypes = itemTypes.ToArray(),
|
|
IsMovie = true,
|
|
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)
|
|
{
|
|
var returnItems = _dtoService.GetBaseItemDtos(items, dtoOptions, user);
|
|
|
|
yield return new RecommendationDto
|
|
{
|
|
BaselineItemName = name,
|
|
CategoryId = name.GetMD5(),
|
|
RecommendationType = type,
|
|
Items = returnItems
|
|
};
|
|
}
|
|
}
|
|
}
|
|
|
|
private IEnumerable<RecommendationDto> GetSimilarTo(User? user, IEnumerable<BaseItem> baselineItems, 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);
|
|
}
|
|
|
|
foreach (var item in baselineItems)
|
|
{
|
|
var similar = _libraryManager.GetItemList(new InternalItemsQuery(user)
|
|
{
|
|
Limit = itemLimit,
|
|
IncludeItemTypes = itemTypes.ToArray(),
|
|
IsMovie = true,
|
|
SimilarTo = item,
|
|
EnableGroupByMetadataKey = true,
|
|
DtoOptions = dtoOptions
|
|
});
|
|
|
|
if (similar.Count > 0)
|
|
{
|
|
var returnItems = _dtoService.GetBaseItemDtos(similar, dtoOptions, user);
|
|
|
|
yield return new RecommendationDto
|
|
{
|
|
BaselineItemName = item.Name,
|
|
CategoryId = item.Id,
|
|
RecommendationType = type,
|
|
Items = returnItems
|
|
};
|
|
}
|
|
}
|
|
}
|
|
|
|
private IEnumerable<string> GetActors(IEnumerable<BaseItem> items)
|
|
{
|
|
var people = _libraryManager.GetPeople(new InternalPeopleQuery(Array.Empty<string>(), new[] { PersonType.Director })
|
|
{
|
|
MaxListOrder = 3
|
|
});
|
|
|
|
var itemIds = items.Select(i => i.Id).ToList();
|
|
|
|
return people
|
|
.Where(i => itemIds.Contains(i.ItemId))
|
|
.Select(i => i.Name)
|
|
.DistinctNames();
|
|
}
|
|
|
|
private IEnumerable<string> GetDirectors(IEnumerable<BaseItem> items)
|
|
{
|
|
var people = _libraryManager.GetPeople(new InternalPeopleQuery(
|
|
new[] { PersonType.Director },
|
|
Array.Empty<string>()));
|
|
|
|
var itemIds = items.Select(i => i.Id).ToList();
|
|
|
|
return people
|
|
.Where(i => itemIds.Contains(i.ItemId))
|
|
.Select(i => i.Name)
|
|
.DistinctNames();
|
|
}
|
|
}
|