WIP GetImage endpoints

pull/3385/head
crobibero 4 years ago
parent 9a8deadc21
commit ccd7b3f524

@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Threading;
@ -13,6 +14,7 @@ using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Net;
using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.Drawing;
using MediaBrowser.Model.Dto;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.IO;
@ -21,6 +23,7 @@ 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
{
@ -302,16 +305,172 @@ namespace Jellyfin.Api.Controllers
return list;
}
/// <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="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="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>
/// <param name="imageIndex">Image index.</param>
/// <param name="enableImageEnhancers">Enable or disable image enhancers such as cover art.</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}")]
[HttpHead("/Items/{itemId}/Images/{imageType}")]
[HttpGet("/Items/{itemId}/Images/{imageType}/{imageIndex?}")]
[HttpHead("/Items/{itemId}/Images/{imageType}/{imageIndex?}")]
public async Task<ActionResult> GetItemImage(
[FromRoute] Guid itemId,
[FromRoute] ImageType imageType,
[FromRoute] int? maxWidth,
[FromRoute] int? maxHeight,
[FromQuery] int? width,
[FromQuery] int? height,
[FromQuery] int? quality,
[FromQuery] string tag,
[FromQuery] bool? cropWhitespace,
[FromQuery] string format,
[FromQuery] bool addPlayedIndicator,
[FromQuery] double? percentPlayed,
[FromQuery] int? unplayedCount,
[FromQuery] int? blur,
[FromQuery] string backgroundColor,
[FromQuery] string foregroundLayer,
[FromRoute] int? imageIndex = null,
[FromQuery] bool enableImageEnhancers = true)
{
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,
cropWhitespace,
addPlayedIndicator,
blur,
backgroundColor,
foregroundLayer,
enableImageEnhancers,
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="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="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>
/// <param name="enableImageEnhancers">Enable or disable image enhancers such as cover art.</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}/{tag}/{format}/{maxWidth}/{maxHeight}/{percentPlayed}/{unplayedCount}")]
[HttpHead("/Items/{itemId}/Images/{imageType}/{imageIndex}/{tag}/{format}/{maxWidth}/{maxHeight}/{percentPlayed}/{unplayedCount}")]
public ActionResult<object> GetItemImage(
[FromRoute] Guid itemId,
[FromRoute] ImageType imageType,
[FromRoute] int? imageIndex,
[FromRoute] string tag,
[FromRoute] string format,
[FromRoute] int? maxWidth,
[FromRoute] int? maxHeight,
[FromRoute] double? percentPlayed,
[FromRoute] int? unplayedCount,
[FromQuery] int? width,
[FromQuery] int? height,
[FromQuery] int? quality,
[FromQuery] bool? cropWhitespace,
[FromQuery] bool addPlayedIndicator,
[FromQuery] int? blur,
[FromQuery] string backgroundColor,
[FromQuery] string foregroundLayer,
[FromQuery] bool enableImageEnhancers = true)
{
var item = _libraryManager.GetItemById(itemId);
if (item == null)
{
return NotFound();
}
return GetImageInternal(
itemId,
imageType,
imageIndex,
tag,
format,
maxWidth,
maxHeight,
percentPlayed,
unplayedCount,
width,
height,
quality,
cropWhitespace,
addPlayedIndicator,
blur,
backgroundColor,
foregroundLayer,
enableImageEnhancers,
item,
Request.Method.Equals(HttpMethods.Head, StringComparison.OrdinalIgnoreCase));
}
private static async Task<MemoryStream> 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)
{
Position = 0
};
return new MemoryStream(bytes) {Position = 0};
}
private ImageInfo? GetImageInfo(BaseItem item, ItemImageInfo info, int? imageIndex)
@ -365,5 +524,239 @@ namespace Jellyfin.Api.Controllers
return null;
}
}
private async Task<ActionResult> GetImageInternal(
Guid itemId,
ImageType imageType,
int? imageIndex,
string tag,
string format,
int? maxWidth,
int? maxHeight,
double? percentPlayed,
int? unplayedCount,
int? width,
int? height,
int? quality,
bool? cropWhitespace,
bool addPlayedIndicator,
int? blur,
string backgroundColor,
string foregroundLayer,
bool enableImageEnhancers,
BaseItem item,
bool isHeadRequest)
{
if (percentPlayed.HasValue)
{
if (percentPlayed.Value <= 0)
{
percentPlayed = null;
}
else if (percentPlayed.Value >= 100)
{
percentPlayed = null;
addPlayedIndicator = true;
}
}
if (percentPlayed.HasValue)
{
unplayedCount = null;
}
if (unplayedCount.HasValue
&& unplayedCount.Value <= 0)
{
unplayedCount = null;
}
var imageInfo = item.GetImageInfo(imageType, imageIndex ?? 0);
if (imageInfo == null)
{
return NotFound(string.Format(NumberFormatInfo.InvariantInfo, "{0} does not have an image of type {1}", item.Name, imageType));
}
if (!cropWhitespace.HasValue)
{
cropWhitespace = imageType == ImageType.Logo || imageType == ImageType.Art;
}
var outputFormats = GetOutputFormats(format);
TimeSpan? cacheDuration = null;
if (!string.IsNullOrEmpty(tag))
{
cacheDuration = TimeSpan.FromDays(365);
}
var responseHeaders = new Dictionary<string, string> {{"transferMode.dlna.org", "Interactive"}, {"realTimeInfo.dlna.org", "DLNA.ORG_TLAG=*"}};
return await GetImageResult(
item,
itemId,
imageIndex,
height,
maxHeight,
maxWidth,
quality,
width,
addPlayedIndicator,
percentPlayed,
unplayedCount,
blur,
backgroundColor,
foregroundLayer,
imageInfo,
cropWhitespace.Value,
outputFormats,
cacheDuration,
responseHeaders,
isHeadRequest).ConfigureAwait(false);
}
private ImageFormat[] GetOutputFormats(string format)
{
if (!string.IsNullOrWhiteSpace(format)
&& Enum.TryParse(format, true, out ImageFormat parsedFormat))
{
return new[] {parsedFormat};
}
return GetClientSupportedFormats();
}
private ImageFormat[] GetClientSupportedFormats()
{
var acceptTypes = Request.Headers[HeaderNames.Accept];
var supportedFormats = new List<string>();
if (acceptTypes.Count > 0)
{
foreach (var type in acceptTypes)
{
int index = type.IndexOf(';', StringComparison.Ordinal);
if (index != -1)
{
supportedFormats.Add(type.Substring(0, index));
}
}
}
var acceptParam = Request.Query[HeaderNames.Accept];
var supportsWebP = SupportsFormat(supportedFormats, acceptParam, "webp", false);
if (!supportsWebP)
{
var userAgent = Request.Headers[HeaderNames.UserAgent].ToString();
if (userAgent.IndexOf("crosswalk", StringComparison.OrdinalIgnoreCase) != -1 &&
userAgent.IndexOf("android", StringComparison.OrdinalIgnoreCase) != -1)
{
supportsWebP = true;
}
}
var formats = new List<ImageFormat>(4);
if (supportsWebP)
{
formats.Add(ImageFormat.Webp);
}
formats.Add(ImageFormat.Jpg);
formats.Add(ImageFormat.Png);
if (SupportsFormat(supportedFormats, acceptParam, "gif", true))
{
formats.Add(ImageFormat.Gif);
}
return formats.ToArray();
}
private bool SupportsFormat(IReadOnlyCollection<string> requestAcceptTypes, string acceptParam, string format, bool acceptAll)
{
var mimeType = "image/" + format;
if (requestAcceptTypes.Contains(mimeType))
{
return true;
}
if (acceptAll && requestAcceptTypes.Contains("*/*"))
{
return true;
}
return string.Equals(acceptParam, format, StringComparison.OrdinalIgnoreCase);
}
private async Task<ActionResult> GetImageResult(
BaseItem item,
Guid itemId,
int? index,
int? height,
int? maxHeight,
int? maxWidth,
int? quality,
int? width,
bool addPlayedIndicator,
double? percentPlayed,
int? unplayedCount,
int? blur,
string backgroundColor,
string foregroundLayer,
ItemImageInfo imageInfo,
bool cropWhitespace,
IReadOnlyCollection<ImageFormat> supportedFormats,
TimeSpan? cacheDuration,
IDictionary<string, string> headers,
bool isHeadRequest)
{
if (!imageInfo.IsLocalFile)
{
imageInfo = await _libraryManager.ConvertImageToLocal(item, imageInfo, index ?? 0).ConfigureAwait(false);
}
var options = new ImageProcessingOptions
{
CropWhiteSpace = cropWhitespace,
Height = height,
ImageIndex = index ?? 0,
Image = imageInfo,
Item = item,
ItemId = itemId,
MaxHeight = maxHeight,
MaxWidth = maxWidth,
Quality = quality ?? 100,
Width = width,
AddPlayedIndicator = addPlayedIndicator,
PercentPlayed = percentPlayed ?? 0,
UnplayedCount = unplayedCount,
Blur = blur,
BackgroundColor = backgroundColor,
ForegroundLayer = foregroundLayer,
SupportedOutputFormats = supportedFormats
};
var imageResult = await _imageProcessor.ProcessImage(options).ConfigureAwait(false);
headers[HeaderNames.Vary] = HeaderNames.Accept;
/*
// TODO
return _resultFactory.GetStaticFileResult(Request, new StaticFileResultOptions
{
CacheDuration = cacheDuration,
ResponseHeaders = headers,
ContentType = imageResult.Item2,
DateLastModified = imageResult.Item3,
IsHeadRequest = isHeadRequest,
Path = imageResult.Item1,
FileShare = FileShare.Read
});
*/
return NoContent();
}
}
}

Loading…
Cancel
Save