< Summary - Jellyfin

Information
Class: Jellyfin.Api.Controllers.UserLibraryController
Assembly: Jellyfin.Api
File(s): /srv/git/jellyfin/Jellyfin.Api/Controllers/UserLibraryController.cs
Line coverage
35%
Covered lines: 49
Uncovered lines: 89
Coverable lines: 138
Total lines: 690
Line coverage: 35.5%
Branch coverage
27%
Covered branches: 13
Total branches: 48
Branch coverage: 27%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Coverage history

Coverage history 0 25 50 75 100

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.ctor(...)100%11100%
GetRootFolder(...)100%22100%
MarkFavoriteItem(...)0%4260%
UnmarkFavoriteItem(...)0%4260%
DeleteUserItemRating(...)0%4260%
UpdateUserItemRating(...)0%4260%
GetLocalTrailers(...)75%8.13887.5%
GetSpecialFeatures(...)83.33%66100%
GetLatestMedia(...)0%7280%
MarkFavorite(...)100%210%
UpdateUserItemRatingInternal(...)100%210%

File(s)

/srv/git/jellyfin/Jellyfin.Api/Controllers/UserLibraryController.cs

#LineLine coverage
 1using System;
 2using System.Collections.Generic;
 3using System.ComponentModel.DataAnnotations;
 4using System.Linq;
 5using System.Threading;
 6using System.Threading.Tasks;
 7using Jellyfin.Api.Extensions;
 8using Jellyfin.Api.Helpers;
 9using Jellyfin.Api.ModelBinders;
 10using Jellyfin.Data.Entities;
 11using Jellyfin.Data.Enums;
 12using Jellyfin.Extensions;
 13using MediaBrowser.Controller.Dto;
 14using MediaBrowser.Controller.Entities;
 15using MediaBrowser.Controller.Entities.Audio;
 16using MediaBrowser.Controller.Library;
 17using MediaBrowser.Controller.Providers;
 18using MediaBrowser.Model.Dto;
 19using MediaBrowser.Model.Entities;
 20using MediaBrowser.Model.IO;
 21using MediaBrowser.Model.Querying;
 22using Microsoft.AspNetCore.Authorization;
 23using Microsoft.AspNetCore.Http;
 24using Microsoft.AspNetCore.Mvc;
 25
 26namespace Jellyfin.Api.Controllers;
 27
 28/// <summary>
 29/// User library controller.
 30/// </summary>
 31[Route("")]
 32[Authorize]
 33public class UserLibraryController : BaseJellyfinApiController
 34{
 35    private readonly IUserManager _userManager;
 36    private readonly IUserDataManager _userDataRepository;
 37    private readonly ILibraryManager _libraryManager;
 38    private readonly IDtoService _dtoService;
 39    private readonly IUserViewManager _userViewManager;
 40    private readonly IFileSystem _fileSystem;
 41
 42    /// <summary>
 43    /// Initializes a new instance of the <see cref="UserLibraryController"/> class.
 44    /// </summary>
 45    /// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param>
 46    /// <param name="userDataRepository">Instance of the <see cref="IUserDataManager"/> interface.</param>
 47    /// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
 48    /// <param name="dtoService">Instance of the <see cref="IDtoService"/> interface.</param>
 49    /// <param name="userViewManager">Instance of the <see cref="IUserViewManager"/> interface.</param>
 50    /// <param name="fileSystem">Instance of the <see cref="IFileSystem"/> interface.</param>
 2351    public UserLibraryController(
 2352        IUserManager userManager,
 2353        IUserDataManager userDataRepository,
 2354        ILibraryManager libraryManager,
 2355        IDtoService dtoService,
 2356        IUserViewManager userViewManager,
 2357        IFileSystem fileSystem)
 58    {
 2359        _userManager = userManager;
 2360        _userDataRepository = userDataRepository;
 2361        _libraryManager = libraryManager;
 2362        _dtoService = dtoService;
 2363        _userViewManager = userViewManager;
 2364        _fileSystem = fileSystem;
 2365    }
 66
 67    /// <summary>
 68    /// Gets an item from a user's library.
 69    /// </summary>
 70    /// <param name="userId">User id.</param>
 71    /// <param name="itemId">Item id.</param>
 72    /// <response code="200">Item returned.</response>
 73    /// <returns>An <see cref="OkResult"/> containing the item.</returns>
 74    [HttpGet("Items/{itemId}")]
 75    [ProducesResponseType(StatusCodes.Status200OK)]
 76    public async Task<ActionResult<BaseItemDto>> GetItem(
 77        [FromQuery] Guid? userId,
 78        [FromRoute, Required] Guid itemId)
 79    {
 80        userId = RequestHelpers.GetUserId(User, userId);
 81        var user = _userManager.GetUserById(userId.Value);
 82        if (user is null)
 83        {
 84            return NotFound();
 85        }
 86
 87        var item = itemId.IsEmpty()
 88            ? _libraryManager.GetUserRootFolder()
 89            : _libraryManager.GetItemById<BaseItem>(itemId, user);
 90        if (item is null)
 91        {
 92            return NotFound();
 93        }
 94
 95        await RefreshItemOnDemandIfNeeded(item).ConfigureAwait(false);
 96
 97        var dtoOptions = new DtoOptions().AddClientFields(User);
 98
 99        return _dtoService.GetBaseItemDto(item, dtoOptions, user);
 100    }
 101
 102    /// <summary>
 103    /// Gets an item from a user's library.
 104    /// </summary>
 105    /// <param name="userId">User id.</param>
 106    /// <param name="itemId">Item id.</param>
 107    /// <response code="200">Item returned.</response>
 108    /// <returns>An <see cref="OkResult"/> containing the item.</returns>
 109    [HttpGet("Users/{userId}/Items/{itemId}")]
 110    [ProducesResponseType(StatusCodes.Status200OK)]
 111    [Obsolete("Kept for backwards compatibility")]
 112    [ApiExplorerSettings(IgnoreApi = true)]
 113    public Task<ActionResult<BaseItemDto>> GetItemLegacy(
 114        [FromRoute, Required] Guid userId,
 115        [FromRoute, Required] Guid itemId)
 116        => GetItem(userId, itemId);
 117
 118    /// <summary>
 119    /// Gets the root folder from a user's library.
 120    /// </summary>
 121    /// <param name="userId">User id.</param>
 122    /// <response code="200">Root folder returned.</response>
 123    /// <returns>An <see cref="OkResult"/> containing the user's root folder.</returns>
 124    [HttpGet("Items/Root")]
 125    [ProducesResponseType(StatusCodes.Status200OK)]
 126    public ActionResult<BaseItemDto> GetRootFolder([FromQuery] Guid? userId)
 127    {
 11128        userId = RequestHelpers.GetUserId(User, userId);
 11129        var user = _userManager.GetUserById(userId.Value);
 11130        if (user is null)
 131        {
 1132            return NotFound();
 133        }
 134
 10135        var item = _libraryManager.GetUserRootFolder();
 10136        var dtoOptions = new DtoOptions().AddClientFields(User);
 10137        return _dtoService.GetBaseItemDto(item, dtoOptions, user);
 138    }
 139
 140    /// <summary>
 141    /// Gets the root folder from a user's library.
 142    /// </summary>
 143    /// <param name="userId">User id.</param>
 144    /// <response code="200">Root folder returned.</response>
 145    /// <returns>An <see cref="OkResult"/> containing the user's root folder.</returns>
 146    [HttpGet("Users/{userId}/Items/Root")]
 147    [ProducesResponseType(StatusCodes.Status200OK)]
 148    [Obsolete("Kept for backwards compatibility")]
 149    [ApiExplorerSettings(IgnoreApi = true)]
 150    public ActionResult<BaseItemDto> GetRootFolderLegacy(
 151        [FromRoute, Required] Guid userId)
 152        => GetRootFolder(userId);
 153
 154    /// <summary>
 155    /// Gets intros to play before the main media item plays.
 156    /// </summary>
 157    /// <param name="userId">User id.</param>
 158    /// <param name="itemId">Item id.</param>
 159    /// <response code="200">Intros returned.</response>
 160    /// <returns>An <see cref="OkResult"/> containing the intros to play.</returns>
 161    [HttpGet("Items/{itemId}/Intros")]
 162    [ProducesResponseType(StatusCodes.Status200OK)]
 163    public async Task<ActionResult<QueryResult<BaseItemDto>>> GetIntros(
 164        [FromQuery] Guid? userId,
 165        [FromRoute, Required] Guid itemId)
 166    {
 167        userId = RequestHelpers.GetUserId(User, userId);
 168        var user = _userManager.GetUserById(userId.Value);
 169        if (user is null)
 170        {
 171            return NotFound();
 172        }
 173
 174        var item = itemId.IsEmpty()
 175            ? _libraryManager.GetUserRootFolder()
 176            : _libraryManager.GetItemById<BaseItem>(itemId, user);
 177        if (item is null)
 178        {
 179            return NotFound();
 180        }
 181
 182        var items = await _libraryManager.GetIntros(item, user).ConfigureAwait(false);
 183        var dtoOptions = new DtoOptions().AddClientFields(User);
 184        var dtos = items.Select(i => _dtoService.GetBaseItemDto(i, dtoOptions, user)).ToArray();
 185
 186        return new QueryResult<BaseItemDto>(dtos);
 187    }
 188
 189    /// <summary>
 190    /// Gets intros to play before the main media item plays.
 191    /// </summary>
 192    /// <param name="userId">User id.</param>
 193    /// <param name="itemId">Item id.</param>
 194    /// <response code="200">Intros returned.</response>
 195    /// <returns>An <see cref="OkResult"/> containing the intros to play.</returns>
 196    [HttpGet("Users/{userId}/Items/{itemId}/Intros")]
 197    [ProducesResponseType(StatusCodes.Status200OK)]
 198    [Obsolete("Kept for backwards compatibility")]
 199    [ApiExplorerSettings(IgnoreApi = true)]
 200    public Task<ActionResult<QueryResult<BaseItemDto>>> GetIntrosLegacy(
 201        [FromRoute, Required] Guid userId,
 202        [FromRoute, Required] Guid itemId)
 203        => GetIntros(userId, itemId);
 204
 205    /// <summary>
 206    /// Marks an item as a favorite.
 207    /// </summary>
 208    /// <param name="userId">User id.</param>
 209    /// <param name="itemId">Item id.</param>
 210    /// <response code="200">Item marked as favorite.</response>
 211    /// <returns>An <see cref="OkResult"/> containing the <see cref="UserItemDataDto"/>.</returns>
 212    [HttpPost("UserFavoriteItems/{itemId}")]
 213    [ProducesResponseType(StatusCodes.Status200OK)]
 214    public ActionResult<UserItemDataDto> MarkFavoriteItem(
 215        [FromQuery] Guid? userId,
 216        [FromRoute, Required] Guid itemId)
 217    {
 0218        userId = RequestHelpers.GetUserId(User, userId);
 0219        var user = _userManager.GetUserById(userId.Value);
 0220        if (user is null)
 221        {
 0222            return NotFound();
 223        }
 224
 0225        var item = itemId.IsEmpty()
 0226            ? _libraryManager.GetUserRootFolder()
 0227            : _libraryManager.GetItemById<BaseItem>(itemId, user);
 0228        if (item is null)
 229        {
 0230            return NotFound();
 231        }
 232
 0233        return MarkFavorite(user, item, true);
 234    }
 235
 236    /// <summary>
 237    /// Marks an item as a favorite.
 238    /// </summary>
 239    /// <param name="userId">User id.</param>
 240    /// <param name="itemId">Item id.</param>
 241    /// <response code="200">Item marked as favorite.</response>
 242    /// <returns>An <see cref="OkResult"/> containing the <see cref="UserItemDataDto"/>.</returns>
 243    [HttpPost("Users/{userId}/FavoriteItems/{itemId}")]
 244    [ProducesResponseType(StatusCodes.Status200OK)]
 245    [Obsolete("Kept for backwards compatibility")]
 246    [ApiExplorerSettings(IgnoreApi = true)]
 247    public ActionResult<UserItemDataDto> MarkFavoriteItemLegacy(
 248        [FromRoute, Required] Guid userId,
 249        [FromRoute, Required] Guid itemId)
 250        => MarkFavoriteItem(userId, itemId);
 251
 252    /// <summary>
 253    /// Unmarks item as a favorite.
 254    /// </summary>
 255    /// <param name="userId">User id.</param>
 256    /// <param name="itemId">Item id.</param>
 257    /// <response code="200">Item unmarked as favorite.</response>
 258    /// <returns>An <see cref="OkResult"/> containing the <see cref="UserItemDataDto"/>.</returns>
 259    [HttpDelete("UserFavoriteItems/{itemId}")]
 260    [ProducesResponseType(StatusCodes.Status200OK)]
 261    public ActionResult<UserItemDataDto> UnmarkFavoriteItem(
 262        [FromQuery] Guid? userId,
 263        [FromRoute, Required] Guid itemId)
 264    {
 0265        userId = RequestHelpers.GetUserId(User, userId);
 0266        var user = _userManager.GetUserById(userId.Value);
 0267        if (user is null)
 268        {
 0269            return NotFound();
 270        }
 271
 0272        var item = itemId.IsEmpty()
 0273            ? _libraryManager.GetUserRootFolder()
 0274            : _libraryManager.GetItemById<BaseItem>(itemId, user);
 0275        if (item is null)
 276        {
 0277            return NotFound();
 278        }
 279
 0280        return MarkFavorite(user, item, false);
 281    }
 282
 283    /// <summary>
 284    /// Unmarks item as a favorite.
 285    /// </summary>
 286    /// <param name="userId">User id.</param>
 287    /// <param name="itemId">Item id.</param>
 288    /// <response code="200">Item unmarked as favorite.</response>
 289    /// <returns>An <see cref="OkResult"/> containing the <see cref="UserItemDataDto"/>.</returns>
 290    [HttpDelete("Users/{userId}/FavoriteItems/{itemId}")]
 291    [ProducesResponseType(StatusCodes.Status200OK)]
 292    [Obsolete("Kept for backwards compatibility")]
 293    [ApiExplorerSettings(IgnoreApi = true)]
 294    public ActionResult<UserItemDataDto> UnmarkFavoriteItemLegacy(
 295        [FromRoute, Required] Guid userId,
 296        [FromRoute, Required] Guid itemId)
 297        => UnmarkFavoriteItem(userId, itemId);
 298
 299    /// <summary>
 300    /// Deletes a user's saved personal rating for an item.
 301    /// </summary>
 302    /// <param name="userId">User id.</param>
 303    /// <param name="itemId">Item id.</param>
 304    /// <response code="200">Personal rating removed.</response>
 305    /// <returns>An <see cref="OkResult"/> containing the <see cref="UserItemDataDto"/>.</returns>
 306    [HttpDelete("UserItems/{itemId}/Rating")]
 307    [ProducesResponseType(StatusCodes.Status200OK)]
 308    public ActionResult<UserItemDataDto> DeleteUserItemRating(
 309        [FromQuery] Guid? userId,
 310        [FromRoute, Required] Guid itemId)
 311    {
 0312        userId = RequestHelpers.GetUserId(User, userId);
 0313        var user = _userManager.GetUserById(userId.Value);
 0314        if (user is null)
 315        {
 0316            return NotFound();
 317        }
 318
 0319        var item = itemId.IsEmpty()
 0320            ? _libraryManager.GetUserRootFolder()
 0321            : _libraryManager.GetItemById<BaseItem>(itemId, user);
 0322        if (item is null)
 323        {
 0324            return NotFound();
 325        }
 326
 0327        return UpdateUserItemRatingInternal(user, item, null);
 328    }
 329
 330    /// <summary>
 331    /// Deletes a user's saved personal rating for an item.
 332    /// </summary>
 333    /// <param name="userId">User id.</param>
 334    /// <param name="itemId">Item id.</param>
 335    /// <response code="200">Personal rating removed.</response>
 336    /// <returns>An <see cref="OkResult"/> containing the <see cref="UserItemDataDto"/>.</returns>
 337    [HttpDelete("Users/{userId}/Items/{itemId}/Rating")]
 338    [ProducesResponseType(StatusCodes.Status200OK)]
 339    [Obsolete("Kept for backwards compatibility")]
 340    [ApiExplorerSettings(IgnoreApi = true)]
 341    public ActionResult<UserItemDataDto> DeleteUserItemRatingLegacy(
 342        [FromRoute, Required] Guid userId,
 343        [FromRoute, Required] Guid itemId)
 344        => DeleteUserItemRating(userId, itemId);
 345
 346    /// <summary>
 347    /// Updates a user's rating for an item.
 348    /// </summary>
 349    /// <param name="userId">User id.</param>
 350    /// <param name="itemId">Item id.</param>
 351    /// <param name="likes">Whether this <see cref="UpdateUserItemRating" /> is likes.</param>
 352    /// <response code="200">Item rating updated.</response>
 353    /// <returns>An <see cref="OkResult"/> containing the <see cref="UserItemDataDto"/>.</returns>
 354    [HttpPost("UserItems/{itemId}/Rating")]
 355    [ProducesResponseType(StatusCodes.Status200OK)]
 356    public ActionResult<UserItemDataDto> UpdateUserItemRating(
 357        [FromQuery] Guid? userId,
 358        [FromRoute, Required] Guid itemId,
 359        [FromQuery] bool? likes)
 360    {
 0361        userId = RequestHelpers.GetUserId(User, userId);
 0362        var user = _userManager.GetUserById(userId.Value);
 0363        if (user is null)
 364        {
 0365            return NotFound();
 366        }
 367
 0368        var item = itemId.IsEmpty()
 0369            ? _libraryManager.GetUserRootFolder()
 0370            : _libraryManager.GetItemById<BaseItem>(itemId, user);
 0371        if (item is null)
 372        {
 0373            return NotFound();
 374        }
 375
 0376        return UpdateUserItemRatingInternal(user, item, likes);
 377    }
 378
 379    /// <summary>
 380    /// Updates a user's rating for an item.
 381    /// </summary>
 382    /// <param name="userId">User id.</param>
 383    /// <param name="itemId">Item id.</param>
 384    /// <param name="likes">Whether this <see cref="UpdateUserItemRating" /> is likes.</param>
 385    /// <response code="200">Item rating updated.</response>
 386    /// <returns>An <see cref="OkResult"/> containing the <see cref="UserItemDataDto"/>.</returns>
 387    [HttpPost("Users/{userId}/Items/{itemId}/Rating")]
 388    [ProducesResponseType(StatusCodes.Status200OK)]
 389    [Obsolete("Kept for backwards compatibility")]
 390    [ApiExplorerSettings(IgnoreApi = true)]
 391    public ActionResult<UserItemDataDto> UpdateUserItemRatingLegacy(
 392        [FromRoute, Required] Guid userId,
 393        [FromRoute, Required] Guid itemId,
 394        [FromQuery] bool? likes)
 395        => UpdateUserItemRating(userId, itemId, likes);
 396
 397    /// <summary>
 398    /// Gets local trailers for an item.
 399    /// </summary>
 400    /// <param name="userId">User id.</param>
 401    /// <param name="itemId">Item id.</param>
 402    /// <response code="200">An <see cref="OkResult"/> containing the item's local trailers.</response>
 403    /// <returns>The items local trailers.</returns>
 404    [HttpGet("Items/{itemId}/LocalTrailers")]
 405    [ProducesResponseType(StatusCodes.Status200OK)]
 406    public ActionResult<IEnumerable<BaseItemDto>> GetLocalTrailers(
 407        [FromQuery] Guid? userId,
 408        [FromRoute, Required] Guid itemId)
 409    {
 3410        userId = RequestHelpers.GetUserId(User, userId);
 3411        var user = _userManager.GetUserById(userId.Value);
 3412        if (user is null)
 413        {
 1414            return NotFound();
 415        }
 416
 2417        var item = itemId.IsEmpty()
 2418            ? _libraryManager.GetUserRootFolder()
 2419            : _libraryManager.GetItemById<BaseItem>(itemId, user);
 2420        if (item is null)
 421        {
 1422            return NotFound();
 423        }
 424
 1425        var dtoOptions = new DtoOptions().AddClientFields(User);
 1426        if (item is IHasTrailers hasTrailers)
 427        {
 0428            var trailers = hasTrailers.LocalTrailers;
 0429            return Ok(_dtoService.GetBaseItemDtos(trailers, dtoOptions, user, item).AsEnumerable());
 430        }
 431
 1432        return Ok(item.GetExtras()
 1433            .Where(e => e.ExtraType == ExtraType.Trailer)
 1434            .Select(i => _dtoService.GetBaseItemDto(i, dtoOptions, user, item)));
 435    }
 436
 437    /// <summary>
 438    /// Gets local trailers for an item.
 439    /// </summary>
 440    /// <param name="userId">User id.</param>
 441    /// <param name="itemId">Item id.</param>
 442    /// <response code="200">An <see cref="OkResult"/> containing the item's local trailers.</response>
 443    /// <returns>The items local trailers.</returns>
 444    [HttpGet("Users/{userId}/Items/{itemId}/LocalTrailers")]
 445    [ProducesResponseType(StatusCodes.Status200OK)]
 446    [Obsolete("Kept for backwards compatibility")]
 447    [ApiExplorerSettings(IgnoreApi = true)]
 448    public ActionResult<IEnumerable<BaseItemDto>> GetLocalTrailersLegacy(
 449        [FromRoute, Required] Guid userId,
 450        [FromRoute, Required] Guid itemId)
 451        => GetLocalTrailers(userId, itemId);
 452
 453    /// <summary>
 454    /// Gets special features for an item.
 455    /// </summary>
 456    /// <param name="userId">User id.</param>
 457    /// <param name="itemId">Item id.</param>
 458    /// <response code="200">Special features returned.</response>
 459    /// <returns>An <see cref="OkResult"/> containing the special features.</returns>
 460    [HttpGet("Items/{itemId}/SpecialFeatures")]
 461    [ProducesResponseType(StatusCodes.Status200OK)]
 462    public ActionResult<IEnumerable<BaseItemDto>> GetSpecialFeatures(
 463        [FromQuery] Guid? userId,
 464        [FromRoute, Required] Guid itemId)
 465    {
 3466        userId = RequestHelpers.GetUserId(User, userId);
 3467        var user = _userManager.GetUserById(userId.Value);
 3468        if (user is null)
 469        {
 1470            return NotFound();
 471        }
 472
 2473        var item = itemId.IsEmpty()
 2474            ? _libraryManager.GetUserRootFolder()
 2475            : _libraryManager.GetItemById<BaseItem>(itemId, user);
 2476        if (item is null)
 477        {
 1478            return NotFound();
 479        }
 480
 1481        var dtoOptions = new DtoOptions().AddClientFields(User);
 482
 1483        return Ok(item
 1484            .GetExtras()
 1485            .Where(i => i.ExtraType.HasValue && BaseItem.DisplayExtraTypes.Contains(i.ExtraType.Value))
 1486            .Select(i => _dtoService.GetBaseItemDto(i, dtoOptions, user, item)));
 487    }
 488
 489    /// <summary>
 490    /// Gets special features for an item.
 491    /// </summary>
 492    /// <param name="userId">User id.</param>
 493    /// <param name="itemId">Item id.</param>
 494    /// <response code="200">Special features returned.</response>
 495    /// <returns>An <see cref="OkResult"/> containing the special features.</returns>
 496    [HttpGet("Users/{userId}/Items/{itemId}/SpecialFeatures")]
 497    [ProducesResponseType(StatusCodes.Status200OK)]
 498    [Obsolete("Kept for backwards compatibility")]
 499    [ApiExplorerSettings(IgnoreApi = true)]
 500    public ActionResult<IEnumerable<BaseItemDto>> GetSpecialFeaturesLegacy(
 501        [FromRoute, Required] Guid userId,
 502        [FromRoute, Required] Guid itemId)
 503        => GetSpecialFeatures(userId, itemId);
 504
 505    /// <summary>
 506    /// Gets latest media.
 507    /// </summary>
 508    /// <param name="userId">User id.</param>
 509    /// <param name="parentId">Specify this to localize the search to a specific item or folder. Omit to use the root.</
 510    /// <param name="fields">Optional. Specify additional fields of information to return in the output.</param>
 511    /// <param name="includeItemTypes">Optional. If specified, results will be filtered based on item type. This allows 
 512    /// <param name="isPlayed">Filter by items that are played, or not.</param>
 513    /// <param name="enableImages">Optional. include image information in output.</param>
 514    /// <param name="imageTypeLimit">Optional. the max number of images to return, per image type.</param>
 515    /// <param name="enableImageTypes">Optional. The image types to include in the output.</param>
 516    /// <param name="enableUserData">Optional. include user data.</param>
 517    /// <param name="limit">Return item limit.</param>
 518    /// <param name="groupItems">Whether or not to group items into a parent container.</param>
 519    /// <response code="200">Latest media returned.</response>
 520    /// <returns>An <see cref="OkResult"/> containing the latest media.</returns>
 521    [HttpGet("Items/Latest")]
 522    [ProducesResponseType(StatusCodes.Status200OK)]
 523    public ActionResult<IEnumerable<BaseItemDto>> GetLatestMedia(
 524        [FromQuery] Guid? userId,
 525        [FromQuery] Guid? parentId,
 526        [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
 527        [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] BaseItemKind[] includeItemTypes,
 528        [FromQuery] bool? isPlayed,
 529        [FromQuery] bool? enableImages,
 530        [FromQuery] int? imageTypeLimit,
 531        [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes,
 532        [FromQuery] bool? enableUserData,
 533        [FromQuery] int limit = 20,
 534        [FromQuery] bool groupItems = true)
 535    {
 0536        var requestUserId = RequestHelpers.GetUserId(User, userId);
 0537        var user = _userManager.GetUserById(requestUserId);
 0538        if (user is null)
 539        {
 0540            return NotFound();
 541        }
 542
 0543        if (!isPlayed.HasValue)
 544        {
 0545            if (user.HidePlayedInLatest)
 546            {
 0547                isPlayed = false;
 548            }
 549        }
 550
 0551        var dtoOptions = new DtoOptions { Fields = fields }
 0552            .AddClientFields(User)
 0553            .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
 554
 0555        var list = _userViewManager.GetLatestItems(
 0556            new LatestItemsQuery
 0557            {
 0558                GroupItems = groupItems,
 0559                IncludeItemTypes = includeItemTypes,
 0560                IsPlayed = isPlayed,
 0561                Limit = limit,
 0562                ParentId = parentId ?? Guid.Empty,
 0563                User = user,
 0564            },
 0565            dtoOptions);
 566
 0567        var dtos = list.Select(i =>
 0568        {
 0569            var item = i.Item2[0];
 0570            var childCount = 0;
 0571
 0572            if (i.Item1 is not null && (i.Item2.Count > 1 || i.Item1 is MusicAlbum))
 0573            {
 0574                item = i.Item1;
 0575                childCount = i.Item2.Count;
 0576            }
 0577
 0578            var dto = _dtoService.GetBaseItemDto(item, dtoOptions, user);
 0579
 0580            dto.ChildCount = childCount;
 0581
 0582            return dto;
 0583        });
 584
 0585        return Ok(dtos);
 586    }
 587
 588    /// <summary>
 589    /// Gets latest media.
 590    /// </summary>
 591    /// <param name="userId">User id.</param>
 592    /// <param name="parentId">Specify this to localize the search to a specific item or folder. Omit to use the root.</
 593    /// <param name="fields">Optional. Specify additional fields of information to return in the output.</param>
 594    /// <param name="includeItemTypes">Optional. If specified, results will be filtered based on item type. This allows 
 595    /// <param name="isPlayed">Filter by items that are played, or not.</param>
 596    /// <param name="enableImages">Optional. include image information in output.</param>
 597    /// <param name="imageTypeLimit">Optional. the max number of images to return, per image type.</param>
 598    /// <param name="enableImageTypes">Optional. The image types to include in the output.</param>
 599    /// <param name="enableUserData">Optional. include user data.</param>
 600    /// <param name="limit">Return item limit.</param>
 601    /// <param name="groupItems">Whether or not to group items into a parent container.</param>
 602    /// <response code="200">Latest media returned.</response>
 603    /// <returns>An <see cref="OkResult"/> containing the latest media.</returns>
 604    [HttpGet("Users/{userId}/Items/Latest")]
 605    [ProducesResponseType(StatusCodes.Status200OK)]
 606    [Obsolete("Kept for backwards compatibility")]
 607    [ApiExplorerSettings(IgnoreApi = true)]
 608    public ActionResult<IEnumerable<BaseItemDto>> GetLatestMediaLegacy(
 609        [FromRoute, Required] Guid userId,
 610        [FromQuery] Guid? parentId,
 611        [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
 612        [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] BaseItemKind[] includeItemTypes,
 613        [FromQuery] bool? isPlayed,
 614        [FromQuery] bool? enableImages,
 615        [FromQuery] int? imageTypeLimit,
 616        [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes,
 617        [FromQuery] bool? enableUserData,
 618        [FromQuery] int limit = 20,
 619        [FromQuery] bool groupItems = true)
 620        => GetLatestMedia(
 621            userId,
 622            parentId,
 623            fields,
 624            includeItemTypes,
 625            isPlayed,
 626            enableImages,
 627            imageTypeLimit,
 628            enableImageTypes,
 629            enableUserData,
 630            limit,
 631            groupItems);
 632
 633    private async Task RefreshItemOnDemandIfNeeded(BaseItem item)
 634    {
 635        if (item is Person)
 636        {
 637            var hasMetdata = !string.IsNullOrWhiteSpace(item.Overview) && item.HasImage(ImageType.Primary);
 638            var performFullRefresh = !hasMetdata && (DateTime.UtcNow - item.DateLastRefreshed).TotalDays >= 3;
 639
 640            if (!hasMetdata)
 641            {
 642                var options = new MetadataRefreshOptions(new DirectoryService(_fileSystem))
 643                {
 644                    MetadataRefreshMode = MetadataRefreshMode.FullRefresh,
 645                    ImageRefreshMode = MetadataRefreshMode.FullRefresh,
 646                    ForceSave = performFullRefresh
 647                };
 648
 649                await item.RefreshMetadata(options, CancellationToken.None).ConfigureAwait(false);
 650            }
 651        }
 652    }
 653
 654    /// <summary>
 655    /// Marks the favorite.
 656    /// </summary>
 657    /// <param name="user">The user.</param>
 658    /// <param name="item">The item.</param>
 659    /// <param name="isFavorite">if set to <c>true</c> [is favorite].</param>
 660    private UserItemDataDto MarkFavorite(User user, BaseItem item, bool isFavorite)
 661    {
 662        // Get the user data for this item
 0663        var data = _userDataRepository.GetUserData(user, item);
 664
 665        // Set favorite status
 0666        data.IsFavorite = isFavorite;
 667
 0668        _userDataRepository.SaveUserData(user, item, data, UserDataSaveReason.UpdateUserRating, CancellationToken.None);
 669
 0670        return _userDataRepository.GetUserDataDto(item, user);
 671    }
 672
 673    /// <summary>
 674    /// Updates the user item rating.
 675    /// </summary>
 676    /// <param name="user">The user.</param>
 677    /// <param name="item">The item.</param>
 678    /// <param name="likes">if set to <c>true</c> [likes].</param>
 679    private UserItemDataDto UpdateUserItemRatingInternal(User user, BaseItem item, bool? likes)
 680    {
 681        // Get the user data for this item
 0682        var data = _userDataRepository.GetUserData(user, item);
 683
 0684        data.Likes = likes;
 685
 0686        _userDataRepository.SaveUserData(user, item, data, UserDataSaveReason.UpdateUserRating, CancellationToken.None);
 687
 0688        return _userDataRepository.GetUserDataDto(item, user);
 689    }
 690}