| | | 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 | | } |