< Summary - Jellyfin

Information
Class: Jellyfin.Api.Controllers.UserLibraryController
Assembly: Jellyfin.Api
File(s): /srv/git/jellyfin/Jellyfin.Api/Controllers/UserLibraryController.cs
Line coverage
34%
Covered lines: 59
Uncovered lines: 114
Coverable lines: 173
Total lines: 707
Line coverage: 34.1%
Branch coverage
21%
Covered branches: 18
Total branches: 82
Branch coverage: 21.9%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Coverage history

Coverage history 0 25 50 75 100 3/5/2026 - 12:13:57 AM Line coverage: 28% (39/139) Branch coverage: 19.2% (10/52) Total lines: 6964/19/2026 - 12:14:27 AM Line coverage: 33.1% (59/178) Branch coverage: 25% (18/72) Total lines: 6965/4/2026 - 12:15:16 AM Line coverage: 33.5% (59/176) Branch coverage: 20.9% (18/86) Total lines: 7065/13/2026 - 12:15:27 AM Line coverage: 33.5% (59/176) Branch coverage: 20.9% (18/86) Total lines: 7116/2/2026 - 12:15:49 AM Line coverage: 33.3% (59/177) Branch coverage: 21.4% (18/84) Total lines: 7136/6/2026 - 12:15:50 AM Line coverage: 34.1% (59/173) Branch coverage: 21.9% (18/82) Total lines: 707 3/5/2026 - 12:13:57 AM Line coverage: 28% (39/139) Branch coverage: 19.2% (10/52) Total lines: 6964/19/2026 - 12:14:27 AM Line coverage: 33.1% (59/178) Branch coverage: 25% (18/72) Total lines: 6965/4/2026 - 12:15:16 AM Line coverage: 33.5% (59/176) Branch coverage: 20.9% (18/86) Total lines: 7065/13/2026 - 12:15:27 AM Line coverage: 33.5% (59/176) Branch coverage: 20.9% (18/86) Total lines: 7116/2/2026 - 12:15:49 AM Line coverage: 33.3% (59/177) Branch coverage: 21.4% (18/84) Total lines: 7136/6/2026 - 12:15:50 AM Line coverage: 34.1% (59/173) Branch coverage: 21.9% (18/82) Total lines: 707

Coverage delta

