| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696 | using System;using System.Collections.Generic;using System.ComponentModel.DataAnnotations;using System.Linq;using System.Threading;using System.Threading.Tasks;using Jellyfin.Api.Extensions;using Jellyfin.Api.Helpers;using Jellyfin.Api.ModelBinders;using Jellyfin.Data.Enums;using Jellyfin.Database.Implementations.Entities;using Jellyfin.Extensions;using MediaBrowser.Controller.Dto;using MediaBrowser.Controller.Entities;using MediaBrowser.Controller.Entities.Audio;using MediaBrowser.Controller.Library;using MediaBrowser.Controller.Providers;using MediaBrowser.Model.Dto;using MediaBrowser.Model.Entities;using MediaBrowser.Model.IO;using MediaBrowser.Model.Querying;using Microsoft.AspNetCore.Authorization;using Microsoft.AspNetCore.Http;using Microsoft.AspNetCore.Mvc;namespace Jellyfin.Api.Controllers;/// <summary>/// User library controller./// </summary>[Route("")][Authorize]public class UserLibraryController : BaseJellyfinApiController{    private readonly IUserManager _userManager;    private readonly IUserDataManager _userDataRepository;    private readonly ILibraryManager _libraryManager;    private readonly IDtoService _dtoService;    private readonly IUserViewManager _userViewManager;    private readonly IFileSystem _fileSystem;    /// <summary>    /// Initializes a new instance of the <see cref="UserLibraryController"/> class.    /// </summary>    /// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param>    /// <param name="userDataRepository">Instance of the <see cref="IUserDataManager"/> interface.</param>    /// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>    /// <param name="dtoService">Instance of the <see cref="IDtoService"/> interface.</param>    /// <param name="userViewManager">Instance of the <see cref="IUserViewManager"/> interface.</param>    /// <param name="fileSystem">Instance of the <see cref="IFileSystem"/> interface.</param>    public UserLibraryController(        IUserManager userManager,        IUserDataManager userDataRepository,        ILibraryManager libraryManager,        IDtoService dtoService,        IUserViewManager userViewManager,        IFileSystem fileSystem)    {        _userManager = userManager;        _userDataRepository = userDataRepository;        _libraryManager = libraryManager;        _dtoService = dtoService;        _userViewManager = userViewManager;        _fileSystem = fileSystem;    }    /// <summary>    /// Gets an item from a user's library.    /// </summary>    /// <param name="userId">User id.</param>    /// <param name="itemId">Item id.</param>    /// <response code="200">Item returned.</response>    /// <returns>An <see cref="OkResult"/> containing the item.</returns>    [HttpGet("Items/{itemId}")]    [ProducesResponseType(StatusCodes.Status200OK)]    public async Task<ActionResult<BaseItemDto>> GetItem(        [FromQuery] Guid? userId,        [FromRoute, Required] Guid itemId)    {        userId = RequestHelpers.GetUserId(User, userId);        var user = _userManager.GetUserById(userId.Value);        if (user is null)        {            return NotFound();        }        var item = itemId.IsEmpty()            ? _libraryManager.GetUserRootFolder()            : _libraryManager.GetItemById<BaseItem>(itemId, user);        if (item is null)        {            return NotFound();        }        await RefreshItemOnDemandIfNeeded(item).ConfigureAwait(false);        var dtoOptions = new DtoOptions().AddClientFields(User);        return _dtoService.GetBaseItemDto(item, dtoOptions, user);    }    /// <summary>    /// Gets an item from a user's library.    /// </summary>    /// <param name="userId">User id.</param>    /// <param name="itemId">Item id.</param>    /// <response code="200">Item returned.</response>    /// <returns>An <see cref="OkResult"/> containing the item.</returns>    [HttpGet("Users/{userId}/Items/{itemId}")]    [ProducesResponseType(StatusCodes.Status200OK)]    [Obsolete("Kept for backwards compatibility")]    [ApiExplorerSettings(IgnoreApi = true)]    public Task<ActionResult<BaseItemDto>> GetItemLegacy(        [FromRoute, Required] Guid userId,        [FromRoute, Required] Guid itemId)        => GetItem(userId, itemId);    /// <summary>    /// Gets the root folder from a user's library.    /// </summary>    /// <param name="userId">User id.</param>    /// <response code="200">Root folder returned.</response>    /// <returns>An <see cref="OkResult"/> containing the user's root folder.</returns>    [HttpGet("Items/Root")]    [ProducesResponseType(StatusCodes.Status200OK)]    public ActionResult<BaseItemDto> GetRootFolder([FromQuery] Guid? userId)    {        userId = RequestHelpers.GetUserId(User, userId);        var user = _userManager.GetUserById(userId.Value);        if (user is null)        {            return NotFound();        }        var item = _libraryManager.GetUserRootFolder();        var dtoOptions = new DtoOptions().AddClientFields(User);        return _dtoService.GetBaseItemDto(item, dtoOptions, user);    }    /// <summary>    /// Gets the root folder from a user's library.    /// </summary>    /// <param name="userId">User id.</param>    /// <response code="200">Root folder returned.</response>    /// <returns>An <see cref="OkResult"/> containing the user's root folder.</returns>    [HttpGet("Users/{userId}/Items/Root")]    [ProducesResponseType(StatusCodes.Status200OK)]    [Obsolete("Kept for backwards compatibility")]    [ApiExplorerSettings(IgnoreApi = true)]    public ActionResult<BaseItemDto> GetRootFolderLegacy(        [FromRoute, Required] Guid userId)        => GetRootFolder(userId);    /// <summary>    /// Gets intros to play before the main media item plays.    /// </summary>    /// <param name="userId">User id.</param>    /// <param name="itemId">Item id.</param>    /// <response code="200">Intros returned.</response>    /// <returns>An <see cref="OkResult"/> containing the intros to play.</returns>    [HttpGet("Items/{itemId}/Intros")]    [ProducesResponseType(StatusCodes.Status200OK)]    public async Task<ActionResult<QueryResult<BaseItemDto>>> GetIntros(        [FromQuery] Guid? userId,        [FromRoute, Required] Guid itemId)    {        userId = RequestHelpers.GetUserId(User, userId);        var user = _userManager.GetUserById(userId.Value);        if (user is null)        {            return NotFound();        }        var item = itemId.IsEmpty()            ? _libraryManager.GetUserRootFolder()            : _libraryManager.GetItemById<BaseItem>(itemId, user);        if (item is null)        {            return NotFound();        }        var items = await _libraryManager.GetIntros(item, user).ConfigureAwait(false);        var dtoOptions = new DtoOptions().AddClientFields(User);        var dtos = items.Select(i => _dtoService.GetBaseItemDto(i, dtoOptions, user)).ToArray();        return new QueryResult<BaseItemDto>(dtos);    }    /// <summary>    /// Gets intros to play before the main media item plays.    /// </summary>    /// <param name="userId">User id.</param>    /// <param name="itemId">Item id.</param>    /// <response code="200">Intros returned.</response>    /// <returns>An <see cref="OkResult"/> containing the intros to play.</returns>    [HttpGet("Users/{userId}/Items/{itemId}/Intros")]    [ProducesResponseType(StatusCodes.Status200OK)]    [Obsolete("Kept for backwards compatibility")]    [ApiExplorerSettings(IgnoreApi = true)]    public Task<ActionResult<QueryResult<BaseItemDto>>> GetIntrosLegacy(        [FromRoute, Required] Guid userId,        [FromRoute, Required] Guid itemId)        => GetIntros(userId, itemId);    /// <summary>    /// Marks an item as a favorite.    /// </summary>    /// <param name="userId">User id.</param>    /// <param name="itemId">Item id.</param>    /// <response code="200">Item marked as favorite.</response>    /// <returns>An <see cref="OkResult"/> containing the <see cref="UserItemDataDto"/>.</returns>    [HttpPost("UserFavoriteItems/{itemId}")]    [ProducesResponseType(StatusCodes.Status200OK)]    public ActionResult<UserItemDataDto> MarkFavoriteItem(        [FromQuery] Guid? userId,        [FromRoute, Required] Guid itemId)    {        userId = RequestHelpers.GetUserId(User, userId);        var user = _userManager.GetUserById(userId.Value);        if (user is null)        {            return NotFound();        }        var item = itemId.IsEmpty()            ? _libraryManager.GetUserRootFolder()            : _libraryManager.GetItemById<BaseItem>(itemId, user);        if (item is null)        {            return NotFound();        }        return MarkFavorite(user, item, true);    }    /// <summary>    /// Marks an item as a favorite.    /// </summary>    /// <param name="userId">User id.</param>    /// <param name="itemId">Item id.</param>    /// <response code="200">Item marked as favorite.</response>    /// <returns>An <see cref="OkResult"/> containing the <see cref="UserItemDataDto"/>.</returns>    [HttpPost("Users/{userId}/FavoriteItems/{itemId}")]    [ProducesResponseType(StatusCodes.Status200OK)]    [Obsolete("Kept for backwards compatibility")]    [ApiExplorerSettings(IgnoreApi = true)]    public ActionResult<UserItemDataDto> MarkFavoriteItemLegacy(        [FromRoute, Required] Guid userId,        [FromRoute, Required] Guid itemId)        => MarkFavoriteItem(userId, itemId);    /// <summary>    /// Unmarks item as a favorite.    /// </summary>    /// <param name="userId">User id.</param>    /// <param name="itemId">Item id.</param>    /// <response code="200">Item unmarked as favorite.</response>    /// <returns>An <see cref="OkResult"/> containing the <see cref="UserItemDataDto"/>.</returns>    [HttpDelete("UserFavoriteItems/{itemId}")]    [ProducesResponseType(StatusCodes.Status200OK)]    public ActionResult<UserItemDataDto> UnmarkFavoriteItem(        [FromQuery] Guid? userId,        [FromRoute, Required] Guid itemId)    {        userId = RequestHelpers.GetUserId(User, userId);        var user = _userManager.GetUserById(userId.Value);        if (user is null)        {            return NotFound();        }        var item = itemId.IsEmpty()            ? _libraryManager.GetUserRootFolder()            : _libraryManager.GetItemById<BaseItem>(itemId, user);        if (item is null)        {            return NotFound();        }        return MarkFavorite(user, item, false);    }    /// <summary>    /// Unmarks item as a favorite.    /// </summary>    /// <param name="userId">User id.</param>    /// <param name="itemId">Item id.</param>    /// <response code="200">Item unmarked as favorite.</response>    /// <returns>An <see cref="OkResult"/> containing the <see cref="UserItemDataDto"/>.</returns>    [HttpDelete("Users/{userId}/FavoriteItems/{itemId}")]    [ProducesResponseType(StatusCodes.Status200OK)]    [Obsolete("Kept for backwards compatibility")]    [ApiExplorerSettings(IgnoreApi = true)]    public ActionResult<UserItemDataDto> UnmarkFavoriteItemLegacy(        [FromRoute, Required] Guid userId,        [FromRoute, Required] Guid itemId)        => UnmarkFavoriteItem(userId, itemId);    /// <summary>    /// Deletes a user's saved personal rating for an item.    /// </summary>    /// <param name="userId">User id.</param>    /// <param name="itemId">Item id.</param>    /// <response code="200">Personal rating removed.</response>    /// <returns>An <see cref="OkResult"/> containing the <see cref="UserItemDataDto"/>.</returns>    [HttpDelete("UserItems/{itemId}/Rating")]    [ProducesResponseType(StatusCodes.Status200OK)]    public ActionResult<UserItemDataDto?> DeleteUserItemRating(        [FromQuery] Guid? userId,        [FromRoute, Required] Guid itemId)    {        userId = RequestHelpers.GetUserId(User, userId);        var user = _userManager.GetUserById(userId.Value);        if (user is null)        {            return NotFound();        }        var item = itemId.IsEmpty()            ? _libraryManager.GetUserRootFolder()            : _libraryManager.GetItemById<BaseItem>(itemId, user);        if (item is null)        {            return NotFound();        }        return UpdateUserItemRatingInternal(user, item, null);    }    /// <summary>    /// Deletes a user's saved personal rating for an item.    /// </summary>    /// <param name="userId">User id.</param>    /// <param name="itemId">Item id.</param>    /// <response code="200">Personal rating removed.</response>    /// <returns>An <see cref="OkResult"/> containing the <see cref="UserItemDataDto"/>.</returns>    [HttpDelete("Users/{userId}/Items/{itemId}/Rating")]    [ProducesResponseType(StatusCodes.Status200OK)]    [Obsolete("Kept for backwards compatibility")]    [ApiExplorerSettings(IgnoreApi = true)]    public ActionResult<UserItemDataDto?> DeleteUserItemRatingLegacy(        [FromRoute, Required] Guid userId,        [FromRoute, Required] Guid itemId)        => DeleteUserItemRating(userId, itemId);    /// <summary>    /// Updates a user's rating for an item.    /// </summary>    /// <param name="userId">User id.</param>    /// <param name="itemId">Item id.</param>    /// <param name="likes">Whether this <see cref="UpdateUserItemRating" /> is likes.</param>    /// <response code="200">Item rating updated.</response>    /// <returns>An <see cref="OkResult"/> containing the <see cref="UserItemDataDto"/>.</returns>    [HttpPost("UserItems/{itemId}/Rating")]    [ProducesResponseType(StatusCodes.Status200OK)]    public ActionResult<UserItemDataDto?> UpdateUserItemRating(        [FromQuery] Guid? userId,        [FromRoute, Required] Guid itemId,        [FromQuery] bool? likes)    {        userId = RequestHelpers.GetUserId(User, userId);        var user = _userManager.GetUserById(userId.Value);        if (user is null)        {            return NotFound();        }        var item = itemId.IsEmpty()            ? _libraryManager.GetUserRootFolder()            : _libraryManager.GetItemById<BaseItem>(itemId, user);        if (item is null)        {            return NotFound();        }        return UpdateUserItemRatingInternal(user, item, likes);    }    /// <summary>    /// Updates a user's rating for an item.    /// </summary>    /// <param name="userId">User id.</param>    /// <param name="itemId">Item id.</param>    /// <param name="likes">Whether this <see cref="UpdateUserItemRating" /> is likes.</param>    /// <response code="200">Item rating updated.</response>    /// <returns>An <see cref="OkResult"/> containing the <see cref="UserItemDataDto"/>.</returns>    [HttpPost("Users/{userId}/Items/{itemId}/Rating")]    [ProducesResponseType(StatusCodes.Status200OK)]    [Obsolete("Kept for backwards compatibility")]    [ApiExplorerSettings(IgnoreApi = true)]    public ActionResult<UserItemDataDto?> UpdateUserItemRatingLegacy(        [FromRoute, Required] Guid userId,        [FromRoute, Required] Guid itemId,        [FromQuery] bool? likes)        => UpdateUserItemRating(userId, itemId, likes);    /// <summary>    /// Gets local trailers for an item.    /// </summary>    /// <param name="userId">User id.</param>    /// <param name="itemId">Item id.</param>    /// <response code="200">An <see cref="OkResult"/> containing the item's local trailers.</response>    /// <returns>The items local trailers.</returns>    [HttpGet("Items/{itemId}/LocalTrailers")]    [ProducesResponseType(StatusCodes.Status200OK)]    public ActionResult<IEnumerable<BaseItemDto>> GetLocalTrailers(        [FromQuery] Guid? userId,        [FromRoute, Required] Guid itemId)    {        userId = RequestHelpers.GetUserId(User, userId);        var user = _userManager.GetUserById(userId.Value);        if (user is null)        {            return NotFound();        }        var item = itemId.IsEmpty()            ? _libraryManager.GetUserRootFolder()            : _libraryManager.GetItemById<BaseItem>(itemId, user);        if (item is null)        {            return NotFound();        }        var dtoOptions = new DtoOptions().AddClientFields(User);        if (item is IHasTrailers hasTrailers)        {            var trailers = hasTrailers.LocalTrailers;            return Ok(_dtoService.GetBaseItemDtos(trailers, dtoOptions, user, item).AsEnumerable());        }        return Ok(item.GetExtras()            .Where(e => e.ExtraType == ExtraType.Trailer)            .Select(i => _dtoService.GetBaseItemDto(i, dtoOptions, user, item)));    }    /// <summary>    /// Gets local trailers for an item.    /// </summary>    /// <param name="userId">User id.</param>    /// <param name="itemId">Item id.</param>    /// <response code="200">An <see cref="OkResult"/> containing the item's local trailers.</response>    /// <returns>The items local trailers.</returns>    [HttpGet("Users/{userId}/Items/{itemId}/LocalTrailers")]    [ProducesResponseType(StatusCodes.Status200OK)]    [Obsolete("Kept for backwards compatibility")]    [ApiExplorerSettings(IgnoreApi = true)]    public ActionResult<IEnumerable<BaseItemDto>> GetLocalTrailersLegacy(        [FromRoute, Required] Guid userId,        [FromRoute, Required] Guid itemId)        => GetLocalTrailers(userId, itemId);    /// <summary>    /// Gets special features for an item.    /// </summary>    /// <param name="userId">User id.</param>    /// <param name="itemId">Item id.</param>    /// <response code="200">Special features returned.</response>    /// <returns>An <see cref="OkResult"/> containing the special features.</returns>    [HttpGet("Items/{itemId}/SpecialFeatures")]    [ProducesResponseType(StatusCodes.Status200OK)]    public ActionResult<IEnumerable<BaseItemDto>> GetSpecialFeatures(        [FromQuery] Guid? userId,        [FromRoute, Required] Guid itemId)    {        userId = RequestHelpers.GetUserId(User, userId);        var user = _userManager.GetUserById(userId.Value);        if (user is null)        {            return NotFound();        }        var item = itemId.IsEmpty()            ? _libraryManager.GetUserRootFolder()            : _libraryManager.GetItemById<BaseItem>(itemId, user);        if (item is null)        {            return NotFound();        }        var dtoOptions = new DtoOptions().AddClientFields(User);        return Ok(item            .GetExtras()            .Where(i => i.ExtraType.HasValue && BaseItem.DisplayExtraTypes.Contains(i.ExtraType.Value))            .Select(i => _dtoService.GetBaseItemDto(i, dtoOptions, user, item)));    }    /// <summary>    /// Gets special features for an item.    /// </summary>    /// <param name="userId">User id.</param>    /// <param name="itemId">Item id.</param>    /// <response code="200">Special features returned.</response>    /// <returns>An <see cref="OkResult"/> containing the special features.</returns>    [HttpGet("Users/{userId}/Items/{itemId}/SpecialFeatures")]    [ProducesResponseType(StatusCodes.Status200OK)]    [Obsolete("Kept for backwards compatibility")]    [ApiExplorerSettings(IgnoreApi = true)]    public ActionResult<IEnumerable<BaseItemDto>> GetSpecialFeaturesLegacy(        [FromRoute, Required] Guid userId,        [FromRoute, Required] Guid itemId)        => GetSpecialFeatures(userId, itemId);    /// <summary>    /// Gets latest media.    /// </summary>    /// <param name="userId">User id.</param>    /// <param name="parentId">Specify this to localize the search to a specific item or folder. Omit to use the root.</param>    /// <param name="fields">Optional. Specify additional fields of information to return in the output.</param>    /// <param name="includeItemTypes">Optional. If specified, results will be filtered based on item type. This allows multiple, comma delimited.</param>    /// <param name="isPlayed">Filter by items that are played, or not.</param>    /// <param name="enableImages">Optional. include image information in output.</param>    /// <param name="imageTypeLimit">Optional. the max number of images to return, per image type.</param>    /// <param name="enableImageTypes">Optional. The image types to include in the output.</param>    /// <param name="enableUserData">Optional. include user data.</param>    /// <param name="limit">Return item limit.</param>    /// <param name="groupItems">Whether or not to group items into a parent container.</param>    /// <response code="200">Latest media returned.</response>    /// <returns>An <see cref="OkResult"/> containing the latest media.</returns>    [HttpGet("Items/Latest")]    [ProducesResponseType(StatusCodes.Status200OK)]    public ActionResult<IEnumerable<BaseItemDto>> GetLatestMedia(        [FromQuery] Guid? userId,        [FromQuery] Guid? parentId,        [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] ItemFields[] fields,        [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] BaseItemKind[] includeItemTypes,        [FromQuery] bool? isPlayed,        [FromQuery] bool? enableImages,        [FromQuery] int? imageTypeLimit,        [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] ImageType[] enableImageTypes,        [FromQuery] bool? enableUserData,        [FromQuery] int limit = 20,        [FromQuery] bool groupItems = true)    {        var requestUserId = RequestHelpers.GetUserId(User, userId);        var user = _userManager.GetUserById(requestUserId);        if (user is null)        {            return NotFound();        }        if (!isPlayed.HasValue)        {            if (user.HidePlayedInLatest)            {                isPlayed = false;            }        }        var dtoOptions = new DtoOptions { Fields = fields }            .AddClientFields(User)            .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);        var list = _userViewManager.GetLatestItems(            new LatestItemsQuery            {                GroupItems = groupItems,                IncludeItemTypes = includeItemTypes,                IsPlayed = isPlayed,                Limit = limit,                ParentId = parentId ?? Guid.Empty,                User = user,            },            dtoOptions);        var dtos = list.Select(i =>        {            var item = i.Item2[0];            var childCount = 0;            if (i.Item1 is not null && (i.Item2.Count > 1 || i.Item1 is MusicAlbum))            {                item = i.Item1;                childCount = i.Item2.Count;            }            var dto = _dtoService.GetBaseItemDto(item, dtoOptions, user);            dto.ChildCount = childCount;            return dto;        });        return Ok(dtos);    }    /// <summary>    /// Gets latest media.    /// </summary>    /// <param name="userId">User id.</param>    /// <param name="parentId">Specify this to localize the search to a specific item or folder. Omit to use the root.</param>    /// <param name="fields">Optional. Specify additional fields of information to return in the output.</param>    /// <param name="includeItemTypes">Optional. If specified, results will be filtered based on item type. This allows multiple, comma delimited.</param>    /// <param name="isPlayed">Filter by items that are played, or not.</param>    /// <param name="enableImages">Optional. include image information in output.</param>    /// <param name="imageTypeLimit">Optional. the max number of images to return, per image type.</param>    /// <param name="enableImageTypes">Optional. The image types to include in the output.</param>    /// <param name="enableUserData">Optional. include user data.</param>    /// <param name="limit">Return item limit.</param>    /// <param name="groupItems">Whether or not to group items into a parent container.</param>    /// <response code="200">Latest media returned.</response>    /// <returns>An <see cref="OkResult"/> containing the latest media.</returns>    [HttpGet("Users/{userId}/Items/Latest")]    [ProducesResponseType(StatusCodes.Status200OK)]    [Obsolete("Kept for backwards compatibility")]    [ApiExplorerSettings(IgnoreApi = true)]    public ActionResult<IEnumerable<BaseItemDto>> GetLatestMediaLegacy(        [FromRoute, Required] Guid userId,        [FromQuery] Guid? parentId,        [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] ItemFields[] fields,        [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] BaseItemKind[] includeItemTypes,        [FromQuery] bool? isPlayed,        [FromQuery] bool? enableImages,        [FromQuery] int? imageTypeLimit,        [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] ImageType[] enableImageTypes,        [FromQuery] bool? enableUserData,        [FromQuery] int limit = 20,        [FromQuery] bool groupItems = true)        => GetLatestMedia(            userId,            parentId,            fields,            includeItemTypes,            isPlayed,            enableImages,            imageTypeLimit,            enableImageTypes,            enableUserData,            limit,            groupItems);    private async Task RefreshItemOnDemandIfNeeded(BaseItem item)    {        if (item is Person)        {            var hasMetadata = !string.IsNullOrWhiteSpace(item.Overview) && item.HasImage(ImageType.Primary);            var performFullRefresh = !hasMetadata && (DateTime.UtcNow - item.DateLastRefreshed).TotalDays >= 3;            if (!hasMetadata)            {                var options = new MetadataRefreshOptions(new DirectoryService(_fileSystem))                {                    MetadataRefreshMode = MetadataRefreshMode.FullRefresh,                    ImageRefreshMode = MetadataRefreshMode.FullRefresh,                    ForceSave = performFullRefresh                };                await item.RefreshMetadata(options, CancellationToken.None).ConfigureAwait(false);            }        }    }    /// <summary>    /// Marks the favorite.    /// </summary>    /// <param name="user">The user.</param>    /// <param name="item">The item.</param>    /// <param name="isFavorite">if set to <c>true</c> [is favorite].</param>    private UserItemDataDto MarkFavorite(User user, BaseItem item, bool isFavorite)    {        // Get the user data for this item        var data = _userDataRepository.GetUserData(user, item);        if (data is not null)        {            // Set favorite status            data.IsFavorite = isFavorite;            _userDataRepository.SaveUserData(user, item, data, UserDataSaveReason.UpdateUserRating, CancellationToken.None);        }        return _userDataRepository.GetUserDataDto(item, user)!;    }    /// <summary>    /// Updates the user item rating.    /// </summary>    /// <param name="user">The user.</param>    /// <param name="item">The item.</param>    /// <param name="likes">if set to <c>true</c> [likes].</param>    private UserItemDataDto? UpdateUserItemRatingInternal(User user, BaseItem item, bool? likes)    {        // Get the user data for this item        var data = _userDataRepository.GetUserData(user, item);        if (data is not null)        {            data.Likes = likes;            _userDataRepository.SaveUserData(user, item, data, UserDataSaveReason.UpdateUserRating, CancellationToken.None);        }        return _userDataRepository.GetUserDataDto(item, user);    }}
 |