| | 1 | | using System; |
| | 2 | | using System.Collections.Generic; |
| | 3 | | using System.Globalization; |
| | 4 | | using System.Linq; |
| | 5 | | using Jellyfin.Api.Extensions; |
| | 6 | | using Jellyfin.Api.Helpers; |
| | 7 | | using Jellyfin.Api.ModelBinders; |
| | 8 | | using Jellyfin.Data.Enums; |
| | 9 | | using Jellyfin.Database.Implementations.Entities; |
| | 10 | | using Jellyfin.Database.Implementations.Enums; |
| | 11 | | using Jellyfin.Extensions; |
| | 12 | | using MediaBrowser.Common.Extensions; |
| | 13 | | using MediaBrowser.Controller.Configuration; |
| | 14 | | using MediaBrowser.Controller.Dto; |
| | 15 | | using MediaBrowser.Controller.Entities; |
| | 16 | | using MediaBrowser.Controller.Library; |
| | 17 | | using MediaBrowser.Model.Dto; |
| | 18 | | using MediaBrowser.Model.Entities; |
| | 19 | | using MediaBrowser.Model.Querying; |
| | 20 | | using Microsoft.AspNetCore.Authorization; |
| | 21 | | using Microsoft.AspNetCore.Mvc; |
| | 22 | |
|
| | 23 | | namespace Jellyfin.Api.Controllers; |
| | 24 | |
|
| | 25 | | /// <summary> |
| | 26 | | /// Movies controller. |
| | 27 | | /// </summary> |
| | 28 | | [Authorize] |
| | 29 | | public class MoviesController : BaseJellyfinApiController |
| | 30 | | { |
| | 31 | | private readonly IUserManager _userManager; |
| | 32 | | private readonly ILibraryManager _libraryManager; |
| | 33 | | private readonly IDtoService _dtoService; |
| | 34 | | private readonly IServerConfigurationManager _serverConfigurationManager; |
| | 35 | |
|
| | 36 | | /// <summary> |
| | 37 | | /// Initializes a new instance of the <see cref="MoviesController"/> class. |
| | 38 | | /// </summary> |
| | 39 | | /// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param> |
| | 40 | | /// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param> |
| | 41 | | /// <param name="dtoService">Instance of the <see cref="IDtoService"/> interface.</param> |
| | 42 | | /// <param name="serverConfigurationManager">Instance of the <see cref="IServerConfigurationManager"/> interface.</p |
| 0 | 43 | | public MoviesController( |
| 0 | 44 | | IUserManager userManager, |
| 0 | 45 | | ILibraryManager libraryManager, |
| 0 | 46 | | IDtoService dtoService, |
| 0 | 47 | | IServerConfigurationManager serverConfigurationManager) |
| | 48 | | { |
| 0 | 49 | | _userManager = userManager; |
| 0 | 50 | | _libraryManager = libraryManager; |
| 0 | 51 | | _dtoService = dtoService; |
| 0 | 52 | | _serverConfigurationManager = serverConfigurationManager; |
| 0 | 53 | | } |
| | 54 | |
|
| | 55 | | /// <summary> |
| | 56 | | /// Gets movie recommendations. |
| | 57 | | /// </summary> |
| | 58 | | /// <param name="userId">Optional. Filter by user id, and attach user data.</param> |
| | 59 | | /// <param name="parentId">Specify this to localize the search to a specific item or folder. Omit to use the root.</ |
| | 60 | | /// <param name="fields">Optional. The fields to return.</param> |
| | 61 | | /// <param name="categoryLimit">The max number of categories to return.</param> |
| | 62 | | /// <param name="itemLimit">The max number of items to return per category.</param> |
| | 63 | | /// <response code="200">Movie recommendations returned.</response> |
| | 64 | | /// <returns>The list of movie recommendations.</returns> |
| | 65 | | [HttpGet("Recommendations")] |
| | 66 | | public ActionResult<IEnumerable<RecommendationDto>> GetMovieRecommendations( |
| | 67 | | [FromQuery] Guid? userId, |
| | 68 | | [FromQuery] Guid? parentId, |
| | 69 | | [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] ItemFields[] fields, |
| | 70 | | [FromQuery] int categoryLimit = 5, |
| | 71 | | [FromQuery] int itemLimit = 8) |
| | 72 | | { |
| 0 | 73 | | userId = RequestHelpers.GetUserId(User, userId); |
| 0 | 74 | | var user = userId.IsNullOrEmpty() |
| 0 | 75 | | ? null |
| 0 | 76 | | : _userManager.GetUserById(userId.Value); |
| 0 | 77 | | var dtoOptions = new DtoOptions { Fields = fields } |
| 0 | 78 | | .AddClientFields(User); |
| | 79 | |
|
| 0 | 80 | | var categories = new List<RecommendationDto>(); |
| | 81 | |
|
| 0 | 82 | | var parentIdGuid = parentId ?? Guid.Empty; |
| | 83 | |
|
| 0 | 84 | | var query = new InternalItemsQuery(user) |
| 0 | 85 | | { |
| 0 | 86 | | IncludeItemTypes = new[] |
| 0 | 87 | | { |
| 0 | 88 | | BaseItemKind.Movie, |
| 0 | 89 | | // nameof(Trailer), |
| 0 | 90 | | // nameof(LiveTvProgram) |
| 0 | 91 | | }, |
| 0 | 92 | | // IsMovie = true |
| 0 | 93 | | OrderBy = new[] { (ItemSortBy.DatePlayed, SortOrder.Descending), (ItemSortBy.Random, SortOrder.Descending) } |
| 0 | 94 | | Limit = 7, |
| 0 | 95 | | ParentId = parentIdGuid, |
| 0 | 96 | | Recursive = true, |
| 0 | 97 | | IsPlayed = true, |
| 0 | 98 | | DtoOptions = dtoOptions |
| 0 | 99 | | }; |
| | 100 | |
|
| 0 | 101 | | var recentlyPlayedMovies = _libraryManager.GetItemList(query); |
| | 102 | |
|
| 0 | 103 | | var itemTypes = new List<BaseItemKind> { BaseItemKind.Movie }; |
| 0 | 104 | | if (_serverConfigurationManager.Configuration.EnableExternalContentInSuggestions) |
| | 105 | | { |
| 0 | 106 | | itemTypes.Add(BaseItemKind.Trailer); |
| 0 | 107 | | itemTypes.Add(BaseItemKind.LiveTvProgram); |
| | 108 | | } |
| | 109 | |
|
| 0 | 110 | | var likedMovies = _libraryManager.GetItemList(new InternalItemsQuery(user) |
| 0 | 111 | | { |
| 0 | 112 | | IncludeItemTypes = itemTypes.ToArray(), |
| 0 | 113 | | IsMovie = true, |
| 0 | 114 | | OrderBy = new[] { (ItemSortBy.Random, SortOrder.Descending) }, |
| 0 | 115 | | Limit = 10, |
| 0 | 116 | | IsFavoriteOrLiked = true, |
| 0 | 117 | | ExcludeItemIds = recentlyPlayedMovies.Select(i => i.Id).ToArray(), |
| 0 | 118 | | EnableGroupByMetadataKey = true, |
| 0 | 119 | | ParentId = parentIdGuid, |
| 0 | 120 | | Recursive = true, |
| 0 | 121 | | DtoOptions = dtoOptions |
| 0 | 122 | | }); |
| | 123 | |
|
| 0 | 124 | | var mostRecentMovies = recentlyPlayedMovies.Take(Math.Min(recentlyPlayedMovies.Count, 6)).ToList(); |
| | 125 | | // Get recently played directors |
| 0 | 126 | | var recentDirectors = GetDirectors(mostRecentMovies) |
| 0 | 127 | | .ToList(); |
| | 128 | |
|
| | 129 | | // Get recently played actors |
| 0 | 130 | | var recentActors = GetActors(mostRecentMovies) |
| 0 | 131 | | .ToList(); |
| | 132 | |
|
| 0 | 133 | | var similarToRecentlyPlayed = GetSimilarTo(user, recentlyPlayedMovies, itemLimit, dtoOptions, RecommendationType |
| 0 | 134 | | var similarToLiked = GetSimilarTo(user, likedMovies, itemLimit, dtoOptions, RecommendationType.SimilarToLikedIte |
| | 135 | |
|
| 0 | 136 | | var hasDirectorFromRecentlyPlayed = GetWithDirector(user, recentDirectors, itemLimit, dtoOptions, Recommendation |
| 0 | 137 | | var hasActorFromRecentlyPlayed = GetWithActor(user, recentActors, itemLimit, dtoOptions, RecommendationType.HasA |
| | 138 | |
|
| 0 | 139 | | var categoryTypes = new List<IEnumerator<RecommendationDto>> |
| 0 | 140 | | { |
| 0 | 141 | | // Give this extra weight |
| 0 | 142 | | similarToRecentlyPlayed, |
| 0 | 143 | | similarToRecentlyPlayed, |
| 0 | 144 | |
|
| 0 | 145 | | // Give this extra weight |
| 0 | 146 | | similarToLiked, |
| 0 | 147 | | similarToLiked, |
| 0 | 148 | | hasDirectorFromRecentlyPlayed, |
| 0 | 149 | | hasActorFromRecentlyPlayed |
| 0 | 150 | | }; |
| | 151 | |
|
| 0 | 152 | | while (categories.Count < categoryLimit) |
| | 153 | | { |
| 0 | 154 | | var allEmpty = true; |
| | 155 | |
|
| 0 | 156 | | foreach (var category in categoryTypes) |
| | 157 | | { |
| 0 | 158 | | if (category.MoveNext()) |
| | 159 | | { |
| 0 | 160 | | categories.Add(category.Current); |
| 0 | 161 | | allEmpty = false; |
| | 162 | |
|
| 0 | 163 | | if (categories.Count >= categoryLimit) |
| | 164 | | { |
| 0 | 165 | | break; |
| | 166 | | } |
| | 167 | | } |
| | 168 | | } |
| | 169 | |
|
| 0 | 170 | | if (allEmpty) |
| | 171 | | { |
| | 172 | | break; |
| | 173 | | } |
| | 174 | | } |
| | 175 | |
|
| 0 | 176 | | return Ok(categories.OrderBy(i => i.RecommendationType).AsEnumerable()); |
| | 177 | | } |
| | 178 | |
|
| | 179 | | private IEnumerable<RecommendationDto> GetWithDirector( |
| | 180 | | User? user, |
| | 181 | | IEnumerable<string> names, |
| | 182 | | int itemLimit, |
| | 183 | | DtoOptions dtoOptions, |
| | 184 | | RecommendationType type) |
| | 185 | | { |
| | 186 | | var itemTypes = new List<BaseItemKind> { BaseItemKind.Movie }; |
| | 187 | | if (_serverConfigurationManager.Configuration.EnableExternalContentInSuggestions) |
| | 188 | | { |
| | 189 | | itemTypes.Add(BaseItemKind.Trailer); |
| | 190 | | itemTypes.Add(BaseItemKind.LiveTvProgram); |
| | 191 | | } |
| | 192 | |
|
| | 193 | | foreach (var name in names) |
| | 194 | | { |
| | 195 | | var items = _libraryManager.GetItemList( |
| | 196 | | new InternalItemsQuery(user) |
| | 197 | | { |
| | 198 | | Person = name, |
| | 199 | | // Account for duplicates by IMDb id, since the database doesn't support this yet |
| | 200 | | Limit = itemLimit + 2, |
| | 201 | | PersonTypes = new[] { PersonType.Director }, |
| | 202 | | IncludeItemTypes = itemTypes.ToArray(), |
| | 203 | | IsMovie = true, |
| | 204 | | EnableGroupByMetadataKey = true, |
| | 205 | | DtoOptions = dtoOptions |
| | 206 | | }).DistinctBy(i => i.GetProviderId(MediaBrowser.Model.Entities.MetadataProvider.Imdb) ?? Guid.NewGuid(). |
| | 207 | | .Take(itemLimit) |
| | 208 | | .ToList(); |
| | 209 | |
|
| | 210 | | if (items.Count > 0) |
| | 211 | | { |
| | 212 | | var returnItems = _dtoService.GetBaseItemDtos(items, dtoOptions, user); |
| | 213 | |
|
| | 214 | | yield return new RecommendationDto |
| | 215 | | { |
| | 216 | | BaselineItemName = name, |
| | 217 | | CategoryId = name.GetMD5(), |
| | 218 | | RecommendationType = type, |
| | 219 | | Items = returnItems |
| | 220 | | }; |
| | 221 | | } |
| | 222 | | } |
| | 223 | | } |
| | 224 | |
|
| | 225 | | private IEnumerable<RecommendationDto> GetWithActor(User? user, IEnumerable<string> names, int itemLimit, DtoOptions |
| | 226 | | { |
| | 227 | | var itemTypes = new List<BaseItemKind> { BaseItemKind.Movie }; |
| | 228 | | if (_serverConfigurationManager.Configuration.EnableExternalContentInSuggestions) |
| | 229 | | { |
| | 230 | | itemTypes.Add(BaseItemKind.Trailer); |
| | 231 | | itemTypes.Add(BaseItemKind.LiveTvProgram); |
| | 232 | | } |
| | 233 | |
|
| | 234 | | foreach (var name in names) |
| | 235 | | { |
| | 236 | | var items = _libraryManager.GetItemList(new InternalItemsQuery(user) |
| | 237 | | { |
| | 238 | | Person = name, |
| | 239 | | // Account for duplicates by IMDb id, since the database doesn't support this yet |
| | 240 | | Limit = itemLimit + 2, |
| | 241 | | IncludeItemTypes = itemTypes.ToArray(), |
| | 242 | | IsMovie = true, |
| | 243 | | EnableGroupByMetadataKey = true, |
| | 244 | | DtoOptions = dtoOptions |
| | 245 | | }).DistinctBy(i => i.GetProviderId(MediaBrowser.Model.Entities.MetadataProvider.Imdb) ?? Guid.NewGuid().ToSt |
| | 246 | | .Take(itemLimit) |
| | 247 | | .ToList(); |
| | 248 | |
|
| | 249 | | if (items.Count > 0) |
| | 250 | | { |
| | 251 | | var returnItems = _dtoService.GetBaseItemDtos(items, dtoOptions, user); |
| | 252 | |
|
| | 253 | | yield return new RecommendationDto |
| | 254 | | { |
| | 255 | | BaselineItemName = name, |
| | 256 | | CategoryId = name.GetMD5(), |
| | 257 | | RecommendationType = type, |
| | 258 | | Items = returnItems |
| | 259 | | }; |
| | 260 | | } |
| | 261 | | } |
| | 262 | | } |
| | 263 | |
|
| | 264 | | private IEnumerable<RecommendationDto> GetSimilarTo(User? user, IEnumerable<BaseItem> baselineItems, int itemLimit, |
| | 265 | | { |
| | 266 | | var itemTypes = new List<BaseItemKind> { BaseItemKind.Movie }; |
| | 267 | | if (_serverConfigurationManager.Configuration.EnableExternalContentInSuggestions) |
| | 268 | | { |
| | 269 | | itemTypes.Add(BaseItemKind.Trailer); |
| | 270 | | itemTypes.Add(BaseItemKind.LiveTvProgram); |
| | 271 | | } |
| | 272 | |
|
| | 273 | | foreach (var item in baselineItems) |
| | 274 | | { |
| | 275 | | var similar = _libraryManager.GetItemList(new InternalItemsQuery(user) |
| | 276 | | { |
| | 277 | | Limit = itemLimit, |
| | 278 | | IncludeItemTypes = itemTypes.ToArray(), |
| | 279 | | IsMovie = true, |
| | 280 | | EnableGroupByMetadataKey = true, |
| | 281 | | DtoOptions = dtoOptions |
| | 282 | | }); |
| | 283 | |
|
| | 284 | | if (similar.Count > 0) |
| | 285 | | { |
| | 286 | | var returnItems = _dtoService.GetBaseItemDtos(similar, dtoOptions, user); |
| | 287 | |
|
| | 288 | | yield return new RecommendationDto |
| | 289 | | { |
| | 290 | | BaselineItemName = item.Name, |
| | 291 | | CategoryId = item.Id, |
| | 292 | | RecommendationType = type, |
| | 293 | | Items = returnItems |
| | 294 | | }; |
| | 295 | | } |
| | 296 | | } |
| | 297 | | } |
| | 298 | |
|
| | 299 | | private IEnumerable<string> GetActors(IEnumerable<BaseItem> items) |
| | 300 | | { |
| 0 | 301 | | var people = _libraryManager.GetPeople(new InternalPeopleQuery(Array.Empty<string>(), new[] { PersonType.Directo |
| 0 | 302 | | { |
| 0 | 303 | | MaxListOrder = 3 |
| 0 | 304 | | }); |
| | 305 | |
|
| 0 | 306 | | var itemIds = items.Select(i => i.Id).ToList(); |
| | 307 | |
|
| 0 | 308 | | return people |
| 0 | 309 | | .Where(i => itemIds.Contains(i.ItemId)) |
| 0 | 310 | | .Select(i => i.Name) |
| 0 | 311 | | .DistinctNames(); |
| | 312 | | } |
| | 313 | |
|
| | 314 | | private IEnumerable<string> GetDirectors(IEnumerable<BaseItem> items) |
| | 315 | | { |
| 0 | 316 | | var people = _libraryManager.GetPeople(new InternalPeopleQuery( |
| 0 | 317 | | new[] { PersonType.Director }, |
| 0 | 318 | | Array.Empty<string>())); |
| | 319 | |
|
| 0 | 320 | | var itemIds = items.Select(i => i.Id).ToList(); |
| | 321 | |
|
| 0 | 322 | | return people |
| 0 | 323 | | .Where(i => itemIds.Contains(i.ItemId)) |
| 0 | 324 | | .Select(i => i.Name) |
| 0 | 325 | | .DistinctNames(); |
| | 326 | | } |
| | 327 | | } |