< 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: 113
Coverable lines: 172
Total lines: 705
Line coverage: 34.3%
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 4/8/2026 - 12:11:47 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: 7077/18/2026 - 12:15:19 AM Line coverage: 34.3% (59/172) Branch coverage: 21.9% (18/82) Total lines: 705 4/8/2026 - 12:11:47 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: 7077/18/2026 - 12:15:19 AM Line coverage: 34.3% (59/172) Branch coverage: 21.9% (18/82) Total lines: 705

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        var list = _userViewManager.GetLatestItems(
 0555            new LatestItemsQuery
 0556            {
 0557                GroupItems = groupItems,
 0558                IncludeItemTypes = includeItemTypes,
 0559                IsPlayed = isPlayed,
 0560                Limit = limit,
 0561                ParentId = parentId ?? Guid.Empty,
 0562                User = user,
 0563            },
 0564            dtoOptions);
 565
 0566        var resolvedItems = new BaseItem[list.Count];
 0567        var childCounts = new int[list.Count];
 0568        for (int i = 0; i < list.Count; i++)
 569        {
 0570            var tuple = list[i];
 0571            var item = tuple.Item2[0];
 0572            var childCount = 0;
 573
 0574            if (tuple.Item1 is not null && (tuple.Item2.Count > 1 || tuple.Item1 is MusicAlbum))
 575            {
 0576                item = tuple.Item1;
 0577                childCount = tuple.Item2.Count;
 578            }
 579
 0580            resolvedItems[i] = item;
 0581            childCounts[i] = childCount;
 582        }
 583
 584        // Fetch DTOs without visibility check since we've already done that in GetLatestItems and restore child counts 
 0585        var dtos = _dtoService.GetBaseItemDtos(resolvedItems, dtoOptions, user, skipVisibilityCheck: true);
 0586        for (int i = 0; i < dtos.Count; i++)
 587        {
 0588            if (childCounts[i] > 0)
 589            {
 0590                dtos[i].ChildCount = childCounts[i];
 591            }
 592        }
 593
 0594        return Ok(dtos.AsEnumerable());
 595    }
 596
 597    /// <summary>
 598    /// Gets latest media.
 599    /// </summary>
 600    /// <param name="userId">User id.</param>
 601    /// <param name="parentId">Specify this to localize the search to a specific item or folder. Omit to use the root.</
 602    /// <param name="fields">Optional. Specify additional fields of information to return in the output.</param>
 603    /// <param name="includeItemTypes">Optional. If specified, results will be filtered based on item type. This allows 
 604    /// <param name="isPlayed">Filter by items that are played, or not.</param>
 605    /// <param name="enableImages">Optional. include image information in output.</param>
 606    /// <param name="imageTypeLimit">Optional. the max number of images to return, per image type.</param>
 607    /// <param name="enableImageTypes">Optional. The image types to include in the output.</param>
 608    /// <param name="enableUserData">Optional. include user data.</param>
 609    /// <param name="limit">Return item limit.</param>
 610    /// <param name="groupItems">Whether or not to group items into a parent container.</param>
 611    /// <response code="200">Latest media returned.</response>
 612    /// <returns>An <see cref="OkResult"/> containing the latest media.</returns>
 613    [HttpGet("Users/{userId}/Items/Latest")]
 614    [ProducesResponseType(StatusCodes.Status200OK)]
 615    [Obsolete("Kept for backwards compatibility")]
 616    [ApiExplorerSettings(IgnoreApi = true)]
 617    public ActionResult<IEnumerable<BaseItemDto>> GetLatestMediaLegacy(
 618        [FromRoute, Required] Guid userId,
 619        [FromQuery] Guid? parentId,
 620        [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] ItemFields[] fields,
 621        [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] BaseItemKind[] includeItemTypes,
 622        [FromQuery] bool? isPlayed,
 623        [FromQuery] bool? enableImages,
 624        [FromQuery] int? imageTypeLimit,
 625        [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] ImageType[] enableImageTypes,
 626        [FromQuery] bool? enableUserData,
 627        [FromQuery] int limit = 20,
 628        [FromQuery] bool groupItems = true)
 629        => GetLatestMedia(
 630            userId,
 631            parentId,
 632            fields,
 633            includeItemTypes,
 634            isPlayed,
 635            enableImages,
 636            imageTypeLimit,
 637            enableImageTypes,
 638            enableUserData,
 639            limit,
 640            groupItems);
 641
 642    private async Task RefreshItemOnDemandIfNeeded(BaseItem item)
 643    {
 0644        if (item is Person)
 645        {
 0646            var hasMetadata = !string.IsNullOrWhiteSpace(item.Overview) && item.HasImage(ImageType.Primary);
 0647            var performFullRefresh = !hasMetadata && (DateTime.UtcNow - item.DateLastRefreshed).TotalDays >= 3;
 648
 0649            if (performFullRefresh)
 650            {
 0651                var options = new MetadataRefreshOptions(new DirectoryService(_fileSystem))
 0652                {
 0653                    MetadataRefreshMode = MetadataRefreshMode.FullRefresh,
 0654                    ImageRefreshMode = MetadataRefreshMode.FullRefresh,
 0655                    ForceSave = true
 0656                };
 657
 0658                await item.RefreshMetadata(options, CancellationToken.None).ConfigureAwait(false);
 659            }
 660        }
 0661    }
 662
 663    /// <summary>
 664    /// Marks the favorite.
 665    /// </summary>
 666    /// <param name="user">The user.</param>
 667    /// <param name="item">The item.</param>
 668    /// <param name="isFavorite">if set to <c>true</c> [is favorite].</param>
 669    private UserItemDataDto MarkFavorite(User user, BaseItem item, bool isFavorite)
 670    {
 671        // Get the user data for this item
 0672        var data = _userDataRepository.GetUserData(user, item);
 673
 0674        if (data is not null)
 675        {
 676            // Set favorite status
 0677            data.IsFavorite = isFavorite;
 678
 0679            _userDataRepository.SaveUserData(user, item, data, UserDataSaveReason.UpdateUserRating, CancellationToken.No
 680        }
 681
 0682        return _userDataRepository.GetUserDataDto(item, user)!;
 683    }
 684
 685    /// <summary>
 686    /// Updates the user item rating.
 687    /// </summary>
 688    /// <param name="user">The user.</param>
 689    /// <param name="item">The item.</param>
 690    /// <param name="likes">if set to <c>true</c> [likes].</param>
 691    private UserItemDataDto? UpdateUserItemRatingInternal(User user, BaseItem item, bool? likes)
 692    {
 693        // Get the user data for this item
 0694        var data = _userDataRepository.GetUserData(user, item);
 695
 0696        if (data is not null)
 697        {
 0698            data.Likes = likes;
 699
 0700            _userDataRepository.SaveUserData(user, item, data, UserDataSaveReason.UpdateUserRating, CancellationToken.No
 701        }
 702
 0703        return _userDataRepository.GetUserDataDto(item, user);
 704    }
 705}