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]
[AcceptsImageFile]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
[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)
{
var user = _userManager.GetUserById(userId);
if (user is null)
{
return NotFound();
}
if (!RequestHelpers.AssertCanUpdateUser(_userManager, HttpContext.User, userId, true))
{
return StatusCode(StatusCodes.Status403Forbidden, "User is not allowed to update the image.");
}
if (!TryGetImageExtensionFromContentType(Request.ContentType, out string? extension))
{
return BadRequest("Incorrect ContentType.");
}
var memoryStream = await GetMemoryStream(Request.Body).ConfigureAwait(false);
await using (memoryStream.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 is not null)
{
await _userManager.ClearProfileImageAsync(user).ConfigureAwait(false);
}
user.ProfileImage = new Data.Entities.ImageInfo(Path.Combine(userDataPath, "profile" + extension));
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]
[AcceptsImageFile]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
[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)
{
var user = _userManager.GetUserById(userId);
if (user is null)
{
return NotFound();
}
if (!RequestHelpers.AssertCanUpdateUser(_userManager, HttpContext.User, userId, true))
{
return StatusCode(StatusCodes.Status403Forbidden, "User is not allowed to update the image.");
}
if (!TryGetImageExtensionFromContentType(Request.ContentType, out string? extension))
{
return BadRequest("Incorrect ContentType.");
}
var memoryStream = await GetMemoryStream(Request.Body).ConfigureAwait(false);
await using (memoryStream.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 is not null)
{
await _userManager.ClearProfileImageAsync(user).ConfigureAwait(false);
}
user.ProfileImage = new Data.Entities.ImageInfo(Path.Combine(userDataPath, "profile" + extension));
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]
[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]
[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.Status400BadRequest)]
[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();
}
if (!TryGetImageExtensionFromContentType(Request.ContentType, out _))
{
return BadRequest("Incorrect ContentType.");
}
var memoryStream = await GetMemoryStream(Request.Body).ConfigureAwait(false);
await using (memoryStream.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.Status400BadRequest)]
[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();
}
if (!TryGetImageExtensionFromContentType(Request.ContentType, out _))
{
return BadRequest("Incorrect ContentType.");
}
var memoryStream = await GetMemoryStream(Request.Body).ConfigureAwait(false);
await using (memoryStream.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]
[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 is not 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 is not 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. 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] 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,
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. 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] 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,
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. 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,
[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,
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. 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] 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,
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. 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] 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,
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. 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] 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,
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. 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] 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,
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. 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] 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,
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. 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] 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,
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. 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] 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,
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. 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] 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,
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. 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] 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,
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. 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] 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,
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. 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] 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,
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()
{
if (!TryGetImageExtensionFromContentType(Request.ContentType, out var extension))
{
return BadRequest("Incorrect ContentType.");
}
var memoryStream = await GetMemoryStream(Request.Body).ConfigureAwait(false);
await using (memoryStream.ConfigureAwait(false))
{
var filePath = Path.Combine(_appPaths.DataPath, "splashscreen-upload" + extension);
var brandingOptions = _serverConfigurationManager.GetConfiguration("branding");
brandingOptions.SplashscreenLocation = filePath;
_serverConfigurationManager.SaveConfiguration("branding", brandingOptions);
var fs = new FileStream(filePath, FileMode.Create, FileAccess.Write, FileShare.None, IODefaults.FileStreamBufferSize, FileOptions.Asynchronous);
await using (fs.ConfigureAwait(false))
{
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,
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;
}
}
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 is not 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,
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);
}
internal static bool TryGetImageExtensionFromContentType(string? contentType, [NotNullWhen(true)] out string? extension)
{
extension = null;
if (string.IsNullOrEmpty(contentType))
{
return false;
}
if (MediaTypeHeaderValue.TryParse(contentType, out var parsedValue)
&& parsedValue.MediaType.HasValue
&& MimeTypes.IsImage(parsedValue.MediaType.Value))
{
extension = MimeTypes.ToExtension(parsedValue.MediaType.Value);
return extension is not null;
}
return false;
}
}