< Summary - Jellyfin

Information
Class: Jellyfin.Api.Controllers.UserLibraryController
Assembly: Jellyfin.Api
File(s): /srv/git/jellyfin/Jellyfin.Api/Controllers/UserLibraryController.cs
Line coverage
33%
Covered lines: 59
Uncovered lines: 117
Coverable lines: 176
Total lines: 711
Line coverage: 33.5%
Branch coverage
20%
Covered branches: 18
Total branches: 86
Branch coverage: 20.9%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Coverage history

Coverage history 0 25 50 75 100 2/13/2026 - 12:11:21 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: 711 2/13/2026 - 12:11:21 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: 711

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(...)50%13856.25%
GetSpecialFeatures(...)66.66%8664.28%
GetLatestMedia(...)0%506220%
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();
 0432        if (item is IHasTrailers hasTrailers)
 433        {
 0434            var trailers = hasTrailers.LocalTrailers;
 0435            return Ok(_dtoService.GetBaseItemDtos(trailers, dtoOptions, user, item).AsEnumerable());
 436        }
 437
 0438        return Ok(item.GetExtras()
 0439            .Where(e => e.ExtraType == ExtraType.Trailer)
 0440            .Select(i => _dtoService.GetBaseItemDto(i, dtoOptions, user, item)));
 441    }
 442
 443    /// <summary>
 444    /// Gets local trailers for an item.
 445    /// </summary>
 446    /// <param name="userId">User id.</param>
 447    /// <param name="itemId">Item id.</param>
 448    /// <response code="200">An <see cref="OkResult"/> containing the item's local trailers.</response>
 449    /// <returns>The items local trailers.</returns>
 450    [HttpGet("Users/{userId}/Items/{itemId}/LocalTrailers")]
 451    [ProducesResponseType(StatusCodes.Status200OK)]
 452    [Obsolete("Kept for backwards compatibility")]
 453    [ApiExplorerSettings(IgnoreApi = true)]
 454    public ActionResult<IEnumerable<BaseItemDto>> GetLocalTrailersLegacy(
 455        [FromRoute, Required] Guid userId,
 456        [FromRoute, Required] Guid itemId)
 457        => GetLocalTrailers(userId, itemId);
 458
 459    /// <summary>
 460    /// Gets special features for an item.
 461    /// </summary>
 462    /// <param name="userId">User id.</param>
 463    /// <param name="itemId">Item id.</param>
 464    /// <response code="200">Special features returned.</response>
 465    /// <returns>An <see cref="OkResult"/> containing the special features.</returns>
 466    [HttpGet("Items/{itemId}/SpecialFeatures")]
 467    [ProducesResponseType(StatusCodes.Status200OK)]
 468    public ActionResult<IEnumerable<BaseItemDto>> GetSpecialFeatures(
 469        [FromQuery] Guid? userId,
 470        [FromRoute, Required] Guid itemId)
 471    {
 2472        userId = RequestHelpers.GetUserId(User, userId);
 2473        var user = _userManager.GetUserById(userId.Value);
 2474        if (user is null)
 475        {
 1476            return NotFound();
 477        }
 478
 1479        var item = itemId.IsEmpty()
 1480            ? _libraryManager.GetUserRootFolder()
 1481            : _libraryManager.GetItemById<BaseItem>(itemId, user);
 1482        if (item is null)
 483        {
 1484            return NotFound();
 485        }
 486
 0487        var dtoOptions = new DtoOptions();
 488
 0489        return Ok(item
 0490            .GetExtras()
 0491            .Where(i => i.ExtraType.HasValue && BaseItem.DisplayExtraTypes.Contains(i.ExtraType.Value))
 0492            .Select(i => _dtoService.GetBaseItemDto(i, dtoOptions, user, item)));
 493    }
 494
 495    /// <summary>
 496    /// Gets special features for an item.
 497    /// </summary>
 498    /// <param name="userId">User id.</param>
 499    /// <param name="itemId">Item id.</param>
 500    /// <response code="200">Special features returned.</response>
 501    /// <returns>An <see cref="OkResult"/> containing the special features.</returns>
 502    [HttpGet("Users/{userId}/Items/{itemId}/SpecialFeatures")]
 503    [ProducesResponseType(StatusCodes.Status200OK)]
 504    [Obsolete("Kept for backwards compatibility")]
 505    [ApiExplorerSettings(IgnoreApi = true)]
 506    public ActionResult<IEnumerable<BaseItemDto>> GetSpecialFeaturesLegacy(
 507        [FromRoute, Required] Guid userId,
 508        [FromRoute, Required] Guid itemId)
 509        => GetSpecialFeatures(userId, itemId);
 510
 511    /// <summary>
 512    /// Gets latest media.
 513    /// </summary>
 514    /// <param name="userId">User id.</param>
 515    /// <param name="parentId">Specify this to localize the search to a specific item or folder. Omit to use the root.</
 516    /// <param name="fields">Optional. Specify additional fields of information to return in the output.</param>
 517    /// <param name="includeItemTypes">Optional. If specified, results will be filtered based on item type. This allows 
 518    /// <param name="isPlayed">Filter by items that are played, or not.</param>
 519    /// <param name="enableImages">Optional. include image information in output.</param>
 520    /// <param name="imageTypeLimit">Optional. the max number of images to return, per image type.</param>
 521    /// <param name="enableImageTypes">Optional. The image types to include in the output.</param>
 522    /// <param name="enableUserData">Optional. include user data.</param>
 523    /// <param name="limit">Return item limit.</param>
 524    /// <param name="groupItems">Whether or not to group items into a parent container.</param>
 525    /// <response code="200">Latest media returned.</response>
 526    /// <returns>An <see cref="OkResult"/> containing the latest media.</returns>
 527    [HttpGet("Items/Latest")]
 528    [ProducesResponseType(StatusCodes.Status200OK)]
 529    public ActionResult<IEnumerable<BaseItemDto>> GetLatestMedia(
 530        [FromQuery] Guid? userId,
 531        [FromQuery] Guid? parentId,
 532        [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] ItemFields[] fields,
 533        [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] BaseItemKind[] includeItemTypes,
 534        [FromQuery] bool? isPlayed,
 535        [FromQuery] bool? enableImages,
 536        [FromQuery] int? imageTypeLimit,
 537        [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] ImageType[] enableImageTypes,
 538        [FromQuery] bool? enableUserData,
 539        [FromQuery] int limit = 20,
 540        [FromQuery] bool groupItems = true)
 541    {
 0542        var requestUserId = RequestHelpers.GetUserId(User, userId);
 0543        var user = _userManager.GetUserById(requestUserId);
 0544        if (user is null)
 545        {
 0546            return NotFound();
 547        }
 548
 0549        if (!isPlayed.HasValue)
 550        {
 0551            if (user.HidePlayedInLatest)
 552            {
 0553                isPlayed = false;
 554            }
 555        }
 556
 0557        var dtoOptions = new DtoOptions { Fields = fields }
 0558            .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
 559
 0560        var list = _userViewManager.GetLatestItems(
 0561            new LatestItemsQuery
 0562            {
 0563                GroupItems = groupItems,
 0564                IncludeItemTypes = includeItemTypes,
 0565                IsPlayed = isPlayed,
 0566                Limit = limit,
 0567                ParentId = parentId ?? Guid.Empty,
 0568                User = user,
 0569            },
 0570            dtoOptions);
 571
 0572        var resolvedItems = new BaseItem[list.Count];
 0573        var childCounts = new int[list.Count];
 0574        for (int i = 0; i < list.Count; i++)
 575        {
 0576            var tuple = list[i];
 0577            var item = tuple.Item2[0];
 0578            var childCount = 0;
 579
 0580            if (tuple.Item1 is not null && (tuple.Item2.Count > 1 || tuple.Item1 is MusicAlbum || tuple.Item1 is Series)
 581            {
 0582                item = tuple.Item1;
 0583                childCount = tuple.Item2.Count;
 584            }
 585
 0586            resolvedItems[i] = item;
 0587            childCounts[i] = childCount;
 588        }
 589
 590        // Fetch DTOs without visibility check since we've already done that in GetLatestItems and restore child counts 
 0591        var dtos = _dtoService.GetBaseItemDtos(resolvedItems, dtoOptions, user, skipVisibilityCheck: true);
 0592        for (int i = 0; i < dtos.Count; i++)
 593        {
 0594            if (childCounts[i] > 0)
 595            {
 0596                dtos[i].ChildCount = childCounts[i];
 597            }
 598        }
 599
 0600        return Ok(dtos.AsEnumerable());
 601    }
 602
 603    /// <summary>
 604    /// Gets latest media.
 605    /// </summary>
 606    /// <param name="userId">User id.</param>
 607    /// <param name="parentId">Specify this to localize the search to a specific item or folder. Omit to use the root.</
 608    /// <param name="fields">Optional. Specify additional fields of information to return in the output.</param>
 609    /// <param name="includeItemTypes">Optional. If specified, results will be filtered based on item type. This allows 
 610    /// <param name="isPlayed">Filter by items that are played, or not.</param>
 611    /// <param name="enableImages">Optional. include image information in output.</param>
 612    /// <param name="imageTypeLimit">Optional. the max number of images to return, per image type.</param>
 613    /// <param name="enableImageTypes">Optional. The image types to include in the output.</param>
 614    /// <param name="enableUserData">Optional. include user data.</param>
 615    /// <param name="limit">Return item limit.</param>
 616    /// <param name="groupItems">Whether or not to group items into a parent container.</param>
 617    /// <response code="200">Latest media returned.</response>
 618    /// <returns>An <see cref="OkResult"/> containing the latest media.</returns>
 619    [HttpGet("Users/{userId}/Items/Latest")]
 620    [ProducesResponseType(StatusCodes.Status200OK)]
 621    [Obsolete("Kept for backwards compatibility")]
 622    [ApiExplorerSettings(IgnoreApi = true)]
 623    public ActionResult<IEnumerable<BaseItemDto>> GetLatestMediaLegacy(
 624        [FromRoute, Required] Guid userId,
 625        [FromQuery] Guid? parentId,
 626        [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] ItemFields[] fields,
 627        [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] BaseItemKind[] includeItemTypes,
 628        [FromQuery] bool? isPlayed,
 629        [FromQuery] bool? enableImages,
 630        [FromQuery] int? imageTypeLimit,
 631        [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] ImageType[] enableImageTypes,
 632        [FromQuery] bool? enableUserData,
 633        [FromQuery] int limit = 20,
 634        [FromQuery] bool groupItems = true)
 635        => GetLatestMedia(
 636            userId,
 637            parentId,
 638            fields,
 639            includeItemTypes,
 640            isPlayed,
 641            enableImages,
 642            imageTypeLimit,
 643            enableImageTypes,
 644            enableUserData,
 645            limit,
 646            groupItems);
 647
 648    private async Task RefreshItemOnDemandIfNeeded(BaseItem item)
 649    {
 0650        if (item is Person)
 651        {
 0652            var hasMetadata = !string.IsNullOrWhiteSpace(item.Overview) && item.HasImage(ImageType.Primary);
 0653            var performFullRefresh = !hasMetadata && (DateTime.UtcNow - item.DateLastRefreshed).TotalDays >= 3;
 654
 0655            if (performFullRefresh)
 656            {
 0657                var options = new MetadataRefreshOptions(new DirectoryService(_fileSystem))
 0658                {
 0659                    MetadataRefreshMode = MetadataRefreshMode.FullRefresh,
 0660                    ImageRefreshMode = MetadataRefreshMode.FullRefresh,
 0661                    ForceSave = true
 0662                };
 663
 0664                await item.RefreshMetadata(options, CancellationToken.None).ConfigureAwait(false);
 665            }
 666        }
 0667    }
 668
 669    /// <summary>
 670    /// Marks the favorite.
 671    /// </summary>
 672    /// <param name="user">The user.</param>
 673    /// <param name="item">The item.</param>
 674    /// <param name="isFavorite">if set to <c>true</c> [is favorite].</param>
 675    private UserItemDataDto MarkFavorite(User user, BaseItem item, bool isFavorite)
 676    {
 677        // Get the user data for this item
 0678        var data = _userDataRepository.GetUserData(user, item);
 679
 0680        if (data is not null)
 681        {
 682            // Set favorite status
 0683            data.IsFavorite = isFavorite;
 684
 0685            _userDataRepository.SaveUserData(user, item, data, UserDataSaveReason.UpdateUserRating, CancellationToken.No
 686        }
 687
 0688        return _userDataRepository.GetUserDataDto(item, user)!;
 689    }
 690
 691    /// <summary>
 692    /// Updates the user item rating.
 693    /// </summary>
 694    /// <param name="user">The user.</param>
 695    /// <param name="item">The item.</param>
 696    /// <param name="likes">if set to <c>true</c> [likes].</param>
 697    private UserItemDataDto? UpdateUserItemRatingInternal(User user, BaseItem item, bool? likes)
 698    {
 699        // Get the user data for this item
 0700        var data = _userDataRepository.GetUserData(user, item);
 701
 0702        if (data is not null)
 703        {
 0704            data.Likes = likes;
 705
 0706            _userDataRepository.SaveUserData(user, item, data, UserDataSaveReason.UpdateUserRating, CancellationToken.No
 707        }
 708
 0709        return _userDataRepository.GetUserDataDto(item, user);
 710    }
 711}