using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using Jellyfin.Api.Constants;
using Jellyfin.Api.Extensions;
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.Entities.Movies;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.LiveTv;
using MediaBrowser.Model.Dto;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.Querying;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
namespace Jellyfin.Api.Controllers
{
///
/// Movies controller.
///
[Authorize(Policy = Policies.DefaultAuthorization)]
public class MoviesController : BaseJellyfinApiController
{
private readonly IUserManager _userManager;
private readonly ILibraryManager _libraryManager;
private readonly IDtoService _dtoService;
private readonly IServerConfigurationManager _serverConfigurationManager;
///
/// Initializes a new instance of the class.
///
/// Instance of the interface.
/// Instance of the interface.
/// Instance of the interface.
/// Instance of the interface.
public MoviesController(
IUserManager userManager,
ILibraryManager libraryManager,
IDtoService dtoService,
IServerConfigurationManager serverConfigurationManager)
{
_userManager = userManager;
_libraryManager = libraryManager;
_dtoService = dtoService;
_serverConfigurationManager = serverConfigurationManager;
}
///
/// Gets movie recommendations.
///
/// Optional. Filter by user id, and attach user data.
/// Specify this to localize the search to a specific item or folder. Omit to use the root.
/// Optional. The fields to return.
/// The max number of categories to return.
/// The max number of items to return per category.
/// Movie recommendations returned.
/// The list of movie recommendations.
[HttpGet("Recommendations")]
public ActionResult> GetMovieRecommendations(
[FromQuery] Guid? userId,
[FromQuery] Guid? parentId,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
[FromQuery] int categoryLimit = 5,
[FromQuery] int itemLimit = 8)
{
var user = userId.HasValue && !userId.Equals(Guid.Empty)
? _userManager.GetUserById(userId.Value)
: null;
var dtoOptions = new DtoOptions { Fields = fields }
.AddClientFields(Request);
var categories = new List();
var parentIdGuid = parentId ?? Guid.Empty;
var query = new InternalItemsQuery(user)
{
IncludeItemTypes = new[]
{
nameof(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 { nameof(Movie) };
if (_serverConfigurationManager.Configuration.EnableExternalContentInSuggestions)
{
itemTypes.Add(nameof(Trailer));
itemTypes.Add(nameof(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>
{
// 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));
}
private IEnumerable GetWithDirector(
User? user,
IEnumerable names,
int itemLimit,
DtoOptions dtoOptions,
RecommendationType type)
{
var itemTypes = new List { nameof(Movie) };
if (_serverConfigurationManager.Configuration.EnableExternalContentInSuggestions)
{
itemTypes.Add(nameof(Trailer));
itemTypes.Add(nameof(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
}).GroupBy(i => i.GetProviderId(MediaBrowser.Model.Entities.MetadataProvider.Imdb) ?? Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture))
.Select(x => x.First())
.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 GetWithActor(User? user, IEnumerable names, int itemLimit, DtoOptions dtoOptions, RecommendationType type)
{
var itemTypes = new List { nameof(Movie) };
if (_serverConfigurationManager.Configuration.EnableExternalContentInSuggestions)
{
itemTypes.Add(nameof(Trailer));
itemTypes.Add(nameof(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
}).GroupBy(i => i.GetProviderId(MediaBrowser.Model.Entities.MetadataProvider.Imdb) ?? Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture))
.Select(x => x.First())
.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 GetSimilarTo(User? user, IEnumerable baselineItems, int itemLimit, DtoOptions dtoOptions, RecommendationType type)
{
var itemTypes = new List { nameof(Movie) };
if (_serverConfigurationManager.Configuration.EnableExternalContentInSuggestions)
{
itemTypes.Add(nameof(Trailer));
itemTypes.Add(nameof(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 GetActors(IEnumerable items)
{
var people = _libraryManager.GetPeople(new InternalPeopleQuery(Array.Empty(), 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 GetDirectors(IEnumerable items)
{
var people = _libraryManager.GetPeople(new InternalPeopleQuery(
new[] { PersonType.Director },
Array.Empty()));
var itemIds = items.Select(i => i.Id).ToList();
return people
.Where(i => itemIds.Contains(i.ItemId))
.Select(i => i.Name)
.DistinctNames();
}
}
}