using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using System.Linq; using System.Threading; using System.Threading.Tasks; using Jellyfin.Api.Constants; using Jellyfin.Api.Extensions; using Jellyfin.Api.Helpers; using Jellyfin.Api.ModelBinders; using MediaBrowser.Common.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 { /// /// User library controller. /// [Route("")] [Authorize(Policy = Policies.DefaultAuthorization)] 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; /// /// Initializes a new instance of the class. /// /// Instance of the interface. /// Instance of the interface. /// Instance of the interface. /// Instance of the interface. /// Instance of the interface. /// Instance of the interface. 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; } /// /// Gets an item from a user's library. /// /// User id. /// Item id. /// Item returned. /// An containing the d item. [HttpGet("Users/{userId}/Items/{itemId}")] [ProducesResponseType(StatusCodes.Status200OK)] public async Task> GetItem([FromRoute, Required] Guid userId, [FromRoute, Required] Guid itemId) { var user = _userManager.GetUserById(userId); var item = itemId.Equals(Guid.Empty) ? _libraryManager.GetUserRootFolder() : _libraryManager.GetItemById(itemId); await RefreshItemOnDemandIfNeeded(item).ConfigureAwait(false); var dtoOptions = new DtoOptions().AddClientFields(Request); return _dtoService.GetBaseItemDto(item, dtoOptions, user); } /// /// Gets the root folder from a user's library. /// /// User id. /// Root folder returned. /// An containing the user's root folder. [HttpGet("Users/{userId}/Items/Root")] [ProducesResponseType(StatusCodes.Status200OK)] public ActionResult GetRootFolder([FromRoute, Required] Guid userId) { var user = _userManager.GetUserById(userId); var item = _libraryManager.GetUserRootFolder(); var dtoOptions = new DtoOptions().AddClientFields(Request); return _dtoService.GetBaseItemDto(item, dtoOptions, user); } /// /// Gets intros to play before the main media item plays. /// /// User id. /// Item id. /// Intros returned. /// An containing the intros to play. [HttpGet("Users/{userId}/Items/{itemId}/Intros")] [ProducesResponseType(StatusCodes.Status200OK)] public async Task>> GetIntros([FromRoute, Required] Guid userId, [FromRoute, Required] Guid itemId) { var user = _userManager.GetUserById(userId); var item = itemId.Equals(Guid.Empty) ? _libraryManager.GetUserRootFolder() : _libraryManager.GetItemById(itemId); var items = await _libraryManager.GetIntros(item, user).ConfigureAwait(false); var dtoOptions = new DtoOptions().AddClientFields(Request); var dtos = items.Select(i => _dtoService.GetBaseItemDto(i, dtoOptions, user)).ToArray(); return new QueryResult { Items = dtos, TotalRecordCount = dtos.Length }; } /// /// Marks an item as a favorite. /// /// User id. /// Item id. /// Item marked as favorite. /// An containing the . [HttpPost("Users/{userId}/FavoriteItems/{itemId}")] [ProducesResponseType(StatusCodes.Status200OK)] public ActionResult MarkFavoriteItem([FromRoute, Required] Guid userId, [FromRoute, Required] Guid itemId) { return MarkFavorite(userId, itemId, true); } /// /// Unmarks item as a favorite. /// /// User id. /// Item id. /// Item unmarked as favorite. /// An containing the . [HttpDelete("Users/{userId}/FavoriteItems/{itemId}")] [ProducesResponseType(StatusCodes.Status200OK)] public ActionResult UnmarkFavoriteItem([FromRoute, Required] Guid userId, [FromRoute, Required] Guid itemId) { return MarkFavorite(userId, itemId, false); } /// /// Deletes a user's saved personal rating for an item. /// /// User id. /// Item id. /// Personal rating removed. /// An containing the . [HttpDelete("Users/{userId}/Items/{itemId}/Rating")] [ProducesResponseType(StatusCodes.Status200OK)] public ActionResult DeleteUserItemRating([FromRoute, Required] Guid userId, [FromRoute, Required] Guid itemId) { return UpdateUserItemRatingInternal(userId, itemId, null); } /// /// Updates a user's rating for an item. /// /// User id. /// Item id. /// Whether this is likes. /// Item rating updated. /// An containing the . [HttpPost("Users/{userId}/Items/{itemId}/Rating")] [ProducesResponseType(StatusCodes.Status200OK)] public ActionResult UpdateUserItemRating([FromRoute, Required] Guid userId, [FromRoute, Required] Guid itemId, [FromQuery] bool? likes) { return UpdateUserItemRatingInternal(userId, itemId, likes); } /// /// Gets local trailers for an item. /// /// User id. /// Item id. /// An containing the item's local trailers. /// The items local trailers. [HttpGet("Users/{userId}/Items/{itemId}/LocalTrailers")] [ProducesResponseType(StatusCodes.Status200OK)] public ActionResult> GetLocalTrailers([FromRoute, Required] Guid userId, [FromRoute, Required] Guid itemId) { var user = _userManager.GetUserById(userId); var item = itemId.Equals(Guid.Empty) ? _libraryManager.GetUserRootFolder() : _libraryManager.GetItemById(itemId); var dtoOptions = new DtoOptions().AddClientFields(Request); var dtosExtras = item.GetExtras(new[] { ExtraType.Trailer }) .Select(i => _dtoService.GetBaseItemDto(i, dtoOptions, user, item)) .ToArray(); if (item is IHasTrailers hasTrailers) { var trailers = hasTrailers.GetTrailers(); var dtosTrailers = _dtoService.GetBaseItemDtos(trailers, dtoOptions, user, item); var allTrailers = new BaseItemDto[dtosExtras.Length + dtosTrailers.Count]; dtosExtras.CopyTo(allTrailers, 0); dtosTrailers.CopyTo(allTrailers, dtosExtras.Length); return allTrailers; } return dtosExtras; } /// /// Gets special features for an item. /// /// User id. /// Item id. /// Special features returned. /// An containing the special features. [HttpGet("Users/{userId}/Items/{itemId}/SpecialFeatures")] [ProducesResponseType(StatusCodes.Status200OK)] public ActionResult> GetSpecialFeatures([FromRoute, Required] Guid userId, [FromRoute, Required] Guid itemId) { var user = _userManager.GetUserById(userId); var item = itemId.Equals(Guid.Empty) ? _libraryManager.GetUserRootFolder() : _libraryManager.GetItemById(itemId); var dtoOptions = new DtoOptions().AddClientFields(Request); return Ok(item .GetExtras(BaseItem.DisplayExtraTypes) .Select(i => _dtoService.GetBaseItemDto(i, dtoOptions, user, item))); } /// /// Gets latest media. /// /// User id. /// Specify this to localize the search to a specific item or folder. Omit to use the root. /// Optional. Specify additional fields of information to return in the output. /// Optional. If specified, results will be filtered based on item type. This allows multiple, comma delimited. /// Filter by items that are played, or not. /// Optional. include image information in output. /// Optional. the max number of images to return, per image type. /// Optional. The image types to include in the output. /// Optional. include user data. /// Return item limit. /// Whether or not to group items into a parent container. /// Latest media returned. /// An containing the latest media. [HttpGet("Users/{userId}/Items/Latest")] [ProducesResponseType(StatusCodes.Status200OK)] public ActionResult> GetLatestMedia( [FromRoute, Required] Guid userId, [FromQuery] Guid? parentId, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields, [FromQuery] string? includeItemTypes, [FromQuery] bool? isPlayed, [FromQuery] bool? enableImages, [FromQuery] int? imageTypeLimit, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes, [FromQuery] bool? enableUserData, [FromQuery] int limit = 20, [FromQuery] bool groupItems = true) { var user = _userManager.GetUserById(userId); if (!isPlayed.HasValue) { if (user.HidePlayedInLatest) { isPlayed = false; } } var dtoOptions = new DtoOptions { Fields = fields } .AddClientFields(Request) .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes); var list = _userViewManager.GetLatestItems( new LatestItemsQuery { GroupItems = groupItems, IncludeItemTypes = RequestHelpers.Split(includeItemTypes, ',', true), IsPlayed = isPlayed, Limit = limit, ParentId = parentId ?? Guid.Empty, UserId = userId, }, dtoOptions); var dtos = list.Select(i => { var item = i.Item2[0]; var childCount = 0; if (i.Item1 != 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); } private async Task RefreshItemOnDemandIfNeeded(BaseItem item) { if (item is Person) { var hasMetdata = !string.IsNullOrWhiteSpace(item.Overview) && item.HasImage(ImageType.Primary); var performFullRefresh = !hasMetdata && (DateTime.UtcNow - item.DateLastRefreshed).TotalDays >= 3; if (!hasMetdata) { var options = new MetadataRefreshOptions(new DirectoryService(_fileSystem)) { MetadataRefreshMode = MetadataRefreshMode.FullRefresh, ImageRefreshMode = MetadataRefreshMode.FullRefresh, ForceSave = performFullRefresh }; await item.RefreshMetadata(options, CancellationToken.None).ConfigureAwait(false); } } } /// /// Marks the favorite. /// /// The user id. /// The item id. /// if set to true [is favorite]. private UserItemDataDto MarkFavorite(Guid userId, Guid itemId, bool isFavorite) { var user = _userManager.GetUserById(userId); var item = itemId.Equals(Guid.Empty) ? _libraryManager.GetUserRootFolder() : _libraryManager.GetItemById(itemId); // Get the user data for this item var data = _userDataRepository.GetUserData(user, item); // Set favorite status data.IsFavorite = isFavorite; _userDataRepository.SaveUserData(user, item, data, UserDataSaveReason.UpdateUserRating, CancellationToken.None); return _userDataRepository.GetUserDataDto(item, user); } /// /// Updates the user item rating. /// /// The user id. /// The item id. /// if set to true [likes]. private UserItemDataDto UpdateUserItemRatingInternal(Guid userId, Guid itemId, bool? likes) { var user = _userManager.GetUserById(userId); var item = itemId.Equals(Guid.Empty) ? _libraryManager.GetUserRootFolder() : _libraryManager.GetItemById(itemId); // Get the user data for this item var data = _userDataRepository.GetUserData(user, item); data.Likes = likes; _userDataRepository.SaveUserData(user, item, data, UserDataSaveReason.UpdateUserRating, CancellationToken.None); return _userDataRepository.GetUserDataDto(item, user); } } }