| | | 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 }; |
| | | 78 | | |
| | 0 | 79 | | var categories = new List<RecommendationDto>(); |
| | | 80 | | |
| | 0 | 81 | | var parentIdGuid = parentId ?? Guid.Empty; |
| | | 82 | | |
| | 0 | 83 | | var query = new InternalItemsQuery(user) |
| | 0 | 84 | | { |
| | 0 | 85 | | IncludeItemTypes = new[] |
| | 0 | 86 | | { |
| | 0 | 87 | | BaseItemKind.Movie, |
| | 0 | 88 | | // nameof(Trailer), |
| | 0 | 89 | | // nameof(LiveTvProgram) |
| | 0 | 90 | | }, |
| | 0 | 91 | | // IsMovie = true |
| | 0 | 92 | | OrderBy = new[] { (ItemSortBy.DatePlayed, SortOrder.Descending), (ItemSortBy.Random, SortOrder.Descending) } |
| | 0 | 93 | | Limit = 7, |
| | 0 | 94 | | ParentId = parentIdGuid, |
| | 0 | 95 | | Recursive = true, |
| | 0 | 96 | | IsPlayed = true, |
| | 0 | 97 | | DtoOptions = dtoOptions |
| | 0 | 98 | | }; |
| | | 99 | | |
| | 0 | 100 | | var recentlyPlayedMovies = _libraryManager.GetItemList(query); |
| | | 101 | | |
| | 0 | 102 | | var itemTypes = new List<BaseItemKind> { BaseItemKind.Movie }; |
| | 0 | 103 | | if (_serverConfigurationManager.Configuration.EnableExternalContentInSuggestions) |
| | | 104 | | { |
| | 0 | 105 | | itemTypes.Add(BaseItemKind.Trailer); |
| | 0 | 106 | | itemTypes.Add(BaseItemKind.LiveTvProgram); |
| | | 107 | | } |
| | | 108 | | |
| | 0 | 109 | | var likedMovies = _libraryManager.GetItemList(new InternalItemsQuery(user) |
| | 0 | 110 | | { |
| | 0 | 111 | | IncludeItemTypes = itemTypes.ToArray(), |
| | 0 | 112 | | IsMovie = true, |
| | 0 | 113 | | OrderBy = new[] { (ItemSortBy.Random, SortOrder.Descending) }, |
| | 0 | 114 | | Limit = 10, |
| | 0 | 115 | | IsFavoriteOrLiked = true, |
| | 0 | 116 | | ExcludeItemIds = recentlyPlayedMovies.Select(i => i.Id).ToArray(), |
| | 0 | 117 | | EnableGroupByMetadataKey = true, |
| | 0 | 118 | | ParentId = parentIdGuid, |
| | 0 | 119 | | Recursive = true, |
| | 0 | 120 | | DtoOptions = dtoOptions |
| | 0 | 121 | | }); |
| | | 122 | | |
| | 0 | 123 | | var mostRecentMovies = recentlyPlayedMovies.Take(Math.Min(recentlyPlayedMovies.Count, 6)).ToList(); |
| | | 124 | | // Get recently played directors |
| | 0 | 125 | | var recentDirectors = GetDirectors(mostRecentMovies) |
| | 0 | 126 | | .ToList(); |
| | | 127 | | |
| | | 128 | | // Get recently played actors |
| | 0 | 129 | | var recentActors = GetActors(mostRecentMovies) |
| | 0 | 130 | | .ToList(); |
| | | 131 | | |
| | 0 | 132 | | var similarToRecentlyPlayed = GetSimilarTo(user, recentlyPlayedMovies, itemLimit, dtoOptions, RecommendationType |
| | 0 | 133 | | var similarToLiked = GetSimilarTo(user, likedMovies, itemLimit, dtoOptions, RecommendationType.SimilarToLikedIte |
| | | 134 | | |
| | 0 | 135 | | var hasDirectorFromRecentlyPlayed = GetWithDirector(user, recentDirectors, itemLimit, dtoOptions, Recommendation |
| | 0 | 136 | | var hasActorFromRecentlyPlayed = GetWithActor(user, recentActors, itemLimit, dtoOptions, RecommendationType.HasA |
| | | 137 | | |
| | 0 | 138 | | var categoryTypes = new List<IEnumerator<RecommendationDto>> |
| | 0 | 139 | | { |
| | 0 | 140 | | // Give this extra weight |
| | 0 | 141 | | similarToRecentlyPlayed, |
| | 0 | 142 | | similarToRecentlyPlayed, |
| | 0 | 143 | | |
| | 0 | 144 | | // Give this extra weight |
| | 0 | 145 | | similarToLiked, |
| | 0 | 146 | | similarToLiked, |
| | 0 | 147 | | hasDirectorFromRecentlyPlayed, |
| | 0 | 148 | | hasActorFromRecentlyPlayed |
| | 0 | 149 | | }; |
| | | 150 | | |
| | 0 | 151 | | while (categories.Count < categoryLimit) |
| | | 152 | | { |
| | 0 | 153 | | var allEmpty = true; |
| | | 154 | | |
| | 0 | 155 | | foreach (var category in categoryTypes) |
| | | 156 | | { |
| | 0 | 157 | | if (category.MoveNext()) |
| | | 158 | | { |
| | 0 | 159 | | categories.Add(category.Current); |
| | 0 | 160 | | allEmpty = false; |
| | | 161 | | |
| | 0 | 162 | | if (categories.Count >= categoryLimit) |
| | | 163 | | { |
| | 0 | 164 | | break; |
| | | 165 | | } |
| | | 166 | | } |
| | | 167 | | } |
| | | 168 | | |
| | 0 | 169 | | if (allEmpty) |
| | | 170 | | { |
| | | 171 | | break; |
| | | 172 | | } |
| | | 173 | | } |
| | | 174 | | |
| | 0 | 175 | | return Ok(categories.OrderBy(i => i.RecommendationType).AsEnumerable()); |
| | | 176 | | } |
| | | 177 | | |
| | | 178 | | private IEnumerable<RecommendationDto> GetWithDirector( |
| | | 179 | | User? user, |
| | | 180 | | IEnumerable<string> names, |
| | | 181 | | int itemLimit, |
| | | 182 | | DtoOptions dtoOptions, |
| | | 183 | | RecommendationType type) |
| | | 184 | | { |
| | 0 | 185 | | var itemTypes = new List<BaseItemKind> { BaseItemKind.Movie }; |
| | 0 | 186 | | if (_serverConfigurationManager.Configuration.EnableExternalContentInSuggestions) |
| | | 187 | | { |
| | 0 | 188 | | itemTypes.Add(BaseItemKind.Trailer); |
| | 0 | 189 | | itemTypes.Add(BaseItemKind.LiveTvProgram); |
| | | 190 | | } |
| | | 191 | | |
| | 0 | 192 | | foreach (var name in names) |
| | | 193 | | { |
| | 0 | 194 | | var items = _libraryManager.GetItemList( |
| | 0 | 195 | | new InternalItemsQuery(user) |
| | 0 | 196 | | { |
| | 0 | 197 | | Person = name, |
| | 0 | 198 | | // Account for duplicates by IMDb id, since the database doesn't support this yet |
| | 0 | 199 | | Limit = itemLimit + 2, |
| | 0 | 200 | | PersonTypes = new[] { PersonType.Director }, |
| | 0 | 201 | | IncludeItemTypes = itemTypes.ToArray(), |
| | 0 | 202 | | IsMovie = true, |
| | 0 | 203 | | EnableGroupByMetadataKey = true, |
| | 0 | 204 | | DtoOptions = dtoOptions |
| | 0 | 205 | | }).DistinctBy(i => i.GetProviderId(MediaBrowser.Model.Entities.MetadataProvider.Imdb) ?? Guid.NewGuid(). |
| | 0 | 206 | | .Take(itemLimit) |
| | 0 | 207 | | .ToList(); |
| | | 208 | | |
| | 0 | 209 | | if (items.Count > 0) |
| | | 210 | | { |
| | 0 | 211 | | var returnItems = _dtoService.GetBaseItemDtos(items, dtoOptions, user); |
| | | 212 | | |
| | 0 | 213 | | yield return new RecommendationDto |
| | 0 | 214 | | { |
| | 0 | 215 | | BaselineItemName = name, |
| | 0 | 216 | | CategoryId = name.GetMD5(), |
| | 0 | 217 | | RecommendationType = type, |
| | 0 | 218 | | Items = returnItems |
| | 0 | 219 | | }; |
| | | 220 | | } |
| | | 221 | | } |
| | 0 | 222 | | } |
| | | 223 | | |
| | | 224 | | private IEnumerable<RecommendationDto> GetWithActor(User? user, IEnumerable<string> names, int itemLimit, DtoOptions |
| | | 225 | | { |
| | 0 | 226 | | var itemTypes = new List<BaseItemKind> { BaseItemKind.Movie }; |
| | 0 | 227 | | if (_serverConfigurationManager.Configuration.EnableExternalContentInSuggestions) |
| | | 228 | | { |
| | 0 | 229 | | itemTypes.Add(BaseItemKind.Trailer); |
| | 0 | 230 | | itemTypes.Add(BaseItemKind.LiveTvProgram); |
| | | 231 | | } |
| | | 232 | | |
| | 0 | 233 | | foreach (var name in names) |
| | | 234 | | { |
| | 0 | 235 | | var items = _libraryManager.GetItemList(new InternalItemsQuery(user) |
| | 0 | 236 | | { |
| | 0 | 237 | | Person = name, |
| | 0 | 238 | | // Account for duplicates by IMDb id, since the database doesn't support this yet |
| | 0 | 239 | | Limit = itemLimit + 2, |
| | 0 | 240 | | IncludeItemTypes = itemTypes.ToArray(), |
| | 0 | 241 | | IsMovie = true, |
| | 0 | 242 | | EnableGroupByMetadataKey = true, |
| | 0 | 243 | | DtoOptions = dtoOptions |
| | 0 | 244 | | }).DistinctBy(i => i.GetProviderId(MediaBrowser.Model.Entities.MetadataProvider.Imdb) ?? Guid.NewGuid().ToSt |
| | 0 | 245 | | .Take(itemLimit) |
| | 0 | 246 | | .ToList(); |
| | | 247 | | |
| | 0 | 248 | | if (items.Count > 0) |
| | | 249 | | { |
| | 0 | 250 | | var returnItems = _dtoService.GetBaseItemDtos(items, dtoOptions, user); |
| | | 251 | | |
| | 0 | 252 | | yield return new RecommendationDto |
| | 0 | 253 | | { |
| | 0 | 254 | | BaselineItemName = name, |
| | 0 | 255 | | CategoryId = name.GetMD5(), |
| | 0 | 256 | | RecommendationType = type, |
| | 0 | 257 | | Items = returnItems |
| | 0 | 258 | | }; |
| | | 259 | | } |
| | | 260 | | } |
| | 0 | 261 | | } |
| | | 262 | | |
| | | 263 | | private IEnumerable<RecommendationDto> GetSimilarTo(User? user, IEnumerable<BaseItem> baselineItems, int itemLimit, |
| | | 264 | | { |
| | 0 | 265 | | var itemTypes = new List<BaseItemKind> { BaseItemKind.Movie }; |
| | 0 | 266 | | if (_serverConfigurationManager.Configuration.EnableExternalContentInSuggestions) |
| | | 267 | | { |
| | 0 | 268 | | itemTypes.Add(BaseItemKind.Trailer); |
| | 0 | 269 | | itemTypes.Add(BaseItemKind.LiveTvProgram); |
| | | 270 | | } |
| | | 271 | | |
| | 0 | 272 | | foreach (var item in baselineItems) |
| | | 273 | | { |
| | 0 | 274 | | var similar = _libraryManager.GetItemList(new InternalItemsQuery(user) |
| | 0 | 275 | | { |
| | 0 | 276 | | Limit = itemLimit, |
| | 0 | 277 | | IncludeItemTypes = itemTypes.ToArray(), |
| | 0 | 278 | | IsMovie = true, |
| | 0 | 279 | | EnableGroupByMetadataKey = true, |
| | 0 | 280 | | DtoOptions = dtoOptions |
| | 0 | 281 | | }); |
| | | 282 | | |
| | 0 | 283 | | if (similar.Count > 0) |
| | | 284 | | { |
| | 0 | 285 | | var returnItems = _dtoService.GetBaseItemDtos(similar, dtoOptions, user); |
| | | 286 | | |
| | 0 | 287 | | yield return new RecommendationDto |
| | 0 | 288 | | { |
| | 0 | 289 | | BaselineItemName = item.Name, |
| | 0 | 290 | | CategoryId = item.Id, |
| | 0 | 291 | | RecommendationType = type, |
| | 0 | 292 | | Items = returnItems |
| | 0 | 293 | | }; |
| | | 294 | | } |
| | | 295 | | } |
| | 0 | 296 | | } |
| | | 297 | | |
| | | 298 | | private IEnumerable<string> GetActors(IEnumerable<BaseItem> items) |
| | | 299 | | { |
| | 0 | 300 | | var people = _libraryManager.GetPeople(new InternalPeopleQuery(Array.Empty<string>(), new[] { PersonType.Directo |
| | 0 | 301 | | { |
| | 0 | 302 | | MaxListOrder = 3 |
| | 0 | 303 | | }); |
| | | 304 | | |
| | 0 | 305 | | var itemIds = items.Select(i => i.Id).ToList(); |
| | | 306 | | |
| | 0 | 307 | | return people |
| | 0 | 308 | | .Where(i => itemIds.Contains(i.ItemId)) |
| | 0 | 309 | | .Select(i => i.Name) |
| | 0 | 310 | | .DistinctNames(); |
| | | 311 | | } |
| | | 312 | | |
| | | 313 | | private IEnumerable<string> GetDirectors(IEnumerable<BaseItem> items) |
| | | 314 | | { |
| | 0 | 315 | | var people = _libraryManager.GetPeople(new InternalPeopleQuery( |
| | 0 | 316 | | new[] { PersonType.Director }, |
| | 0 | 317 | | Array.Empty<string>())); |
| | | 318 | | |
| | 0 | 319 | | var itemIds = items.Select(i => i.Id).ToList(); |
| | | 320 | | |
| | 0 | 321 | | return people |
| | 0 | 322 | | .Where(i => itemIds.Contains(i.ItemId)) |
| | 0 | 323 | | .Select(i => i.Name) |
| | 0 | 324 | | .DistinctNames(); |
| | | 325 | | } |
| | | 326 | | } |