MoviesService.cs 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332
  1. using MediaBrowser.Common.Extensions;
  2. using MediaBrowser.Controller.Dto;
  3. using MediaBrowser.Controller.Entities;
  4. using MediaBrowser.Controller.Entities.Movies;
  5. using MediaBrowser.Controller.Library;
  6. using MediaBrowser.Controller.Persistence;
  7. using MediaBrowser.Model.Dto;
  8. using MediaBrowser.Model.Entities;
  9. using MediaBrowser.Model.Querying;
  10. using ServiceStack;
  11. using System;
  12. using System.Collections.Generic;
  13. using System.Linq;
  14. namespace MediaBrowser.Api.Movies
  15. {
  16. /// <summary>
  17. /// Class GetSimilarMovies
  18. /// </summary>
  19. [Route("/Movies/{Id}/Similar", "GET", Summary = "Finds movies and trailers similar to a given movie.")]
  20. public class GetSimilarMovies : BaseGetSimilarItemsFromItem
  21. {
  22. [ApiMember(Name = "IncludeTrailers", Description = "Whether or not to include trailers within the results. Defaults to true.", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "GET")]
  23. public bool IncludeTrailers { get; set; }
  24. public GetSimilarMovies()
  25. {
  26. IncludeTrailers = true;
  27. }
  28. }
  29. [Route("/Movies/Recommendations", "GET", Summary = "Gets movie recommendations")]
  30. public class GetMovieRecommendations : IReturn<RecommendationDto[]>, IHasItemFields
  31. {
  32. [ApiMember(Name = "CategoryLimit", Description = "The max number of categories to return", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")]
  33. public int CategoryLimit { get; set; }
  34. [ApiMember(Name = "ItemLimit", Description = "The max number of items to return per category", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")]
  35. public int ItemLimit { get; set; }
  36. /// <summary>
  37. /// Gets or sets the user id.
  38. /// </summary>
  39. /// <value>The user id.</value>
  40. [ApiMember(Name = "UserId", Description = "Optional. Filter by user id, and attach user data", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
  41. public Guid? UserId { get; set; }
  42. /// <summary>
  43. /// Specify this to localize the search to a specific item or folder. Omit to use the root.
  44. /// </summary>
  45. /// <value>The parent id.</value>
  46. [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")]
  47. public string ParentId { get; set; }
  48. public GetMovieRecommendations()
  49. {
  50. CategoryLimit = 5;
  51. ItemLimit = 8;
  52. }
  53. public string Fields { get; set; }
  54. }
  55. /// <summary>
  56. /// Class MoviesService
  57. /// </summary>
  58. public class MoviesService : BaseApiService
  59. {
  60. /// <summary>
  61. /// The _user manager
  62. /// </summary>
  63. private readonly IUserManager _userManager;
  64. /// <summary>
  65. /// The _user data repository
  66. /// </summary>
  67. private readonly IUserDataManager _userDataRepository;
  68. /// <summary>
  69. /// The _library manager
  70. /// </summary>
  71. private readonly ILibraryManager _libraryManager;
  72. private readonly IItemRepository _itemRepo;
  73. private readonly IDtoService _dtoService;
  74. /// <summary>
  75. /// Initializes a new instance of the <see cref="MoviesService"/> class.
  76. /// </summary>
  77. /// <param name="userManager">The user manager.</param>
  78. /// <param name="userDataRepository">The user data repository.</param>
  79. /// <param name="libraryManager">The library manager.</param>
  80. public MoviesService(IUserManager userManager, IUserDataManager userDataRepository, ILibraryManager libraryManager, IItemRepository itemRepo, IDtoService dtoService)
  81. {
  82. _userManager = userManager;
  83. _userDataRepository = userDataRepository;
  84. _libraryManager = libraryManager;
  85. _itemRepo = itemRepo;
  86. _dtoService = dtoService;
  87. }
  88. /// <summary>
  89. /// Gets the specified request.
  90. /// </summary>
  91. /// <param name="request">The request.</param>
  92. /// <returns>System.Object.</returns>
  93. public object Get(GetSimilarMovies request)
  94. {
  95. var result = SimilarItemsHelper.GetSimilarItemsResult(_userManager,
  96. _itemRepo,
  97. _libraryManager,
  98. _userDataRepository,
  99. _dtoService,
  100. Logger,
  101. // Strip out secondary versions
  102. request, item => (item is Movie || (item is Trailer && request.IncludeTrailers)) && !((Video)item).PrimaryVersionId.HasValue,
  103. SimilarItemsHelper.GetSimiliarityScore);
  104. return ToOptimizedSerializedResultUsingCache(result);
  105. }
  106. public object Get(GetMovieRecommendations request)
  107. {
  108. var user = _userManager.GetUserById(request.UserId.Value);
  109. var movies = GetAllLibraryItems(request.UserId, _userManager, _libraryManager, request.ParentId)
  110. .OfType<Movie>()
  111. .ToList();
  112. var result = GetRecommendationCategories(user, movies, request.CategoryLimit, request.ItemLimit, request.GetItemFields().ToList());
  113. return ToOptimizedResult(result);
  114. }
  115. private IEnumerable<RecommendationDto> GetRecommendationCategories(User user, List<Movie> allMovies, int categoryLimit, int itemLimit, List<ItemFields> fields)
  116. {
  117. var categories = new List<RecommendationDto>();
  118. var recentlyPlayedMovies = allMovies
  119. .Select(i =>
  120. {
  121. var userdata = _userDataRepository.GetUserData(user.Id, i.GetUserDataKey());
  122. return new Tuple<Movie, bool, DateTime>(i, userdata.Played, userdata.LastPlayedDate ?? DateTime.MinValue);
  123. })
  124. .Where(i => i.Item2)
  125. .OrderByDescending(i => i.Item3)
  126. .Select(i => i.Item1)
  127. .ToList();
  128. var excludeFromLiked = recentlyPlayedMovies.Take(10);
  129. var likedMovies = allMovies
  130. .Select(i =>
  131. {
  132. var score = 0;
  133. var userData = _userDataRepository.GetUserData(user.Id, i.GetUserDataKey());
  134. if (userData.IsFavorite)
  135. {
  136. score = 2;
  137. }
  138. else
  139. {
  140. score = userData.Likes.HasValue ? userData.Likes.Value ? 1 : -1 : 0;
  141. }
  142. return new Tuple<Movie, int>(i, score);
  143. })
  144. .OrderByDescending(i => i.Item2)
  145. .ThenBy(i => Guid.NewGuid())
  146. .Where(i => i.Item2 > 0)
  147. .Select(i => i.Item1)
  148. .Where(i => !excludeFromLiked.Contains(i));
  149. var mostRecentMovies = recentlyPlayedMovies.Take(6).ToList();
  150. // Get recently played directors
  151. var recentDirectors = GetDirectors(mostRecentMovies)
  152. .OrderBy(i => Guid.NewGuid())
  153. .ToList();
  154. // Get recently played actors
  155. var recentActors = GetActors(mostRecentMovies)
  156. .OrderBy(i => Guid.NewGuid())
  157. .ToList();
  158. var similarToRecentlyPlayed = GetSimilarTo(user, allMovies, recentlyPlayedMovies.Take(7).OrderBy(i => Guid.NewGuid()), itemLimit, fields, RecommendationType.SimilarToRecentlyPlayed).GetEnumerator();
  159. var similarToLiked = GetSimilarTo(user, allMovies, likedMovies, itemLimit, fields, RecommendationType.SimilarToLikedItem).GetEnumerator();
  160. var hasDirectorFromRecentlyPlayed = GetWithDirector(user, allMovies, recentDirectors, itemLimit, fields, RecommendationType.HasDirectorFromRecentlyPlayed).GetEnumerator();
  161. var hasActorFromRecentlyPlayed = GetWithActor(user, allMovies, recentActors, itemLimit, fields, RecommendationType.HasActorFromRecentlyPlayed).GetEnumerator();
  162. var categoryTypes = new List<IEnumerator<RecommendationDto>>
  163. {
  164. // Give this extra weight
  165. similarToRecentlyPlayed,
  166. similarToRecentlyPlayed,
  167. // Give this extra weight
  168. similarToLiked,
  169. similarToLiked,
  170. hasDirectorFromRecentlyPlayed,
  171. hasActorFromRecentlyPlayed
  172. };
  173. while (categories.Count < categoryLimit)
  174. {
  175. var allEmpty = true;
  176. foreach (var category in categoryTypes)
  177. {
  178. if (category.MoveNext())
  179. {
  180. categories.Add(category.Current);
  181. allEmpty = false;
  182. if (categories.Count >= categoryLimit)
  183. {
  184. break;
  185. }
  186. }
  187. }
  188. if (allEmpty)
  189. {
  190. break;
  191. }
  192. }
  193. //// Get the lead actor for all movies
  194. //var allActors = GetActors(allMovies)
  195. // .ToList();
  196. //foreach (var actor in recentActors)
  197. //{
  198. //}
  199. return categories.OrderBy(i => i.RecommendationType).ThenBy(i => Guid.NewGuid());
  200. }
  201. private IEnumerable<RecommendationDto> GetWithDirector(User user, List<Movie> allMovies, IEnumerable<string> directors, int itemLimit, List<ItemFields> fields, RecommendationType type)
  202. {
  203. var userId = user.Id;
  204. foreach (var director in directors)
  205. {
  206. var items = allMovies
  207. .Where(i => i.People.Any(p => string.Equals(p.Type, PersonType.Director, StringComparison.OrdinalIgnoreCase) && string.Equals(p.Name, director, StringComparison.OrdinalIgnoreCase)))
  208. .Take(itemLimit)
  209. .ToList();
  210. if (items.Count > 0)
  211. {
  212. yield return new RecommendationDto
  213. {
  214. BaselineItemName = director,
  215. CategoryId = director.GetMD5().ToString("N"),
  216. RecommendationType = type,
  217. Items = items.Select(i => _dtoService.GetBaseItemDto(i, fields, user)).ToArray()
  218. };
  219. }
  220. }
  221. }
  222. private IEnumerable<RecommendationDto> GetWithActor(User user, List<Movie> allMovies, IEnumerable<string> names, int itemLimit, List<ItemFields> fields, RecommendationType type)
  223. {
  224. var userId = user.Id;
  225. foreach (var name in names)
  226. {
  227. var items = allMovies
  228. .Where(i => i.People.Any(p => string.Equals(p.Name, name, StringComparison.OrdinalIgnoreCase)))
  229. .Take(itemLimit)
  230. .ToList();
  231. if (items.Count > 0)
  232. {
  233. yield return new RecommendationDto
  234. {
  235. BaselineItemName = name,
  236. CategoryId = name.GetMD5().ToString("N"),
  237. RecommendationType = type,
  238. Items = items.Select(i => _dtoService.GetBaseItemDto(i, fields, user)).ToArray()
  239. };
  240. }
  241. }
  242. }
  243. private IEnumerable<RecommendationDto> GetSimilarTo(User user, List<Movie> allMovies, IEnumerable<Movie> baselineItems, int itemLimit, List<ItemFields> fields, RecommendationType type)
  244. {
  245. var userId = user.Id;
  246. foreach (var item in baselineItems)
  247. {
  248. var similar = SimilarItemsHelper
  249. .GetSimilaritems(item, allMovies, SimilarItemsHelper.GetSimiliarityScore)
  250. .Take(itemLimit)
  251. .ToList();
  252. if (similar.Count > 0)
  253. {
  254. yield return new RecommendationDto
  255. {
  256. BaselineItemName = item.Name,
  257. CategoryId = item.Id.ToString("N"),
  258. RecommendationType = type,
  259. Items = similar.Select(i => _dtoService.GetBaseItemDto(i, fields, user)).ToArray()
  260. };
  261. }
  262. }
  263. }
  264. private IEnumerable<string> GetActors(IEnumerable<BaseItem> items)
  265. {
  266. // Get the two leading actors for all movies
  267. return items
  268. .SelectMany(i => i.People.Where(p => !string.Equals(PersonType.Director, p.Type, StringComparison.OrdinalIgnoreCase)).Take(2))
  269. .Select(i => i.Name)
  270. .Distinct(StringComparer.OrdinalIgnoreCase);
  271. }
  272. private IEnumerable<string> GetDirectors(IEnumerable<BaseItem> items)
  273. {
  274. return items
  275. .Select(i => i.People.FirstOrDefault(p => string.Equals(PersonType.Director, p.Type, StringComparison.OrdinalIgnoreCase)))
  276. .Where(i => i != null)
  277. .Select(i => i.Name)
  278. .Distinct(StringComparer.OrdinalIgnoreCase);
  279. }
  280. }
  281. }