< Summary - Jellyfin

Information
Class: Jellyfin.Api.Controllers.UserLibraryController
Assembly: Jellyfin.Api
File(s): /srv/git/jellyfin/Jellyfin.Api/Controllers/UserLibraryController.cs
Line coverage
28%
Covered lines: 39
Uncovered lines: 100
Coverable lines: 139
Total lines: 696
Line coverage: 28%
Branch coverage
19%
Covered branches: 10
Total branches: 52
Branch coverage: 19.2%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Coverage history

Coverage history 0 25 50 75 100 9/14/2025 - 12:09:49 AM Line coverage: 27.8% (39/140) Branch coverage: 19.2% (10/52) Total lines: 69612/1/2025 - 12:11:46 AM Line coverage: 28% (39/139) Branch coverage: 19.2% (10/52) Total lines: 69512/9/2025 - 12:12:43 AM Line coverage: 28% (39/139) Branch coverage: 19.2% (10/52) Total lines: 696 9/14/2025 - 12:09:49 AM Line coverage: 27.8% (39/140) Branch coverage: 19.2% (10/52) Total lines: 69612/1/2025 - 12:11:46 AM Line coverage: 28% (39/139) Branch coverage: 19.2% (10/52) Total lines: 69512/9/2025 - 12:12:43 AM Line coverage: 28% (39/139) Branch coverage: 19.2% (10/52) Total lines: 696

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(...)50%13856.25%
GetSpecialFeatures(...)66.66%8664.28%
GetLatestMedia(...)0%7280%
MarkFavorite(...)0%620%
UpdateUserItemRatingInternal(...)0%620%

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.Enums;
 11using Jellyfin.Database.Implementations.Entities;
 12using Jellyfin.Extensions;
 13using MediaBrowser.Controller.Dto;
 14using MediaBrowser.Controller.Entities;
 15using MediaBrowser.Controller.Entities.Audio;
 16using MediaBrowser.Controller.Entities.TV;
 17using MediaBrowser.Controller.Library;
 18using MediaBrowser.Controller.Providers;
 19using MediaBrowser.Model.Dto;
 20using MediaBrowser.Model.Entities;
 21using MediaBrowser.Model.IO;
 22using MediaBrowser.Model.Querying;
 23using Microsoft.AspNetCore.Authorization;
 24using Microsoft.AspNetCore.Http;
 25using Microsoft.AspNetCore.Mvc;
 26
 27namespace Jellyfin.Api.Controllers;
 28
 29/// <summary>
 30/// User library controller.
 31/// </summary>
 32[Route("")]
 33[Authorize]
 34public class UserLibraryController : BaseJellyfinApiController
 35{
 36    private readonly IUserManager _userManager;
 37    private readonly IUserDataManager _userDataRepository;
 38    private readonly ILibraryManager _libraryManager;
 39    private readonly IDtoService _dtoService;
 40    private readonly IUserViewManager _userViewManager;
 41    private readonly IFileSystem _fileSystem;
 42
 43    /// <summary>
 44    /// Initializes a new instance of the <see cref="UserLibraryController"/> class.
 45    /// </summary>
 46    /// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param>
 47    /// <param name="userDataRepository">Instance of the <see cref="IUserDataManager"/> interface.</param>
 48    /// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
 49    /// <param name="dtoService">Instance of the <see cref="IDtoService"/> interface.</param>
 50    /// <param name="userViewManager">Instance of the <see cref="IUserViewManager"/> interface.</param>
 51    /// <param name="fileSystem">Instance of the <see cref="IFileSystem"/> interface.</param>
 1552    public UserLibraryController(
 1553        IUserManager userManager,
 1554        IUserDataManager userDataRepository,
 1555        ILibraryManager libraryManager,
 1556        IDtoService dtoService,
 1557        IUserViewManager userViewManager,
 1558        IFileSystem fileSystem)
 59    {
 1560        _userManager = userManager;
 1561        _userDataRepository = userDataRepository;
 1562        _libraryManager = libraryManager;
 1563        _dtoService = dtoService;
 1564        _userViewManager = userViewManager;
 1565        _fileSystem = fileSystem;
 1566    }
 67
 68    /// <summary>
 69    /// Gets an item from a user's library.
 70    /// </summary>
 71    /// <param name="userId">User id.</param>
 72    /// <param name="itemId">Item id.</param>
 73    /// <response code="200">Item returned.</response>
 74    /// <returns>An <see cref="OkResult"/> containing the item.</returns>
 75    [HttpGet("Items/{itemId}")]
 76    [ProducesResponseType(StatusCodes.Status200OK)]
 77    public async Task<ActionResult<BaseItemDto>> GetItem(
 78        [FromQuery] Guid? userId,
 79        [FromRoute, Required] Guid itemId)
 80    {
 81        userId = RequestHelpers.GetUserId(User, userId);
 82        var user = _userManager.GetUserById(userId.Value);
 83        if (user is null)
 84        {
 85            return NotFound();
 86        }
 87
 88        var item = itemId.IsEmpty()
 89            ? _libraryManager.GetUserRootFolder()
 90            : _libraryManager.GetItemById<BaseItem>(itemId, user);
 91        if (item is null)
 92        {
 93            return NotFound();
 94        }
 95
 96        await RefreshItemOnDemandIfNeeded(item).ConfigureAwait(false);
 97
 98        var dtoOptions = new DtoOptions();
 99
 100        return _dtoService.GetBaseItemDto(item, dtoOptions, user);
 101    }
 102
 103    /// <summary>
 104    /// Gets an item from a user's library.
 105    /// </summary>
 106    /// <param name="userId">User id.</param>
 107    /// <param name="itemId">Item id.</param>
 108    /// <response code="200">Item returned.</response>
 109    /// <returns>An <see cref="OkResult"/> containing the item.</returns>
 110    [HttpGet("Users/{userId}/Items/{itemId}")]
 111    [ProducesResponseType(StatusCodes.Status200OK)]
 112    [Obsolete("Kept for backwards compatibility")]
 113    [ApiExplorerSettings(IgnoreApi = true)]
 114    public Task<ActionResult<BaseItemDto>> GetItemLegacy(
 115        [FromRoute, Required] Guid userId,
 116        [FromRoute, Required] Guid itemId)
 117        => GetItem(userId, itemId);
 118
 119    /// <summary>
 120    /// Gets the root folder from a user's library.
 121    /// </summary>
 122    /// <param name="userId">User id.</param>
 123    /// <response code="200">Root folder returned.</response>
 124    /// <returns>An <see cref="OkResult"/> containing the user's root folder.</returns>
 125    [HttpGet("Items/Root")]
 126    [ProducesResponseType(StatusCodes.Status200OK)]
 127    public ActionResult<BaseItemDto> GetRootFolder([FromQuery] Guid? userId)
 128    {
 7129        userId = RequestHelpers.GetUserId(User, userId);
 7130        var user = _userManager.GetUserById(userId.Value);
 7131        if (user is null)
 132        {
 1133            return NotFound();
 134        }
 135
 6136        var item = _libraryManager.GetUserRootFolder();
 6137        var dtoOptions = new DtoOptions();
 6138        return _dtoService.GetBaseItemDto(item, dtoOptions, user);
 139    }
 140
 141    /// <summary>
 142    /// Gets the root folder from a user's library.
 143    /// </summary>
 144    /// <param name="userId">User id.</param>
 145    /// <response code="200">Root folder returned.</response>
 146    /// <returns>An <see cref="OkResult"/> containing the user's root folder.</returns>
 147    [HttpGet("Users/{userId}/Items/Root")]
 148    [ProducesResponseType(StatusCodes.Status200OK)]
 149    [Obsolete("Kept for backwards compatibility")]
 150    [ApiExplorerSettings(IgnoreApi = true)]
 151    public ActionResult<BaseItemDto> GetRootFolderLegacy(
 152        [FromRoute, Required] Guid userId)
 153        => GetRootFolder(userId);
 154
 155    /// <summary>
 156    /// Gets intros to play before the main media item plays.
 157    /// </summary>
 158    /// <param name="userId">User id.</param>
 159    /// <param name="itemId">Item id.</param>
 160    /// <response code="200">Intros returned.</response>
 161    /// <returns>An <see cref="OkResult"/> containing the intros to play.</returns>
 162    [HttpGet("Items/{itemId}/Intros")]
 163    [ProducesResponseType(StatusCodes.Status200OK)]
 164    public async Task<ActionResult<QueryResult<BaseItemDto>>> GetIntros(
 165        [FromQuery] Guid? userId,
 166        [FromRoute, Required] Guid itemId)
 167    {
 168        userId = RequestHelpers.GetUserId(User, userId);
 169        var user = _userManager.GetUserById(userId.Value);
 170        if (user is null)
 171        {
 172            return NotFound();
 173        }
 174
 175        var item = itemId.IsEmpty()
 176            ? _libraryManager.GetUserRootFolder()
 177            : _libraryManager.GetItemById<BaseItem>(itemId, user);
 178        if (item is null)
 179        {
 180            return NotFound();
 181        }
 182
 183        var items = await _libraryManager.GetIntros(item, user).ConfigureAwait(false);
 184        var dtoOptions = new DtoOptions();
 185        var dtos = items.Select(i => _dtoService.GetBaseItemDto(i, dtoOptions, user)).ToArray();
 186
 187        return new QueryResult<BaseItemDto>(dtos);
 188    }
 189
 190    /// <summary>
 191    /// Gets intros to play before the main media item plays.
 192    /// </summary>
 193    /// <param name="userId">User id.</param>
 194    /// <param name="itemId">Item id.</param>
 195    /// <response code="200">Intros returned.</response>
 196    /// <returns>An <see cref="OkResult"/> containing the intros to play.</returns>
 197    [HttpGet("Users/{userId}/Items/{itemId}/Intros")]
 198    [ProducesResponseType(StatusCodes.Status200OK)]
 199    [Obsolete("Kept for backwards compatibility")]
 200    [ApiExplorerSettings(IgnoreApi = true)]
 201    public Task<ActionResult<QueryResult<BaseItemDto>>> GetIntrosLegacy(
 202        [FromRoute, Required] Guid userId,
 203        [FromRoute, Required] Guid itemId)
 204        => GetIntros(userId, itemId);
 205
 206    /// <summary>
 207    /// Marks an item as a favorite.
 208    /// </summary>
 209    /// <param name="userId">User id.</param>
 210    /// <param name="itemId">Item id.</param>
 211    /// <response code="200">Item marked as favorite.</response>
 212    /// <returns>An <see cref="OkResult"/> containing the <see cref="UserItemDataDto"/>.</returns>
 213    [HttpPost("UserFavoriteItems/{itemId}")]
 214    [ProducesResponseType(StatusCodes.Status200OK)]
 215    public ActionResult<UserItemDataDto> MarkFavoriteItem(
 216        [FromQuery] Guid? userId,
 217        [FromRoute, Required] Guid itemId)
 218    {
 0219        userId = RequestHelpers.GetUserId(User, userId);
 0220        var user = _userManager.GetUserById(userId.Value);
 0221        if (user is null)
 222        {
 0223            return NotFound();
 224        }
 225
 0226        var item = itemId.IsEmpty()
 0227            ? _libraryManager.GetUserRootFolder()
 0228            : _libraryManager.GetItemById<BaseItem>(itemId, user);
 0229        if (item is null)
 230        {
 0231            return NotFound();
 232        }
 233
 0234        return MarkFavorite(user, item, true);
 235    }
 236
 237    /// <summary>
 238    /// Marks an item as a favorite.
 239    /// </summary>
 240    /// <param name="userId">User id.</param>
 241    /// <param name="itemId">Item id.</param>
 242    /// <response code="200">Item marked as favorite.</response>
 243    /// <returns>An <see cref="OkResult"/> containing the <see cref="UserItemDataDto"/>.</returns>
 244    [HttpPost("Users/{userId}/FavoriteItems/{itemId}")]
 245    [ProducesResponseType(StatusCodes.Status200OK)]
 246    [Obsolete("Kept for backwards compatibility")]
 247    [ApiExplorerSettings(IgnoreApi = true)]
 248    public ActionResult<UserItemDataDto> MarkFavoriteItemLegacy(
 249        [FromRoute, Required] Guid userId,
 250        [FromRoute, Required] Guid itemId)
 251        => MarkFavoriteItem(userId, itemId);
 252
 253    /// <summary>
 254    /// Unmarks item as a favorite.
 255    /// </summary>
 256    /// <param name="userId">User id.</param>
 257    /// <param name="itemId">Item id.</param>
 258    /// <response code="200">Item unmarked as favorite.</response>
 259    /// <returns>An <see cref="OkResult"/> containing the <see cref="UserItemDataDto"/>.</returns>
 260    [HttpDelete("UserFavoriteItems/{itemId}")]
 261    [ProducesResponseType(StatusCodes.Status200OK)]
 262    public ActionResult<UserItemDataDto> UnmarkFavoriteItem(
 263        [FromQuery] Guid? userId,
 264        [FromRoute, Required] Guid itemId)
 265    {
 0266        userId = RequestHelpers.GetUserId(User, userId);
 0267        var user = _userManager.GetUserById(userId.Value);
 0268        if (user is null)
 269        {
 0270            return NotFound();
 271        }
 272
 0273        var item = itemId.IsEmpty()
 0274            ? _libraryManager.GetUserRootFolder()
 0275            : _libraryManager.GetItemById<BaseItem>(itemId, user);
 0276        if (item is null)
 277        {
 0278            return NotFound();
 279        }
 280
 0281        return MarkFavorite(user, item, false);
 282    }
 283
 284    /// <summary>
 285    /// Unmarks item as a favorite.
 286    /// </summary>
 287    /// <param name="userId">User id.</param>
 288    /// <param name="itemId">Item id.</param>
 289    /// <response code="200">Item unmarked as favorite.</response>
 290    /// <returns>An <see cref="OkResult"/> containing the <see cref="UserItemDataDto"/>.</returns>
 291    [HttpDelete("Users/{userId}/FavoriteItems/{itemId}")]
 292    [ProducesResponseType(StatusCodes.Status200OK)]
 293    [Obsolete("Kept for backwards compatibility")]
 294    [ApiExplorerSettings(IgnoreApi = true)]
 295    public ActionResult<UserItemDataDto> UnmarkFavoriteItemLegacy(
 296        [FromRoute, Required] Guid userId,
 297        [FromRoute, Required] Guid itemId)
 298        => UnmarkFavoriteItem(userId, itemId);
 299
 300    /// <summary>
 301    /// Deletes a user's saved personal rating for an item.
 302    /// </summary>
 303    /// <param name="userId">User id.</param>
 304    /// <param name="itemId">Item id.</param>
 305    /// <response code="200">Personal rating removed.</response>
 306    /// <returns>An <see cref="OkResult"/> containing the <see cref="UserItemDataDto"/>.</returns>
 307    [HttpDelete("UserItems/{itemId}/Rating")]
 308    [ProducesResponseType(StatusCodes.Status200OK)]
 309    public ActionResult<UserItemDataDto?> DeleteUserItemRating(
 310        [FromQuery] Guid? userId,
 311        [FromRoute, Required] Guid itemId)
 312    {
 0313        userId = RequestHelpers.GetUserId(User, userId);
 0314        var user = _userManager.GetUserById(userId.Value);
 0315        if (user is null)
 316        {
 0317            return NotFound();
 318        }
 319
 0320        var item = itemId.IsEmpty()
 0321            ? _libraryManager.GetUserRootFolder()
 0322            : _libraryManager.GetItemById<BaseItem>(itemId, user);
 0323        if (item is null)
 324        {
 0325            return NotFound();
 326        }
 327
 0328        return UpdateUserItemRatingInternal(user, item, null);
 329    }
 330
 331    /// <summary>
 332    /// Deletes a user's saved personal rating for an item.
 333    /// </summary>
 334    /// <param name="userId">User id.</param>
 335    /// <param name="itemId">Item id.</param>
 336    /// <response code="200">Personal rating removed.</response>
 337    /// <returns>An <see cref="OkResult"/> containing the <see cref="UserItemDataDto"/>.</returns>
 338    [HttpDelete("Users/{userId}/Items/{itemId}/Rating")]
 339    [ProducesResponseType(StatusCodes.Status200OK)]
 340    [Obsolete("Kept for backwards compatibility")]
 341    [ApiExplorerSettings(IgnoreApi = true)]
 342    public ActionResult<UserItemDataDto?> DeleteUserItemRatingLegacy(
 343        [FromRoute, Required] Guid userId,
 344        [FromRoute, Required] Guid itemId)
 345        => DeleteUserItemRating(userId, itemId);
 346
 347    /// <summary>
 348    /// Updates a user's rating for an item.
 349    /// </summary>
 350    /// <param name="userId">User id.</param>
 351    /// <param name="itemId">Item id.</param>
 352    /// <param name="likes">Whether this <see cref="UpdateUserItemRating" /> is likes.</param>
 353    /// <response code="200">Item rating updated.</response>
 354    /// <returns>An <see cref="OkResult"/> containing the <see cref="UserItemDataDto"/>.</returns>
 355    [HttpPost("UserItems/{itemId}/Rating")]
 356    [ProducesResponseType(StatusCodes.Status200OK)]
 357    public ActionResult<UserItemDataDto?> UpdateUserItemRating(
 358        [FromQuery] Guid? userId,
 359        [FromRoute, Required] Guid itemId,
 360        [FromQuery] bool? likes)
 361    {
 0362        userId = RequestHelpers.GetUserId(User, userId);
 0363        var user = _userManager.GetUserById(userId.Value);
 0364        if (user is null)
 365        {
 0366            return NotFound();
 367        }
 368
 0369        var item = itemId.IsEmpty()
 0370            ? _libraryManager.GetUserRootFolder()
 0371            : _libraryManager.GetItemById<BaseItem>(itemId, user);
 0372        if (item is null)
 373        {
 0374            return NotFound();
 375        }
 376
 0377        return UpdateUserItemRatingInternal(user, item, likes);
 378    }
 379
 380    /// <summary>
 381    /// Updates a user's rating for an item.
 382    /// </summary>
 383    /// <param name="userId">User id.</param>
 384    /// <param name="itemId">Item id.</param>
 385    /// <param name="likes">Whether this <see cref="UpdateUserItemRating" /> is likes.</param>
 386    /// <response code="200">Item rating updated.</response>
 387    /// <returns>An <see cref="OkResult"/> containing the <see cref="UserItemDataDto"/>.</returns>
 388    [HttpPost("Users/{userId}/Items/{itemId}/Rating")]
 389    [ProducesResponseType(StatusCodes.Status200OK)]
 390    [Obsolete("Kept for backwards compatibility")]
 391    [ApiExplorerSettings(IgnoreApi = true)]
 392    public ActionResult<UserItemDataDto?> UpdateUserItemRatingLegacy(
 393        [FromRoute, Required] Guid userId,
 394        [FromRoute, Required] Guid itemId,
 395        [FromQuery] bool? likes)
 396        => UpdateUserItemRating(userId, itemId, likes);
 397
 398    /// <summary>
 399    /// Gets local trailers for an item.
 400    /// </summary>
 401    /// <param name="userId">User id.</param>
 402    /// <param name="itemId">Item id.</param>
 403    /// <response code="200">An <see cref="OkResult"/> containing the item's local trailers.</response>
 404    /// <returns>The items local trailers.</returns>
 405    [HttpGet("Items/{itemId}/LocalTrailers")]
 406    [ProducesResponseType(StatusCodes.Status200OK)]
 407    public ActionResult<IEnumerable<BaseItemDto>> GetLocalTrailers(
 408        [FromQuery] Guid? userId,
 409        [FromRoute, Required] Guid itemId)
 410    {
 2411        userId = RequestHelpers.GetUserId(User, userId);
 2412        var user = _userManager.GetUserById(userId.Value);
 2413        if (user is null)
 414        {
 1415            return NotFound();
 416        }
 417
 1418        var item = itemId.IsEmpty()
 1419            ? _libraryManager.GetUserRootFolder()
 1420            : _libraryManager.GetItemById<BaseItem>(itemId, user);
 1421        if (item is null)
 422        {
 1423            return NotFound();
 424        }
 425
 0426        var dtoOptions = new DtoOptions();
 0427        if (item is IHasTrailers hasTrailers)
 428        {
 0429            var trailers = hasTrailers.LocalTrailers;
 0430            return Ok(_dtoService.GetBaseItemDtos(trailers, dtoOptions, user, item).AsEnumerable());
 431        }
 432
 0433        return Ok(item.GetExtras()
 0434            .Where(e => e.ExtraType == ExtraType.Trailer)
 0435            .Select(i => _dtoService.GetBaseItemDto(i, dtoOptions, user, item)));
 436    }
 437
 438    /// <summary>
 439    /// Gets local trailers for an item.
 440    /// </summary>
 441    /// <param name="userId">User id.</param>
 442    /// <param name="itemId">Item id.</param>
 443    /// <response code="200">An <see cref="OkResult"/> containing the item's local trailers.</response>
 444    /// <returns>The items local trailers.</returns>
 445    [HttpGet("Users/{userId}/Items/{itemId}/LocalTrailers")]
 446    [ProducesResponseType(StatusCodes.Status200OK)]
 447    [Obsolete("Kept for backwards compatibility")]
 448    [ApiExplorerSettings(IgnoreApi = true)]
 449    public ActionResult<IEnumerable<BaseItemDto>> GetLocalTrailersLegacy(
 450        [FromRoute, Required] Guid userId,
 451        [FromRoute, Required] Guid itemId)
 452        => GetLocalTrailers(userId, itemId);
 453
 454    /// <summary>
 455    /// Gets special features for an item.
 456    /// </summary>
 457    /// <param name="userId">User id.</param>
 458    /// <param name="itemId">Item id.</param>
 459    /// <response code="200">Special features returned.</response>
 460    /// <returns>An <see cref="OkResult"/> containing the special features.</returns>
 461    [HttpGet("Items/{itemId}/SpecialFeatures")]
 462    [ProducesResponseType(StatusCodes.Status200OK)]
 463    public ActionResult<IEnumerable<BaseItemDto>> GetSpecialFeatures(
 464        [FromQuery] Guid? userId,
 465        [FromRoute, Required] Guid itemId)
 466    {
 2467        userId = RequestHelpers.GetUserId(User, userId);
 2468        var user = _userManager.GetUserById(userId.Value);
 2469        if (user is null)
 470        {
 1471            return NotFound();
 472        }
 473
 1474        var item = itemId.IsEmpty()
 1475            ? _libraryManager.GetUserRootFolder()
 1476            : _libraryManager.GetItemById<BaseItem>(itemId, user);
 1477        if (item is null)
 478        {
 1479            return NotFound();
 480        }
 481
 0482        var dtoOptions = new DtoOptions();
 483
 0484        return Ok(item
 0485            .GetExtras()
 0486            .Where(i => i.ExtraType.HasValue && BaseItem.DisplayExtraTypes.Contains(i.ExtraType.Value))
 0487            .Select(i => _dtoService.GetBaseItemDto(i, dtoOptions, user, item)));
 488    }
 489
 490    /// <summary>
 491    /// Gets special features for an item.
 492    /// </summary>
 493    /// <param name="userId">User id.</param>
 494    /// <param name="itemId">Item id.</param>
 495    /// <response code="200">Special features returned.</response>
 496    /// <returns>An <see cref="OkResult"/> containing the special features.</returns>
 497    [HttpGet("Users/{userId}/Items/{itemId}/SpecialFeatures")]
 498    [ProducesResponseType(StatusCodes.Status200OK)]
 499    [Obsolete("Kept for backwards compatibility")]
 500    [ApiExplorerSettings(IgnoreApi = true)]
 501    public ActionResult<IEnumerable<BaseItemDto>> GetSpecialFeaturesLegacy(
 502        [FromRoute, Required] Guid userId,
 503        [FromRoute, Required] Guid itemId)
 504        => GetSpecialFeatures(userId, itemId);
 505
 506    /// <summary>
 507    /// Gets latest media.
 508    /// </summary>
 509    /// <param name="userId">User id.</param>
 510    /// <param name="parentId">Specify this to localize the search to a specific item or folder. Omit to use the root.</
 511    /// <param name="fields">Optional. Specify additional fields of information to return in the output.</param>
 512    /// <param name="includeItemTypes">Optional. If specified, results will be filtered based on item type. This allows 
 513    /// <param name="isPlayed">Filter by items that are played, or not.</param>
 514    /// <param name="enableImages">Optional. include image information in output.</param>
 515    /// <param name="imageTypeLimit">Optional. the max number of images to return, per image type.</param>
 516    /// <param name="enableImageTypes">Optional. The image types to include in the output.</param>
 517    /// <param name="enableUserData">Optional. include user data.</param>
 518    /// <param name="limit">Return item limit.</param>
 519    /// <param name="groupItems">Whether or not to group items into a parent container.</param>
 520    /// <response code="200">Latest media returned.</response>
 521    /// <returns>An <see cref="OkResult"/> containing the latest media.</returns>
 522    [HttpGet("Items/Latest")]
 523    [ProducesResponseType(StatusCodes.Status200OK)]
 524    public ActionResult<IEnumerable<BaseItemDto>> GetLatestMedia(
 525        [FromQuery] Guid? userId,
 526        [FromQuery] Guid? parentId,
 527        [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] ItemFields[] fields,
 528        [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] BaseItemKind[] includeItemTypes,
 529        [FromQuery] bool? isPlayed,
 530        [FromQuery] bool? enableImages,
 531        [FromQuery] int? imageTypeLimit,
 532        [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] ImageType[] enableImageTypes,
 533        [FromQuery] bool? enableUserData,
 534        [FromQuery] int limit = 20,
 535        [FromQuery] bool groupItems = true)
 536    {
 0537        var requestUserId = RequestHelpers.GetUserId(User, userId);
 0538        var user = _userManager.GetUserById(requestUserId);
 0539        if (user is null)
 540        {
 0541            return NotFound();
 542        }
 543
 0544        if (!isPlayed.HasValue)
 545        {
 0546            if (user.HidePlayedInLatest)
 547            {
 0548                isPlayed = false;
 549            }
 550        }
 551
 0552        var dtoOptions = new DtoOptions { Fields = fields }
 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 || i.Item1 is Series ))
 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(CommaDelimitedCollectionModelBinder))] ItemFields[] fields,
 612        [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] BaseItemKind[] includeItemTypes,
 613        [FromQuery] bool? isPlayed,
 614        [FromQuery] bool? enableImages,
 615        [FromQuery] int? imageTypeLimit,
 616        [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] 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 hasMetadata = !string.IsNullOrWhiteSpace(item.Overview) && item.HasImage(ImageType.Primary);
 638            var performFullRefresh = !hasMetadata && (DateTime.UtcNow - item.DateLastRefreshed).TotalDays >= 3;
 639
 640            if (!hasMetadata)
 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
 0665        if (data is not null)
 666        {
 667            // Set favorite status
 0668            data.IsFavorite = isFavorite;
 669
 0670            _userDataRepository.SaveUserData(user, item, data, UserDataSaveReason.UpdateUserRating, CancellationToken.No
 671        }
 672
 0673        return _userDataRepository.GetUserDataDto(item, user)!;
 674    }
 675
 676    /// <summary>
 677    /// Updates the user item rating.
 678    /// </summary>
 679    /// <param name="user">The user.</param>
 680    /// <param name="item">The item.</param>
 681    /// <param name="likes">if set to <c>true</c> [likes].</param>
 682    private UserItemDataDto? UpdateUserItemRatingInternal(User user, BaseItem item, bool? likes)
 683    {
 684        // Get the user data for this item
 0685        var data = _userDataRepository.GetUserData(user, item);
 686
 0687        if (data is not null)
 688        {
 0689            data.Likes = likes;
 690
 0691            _userDataRepository.SaveUserData(user, item, data, UserDataSaveReason.UpdateUserRating, CancellationToken.No
 692        }
 693
 0694        return _userDataRepository.GetUserDataDto(item, user);
 695    }
 696}