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.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(Policy = Policies.DefaultAuthorization)]
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 )
{
var user = userId is null | | userId . Value . Equals ( default )
? null
: _userManager . GetUserById ( userId . Value ) ;
var dtoOptions = new DtoOptions { Fields = fields }
. AddClientFields ( Request ) ;
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 ) ) ;
}
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
} ) . 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 < 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
} ) . 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 < 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 ( ) ;
}
}
}