using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using System.IO; using System.Linq; using System.Net.Mime; using Jellyfin.Api.Attributes; using Jellyfin.Api.Constants; using MediaBrowser.Controller; using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Entities; using MediaBrowser.Model.Dto; using MediaBrowser.Model.IO; using MediaBrowser.Model.Net; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; namespace Jellyfin.Api.Controllers { /// /// Images By Name Controller. /// [Route("Images")] public class ImageByNameController : BaseJellyfinApiController { private readonly IServerApplicationPaths _applicationPaths; private readonly IFileSystem _fileSystem; /// /// Initializes a new instance of the class. /// /// Instance of the interface. /// Instance of the interface. public ImageByNameController( IServerConfigurationManager serverConfigurationManager, IFileSystem fileSystem) { _applicationPaths = serverConfigurationManager.ApplicationPaths; _fileSystem = fileSystem; } /// /// Get all general images. /// /// Retrieved list of images. /// An containing the list of images. [HttpGet("General")] [Authorize(Policy = Policies.DefaultAuthorization)] [ProducesResponseType(StatusCodes.Status200OK)] public ActionResult> GetGeneralImages() { return GetImageList(_applicationPaths.GeneralPath, false); } /// /// Get General Image. /// /// The name of the image. /// Image Type (primary, backdrop, logo, etc). /// Image stream retrieved. /// Image not found. /// A containing the image contents on success, or a if the image could not be found. [HttpGet("General/{name}/{type}")] [AllowAnonymous] [Produces(MediaTypeNames.Application.Octet)] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] [ProducesImageFile] public ActionResult GetGeneralImage([FromRoute, Required] string name, [FromRoute, Required] string type) { var filename = string.Equals(type, "primary", StringComparison.OrdinalIgnoreCase) ? "folder" : type; var path = BaseItem.SupportedImageExtensions .Select(i => Path.GetFullPath(Path.Combine(_applicationPaths.GeneralPath, name, filename + i))) .FirstOrDefault(System.IO.File.Exists); if (path is null) { return NotFound(); } if (!path.StartsWith(_applicationPaths.GeneralPath, StringComparison.InvariantCulture)) { return BadRequest("Invalid image path."); } var contentType = MimeTypes.GetMimeType(path); return File(AsyncFile.OpenRead(path), contentType); } /// /// Get all general images. /// /// Retrieved list of images. /// An containing the list of images. [HttpGet("Ratings")] [Authorize(Policy = Policies.DefaultAuthorization)] [ProducesResponseType(StatusCodes.Status200OK)] public ActionResult> GetRatingImages() { return GetImageList(_applicationPaths.RatingsPath, false); } /// /// Get rating image. /// /// The theme to get the image from. /// The name of the image. /// Image stream retrieved. /// Image not found. /// A containing the image contents on success, or a if the image could not be found. [HttpGet("Ratings/{theme}/{name}")] [AllowAnonymous] [Produces(MediaTypeNames.Application.Octet)] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] [ProducesImageFile] public ActionResult GetRatingImage( [FromRoute, Required] string theme, [FromRoute, Required] string name) { return GetImageFile(_applicationPaths.RatingsPath, theme, name); } /// /// Get all media info images. /// /// Image list retrieved. /// An containing the list of images. [HttpGet("MediaInfo")] [Authorize(Policy = Policies.DefaultAuthorization)] [ProducesResponseType(StatusCodes.Status200OK)] public ActionResult> GetMediaInfoImages() { return GetImageList(_applicationPaths.MediaInfoImagesPath, false); } /// /// Get media info image. /// /// The theme to get the image from. /// The name of the image. /// Image stream retrieved. /// Image not found. /// A containing the image contents on success, or a if the image could not be found. [HttpGet("MediaInfo/{theme}/{name}")] [AllowAnonymous] [Produces(MediaTypeNames.Application.Octet)] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] [ProducesImageFile] public ActionResult GetMediaInfoImage( [FromRoute, Required] string theme, [FromRoute, Required] string name) { return GetImageFile(_applicationPaths.MediaInfoImagesPath, theme, name); } /// /// Internal FileHelper. /// /// Path to begin search. /// Theme to search. /// File name to search for. /// A containing the image contents on success, or a if the image could not be found. private ActionResult GetImageFile(string basePath, string theme, string? name) { var themeFolder = Path.GetFullPath(Path.Combine(basePath, theme)); if (Directory.Exists(themeFolder)) { var path = BaseItem.SupportedImageExtensions.Select(i => Path.Combine(themeFolder, name + i)) .FirstOrDefault(System.IO.File.Exists); if (!string.IsNullOrEmpty(path) && System.IO.File.Exists(path)) { if (!path.StartsWith(basePath, StringComparison.InvariantCulture)) { return BadRequest("Invalid image path."); } var contentType = MimeTypes.GetMimeType(path); return PhysicalFile(path, contentType); } } var allFolder = Path.GetFullPath(Path.Combine(basePath, "all")); if (Directory.Exists(allFolder)) { var path = BaseItem.SupportedImageExtensions.Select(i => Path.Combine(allFolder, name + i)) .FirstOrDefault(System.IO.File.Exists); if (!string.IsNullOrEmpty(path) && System.IO.File.Exists(path)) { if (!path.StartsWith(basePath, StringComparison.InvariantCulture)) { return BadRequest("Invalid image path."); } var contentType = MimeTypes.GetMimeType(path); return PhysicalFile(path, contentType); } } return NotFound(); } private List GetImageList(string path, bool supportsThemes) { try { return _fileSystem.GetFiles(path, BaseItem.SupportedImageExtensions, false, true) .Select(i => new ImageByNameInfo { Name = _fileSystem.GetFileNameWithoutExtension(i), FileLength = i.Length, // For themeable images, use the Theme property // For general images, the same object structure is fine, // but it's not owned by a theme, so call it Context Theme = supportsThemes ? GetThemeName(i.FullName, path) : null, Context = supportsThemes ? null : GetThemeName(i.FullName, path), Format = i.Extension.ToLowerInvariant().TrimStart('.') }) .OrderBy(i => i.Name) .ToList(); } catch (IOException) { return new List(); } } private string? GetThemeName(string path, string rootImagePath) { var parentName = Path.GetDirectoryName(path); if (string.Equals(parentName, rootImagePath, StringComparison.OrdinalIgnoreCase)) { return null; } parentName = Path.GetFileName(parentName); return string.Equals(parentName, "all", StringComparison.OrdinalIgnoreCase) ? null : parentName; } } }