using MediaBrowser.Common.Extensions; using MediaBrowser.Controller.Channels; using MediaBrowser.Controller.Dto; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities.Movies; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Net; using MediaBrowser.Controller.Persistence; using MediaBrowser.Model.Channels; using MediaBrowser.Model.Dto; using MediaBrowser.Model.Entities; using MediaBrowser.Model.Querying; using MoreLinq; using ServiceStack; using System; using System.Collections.Generic; using System.Linq; using System.Threading; using System.Threading.Tasks; namespace MediaBrowser.Api.Movies { /// /// Class GetSimilarMovies /// [Route("/Movies/{Id}/Similar", "GET", Summary = "Finds movies and trailers similar to a given movie.")] public class GetSimilarMovies : BaseGetSimilarItemsFromItem { } [Route("/Movies/Recommendations", "GET", Summary = "Gets movie recommendations")] public class GetMovieRecommendations : IReturn, IHasItemFields { [ApiMember(Name = "CategoryLimit", Description = "The max number of categories to return", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")] public int CategoryLimit { get; set; } [ApiMember(Name = "ItemLimit", Description = "The max number of items to return per category", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")] public int ItemLimit { get; set; } /// /// Gets or sets the user id. /// /// The user id. [ApiMember(Name = "UserId", Description = "Optional. Filter by user id, and attach user data", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")] public Guid? UserId { get; set; } /// /// Specify this to localize the search to a specific item or folder. Omit to use the root. /// /// The parent id. [ApiMember(Name = "ParentId", Description = "Specify this to localize the search to a specific item or folder. Omit to use the root", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")] public string ParentId { get; set; } public GetMovieRecommendations() { CategoryLimit = 5; ItemLimit = 8; } public string Fields { get; set; } } /// /// Class MoviesService /// [Authenticated] public class MoviesService : BaseApiService { /// /// The _user manager /// private readonly IUserManager _userManager; /// /// The _user data repository /// private readonly IUserDataManager _userDataRepository; /// /// The _library manager /// private readonly ILibraryManager _libraryManager; private readonly IItemRepository _itemRepo; private readonly IDtoService _dtoService; private readonly IChannelManager _channelManager; /// /// Initializes a new instance of the class. /// /// The user manager. /// The user data repository. /// The library manager. public MoviesService(IUserManager userManager, IUserDataManager userDataRepository, ILibraryManager libraryManager, IItemRepository itemRepo, IDtoService dtoService, IChannelManager channelManager) { _userManager = userManager; _userDataRepository = userDataRepository; _libraryManager = libraryManager; _itemRepo = itemRepo; _dtoService = dtoService; _channelManager = channelManager; } /// /// Gets the specified request. /// /// The request. /// System.Object. public async Task Get(GetSimilarMovies request) { var result = await GetSimilarItemsResult( // Strip out secondary versions request, item => (item is Movie) && !((Video)item).PrimaryVersionId.HasValue, SimilarItemsHelper.GetSimiliarityScore).ConfigureAwait(false); return ToOptimizedSerializedResultUsingCache(result); } public async Task Get(GetMovieRecommendations request) { var user = _userManager.GetUserById(request.UserId.Value); IEnumerable movies = GetAllLibraryItems(request.UserId, _userManager, _libraryManager, request.ParentId, i => i is Movie); movies = _libraryManager.ReplaceVideosWithPrimaryVersions(movies); var listEligibleForCategories = new List(); var listEligibleForSuggestion = new List (); var list = movies.ToList(); listEligibleForCategories.AddRange(list); listEligibleForSuggestion.AddRange(list); if (user.Configuration.IncludeTrailersInSuggestions) { var trailerResult = await _channelManager.GetAllMediaInternal(new AllChannelMediaQuery { ContentTypes = new[] { ChannelMediaContentType.MovieExtra }, ExtraTypes = new[] { ExtraType.Trailer }, UserId = user.Id.ToString("N") }, CancellationToken.None).ConfigureAwait(false); listEligibleForSuggestion.AddRange(trailerResult.Items); } listEligibleForCategories = listEligibleForCategories .DistinctBy(i => i.Name, StringComparer.OrdinalIgnoreCase) .DistinctBy(i => i.GetProviderId(MetadataProviders.Imdb) ?? Guid.NewGuid().ToString(), StringComparer.OrdinalIgnoreCase) .ToList(); listEligibleForSuggestion = listEligibleForSuggestion .DistinctBy(i => i.Name, StringComparer.OrdinalIgnoreCase) .DistinctBy(i => i.GetProviderId(MetadataProviders.Imdb) ?? Guid.NewGuid().ToString(), StringComparer.OrdinalIgnoreCase) .ToList(); var dtoOptions = GetDtoOptions(request); dtoOptions.Fields = request.GetItemFields().ToList(); var result = GetRecommendationCategories(user, listEligibleForCategories, listEligibleForSuggestion, request.CategoryLimit, request.ItemLimit, dtoOptions); return ToOptimizedResult(result); } private async Task GetSimilarItemsResult(BaseGetSimilarItemsFromItem request, Func includeInSearch, Func getSimilarityScore) { var user = request.UserId.HasValue ? _userManager.GetUserById(request.UserId.Value) : null; var item = string.IsNullOrEmpty(request.Id) ? (request.UserId.HasValue ? user.RootFolder : _libraryManager.RootFolder) : _libraryManager.GetItemById(request.Id); Func filter = i => i.Id != item.Id && includeInSearch(i); var inputItems = user == null ? _libraryManager.RootFolder.GetRecursiveChildren(filter) : user.RootFolder.GetRecursiveChildren(user, filter); var list = inputItems.ToList(); if (item is Movie && user != null && user.Configuration.IncludeTrailersInSuggestions) { var trailerResult = await _channelManager.GetAllMediaInternal(new AllChannelMediaQuery { ContentTypes = new[] { ChannelMediaContentType.MovieExtra }, ExtraTypes = new[] { ExtraType.Trailer }, UserId = user.Id.ToString("N") }, CancellationToken.None).ConfigureAwait(false); var newTrailers = trailerResult.Items; list.AddRange(newTrailers); list = list .DistinctBy(i => i.Name, StringComparer.OrdinalIgnoreCase) .DistinctBy(i => i.GetProviderId(MetadataProviders.Imdb) ?? Guid.NewGuid().ToString(), StringComparer.OrdinalIgnoreCase) .ToList(); } if (item is Video) { var imdbId = item.GetProviderId(MetadataProviders.Imdb); // Use imdb id to try to filter duplicates of the same item if (!string.IsNullOrWhiteSpace(imdbId)) { list = list .Where(i => !string.Equals(imdbId, i.GetProviderId(MetadataProviders.Imdb), StringComparison.OrdinalIgnoreCase)) .ToList(); } } var items = SimilarItemsHelper.GetSimilaritems(item, list, getSimilarityScore).ToList(); IEnumerable returnItems = items; if (request.Limit.HasValue) { returnItems = returnItems.Take(request.Limit.Value); } var dtoOptions = GetDtoOptions(request); var result = new ItemsResult { Items = _dtoService.GetBaseItemDtos(returnItems, dtoOptions, user).ToArray(), TotalRecordCount = items.Count }; return result; } private IEnumerable GetRecommendationCategories(User user, List allMoviesForCategories, List allMovies, int categoryLimit, int itemLimit, DtoOptions dtoOptions) { var categories = new List(); var recentlyPlayedMovies = allMoviesForCategories .Select(i => { var userdata = _userDataRepository.GetUserData(user.Id, i.GetUserDataKey()); return new Tuple(i, userdata.Played, userdata.LastPlayedDate ?? DateTime.MinValue); }) .Where(i => i.Item2) .OrderByDescending(i => i.Item3) .Select(i => i.Item1) .ToList(); var excludeFromLiked = recentlyPlayedMovies.Take(10); var likedMovies = allMovies .Select(i => { var score = 0; var userData = _userDataRepository.GetUserData(user.Id, i.GetUserDataKey()); if (userData.IsFavorite) { score = 2; } else { score = userData.Likes.HasValue ? userData.Likes.Value ? 1 : -1 : 0; } return new Tuple(i, score); }) .OrderByDescending(i => i.Item2) .ThenBy(i => Guid.NewGuid()) .Where(i => i.Item2 > 0) .Select(i => i.Item1) .Where(i => !excludeFromLiked.Contains(i)); var mostRecentMovies = recentlyPlayedMovies.Take(6).ToList(); // Get recently played directors var recentDirectors = GetDirectors(mostRecentMovies) .OrderBy(i => Guid.NewGuid()) .ToList(); // Get recently played actors var recentActors = GetActors(mostRecentMovies) .OrderBy(i => Guid.NewGuid()) .ToList(); var similarToRecentlyPlayed = GetSimilarTo(user, allMovies, recentlyPlayedMovies.Take(7).OrderBy(i => Guid.NewGuid()), itemLimit, dtoOptions, RecommendationType.SimilarToRecentlyPlayed).GetEnumerator(); var similarToLiked = GetSimilarTo(user, allMovies, likedMovies, itemLimit, dtoOptions, RecommendationType.SimilarToLikedItem).GetEnumerator(); var hasDirectorFromRecentlyPlayed = GetWithDirector(user, allMovies, recentDirectors, itemLimit, dtoOptions, RecommendationType.HasDirectorFromRecentlyPlayed).GetEnumerator(); var hasActorFromRecentlyPlayed = GetWithActor(user, allMovies, 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 categories.OrderBy(i => i.RecommendationType).ThenBy(i => Guid.NewGuid()); } private IEnumerable GetWithDirector(User user, List allMovies, IEnumerable directors, int itemLimit, DtoOptions dtoOptions, RecommendationType type) { var userId = user.Id; foreach (var director in directors) { var items = allMovies .Where(i => i.People.Any(p => string.Equals(p.Type, PersonType.Director, StringComparison.OrdinalIgnoreCase) && string.Equals(p.Name, director, StringComparison.OrdinalIgnoreCase))) .Take(itemLimit) .ToList(); if (items.Count > 0) { yield return new RecommendationDto { BaselineItemName = director, CategoryId = director.GetMD5().ToString("N"), RecommendationType = type, Items = _dtoService.GetBaseItemDtos(items, dtoOptions, user).ToArray() }; } } } private IEnumerable GetWithActor(User user, List allMovies, IEnumerable names, int itemLimit, DtoOptions dtoOptions, RecommendationType type) { var userId = user.Id; foreach (var name in names) { var items = allMovies .Where(i => i.People.Any(p => string.Equals(p.Name, name, StringComparison.OrdinalIgnoreCase))) .Take(itemLimit) .ToList(); if (items.Count > 0) { yield return new RecommendationDto { BaselineItemName = name, CategoryId = name.GetMD5().ToString("N"), RecommendationType = type, Items = _dtoService.GetBaseItemDtos(items, dtoOptions, user).ToArray() }; } } } private IEnumerable GetSimilarTo(User user, List allMovies, IEnumerable baselineItems, int itemLimit, DtoOptions dtoOptions, RecommendationType type) { var userId = user.Id; foreach (var item in baselineItems) { var similar = SimilarItemsHelper .GetSimilaritems(item, allMovies, SimilarItemsHelper.GetSimiliarityScore) .Take(itemLimit) .ToList(); if (similar.Count > 0) { yield return new RecommendationDto { BaselineItemName = item.Name, CategoryId = item.Id.ToString("N"), RecommendationType = type, Items = _dtoService.GetBaseItemDtos(similar, dtoOptions, user).ToArray() }; } } } private IEnumerable GetActors(IEnumerable items) { // Get the two leading actors for all movies return items .SelectMany(i => i.People.Where(p => !string.Equals(PersonType.Director, p.Type, StringComparison.OrdinalIgnoreCase)).Take(2)) .Select(i => i.Name) .Distinct(StringComparer.OrdinalIgnoreCase); } private IEnumerable GetDirectors(IEnumerable items) { return items .Select(i => i.People.FirstOrDefault(p => string.Equals(PersonType.Director, p.Type, StringComparison.OrdinalIgnoreCase))) .Where(i => i != null) .Select(i => i.Name) .Distinct(StringComparer.OrdinalIgnoreCase); } } }