using System; using System.Collections.Generic; using System.Collections.Immutable; using System.ComponentModel.DataAnnotations; using System.Diagnostics.CodeAnalysis; using System.Globalization; using System.IO; using System.Linq; using System.Net.Mime; using System.Threading; using System.Threading.Tasks; using Jellyfin.Api.Attributes; using Jellyfin.Api.Constants; using Jellyfin.Api.Helpers; using MediaBrowser.Common.Configuration; using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Drawing; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Providers; using MediaBrowser.Model.Branding; using MediaBrowser.Model.Drawing; using MediaBrowser.Model.Dto; using MediaBrowser.Model.Entities; using MediaBrowser.Model.IO; using MediaBrowser.Model.Net; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Logging; using Microsoft.Net.Http.Headers; namespace Jellyfin.Api.Controllers { /// /// Image controller. /// [Route("")] public class ImageController : BaseJellyfinApiController { private readonly IUserManager _userManager; private readonly ILibraryManager _libraryManager; private readonly IProviderManager _providerManager; private readonly IImageProcessor _imageProcessor; private readonly IFileSystem _fileSystem; private readonly ILogger _logger; private readonly IServerConfigurationManager _serverConfigurationManager; private readonly IApplicationPaths _appPaths; /// /// 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. /// Instance of the interface. /// Instance of the interface. public ImageController( IUserManager userManager, ILibraryManager libraryManager, IProviderManager providerManager, IImageProcessor imageProcessor, IFileSystem fileSystem, ILogger logger, IServerConfigurationManager serverConfigurationManager, IApplicationPaths appPaths) { _userManager = userManager; _libraryManager = libraryManager; _providerManager = providerManager; _imageProcessor = imageProcessor; _fileSystem = fileSystem; _logger = logger; _serverConfigurationManager = serverConfigurationManager; _appPaths = appPaths; } /// /// Sets the user image. /// /// User Id. /// (Unused) Image type. /// (Unused) Image index. /// Image updated. /// User does not have permission to delete the image. /// A . [HttpPost("Users/{userId}/Images/{imageType}")] [Authorize(Policy = Policies.DefaultAuthorization)] [AcceptsImageFile] [ProducesResponseType(StatusCodes.Status204NoContent)] [ProducesResponseType(StatusCodes.Status403Forbidden)] [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "imageType", Justification = "Imported from ServiceStack")] [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "index", Justification = "Imported from ServiceStack")] public async Task PostUserImage( [FromRoute, Required] Guid userId, [FromRoute, Required] ImageType imageType, [FromQuery] int? index = null) { if (!RequestHelpers.AssertCanUpdateUser(_userManager, HttpContext.User, userId, true)) { return StatusCode(StatusCodes.Status403Forbidden, "User is not allowed to update the image."); } var user = _userManager.GetUserById(userId); await using var memoryStream = await GetMemoryStream(Request.Body).ConfigureAwait(false); // Handle image/png; charset=utf-8 var mimeType = Request.ContentType?.Split(';').FirstOrDefault(); var userDataPath = Path.Combine(_serverConfigurationManager.ApplicationPaths.UserConfigurationDirectoryPath, user.Username); if (user.ProfileImage != null) { await _userManager.ClearProfileImageAsync(user).ConfigureAwait(false); } user.ProfileImage = new Data.Entities.ImageInfo(Path.Combine(userDataPath, "profile" + MimeTypes.ToExtension(mimeType ?? string.Empty))); await _providerManager .SaveImage(memoryStream, mimeType, user.ProfileImage.Path) .ConfigureAwait(false); await _userManager.UpdateUserAsync(user).ConfigureAwait(false); return NoContent(); } /// /// Sets the user image. /// /// User Id. /// (Unused) Image type. /// (Unused) Image index. /// Image updated. /// User does not have permission to delete the image. /// A . [HttpPost("Users/{userId}/Images/{imageType}/{index}")] [Authorize(Policy = Policies.DefaultAuthorization)] [AcceptsImageFile] [ProducesResponseType(StatusCodes.Status204NoContent)] [ProducesResponseType(StatusCodes.Status403Forbidden)] [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "imageType", Justification = "Imported from ServiceStack")] [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "index", Justification = "Imported from ServiceStack")] public async Task PostUserImageByIndex( [FromRoute, Required] Guid userId, [FromRoute, Required] ImageType imageType, [FromRoute] int index) { if (!RequestHelpers.AssertCanUpdateUser(_userManager, HttpContext.User, userId, true)) { return StatusCode(StatusCodes.Status403Forbidden, "User is not allowed to update the image."); } var user = _userManager.GetUserById(userId); await using var memoryStream = await GetMemoryStream(Request.Body).ConfigureAwait(false); // Handle image/png; charset=utf-8 var mimeType = Request.ContentType?.Split(';').FirstOrDefault(); var userDataPath = Path.Combine(_serverConfigurationManager.ApplicationPaths.UserConfigurationDirectoryPath, user.Username); if (user.ProfileImage != null) { await _userManager.ClearProfileImageAsync(user).ConfigureAwait(false); } user.ProfileImage = new Data.Entities.ImageInfo(Path.Combine(userDataPath, "profile" + MimeTypes.ToExtension(mimeType ?? string.Empty))); await _providerManager .SaveImage(memoryStream, mimeType, user.ProfileImage.Path) .ConfigureAwait(false); await _userManager.UpdateUserAsync(user).ConfigureAwait(false); return NoContent(); } /// /// Delete the user's image. /// /// User Id. /// (Unused) Image type. /// (Unused) Image index. /// Image deleted. /// User does not have permission to delete the image. /// A . [HttpDelete("Users/{userId}/Images/{imageType}")] [Authorize(Policy = Policies.DefaultAuthorization)] [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "imageType", Justification = "Imported from ServiceStack")] [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "index", Justification = "Imported from ServiceStack")] [ProducesResponseType(StatusCodes.Status204NoContent)] [ProducesResponseType(StatusCodes.Status403Forbidden)] public async Task DeleteUserImage( [FromRoute, Required] Guid userId, [FromRoute, Required] ImageType imageType, [FromQuery] int? index = null) { if (!RequestHelpers.AssertCanUpdateUser(_userManager, HttpContext.User, userId, true)) { return StatusCode(StatusCodes.Status403Forbidden, "User is not allowed to delete the image."); } var user = _userManager.GetUserById(userId); if (user?.ProfileImage is null) { return NoContent(); } try { System.IO.File.Delete(user.ProfileImage.Path); } catch (IOException e) { _logger.LogError(e, "Error deleting user profile image:"); } await _userManager.ClearProfileImageAsync(user).ConfigureAwait(false); return NoContent(); } /// /// Delete the user's image. /// /// User Id. /// (Unused) Image type. /// (Unused) Image index. /// Image deleted. /// User does not have permission to delete the image. /// A . [HttpDelete("Users/{userId}/Images/{imageType}/{index}")] [Authorize(Policy = Policies.DefaultAuthorization)] [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "imageType", Justification = "Imported from ServiceStack")] [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "index", Justification = "Imported from ServiceStack")] [ProducesResponseType(StatusCodes.Status204NoContent)] [ProducesResponseType(StatusCodes.Status403Forbidden)] public async Task DeleteUserImageByIndex( [FromRoute, Required] Guid userId, [FromRoute, Required] ImageType imageType, [FromRoute] int index) { if (!RequestHelpers.AssertCanUpdateUser(_userManager, HttpContext.User, userId, true)) { return StatusCode(StatusCodes.Status403Forbidden, "User is not allowed to delete the image."); } var user = _userManager.GetUserById(userId); if (user?.ProfileImage is null) { return NoContent(); } try { System.IO.File.Delete(user.ProfileImage.Path); } catch (IOException e) { _logger.LogError(e, "Error deleting user profile image:"); } await _userManager.ClearProfileImageAsync(user).ConfigureAwait(false); return NoContent(); } /// /// Delete an item's image. /// /// Item id. /// Image type. /// The image index. /// Image deleted. /// Item not found. /// A on success, or a if item not found. [HttpDelete("Items/{itemId}/Images/{imageType}")] [Authorize(Policy = Policies.RequiresElevation)] [ProducesResponseType(StatusCodes.Status204NoContent)] [ProducesResponseType(StatusCodes.Status404NotFound)] public async Task DeleteItemImage( [FromRoute, Required] Guid itemId, [FromRoute, Required] ImageType imageType, [FromQuery] int? imageIndex) { var item = _libraryManager.GetItemById(itemId); if (item is null) { return NotFound(); } await item.DeleteImageAsync(imageType, imageIndex ?? 0).ConfigureAwait(false); return NoContent(); } /// /// Delete an item's image. /// /// Item id. /// Image type. /// The image index. /// Image deleted. /// Item not found. /// A on success, or a if item not found. [HttpDelete("Items/{itemId}/Images/{imageType}/{imageIndex}")] [Authorize(Policy = Policies.RequiresElevation)] [ProducesResponseType(StatusCodes.Status204NoContent)] [ProducesResponseType(StatusCodes.Status404NotFound)] public async Task DeleteItemImageByIndex( [FromRoute, Required] Guid itemId, [FromRoute, Required] ImageType imageType, [FromRoute] int imageIndex) { var item = _libraryManager.GetItemById(itemId); if (item is null) { return NotFound(); } await item.DeleteImageAsync(imageType, imageIndex).ConfigureAwait(false); return NoContent(); } /// /// Set item image. /// /// Item id. /// Image type. /// Image saved. /// Item not found. /// A on success, or a if item not found. [HttpPost("Items/{itemId}/Images/{imageType}")] [Authorize(Policy = Policies.RequiresElevation)] [AcceptsImageFile] [ProducesResponseType(StatusCodes.Status204NoContent)] [ProducesResponseType(StatusCodes.Status404NotFound)] [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "index", Justification = "Imported from ServiceStack")] public async Task SetItemImage( [FromRoute, Required] Guid itemId, [FromRoute, Required] ImageType imageType) { var item = _libraryManager.GetItemById(itemId); if (item is null) { return NotFound(); } await using var memoryStream = await GetMemoryStream(Request.Body).ConfigureAwait(false); // Handle image/png; charset=utf-8 var mimeType = Request.ContentType?.Split(';').FirstOrDefault(); await _providerManager.SaveImage(item, memoryStream, mimeType, imageType, null, CancellationToken.None).ConfigureAwait(false); await item.UpdateToRepositoryAsync(ItemUpdateType.ImageUpdate, CancellationToken.None).ConfigureAwait(false); return NoContent(); } /// /// Set item image. /// /// Item id. /// Image type. /// (Unused) Image index. /// Image saved. /// Item not found. /// A on success, or a if item not found. [HttpPost("Items/{itemId}/Images/{imageType}/{imageIndex}")] [Authorize(Policy = Policies.RequiresElevation)] [AcceptsImageFile] [ProducesResponseType(StatusCodes.Status204NoContent)] [ProducesResponseType(StatusCodes.Status404NotFound)] [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "index", Justification = "Imported from ServiceStack")] public async Task SetItemImageByIndex( [FromRoute, Required] Guid itemId, [FromRoute, Required] ImageType imageType, [FromRoute] int imageIndex) { var item = _libraryManager.GetItemById(itemId); if (item is null) { return NotFound(); } await using var memoryStream = await GetMemoryStream(Request.Body).ConfigureAwait(false); // Handle image/png; charset=utf-8 var mimeType = Request.ContentType?.Split(';').FirstOrDefault(); await _providerManager.SaveImage(item, memoryStream, mimeType, imageType, null, CancellationToken.None).ConfigureAwait(false); await item.UpdateToRepositoryAsync(ItemUpdateType.ImageUpdate, CancellationToken.None).ConfigureAwait(false); return NoContent(); } /// /// Updates the index for an item image. /// /// Item id. /// Image type. /// Old image index. /// New image index. /// Image index updated. /// Item not found. /// A on success, or a if item not found. [HttpPost("Items/{itemId}/Images/{imageType}/{imageIndex}/Index")] [Authorize(Policy = Policies.RequiresElevation)] [ProducesResponseType(StatusCodes.Status204NoContent)] [ProducesResponseType(StatusCodes.Status404NotFound)] public async Task UpdateItemImageIndex( [FromRoute, Required] Guid itemId, [FromRoute, Required] ImageType imageType, [FromRoute, Required] int imageIndex, [FromQuery, Required] int newIndex) { var item = _libraryManager.GetItemById(itemId); if (item is null) { return NotFound(); } await item.SwapImagesAsync(imageType, imageIndex, newIndex).ConfigureAwait(false); return NoContent(); } /// /// Get item image infos. /// /// Item id. /// Item images returned. /// Item not found. /// The list of image infos on success, or if item not found. [HttpGet("Items/{itemId}/Images")] [Authorize(Policy = Policies.DefaultAuthorization)] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] public async Task>> GetItemImageInfos([FromRoute, Required] Guid itemId) { var item = _libraryManager.GetItemById(itemId); if (item is null) { return NotFound(); } var list = new List(); var itemImages = item.ImageInfos; if (itemImages.Length == 0) { // short-circuit return list; } await _libraryManager.UpdateImagesAsync(item).ConfigureAwait(false); // this makes sure dimensions and hashes are correct foreach (var image in itemImages) { if (!item.AllowsMultipleImages(image.Type)) { var info = GetImageInfo(item, image, null); if (info != null) { list.Add(info); } } } foreach (var imageType in itemImages.Select(i => i.Type).Distinct().Where(item.AllowsMultipleImages)) { var index = 0; // Prevent implicitly captured closure var currentImageType = imageType; foreach (var image in itemImages.Where(i => i.Type == currentImageType)) { var info = GetImageInfo(item, image, index); if (info != null) { list.Add(info); } index++; } } return list; } /// /// Gets the item's image. /// /// Item id. /// Image type. /// The maximum image width to return. /// The maximum image height to return. /// The fixed image width to return. /// The fixed image height to return. /// Optional. Quality setting, from 0-100. Defaults to 90 and should suffice in most cases. /// Width of box to fill. /// Height of box to fill. /// Optional. Supply the cache tag from the item object to receive strong caching headers. /// Optional. Specify if whitespace should be cropped out of the image. True/False. If unspecified, whitespace will be cropped from logos and clear art. /// Optional. The of the returned image. /// Optional. Add a played indicator. /// Optional. Percent to render for the percent played overlay. /// Optional. Unplayed count overlay to render. /// Optional. Blur image. /// Optional. Apply a background color for transparent images. /// Optional. Apply a foreground layer on top of the image. /// Image index. /// Image stream returned. /// Item not found. /// /// A containing the file stream on success, /// or a if item not found. /// [HttpGet("Items/{itemId}/Images/{imageType}")] [HttpHead("Items/{itemId}/Images/{imageType}", Name = "HeadItemImage")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] [ProducesImageFile] public async Task GetItemImage( [FromRoute, Required] Guid itemId, [FromRoute, Required] ImageType imageType, [FromQuery] int? maxWidth, [FromQuery] int? maxHeight, [FromQuery] int? width, [FromQuery] int? height, [FromQuery] int? quality, [FromQuery] int? fillWidth, [FromQuery] int? fillHeight, [FromQuery] string? tag, [FromQuery, ParameterObsolete] bool? cropWhitespace, [FromQuery] ImageFormat? format, [FromQuery] bool? addPlayedIndicator, [FromQuery] double? percentPlayed, [FromQuery] int? unplayedCount, [FromQuery] int? blur, [FromQuery] string? backgroundColor, [FromQuery] string? foregroundLayer, [FromQuery] int? imageIndex) { var item = _libraryManager.GetItemById(itemId); if (item is null) { return NotFound(); } return await GetImageInternal( itemId, imageType, imageIndex, tag, format, maxWidth, maxHeight, percentPlayed, unplayedCount, width, height, quality, fillWidth, fillHeight, addPlayedIndicator, blur, backgroundColor, foregroundLayer, item) .ConfigureAwait(false); } /// /// Gets the item's image. /// /// Item id. /// Image type. /// Image index. /// The maximum image width to return. /// The maximum image height to return. /// The fixed image width to return. /// The fixed image height to return. /// Optional. Quality setting, from 0-100. Defaults to 90 and should suffice in most cases. /// Width of box to fill. /// Height of box to fill. /// Optional. Supply the cache tag from the item object to receive strong caching headers. /// Optional. Specify if whitespace should be cropped out of the image. True/False. If unspecified, whitespace will be cropped from logos and clear art. /// Optional. The of the returned image. /// Optional. Add a played indicator. /// Optional. Percent to render for the percent played overlay. /// Optional. Unplayed count overlay to render. /// Optional. Blur image. /// Optional. Apply a background color for transparent images. /// Optional. Apply a foreground layer on top of the image. /// Image stream returned. /// Item not found. /// /// A containing the file stream on success, /// or a if item not found. /// [HttpGet("Items/{itemId}/Images/{imageType}/{imageIndex}")] [HttpHead("Items/{itemId}/Images/{imageType}/{imageIndex}", Name = "HeadItemImageByIndex")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] [ProducesImageFile] public async Task GetItemImageByIndex( [FromRoute, Required] Guid itemId, [FromRoute, Required] ImageType imageType, [FromRoute] int imageIndex, [FromQuery] int? maxWidth, [FromQuery] int? maxHeight, [FromQuery] int? width, [FromQuery] int? height, [FromQuery] int? quality, [FromQuery] int? fillWidth, [FromQuery] int? fillHeight, [FromQuery] string? tag, [FromQuery, ParameterObsolete] bool? cropWhitespace, [FromQuery] ImageFormat? format, [FromQuery] bool? addPlayedIndicator, [FromQuery] double? percentPlayed, [FromQuery] int? unplayedCount, [FromQuery] int? blur, [FromQuery] string? backgroundColor, [FromQuery] string? foregroundLayer) { var item = _libraryManager.GetItemById(itemId); if (item is null) { return NotFound(); } return await GetImageInternal( itemId, imageType, imageIndex, tag, format, maxWidth, maxHeight, percentPlayed, unplayedCount, width, height, quality, fillWidth, fillHeight, addPlayedIndicator, blur, backgroundColor, foregroundLayer, item) .ConfigureAwait(false); } /// /// Gets the item's image. /// /// Item id. /// Image type. /// The maximum image width to return. /// The maximum image height to return. /// The fixed image width to return. /// The fixed image height to return. /// Optional. Quality setting, from 0-100. Defaults to 90 and should suffice in most cases. /// Width of box to fill. /// Height of box to fill. /// Optional. Supply the cache tag from the item object to receive strong caching headers. /// Optional. Specify if whitespace should be cropped out of the image. True/False. If unspecified, whitespace will be cropped from logos and clear art. /// Determines the output format of the image - original,gif,jpg,png. /// Optional. Add a played indicator. /// Optional. Percent to render for the percent played overlay. /// Optional. Unplayed count overlay to render. /// Optional. Blur image. /// Optional. Apply a background color for transparent images. /// Optional. Apply a foreground layer on top of the image. /// Image index. /// Image stream returned. /// Item not found. /// /// A containing the file stream on success, /// or a if item not found. /// [HttpGet("Items/{itemId}/Images/{imageType}/{imageIndex}/{tag}/{format}/{maxWidth}/{maxHeight}/{percentPlayed}/{unplayedCount}")] [HttpHead("Items/{itemId}/Images/{imageType}/{imageIndex}/{tag}/{format}/{maxWidth}/{maxHeight}/{percentPlayed}/{unplayedCount}", Name = "HeadItemImage2")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] [ProducesImageFile] public async Task GetItemImage2( [FromRoute, Required] Guid itemId, [FromRoute, Required] ImageType imageType, [FromRoute, Required] int maxWidth, [FromRoute, Required] int maxHeight, [FromQuery] int? width, [FromQuery] int? height, [FromQuery] int? quality, [FromQuery] int? fillWidth, [FromQuery] int? fillHeight, [FromRoute, Required] string tag, [FromQuery, ParameterObsolete] bool? cropWhitespace, [FromRoute, Required] ImageFormat format, [FromQuery] bool? addPlayedIndicator, [FromRoute, Required] double percentPlayed, [FromRoute, Required] int unplayedCount, [FromQuery] int? blur, [FromQuery] string? backgroundColor, [FromQuery] string? foregroundLayer, [FromRoute, Required] int imageIndex) { var item = _libraryManager.GetItemById(itemId); if (item is null) { return NotFound(); } return await GetImageInternal( itemId, imageType, imageIndex, tag, format, maxWidth, maxHeight, percentPlayed, unplayedCount, width, height, quality, fillWidth, fillHeight, addPlayedIndicator, blur, backgroundColor, foregroundLayer, item) .ConfigureAwait(false); } /// /// Get artist image by name. /// /// Artist name. /// Image type. /// Optional. Supply the cache tag from the item object to receive strong caching headers. /// Determines the output format of the image - original,gif,jpg,png. /// The maximum image width to return. /// The maximum image height to return. /// Optional. Percent to render for the percent played overlay. /// Optional. Unplayed count overlay to render. /// The fixed image width to return. /// The fixed image height to return. /// Optional. Quality setting, from 0-100. Defaults to 90 and should suffice in most cases. /// Width of box to fill. /// Height of box to fill. /// Optional. Specify if whitespace should be cropped out of the image. True/False. If unspecified, whitespace will be cropped from logos and clear art. /// Optional. Add a played indicator. /// Optional. Blur image. /// Optional. Apply a background color for transparent images. /// Optional. Apply a foreground layer on top of the image. /// Image index. /// Image stream returned. /// Item not found. /// /// A containing the file stream on success, /// or a if item not found. /// [HttpGet("Artists/{name}/Images/{imageType}/{imageIndex}")] [HttpHead("Artists/{name}/Images/{imageType}/{imageIndex}", Name = "HeadArtistImage")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] [ProducesImageFile] public async Task GetArtistImage( [FromRoute, Required] string name, [FromRoute, Required] ImageType imageType, [FromQuery] string? tag, [FromQuery] ImageFormat? format, [FromQuery] int? maxWidth, [FromQuery] int? maxHeight, [FromQuery] double? percentPlayed, [FromQuery] int? unplayedCount, [FromQuery] int? width, [FromQuery] int? height, [FromQuery] int? quality, [FromQuery] int? fillWidth, [FromQuery] int? fillHeight, [FromQuery, ParameterObsolete] bool? cropWhitespace, [FromQuery] bool? addPlayedIndicator, [FromQuery] int? blur, [FromQuery] string? backgroundColor, [FromQuery] string? foregroundLayer, [FromRoute, Required] int imageIndex) { var item = _libraryManager.GetArtist(name); if (item is null) { return NotFound(); } return await GetImageInternal( item.Id, imageType, imageIndex, tag, format, maxWidth, maxHeight, percentPlayed, unplayedCount, width, height, quality, fillWidth, fillHeight, addPlayedIndicator, blur, backgroundColor, foregroundLayer, item) .ConfigureAwait(false); } /// /// Get genre image by name. /// /// Genre name. /// Image type. /// Optional. Supply the cache tag from the item object to receive strong caching headers. /// Determines the output format of the image - original,gif,jpg,png. /// The maximum image width to return. /// The maximum image height to return. /// Optional. Percent to render for the percent played overlay. /// Optional. Unplayed count overlay to render. /// The fixed image width to return. /// The fixed image height to return. /// Optional. Quality setting, from 0-100. Defaults to 90 and should suffice in most cases. /// Width of box to fill. /// Height of box to fill. /// Optional. Specify if whitespace should be cropped out of the image. True/False. If unspecified, whitespace will be cropped from logos and clear art. /// Optional. Add a played indicator. /// Optional. Blur image. /// Optional. Apply a background color for transparent images. /// Optional. Apply a foreground layer on top of the image. /// Image index. /// Image stream returned. /// Item not found. /// /// A containing the file stream on success, /// or a if item not found. /// [HttpGet("Genres/{name}/Images/{imageType}")] [HttpHead("Genres/{name}/Images/{imageType}", Name = "HeadGenreImage")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] [ProducesImageFile] public async Task GetGenreImage( [FromRoute, Required] string name, [FromRoute, Required] ImageType imageType, [FromQuery] string? tag, [FromQuery] ImageFormat? format, [FromQuery] int? maxWidth, [FromQuery] int? maxHeight, [FromQuery] double? percentPlayed, [FromQuery] int? unplayedCount, [FromQuery] int? width, [FromQuery] int? height, [FromQuery] int? quality, [FromQuery] int? fillWidth, [FromQuery] int? fillHeight, [FromQuery, ParameterObsolete] bool? cropWhitespace, [FromQuery] bool? addPlayedIndicator, [FromQuery] int? blur, [FromQuery] string? backgroundColor, [FromQuery] string? foregroundLayer, [FromQuery] int? imageIndex) { var item = _libraryManager.GetGenre(name); if (item is null) { return NotFound(); } return await GetImageInternal( item.Id, imageType, imageIndex, tag, format, maxWidth, maxHeight, percentPlayed, unplayedCount, width, height, quality, fillWidth, fillHeight, addPlayedIndicator, blur, backgroundColor, foregroundLayer, item) .ConfigureAwait(false); } /// /// Get genre image by name. /// /// Genre name. /// Image type. /// Image index. /// Optional. Supply the cache tag from the item object to receive strong caching headers. /// Determines the output format of the image - original,gif,jpg,png. /// The maximum image width to return. /// The maximum image height to return. /// Optional. Percent to render for the percent played overlay. /// Optional. Unplayed count overlay to render. /// The fixed image width to return. /// The fixed image height to return. /// Optional. Quality setting, from 0-100. Defaults to 90 and should suffice in most cases. /// Width of box to fill. /// Height of box to fill. /// Optional. Specify if whitespace should be cropped out of the image. True/False. If unspecified, whitespace will be cropped from logos and clear art. /// Optional. Add a played indicator. /// Optional. Blur image. /// Optional. Apply a background color for transparent images. /// Optional. Apply a foreground layer on top of the image. /// Image stream returned. /// Item not found. /// /// A containing the file stream on success, /// or a if item not found. /// [HttpGet("Genres/{name}/Images/{imageType}/{imageIndex}")] [HttpHead("Genres/{name}/Images/{imageType}/{imageIndex}", Name = "HeadGenreImageByIndex")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] [ProducesImageFile] public async Task GetGenreImageByIndex( [FromRoute, Required] string name, [FromRoute, Required] ImageType imageType, [FromRoute, Required] int imageIndex, [FromQuery] string? tag, [FromQuery] ImageFormat? format, [FromQuery] int? maxWidth, [FromQuery] int? maxHeight, [FromQuery] double? percentPlayed, [FromQuery] int? unplayedCount, [FromQuery] int? width, [FromQuery] int? height, [FromQuery] int? quality, [FromQuery] int? fillWidth, [FromQuery] int? fillHeight, [FromQuery, ParameterObsolete] bool? cropWhitespace, [FromQuery] bool? addPlayedIndicator, [FromQuery] int? blur, [FromQuery] string? backgroundColor, [FromQuery] string? foregroundLayer) { var item = _libraryManager.GetGenre(name); if (item is null) { return NotFound(); } return await GetImageInternal( item.Id, imageType, imageIndex, tag, format, maxWidth, maxHeight, percentPlayed, unplayedCount, width, height, quality, fillWidth, fillHeight, addPlayedIndicator, blur, backgroundColor, foregroundLayer, item) .ConfigureAwait(false); } /// /// Get music genre image by name. /// /// Music genre name. /// Image type. /// Optional. Supply the cache tag from the item object to receive strong caching headers. /// Determines the output format of the image - original,gif,jpg,png. /// The maximum image width to return. /// The maximum image height to return. /// Optional. Percent to render for the percent played overlay. /// Optional. Unplayed count overlay to render. /// The fixed image width to return. /// The fixed image height to return. /// Optional. Quality setting, from 0-100. Defaults to 90 and should suffice in most cases. /// Width of box to fill. /// Height of box to fill. /// Optional. Specify if whitespace should be cropped out of the image. True/False. If unspecified, whitespace will be cropped from logos and clear art. /// Optional. Add a played indicator. /// Optional. Blur image. /// Optional. Apply a background color for transparent images. /// Optional. Apply a foreground layer on top of the image. /// Image index. /// Image stream returned. /// Item not found. /// /// A containing the file stream on success, /// or a if item not found. /// [HttpGet("MusicGenres/{name}/Images/{imageType}")] [HttpHead("MusicGenres/{name}/Images/{imageType}", Name = "HeadMusicGenreImage")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] [ProducesImageFile] public async Task GetMusicGenreImage( [FromRoute, Required] string name, [FromRoute, Required] ImageType imageType, [FromQuery] string? tag, [FromQuery] ImageFormat? format, [FromQuery] int? maxWidth, [FromQuery] int? maxHeight, [FromQuery] double? percentPlayed, [FromQuery] int? unplayedCount, [FromQuery] int? width, [FromQuery] int? height, [FromQuery] int? quality, [FromQuery] int? fillWidth, [FromQuery] int? fillHeight, [FromQuery, ParameterObsolete] bool? cropWhitespace, [FromQuery] bool? addPlayedIndicator, [FromQuery] int? blur, [FromQuery] string? backgroundColor, [FromQuery] string? foregroundLayer, [FromQuery] int? imageIndex) { var item = _libraryManager.GetMusicGenre(name); if (item is null) { return NotFound(); } return await GetImageInternal( item.Id, imageType, imageIndex, tag, format, maxWidth, maxHeight, percentPlayed, unplayedCount, width, height, quality, fillWidth, fillHeight, addPlayedIndicator, blur, backgroundColor, foregroundLayer, item) .ConfigureAwait(false); } /// /// Get music genre image by name. /// /// Music genre name. /// Image type. /// Image index. /// Optional. Supply the cache tag from the item object to receive strong caching headers. /// Determines the output format of the image - original,gif,jpg,png. /// The maximum image width to return. /// The maximum image height to return. /// Optional. Percent to render for the percent played overlay. /// Optional. Unplayed count overlay to render. /// The fixed image width to return. /// The fixed image height to return. /// Optional. Quality setting, from 0-100. Defaults to 90 and should suffice in most cases. /// Width of box to fill. /// Height of box to fill. /// Optional. Specify if whitespace should be cropped out of the image. True/False. If unspecified, whitespace will be cropped from logos and clear art. /// Optional. Add a played indicator. /// Optional. Blur image. /// Optional. Apply a background color for transparent images. /// Optional. Apply a foreground layer on top of the image. /// Image stream returned. /// Item not found. /// /// A containing the file stream on success, /// or a if item not found. /// [HttpGet("MusicGenres/{name}/Images/{imageType}/{imageIndex}")] [HttpHead("MusicGenres/{name}/Images/{imageType}/{imageIndex}", Name = "HeadMusicGenreImageByIndex")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] [ProducesImageFile] public async Task GetMusicGenreImageByIndex( [FromRoute, Required] string name, [FromRoute, Required] ImageType imageType, [FromRoute, Required] int imageIndex, [FromQuery] string? tag, [FromQuery] ImageFormat? format, [FromQuery] int? maxWidth, [FromQuery] int? maxHeight, [FromQuery] double? percentPlayed, [FromQuery] int? unplayedCount, [FromQuery] int? width, [FromQuery] int? height, [FromQuery] int? quality, [FromQuery] int? fillWidth, [FromQuery] int? fillHeight, [FromQuery, ParameterObsolete] bool? cropWhitespace, [FromQuery] bool? addPlayedIndicator, [FromQuery] int? blur, [FromQuery] string? backgroundColor, [FromQuery] string? foregroundLayer) { var item = _libraryManager.GetMusicGenre(name); if (item is null) { return NotFound(); } return await GetImageInternal( item.Id, imageType, imageIndex, tag, format, maxWidth, maxHeight, percentPlayed, unplayedCount, width, height, quality, fillWidth, fillHeight, addPlayedIndicator, blur, backgroundColor, foregroundLayer, item) .ConfigureAwait(false); } /// /// Get person image by name. /// /// Person name. /// Image type. /// Optional. Supply the cache tag from the item object to receive strong caching headers. /// Determines the output format of the image - original,gif,jpg,png. /// The maximum image width to return. /// The maximum image height to return. /// Optional. Percent to render for the percent played overlay. /// Optional. Unplayed count overlay to render. /// The fixed image width to return. /// The fixed image height to return. /// Optional. Quality setting, from 0-100. Defaults to 90 and should suffice in most cases. /// Width of box to fill. /// Height of box to fill. /// Optional. Specify if whitespace should be cropped out of the image. True/False. If unspecified, whitespace will be cropped from logos and clear art. /// Optional. Add a played indicator. /// Optional. Blur image. /// Optional. Apply a background color for transparent images. /// Optional. Apply a foreground layer on top of the image. /// Image index. /// Image stream returned. /// Item not found. /// /// A containing the file stream on success, /// or a if item not found. /// [HttpGet("Persons/{name}/Images/{imageType}")] [HttpHead("Persons/{name}/Images/{imageType}", Name = "HeadPersonImage")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] [ProducesImageFile] public async Task GetPersonImage( [FromRoute, Required] string name, [FromRoute, Required] ImageType imageType, [FromQuery] string? tag, [FromQuery] ImageFormat? format, [FromQuery] int? maxWidth, [FromQuery] int? maxHeight, [FromQuery] double? percentPlayed, [FromQuery] int? unplayedCount, [FromQuery] int? width, [FromQuery] int? height, [FromQuery] int? quality, [FromQuery] int? fillWidth, [FromQuery] int? fillHeight, [FromQuery, ParameterObsolete] bool? cropWhitespace, [FromQuery] bool? addPlayedIndicator, [FromQuery] int? blur, [FromQuery] string? backgroundColor, [FromQuery] string? foregroundLayer, [FromQuery] int? imageIndex) { var item = _libraryManager.GetPerson(name); if (item is null) { return NotFound(); } return await GetImageInternal( item.Id, imageType, imageIndex, tag, format, maxWidth, maxHeight, percentPlayed, unplayedCount, width, height, quality, fillWidth, fillHeight, addPlayedIndicator, blur, backgroundColor, foregroundLayer, item) .ConfigureAwait(false); } /// /// Get person image by name. /// /// Person name. /// Image type. /// Image index. /// Optional. Supply the cache tag from the item object to receive strong caching headers. /// Determines the output format of the image - original,gif,jpg,png. /// The maximum image width to return. /// The maximum image height to return. /// Optional. Percent to render for the percent played overlay. /// Optional. Unplayed count overlay to render. /// The fixed image width to return. /// The fixed image height to return. /// Optional. Quality setting, from 0-100. Defaults to 90 and should suffice in most cases. /// Width of box to fill. /// Height of box to fill. /// Optional. Specify if whitespace should be cropped out of the image. True/False. If unspecified, whitespace will be cropped from logos and clear art. /// Optional. Add a played indicator. /// Optional. Blur image. /// Optional. Apply a background color for transparent images. /// Optional. Apply a foreground layer on top of the image. /// Image stream returned. /// Item not found. /// /// A containing the file stream on success, /// or a if item not found. /// [HttpGet("Persons/{name}/Images/{imageType}/{imageIndex}")] [HttpHead("Persons/{name}/Images/{imageType}/{imageIndex}", Name = "HeadPersonImageByIndex")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] [ProducesImageFile] public async Task GetPersonImageByIndex( [FromRoute, Required] string name, [FromRoute, Required] ImageType imageType, [FromRoute, Required] int imageIndex, [FromQuery] string? tag, [FromQuery] ImageFormat? format, [FromQuery] int? maxWidth, [FromQuery] int? maxHeight, [FromQuery] double? percentPlayed, [FromQuery] int? unplayedCount, [FromQuery] int? width, [FromQuery] int? height, [FromQuery] int? quality, [FromQuery] int? fillWidth, [FromQuery] int? fillHeight, [FromQuery, ParameterObsolete] bool? cropWhitespace, [FromQuery] bool? addPlayedIndicator, [FromQuery] int? blur, [FromQuery] string? backgroundColor, [FromQuery] string? foregroundLayer) { var item = _libraryManager.GetPerson(name); if (item is null) { return NotFound(); } return await GetImageInternal( item.Id, imageType, imageIndex, tag, format, maxWidth, maxHeight, percentPlayed, unplayedCount, width, height, quality, fillWidth, fillHeight, addPlayedIndicator, blur, backgroundColor, foregroundLayer, item) .ConfigureAwait(false); } /// /// Get studio image by name. /// /// Studio name. /// Image type. /// Optional. Supply the cache tag from the item object to receive strong caching headers. /// Determines the output format of the image - original,gif,jpg,png. /// The maximum image width to return. /// The maximum image height to return. /// Optional. Percent to render for the percent played overlay. /// Optional. Unplayed count overlay to render. /// The fixed image width to return. /// The fixed image height to return. /// Optional. Quality setting, from 0-100. Defaults to 90 and should suffice in most cases. /// Width of box to fill. /// Height of box to fill. /// Optional. Specify if whitespace should be cropped out of the image. True/False. If unspecified, whitespace will be cropped from logos and clear art. /// Optional. Add a played indicator. /// Optional. Blur image. /// Optional. Apply a background color for transparent images. /// Optional. Apply a foreground layer on top of the image. /// Image index. /// Image stream returned. /// Item not found. /// /// A containing the file stream on success, /// or a if item not found. /// [HttpGet("Studios/{name}/Images/{imageType}")] [HttpHead("Studios/{name}/Images/{imageType}", Name = "HeadStudioImage")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] [ProducesImageFile] public async Task GetStudioImage( [FromRoute, Required] string name, [FromRoute, Required] ImageType imageType, [FromQuery] string? tag, [FromQuery] ImageFormat? format, [FromQuery] int? maxWidth, [FromQuery] int? maxHeight, [FromQuery] double? percentPlayed, [FromQuery] int? unplayedCount, [FromQuery] int? width, [FromQuery] int? height, [FromQuery] int? quality, [FromQuery] int? fillWidth, [FromQuery] int? fillHeight, [FromQuery, ParameterObsolete] bool? cropWhitespace, [FromQuery] bool? addPlayedIndicator, [FromQuery] int? blur, [FromQuery] string? backgroundColor, [FromQuery] string? foregroundLayer, [FromQuery] int? imageIndex) { var item = _libraryManager.GetStudio(name); if (item is null) { return NotFound(); } return await GetImageInternal( item.Id, imageType, imageIndex, tag, format, maxWidth, maxHeight, percentPlayed, unplayedCount, width, height, quality, fillWidth, fillHeight, addPlayedIndicator, blur, backgroundColor, foregroundLayer, item) .ConfigureAwait(false); } /// /// Get studio image by name. /// /// Studio name. /// Image type. /// Image index. /// Optional. Supply the cache tag from the item object to receive strong caching headers. /// Determines the output format of the image - original,gif,jpg,png. /// The maximum image width to return. /// The maximum image height to return. /// Optional. Percent to render for the percent played overlay. /// Optional. Unplayed count overlay to render. /// The fixed image width to return. /// The fixed image height to return. /// Optional. Quality setting, from 0-100. Defaults to 90 and should suffice in most cases. /// Width of box to fill. /// Height of box to fill. /// Optional. Specify if whitespace should be cropped out of the image. True/False. If unspecified, whitespace will be cropped from logos and clear art. /// Optional. Add a played indicator. /// Optional. Blur image. /// Optional. Apply a background color for transparent images. /// Optional. Apply a foreground layer on top of the image. /// Image stream returned. /// Item not found. /// /// A containing the file stream on success, /// or a if item not found. /// [HttpGet("Studios/{name}/Images/{imageType}/{imageIndex}")] [HttpHead("Studios/{name}/Images/{imageType}/{imageIndex}", Name = "HeadStudioImageByIndex")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] [ProducesImageFile] public async Task GetStudioImageByIndex( [FromRoute, Required] string name, [FromRoute, Required] ImageType imageType, [FromRoute, Required] int imageIndex, [FromQuery] string? tag, [FromQuery] ImageFormat? format, [FromQuery] int? maxWidth, [FromQuery] int? maxHeight, [FromQuery] double? percentPlayed, [FromQuery] int? unplayedCount, [FromQuery] int? width, [FromQuery] int? height, [FromQuery] int? quality, [FromQuery] int? fillWidth, [FromQuery] int? fillHeight, [FromQuery, ParameterObsolete] bool? cropWhitespace, [FromQuery] bool? addPlayedIndicator, [FromQuery] int? blur, [FromQuery] string? backgroundColor, [FromQuery] string? foregroundLayer) { var item = _libraryManager.GetStudio(name); if (item is null) { return NotFound(); } return await GetImageInternal( item.Id, imageType, imageIndex, tag, format, maxWidth, maxHeight, percentPlayed, unplayedCount, width, height, quality, fillWidth, fillHeight, addPlayedIndicator, blur, backgroundColor, foregroundLayer, item) .ConfigureAwait(false); } /// /// Get user profile image. /// /// User id. /// Image type. /// Optional. Supply the cache tag from the item object to receive strong caching headers. /// Determines the output format of the image - original,gif,jpg,png. /// The maximum image width to return. /// The maximum image height to return. /// Optional. Percent to render for the percent played overlay. /// Optional. Unplayed count overlay to render. /// The fixed image width to return. /// The fixed image height to return. /// Optional. Quality setting, from 0-100. Defaults to 90 and should suffice in most cases. /// Width of box to fill. /// Height of box to fill. /// Optional. Specify if whitespace should be cropped out of the image. True/False. If unspecified, whitespace will be cropped from logos and clear art. /// Optional. Add a played indicator. /// Optional. Blur image. /// Optional. Apply a background color for transparent images. /// Optional. Apply a foreground layer on top of the image. /// Image index. /// Image stream returned. /// Item not found. /// /// A containing the file stream on success, /// or a if item not found. /// [HttpGet("Users/{userId}/Images/{imageType}")] [HttpHead("Users/{userId}/Images/{imageType}", Name = "HeadUserImage")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] [ProducesImageFile] public async Task GetUserImage( [FromRoute, Required] Guid userId, [FromRoute, Required] ImageType imageType, [FromQuery] string? tag, [FromQuery] ImageFormat? format, [FromQuery] int? maxWidth, [FromQuery] int? maxHeight, [FromQuery] double? percentPlayed, [FromQuery] int? unplayedCount, [FromQuery] int? width, [FromQuery] int? height, [FromQuery] int? quality, [FromQuery] int? fillWidth, [FromQuery] int? fillHeight, [FromQuery, ParameterObsolete] bool? cropWhitespace, [FromQuery] bool? addPlayedIndicator, [FromQuery] int? blur, [FromQuery] string? backgroundColor, [FromQuery] string? foregroundLayer, [FromQuery] int? imageIndex) { var user = _userManager.GetUserById(userId); if (user?.ProfileImage is null) { return NotFound(); } var info = new ItemImageInfo { Path = user.ProfileImage.Path, Type = ImageType.Profile, DateModified = user.ProfileImage.LastModified }; if (width.HasValue) { info.Width = width.Value; } if (height.HasValue) { info.Height = height.Value; } return await GetImageInternal( user.Id, imageType, imageIndex, tag, format, maxWidth, maxHeight, percentPlayed, unplayedCount, width, height, quality, fillWidth, fillHeight, addPlayedIndicator, blur, backgroundColor, foregroundLayer, null, info) .ConfigureAwait(false); } /// /// Get user profile image. /// /// User id. /// Image type. /// Image index. /// Optional. Supply the cache tag from the item object to receive strong caching headers. /// Determines the output format of the image - original,gif,jpg,png. /// The maximum image width to return. /// The maximum image height to return. /// Optional. Percent to render for the percent played overlay. /// Optional. Unplayed count overlay to render. /// The fixed image width to return. /// The fixed image height to return. /// Optional. Quality setting, from 0-100. Defaults to 90 and should suffice in most cases. /// Width of box to fill. /// Height of box to fill. /// Optional. Specify if whitespace should be cropped out of the image. True/False. If unspecified, whitespace will be cropped from logos and clear art. /// Optional. Add a played indicator. /// Optional. Blur image. /// Optional. Apply a background color for transparent images. /// Optional. Apply a foreground layer on top of the image. /// Image stream returned. /// Item not found. /// /// A containing the file stream on success, /// or a if item not found. /// [HttpGet("Users/{userId}/Images/{imageType}/{imageIndex}")] [HttpHead("Users/{userId}/Images/{imageType}/{imageIndex}", Name = "HeadUserImageByIndex")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] [ProducesImageFile] public async Task GetUserImageByIndex( [FromRoute, Required] Guid userId, [FromRoute, Required] ImageType imageType, [FromRoute, Required] int imageIndex, [FromQuery] string? tag, [FromQuery] ImageFormat? format, [FromQuery] int? maxWidth, [FromQuery] int? maxHeight, [FromQuery] double? percentPlayed, [FromQuery] int? unplayedCount, [FromQuery] int? width, [FromQuery] int? height, [FromQuery] int? quality, [FromQuery] int? fillWidth, [FromQuery] int? fillHeight, [FromQuery, ParameterObsolete] bool? cropWhitespace, [FromQuery] bool? addPlayedIndicator, [FromQuery] int? blur, [FromQuery] string? backgroundColor, [FromQuery] string? foregroundLayer) { var user = _userManager.GetUserById(userId); if (user?.ProfileImage is null) { return NotFound(); } var info = new ItemImageInfo { Path = user.ProfileImage.Path, Type = ImageType.Profile, DateModified = user.ProfileImage.LastModified }; if (width.HasValue) { info.Width = width.Value; } if (height.HasValue) { info.Height = height.Value; } return await GetImageInternal( user.Id, imageType, imageIndex, tag, format, maxWidth, maxHeight, percentPlayed, unplayedCount, width, height, quality, fillWidth, fillHeight, addPlayedIndicator, blur, backgroundColor, foregroundLayer, null, info) .ConfigureAwait(false); } /// /// Generates or gets the splashscreen. /// /// Supply the cache tag from the item object to receive strong caching headers. /// Determines the output format of the image - original,gif,jpg,png. /// The maximum image width to return. /// The maximum image height to return. /// The fixed image width to return. /// The fixed image height to return. /// Width of box to fill. /// Height of box to fill. /// Blur image. /// Apply a background color for transparent images. /// Apply a foreground layer on top of the image. /// Quality setting, from 0-100. /// Splashscreen returned successfully. /// The splashscreen. [HttpGet("Branding/Splashscreen")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesImageFile] public async Task GetSplashscreen( [FromQuery] string? tag, [FromQuery] ImageFormat? format, [FromQuery] int? maxWidth, [FromQuery] int? maxHeight, [FromQuery] int? width, [FromQuery] int? height, [FromQuery] int? fillWidth, [FromQuery] int? fillHeight, [FromQuery] int? blur, [FromQuery] string? backgroundColor, [FromQuery] string? foregroundLayer, [FromQuery, Range(0, 100)] int quality = 90) { var brandingOptions = _serverConfigurationManager.GetConfiguration("branding"); if (!brandingOptions.SplashscreenEnabled) { return NotFound(); } string splashscreenPath; if (!string.IsNullOrWhiteSpace(brandingOptions.SplashscreenLocation) && System.IO.File.Exists(brandingOptions.SplashscreenLocation)) { splashscreenPath = brandingOptions.SplashscreenLocation; } else { splashscreenPath = Path.Combine(_appPaths.DataPath, "splashscreen.png"); if (!System.IO.File.Exists(splashscreenPath)) { return NotFound(); } } var outputFormats = GetOutputFormats(format); TimeSpan? cacheDuration = null; if (!string.IsNullOrEmpty(tag)) { cacheDuration = TimeSpan.FromDays(365); } var options = new ImageProcessingOptions { Image = new ItemImageInfo { Path = splashscreenPath }, Height = height, MaxHeight = maxHeight, MaxWidth = maxWidth, FillHeight = fillHeight, FillWidth = fillWidth, Quality = quality, Width = width, Blur = blur, BackgroundColor = backgroundColor, ForegroundLayer = foregroundLayer, SupportedOutputFormats = outputFormats }; return await GetImageResult( options, cacheDuration, ImmutableDictionary.Empty) .ConfigureAwait(false); } /// /// Uploads a custom splashscreen. /// The body is expected to the image contents base64 encoded. /// /// A indicating success. /// Successfully uploaded new splashscreen. /// Error reading MimeType from uploaded image. /// User does not have permission to upload splashscreen.. /// Error reading the image format. [HttpPost("Branding/Splashscreen")] [Authorize(Policy = Policies.RequiresElevation)] [ProducesResponseType(StatusCodes.Status204NoContent)] [ProducesResponseType(StatusCodes.Status400BadRequest)] [ProducesResponseType(StatusCodes.Status403Forbidden)] [AcceptsImageFile] public async Task UploadCustomSplashscreen() { await using var memoryStream = await GetMemoryStream(Request.Body).ConfigureAwait(false); var mimeType = MediaTypeHeaderValue.Parse(Request.ContentType).MediaType; if (!mimeType.HasValue) { return BadRequest("Error reading mimetype from uploaded image"); } var extension = MimeTypes.ToExtension(mimeType.Value); if (string.IsNullOrEmpty(extension)) { return BadRequest("Error converting mimetype to an image extension"); } var filePath = Path.Combine(_appPaths.DataPath, "splashscreen-upload" + extension); var brandingOptions = _serverConfigurationManager.GetConfiguration("branding"); brandingOptions.SplashscreenLocation = filePath; _serverConfigurationManager.SaveConfiguration("branding", brandingOptions); await using (var fs = new FileStream(filePath, FileMode.Create, FileAccess.Write, FileShare.None, IODefaults.FileStreamBufferSize, FileOptions.Asynchronous)) { await memoryStream.CopyToAsync(fs, CancellationToken.None).ConfigureAwait(false); } return NoContent(); } /// /// Delete a custom splashscreen. /// /// A indicating success. /// Successfully deleted the custom splashscreen. /// User does not have permission to delete splashscreen.. [HttpDelete("Branding/Splashscreen")] [Authorize(Policy = Policies.RequiresElevation)] [ProducesResponseType(StatusCodes.Status204NoContent)] public ActionResult DeleteCustomSplashscreen() { var brandingOptions = _serverConfigurationManager.GetConfiguration("branding"); if (!string.IsNullOrEmpty(brandingOptions.SplashscreenLocation) && System.IO.File.Exists(brandingOptions.SplashscreenLocation)) { System.IO.File.Delete(brandingOptions.SplashscreenLocation); brandingOptions.SplashscreenLocation = null; _serverConfigurationManager.SaveConfiguration("branding", brandingOptions); } return NoContent(); } private static async Task GetMemoryStream(Stream inputStream) { using var reader = new StreamReader(inputStream); var text = await reader.ReadToEndAsync().ConfigureAwait(false); var bytes = Convert.FromBase64String(text); return new MemoryStream(bytes, 0, bytes.Length, false, true); } private ImageInfo? GetImageInfo(BaseItem item, ItemImageInfo info, int? imageIndex) { int? width = null; int? height = null; string? blurhash = null; long length = 0; try { if (info.IsLocalFile) { var fileInfo = _fileSystem.GetFileInfo(info.Path); length = fileInfo.Length; blurhash = info.BlurHash; width = info.Width; height = info.Height; if (width <= 0 || height <= 0) { width = null; height = null; } } } catch (Exception ex) { _logger.LogError(ex, "Error getting image information for {Item}", item.Name); } try { return new ImageInfo { Path = info.Path, ImageIndex = imageIndex, ImageType = info.Type, ImageTag = _imageProcessor.GetImageCacheTag(item, info), Size = length, BlurHash = blurhash, Width = width, Height = height }; } catch (Exception ex) { _logger.LogError(ex, "Error getting image information for {Path}", info.Path); return null; } } private async Task GetImageInternal( Guid itemId, ImageType imageType, int? imageIndex, string? tag, ImageFormat? format, int? maxWidth, int? maxHeight, double? percentPlayed, int? unplayedCount, int? width, int? height, int? quality, int? fillWidth, int? fillHeight, bool? addPlayedIndicator, int? blur, string? backgroundColor, string? foregroundLayer, BaseItem? item, ItemImageInfo? imageInfo = null) { if (percentPlayed.HasValue) { if (percentPlayed.Value <= 0) { percentPlayed = null; } else if (percentPlayed.Value >= 100) { percentPlayed = null; addPlayedIndicator = true; } } if (percentPlayed.HasValue) { unplayedCount = null; } if (unplayedCount.HasValue && unplayedCount.Value <= 0) { unplayedCount = null; } if (imageInfo is null) { imageInfo = item?.GetImageInfo(imageType, imageIndex ?? 0); if (imageInfo is null) { return NotFound(string.Format(NumberFormatInfo.InvariantInfo, "{0} does not have an image of type {1}", item?.Name, imageType)); } } var outputFormats = GetOutputFormats(format); TimeSpan? cacheDuration = null; if (!string.IsNullOrEmpty(tag)) { cacheDuration = TimeSpan.FromDays(365); } var responseHeaders = new Dictionary { { "transferMode.dlna.org", "Interactive" }, { "realTimeInfo.dlna.org", "DLNA.ORG_TLAG=*" } }; if (!imageInfo.IsLocalFile && item != null) { imageInfo = await _libraryManager.ConvertImageToLocal(item, imageInfo, imageIndex ?? 0).ConfigureAwait(false); } var options = new ImageProcessingOptions { Height = height, ImageIndex = imageIndex ?? 0, Image = imageInfo, Item = item, ItemId = itemId, MaxHeight = maxHeight, MaxWidth = maxWidth, FillHeight = fillHeight, FillWidth = fillWidth, Quality = quality ?? 100, Width = width, AddPlayedIndicator = addPlayedIndicator ?? false, PercentPlayed = percentPlayed ?? 0, UnplayedCount = unplayedCount, Blur = blur, BackgroundColor = backgroundColor, ForegroundLayer = foregroundLayer, SupportedOutputFormats = outputFormats }; return await GetImageResult( options, cacheDuration, responseHeaders).ConfigureAwait(false); } private ImageFormat[] GetOutputFormats(ImageFormat? format) { if (format.HasValue) { return new[] { format.Value }; } return GetClientSupportedFormats(); } private ImageFormat[] GetClientSupportedFormats() { var supportedFormats = Request.Headers.GetCommaSeparatedValues(HeaderNames.Accept); for (var i = 0; i < supportedFormats.Length; i++) { // Remove charsets etc. (anything after semi-colon) var type = supportedFormats[i]; int index = type.IndexOf(';', StringComparison.Ordinal); if (index != -1) { supportedFormats[i] = type.Substring(0, index); } } var acceptParam = Request.Query[HeaderNames.Accept]; var supportsWebP = SupportsFormat(supportedFormats, acceptParam, ImageFormat.Webp, false); if (!supportsWebP) { var userAgent = Request.Headers[HeaderNames.UserAgent].ToString(); if (userAgent.Contains("crosswalk", StringComparison.OrdinalIgnoreCase) && userAgent.Contains("android", StringComparison.OrdinalIgnoreCase)) { supportsWebP = true; } } var formats = new List(4); if (supportsWebP) { formats.Add(ImageFormat.Webp); } formats.Add(ImageFormat.Jpg); formats.Add(ImageFormat.Png); if (SupportsFormat(supportedFormats, acceptParam, ImageFormat.Gif, true)) { formats.Add(ImageFormat.Gif); } return formats.ToArray(); } private bool SupportsFormat(IReadOnlyCollection requestAcceptTypes, string acceptParam, ImageFormat format, bool acceptAll) { if (requestAcceptTypes.Contains(format.GetMimeType())) { return true; } if (acceptAll && requestAcceptTypes.Contains("*/*")) { return true; } // Review if this should be jpeg, jpg or both for ImageFormat.Jpg var normalized = format.ToString().ToLowerInvariant(); return string.Equals(acceptParam, normalized, StringComparison.OrdinalIgnoreCase); } private async Task GetImageResult( ImageProcessingOptions imageProcessingOptions, TimeSpan? cacheDuration, IDictionary headers) { var (imagePath, imageContentType, dateImageModified) = await _imageProcessor.ProcessImage(imageProcessingOptions).ConfigureAwait(false); var disableCaching = Request.Headers[HeaderNames.CacheControl].Contains("no-cache"); var parsingSuccessful = DateTime.TryParse(Request.Headers[HeaderNames.IfModifiedSince], out var ifModifiedSinceHeader); // if the parsing of the IfModifiedSince header was not successful, disable caching if (!parsingSuccessful) { // disableCaching = true; } foreach (var (key, value) in headers) { Response.Headers.Add(key, value); } Response.ContentType = imageContentType ?? MediaTypeNames.Text.Plain; Response.Headers.Add(HeaderNames.Age, Convert.ToInt64((DateTime.UtcNow - dateImageModified).TotalSeconds).ToString(CultureInfo.InvariantCulture)); Response.Headers.Add(HeaderNames.Vary, HeaderNames.Accept); if (disableCaching) { Response.Headers.Add(HeaderNames.CacheControl, "no-cache, no-store, must-revalidate"); Response.Headers.Add(HeaderNames.Pragma, "no-cache, no-store, must-revalidate"); } else { if (cacheDuration.HasValue) { Response.Headers.Add(HeaderNames.CacheControl, "public, max-age=" + cacheDuration.Value.TotalSeconds); } else { Response.Headers.Add(HeaderNames.CacheControl, "public"); } Response.Headers.Add(HeaderNames.LastModified, dateImageModified.ToUniversalTime().ToString("ddd, dd MMM yyyy HH:mm:ss \"GMT\"", CultureInfo.InvariantCulture)); // if the image was not modified since "ifModifiedSinceHeader"-header, return a HTTP status code 304 not modified if (!(dateImageModified > ifModifiedSinceHeader) && cacheDuration.HasValue) { if (ifModifiedSinceHeader.Add(cacheDuration.Value) < DateTime.UtcNow) { Response.StatusCode = StatusCodes.Status304NotModified; return new ContentResult(); } } } return PhysicalFile(imagePath, imageContentType ?? MediaTypeNames.Text.Plain); } } }