using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using System.Linq; using Jellyfin.Api.Models.HomeSectionDto; using Jellyfin.Data.Enums; using Jellyfin.Database.Implementations.Enums; using MediaBrowser.Controller; using MediaBrowser.Controller.Dto; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities.Movies; using MediaBrowser.Controller.Entities.TV; using MediaBrowser.Controller.Library; using MediaBrowser.Model.Configuration; using MediaBrowser.Model.Dto; using MediaBrowser.Model.Entities; using MediaBrowser.Model.Querying; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; namespace Jellyfin.Api.Controllers { /// /// Home Section controller. /// [Route("Users/{userId}/HomeSections")] [Authorize] public class HomeSectionController : BaseJellyfinApiController { private readonly IHomeSectionManager _homeSectionManager; private readonly IUserManager _userManager; private readonly ILibraryManager _libraryManager; private readonly IDtoService _dtoService; /// /// Initializes a new instance of the class. /// /// Instance of the interface. /// Instance of the interface. /// Instance of the interface. /// Instance of the interface. public HomeSectionController( IHomeSectionManager homeSectionManager, IUserManager userManager, ILibraryManager libraryManager, IDtoService dtoService) { _homeSectionManager = homeSectionManager; _userManager = userManager; _libraryManager = libraryManager; _dtoService = dtoService; } /// /// Get all home sections. /// /// User id. /// Home sections retrieved. /// An containing the home sections. [HttpGet] [ProducesResponseType(StatusCodes.Status200OK)] public ActionResult> GetHomeSections([FromRoute, Required] Guid userId) { var sections = _homeSectionManager.GetHomeSections(userId); var result = new List(); var user = _userManager.GetUserById(userId); if (user == null) { return NotFound("User not found"); } foreach (var section in sections) { var enrichedSection = new EnrichedHomeSectionDto { Id = null, // We'll need to retrieve the ID from the database SectionOptions = section, Items = GetItemsForSection(userId, section) }; result.Add(enrichedSection); } return Ok(result); } /// /// Get home section. /// /// User id. /// Section id. /// Home section retrieved. /// Home section not found. /// An containing the home section. [HttpGet("{sectionId}")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] public ActionResult GetHomeSection([FromRoute, Required] Guid userId, [FromRoute, Required] Guid sectionId) { var section = _homeSectionManager.GetHomeSection(userId, sectionId); if (section == null) { return NotFound(); } var user = _userManager.GetUserById(userId); if (user == null) { return NotFound("User not found"); } var result = new EnrichedHomeSectionDto { Id = sectionId, SectionOptions = section, Items = GetItemsForSection(userId, section) }; return Ok(result); } /// /// Create a new home section. /// /// User id. /// The home section dto. /// Home section created. /// An containing the new home section. [HttpPost] [ProducesResponseType(StatusCodes.Status201Created)] public ActionResult CreateHomeSection([FromRoute, Required] Guid userId, [FromBody, Required] HomeSectionDto dto) { var sectionId = _homeSectionManager.CreateHomeSection(userId, dto.SectionOptions); _homeSectionManager.SaveChanges(); dto.Id = sectionId; return CreatedAtAction(nameof(GetHomeSection), new { userId, sectionId }, dto); } /// /// Update a home section. /// /// User id. /// Section id. /// The home section dto. /// Home section updated. /// Home section not found. /// A . [HttpPut("{sectionId}")] [ProducesResponseType(StatusCodes.Status204NoContent)] [ProducesResponseType(StatusCodes.Status404NotFound)] public ActionResult UpdateHomeSection( [FromRoute, Required] Guid userId, [FromRoute, Required] Guid sectionId, [FromBody, Required] HomeSectionDto dto) { var success = _homeSectionManager.UpdateHomeSection(userId, sectionId, dto.SectionOptions); if (!success) { return NotFound(); } _homeSectionManager.SaveChanges(); return NoContent(); } /// /// Delete a home section. /// /// User id. /// Section id. /// Home section deleted. /// Home section not found. /// A . [HttpDelete("{sectionId}")] [ProducesResponseType(StatusCodes.Status204NoContent)] [ProducesResponseType(StatusCodes.Status404NotFound)] public ActionResult DeleteHomeSection([FromRoute, Required] Guid userId, [FromRoute, Required] Guid sectionId) { var success = _homeSectionManager.DeleteHomeSection(userId, sectionId); if (!success) { return NotFound(); } _homeSectionManager.SaveChanges(); return NoContent(); } private IEnumerable GetItemsForSection(Guid userId, HomeSectionOptions options) { var user = _userManager.GetUserById(userId); if (user == null) { return Array.Empty(); } switch (options.SectionType) { case HomeSectionType.None: return Array.Empty(); case HomeSectionType.SmallLibraryTiles: return GetLibraryTilesHomeSectionItems(userId, true); case HomeSectionType.LibraryButtons: return GetLibraryTilesHomeSectionItems(userId, false); // TODO: Implement GetActiveRecordingsHomeSectionItems case HomeSectionType.ActiveRecordings: return Array.Empty(); // TODO: Implement GetResumeItemsHomeSectionItems case HomeSectionType.Resume: return Array.Empty(); // TODO: Implement GetResumeAudioHomeSectionItems case HomeSectionType.ResumeAudio: return Array.Empty(); case HomeSectionType.LatestMedia: return GetLatestMediaHomeSectionItems(userId, options.MaxItems); // TODO: Implement GetNextUpHomeSectionItems case HomeSectionType.NextUp: return Array.Empty(); // TODO: Implement GetLiveTvHomeSectionItems case HomeSectionType.LiveTv: return Array.Empty(); // TODO: Implement ResumeBookHomeSectionItems case HomeSectionType.ResumeBook: return Array.Empty(); // Major TODO: Implement GetPinnedCollectionHomeSectionItems and add HomeSectionType.PinnedCollection // See example at https://github.com/johnpc/jellyfin-plugin-home-sections/blob/main/Jellyfin.Plugin.HomeSections/Api/HomeSectionsController.cs // Question: what should I do in the case of an unexpected HomeSectionType? Throw an exception? default: return Array.Empty(); } } private IEnumerable GetLatestMediaHomeSectionItems(Guid userId, int maxItems) { var user = _userManager.GetUserById(userId); if (user == null) { return Array.Empty(); } var query = new InternalItemsQuery(user) { Recursive = true, Limit = maxItems, IsVirtualItem = false, OrderBy = new[] { (ItemSortBy.DateCreated, SortOrder.Descending) } }; var items = _libraryManager.GetItemsResult(query); return items.Items .Where(i => i != null && (i is Movie || i is Series || i is Episode)) .Select(i => { try { return _dtoService.GetBaseItemDto(i, new DtoOptions(), user); } catch (Exception ex) { // Log the error but don't crash System.Diagnostics.Debug.WriteLine($"Error converting item {i.Id} to DTO: {ex.Message}"); return null; } }) .Where(dto => dto != null) .Cast(); } private IEnumerable GetLibraryTilesHomeSectionItems(Guid userId, bool smallTiles = false) { var user = _userManager.GetUserById(userId); if (user == null) { return Array.Empty(); } // Get the user's view items (libraries) var folders = _libraryManager.GetUserRootFolder() .GetChildren(user, true) .Where(i => i.IsFolder && !i.IsHidden) .OrderBy(i => i.SortName) .ToList(); // Convert to DTOs with appropriate options var options = new DtoOptions { // For small tiles, we might want to limit the fields returned // to make the response smaller Fields = smallTiles ? new[] { ItemFields.PrimaryImageAspectRatio, ItemFields.DisplayPreferencesId } : new[] { ItemFields.PrimaryImageAspectRatio, ItemFields.DisplayPreferencesId, ItemFields.Overview, ItemFields.ChildCount } }; return folders .Select(i => { try { return _dtoService.GetBaseItemDto(i, options, user); } catch (Exception) { return null; } }) .Where(dto => dto != null) .Cast(); } } }