aboutsummaryrefslogtreecommitdiff
path: root/Jellyfin.Api/Controllers/ImageController.cs
diff options
context:
space:
mode:
authorJoshua M. Boniface <joshua@boniface.me>2021-08-18 02:46:59 -0400
committerGitHub <noreply@github.com>2021-08-18 02:46:59 -0400
commit72d3f7020ad80ce1a53eeae8c5d57abeb22a4679 (patch)
treedd43e663838cdc7d99a4af565523df58ae23c856 /Jellyfin.Api/Controllers/ImageController.cs
parent7aef0fce444e6d8e06386553ec7ea1401a01bbb1 (diff)
parente5cbafdb6b47377052e0d638908ef96e30a997d6 (diff)
Merge branch 'master' into patch-2
Diffstat (limited to 'Jellyfin.Api/Controllers/ImageController.cs')
-rw-r--r--Jellyfin.Api/Controllers/ImageController.cs1050
1 files changed, 889 insertions, 161 deletions
diff --git a/Jellyfin.Api/Controllers/ImageController.cs b/Jellyfin.Api/Controllers/ImageController.cs
index ca9c2fa46..8f7500ac6 100644
--- a/Jellyfin.Api/Controllers/ImageController.cs
+++ b/Jellyfin.Api/Controllers/ImageController.cs
@@ -1,11 +1,14 @@
-using System;
+using System;
using System.Collections.Generic;
+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.Controller.Configuration;
@@ -83,20 +86,20 @@ namespace Jellyfin.Api.Controllers
/// <response code="403">User does not have permission to delete the image.</response>
/// <returns>A <see cref="NoContentResult"/>.</returns>
[HttpPost("Users/{userId}/Images/{imageType}")]
- [HttpPost("Users/{userId}/Images/{imageType}/{index?}", Name = "PostUserImage_2")]
[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<ActionResult> PostUserImage(
- [FromRoute] Guid userId,
- [FromRoute] ImageType imageType,
- [FromRoute] int? index = null)
+ [FromRoute, Required] Guid userId,
+ [FromRoute, Required] ImageType imageType,
+ [FromQuery] int? index = null)
{
if (!RequestHelpers.AssertCanUpdateUser(_authContext, HttpContext.Request, userId, true))
{
- return Forbid("User is not allowed to update the image.");
+ return StatusCode(StatusCodes.Status403Forbidden, "User is not allowed to update the image.");
}
var user = _userManager.GetUserById(userId);
@@ -107,10 +110,57 @@ namespace Jellyfin.Api.Controllers
var userDataPath = Path.Combine(_serverConfigurationManager.ApplicationPaths.UserConfigurationDirectoryPath, user.Username);
if (user.ProfileImage != null)
{
- _userManager.ClearProfileImage(user);
+ await _userManager.ClearProfileImageAsync(user).ConfigureAwait(false);
}
- user.ProfileImage = new Data.Entities.ImageInfo(Path.Combine(userDataPath, "profile" + MimeTypes.ToExtension(mimeType)));
+ 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();
+ }
+
+ /// <summary>
+ /// Sets the user image.
+ /// </summary>
+ /// <param name="userId">User Id.</param>
+ /// <param name="imageType">(Unused) Image type.</param>
+ /// <param name="index">(Unused) Image index.</param>
+ /// <response code="204">Image updated.</response>
+ /// <response code="403">User does not have permission to delete the image.</response>
+ /// <returns>A <see cref="NoContentResult"/>.</returns>
+ [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<ActionResult> PostUserImageByIndex(
+ [FromRoute, Required] Guid userId,
+ [FromRoute, Required] ImageType imageType,
+ [FromRoute] int index)
+ {
+ if (!RequestHelpers.AssertCanUpdateUser(_authContext, HttpContext.Request, 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)
@@ -129,24 +179,72 @@ namespace Jellyfin.Api.Controllers
/// <response code="204">Image deleted.</response>
/// <response code="403">User does not have permission to delete the image.</response>
/// <returns>A <see cref="NoContentResult"/>.</returns>
- [HttpDelete("Users/{userId}/Images/{itemType}")]
- [HttpDelete("Users/{userId}/Images/{itemType}/{index?}", Name = "DeleteUserImage_2")]
+ [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 ActionResult DeleteUserImage(
- [FromRoute] Guid userId,
- [FromRoute] ImageType imageType,
- [FromRoute] int? index = null)
+ public async Task<ActionResult> DeleteUserImage(
+ [FromRoute, Required] Guid userId,
+ [FromRoute, Required] ImageType imageType,
+ [FromQuery] int? index = null)
{
if (!RequestHelpers.AssertCanUpdateUser(_authContext, HttpContext.Request, userId, true))
{
- return Forbid("User is not allowed to delete the image.");
+ return StatusCode(StatusCodes.Status403Forbidden, "User is not allowed to delete the image.");
}
var user = _userManager.GetUserById(userId);
+ if (user?.ProfileImage == 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();
+ }
+
+ /// <summary>
+ /// Delete the user's image.
+ /// </summary>
+ /// <param name="userId">User Id.</param>
+ /// <param name="imageType">(Unused) Image type.</param>
+ /// <param name="index">(Unused) Image index.</param>
+ /// <response code="204">Image deleted.</response>
+ /// <response code="403">User does not have permission to delete the image.</response>
+ /// <returns>A <see cref="NoContentResult"/>.</returns>
+ [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<ActionResult> DeleteUserImageByIndex(
+ [FromRoute, Required] Guid userId,
+ [FromRoute, Required] ImageType imageType,
+ [FromRoute] int index)
+ {
+ if (!RequestHelpers.AssertCanUpdateUser(_authContext, HttpContext.Request, userId, true))
+ {
+ return StatusCode(StatusCodes.Status403Forbidden, "User is not allowed to delete the image.");
+ }
+
+ var user = _userManager.GetUserById(userId);
+ if (user?.ProfileImage == null)
+ {
+ return NoContent();
+ }
+
try
{
System.IO.File.Delete(user.ProfileImage.Path);
@@ -156,7 +254,7 @@ namespace Jellyfin.Api.Controllers
_logger.LogError(e, "Error deleting user profile image:");
}
- _userManager.ClearProfileImage(user);
+ await _userManager.ClearProfileImageAsync(user).ConfigureAwait(false);
return NoContent();
}
@@ -170,14 +268,13 @@ namespace Jellyfin.Api.Controllers
/// <response code="404">Item not found.</response>
/// <returns>A <see cref="NoContentResult"/> on success, or a <see cref="NotFoundResult"/> if item not found.</returns>
[HttpDelete("Items/{itemId}/Images/{imageType}")]
- [HttpDelete("Items/{itemId}/Images/{imageType}/{imageIndex?}", Name = "DeleteItemImage_2")]
[Authorize(Policy = Policies.RequiresElevation)]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<ActionResult> DeleteItemImage(
- [FromRoute] Guid itemId,
- [FromRoute] ImageType imageType,
- [FromRoute] int? imageIndex = null)
+ [FromRoute, Required] Guid itemId,
+ [FromRoute, Required] ImageType imageType,
+ [FromQuery] int? imageIndex)
{
var item = _libraryManager.GetItemById(itemId);
if (item == null)
@@ -190,24 +287,50 @@ namespace Jellyfin.Api.Controllers
}
/// <summary>
+ /// Delete an item's image.
+ /// </summary>
+ /// <param name="itemId">Item id.</param>
+ /// <param name="imageType">Image type.</param>
+ /// <param name="imageIndex">The image index.</param>
+ /// <response code="204">Image deleted.</response>
+ /// <response code="404">Item not found.</response>
+ /// <returns>A <see cref="NoContentResult"/> on success, or a <see cref="NotFoundResult"/> if item not found.</returns>
+ [HttpDelete("Items/{itemId}/Images/{imageType}/{imageIndex}")]
+ [Authorize(Policy = Policies.RequiresElevation)]
+ [ProducesResponseType(StatusCodes.Status204NoContent)]
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
+ public async Task<ActionResult> DeleteItemImageByIndex(
+ [FromRoute, Required] Guid itemId,
+ [FromRoute, Required] ImageType imageType,
+ [FromRoute] int imageIndex)
+ {
+ var item = _libraryManager.GetItemById(itemId);
+ if (item == null)
+ {
+ return NotFound();
+ }
+
+ await item.DeleteImageAsync(imageType, imageIndex).ConfigureAwait(false);
+ return NoContent();
+ }
+
+ /// <summary>
/// Set item image.
/// </summary>
/// <param name="itemId">Item id.</param>
/// <param name="imageType">Image type.</param>
- /// <param name="imageIndex">(Unused) Image index.</param>
/// <response code="204">Image saved.</response>
/// <response code="404">Item not found.</response>
/// <returns>A <see cref="NoContentResult"/> on success, or a <see cref="NotFoundResult"/> if item not found.</returns>
[HttpPost("Items/{itemId}/Images/{imageType}")]
- [HttpPost("Items/{itemId}/Images/{imageType}/{imageIndex?}", Name = "SetItemImage_2")]
[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<ActionResult> SetItemImage(
- [FromRoute] Guid itemId,
- [FromRoute] ImageType imageType,
- [FromRoute] int? imageIndex = null)
+ [FromRoute, Required] Guid itemId,
+ [FromRoute, Required] ImageType imageType)
{
var item = _libraryManager.GetItemById(itemId);
if (item == null)
@@ -215,9 +338,47 @@ namespace Jellyfin.Api.Controllers
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, Request.Body, mimeType, imageType, null, CancellationToken.None).ConfigureAwait(false);
+ await _providerManager.SaveImage(item, memoryStream, mimeType, imageType, null, CancellationToken.None).ConfigureAwait(false);
+ await item.UpdateToRepositoryAsync(ItemUpdateType.ImageUpdate, CancellationToken.None).ConfigureAwait(false);
+
+ return NoContent();
+ }
+
+ /// <summary>
+ /// Set item image.
+ /// </summary>
+ /// <param name="itemId">Item id.</param>
+ /// <param name="imageType">Image type.</param>
+ /// <param name="imageIndex">(Unused) Image index.</param>
+ /// <response code="204">Image saved.</response>
+ /// <response code="404">Item not found.</response>
+ /// <returns>A <see cref="NoContentResult"/> on success, or a <see cref="NotFoundResult"/> if item not found.</returns>
+ [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<ActionResult> SetItemImageByIndex(
+ [FromRoute, Required] Guid itemId,
+ [FromRoute, Required] ImageType imageType,
+ [FromRoute] int imageIndex)
+ {
+ var item = _libraryManager.GetItemById(itemId);
+ if (item == 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();
@@ -238,10 +399,10 @@ namespace Jellyfin.Api.Controllers
[ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<ActionResult> UpdateItemImageIndex(
- [FromRoute] Guid itemId,
- [FromRoute] ImageType imageType,
- [FromRoute] int imageIndex,
- [FromQuery] int newIndex)
+ [FromRoute, Required] Guid itemId,
+ [FromRoute, Required] ImageType imageType,
+ [FromRoute, Required] int imageIndex,
+ [FromQuery, Required] int newIndex)
{
var item = _libraryManager.GetItemById(itemId);
if (item == null)
@@ -264,7 +425,7 @@ namespace Jellyfin.Api.Controllers
[Authorize(Policy = Policies.DefaultAuthorization)]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
- public async Task<ActionResult<IEnumerable<ImageInfo>>> GetItemImageInfos([FromRoute] Guid itemId)
+ public async Task<ActionResult<IEnumerable<ImageInfo>>> GetItemImageInfos([FromRoute, Required] Guid itemId)
{
var item = _libraryManager.GetItemById(itemId);
if (item == null)
@@ -329,9 +490,11 @@ namespace Jellyfin.Api.Controllers
/// <param name="width">The fixed image width to return.</param>
/// <param name="height">The fixed image height to return.</param>
/// <param name="quality">Optional. Quality setting, from 0-100. Defaults to 90 and should suffice in most cases.</param>
+ /// <param name="fillWidth">Width of box to fill.</param>
+ /// <param name="fillHeight">Height of box to fill.</param>
/// <param name="tag">Optional. Supply the cache tag from the item object to receive strong caching headers.</param>
/// <param name="cropWhitespace">Optional. Specify if whitespace should be cropped out of the image. True/False. If unspecified, whitespace will be cropped from logos and clear art.</param>
- /// <param name="format">Determines the output format of the image - original,gif,jpg,png.</param>
+ /// <param name="format">Optional. The <see cref="ImageFormat"/> of the returned image.</param>
/// <param name="addPlayedIndicator">Optional. Add a played indicator.</param>
/// <param name="percentPlayed">Optional. Percent to render for the percent played overlay.</param>
/// <param name="unplayedCount">Optional. Unplayed count overlay to render.</param>
@@ -347,28 +510,29 @@ namespace Jellyfin.Api.Controllers
/// </returns>
[HttpGet("Items/{itemId}/Images/{imageType}")]
[HttpHead("Items/{itemId}/Images/{imageType}", Name = "HeadItemImage")]
- [HttpGet("Items/{itemId}/Images/{imageType}/{imageIndex?}", Name = "GetItemImage_2")]
- [HttpHead("Items/{itemId}/Images/{imageType}/{imageIndex?}", Name = "HeadItemImage_2")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
+ [ProducesImageFile]
public async Task<ActionResult> GetItemImage(
- [FromRoute] Guid itemId,
- [FromRoute] ImageType imageType,
- [FromRoute] int? maxWidth,
- [FromRoute] int? maxHeight,
+ [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] bool? cropWhitespace,
- [FromQuery] string? format,
+ [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,
- [FromRoute] int? imageIndex = null)
+ [FromQuery] int? imageIndex)
{
var item = _libraryManager.GetItemById(itemId);
if (item == null)
@@ -389,7 +553,8 @@ namespace Jellyfin.Api.Controllers
width,
height,
quality,
- cropWhitespace,
+ fillWidth,
+ fillHeight,
addPlayedIndicator,
blur,
backgroundColor,
@@ -404,11 +569,97 @@ namespace Jellyfin.Api.Controllers
/// </summary>
/// <param name="itemId">Item id.</param>
/// <param name="imageType">Image type.</param>
+ /// <param name="imageIndex">Image index.</param>
/// <param name="maxWidth">The maximum image width to return.</param>
/// <param name="maxHeight">The maximum image height to return.</param>
/// <param name="width">The fixed image width to return.</param>
/// <param name="height">The fixed image height to return.</param>
/// <param name="quality">Optional. Quality setting, from 0-100. Defaults to 90 and should suffice in most cases.</param>
+ /// <param name="fillWidth">Width of box to fill.</param>
+ /// <param name="fillHeight">Height of box to fill.</param>
+ /// <param name="tag">Optional. Supply the cache tag from the item object to receive strong caching headers.</param>
+ /// <param name="cropWhitespace">Optional. Specify if whitespace should be cropped out of the image. True/False. If unspecified, whitespace will be cropped from logos and clear art.</param>
+ /// <param name="format">Optional. The <see cref="ImageFormat"/> of the returned image.</param>
+ /// <param name="addPlayedIndicator">Optional. Add a played indicator.</param>
+ /// <param name="percentPlayed">Optional. Percent to render for the percent played overlay.</param>
+ /// <param name="unplayedCount">Optional. Unplayed count overlay to render.</param>
+ /// <param name="blur">Optional. Blur image.</param>
+ /// <param name="backgroundColor">Optional. Apply a background color for transparent images.</param>
+ /// <param name="foregroundLayer">Optional. Apply a foreground layer on top of the image.</param>
+ /// <response code="200">Image stream returned.</response>
+ /// <response code="404">Item not found.</response>
+ /// <returns>
+ /// A <see cref="FileStreamResult"/> containing the file stream on success,
+ /// or a <see cref="NotFoundResult"/> if item not found.
+ /// </returns>
+ [HttpGet("Items/{itemId}/Images/{imageType}/{imageIndex}")]
+ [HttpHead("Items/{itemId}/Images/{imageType}/{imageIndex}", Name = "HeadItemImageByIndex")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
+ [ProducesImageFile]
+ public async Task<ActionResult> 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 == 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,
+ Request.Method.Equals(HttpMethods.Head, StringComparison.OrdinalIgnoreCase))
+ .ConfigureAwait(false);
+ }
+
+ /// <summary>
+ /// Gets the item's image.
+ /// </summary>
+ /// <param name="itemId">Item id.</param>
+ /// <param name="imageType">Image type.</param>
+ /// <param name="maxWidth">The maximum image width to return.</param>
+ /// <param name="maxHeight">The maximum image height to return.</param>
+ /// <param name="width">The fixed image width to return.</param>
+ /// <param name="height">The fixed image height to return.</param>
+ /// <param name="quality">Optional. Quality setting, from 0-100. Defaults to 90 and should suffice in most cases.</param>
+ /// <param name="fillWidth">Width of box to fill.</param>
+ /// <param name="fillHeight">Height of box to fill.</param>
/// <param name="tag">Optional. Supply the cache tag from the item object to receive strong caching headers.</param>
/// <param name="cropWhitespace">Optional. Specify if whitespace should be cropped out of the image. True/False. If unspecified, whitespace will be cropped from logos and clear art.</param>
/// <param name="format">Determines the output format of the image - original,gif,jpg,png.</param>
@@ -429,24 +680,27 @@ namespace Jellyfin.Api.Controllers
[HttpHead("Items/{itemId}/Images/{imageType}/{imageIndex}/{tag}/{format}/{maxWidth}/{maxHeight}/{percentPlayed}/{unplayedCount}", Name = "HeadItemImage2")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
+ [ProducesImageFile]
public async Task<ActionResult> GetItemImage2(
- [FromRoute] Guid itemId,
- [FromRoute] ImageType imageType,
- [FromRoute] int? maxWidth,
- [FromRoute] int? maxHeight,
+ [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,
- [FromRoute] string tag,
- [FromQuery] bool? cropWhitespace,
- [FromRoute] string format,
+ [FromQuery] int? fillWidth,
+ [FromQuery] int? fillHeight,
+ [FromRoute, Required] string tag,
+ [FromQuery, ParameterObsolete] bool? cropWhitespace,
+ [FromRoute, Required] ImageFormat format,
[FromQuery] bool? addPlayedIndicator,
- [FromRoute] double? percentPlayed,
- [FromRoute] int? unplayedCount,
+ [FromRoute, Required] double percentPlayed,
+ [FromRoute, Required] int unplayedCount,
[FromQuery] int? blur,
[FromQuery] string? backgroundColor,
[FromQuery] string? foregroundLayer,
- [FromRoute] int? imageIndex = null)
+ [FromRoute, Required] int imageIndex)
{
var item = _libraryManager.GetItemById(itemId);
if (item == null)
@@ -467,7 +721,8 @@ namespace Jellyfin.Api.Controllers
width,
height,
quality,
- cropWhitespace,
+ fillWidth,
+ fillHeight,
addPlayedIndicator,
blur,
backgroundColor,
@@ -491,6 +746,8 @@ namespace Jellyfin.Api.Controllers
/// <param name="width">The fixed image width to return.</param>
/// <param name="height">The fixed image height to return.</param>
/// <param name="quality">Optional. Quality setting, from 0-100. Defaults to 90 and should suffice in most cases.</param>
+ /// <param name="fillWidth">Width of box to fill.</param>
+ /// <param name="fillHeight">Height of box to fill.</param>
/// <param name="cropWhitespace">Optional. Specify if whitespace should be cropped out of the image. True/False. If unspecified, whitespace will be cropped from logos and clear art.</param>
/// <param name="addPlayedIndicator">Optional. Add a played indicator.</param>
/// <param name="blur">Optional. Blur image.</param>
@@ -503,28 +760,31 @@ namespace Jellyfin.Api.Controllers
/// A <see cref="FileStreamResult"/> containing the file stream on success,
/// or a <see cref="NotFoundResult"/> if item not found.
/// </returns>
- [HttpGet("Artists/{name}/Images/{imageType}/{imageIndex?}")]
- [HttpHead("Artists/{name}/Images/{imageType}/{imageIndex?}", Name = "HeadArtistImage")]
+ [HttpGet("Artists/{name}/Images/{imageType}/{imageIndex}")]
+ [HttpHead("Artists/{name}/Images/{imageType}/{imageIndex}", Name = "HeadArtistImage")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
+ [ProducesImageFile]
public async Task<ActionResult> GetArtistImage(
- [FromRoute] string name,
- [FromRoute] ImageType imageType,
- [FromRoute] string tag,
- [FromRoute] string format,
- [FromRoute] int? maxWidth,
- [FromRoute] int? maxHeight,
- [FromRoute] double? percentPlayed,
- [FromRoute] int? unplayedCount,
+ [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] bool? cropWhitespace,
+ [FromQuery] int? fillWidth,
+ [FromQuery] int? fillHeight,
+ [FromQuery, ParameterObsolete] bool? cropWhitespace,
[FromQuery] bool? addPlayedIndicator,
[FromQuery] int? blur,
[FromQuery] string? backgroundColor,
[FromQuery] string? foregroundLayer,
- [FromRoute] int? imageIndex = null)
+ [FromRoute, Required] int imageIndex)
{
var item = _libraryManager.GetArtist(name);
if (item == null)
@@ -545,7 +805,8 @@ namespace Jellyfin.Api.Controllers
width,
height,
quality,
- cropWhitespace,
+ fillWidth,
+ fillHeight,
addPlayedIndicator,
blur,
backgroundColor,
@@ -569,6 +830,8 @@ namespace Jellyfin.Api.Controllers
/// <param name="width">The fixed image width to return.</param>
/// <param name="height">The fixed image height to return.</param>
/// <param name="quality">Optional. Quality setting, from 0-100. Defaults to 90 and should suffice in most cases.</param>
+ /// <param name="fillWidth">Width of box to fill.</param>
+ /// <param name="fillHeight">Height of box to fill.</param>
/// <param name="cropWhitespace">Optional. Specify if whitespace should be cropped out of the image. True/False. If unspecified, whitespace will be cropped from logos and clear art.</param>
/// <param name="addPlayedIndicator">Optional. Add a played indicator.</param>
/// <param name="blur">Optional. Blur image.</param>
@@ -581,28 +844,31 @@ namespace Jellyfin.Api.Controllers
/// A <see cref="FileStreamResult"/> containing the file stream on success,
/// or a <see cref="NotFoundResult"/> if item not found.
/// </returns>
- [HttpGet("Genres/{name}/Images/{imageType}/{imageIndex?}")]
- [HttpHead("Genres/{name}/Images/{imageType}/{imageIndex?}", Name = "HeadGenreImage")]
+ [HttpGet("Genres/{name}/Images/{imageType}")]
+ [HttpHead("Genres/{name}/Images/{imageType}", Name = "HeadGenreImage")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
+ [ProducesImageFile]
public async Task<ActionResult> GetGenreImage(
- [FromRoute] string name,
- [FromRoute] ImageType imageType,
- [FromRoute] string tag,
- [FromRoute] string format,
- [FromRoute] int? maxWidth,
- [FromRoute] int? maxHeight,
- [FromRoute] double? percentPlayed,
- [FromRoute] int? unplayedCount,
+ [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] bool? cropWhitespace,
+ [FromQuery] int? fillWidth,
+ [FromQuery] int? fillHeight,
+ [FromQuery, ParameterObsolete] bool? cropWhitespace,
[FromQuery] bool? addPlayedIndicator,
[FromQuery] int? blur,
[FromQuery] string? backgroundColor,
[FromQuery] string? foregroundLayer,
- [FromRoute] int? imageIndex = null)
+ [FromQuery] int? imageIndex)
{
var item = _libraryManager.GetGenre(name);
if (item == null)
@@ -623,7 +889,92 @@ namespace Jellyfin.Api.Controllers
width,
height,
quality,
- cropWhitespace,
+ fillWidth,
+ fillHeight,
+ addPlayedIndicator,
+ blur,
+ backgroundColor,
+ foregroundLayer,
+ item,
+ Request.Method.Equals(HttpMethods.Head, StringComparison.OrdinalIgnoreCase))
+ .ConfigureAwait(false);
+ }
+
+ /// <summary>
+ /// Get genre image by name.
+ /// </summary>
+ /// <param name="name">Genre name.</param>
+ /// <param name="imageType">Image type.</param>
+ /// <param name="imageIndex">Image index.</param>
+ /// <param name="tag">Optional. Supply the cache tag from the item object to receive strong caching headers.</param>
+ /// <param name="format">Determines the output format of the image - original,gif,jpg,png.</param>
+ /// <param name="maxWidth">The maximum image width to return.</param>
+ /// <param name="maxHeight">The maximum image height to return.</param>
+ /// <param name="percentPlayed">Optional. Percent to render for the percent played overlay.</param>
+ /// <param name="unplayedCount">Optional. Unplayed count overlay to render.</param>
+ /// <param name="width">The fixed image width to return.</param>
+ /// <param name="height">The fixed image height to return.</param>
+ /// <param name="quality">Optional. Quality setting, from 0-100. Defaults to 90 and should suffice in most cases.</param>
+ /// <param name="fillWidth">Width of box to fill.</param>
+ /// <param name="fillHeight">Height of box to fill.</param>
+ /// <param name="cropWhitespace">Optional. Specify if whitespace should be cropped out of the image. True/False. If unspecified, whitespace will be cropped from logos and clear art.</param>
+ /// <param name="addPlayedIndicator">Optional. Add a played indicator.</param>
+ /// <param name="blur">Optional. Blur image.</param>
+ /// <param name="backgroundColor">Optional. Apply a background color for transparent images.</param>
+ /// <param name="foregroundLayer">Optional. Apply a foreground layer on top of the image.</param>
+ /// <response code="200">Image stream returned.</response>
+ /// <response code="404">Item not found.</response>
+ /// <returns>
+ /// A <see cref="FileStreamResult"/> containing the file stream on success,
+ /// or a <see cref="NotFoundResult"/> if item not found.
+ /// </returns>
+ [HttpGet("Genres/{name}/Images/{imageType}/{imageIndex}")]
+ [HttpHead("Genres/{name}/Images/{imageType}/{imageIndex}", Name = "HeadGenreImageByIndex")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
+ [ProducesImageFile]
+ public async Task<ActionResult> 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 == null)
+ {
+ return NotFound();
+ }
+
+ return await GetImageInternal(
+ item.Id,
+ imageType,
+ imageIndex,
+ tag,
+ format,
+ maxWidth,
+ maxHeight,
+ percentPlayed,
+ unplayedCount,
+ width,
+ height,
+ quality,
+ fillWidth,
+ fillHeight,
addPlayedIndicator,
blur,
backgroundColor,
@@ -647,6 +998,8 @@ namespace Jellyfin.Api.Controllers
/// <param name="width">The fixed image width to return.</param>
/// <param name="height">The fixed image height to return.</param>
/// <param name="quality">Optional. Quality setting, from 0-100. Defaults to 90 and should suffice in most cases.</param>
+ /// <param name="fillWidth">Width of box to fill.</param>
+ /// <param name="fillHeight">Height of box to fill.</param>
/// <param name="cropWhitespace">Optional. Specify if whitespace should be cropped out of the image. True/False. If unspecified, whitespace will be cropped from logos and clear art.</param>
/// <param name="addPlayedIndicator">Optional. Add a played indicator.</param>
/// <param name="blur">Optional. Blur image.</param>
@@ -659,28 +1012,31 @@ namespace Jellyfin.Api.Controllers
/// A <see cref="FileStreamResult"/> containing the file stream on success,
/// or a <see cref="NotFoundResult"/> if item not found.
/// </returns>
- [HttpGet("MusicGenres/{name}/Images/{imageType}/{imageIndex?}")]
- [HttpHead("MusicGenres/{name}/Images/{imageType}/{imageIndex?}", Name = "HeadMusicGenreImage")]
+ [HttpGet("MusicGenres/{name}/Images/{imageType}")]
+ [HttpHead("MusicGenres/{name}/Images/{imageType}", Name = "HeadMusicGenreImage")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
+ [ProducesImageFile]
public async Task<ActionResult> GetMusicGenreImage(
- [FromRoute] string name,
- [FromRoute] ImageType imageType,
- [FromRoute] string tag,
- [FromRoute] string format,
- [FromRoute] int? maxWidth,
- [FromRoute] int? maxHeight,
- [FromRoute] double? percentPlayed,
- [FromRoute] int? unplayedCount,
+ [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] bool? cropWhitespace,
+ [FromQuery] int? fillWidth,
+ [FromQuery] int? fillHeight,
+ [FromQuery, ParameterObsolete] bool? cropWhitespace,
[FromQuery] bool? addPlayedIndicator,
[FromQuery] int? blur,
[FromQuery] string? backgroundColor,
[FromQuery] string? foregroundLayer,
- [FromRoute] int? imageIndex = null)
+ [FromQuery] int? imageIndex)
{
var item = _libraryManager.GetMusicGenre(name);
if (item == null)
@@ -701,7 +1057,92 @@ namespace Jellyfin.Api.Controllers
width,
height,
quality,
- cropWhitespace,
+ fillWidth,
+ fillHeight,
+ addPlayedIndicator,
+ blur,
+ backgroundColor,
+ foregroundLayer,
+ item,
+ Request.Method.Equals(HttpMethods.Head, StringComparison.OrdinalIgnoreCase))
+ .ConfigureAwait(false);
+ }
+
+ /// <summary>
+ /// Get music genre image by name.
+ /// </summary>
+ /// <param name="name">Music genre name.</param>
+ /// <param name="imageType">Image type.</param>
+ /// <param name="imageIndex">Image index.</param>
+ /// <param name="tag">Optional. Supply the cache tag from the item object to receive strong caching headers.</param>
+ /// <param name="format">Determines the output format of the image - original,gif,jpg,png.</param>
+ /// <param name="maxWidth">The maximum image width to return.</param>
+ /// <param name="maxHeight">The maximum image height to return.</param>
+ /// <param name="percentPlayed">Optional. Percent to render for the percent played overlay.</param>
+ /// <param name="unplayedCount">Optional. Unplayed count overlay to render.</param>
+ /// <param name="width">The fixed image width to return.</param>
+ /// <param name="height">The fixed image height to return.</param>
+ /// <param name="quality">Optional. Quality setting, from 0-100. Defaults to 90 and should suffice in most cases.</param>
+ /// <param name="fillWidth">Width of box to fill.</param>
+ /// <param name="fillHeight">Height of box to fill.</param>
+ /// <param name="cropWhitespace">Optional. Specify if whitespace should be cropped out of the image. True/False. If unspecified, whitespace will be cropped from logos and clear art.</param>
+ /// <param name="addPlayedIndicator">Optional. Add a played indicator.</param>
+ /// <param name="blur">Optional. Blur image.</param>
+ /// <param name="backgroundColor">Optional. Apply a background color for transparent images.</param>
+ /// <param name="foregroundLayer">Optional. Apply a foreground layer on top of the image.</param>
+ /// <response code="200">Image stream returned.</response>
+ /// <response code="404">Item not found.</response>
+ /// <returns>
+ /// A <see cref="FileStreamResult"/> containing the file stream on success,
+ /// or a <see cref="NotFoundResult"/> if item not found.
+ /// </returns>
+ [HttpGet("MusicGenres/{name}/Images/{imageType}/{imageIndex}")]
+ [HttpHead("MusicGenres/{name}/Images/{imageType}/{imageIndex}", Name = "HeadMusicGenreImageByIndex")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
+ [ProducesImageFile]
+ public async Task<ActionResult> 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 == null)
+ {
+ return NotFound();
+ }
+
+ return await GetImageInternal(
+ item.Id,
+ imageType,
+ imageIndex,
+ tag,
+ format,
+ maxWidth,
+ maxHeight,
+ percentPlayed,
+ unplayedCount,
+ width,
+ height,
+ quality,
+ fillWidth,
+ fillHeight,
addPlayedIndicator,
blur,
backgroundColor,
@@ -725,6 +1166,8 @@ namespace Jellyfin.Api.Controllers
/// <param name="width">The fixed image width to return.</param>
/// <param name="height">The fixed image height to return.</param>
/// <param name="quality">Optional. Quality setting, from 0-100. Defaults to 90 and should suffice in most cases.</param>
+ /// <param name="fillWidth">Width of box to fill.</param>
+ /// <param name="fillHeight">Height of box to fill.</param>
/// <param name="cropWhitespace">Optional. Specify if whitespace should be cropped out of the image. True/False. If unspecified, whitespace will be cropped from logos and clear art.</param>
/// <param name="addPlayedIndicator">Optional. Add a played indicator.</param>
/// <param name="blur">Optional. Blur image.</param>
@@ -737,28 +1180,31 @@ namespace Jellyfin.Api.Controllers
/// A <see cref="FileStreamResult"/> containing the file stream on success,
/// or a <see cref="NotFoundResult"/> if item not found.
/// </returns>
- [HttpGet("Persons/{name}/Images/{imageType}/{imageIndex?}")]
- [HttpHead("Persons/{name}/Images/{imageType}/{imageIndex?}", Name = "HeadPersonImage")]
+ [HttpGet("Persons/{name}/Images/{imageType}")]
+ [HttpHead("Persons/{name}/Images/{imageType}", Name = "HeadPersonImage")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
+ [ProducesImageFile]
public async Task<ActionResult> GetPersonImage(
- [FromRoute] string name,
- [FromRoute] ImageType imageType,
- [FromRoute] string tag,
- [FromRoute] string format,
- [FromRoute] int? maxWidth,
- [FromRoute] int? maxHeight,
- [FromRoute] double? percentPlayed,
- [FromRoute] int? unplayedCount,
+ [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] bool? cropWhitespace,
+ [FromQuery] int? fillWidth,
+ [FromQuery] int? fillHeight,
+ [FromQuery, ParameterObsolete] bool? cropWhitespace,
[FromQuery] bool? addPlayedIndicator,
[FromQuery] int? blur,
[FromQuery] string? backgroundColor,
[FromQuery] string? foregroundLayer,
- [FromRoute] int? imageIndex = null)
+ [FromQuery] int? imageIndex)
{
var item = _libraryManager.GetPerson(name);
if (item == null)
@@ -779,7 +1225,92 @@ namespace Jellyfin.Api.Controllers
width,
height,
quality,
- cropWhitespace,
+ fillWidth,
+ fillHeight,
+ addPlayedIndicator,
+ blur,
+ backgroundColor,
+ foregroundLayer,
+ item,
+ Request.Method.Equals(HttpMethods.Head, StringComparison.OrdinalIgnoreCase))
+ .ConfigureAwait(false);
+ }
+
+ /// <summary>
+ /// Get person image by name.
+ /// </summary>
+ /// <param name="name">Person name.</param>
+ /// <param name="imageType">Image type.</param>
+ /// <param name="imageIndex">Image index.</param>
+ /// <param name="tag">Optional. Supply the cache tag from the item object to receive strong caching headers.</param>
+ /// <param name="format">Determines the output format of the image - original,gif,jpg,png.</param>
+ /// <param name="maxWidth">The maximum image width to return.</param>
+ /// <param name="maxHeight">The maximum image height to return.</param>
+ /// <param name="percentPlayed">Optional. Percent to render for the percent played overlay.</param>
+ /// <param name="unplayedCount">Optional. Unplayed count overlay to render.</param>
+ /// <param name="width">The fixed image width to return.</param>
+ /// <param name="height">The fixed image height to return.</param>
+ /// <param name="quality">Optional. Quality setting, from 0-100. Defaults to 90 and should suffice in most cases.</param>
+ /// <param name="fillWidth">Width of box to fill.</param>
+ /// <param name="fillHeight">Height of box to fill.</param>
+ /// <param name="cropWhitespace">Optional. Specify if whitespace should be cropped out of the image. True/False. If unspecified, whitespace will be cropped from logos and clear art.</param>
+ /// <param name="addPlayedIndicator">Optional. Add a played indicator.</param>
+ /// <param name="blur">Optional. Blur image.</param>
+ /// <param name="backgroundColor">Optional. Apply a background color for transparent images.</param>
+ /// <param name="foregroundLayer">Optional. Apply a foreground layer on top of the image.</param>
+ /// <response code="200">Image stream returned.</response>
+ /// <response code="404">Item not found.</response>
+ /// <returns>
+ /// A <see cref="FileStreamResult"/> containing the file stream on success,
+ /// or a <see cref="NotFoundResult"/> if item not found.
+ /// </returns>
+ [HttpGet("Persons/{name}/Images/{imageType}/{imageIndex}")]
+ [HttpHead("Persons/{name}/Images/{imageType}/{imageIndex}", Name = "HeadPersonImageByIndex")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
+ [ProducesImageFile]
+ public async Task<ActionResult> 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 == null)
+ {
+ return NotFound();
+ }
+
+ return await GetImageInternal(
+ item.Id,
+ imageType,
+ imageIndex,
+ tag,
+ format,
+ maxWidth,
+ maxHeight,
+ percentPlayed,
+ unplayedCount,
+ width,
+ height,
+ quality,
+ fillWidth,
+ fillHeight,
addPlayedIndicator,
blur,
backgroundColor,
@@ -803,6 +1334,8 @@ namespace Jellyfin.Api.Controllers
/// <param name="width">The fixed image width to return.</param>
/// <param name="height">The fixed image height to return.</param>
/// <param name="quality">Optional. Quality setting, from 0-100. Defaults to 90 and should suffice in most cases.</param>
+ /// <param name="fillWidth">Width of box to fill.</param>
+ /// <param name="fillHeight">Height of box to fill.</param>
/// <param name="cropWhitespace">Optional. Specify if whitespace should be cropped out of the image. True/False. If unspecified, whitespace will be cropped from logos and clear art.</param>
/// <param name="addPlayedIndicator">Optional. Add a played indicator.</param>
/// <param name="blur">Optional. Blur image.</param>
@@ -815,28 +1348,115 @@ namespace Jellyfin.Api.Controllers
/// A <see cref="FileStreamResult"/> containing the file stream on success,
/// or a <see cref="NotFoundResult"/> if item not found.
/// </returns>
- [HttpGet("Studios/{name}/Images/{imageType}/{imageIndex?}")]
- [HttpHead("Studios/{name}/Images/{imageType}/{imageIndex?}", Name = "HeadStudioImage")]
+ [HttpGet("Studios/{name}/Images/{imageType}")]
+ [HttpHead("Studios/{name}/Images/{imageType}", Name = "HeadStudioImage")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
+ [ProducesImageFile]
public async Task<ActionResult> GetStudioImage(
- [FromRoute] string name,
- [FromRoute] ImageType imageType,
- [FromRoute] string tag,
- [FromRoute] string format,
- [FromRoute] int? maxWidth,
- [FromRoute] int? maxHeight,
- [FromRoute] double? percentPlayed,
- [FromRoute] int? unplayedCount,
+ [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] bool? cropWhitespace,
+ [FromQuery] int? fillWidth,
+ [FromQuery] int? fillHeight,
+ [FromQuery, ParameterObsolete] bool? cropWhitespace,
[FromQuery] bool? addPlayedIndicator,
[FromQuery] int? blur,
[FromQuery] string? backgroundColor,
[FromQuery] string? foregroundLayer,
- [FromRoute] int? imageIndex = null)
+ [FromQuery] int? imageIndex)
+ {
+ var item = _libraryManager.GetStudio(name);
+ if (item == 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,
+ Request.Method.Equals(HttpMethods.Head, StringComparison.OrdinalIgnoreCase))
+ .ConfigureAwait(false);
+ }
+
+ /// <summary>
+ /// Get studio image by name.
+ /// </summary>
+ /// <param name="name">Studio name.</param>
+ /// <param name="imageType">Image type.</param>
+ /// <param name="imageIndex">Image index.</param>
+ /// <param name="tag">Optional. Supply the cache tag from the item object to receive strong caching headers.</param>
+ /// <param name="format">Determines the output format of the image - original,gif,jpg,png.</param>
+ /// <param name="maxWidth">The maximum image width to return.</param>
+ /// <param name="maxHeight">The maximum image height to return.</param>
+ /// <param name="percentPlayed">Optional. Percent to render for the percent played overlay.</param>
+ /// <param name="unplayedCount">Optional. Unplayed count overlay to render.</param>
+ /// <param name="width">The fixed image width to return.</param>
+ /// <param name="height">The fixed image height to return.</param>
+ /// <param name="quality">Optional. Quality setting, from 0-100. Defaults to 90 and should suffice in most cases.</param>
+ /// <param name="fillWidth">Width of box to fill.</param>
+ /// <param name="fillHeight">Height of box to fill.</param>
+ /// <param name="cropWhitespace">Optional. Specify if whitespace should be cropped out of the image. True/False. If unspecified, whitespace will be cropped from logos and clear art.</param>
+ /// <param name="addPlayedIndicator">Optional. Add a played indicator.</param>
+ /// <param name="blur">Optional. Blur image.</param>
+ /// <param name="backgroundColor">Optional. Apply a background color for transparent images.</param>
+ /// <param name="foregroundLayer">Optional. Apply a foreground layer on top of the image.</param>
+ /// <response code="200">Image stream returned.</response>
+ /// <response code="404">Item not found.</response>
+ /// <returns>
+ /// A <see cref="FileStreamResult"/> containing the file stream on success,
+ /// or a <see cref="NotFoundResult"/> if item not found.
+ /// </returns>
+ [HttpGet("Studios/{name}/Images/{imageType}/{imageIndex}")]
+ [HttpHead("Studios/{name}/Images/{imageType}/{imageIndex}", Name = "HeadStudioImageByIndex")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
+ [ProducesImageFile]
+ public async Task<ActionResult> 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 == null)
@@ -857,7 +1477,8 @@ namespace Jellyfin.Api.Controllers
width,
height,
quality,
- cropWhitespace,
+ fillWidth,
+ fillHeight,
addPlayedIndicator,
blur,
backgroundColor,
@@ -881,6 +1502,8 @@ namespace Jellyfin.Api.Controllers
/// <param name="width">The fixed image width to return.</param>
/// <param name="height">The fixed image height to return.</param>
/// <param name="quality">Optional. Quality setting, from 0-100. Defaults to 90 and should suffice in most cases.</param>
+ /// <param name="fillWidth">Width of box to fill.</param>
+ /// <param name="fillHeight">Height of box to fill.</param>
/// <param name="cropWhitespace">Optional. Specify if whitespace should be cropped out of the image. True/False. If unspecified, whitespace will be cropped from logos and clear art.</param>
/// <param name="addPlayedIndicator">Optional. Add a played indicator.</param>
/// <param name="blur">Optional. Blur image.</param>
@@ -893,15 +1516,16 @@ namespace Jellyfin.Api.Controllers
/// A <see cref="FileStreamResult"/> containing the file stream on success,
/// or a <see cref="NotFoundResult"/> if item not found.
/// </returns>
- [HttpGet("Users/{userId}/Images/{imageType}/{imageIndex?}")]
- [HttpHead("Users/{userId}/Images/{imageType}/{imageIndex?}", Name = "HeadUserImage")]
+ [HttpGet("Users/{userId}/Images/{imageType}")]
+ [HttpHead("Users/{userId}/Images/{imageType}", Name = "HeadUserImage")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
+ [ProducesImageFile]
public async Task<ActionResult> GetUserImage(
- [FromRoute] Guid userId,
- [FromRoute] ImageType imageType,
+ [FromRoute, Required] Guid userId,
+ [FromRoute, Required] ImageType imageType,
[FromQuery] string? tag,
- [FromQuery] string? format,
+ [FromQuery] ImageFormat? format,
[FromQuery] int? maxWidth,
[FromQuery] int? maxHeight,
[FromQuery] double? percentPlayed,
@@ -909,15 +1533,17 @@ namespace Jellyfin.Api.Controllers
[FromQuery] int? width,
[FromQuery] int? height,
[FromQuery] int? quality,
- [FromQuery] bool? cropWhitespace,
+ [FromQuery] int? fillWidth,
+ [FromQuery] int? fillHeight,
+ [FromQuery, ParameterObsolete] bool? cropWhitespace,
[FromQuery] bool? addPlayedIndicator,
[FromQuery] int? blur,
[FromQuery] string? backgroundColor,
[FromQuery] string? foregroundLayer,
- [FromRoute] int? imageIndex = null)
+ [FromQuery] int? imageIndex)
{
var user = _userManager.GetUserById(userId);
- if (user == null)
+ if (user?.ProfileImage == null)
{
return NotFound();
}
@@ -952,7 +1578,110 @@ namespace Jellyfin.Api.Controllers
width,
height,
quality,
- cropWhitespace,
+ fillWidth,
+ fillHeight,
+ addPlayedIndicator,
+ blur,
+ backgroundColor,
+ foregroundLayer,
+ null,
+ Request.Method.Equals(HttpMethods.Head, StringComparison.OrdinalIgnoreCase),
+ info)
+ .ConfigureAwait(false);
+ }
+
+ /// <summary>
+ /// Get user profile image.
+ /// </summary>
+ /// <param name="userId">User id.</param>
+ /// <param name="imageType">Image type.</param>
+ /// <param name="imageIndex">Image index.</param>
+ /// <param name="tag">Optional. Supply the cache tag from the item object to receive strong caching headers.</param>
+ /// <param name="format">Determines the output format of the image - original,gif,jpg,png.</param>
+ /// <param name="maxWidth">The maximum image width to return.</param>
+ /// <param name="maxHeight">The maximum image height to return.</param>
+ /// <param name="percentPlayed">Optional. Percent to render for the percent played overlay.</param>
+ /// <param name="unplayedCount">Optional. Unplayed count overlay to render.</param>
+ /// <param name="width">The fixed image width to return.</param>
+ /// <param name="height">The fixed image height to return.</param>
+ /// <param name="quality">Optional. Quality setting, from 0-100. Defaults to 90 and should suffice in most cases.</param>
+ /// <param name="fillWidth">Width of box to fill.</param>
+ /// <param name="fillHeight">Height of box to fill.</param>
+ /// <param name="cropWhitespace">Optional. Specify if whitespace should be cropped out of the image. True/False. If unspecified, whitespace will be cropped from logos and clear art.</param>
+ /// <param name="addPlayedIndicator">Optional. Add a played indicator.</param>
+ /// <param name="blur">Optional. Blur image.</param>
+ /// <param name="backgroundColor">Optional. Apply a background color for transparent images.</param>
+ /// <param name="foregroundLayer">Optional. Apply a foreground layer on top of the image.</param>
+ /// <response code="200">Image stream returned.</response>
+ /// <response code="404">Item not found.</response>
+ /// <returns>
+ /// A <see cref="FileStreamResult"/> containing the file stream on success,
+ /// or a <see cref="NotFoundResult"/> if item not found.
+ /// </returns>
+ [HttpGet("Users/{userId}/Images/{imageType}/{imageIndex}")]
+ [HttpHead("Users/{userId}/Images/{imageType}/{imageIndex}", Name = "HeadUserImageByIndex")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
+ [ProducesImageFile]
+ public async Task<ActionResult> 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 == 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,
@@ -1028,7 +1757,7 @@ namespace Jellyfin.Api.Controllers
ImageType imageType,
int? imageIndex,
string? tag,
- string? format,
+ ImageFormat? format,
int? maxWidth,
int? maxHeight,
double? percentPlayed,
@@ -1036,7 +1765,8 @@ namespace Jellyfin.Api.Controllers
int? width,
int? height,
int? quality,
- bool? cropWhitespace,
+ int? fillWidth,
+ int? fillHeight,
bool? addPlayedIndicator,
int? blur,
string? backgroundColor,
@@ -1078,8 +1808,6 @@ namespace Jellyfin.Api.Controllers
}
}
- cropWhitespace ??= imageType == ImageType.Logo || imageType == ImageType.Art;
-
var outputFormats = GetOutputFormats(format);
TimeSpan? cacheDuration = null;
@@ -1099,11 +1827,13 @@ namespace Jellyfin.Api.Controllers
item,
itemId,
imageIndex,
+ width,
height,
- maxHeight,
maxWidth,
+ maxHeight,
+ fillWidth,
+ fillHeight,
quality,
- width,
addPlayedIndicator,
percentPlayed,
unplayedCount,
@@ -1111,19 +1841,17 @@ namespace Jellyfin.Api.Controllers
backgroundColor,
foregroundLayer,
imageInfo,
- cropWhitespace.Value,
outputFormats,
cacheDuration,
responseHeaders,
isHeadRequest).ConfigureAwait(false);
}
- private ImageFormat[] GetOutputFormats(string? format)
+ private ImageFormat[] GetOutputFormats(ImageFormat? format)
{
- if (!string.IsNullOrWhiteSpace(format)
- && Enum.TryParse(format, true, out ImageFormat parsedFormat))
+ if (format.HasValue)
{
- return new[] { parsedFormat };
+ return new[] { format.Value };
}
return GetClientSupportedFormats();
@@ -1131,23 +1859,21 @@ namespace Jellyfin.Api.Controllers
private ImageFormat[] GetClientSupportedFormats()
{
- var acceptTypes = Request.Headers[HeaderNames.Accept];
- var supportedFormats = new List<string>();
- if (acceptTypes.Count > 0)
+ var supportedFormats = Request.Headers.GetCommaSeparatedValues(HeaderNames.Accept);
+ for (var i = 0; i < supportedFormats.Length; i++)
{
- foreach (var type in acceptTypes)
+ // Remove charsets etc. (anything after semi-colon)
+ var type = supportedFormats[i];
+ int index = type.IndexOf(';', StringComparison.Ordinal);
+ if (index != -1)
{
- int index = type.IndexOf(';', StringComparison.Ordinal);
- if (index != -1)
- {
- supportedFormats.Add(type.Substring(0, index));
- }
+ supportedFormats[i] = type.Substring(0, index);
}
}
var acceptParam = Request.Query[HeaderNames.Accept];
- var supportsWebP = SupportsFormat(supportedFormats, acceptParam, "webp", false);
+ var supportsWebP = SupportsFormat(supportedFormats, acceptParam, ImageFormat.Webp, false);
if (!supportsWebP)
{
@@ -1169,7 +1895,7 @@ namespace Jellyfin.Api.Controllers
formats.Add(ImageFormat.Jpg);
formats.Add(ImageFormat.Png);
- if (SupportsFormat(supportedFormats, acceptParam, "gif", true))
+ if (SupportsFormat(supportedFormats, acceptParam, ImageFormat.Gif, true))
{
formats.Add(ImageFormat.Gif);
}
@@ -1177,9 +1903,10 @@ namespace Jellyfin.Api.Controllers
return formats.ToArray();
}
- private bool SupportsFormat(IReadOnlyCollection<string> requestAcceptTypes, string acceptParam, string format, bool acceptAll)
+ private bool SupportsFormat(IReadOnlyCollection<string> requestAcceptTypes, string acceptParam, ImageFormat format, bool acceptAll)
{
- var mimeType = "image/" + format;
+ var normalized = format.ToString().ToLowerInvariant();
+ var mimeType = "image/" + normalized;
if (requestAcceptTypes.Contains(mimeType))
{
@@ -1191,18 +1918,20 @@ namespace Jellyfin.Api.Controllers
return true;
}
- return string.Equals(acceptParam, format, StringComparison.OrdinalIgnoreCase);
+ return string.Equals(acceptParam, normalized, StringComparison.OrdinalIgnoreCase);
}
private async Task<ActionResult> GetImageResult(
BaseItem? item,
Guid itemId,
int? index,
+ int? width,
int? height,
- int? maxHeight,
int? maxWidth,
+ int? maxHeight,
+ int? fillWidth,
+ int? fillHeight,
int? quality,
- int? width,
bool? addPlayedIndicator,
double? percentPlayed,
int? unplayedCount,
@@ -1210,7 +1939,6 @@ namespace Jellyfin.Api.Controllers
string? backgroundColor,
string? foregroundLayer,
ItemImageInfo imageInfo,
- bool cropWhitespace,
IReadOnlyCollection<ImageFormat> supportedFormats,
TimeSpan? cacheDuration,
IDictionary<string, string> headers,
@@ -1223,7 +1951,6 @@ namespace Jellyfin.Api.Controllers
var options = new ImageProcessingOptions
{
- CropWhiteSpace = cropWhitespace,
Height = height,
ImageIndex = index ?? 0,
Image = imageInfo,
@@ -1231,6 +1958,8 @@ namespace Jellyfin.Api.Controllers
ItemId = itemId,
MaxHeight = maxHeight,
MaxWidth = maxWidth,
+ FillHeight = fillHeight,
+ FillWidth = fillWidth,
Quality = quality ?? 100,
Width = width,
AddPlayedIndicator = addPlayedIndicator ?? false,
@@ -1258,7 +1987,7 @@ namespace Jellyfin.Api.Controllers
Response.Headers.Add(key, value);
}
- Response.ContentType = imageContentType;
+ 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);
@@ -1281,9 +2010,9 @@ namespace Jellyfin.Api.Controllers
Response.Headers.Add(HeaderNames.LastModified, dateImageModified.ToUniversalTime().ToString("ddd, dd MMM yyyy HH:mm:ss \"GMT\"", new CultureInfo("en-US", false)));
// if the image was not modified since "ifModifiedSinceHeader"-header, return a HTTP status code 304 not modified
- if (!(dateImageModified > ifModifiedSinceHeader))
+ if (!(dateImageModified > ifModifiedSinceHeader) && cacheDuration.HasValue)
{
- if (ifModifiedSinceHeader.Add(cacheDuration!.Value) < DateTime.UtcNow)
+ if (ifModifiedSinceHeader.Add(cacheDuration.Value) < DateTime.UtcNow)
{
Response.StatusCode = StatusCodes.Status304NotModified;
return new ContentResult();
@@ -1297,8 +2026,7 @@ namespace Jellyfin.Api.Controllers
return NoContent();
}
- var stream = new FileStream(imagePath, FileMode.Open, FileAccess.Read);
- return File(stream, imageContentType);
+ return PhysicalFile(imagePath, imageContentType);
}
}
}