using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using System.IO; 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 Jellyfin.Api.Models.UserDtos; using Jellyfin.Data.Enums; using Jellyfin.Extensions; using Kfstorm.LrcParser; 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(default) ? _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(default) ? _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(dtos); } /// /// 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(default) ? _libraryManager.GetUserRootFolder() : _libraryManager.GetItemById(itemId); var dtoOptions = new DtoOptions().AddClientFields(Request); if (item is IHasTrailers hasTrailers) { var trailers = hasTrailers.LocalTrailers; return Ok(_dtoService.GetBaseItemDtos(trailers, dtoOptions, user, item)); } return Ok(item.GetExtras() .Where(e => e.ExtraType == ExtraType.Trailer) .Select(i => _dtoService.GetBaseItemDto(i, dtoOptions, user, item))); } /// /// 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(default) ? _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, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] BaseItemKind[] 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 = includeItemTypes, 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(default) ? _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(default) ? _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); } /// /// Gets an item's lyrics. /// /// User id. /// Item id. /// Lyrics returned. /// Something went wrong. No Lyrics will be returned. /// An containing the intros to play. [HttpGet("Users/{userId}/Items/{itemId}/Lyrics")] [ProducesResponseType(StatusCodes.Status200OK)] public ActionResult> GetLyrics([FromRoute, Required] Guid userId, [FromRoute, Required] Guid itemId) { var user = _userManager.GetUserById(userId); if (user == null) { List lyricsList = new List { new Lyrics { Error = "User Not Found" } }; return NotFound(new { Results = lyricsList.ToArray() }); } var item = itemId.Equals(default) ? _libraryManager.GetUserRootFolder() : _libraryManager.GetItemById(itemId); if (item == null) { List lyricsList = new List { new Lyrics { Error = "Requested Item Not Found" } }; return NotFound(new { Results = lyricsList.ToArray() }); } List result = ItemHelper.GetLyricData(item); if (string.IsNullOrEmpty(result.ElementAt(0).Error)) { return Ok(new { Results = result }); } return NotFound(new { Results = result.ToArray() }); } } }