Coverage delta 6 -6

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.ctor(...)100%11100%
GetItem()66.66%6676.92%
GetRootFolder(...)100%22100%
GetIntros()66.66%7671.42%
MarkFavoriteItem(...)0%4260%
UnmarkFavoriteItem(...)0%4260%
DeleteUserItemRating(...)0%4260%
UpdateUserItemRating(...)0%4260%
GetLocalTrailers(...)66.66%7675%
GetSpecialFeatures(...)66.66%8664.28%
GetLatestMedia(...)0%420200%
RefreshItemOnDemandIfNeeded()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]
 34[Tags("Library")]
 35public class UserLibraryController : BaseJellyfinApiController
 36{
 37    private readonly IUserManager _userManager;
 38    private readonly IUserDataManager _userDataRepository;
 39    private readonly ILibraryManager _libraryManager;
 40    private readonly IDtoService _dtoService;
 41    private readonly IUserViewManager _userViewManager;
 42    private readonly IFileSystem _fileSystem;
 43
 44    /// <summary>
 45    /// Initializes a new instance of the <see cref="UserLibraryController"/> class.
 46    /// </summary>
 47    /// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param>
 48    /// <param name="userDataRepository">Instance of the <see cref="IUserDataManager"/> interface.</param>
 49    /// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
 50    /// <param name="dtoService">Instance of the <see cref="IDtoService"/> interface.</param>
 51    /// <param name="userViewManager">Instance of the <see cref="IUserViewManager"/> interface.</param>
 52    /// <param name="fileSystem">Instance of the <see cref="IFileSystem"/> interface.</param>
 1553    public UserLibraryController(
 1554        IUserManager userManager,
 1555        IUserDataManager userDataRepository,
 1556        ILibraryManager libraryManager,
 1557        IDtoService dtoService,
 1558        IUserViewManager userViewManager,
 1559        IFileSystem fileSystem)
 60    {
 1561        _userManager = userManager;
 1562        _userDataRepository = userDataRepository;
 1563        _libraryManager = libraryManager;
 1564        _dtoService = dtoService;
 1565        _userViewManager = userViewManager;
 1566        _fileSystem = fileSystem;
 1567    }
 68
 69    /// <summary>
 70    /// Gets an item from a user's library.
 71    /// </summary>
 72    /// <param name="userId">User id.</param>
 73    /// <param name="itemId">Item id.</param>
 74    /// <response code="200">Item returned.</response>
 75    /// <returns>An <see cref="OkResult"/> containing the item.</returns>
 76    [HttpGet("Items/{itemId}")]
 77    [ProducesResponseType(StatusCodes.Status200OK)]
 78    public async Task<ActionResult<BaseItemDto>> GetItem(
 79        [FromQuery] Guid? userId,
 80        [FromRoute, Required] Guid itemId)
 81    {
 282        userId = RequestHelpers.GetUserId(User, userId);
 283        var user = _userManager.GetUserById(userId.Value);
 284        if (user is null)
 85        {
 186            return NotFound();
 87        }
 88
 189        var item = itemId.IsEmpty()
 190            ? _libraryManager.GetUserRootFolder()
 191            : _libraryManager.GetItemById<BaseItem>(itemId, user);
 192        if (item is null)
 93        {
 194            return NotFound();
 95        }
 96
 097        await RefreshItemOnDemandIfNeeded(item).ConfigureAwait(false);
 98
 099        var dtoOptions = new DtoOptions();
 100
 0101        return _dtoService.GetBaseItemDto(item, dtoOptions, user);
 2102    }
 103
 104    /// <summary>
 105    /// Gets an item from a user's library.
 106    /// </summary>
 107    /// <param name="userId">User id.</param>
 108    /// <param name="itemId">Item id.</param>
 109    /// <response code="200">Item returned.</response>
 110    /// <returns>An <see cref="OkResult"/> containing the item.</returns>
 111    [HttpGet("Users/{userId}/Items/{itemId}")]
 112    [ProducesResponseType(StatusCodes.Status200OK)]
 113    [Obsolete("Kept for backwards compatibility")]
 114    [ApiExplorerSettings(IgnoreApi = true)]
 115    public Task<ActionResult<BaseItemDto>> GetItemLegacy(
 116        [FromRoute, Required] Guid userId,
 117        [FromRoute, Required] Guid itemId)
 118        => GetItem(userId, itemId);
 119
 120    /// <summary>
 121    /// Gets the root folder from a user's library.
 122    /// </summary>
 123    /// <param name="userId">User id.</param>
 124    /// <response code="200">Root folder returned.</response>
 125    /// <returns>An <see cref="OkResult"/> containing the user's root folder.</returns>
 126    [HttpGet("Items/Root")]
 127    [ProducesResponseType(StatusCodes.Status200OK)]
 128    public ActionResult<BaseItemDto> GetRootFolder([FromQuery] Guid? userId)
 129    {
 7130        userId = RequestHelpers.GetUserId(User, userId);
 7131        var user = _userManager.GetUserById(userId.Value);
 7132        if (user is null)
 133        {
 1134            return NotFound();
 135        }
 136
 6137        var item = _libraryManager.GetUserRootFolder();
 6138        var dtoOptions = new DtoOptions();
 6139        return _dtoService.GetBaseItemDto(item, dtoOptions, user);
 140    }
 141
 142    /// <summary>
 143    /// Gets the root folder from a user's library.
 144    /// </summary>
 145    /// <param name="userId">User id.</param>
 146    /// <response code="200">Root folder returned.</response>
 147    /// <returns>An <see cref="OkResult"/> containing the user's root folder.</returns>
 148    [HttpGet("Users/{userId}/Items/Root")]
 149    [ProducesResponseType(StatusCodes.Status200OK)]
 150    [Obsolete("Kept for backwards compatibility")]
 151    [ApiExplorerSettings(IgnoreApi = true)]
 152    public ActionResult<BaseItemDto> GetRootFolderLegacy(
 153        [FromRoute, Required] Guid userId)
 154        => GetRootFolder(userId);
 155
 156    /// <summary>
 157    /// Gets intros to play before the main media item plays.
 158    /// </summary>
 159    /// <param name="userId">User id.</param>
 160    /// <param name="itemId">Item id.</param>
 161    /// <response code="200">Intros returned.</response>
 162    /// <returns>An <see cref="OkResult"/> containing the intros to play.</returns>
 163    [HttpGet("Items/{itemId}/Intros")]
 164    [ProducesResponseType(StatusCodes.Status200OK)]
 165    public async Task<ActionResult<QueryResult<BaseItemDto>>> GetIntros(
 166        [FromQuery] Guid? userId,
 167        [FromRoute, Required] Guid itemId)
 168    {
 2169        userId = RequestHelpers.GetUserId(User, userId);
 2170        var user = _userManager.GetUserById(userId.Value);
 2171        if (user is null)
 172        {
 1173            return NotFound();
 174        }
 175
 1176        var item = itemId.IsEmpty()
 1177            ? _libraryManager.GetUserRootFolder()
 1178            : _libraryManager.GetItemById<BaseItem>(itemId, user);
 1179        if (item is null)
 180        {
 1181            return NotFound();
 182        }
 183
 0184        var items = await _libraryManager.GetIntros(item, user).ConfigureAwait(false);
 0185        var dtoOptions = new DtoOptions();
 0186        var dtos = items.Select(i => _dtoService.GetBaseItemDto(i, dtoOptions, user)).ToArray();
 187
 0188        return new QueryResult<BaseItemDto>(dtos);
 2189    }
 190
 191    /// <summary>
 192    /// Gets intros to play before the main media item plays.
 193    /// </summary>
 194    /// <param name="userId">User id.</param>
 195    /// <param name="itemId">Item id.</param>
 196    /// <response code="200">Intros returned.</response>
 197    /// <returns>An <see cref="OkResult"/> containing the intros to play.</returns>
 198    [HttpGet("Users/{userId}/Items/{itemId}/Intros")]
 199    [ProducesResponseType(StatusCodes.Status200OK)]
 200    [Obsolete("Kept for backwards compatibility")]
 201    [ApiExplorerSettings(IgnoreApi = true)]
 202    public Task<ActionResult<QueryResult<BaseItemDto>>> GetIntrosLegacy(
 203        [FromRoute, Required] Guid userId,
 204        [FromRoute, Required] Guid itemId)
 205        => GetIntros(userId, itemId);
 206
 207    /// <summary>
 208    /// Marks an item as a favorite.
 209    /// </summary>
 210    /// <param name="userId">User id.</param>
 211    /// <param name="itemId">Item id.</param>
 212    /// <response code="200">Item marked as favorite.</response>
 213    /// <returns>An <see cref="OkResult"/> containing the <see cref="UserItemDataDto"/>.</returns>
 214    [HttpPost("UserFavoriteItems/{itemId}")]
 215    [ProducesResponseType(StatusCodes.Status200OK)]
 216    [Tags("UserData")]
 217    public ActionResult<UserItemDataDto> MarkFavoriteItem(
 218        [FromQuery] Guid? userId,
 219        [FromRoute, Required] Guid itemId)
 220    {
 0221        userId = RequestHelpers.GetUserId(User, userId);
 0222        var user = _userManager.GetUserById(userId.Value);
 0223        if (user is null)
 224        {
 0225            return NotFound();
 226        }
 227
 0228        var item = itemId.IsEmpty()
 0229            ? _libraryManager.GetUserRootFolder()
 0230            : _libraryManager.GetItemById<BaseItem>(itemId, user);
 0231        if (item is null)
 232        {
 0233            return NotFound();
 234        }
 235
 0236        return MarkFavorite(user, item, true);
 237    }
 238
 239    /// <summary>
 240    /// Marks an item as a favorite.
 241    /// </summary>
 242    /// <param name="userId">User id.</param>
 243    /// <param name="itemId">Item id.</param>
 244    /// <response code="200">Item marked as favorite.</response>
 245    /// <returns>An <see cref="OkResult"/> containing the <see cref="UserItemDataDto"/>.</returns>
 246    [HttpPost("Users/{userId}/FavoriteItems/{itemId}")]
 247    [ProducesResponseType(StatusCodes.Status200OK)]
 248    [Obsolete("Kept for backwards compatibility")]
 249    [ApiExplorerSettings(IgnoreApi = true)]
 250    public ActionResult<UserItemDataDto> MarkFavoriteItemLegacy(
 251        [FromRoute, Required] Guid userId,
 252        [FromRoute, Required] Guid itemId)
 253        => MarkFavoriteItem(userId, itemId);
 254
 255    /// <summary>
 256    /// Unmarks item as a favorite.
 257    /// </summary>
 258    /// <param name="userId">User id.</param>
 259    /// <param name="itemId">Item id.</param>
 260    /// <response code="200">Item unmarked as favorite.</response>
 261    /// <returns>An <see cref="OkResult"/> containing the <see cref="UserItemDataDto"/>.</returns>
 262    [HttpDelete("UserFavoriteItems/{itemId}")]
 263    [ProducesResponseType(StatusCodes.Status200OK)]
 264    [Tags("UserData")]
 265    public ActionResult<UserItemDataDto> UnmarkFavoriteItem(
 266        [FromQuery] Guid? userId,
 267        [FromRoute, Required] Guid itemId)
 268    {
 0269        userId = RequestHelpers.GetUserId(User, userId);
 0270        var user = _userManager.GetUserById(userId.Value);
 0271        if (user is null)
 272        {
 0273            return NotFound();
 274        }
 275
 0276        var item = itemId.IsEmpty()
 0277            ? _libraryManager.GetUserRootFolder()
 0278            : _libraryManager.GetItemById<BaseItem>(itemId, user);
 0279        if (item is null)
 280        {
 0281            return NotFound();
 282        }
 283
 0284        return MarkFavorite(user, item, false);
 285    }
 286
 287    /// <summary>
 288    /// Unmarks item as a favorite.
 289    /// </summary>
 290    /// <param name="userId">User id.</param>
 291    /// <param name="itemId">Item id.</param>
 292    /// <response code="200">Item unmarked as favorite.</response>
 293    /// <returns>An <see cref="OkResult"/> containing the <see cref="UserItemDataDto"/>.</returns>
 294    [HttpDelete("Users/{userId}/FavoriteItems/{itemId}")]
 295    [ProducesResponseType(StatusCodes.Status200OK)]
 296    [Obsolete("Kept for backwards compatibility")]
 297    [ApiExplorerSettings(IgnoreApi = true)]
 298    public ActionResult<UserItemDataDto> UnmarkFavoriteItemLegacy(
 299        [FromRoute, Required] Guid userId,
 300        [FromRoute, Required] Guid itemId)
 301        => UnmarkFavoriteItem(userId, itemId);
 302
 303    /// <summary>
 304    /// Deletes a user's saved personal rating for an item.
 305    /// </summary>
 306    /// <param name="userId">User id.</param>
 307    /// <param name="itemId">Item id.</param>
 308    /// <response code="200">Personal rating removed.</response>
 309    /// <returns>An <see cref="OkResult"/> containing the <see cref="UserItemDataDto"/>.</returns>
 310    [HttpDelete("UserItems/{itemId}/Rating")]
 311    [ProducesResponseType(StatusCodes.Status200OK)]
 312    [Tags("UserData")]
 313    public ActionResult<UserItemDataDto?> DeleteUserItemRating(
 314        [FromQuery] Guid? userId,
 315        [FromRoute, Required] Guid itemId)
 316    {
 0317        userId = RequestHelpers.GetUserId(User, userId);
 0318        var user = _userManager.GetUserById(userId.Value);
 0319        if (user is null)
 320        {
 0321            return NotFound();
 322        }
 323
 0324        var item = itemId.IsEmpty()
 0325            ? _libraryManager.GetUserRootFolder()
 0326            : _libraryManager.GetItemById<BaseItem>(itemId, user);
 0327        if (item is null)
 328        {
 0329            return NotFound();
 330        }
 331
 0332        return UpdateUserItemRatingInternal(user, item, null);
 333    }
 334
 335    /// <summary>
 336    /// Deletes a user's saved personal rating for an item.
 337    /// </summary>
 338    /// <param name="userId">User id.</param>
 339    /// <param name="itemId">Item id.</param>
 340    /// <response code="200">Personal rating removed.</response>
 341    /// <returns>An <see cref="OkResult"/> containing the <see cref="UserItemDataDto"/>.</returns>
 342    [HttpDelete("Users/{userId}/Items/{itemId}/Rating")]
 343    [ProducesResponseType(StatusCodes.Status200OK)]
 344    [Obsolete("Kept for backwards compatibility")]
 345    [ApiExplorerSettings(IgnoreApi = true)]
 346    public ActionResult<UserItemDataDto?> DeleteUserItemRatingLegacy(
 347        [FromRoute, Required] Guid userId,
 348        [FromRoute, Required] Guid itemId)
 349        => DeleteUserItemRating(userId, itemId);
 350
 351    /// <summary>
 352    /// Updates a user's rating for an item.
 353    /// </summary>
 354    /// <param name="userId">User id.</param>
 355    /// <param name="itemId">Item id.</param>
 356    /// <param name="likes">Whether this <see cref="UpdateUserItemRating" /> is likes.</param>
 357    /// <response code="200">Item rating updated.</response>
 358    /// <returns>An <see cref="OkResult"/> containing the <see cref="UserItemDataDto"/>.</returns>
 359    [HttpPost("UserItems/{itemId}/Rating")]
 360    [ProducesResponseType(StatusCodes.Status200OK)]
 361    [Tags("UserData")]
 362    public ActionResult<UserItemDataDto?> UpdateUserItemRating(
 363        [FromQuery] Guid? userId,
 364        [FromRoute, Required] Guid itemId,
 365        [FromQuery] bool? likes)
 366    {
 0367        userId = RequestHelpers.GetUserId(User, userId);
 0368        var user = _userManager.GetUserById(userId.Value);
 0369        if (user is null)
 370        {
 0371            return NotFound();
 372        }
 373
 0374        var item = itemId.IsEmpty()
 0375            ? _libraryManager.GetUserRootFolder()
 0376            : _libraryManager.GetItemById<BaseItem>(itemId, user);
 0377        if (item is null)
 378        {
 0379            return NotFound();
 380        }
 381
 0382        return UpdateUserItemRatingInternal(user, item, likes);
 383    }
 384
 385    /// <summary>
 386    /// Updates a user's rating for an item.
 387    /// </summary>
 388    /// <param name="userId">User id.</param>
 389    /// <param name="itemId">Item id.</param>
 390    /// <param name="likes">Whether this <see cref="UpdateUserItemRating" /> is likes.</param>
 391    /// <response code="200">Item rating updated.</response>
 392    /// <returns>An <see cref="OkResult"/> containing the <see cref="UserItemDataDto"/>.</returns>
 393    [HttpPost("Users/{userId}/Items/{itemId}/Rating")]
 394    [ProducesResponseType(StatusCodes.Status200OK)]
 395    [Obsolete("Kept for backwards compatibility")]
 396    [ApiExplorerSettings(IgnoreApi = true)]
 397    public ActionResult<UserItemDataDto?> UpdateUserItemRatingLegacy(
 398        [FromRoute, Required] Guid userId,
 399        [FromRoute, Required] Guid itemId,
 400        [FromQuery] bool? likes)
 401        => UpdateUserItemRating(userId, itemId, likes);
 402
 403    /// <summary>
 404    /// Gets local trailers for an item.
 405    /// </summary>
 406    /// <param name="userId">User id.</param>
 407    /// <param name="itemId">Item id.</param>
 408    /// <response code="200">An <see cref="OkResult"/> containing the item's local trailers.</response>
 409    /// <returns>The items local trailers.</returns>
 410    [HttpGet("Items/{itemId}/LocalTrailers")]
 411    [ProducesResponseType(StatusCodes.Status200OK)]
 412    public ActionResult<IEnumerable<BaseItemDto>> GetLocalTrailers(
 413        [FromQuery] Guid? userId,
 414        [FromRoute, Required] Guid itemId)
 415    {
 2416        userId = RequestHelpers.GetUserId(User, userId);
 2417        var user = _userManager.GetUserById(userId.Value);
 2418        if (user is null)
 419        {
 1420            return NotFound();
 421        }
 422
 1423        var item = itemId.IsEmpty()
 1424            ? _libraryManager.GetUserRootFolder()
 1425            : _libraryManager.GetItemById<BaseItem>(itemId, user);
 1426        if (item is null)
 427        {
 1428            return NotFound();
 429        }
 430
 0431        var dtoOptions = new DtoOptions();
 432
 0433        return Ok(item.GetExtras([ExtraType.Trailer], user)
 0434            .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    {
 2466        userId = RequestHelpers.GetUserId(User, userId);
 2467        var user = _userManager.GetUserById(userId.Value);
 2468        if (user is null)
 469        {
 1470            return NotFound();
 471        }
 472
 1473        var item = itemId.IsEmpty()
 1474            ? _libraryManager.GetUserRootFolder()
 1475            : _libraryManager.GetItemById<BaseItem>(itemId, user);
 1476        if (item is null)
 477        {
 1478            return NotFound();
 479        }
 480
 0481        var dtoOptions = new DtoOptions();
 482
 0483        return Ok(item
 0484            .GetExtras(user)
 0485            .Where(i => i.ExtraType.HasValue && BaseItem.DisplayExtraTypes.Contains(i.ExtraType.Value))
 0486            .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(CommaDelimitedCollectionModelBinder))] ItemFields[] fields,
 527        [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] BaseItemKind[] includeItemTypes,
 528        [FromQuery] bool? isPlayed,
 529        [FromQuery] bool? enableImages,
 530        [FromQuery] int? imageTypeLimit,
 531        [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] 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            .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
 553
 0554        dtoOptions.PreferEpisodeParentPoster = true;
 555
 0556        var list = _userViewManager.GetLatestItems(
 0557            new LatestItemsQuery
 0558            {
 0559                GroupItems = groupItems,
 0560                IncludeItemTypes = includeItemTypes,
 0561                IsPlayed = isPlayed,
 0562                Limit = limit,
 0563                ParentId = parentId ?? Guid.Empty,
 0564                User = user,
 0565            },
 0566            dtoOptions);
 567
 0568        var resolvedItems = new BaseItem[list.Count];
 0569        var childCounts = new int[list.Count];
 0570        for (int i = 0; i < list.Count; i++)
 571        {
 0572            var tuple = list[i];
 0573            var item = tuple.Item2[0];
 0574            var childCount = 0;
 575
 0576            if (tuple.Item1 is not null && (tuple.Item2.Count > 1 || tuple.Item1 is MusicAlbum))
 577            {
 0578                item = tuple.Item1;
 0579                childCount = tuple.Item2.Count;
 580            }
 581
 0582            resolvedItems[i] = item;
 0583            childCounts[i] = childCount;
 584        }
 585
 586        // Fetch DTOs without visibility check since we've already done that in GetLatestItems and restore child counts 
 0587        var dtos = _dtoService.GetBaseItemDtos(resolvedItems, dtoOptions, user, skipVisibilityCheck: true);
 0588        for (int i = 0; i < dtos.Count; i++)
 589        {
 0590            if (childCounts[i] > 0)
 591            {
 0592                dtos[i].ChildCount = childCounts[i];
 593            }
 594        }
 595
 0596        return Ok(dtos.AsEnumerable());
 597    }
 598
 599    /// <summary>
 600    /// Gets latest media.
 601    /// </summary>
 602    /// <param name="userId">User id.</param>
 603    /// <param name="parentId">Specify this to localize the search to a specific item or folder. Omit to use the root.</
 604    /// <param name="fields">Optional. Specify additional fields of information to return in the output.</param>
 605    /// <param name="includeItemTypes">Optional. If specified, results will be filtered based on item type. This allows 
 606    /// <param name="isPlayed">Filter by items that are played, or not.</param>
 607    /// <param name="enableImages">Optional. include image information in output.</param>
 608    /// <param name="imageTypeLimit">Optional. the max number of images to return, per image type.</param>
 609    /// <param name="enableImageTypes">Optional. The image types to include in the output.</param>
 610    /// <param name="enableUserData">Optional. include user data.</param>
 611    /// <param name="limit">Return item limit.</param>
 612    /// <param name="groupItems">Whether or not to group items into a parent container.</param>
 613    /// <response code="200">Latest media returned.</response>
 614    /// <returns>An <see cref="OkResult"/> containing the latest media.</returns>
 615    [HttpGet("Users/{userId}/Items/Latest")]
 616    [ProducesResponseType(StatusCodes.Status200OK)]
 617    [Obsolete("Kept for backwards compatibility")]
 618    [ApiExplorerSettings(IgnoreApi = true)]
 619    public ActionResult<IEnumerable<BaseItemDto>> GetLatestMediaLegacy(
 620        [FromRoute, Required] Guid userId,
 621        [FromQuery] Guid? parentId,
 622        [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] ItemFields[] fields,
 623        [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] BaseItemKind[] includeItemTypes,
 624        [FromQuery] bool? isPlayed,
 625        [FromQuery] bool? enableImages,
 626        [FromQuery] int? imageTypeLimit,
 627        [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] ImageType[] enableImageTypes,
 628        [FromQuery] bool? enableUserData,
 629        [FromQuery] int limit = 20,
 630        [FromQuery] bool groupItems = true)
 631        => GetLatestMedia(
 632            userId,
 633            parentId,
 634            fields,
 635            includeItemTypes,
 636            isPlayed,
 637            enableImages,
 638            imageTypeLimit,
 639            enableImageTypes,
 640            enableUserData,
 641            limit,
 642            groupItems);
 643
 644    private async Task RefreshItemOnDemandIfNeeded(BaseItem item)
 645    {
 0646        if (item is Person)
 647        {
 0648            var hasMetadata = !string.IsNullOrWhiteSpace(item.Overview) && item.HasImage(ImageType.Primary);
 0649            var performFullRefresh = !hasMetadata && (DateTime.UtcNow - item.DateLastRefreshed).TotalDays >= 3;
 650
 0651            if (performFullRefresh)
 652            {
 0653                var options = new MetadataRefreshOptions(new DirectoryService(_fileSystem))
 0654                {
 0655                    MetadataRefreshMode = MetadataRefreshMode.FullRefresh,
 0656                    ImageRefreshMode = MetadataRefreshMode.FullRefresh,
 0657                    ForceSave = true
 0658                };
 659
 0660                await item.RefreshMetadata(options, CancellationToken.None).ConfigureAwait(false);
 661            }
 662        }
 0663    }
 664
 665    /// <summary>
 666    /// Marks the favorite.
 667    /// </summary>
 668    /// <param name="user">The user.</param>
 669    /// <param name="item">The item.</param>
 670    /// <param name="isFavorite">if set to <c>true</c> [is favorite].</param>
 671    private UserItemDataDto MarkFavorite(User user, BaseItem item, bool isFavorite)
 672    {
 673        // Get the user data for this item
 0674        var data = _userDataRepository.GetUserData(user, item);
 675
 0676        if (data is not null)
 677        {
 678            // Set favorite status
 0679            data.IsFavorite = isFavorite;
 680
 0681            _userDataRepository.SaveUserData(user, item, data, UserDataSaveReason.UpdateUserRating, CancellationToken.No
 682        }
 683
 0684        return _userDataRepository.GetUserDataDto(item, user)!;
 685    }
 686
 687    /// <summary>
 688    /// Updates the user item rating.
 689    /// </summary>
 690    /// <param name="user">The user.</param>
 691    /// <param name="item">The item.</param>
 692    /// <param name="likes">if set to <c>true</c> [likes].</param>
 693    private UserItemDataDto? UpdateUserItemRatingInternal(User user, BaseItem item, bool? likes)
 694    {
 695        // Get the user data for this item
 0696        var data = _userDataRepository.GetUserData(user, item);
 697
 0698        if (data is not null)
 699        {
 0700            data.Likes = likes;
 701
 0702            _userDataRepository.SaveUserData(user, item, data, UserDataSaveReason.UpdateUserRating, CancellationToken.No
 703        }
 704
 0705        return _userDataRepository.GetUserDataDto(item, user);
 706    }
 707}