From ccd7b3f52435de880158bc41dec9268dc9acbdd5 Mon Sep 17 00:00:00 2001 From: crobibero Date: Sun, 21 Jun 2020 11:31:44 -0600 Subject: [PATCH] WIP GetImage endpoints --- Jellyfin.Api/Controllers/ImageController.cs | 401 +++++++++++++++++++- 1 file changed, 397 insertions(+), 4 deletions(-) diff --git a/Jellyfin.Api/Controllers/ImageController.cs b/Jellyfin.Api/Controllers/ImageController.cs index d8c67dbea0..c24c5e24c5 100644 --- a/Jellyfin.Api/Controllers/ImageController.cs +++ b/Jellyfin.Api/Controllers/ImageController.cs @@ -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; } + /// + /// 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. + /// Optional. Supply the cache tag from the item object to receive strong caching headers. + /// Optional. Specify if whitespace should be cropped out of the image. True/False. If unspecified, whitespace will be cropped from logos and clear art. + /// Determines the output format of the image - original,gif,jpg,png. + /// Optional. Add a played indicator. + /// Optional. Percent to render for the percent played overlay. + /// Optional. Unplayed count overlay to render. + /// Optional. Blur image. + /// Optional. Apply a background color for transparent images. + /// Optional. Apply a foreground layer on top of the image. + /// Image index. + /// Enable or disable image enhancers such as cover art. + /// 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}")] + [HttpGet("/Items/{itemId}/Images/{imageType}/{imageIndex?}")] + [HttpHead("/Items/{itemId}/Images/{imageType}/{imageIndex?}")] + public async Task 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); + } + + /// + /// Gets the item's image. + /// + /// Item 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. + /// Optional. Specify if whitespace should be cropped out of the image. True/False. If unspecified, whitespace will be cropped from logos and clear art. + /// Optional. Add a played indicator. + /// Optional. Blur image. + /// Optional. Apply a background color for transparent images. + /// Optional. Apply a foreground layer on top of the image. + /// Enable or disable image enhancers such as cover art. + /// 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}")] + public ActionResult 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 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 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 {{"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(); + 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(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 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 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 supportedFormats, + TimeSpan? cacheDuration, + IDictionary 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(); + } } }