< Summary - Jellyfin

Information
Class: Jellyfin.Api.Controllers.ImageController
Assembly: Jellyfin.Api
File(s): /srv/git/jellyfin/Jellyfin.Api/Controllers/ImageController.cs
Line coverage
9%
Covered lines: 9
Uncovered lines: 88
Coverable lines: 97
Total lines: 2149
Line coverage: 9.2%
Branch coverage
20%
Covered branches: 8
Total branches: 40
Branch coverage: 20%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Coverage history

Coverage history 0 25 50 75 100

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.ctor(...)100%210%
GetFromBase64Stream(...)100%210%
DeleteCustomSplashscreen()0%2040%
GetImageInfo(...)0%4260%
GetOutputFormats(...)0%620%
GetClientSupportedFormats()0%210140%
SupportsFormat(...)0%4260%
TryGetImageExtensionFromContentType(...)100%88100%

File(s)

/srv/git/jellyfin/Jellyfin.Api/Controllers/ImageController.cs

#LineLine coverage
 1using System;
 2using System.Collections.Generic;
 3using System.Collections.Immutable;
 4using System.ComponentModel.DataAnnotations;
 5using System.Diagnostics.CodeAnalysis;
 6using System.Globalization;
 7using System.IO;
 8using System.Linq;
 9using System.Net.Mime;
 10using System.Security.Cryptography;
 11using System.Threading;
 12using System.Threading.Tasks;
 13using Jellyfin.Api.Attributes;
 14using Jellyfin.Api.Extensions;
 15using Jellyfin.Api.Helpers;
 16using Jellyfin.Extensions;
 17using MediaBrowser.Common.Api;
 18using MediaBrowser.Common.Configuration;
 19using MediaBrowser.Controller.Configuration;
 20using MediaBrowser.Controller.Drawing;
 21using MediaBrowser.Controller.Entities;
 22using MediaBrowser.Controller.Library;
 23using MediaBrowser.Controller.Providers;
 24using MediaBrowser.Model.Branding;
 25using MediaBrowser.Model.Drawing;
 26using MediaBrowser.Model.Dto;
 27using MediaBrowser.Model.Entities;
 28using MediaBrowser.Model.IO;
 29using MediaBrowser.Model.Net;
 30using Microsoft.AspNetCore.Authorization;
 31using Microsoft.AspNetCore.Http;
 32using Microsoft.AspNetCore.Mvc;
 33using Microsoft.Extensions.Logging;
 34using Microsoft.Net.Http.Headers;
 35
 36namespace Jellyfin.Api.Controllers;
 37
 38/// <summary>
 39/// Image controller.
 40/// </summary>
 41[Route("")]
 42public class ImageController : BaseJellyfinApiController
 43{
 44    private readonly IUserManager _userManager;
 45    private readonly ILibraryManager _libraryManager;
 46    private readonly IProviderManager _providerManager;
 47    private readonly IImageProcessor _imageProcessor;
 48    private readonly IFileSystem _fileSystem;
 49    private readonly ILogger<ImageController> _logger;
 50    private readonly IServerConfigurationManager _serverConfigurationManager;
 51    private readonly IApplicationPaths _appPaths;
 52
 53    /// <summary>
 54    /// Initializes a new instance of the <see cref="ImageController"/> class.
 55    /// </summary>
 56    /// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param>
 57    /// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
 58    /// <param name="providerManager">Instance of the <see cref="IProviderManager"/> interface.</param>
 59    /// <param name="imageProcessor">Instance of the <see cref="IImageProcessor"/> interface.</param>
 60    /// <param name="fileSystem">Instance of the <see cref="IFileSystem"/> interface.</param>
 61    /// <param name="logger">Instance of the <see cref="ILogger{ImageController}"/> interface.</param>
 62    /// <param name="serverConfigurationManager">Instance of the <see cref="IServerConfigurationManager"/> interface.</p
 63    /// <param name="appPaths">Instance of the <see cref="IApplicationPaths"/> interface.</param>
 064    public ImageController(
 065        IUserManager userManager,
 066        ILibraryManager libraryManager,
 067        IProviderManager providerManager,
 068        IImageProcessor imageProcessor,
 069        IFileSystem fileSystem,
 070        ILogger<ImageController> logger,
 071        IServerConfigurationManager serverConfigurationManager,
 072        IApplicationPaths appPaths)
 73    {
 074        _userManager = userManager;
 075        _libraryManager = libraryManager;
 076        _providerManager = providerManager;
 077        _imageProcessor = imageProcessor;
 078        _fileSystem = fileSystem;
 079        _logger = logger;
 080        _serverConfigurationManager = serverConfigurationManager;
 081        _appPaths = appPaths;
 082    }
 83
 84    private static CryptoStream GetFromBase64Stream(Stream inputStream)
 085        => new CryptoStream(inputStream, new FromBase64Transform(), CryptoStreamMode.Read);
 86
 87    /// <summary>
 88    /// Sets the user image.
 89    /// </summary>
 90    /// <param name="userId">User Id.</param>
 91    /// <response code="204">Image updated.</response>
 92    /// <response code="403">User does not have permission to delete the image.</response>
 93    /// <response code="404">Item not found.</response>
 94    /// <returns>A <see cref="NoContentResult"/>.</returns>
 95    [HttpPost("UserImage")]
 96    [Authorize]
 97    [AcceptsImageFile]
 98    [ProducesResponseType(StatusCodes.Status204NoContent)]
 99    [ProducesResponseType(StatusCodes.Status400BadRequest)]
 100    [ProducesResponseType(StatusCodes.Status403Forbidden)]
 101    [ProducesResponseType(StatusCodes.Status404NotFound)]
 102    public async Task<ActionResult> PostUserImage(
 103        [FromQuery] Guid? userId)
 104    {
 105        var requestUserId = RequestHelpers.GetUserId(User, userId);
 106        var user = _userManager.GetUserById(requestUserId);
 107        if (user is null)
 108        {
 109            return NotFound();
 110        }
 111
 112        if (!RequestHelpers.AssertCanUpdateUser(HttpContext.User, user, true))
 113        {
 114            return StatusCode(StatusCodes.Status403Forbidden, "User is not allowed to update the image.");
 115        }
 116
 117        if (!TryGetImageExtensionFromContentType(Request.ContentType, out string? extension))
 118        {
 119            return BadRequest("Incorrect ContentType.");
 120        }
 121
 122        var stream = GetFromBase64Stream(Request.Body);
 123        await using (stream.ConfigureAwait(false))
 124        {
 125            // Handle image/png; charset=utf-8
 126            var mimeType = Request.ContentType?.Split(';').FirstOrDefault();
 127            var userDataPath = Path.Combine(_serverConfigurationManager.ApplicationPaths.UserConfigurationDirectoryPath,
 128            if (user.ProfileImage is not null)
 129            {
 130                await _userManager.ClearProfileImageAsync(user).ConfigureAwait(false);
 131            }
 132
 133            user.ProfileImage = new Data.Entities.ImageInfo(Path.Combine(userDataPath, "profile" + extension));
 134
 135            await _providerManager
 136                .SaveImage(stream, mimeType, user.ProfileImage.Path)
 137                .ConfigureAwait(false);
 138            await _userManager.UpdateUserAsync(user).ConfigureAwait(false);
 139
 140            return NoContent();
 141        }
 142    }
 143
 144    /// <summary>
 145    /// Sets the user image.
 146    /// </summary>
 147    /// <param name="userId">User Id.</param>
 148    /// <param name="imageType">(Unused) Image type.</param>
 149    /// <response code="204">Image updated.</response>
 150    /// <response code="403">User does not have permission to delete the image.</response>
 151    /// <returns>A <see cref="NoContentResult"/>.</returns>
 152    [HttpPost("Users/{userId}/Images/{imageType}")]
 153    [Authorize]
 154    [Obsolete("Kept for backwards compatibility")]
 155    [ApiExplorerSettings(IgnoreApi = true)]
 156    [AcceptsImageFile]
 157    [ProducesResponseType(StatusCodes.Status204NoContent)]
 158    [ProducesResponseType(StatusCodes.Status400BadRequest)]
 159    [ProducesResponseType(StatusCodes.Status403Forbidden)]
 160    [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "imageType", Justification = 
 161    public Task<ActionResult> PostUserImageLegacy(
 162        [FromRoute, Required] Guid userId,
 163        [FromRoute, Required] ImageType imageType)
 164        => PostUserImage(userId);
 165
 166    /// <summary>
 167    /// Sets the user image.
 168    /// </summary>
 169    /// <param name="userId">User Id.</param>
 170    /// <param name="imageType">(Unused) Image type.</param>
 171    /// <param name="index">(Unused) Image index.</param>
 172    /// <response code="204">Image updated.</response>
 173    /// <response code="403">User does not have permission to delete the image.</response>
 174    /// <returns>A <see cref="NoContentResult"/>.</returns>
 175    [HttpPost("Users/{userId}/Images/{imageType}/{index}")]
 176    [Authorize]
 177    [Obsolete("Kept for backwards compatibility")]
 178    [ApiExplorerSettings(IgnoreApi = true)]
 179    [AcceptsImageFile]
 180    [ProducesResponseType(StatusCodes.Status204NoContent)]
 181    [ProducesResponseType(StatusCodes.Status400BadRequest)]
 182    [ProducesResponseType(StatusCodes.Status403Forbidden)]
 183    [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "imageType", Justification = 
 184    [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "index", Justification = "Imp
 185    public Task<ActionResult> PostUserImageByIndexLegacy(
 186        [FromRoute, Required] Guid userId,
 187        [FromRoute, Required] ImageType imageType,
 188        [FromRoute] int index)
 189        => PostUserImage(userId);
 190
 191    /// <summary>
 192    /// Delete the user's image.
 193    /// </summary>
 194    /// <param name="userId">User Id.</param>
 195    /// <response code="204">Image deleted.</response>
 196    /// <response code="403">User does not have permission to delete the image.</response>
 197    /// <returns>A <see cref="NoContentResult"/>.</returns>
 198    [HttpDelete("UserImage")]
 199    [Authorize]
 200    [ProducesResponseType(StatusCodes.Status204NoContent)]
 201    [ProducesResponseType(StatusCodes.Status403Forbidden)]
 202    public async Task<ActionResult> DeleteUserImage(
 203        [FromQuery] Guid? userId)
 204    {
 205        var requestUserId = RequestHelpers.GetUserId(User, userId);
 206        var user = _userManager.GetUserById(requestUserId);
 207        if (user is null)
 208        {
 209            return NotFound();
 210        }
 211
 212        if (!RequestHelpers.AssertCanUpdateUser(HttpContext.User, user, true))
 213        {
 214            return StatusCode(StatusCodes.Status403Forbidden, "User is not allowed to delete the image.");
 215        }
 216
 217        if (user.ProfileImage is null)
 218        {
 219            return NoContent();
 220        }
 221
 222        try
 223        {
 224            System.IO.File.Delete(user.ProfileImage.Path);
 225        }
 226        catch (IOException e)
 227        {
 228            _logger.LogError(e, "Error deleting user profile image:");
 229        }
 230
 231        await _userManager.ClearProfileImageAsync(user).ConfigureAwait(false);
 232        return NoContent();
 233    }
 234
 235    /// <summary>
 236    /// Delete the user's image.
 237    /// </summary>
 238    /// <param name="userId">User Id.</param>
 239    /// <param name="imageType">(Unused) Image type.</param>
 240    /// <param name="index">(Unused) Image index.</param>
 241    /// <response code="204">Image deleted.</response>
 242    /// <response code="403">User does not have permission to delete the image.</response>
 243    /// <returns>A <see cref="NoContentResult"/>.</returns>
 244    [HttpDelete("Users/{userId}/Images/{imageType}")]
 245    [Authorize]
 246    [Obsolete("Kept for backwards compatibility")]
 247    [ApiExplorerSettings(IgnoreApi = true)]
 248    [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "imageType", Justification = 
 249    [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "index", Justification = "Imp
 250    [ProducesResponseType(StatusCodes.Status204NoContent)]
 251    [ProducesResponseType(StatusCodes.Status403Forbidden)]
 252    public Task<ActionResult> DeleteUserImageLegacy(
 253        [FromRoute, Required] Guid userId,
 254        [FromRoute, Required] ImageType imageType,
 255        [FromQuery] int? index = null)
 256        => DeleteUserImage(userId);
 257
 258    /// <summary>
 259    /// Delete the user's image.
 260    /// </summary>
 261    /// <param name="userId">User Id.</param>
 262    /// <param name="imageType">(Unused) Image type.</param>
 263    /// <param name="index">(Unused) Image index.</param>
 264    /// <response code="204">Image deleted.</response>
 265    /// <response code="403">User does not have permission to delete the image.</response>
 266    /// <returns>A <see cref="NoContentResult"/>.</returns>
 267    [HttpDelete("Users/{userId}/Images/{imageType}/{index}")]
 268    [Authorize]
 269    [Obsolete("Kept for backwards compatibility")]
 270    [ApiExplorerSettings(IgnoreApi = true)]
 271    [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "imageType", Justification = 
 272    [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "index", Justification = "Imp
 273    [ProducesResponseType(StatusCodes.Status204NoContent)]
 274    [ProducesResponseType(StatusCodes.Status403Forbidden)]
 275    public Task<ActionResult> DeleteUserImageByIndexLegacy(
 276        [FromRoute, Required] Guid userId,
 277        [FromRoute, Required] ImageType imageType,
 278        [FromRoute] int index)
 279        => DeleteUserImage(userId);
 280
 281    /// <summary>
 282    /// Delete an item's image.
 283    /// </summary>
 284    /// <param name="itemId">Item id.</param>
 285    /// <param name="imageType">Image type.</param>
 286    /// <param name="imageIndex">The image index.</param>
 287    /// <response code="204">Image deleted.</response>
 288    /// <response code="404">Item not found.</response>
 289    /// <returns>A <see cref="NoContentResult"/> on success, or a <see cref="NotFoundResult"/> if item not found.</retur
 290    [HttpDelete("Items/{itemId}/Images/{imageType}")]
 291    [Authorize(Policy = Policies.RequiresElevation)]
 292    [ProducesResponseType(StatusCodes.Status204NoContent)]
 293    [ProducesResponseType(StatusCodes.Status404NotFound)]
 294    public async Task<ActionResult> DeleteItemImage(
 295        [FromRoute, Required] Guid itemId,
 296        [FromRoute, Required] ImageType imageType,
 297        [FromQuery] int? imageIndex)
 298    {
 299        var item = _libraryManager.GetItemById<BaseItem>(itemId, User.GetUserId());
 300        if (item is null)
 301        {
 302            return NotFound();
 303        }
 304
 305        await item.DeleteImageAsync(imageType, imageIndex ?? 0).ConfigureAwait(false);
 306        return NoContent();
 307    }
 308
 309    /// <summary>
 310    /// Delete an item's image.
 311    /// </summary>
 312    /// <param name="itemId">Item id.</param>
 313    /// <param name="imageType">Image type.</param>
 314    /// <param name="imageIndex">The image index.</param>
 315    /// <response code="204">Image deleted.</response>
 316    /// <response code="404">Item not found.</response>
 317    /// <returns>A <see cref="NoContentResult"/> on success, or a <see cref="NotFoundResult"/> if item not found.</retur
 318    [HttpDelete("Items/{itemId}/Images/{imageType}/{imageIndex}")]
 319    [Authorize(Policy = Policies.RequiresElevation)]
 320    [ProducesResponseType(StatusCodes.Status204NoContent)]
 321    [ProducesResponseType(StatusCodes.Status404NotFound)]
 322    public async Task<ActionResult> DeleteItemImageByIndex(
 323        [FromRoute, Required] Guid itemId,
 324        [FromRoute, Required] ImageType imageType,
 325        [FromRoute] int imageIndex)
 326    {
 327        var item = _libraryManager.GetItemById<BaseItem>(itemId, User.GetUserId());
 328        if (item is null)
 329        {
 330            return NotFound();
 331        }
 332
 333        await item.DeleteImageAsync(imageType, imageIndex).ConfigureAwait(false);
 334        return NoContent();
 335    }
 336
 337    /// <summary>
 338    /// Set item image.
 339    /// </summary>
 340    /// <param name="itemId">Item id.</param>
 341    /// <param name="imageType">Image type.</param>
 342    /// <response code="204">Image saved.</response>
 343    /// <response code="404">Item not found.</response>
 344    /// <returns>A <see cref="NoContentResult"/> on success, or a <see cref="NotFoundResult"/> if item not found.</retur
 345    [HttpPost("Items/{itemId}/Images/{imageType}")]
 346    [Authorize(Policy = Policies.RequiresElevation)]
 347    [AcceptsImageFile]
 348    [ProducesResponseType(StatusCodes.Status204NoContent)]
 349    [ProducesResponseType(StatusCodes.Status400BadRequest)]
 350    [ProducesResponseType(StatusCodes.Status404NotFound)]
 351    [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "index", Justification = "Imp
 352    public async Task<ActionResult> SetItemImage(
 353        [FromRoute, Required] Guid itemId,
 354        [FromRoute, Required] ImageType imageType)
 355    {
 356        var item = _libraryManager.GetItemById<BaseItem>(itemId, User.GetUserId());
 357        if (item is null)
 358        {
 359            return NotFound();
 360        }
 361
 362        if (!TryGetImageExtensionFromContentType(Request.ContentType, out _))
 363        {
 364            return BadRequest("Incorrect ContentType.");
 365        }
 366
 367        var stream = GetFromBase64Stream(Request.Body);
 368        await using (stream.ConfigureAwait(false))
 369        {
 370            // Handle image/png; charset=utf-8
 371            var mimeType = Request.ContentType?.Split(';').FirstOrDefault();
 372            await _providerManager.SaveImage(item, stream, mimeType, imageType, null, CancellationToken.None).ConfigureA
 373            await item.UpdateToRepositoryAsync(ItemUpdateType.ImageUpdate, CancellationToken.None).ConfigureAwait(false)
 374
 375            return NoContent();
 376        }
 377    }
 378
 379    /// <summary>
 380    /// Set item image.
 381    /// </summary>
 382    /// <param name="itemId">Item id.</param>
 383    /// <param name="imageType">Image type.</param>
 384    /// <param name="imageIndex">(Unused) Image index.</param>
 385    /// <response code="204">Image saved.</response>
 386    /// <response code="404">Item not found.</response>
 387    /// <returns>A <see cref="NoContentResult"/> on success, or a <see cref="NotFoundResult"/> if item not found.</retur
 388    [HttpPost("Items/{itemId}/Images/{imageType}/{imageIndex}")]
 389    [Authorize(Policy = Policies.RequiresElevation)]
 390    [AcceptsImageFile]
 391    [ProducesResponseType(StatusCodes.Status204NoContent)]
 392    [ProducesResponseType(StatusCodes.Status400BadRequest)]
 393    [ProducesResponseType(StatusCodes.Status404NotFound)]
 394    [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "index", Justification = "Imp
 395    public async Task<ActionResult> SetItemImageByIndex(
 396        [FromRoute, Required] Guid itemId,
 397        [FromRoute, Required] ImageType imageType,
 398        [FromRoute] int imageIndex)
 399    {
 400        var item = _libraryManager.GetItemById<BaseItem>(itemId, User.GetUserId());
 401        if (item is null)
 402        {
 403            return NotFound();
 404        }
 405
 406        if (!TryGetImageExtensionFromContentType(Request.ContentType, out _))
 407        {
 408            return BadRequest("Incorrect ContentType.");
 409        }
 410
 411        var stream = GetFromBase64Stream(Request.Body);
 412        await using (stream.ConfigureAwait(false))
 413        {
 414            // Handle image/png; charset=utf-8
 415            var mimeType = Request.ContentType?.Split(';').FirstOrDefault();
 416            await _providerManager.SaveImage(item, stream, mimeType, imageType, null, CancellationToken.None).ConfigureA
 417            await item.UpdateToRepositoryAsync(ItemUpdateType.ImageUpdate, CancellationToken.None).ConfigureAwait(false)
 418
 419            return NoContent();
 420        }
 421    }
 422
 423    /// <summary>
 424    /// Updates the index for an item image.
 425    /// </summary>
 426    /// <param name="itemId">Item id.</param>
 427    /// <param name="imageType">Image type.</param>
 428    /// <param name="imageIndex">Old image index.</param>
 429    /// <param name="newIndex">New image index.</param>
 430    /// <response code="204">Image index updated.</response>
 431    /// <response code="404">Item not found.</response>
 432    /// <returns>A <see cref="NoContentResult"/> on success, or a <see cref="NotFoundResult"/> if item not found.</retur
 433    [HttpPost("Items/{itemId}/Images/{imageType}/{imageIndex}/Index")]
 434    [Authorize(Policy = Policies.RequiresElevation)]
 435    [ProducesResponseType(StatusCodes.Status204NoContent)]
 436    [ProducesResponseType(StatusCodes.Status404NotFound)]
 437    public async Task<ActionResult> UpdateItemImageIndex(
 438        [FromRoute, Required] Guid itemId,
 439        [FromRoute, Required] ImageType imageType,
 440        [FromRoute, Required] int imageIndex,
 441        [FromQuery, Required] int newIndex)
 442    {
 443        var item = _libraryManager.GetItemById<BaseItem>(itemId, User.GetUserId());
 444        if (item is null)
 445        {
 446            return NotFound();
 447        }
 448
 449        await item.SwapImagesAsync(imageType, imageIndex, newIndex).ConfigureAwait(false);
 450        return NoContent();
 451    }
 452
 453    /// <summary>
 454    /// Get item image infos.
 455    /// </summary>
 456    /// <param name="itemId">Item id.</param>
 457    /// <response code="200">Item images returned.</response>
 458    /// <response code="404">Item not found.</response>
 459    /// <returns>The list of image infos on success, or <see cref="NotFoundResult"/> if item not found.</returns>
 460    [HttpGet("Items/{itemId}/Images")]
 461    [Authorize]
 462    [ProducesResponseType(StatusCodes.Status200OK)]
 463    [ProducesResponseType(StatusCodes.Status404NotFound)]
 464    public async Task<ActionResult<IEnumerable<ImageInfo>>> GetItemImageInfos([FromRoute, Required] Guid itemId)
 465    {
 466        var item = _libraryManager.GetItemById<BaseItem>(itemId, User.GetUserId());
 467        if (item is null)
 468        {
 469            return NotFound();
 470        }
 471
 472        var list = new List<ImageInfo>();
 473        var itemImages = item.ImageInfos;
 474
 475        if (itemImages.Length == 0)
 476        {
 477            // short-circuit
 478            return list;
 479        }
 480
 481        await _libraryManager.UpdateImagesAsync(item).ConfigureAwait(false); // this makes sure dimensions and hashes ar
 482
 483        foreach (var image in itemImages)
 484        {
 485            if (!item.AllowsMultipleImages(image.Type))
 486            {
 487                var info = GetImageInfo(item, image, null);
 488
 489                if (info is not null)
 490                {
 491                    list.Add(info);
 492                }
 493            }
 494        }
 495
 496        foreach (var imageType in itemImages.Select(i => i.Type).Distinct().Where(item.AllowsMultipleImages))
 497        {
 498            var index = 0;
 499
 500            // Prevent implicitly captured closure
 501            var currentImageType = imageType;
 502
 503            foreach (var image in itemImages.Where(i => i.Type == currentImageType))
 504            {
 505                var info = GetImageInfo(item, image, index);
 506
 507                if (info is not null)
 508                {
 509                    list.Add(info);
 510                }
 511
 512                index++;
 513            }
 514        }
 515
 516        return list;
 517    }
 518
 519    /// <summary>
 520    /// Gets the item's image.
 521    /// </summary>
 522    /// <param name="itemId">Item id.</param>
 523    /// <param name="imageType">Image type.</param>
 524    /// <param name="maxWidth">The maximum image width to return.</param>
 525    /// <param name="maxHeight">The maximum image height to return.</param>
 526    /// <param name="width">The fixed image width to return.</param>
 527    /// <param name="height">The fixed image height to return.</param>
 528    /// <param name="quality">Optional. Quality setting, from 0-100. Defaults to 90 and should suffice in most cases.</p
 529    /// <param name="fillWidth">Width of box to fill.</param>
 530    /// <param name="fillHeight">Height of box to fill.</param>
 531    /// <param name="tag">Optional. Supply the cache tag from the item object to receive strong caching headers.</param>
 532    /// <param name="format">Optional. The <see cref="ImageFormat"/> of the returned image.</param>
 533    /// <param name="percentPlayed">Optional. Percent to render for the percent played overlay.</param>
 534    /// <param name="unplayedCount">Optional. Unplayed count overlay to render.</param>
 535    /// <param name="blur">Optional. Blur image.</param>
 536    /// <param name="backgroundColor">Optional. Apply a background color for transparent images.</param>
 537    /// <param name="foregroundLayer">Optional. Apply a foreground layer on top of the image.</param>
 538    /// <param name="imageIndex">Image index.</param>
 539    /// <response code="200">Image stream returned.</response>
 540    /// <response code="404">Item not found.</response>
 541    /// <returns>
 542    /// A <see cref="FileStreamResult"/> containing the file stream on success,
 543    /// or a <see cref="NotFoundResult"/> if item not found.
 544    /// </returns>
 545    [HttpGet("Items/{itemId}/Images/{imageType}")]
 546    [HttpHead("Items/{itemId}/Images/{imageType}", Name = "HeadItemImage")]
 547    [ProducesResponseType(StatusCodes.Status200OK)]
 548    [ProducesResponseType(StatusCodes.Status404NotFound)]
 549    [ProducesImageFile]
 550    public async Task<ActionResult> GetItemImage(
 551        [FromRoute, Required] Guid itemId,
 552        [FromRoute, Required] ImageType imageType,
 553        [FromQuery] int? maxWidth,
 554        [FromQuery] int? maxHeight,
 555        [FromQuery] int? width,
 556        [FromQuery] int? height,
 557        [FromQuery] int? quality,
 558        [FromQuery] int? fillWidth,
 559        [FromQuery] int? fillHeight,
 560        [FromQuery] string? tag,
 561        [FromQuery] ImageFormat? format,
 562        [FromQuery] double? percentPlayed,
 563        [FromQuery] int? unplayedCount,
 564        [FromQuery] int? blur,
 565        [FromQuery] string? backgroundColor,
 566        [FromQuery] string? foregroundLayer,
 567        [FromQuery] int? imageIndex)
 568    {
 569        var item = _libraryManager.GetItemById<BaseItem>(itemId, User.GetUserId());
 570        if (item is null)
 571        {
 572            return NotFound();
 573        }
 574
 575        return await GetImageInternal(
 576                itemId,
 577                imageType,
 578                imageIndex,
 579                tag,
 580                format,
 581                maxWidth,
 582                maxHeight,
 583                percentPlayed,
 584                unplayedCount,
 585                width,
 586                height,
 587                quality,
 588                fillWidth,
 589                fillHeight,
 590                blur,
 591                backgroundColor,
 592                foregroundLayer,
 593                item)
 594            .ConfigureAwait(false);
 595    }
 596
 597    /// <summary>
 598    /// Gets the item's image.
 599    /// </summary>
 600    /// <param name="itemId">Item id.</param>
 601    /// <param name="imageType">Image type.</param>
 602    /// <param name="imageIndex">Image index.</param>
 603    /// <param name="maxWidth">The maximum image width to return.</param>
 604    /// <param name="maxHeight">The maximum image height to return.</param>
 605    /// <param name="width">The fixed image width to return.</param>
 606    /// <param name="height">The fixed image height to return.</param>
 607    /// <param name="quality">Optional. Quality setting, from 0-100. Defaults to 90 and should suffice in most cases.</p
 608    /// <param name="fillWidth">Width of box to fill.</param>
 609    /// <param name="fillHeight">Height of box to fill.</param>
 610    /// <param name="tag">Optional. Supply the cache tag from the item object to receive strong caching headers.</param>
 611    /// <param name="format">Optional. The <see cref="ImageFormat"/> of the returned image.</param>
 612    /// <param name="percentPlayed">Optional. Percent to render for the percent played overlay.</param>
 613    /// <param name="unplayedCount">Optional. Unplayed count overlay to render.</param>
 614    /// <param name="blur">Optional. Blur image.</param>
 615    /// <param name="backgroundColor">Optional. Apply a background color for transparent images.</param>
 616    /// <param name="foregroundLayer">Optional. Apply a foreground layer on top of the image.</param>
 617    /// <response code="200">Image stream returned.</response>
 618    /// <response code="404">Item not found.</response>
 619    /// <returns>
 620    /// A <see cref="FileStreamResult"/> containing the file stream on success,
 621    /// or a <see cref="NotFoundResult"/> if item not found.
 622    /// </returns>
 623    [HttpGet("Items/{itemId}/Images/{imageType}/{imageIndex}")]
 624    [HttpHead("Items/{itemId}/Images/{imageType}/{imageIndex}", Name = "HeadItemImageByIndex")]
 625    [ProducesResponseType(StatusCodes.Status200OK)]
 626    [ProducesResponseType(StatusCodes.Status404NotFound)]
 627    [ProducesImageFile]
 628    public async Task<ActionResult> GetItemImageByIndex(
 629        [FromRoute, Required] Guid itemId,
 630        [FromRoute, Required] ImageType imageType,
 631        [FromRoute] int imageIndex,
 632        [FromQuery] int? maxWidth,
 633        [FromQuery] int? maxHeight,
 634        [FromQuery] int? width,
 635        [FromQuery] int? height,
 636        [FromQuery] int? quality,
 637        [FromQuery] int? fillWidth,
 638        [FromQuery] int? fillHeight,
 639        [FromQuery] string? tag,
 640        [FromQuery] ImageFormat? format,
 641        [FromQuery] double? percentPlayed,
 642        [FromQuery] int? unplayedCount,
 643        [FromQuery] int? blur,
 644        [FromQuery] string? backgroundColor,
 645        [FromQuery] string? foregroundLayer)
 646    {
 647        var item = _libraryManager.GetItemById<BaseItem>(itemId, User.GetUserId());
 648        if (item is null)
 649        {
 650            return NotFound();
 651        }
 652
 653        return await GetImageInternal(
 654                itemId,
 655                imageType,
 656                imageIndex,
 657                tag,
 658                format,
 659                maxWidth,
 660                maxHeight,
 661                percentPlayed,
 662                unplayedCount,
 663                width,
 664                height,
 665                quality,
 666                fillWidth,
 667                fillHeight,
 668                blur,
 669                backgroundColor,
 670                foregroundLayer,
 671                item)
 672            .ConfigureAwait(false);
 673    }
 674
 675    /// <summary>
 676    /// Gets the item's image.
 677    /// </summary>
 678    /// <param name="itemId">Item id.</param>
 679    /// <param name="imageType">Image type.</param>
 680    /// <param name="maxWidth">The maximum image width to return.</param>
 681    /// <param name="maxHeight">The maximum image height to return.</param>
 682    /// <param name="width">The fixed image width to return.</param>
 683    /// <param name="height">The fixed image height to return.</param>
 684    /// <param name="quality">Optional. Quality setting, from 0-100. Defaults to 90 and should suffice in most cases.</p
 685    /// <param name="fillWidth">Width of box to fill.</param>
 686    /// <param name="fillHeight">Height of box to fill.</param>
 687    /// <param name="tag">Optional. Supply the cache tag from the item object to receive strong caching headers.</param>
 688    /// <param name="format">Determines the output format of the image - original,gif,jpg,png.</param>
 689    /// <param name="percentPlayed">Optional. Percent to render for the percent played overlay.</param>
 690    /// <param name="unplayedCount">Optional. Unplayed count overlay to render.</param>
 691    /// <param name="blur">Optional. Blur image.</param>
 692    /// <param name="backgroundColor">Optional. Apply a background color for transparent images.</param>
 693    /// <param name="foregroundLayer">Optional. Apply a foreground layer on top of the image.</param>
 694    /// <param name="imageIndex">Image index.</param>
 695    /// <response code="200">Image stream returned.</response>
 696    /// <response code="404">Item not found.</response>
 697    /// <returns>
 698    /// A <see cref="FileStreamResult"/> containing the file stream on success,
 699    /// or a <see cref="NotFoundResult"/> if item not found.
 700    /// </returns>
 701    [HttpGet("Items/{itemId}/Images/{imageType}/{imageIndex}/{tag}/{format}/{maxWidth}/{maxHeight}/{percentPlayed}/{unpl
 702    [HttpHead("Items/{itemId}/Images/{imageType}/{imageIndex}/{tag}/{format}/{maxWidth}/{maxHeight}/{percentPlayed}/{unp
 703    [ProducesResponseType(StatusCodes.Status200OK)]
 704    [ProducesResponseType(StatusCodes.Status404NotFound)]
 705    [ProducesImageFile]
 706    public async Task<ActionResult> GetItemImage2(
 707        [FromRoute, Required] Guid itemId,
 708        [FromRoute, Required] ImageType imageType,
 709        [FromRoute, Required] int maxWidth,
 710        [FromRoute, Required] int maxHeight,
 711        [FromQuery] int? width,
 712        [FromQuery] int? height,
 713        [FromQuery] int? quality,
 714        [FromQuery] int? fillWidth,
 715        [FromQuery] int? fillHeight,
 716        [FromRoute, Required] string tag,
 717        [FromRoute, Required] ImageFormat format,
 718        [FromRoute, Required] double percentPlayed,
 719        [FromRoute, Required] int unplayedCount,
 720        [FromQuery] int? blur,
 721        [FromQuery] string? backgroundColor,
 722        [FromQuery] string? foregroundLayer,
 723        [FromRoute, Required] int imageIndex)
 724    {
 725        var item = _libraryManager.GetItemById<BaseItem>(itemId, User.GetUserId());
 726        if (item is null)
 727        {
 728            return NotFound();
 729        }
 730
 731        return await GetImageInternal(
 732                itemId,
 733                imageType,
 734                imageIndex,
 735                tag,
 736                format,
 737                maxWidth,
 738                maxHeight,
 739                percentPlayed,
 740                unplayedCount,
 741                width,
 742                height,
 743                quality,
 744                fillWidth,
 745                fillHeight,
 746                blur,
 747                backgroundColor,
 748                foregroundLayer,
 749                item)
 750            .ConfigureAwait(false);
 751    }
 752
 753    /// <summary>
 754    /// Get artist image by name.
 755    /// </summary>
 756    /// <param name="name">Artist name.</param>
 757    /// <param name="imageType">Image type.</param>
 758    /// <param name="tag">Optional. Supply the cache tag from the item object to receive strong caching headers.</param>
 759    /// <param name="format">Determines the output format of the image - original,gif,jpg,png.</param>
 760    /// <param name="maxWidth">The maximum image width to return.</param>
 761    /// <param name="maxHeight">The maximum image height to return.</param>
 762    /// <param name="percentPlayed">Optional. Percent to render for the percent played overlay.</param>
 763    /// <param name="unplayedCount">Optional. Unplayed count overlay to render.</param>
 764    /// <param name="width">The fixed image width to return.</param>
 765    /// <param name="height">The fixed image height to return.</param>
 766    /// <param name="quality">Optional. Quality setting, from 0-100. Defaults to 90 and should suffice in most cases.</p
 767    /// <param name="fillWidth">Width of box to fill.</param>
 768    /// <param name="fillHeight">Height of box to fill.</param>
 769    /// <param name="blur">Optional. Blur image.</param>
 770    /// <param name="backgroundColor">Optional. Apply a background color for transparent images.</param>
 771    /// <param name="foregroundLayer">Optional. Apply a foreground layer on top of the image.</param>
 772    /// <param name="imageIndex">Image index.</param>
 773    /// <response code="200">Image stream returned.</response>
 774    /// <response code="404">Item not found.</response>
 775    /// <returns>
 776    /// A <see cref="FileStreamResult"/> containing the file stream on success,
 777    /// or a <see cref="NotFoundResult"/> if item not found.
 778    /// </returns>
 779    [HttpGet("Artists/{name}/Images/{imageType}/{imageIndex}")]
 780    [HttpHead("Artists/{name}/Images/{imageType}/{imageIndex}", Name = "HeadArtistImage")]
 781    [ProducesResponseType(StatusCodes.Status200OK)]
 782    [ProducesResponseType(StatusCodes.Status404NotFound)]
 783    [ProducesImageFile]
 784    public async Task<ActionResult> GetArtistImage(
 785        [FromRoute, Required] string name,
 786        [FromRoute, Required] ImageType imageType,
 787        [FromQuery] string? tag,
 788        [FromQuery] ImageFormat? format,
 789        [FromQuery] int? maxWidth,
 790        [FromQuery] int? maxHeight,
 791        [FromQuery] double? percentPlayed,
 792        [FromQuery] int? unplayedCount,
 793        [FromQuery] int? width,
 794        [FromQuery] int? height,
 795        [FromQuery] int? quality,
 796        [FromQuery] int? fillWidth,
 797        [FromQuery] int? fillHeight,
 798        [FromQuery] int? blur,
 799        [FromQuery] string? backgroundColor,
 800        [FromQuery] string? foregroundLayer,
 801        [FromRoute, Required] int imageIndex)
 802    {
 803        var item = _libraryManager.GetArtist(name);
 804        if (item is null)
 805        {
 806            return NotFound();
 807        }
 808
 809        return await GetImageInternal(
 810                item.Id,
 811                imageType,
 812                imageIndex,
 813                tag,
 814                format,
 815                maxWidth,
 816                maxHeight,
 817                percentPlayed,
 818                unplayedCount,
 819                width,
 820                height,
 821                quality,
 822                fillWidth,
 823                fillHeight,
 824                blur,
 825                backgroundColor,
 826                foregroundLayer,
 827                item)
 828            .ConfigureAwait(false);
 829    }
 830
 831    /// <summary>
 832    /// Get genre image by name.
 833    /// </summary>
 834    /// <param name="name">Genre name.</param>
 835    /// <param name="imageType">Image type.</param>
 836    /// <param name="tag">Optional. Supply the cache tag from the item object to receive strong caching headers.</param>
 837    /// <param name="format">Determines the output format of the image - original,gif,jpg,png.</param>
 838    /// <param name="maxWidth">The maximum image width to return.</param>
 839    /// <param name="maxHeight">The maximum image height to return.</param>
 840    /// <param name="percentPlayed">Optional. Percent to render for the percent played overlay.</param>
 841    /// <param name="unplayedCount">Optional. Unplayed count overlay to render.</param>
 842    /// <param name="width">The fixed image width to return.</param>
 843    /// <param name="height">The fixed image height to return.</param>
 844    /// <param name="quality">Optional. Quality setting, from 0-100. Defaults to 90 and should suffice in most cases.</p
 845    /// <param name="fillWidth">Width of box to fill.</param>
 846    /// <param name="fillHeight">Height of box to fill.</param>
 847    /// <param name="blur">Optional. Blur image.</param>
 848    /// <param name="backgroundColor">Optional. Apply a background color for transparent images.</param>
 849    /// <param name="foregroundLayer">Optional. Apply a foreground layer on top of the image.</param>
 850    /// <param name="imageIndex">Image index.</param>
 851    /// <response code="200">Image stream returned.</response>
 852    /// <response code="404">Item not found.</response>
 853    /// <returns>
 854    /// A <see cref="FileStreamResult"/> containing the file stream on success,
 855    /// or a <see cref="NotFoundResult"/> if item not found.
 856    /// </returns>
 857    [HttpGet("Genres/{name}/Images/{imageType}")]
 858    [HttpHead("Genres/{name}/Images/{imageType}", Name = "HeadGenreImage")]
 859    [ProducesResponseType(StatusCodes.Status200OK)]
 860    [ProducesResponseType(StatusCodes.Status404NotFound)]
 861    [ProducesImageFile]
 862    public async Task<ActionResult> GetGenreImage(
 863        [FromRoute, Required] string name,
 864        [FromRoute, Required] ImageType imageType,
 865        [FromQuery] string? tag,
 866        [FromQuery] ImageFormat? format,
 867        [FromQuery] int? maxWidth,
 868        [FromQuery] int? maxHeight,
 869        [FromQuery] double? percentPlayed,
 870        [FromQuery] int? unplayedCount,
 871        [FromQuery] int? width,
 872        [FromQuery] int? height,
 873        [FromQuery] int? quality,
 874        [FromQuery] int? fillWidth,
 875        [FromQuery] int? fillHeight,
 876        [FromQuery] int? blur,
 877        [FromQuery] string? backgroundColor,
 878        [FromQuery] string? foregroundLayer,
 879        [FromQuery] int? imageIndex)
 880    {
 881        var item = _libraryManager.GetGenre(name);
 882        if (item is null)
 883        {
 884            return NotFound();
 885        }
 886
 887        return await GetImageInternal(
 888                item.Id,
 889                imageType,
 890                imageIndex,
 891                tag,
 892                format,
 893                maxWidth,
 894                maxHeight,
 895                percentPlayed,
 896                unplayedCount,
 897                width,
 898                height,
 899                quality,
 900                fillWidth,
 901                fillHeight,
 902                blur,
 903                backgroundColor,
 904                foregroundLayer,
 905                item)
 906            .ConfigureAwait(false);
 907    }
 908
 909    /// <summary>
 910    /// Get genre image by name.
 911    /// </summary>
 912    /// <param name="name">Genre name.</param>
 913    /// <param name="imageType">Image type.</param>
 914    /// <param name="imageIndex">Image index.</param>
 915    /// <param name="tag">Optional. Supply the cache tag from the item object to receive strong caching headers.</param>
 916    /// <param name="format">Determines the output format of the image - original,gif,jpg,png.</param>
 917    /// <param name="maxWidth">The maximum image width to return.</param>
 918    /// <param name="maxHeight">The maximum image height to return.</param>
 919    /// <param name="percentPlayed">Optional. Percent to render for the percent played overlay.</param>
 920    /// <param name="unplayedCount">Optional. Unplayed count overlay to render.</param>
 921    /// <param name="width">The fixed image width to return.</param>
 922    /// <param name="height">The fixed image height to return.</param>
 923    /// <param name="quality">Optional. Quality setting, from 0-100. Defaults to 90 and should suffice in most cases.</p
 924    /// <param name="fillWidth">Width of box to fill.</param>
 925    /// <param name="fillHeight">Height of box to fill.</param>
 926    /// <param name="blur">Optional. Blur image.</param>
 927    /// <param name="backgroundColor">Optional. Apply a background color for transparent images.</param>
 928    /// <param name="foregroundLayer">Optional. Apply a foreground layer on top of the image.</param>
 929    /// <response code="200">Image stream returned.</response>
 930    /// <response code="404">Item not found.</response>
 931    /// <returns>
 932    /// A <see cref="FileStreamResult"/> containing the file stream on success,
 933    /// or a <see cref="NotFoundResult"/> if item not found.
 934    /// </returns>
 935    [HttpGet("Genres/{name}/Images/{imageType}/{imageIndex}")]
 936    [HttpHead("Genres/{name}/Images/{imageType}/{imageIndex}", Name = "HeadGenreImageByIndex")]
 937    [ProducesResponseType(StatusCodes.Status200OK)]
 938    [ProducesResponseType(StatusCodes.Status404NotFound)]
 939    [ProducesImageFile]
 940    public async Task<ActionResult> GetGenreImageByIndex(
 941        [FromRoute, Required] string name,
 942        [FromRoute, Required] ImageType imageType,
 943        [FromRoute, Required] int imageIndex,
 944        [FromQuery] string? tag,
 945        [FromQuery] ImageFormat? format,
 946        [FromQuery] int? maxWidth,
 947        [FromQuery] int? maxHeight,
 948        [FromQuery] double? percentPlayed,
 949        [FromQuery] int? unplayedCount,
 950        [FromQuery] int? width,
 951        [FromQuery] int? height,
 952        [FromQuery] int? quality,
 953        [FromQuery] int? fillWidth,
 954        [FromQuery] int? fillHeight,
 955        [FromQuery] int? blur,
 956        [FromQuery] string? backgroundColor,
 957        [FromQuery] string? foregroundLayer)
 958    {
 959        var item = _libraryManager.GetGenre(name);
 960        if (item is null)
 961        {
 962            return NotFound();
 963        }
 964
 965        return await GetImageInternal(
 966                item.Id,
 967                imageType,
 968                imageIndex,
 969                tag,
 970                format,
 971                maxWidth,
 972                maxHeight,
 973                percentPlayed,
 974                unplayedCount,
 975                width,
 976                height,
 977                quality,
 978                fillWidth,
 979                fillHeight,
 980                blur,
 981                backgroundColor,
 982                foregroundLayer,
 983                item)
 984            .ConfigureAwait(false);
 985    }
 986
 987    /// <summary>
 988    /// Get music genre image by name.
 989    /// </summary>
 990    /// <param name="name">Music genre name.</param>
 991    /// <param name="imageType">Image type.</param>
 992    /// <param name="tag">Optional. Supply the cache tag from the item object to receive strong caching headers.</param>
 993    /// <param name="format">Determines the output format of the image - original,gif,jpg,png.</param>
 994    /// <param name="maxWidth">The maximum image width to return.</param>
 995    /// <param name="maxHeight">The maximum image height to return.</param>
 996    /// <param name="percentPlayed">Optional. Percent to render for the percent played overlay.</param>
 997    /// <param name="unplayedCount">Optional. Unplayed count overlay to render.</param>
 998    /// <param name="width">The fixed image width to return.</param>
 999    /// <param name="height">The fixed image height to return.</param>
 1000    /// <param name="quality">Optional. Quality setting, from 0-100. Defaults to 90 and should suffice in most cases.</p
 1001    /// <param name="fillWidth">Width of box to fill.</param>
 1002    /// <param name="fillHeight">Height of box to fill.</param>
 1003    /// <param name="blur">Optional. Blur image.</param>
 1004    /// <param name="backgroundColor">Optional. Apply a background color for transparent images.</param>
 1005    /// <param name="foregroundLayer">Optional. Apply a foreground layer on top of the image.</param>
 1006    /// <param name="imageIndex">Image index.</param>
 1007    /// <response code="200">Image stream returned.</response>
 1008    /// <response code="404">Item not found.</response>
 1009    /// <returns>
 1010    /// A <see cref="FileStreamResult"/> containing the file stream on success,
 1011    /// or a <see cref="NotFoundResult"/> if item not found.
 1012    /// </returns>
 1013    [HttpGet("MusicGenres/{name}/Images/{imageType}")]
 1014    [HttpHead("MusicGenres/{name}/Images/{imageType}", Name = "HeadMusicGenreImage")]
 1015    [ProducesResponseType(StatusCodes.Status200OK)]
 1016    [ProducesResponseType(StatusCodes.Status404NotFound)]
 1017    [ProducesImageFile]
 1018    public async Task<ActionResult> GetMusicGenreImage(
 1019        [FromRoute, Required] string name,
 1020        [FromRoute, Required] ImageType imageType,
 1021        [FromQuery] string? tag,
 1022        [FromQuery] ImageFormat? format,
 1023        [FromQuery] int? maxWidth,
 1024        [FromQuery] int? maxHeight,
 1025        [FromQuery] double? percentPlayed,
 1026        [FromQuery] int? unplayedCount,
 1027        [FromQuery] int? width,
 1028        [FromQuery] int? height,
 1029        [FromQuery] int? quality,
 1030        [FromQuery] int? fillWidth,
 1031        [FromQuery] int? fillHeight,
 1032        [FromQuery] int? blur,
 1033        [FromQuery] string? backgroundColor,
 1034        [FromQuery] string? foregroundLayer,
 1035        [FromQuery] int? imageIndex)
 1036    {
 1037        var item = _libraryManager.GetMusicGenre(name);
 1038        if (item is null)
 1039        {
 1040            return NotFound();
 1041        }
 1042
 1043        return await GetImageInternal(
 1044                item.Id,
 1045                imageType,
 1046                imageIndex,
 1047                tag,
 1048                format,
 1049                maxWidth,
 1050                maxHeight,
 1051                percentPlayed,
 1052                unplayedCount,
 1053                width,
 1054                height,
 1055                quality,
 1056                fillWidth,
 1057                fillHeight,
 1058                blur,
 1059                backgroundColor,
 1060                foregroundLayer,
 1061                item)
 1062            .ConfigureAwait(false);
 1063    }
 1064
 1065    /// <summary>
 1066    /// Get music genre image by name.
 1067    /// </summary>
 1068    /// <param name="name">Music genre name.</param>
 1069    /// <param name="imageType">Image type.</param>
 1070    /// <param name="imageIndex">Image index.</param>
 1071    /// <param name="tag">Optional. Supply the cache tag from the item object to receive strong caching headers.</param>
 1072    /// <param name="format">Determines the output format of the image - original,gif,jpg,png.</param>
 1073    /// <param name="maxWidth">The maximum image width to return.</param>
 1074    /// <param name="maxHeight">The maximum image height to return.</param>
 1075    /// <param name="percentPlayed">Optional. Percent to render for the percent played overlay.</param>
 1076    /// <param name="unplayedCount">Optional. Unplayed count overlay to render.</param>
 1077    /// <param name="width">The fixed image width to return.</param>
 1078    /// <param name="height">The fixed image height to return.</param>
 1079    /// <param name="quality">Optional. Quality setting, from 0-100. Defaults to 90 and should suffice in most cases.</p
 1080    /// <param name="fillWidth">Width of box to fill.</param>
 1081    /// <param name="fillHeight">Height of box to fill.</param>
 1082    /// <param name="blur">Optional. Blur image.</param>
 1083    /// <param name="backgroundColor">Optional. Apply a background color for transparent images.</param>
 1084    /// <param name="foregroundLayer">Optional. Apply a foreground layer on top of the image.</param>
 1085    /// <response code="200">Image stream returned.</response>
 1086    /// <response code="404">Item not found.</response>
 1087    /// <returns>
 1088    /// A <see cref="FileStreamResult"/> containing the file stream on success,
 1089    /// or a <see cref="NotFoundResult"/> if item not found.
 1090    /// </returns>
 1091    [HttpGet("MusicGenres/{name}/Images/{imageType}/{imageIndex}")]
 1092    [HttpHead("MusicGenres/{name}/Images/{imageType}/{imageIndex}", Name = "HeadMusicGenreImageByIndex")]
 1093    [ProducesResponseType(StatusCodes.Status200OK)]
 1094    [ProducesResponseType(StatusCodes.Status404NotFound)]
 1095    [ProducesImageFile]
 1096    public async Task<ActionResult> GetMusicGenreImageByIndex(
 1097        [FromRoute, Required] string name,
 1098        [FromRoute, Required] ImageType imageType,
 1099        [FromRoute, Required] int imageIndex,
 1100        [FromQuery] string? tag,
 1101        [FromQuery] ImageFormat? format,
 1102        [FromQuery] int? maxWidth,
 1103        [FromQuery] int? maxHeight,
 1104        [FromQuery] double? percentPlayed,
 1105        [FromQuery] int? unplayedCount,
 1106        [FromQuery] int? width,
 1107        [FromQuery] int? height,
 1108        [FromQuery] int? quality,
 1109        [FromQuery] int? fillWidth,
 1110        [FromQuery] int? fillHeight,
 1111        [FromQuery] int? blur,
 1112        [FromQuery] string? backgroundColor,
 1113        [FromQuery] string? foregroundLayer)
 1114    {
 1115        var item = _libraryManager.GetMusicGenre(name);
 1116        if (item is null)
 1117        {
 1118            return NotFound();
 1119        }
 1120
 1121        return await GetImageInternal(
 1122                item.Id,
 1123                imageType,
 1124                imageIndex,
 1125                tag,
 1126                format,
 1127                maxWidth,
 1128                maxHeight,
 1129                percentPlayed,
 1130                unplayedCount,
 1131                width,
 1132                height,
 1133                quality,
 1134                fillWidth,
 1135                fillHeight,
 1136                blur,
 1137                backgroundColor,
 1138                foregroundLayer,
 1139                item)
 1140            .ConfigureAwait(false);
 1141    }
 1142
 1143    /// <summary>
 1144    /// Get person image by name.
 1145    /// </summary>
 1146    /// <param name="name">Person name.</param>
 1147    /// <param name="imageType">Image type.</param>
 1148    /// <param name="tag">Optional. Supply the cache tag from the item object to receive strong caching headers.</param>
 1149    /// <param name="format">Determines the output format of the image - original,gif,jpg,png.</param>
 1150    /// <param name="maxWidth">The maximum image width to return.</param>
 1151    /// <param name="maxHeight">The maximum image height to return.</param>
 1152    /// <param name="percentPlayed">Optional. Percent to render for the percent played overlay.</param>
 1153    /// <param name="unplayedCount">Optional. Unplayed count overlay to render.</param>
 1154    /// <param name="width">The fixed image width to return.</param>
 1155    /// <param name="height">The fixed image height to return.</param>
 1156    /// <param name="quality">Optional. Quality setting, from 0-100. Defaults to 90 and should suffice in most cases.</p
 1157    /// <param name="fillWidth">Width of box to fill.</param>
 1158    /// <param name="fillHeight">Height of box to fill.</param>
 1159    /// <param name="blur">Optional. Blur image.</param>
 1160    /// <param name="backgroundColor">Optional. Apply a background color for transparent images.</param>
 1161    /// <param name="foregroundLayer">Optional. Apply a foreground layer on top of the image.</param>
 1162    /// <param name="imageIndex">Image index.</param>
 1163    /// <response code="200">Image stream returned.</response>
 1164    /// <response code="404">Item not found.</response>
 1165    /// <returns>
 1166    /// A <see cref="FileStreamResult"/> containing the file stream on success,
 1167    /// or a <see cref="NotFoundResult"/> if item not found.
 1168    /// </returns>
 1169    [HttpGet("Persons/{name}/Images/{imageType}")]
 1170    [HttpHead("Persons/{name}/Images/{imageType}", Name = "HeadPersonImage")]
 1171    [ProducesResponseType(StatusCodes.Status200OK)]
 1172    [ProducesResponseType(StatusCodes.Status404NotFound)]
 1173    [ProducesImageFile]
 1174    public async Task<ActionResult> GetPersonImage(
 1175        [FromRoute, Required] string name,
 1176        [FromRoute, Required] ImageType imageType,
 1177        [FromQuery] string? tag,
 1178        [FromQuery] ImageFormat? format,
 1179        [FromQuery] int? maxWidth,
 1180        [FromQuery] int? maxHeight,
 1181        [FromQuery] double? percentPlayed,
 1182        [FromQuery] int? unplayedCount,
 1183        [FromQuery] int? width,
 1184        [FromQuery] int? height,
 1185        [FromQuery] int? quality,
 1186        [FromQuery] int? fillWidth,
 1187        [FromQuery] int? fillHeight,
 1188        [FromQuery] int? blur,
 1189        [FromQuery] string? backgroundColor,
 1190        [FromQuery] string? foregroundLayer,
 1191        [FromQuery] int? imageIndex)
 1192    {
 1193        var item = _libraryManager.GetPerson(name);
 1194        if (item is null)
 1195        {
 1196            return NotFound();
 1197        }
 1198
 1199        return await GetImageInternal(
 1200                item.Id,
 1201                imageType,
 1202                imageIndex,
 1203                tag,
 1204                format,
 1205                maxWidth,
 1206                maxHeight,
 1207                percentPlayed,
 1208                unplayedCount,
 1209                width,
 1210                height,
 1211                quality,
 1212                fillWidth,
 1213                fillHeight,
 1214                blur,
 1215                backgroundColor,
 1216                foregroundLayer,
 1217                item)
 1218            .ConfigureAwait(false);
 1219    }
 1220
 1221    /// <summary>
 1222    /// Get person image by name.
 1223    /// </summary>
 1224    /// <param name="name">Person name.</param>
 1225    /// <param name="imageType">Image type.</param>
 1226    /// <param name="imageIndex">Image index.</param>
 1227    /// <param name="tag">Optional. Supply the cache tag from the item object to receive strong caching headers.</param>
 1228    /// <param name="format">Determines the output format of the image - original,gif,jpg,png.</param>
 1229    /// <param name="maxWidth">The maximum image width to return.</param>
 1230    /// <param name="maxHeight">The maximum image height to return.</param>
 1231    /// <param name="percentPlayed">Optional. Percent to render for the percent played overlay.</param>
 1232    /// <param name="unplayedCount">Optional. Unplayed count overlay to render.</param>
 1233    /// <param name="width">The fixed image width to return.</param>
 1234    /// <param name="height">The fixed image height to return.</param>
 1235    /// <param name="quality">Optional. Quality setting, from 0-100. Defaults to 90 and should suffice in most cases.</p
 1236    /// <param name="fillWidth">Width of box to fill.</param>
 1237    /// <param name="fillHeight">Height of box to fill.</param>
 1238    /// <param name="blur">Optional. Blur image.</param>
 1239    /// <param name="backgroundColor">Optional. Apply a background color for transparent images.</param>
 1240    /// <param name="foregroundLayer">Optional. Apply a foreground layer on top of the image.</param>
 1241    /// <response code="200">Image stream returned.</response>
 1242    /// <response code="404">Item not found.</response>
 1243    /// <returns>
 1244    /// A <see cref="FileStreamResult"/> containing the file stream on success,
 1245    /// or a <see cref="NotFoundResult"/> if item not found.
 1246    /// </returns>
 1247    [HttpGet("Persons/{name}/Images/{imageType}/{imageIndex}")]
 1248    [HttpHead("Persons/{name}/Images/{imageType}/{imageIndex}", Name = "HeadPersonImageByIndex")]
 1249    [ProducesResponseType(StatusCodes.Status200OK)]
 1250    [ProducesResponseType(StatusCodes.Status404NotFound)]
 1251    [ProducesImageFile]
 1252    public async Task<ActionResult> GetPersonImageByIndex(
 1253        [FromRoute, Required] string name,
 1254        [FromRoute, Required] ImageType imageType,
 1255        [FromRoute, Required] int imageIndex,
 1256        [FromQuery] string? tag,
 1257        [FromQuery] ImageFormat? format,
 1258        [FromQuery] int? maxWidth,
 1259        [FromQuery] int? maxHeight,
 1260        [FromQuery] double? percentPlayed,
 1261        [FromQuery] int? unplayedCount,
 1262        [FromQuery] int? width,
 1263        [FromQuery] int? height,
 1264        [FromQuery] int? quality,
 1265        [FromQuery] int? fillWidth,
 1266        [FromQuery] int? fillHeight,
 1267        [FromQuery] int? blur,
 1268        [FromQuery] string? backgroundColor,
 1269        [FromQuery] string? foregroundLayer)
 1270    {
 1271        var item = _libraryManager.GetPerson(name);
 1272        if (item is null)
 1273        {
 1274            return NotFound();
 1275        }
 1276
 1277        return await GetImageInternal(
 1278                item.Id,
 1279                imageType,
 1280                imageIndex,
 1281                tag,
 1282                format,
 1283                maxWidth,
 1284                maxHeight,
 1285                percentPlayed,
 1286                unplayedCount,
 1287                width,
 1288                height,
 1289                quality,
 1290                fillWidth,
 1291                fillHeight,
 1292                blur,
 1293                backgroundColor,
 1294                foregroundLayer,
 1295                item)
 1296            .ConfigureAwait(false);
 1297    }
 1298
 1299    /// <summary>
 1300    /// Get studio image by name.
 1301    /// </summary>
 1302    /// <param name="name">Studio name.</param>
 1303    /// <param name="imageType">Image type.</param>
 1304    /// <param name="tag">Optional. Supply the cache tag from the item object to receive strong caching headers.</param>
 1305    /// <param name="format">Determines the output format of the image - original,gif,jpg,png.</param>
 1306    /// <param name="maxWidth">The maximum image width to return.</param>
 1307    /// <param name="maxHeight">The maximum image height to return.</param>
 1308    /// <param name="percentPlayed">Optional. Percent to render for the percent played overlay.</param>
 1309    /// <param name="unplayedCount">Optional. Unplayed count overlay to render.</param>
 1310    /// <param name="width">The fixed image width to return.</param>
 1311    /// <param name="height">The fixed image height to return.</param>
 1312    /// <param name="quality">Optional. Quality setting, from 0-100. Defaults to 90 and should suffice in most cases.</p
 1313    /// <param name="fillWidth">Width of box to fill.</param>
 1314    /// <param name="fillHeight">Height of box to fill.</param>
 1315    /// <param name="blur">Optional. Blur image.</param>
 1316    /// <param name="backgroundColor">Optional. Apply a background color for transparent images.</param>
 1317    /// <param name="foregroundLayer">Optional. Apply a foreground layer on top of the image.</param>
 1318    /// <param name="imageIndex">Image index.</param>
 1319    /// <response code="200">Image stream returned.</response>
 1320    /// <response code="404">Item not found.</response>
 1321    /// <returns>
 1322    /// A <see cref="FileStreamResult"/> containing the file stream on success,
 1323    /// or a <see cref="NotFoundResult"/> if item not found.
 1324    /// </returns>
 1325    [HttpGet("Studios/{name}/Images/{imageType}")]
 1326    [HttpHead("Studios/{name}/Images/{imageType}", Name = "HeadStudioImage")]
 1327    [ProducesResponseType(StatusCodes.Status200OK)]
 1328    [ProducesResponseType(StatusCodes.Status404NotFound)]
 1329    [ProducesImageFile]
 1330    public async Task<ActionResult> GetStudioImage(
 1331        [FromRoute, Required] string name,
 1332        [FromRoute, Required] ImageType imageType,
 1333        [FromQuery] string? tag,
 1334        [FromQuery] ImageFormat? format,
 1335        [FromQuery] int? maxWidth,
 1336        [FromQuery] int? maxHeight,
 1337        [FromQuery] double? percentPlayed,
 1338        [FromQuery] int? unplayedCount,
 1339        [FromQuery] int? width,
 1340        [FromQuery] int? height,
 1341        [FromQuery] int? quality,
 1342        [FromQuery] int? fillWidth,
 1343        [FromQuery] int? fillHeight,
 1344        [FromQuery] int? blur,
 1345        [FromQuery] string? backgroundColor,
 1346        [FromQuery] string? foregroundLayer,
 1347        [FromQuery] int? imageIndex)
 1348    {
 1349        var item = _libraryManager.GetStudio(name);
 1350        if (item is null)
 1351        {
 1352            return NotFound();
 1353        }
 1354
 1355        return await GetImageInternal(
 1356                item.Id,
 1357                imageType,
 1358                imageIndex,
 1359                tag,
 1360                format,
 1361                maxWidth,
 1362                maxHeight,
 1363                percentPlayed,
 1364                unplayedCount,
 1365                width,
 1366                height,
 1367                quality,
 1368                fillWidth,
 1369                fillHeight,
 1370                blur,
 1371                backgroundColor,
 1372                foregroundLayer,
 1373                item)
 1374            .ConfigureAwait(false);
 1375    }
 1376
 1377    /// <summary>
 1378    /// Get studio image by name.
 1379    /// </summary>
 1380    /// <param name="name">Studio name.</param>
 1381    /// <param name="imageType">Image type.</param>
 1382    /// <param name="imageIndex">Image index.</param>
 1383    /// <param name="tag">Optional. Supply the cache tag from the item object to receive strong caching headers.</param>
 1384    /// <param name="format">Determines the output format of the image - original,gif,jpg,png.</param>
 1385    /// <param name="maxWidth">The maximum image width to return.</param>
 1386    /// <param name="maxHeight">The maximum image height to return.</param>
 1387    /// <param name="percentPlayed">Optional. Percent to render for the percent played overlay.</param>
 1388    /// <param name="unplayedCount">Optional. Unplayed count overlay to render.</param>
 1389    /// <param name="width">The fixed image width to return.</param>
 1390    /// <param name="height">The fixed image height to return.</param>
 1391    /// <param name="quality">Optional. Quality setting, from 0-100. Defaults to 90 and should suffice in most cases.</p
 1392    /// <param name="fillWidth">Width of box to fill.</param>
 1393    /// <param name="fillHeight">Height of box to fill.</param>
 1394    /// <param name="blur">Optional. Blur image.</param>
 1395    /// <param name="backgroundColor">Optional. Apply a background color for transparent images.</param>
 1396    /// <param name="foregroundLayer">Optional. Apply a foreground layer on top of the image.</param>
 1397    /// <response code="200">Image stream returned.</response>
 1398    /// <response code="404">Item not found.</response>
 1399    /// <returns>
 1400    /// A <see cref="FileStreamResult"/> containing the file stream on success,
 1401    /// or a <see cref="NotFoundResult"/> if item not found.
 1402    /// </returns>
 1403    [HttpGet("Studios/{name}/Images/{imageType}/{imageIndex}")]
 1404    [HttpHead("Studios/{name}/Images/{imageType}/{imageIndex}", Name = "HeadStudioImageByIndex")]
 1405    [ProducesResponseType(StatusCodes.Status200OK)]
 1406    [ProducesResponseType(StatusCodes.Status404NotFound)]
 1407    [ProducesImageFile]
 1408    public async Task<ActionResult> GetStudioImageByIndex(
 1409        [FromRoute, Required] string name,
 1410        [FromRoute, Required] ImageType imageType,
 1411        [FromRoute, Required] int imageIndex,
 1412        [FromQuery] string? tag,
 1413        [FromQuery] ImageFormat? format,
 1414        [FromQuery] int? maxWidth,
 1415        [FromQuery] int? maxHeight,
 1416        [FromQuery] double? percentPlayed,
 1417        [FromQuery] int? unplayedCount,
 1418        [FromQuery] int? width,
 1419        [FromQuery] int? height,
 1420        [FromQuery] int? quality,
 1421        [FromQuery] int? fillWidth,
 1422        [FromQuery] int? fillHeight,
 1423        [FromQuery] int? blur,
 1424        [FromQuery] string? backgroundColor,
 1425        [FromQuery] string? foregroundLayer)
 1426    {
 1427        var item = _libraryManager.GetStudio(name);
 1428        if (item is null)
 1429        {
 1430            return NotFound();
 1431        }
 1432
 1433        return await GetImageInternal(
 1434                item.Id,
 1435                imageType,
 1436                imageIndex,
 1437                tag,
 1438                format,
 1439                maxWidth,
 1440                maxHeight,
 1441                percentPlayed,
 1442                unplayedCount,
 1443                width,
 1444                height,
 1445                quality,
 1446                fillWidth,
 1447                fillHeight,
 1448                blur,
 1449                backgroundColor,
 1450                foregroundLayer,
 1451                item)
 1452            .ConfigureAwait(false);
 1453    }
 1454
 1455    /// <summary>
 1456    /// Get user profile image.
 1457    /// </summary>
 1458    /// <param name="userId">User id.</param>
 1459    /// <param name="tag">Optional. Supply the cache tag from the item object to receive strong caching headers.</param>
 1460    /// <param name="format">Determines the output format of the image - original,gif,jpg,png.</param>
 1461    /// <param name="maxWidth">The maximum image width to return.</param>
 1462    /// <param name="maxHeight">The maximum image height to return.</param>
 1463    /// <param name="percentPlayed">Optional. Percent to render for the percent played overlay.</param>
 1464    /// <param name="unplayedCount">Optional. Unplayed count overlay to render.</param>
 1465    /// <param name="width">The fixed image width to return.</param>
 1466    /// <param name="height">The fixed image height to return.</param>
 1467    /// <param name="quality">Optional. Quality setting, from 0-100. Defaults to 90 and should suffice in most cases.</p
 1468    /// <param name="fillWidth">Width of box to fill.</param>
 1469    /// <param name="fillHeight">Height of box to fill.</param>
 1470    /// <param name="blur">Optional. Blur image.</param>
 1471    /// <param name="backgroundColor">Optional. Apply a background color for transparent images.</param>
 1472    /// <param name="foregroundLayer">Optional. Apply a foreground layer on top of the image.</param>
 1473    /// <param name="imageIndex">Image index.</param>
 1474    /// <response code="200">Image stream returned.</response>
 1475    /// <response code="400">User id not provided.</response>
 1476    /// <response code="404">Item not found.</response>
 1477    /// <returns>
 1478    /// A <see cref="FileStreamResult"/> containing the file stream on success,
 1479    /// or a <see cref="NotFoundResult"/> if item not found.
 1480    /// </returns>
 1481    [HttpGet("UserImage")]
 1482    [HttpHead("UserImage", Name = "HeadUserImage")]
 1483    [ProducesResponseType(StatusCodes.Status200OK)]
 1484    [ProducesResponseType(StatusCodes.Status400BadRequest)]
 1485    [ProducesResponseType(StatusCodes.Status404NotFound)]
 1486    [ProducesImageFile]
 1487    public async Task<ActionResult> GetUserImage(
 1488        [FromQuery] Guid? userId,
 1489        [FromQuery] string? tag,
 1490        [FromQuery] ImageFormat? format,
 1491        [FromQuery] int? maxWidth,
 1492        [FromQuery] int? maxHeight,
 1493        [FromQuery] double? percentPlayed,
 1494        [FromQuery] int? unplayedCount,
 1495        [FromQuery] int? width,
 1496        [FromQuery] int? height,
 1497        [FromQuery] int? quality,
 1498        [FromQuery] int? fillWidth,
 1499        [FromQuery] int? fillHeight,
 1500        [FromQuery] int? blur,
 1501        [FromQuery] string? backgroundColor,
 1502        [FromQuery] string? foregroundLayer,
 1503        [FromQuery] int? imageIndex)
 1504    {
 1505        var requestUserId = userId ?? User.GetUserId();
 1506        if (requestUserId.IsEmpty())
 1507        {
 1508            return BadRequest("UserId is required if unauthenticated");
 1509        }
 1510
 1511        var user = _userManager.GetUserById(requestUserId);
 1512        if (user?.ProfileImage is null)
 1513        {
 1514            return NotFound();
 1515        }
 1516
 1517        var info = new ItemImageInfo
 1518        {
 1519            Path = user.ProfileImage.Path,
 1520            Type = ImageType.Profile,
 1521            DateModified = user.ProfileImage.LastModified
 1522        };
 1523
 1524        if (width.HasValue)
 1525        {
 1526            info.Width = width.Value;
 1527        }
 1528
 1529        if (height.HasValue)
 1530        {
 1531            info.Height = height.Value;
 1532        }
 1533
 1534        return await GetImageInternal(
 1535                user.Id,
 1536                ImageType.Profile,
 1537                imageIndex,
 1538                tag,
 1539                format,
 1540                maxWidth,
 1541                maxHeight,
 1542                percentPlayed,
 1543                unplayedCount,
 1544                width,
 1545                height,
 1546                quality,
 1547                fillWidth,
 1548                fillHeight,
 1549                blur,
 1550                backgroundColor,
 1551                foregroundLayer,
 1552                null,
 1553                info)
 1554            .ConfigureAwait(false);
 1555    }
 1556
 1557    /// <summary>
 1558    /// Get user profile image.
 1559    /// </summary>
 1560    /// <param name="userId">User id.</param>
 1561    /// <param name="imageType">Image type.</param>
 1562    /// <param name="tag">Optional. Supply the cache tag from the item object to receive strong caching headers.</param>
 1563    /// <param name="format">Determines the output format of the image - original,gif,jpg,png.</param>
 1564    /// <param name="maxWidth">The maximum image width to return.</param>
 1565    /// <param name="maxHeight">The maximum image height to return.</param>
 1566    /// <param name="percentPlayed">Optional. Percent to render for the percent played overlay.</param>
 1567    /// <param name="unplayedCount">Optional. Unplayed count overlay to render.</param>
 1568    /// <param name="width">The fixed image width to return.</param>
 1569    /// <param name="height">The fixed image height to return.</param>
 1570    /// <param name="quality">Optional. Quality setting, from 0-100. Defaults to 90 and should suffice in most cases.</p
 1571    /// <param name="fillWidth">Width of box to fill.</param>
 1572    /// <param name="fillHeight">Height of box to fill.</param>
 1573    /// <param name="blur">Optional. Blur image.</param>
 1574    /// <param name="backgroundColor">Optional. Apply a background color for transparent images.</param>
 1575    /// <param name="foregroundLayer">Optional. Apply a foreground layer on top of the image.</param>
 1576    /// <param name="imageIndex">Image index.</param>
 1577    /// <response code="200">Image stream returned.</response>
 1578    /// <response code="404">Item not found.</response>
 1579    /// <returns>
 1580    /// A <see cref="FileStreamResult"/> containing the file stream on success,
 1581    /// or a <see cref="NotFoundResult"/> if item not found.
 1582    /// </returns>
 1583    [HttpGet("Users/{userId}/Images/{imageType}")]
 1584    [HttpHead("Users/{userId}/Images/{imageType}", Name = "HeadUserImageLegacy")]
 1585    [Obsolete("Kept for backwards compatibility")]
 1586    [ApiExplorerSettings(IgnoreApi = true)]
 1587    [ProducesResponseType(StatusCodes.Status200OK)]
 1588    [ProducesResponseType(StatusCodes.Status404NotFound)]
 1589    [ProducesImageFile]
 1590    public Task<ActionResult> GetUserImageLegacy(
 1591        [FromRoute, Required] Guid userId,
 1592        [FromRoute, Required] ImageType imageType,
 1593        [FromQuery] string? tag,
 1594        [FromQuery] ImageFormat? format,
 1595        [FromQuery] int? maxWidth,
 1596        [FromQuery] int? maxHeight,
 1597        [FromQuery] double? percentPlayed,
 1598        [FromQuery] int? unplayedCount,
 1599        [FromQuery] int? width,
 1600        [FromQuery] int? height,
 1601        [FromQuery] int? quality,
 1602        [FromQuery] int? fillWidth,
 1603        [FromQuery] int? fillHeight,
 1604        [FromQuery] int? blur,
 1605        [FromQuery] string? backgroundColor,
 1606        [FromQuery] string? foregroundLayer,
 1607        [FromQuery] int? imageIndex)
 1608        => GetUserImage(
 1609            userId,
 1610            tag,
 1611            format,
 1612            maxWidth,
 1613            maxHeight,
 1614            percentPlayed,
 1615            unplayedCount,
 1616            width,
 1617            height,
 1618            quality,
 1619            fillWidth,
 1620            fillHeight,
 1621            blur,
 1622            backgroundColor,
 1623            foregroundLayer,
 1624            imageIndex);
 1625
 1626    /// <summary>
 1627    /// Get user profile image.
 1628    /// </summary>
 1629    /// <param name="userId">User id.</param>
 1630    /// <param name="imageType">Image type.</param>
 1631    /// <param name="imageIndex">Image index.</param>
 1632    /// <param name="tag">Optional. Supply the cache tag from the item object to receive strong caching headers.</param>
 1633    /// <param name="format">Determines the output format of the image - original,gif,jpg,png.</param>
 1634    /// <param name="maxWidth">The maximum image width to return.</param>
 1635    /// <param name="maxHeight">The maximum image height to return.</param>
 1636    /// <param name="percentPlayed">Optional. Percent to render for the percent played overlay.</param>
 1637    /// <param name="unplayedCount">Optional. Unplayed count overlay to render.</param>
 1638    /// <param name="width">The fixed image width to return.</param>
 1639    /// <param name="height">The fixed image height to return.</param>
 1640    /// <param name="quality">Optional. Quality setting, from 0-100. Defaults to 90 and should suffice in most cases.</p
 1641    /// <param name="fillWidth">Width of box to fill.</param>
 1642    /// <param name="fillHeight">Height of box to fill.</param>
 1643    /// <param name="blur">Optional. Blur image.</param>
 1644    /// <param name="backgroundColor">Optional. Apply a background color for transparent images.</param>
 1645    /// <param name="foregroundLayer">Optional. Apply a foreground layer on top of the image.</param>
 1646    /// <response code="200">Image stream returned.</response>
 1647    /// <response code="404">Item not found.</response>
 1648    /// <returns>
 1649    /// A <see cref="FileStreamResult"/> containing the file stream on success,
 1650    /// or a <see cref="NotFoundResult"/> if item not found.
 1651    /// </returns>
 1652    [HttpGet("Users/{userId}/Images/{imageType}/{imageIndex}")]
 1653    [HttpHead("Users/{userId}/Images/{imageType}/{imageIndex}", Name = "HeadUserImageByIndexLegacy")]
 1654    [Obsolete("Kept for backwards compatibility")]
 1655    [ApiExplorerSettings(IgnoreApi = true)]
 1656    [ProducesResponseType(StatusCodes.Status200OK)]
 1657    [ProducesResponseType(StatusCodes.Status404NotFound)]
 1658    [ProducesImageFile]
 1659    public Task<ActionResult> GetUserImageByIndexLegacy(
 1660        [FromRoute, Required] Guid userId,
 1661        [FromRoute, Required] ImageType imageType,
 1662        [FromRoute, Required] int imageIndex,
 1663        [FromQuery] string? tag,
 1664        [FromQuery] ImageFormat? format,
 1665        [FromQuery] int? maxWidth,
 1666        [FromQuery] int? maxHeight,
 1667        [FromQuery] double? percentPlayed,
 1668        [FromQuery] int? unplayedCount,
 1669        [FromQuery] int? width,
 1670        [FromQuery] int? height,
 1671        [FromQuery] int? quality,
 1672        [FromQuery] int? fillWidth,
 1673        [FromQuery] int? fillHeight,
 1674        [FromQuery] int? blur,
 1675        [FromQuery] string? backgroundColor,
 1676        [FromQuery] string? foregroundLayer)
 1677        => GetUserImage(
 1678            userId,
 1679            tag,
 1680            format,
 1681            maxWidth,
 1682            maxHeight,
 1683            percentPlayed,
 1684            unplayedCount,
 1685            width,
 1686            height,
 1687            quality,
 1688            fillWidth,
 1689            fillHeight,
 1690            blur,
 1691            backgroundColor,
 1692            foregroundLayer,
 1693            imageIndex);
 1694
 1695    /// <summary>
 1696    /// Generates or gets the splashscreen.
 1697    /// </summary>
 1698    /// <param name="tag">Supply the cache tag from the item object to receive strong caching headers.</param>
 1699    /// <param name="format">Determines the output format of the image - original,gif,jpg,png.</param>
 1700    /// <param name="maxWidth">The maximum image width to return.</param>
 1701    /// <param name="maxHeight">The maximum image height to return.</param>
 1702    /// <param name="width">The fixed image width to return.</param>
 1703    /// <param name="height">The fixed image height to return.</param>
 1704    /// <param name="fillWidth">Width of box to fill.</param>
 1705    /// <param name="fillHeight">Height of box to fill.</param>
 1706    /// <param name="blur">Blur image.</param>
 1707    /// <param name="backgroundColor">Apply a background color for transparent images.</param>
 1708    /// <param name="foregroundLayer">Apply a foreground layer on top of the image.</param>
 1709    /// <param name="quality">Quality setting, from 0-100.</param>
 1710    /// <response code="200">Splashscreen returned successfully.</response>
 1711    /// <returns>The splashscreen.</returns>
 1712    [HttpGet("Branding/Splashscreen")]
 1713    [ProducesResponseType(StatusCodes.Status200OK)]
 1714    [ProducesImageFile]
 1715    public async Task<ActionResult> GetSplashscreen(
 1716        [FromQuery] string? tag,
 1717        [FromQuery] ImageFormat? format,
 1718        [FromQuery] int? maxWidth,
 1719        [FromQuery] int? maxHeight,
 1720        [FromQuery] int? width,
 1721        [FromQuery] int? height,
 1722        [FromQuery] int? fillWidth,
 1723        [FromQuery] int? fillHeight,
 1724        [FromQuery] int? blur,
 1725        [FromQuery] string? backgroundColor,
 1726        [FromQuery] string? foregroundLayer,
 1727        [FromQuery, Range(0, 100)] int quality = 90)
 1728    {
 1729        var brandingOptions = _serverConfigurationManager.GetConfiguration<BrandingOptions>("branding");
 1730        if (!brandingOptions.SplashscreenEnabled)
 1731        {
 1732            return NotFound();
 1733        }
 1734
 1735        string splashscreenPath;
 1736
 1737        if (!string.IsNullOrWhiteSpace(brandingOptions.SplashscreenLocation)
 1738            && System.IO.File.Exists(brandingOptions.SplashscreenLocation))
 1739        {
 1740            splashscreenPath = brandingOptions.SplashscreenLocation;
 1741        }
 1742        else
 1743        {
 1744            splashscreenPath = Path.Combine(_appPaths.DataPath, "splashscreen.png");
 1745            if (!System.IO.File.Exists(splashscreenPath))
 1746            {
 1747                return NotFound();
 1748            }
 1749        }
 1750
 1751        var outputFormats = GetOutputFormats(format);
 1752
 1753        TimeSpan? cacheDuration = null;
 1754        if (!string.IsNullOrEmpty(tag))
 1755        {
 1756            cacheDuration = TimeSpan.FromDays(365);
 1757        }
 1758
 1759        var options = new ImageProcessingOptions
 1760        {
 1761            Image = new ItemImageInfo
 1762            {
 1763                Path = splashscreenPath
 1764            },
 1765            Height = height,
 1766            MaxHeight = maxHeight,
 1767            MaxWidth = maxWidth,
 1768            FillHeight = fillHeight,
 1769            FillWidth = fillWidth,
 1770            Quality = quality,
 1771            Width = width,
 1772            Blur = blur,
 1773            BackgroundColor = backgroundColor,
 1774            ForegroundLayer = foregroundLayer,
 1775            SupportedOutputFormats = outputFormats
 1776        };
 1777
 1778        return await GetImageResult(
 1779                options,
 1780                cacheDuration,
 1781                ImmutableDictionary<string, string>.Empty)
 1782            .ConfigureAwait(false);
 1783    }
 1784
 1785    /// <summary>
 1786    /// Uploads a custom splashscreen.
 1787    /// The body is expected to the image contents base64 encoded.
 1788    /// </summary>
 1789    /// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
 1790    /// <response code="204">Successfully uploaded new splashscreen.</response>
 1791    /// <response code="400">Error reading MimeType from uploaded image.</response>
 1792    /// <response code="403">User does not have permission to upload splashscreen..</response>
 1793    /// <exception cref="ArgumentException">Error reading the image format.</exception>
 1794    [HttpPost("Branding/Splashscreen")]
 1795    [Authorize(Policy = Policies.RequiresElevation)]
 1796    [ProducesResponseType(StatusCodes.Status204NoContent)]
 1797    [ProducesResponseType(StatusCodes.Status400BadRequest)]
 1798    [ProducesResponseType(StatusCodes.Status403Forbidden)]
 1799    [AcceptsImageFile]
 1800    public async Task<ActionResult> UploadCustomSplashscreen()
 1801    {
 1802        if (!TryGetImageExtensionFromContentType(Request.ContentType, out var extension))
 1803        {
 1804            return BadRequest("Incorrect ContentType.");
 1805        }
 1806
 1807        var stream = GetFromBase64Stream(Request.Body);
 1808        await using (stream.ConfigureAwait(false))
 1809        {
 1810            var filePath = Path.Combine(_appPaths.DataPath, "splashscreen-upload" + extension);
 1811            var brandingOptions = _serverConfigurationManager.GetConfiguration<BrandingOptions>("branding");
 1812            brandingOptions.SplashscreenLocation = filePath;
 1813            _serverConfigurationManager.SaveConfiguration("branding", brandingOptions);
 1814
 1815            var fs = new FileStream(filePath, FileMode.Create, FileAccess.Write, FileShare.None, IODefaults.FileStreamBu
 1816            await using (fs.ConfigureAwait(false))
 1817            {
 1818                await stream.CopyToAsync(fs, CancellationToken.None).ConfigureAwait(false);
 1819            }
 1820
 1821            return NoContent();
 1822        }
 1823    }
 1824
 1825    /// <summary>
 1826    /// Delete a custom splashscreen.
 1827    /// </summary>
 1828    /// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
 1829    /// <response code="204">Successfully deleted the custom splashscreen.</response>
 1830    /// <response code="403">User does not have permission to delete splashscreen..</response>
 1831    [HttpDelete("Branding/Splashscreen")]
 1832    [Authorize(Policy = Policies.RequiresElevation)]
 1833    [ProducesResponseType(StatusCodes.Status204NoContent)]
 1834    public ActionResult DeleteCustomSplashscreen()
 1835    {
 01836        var brandingOptions = _serverConfigurationManager.GetConfiguration<BrandingOptions>("branding");
 01837        if (!string.IsNullOrEmpty(brandingOptions.SplashscreenLocation)
 01838            && System.IO.File.Exists(brandingOptions.SplashscreenLocation))
 1839        {
 01840            System.IO.File.Delete(brandingOptions.SplashscreenLocation);
 01841            brandingOptions.SplashscreenLocation = null;
 01842            _serverConfigurationManager.SaveConfiguration("branding", brandingOptions);
 1843        }
 1844
 01845        return NoContent();
 1846    }
 1847
 1848    private ImageInfo? GetImageInfo(BaseItem item, ItemImageInfo info, int? imageIndex)
 1849    {
 01850        int? width = null;
 01851        int? height = null;
 01852        string? blurhash = null;
 01853        long length = 0;
 1854
 1855        try
 1856        {
 01857            if (info.IsLocalFile)
 1858            {
 01859                var fileInfo = _fileSystem.GetFileInfo(info.Path);
 01860                length = fileInfo.Length;
 1861
 01862                blurhash = info.BlurHash;
 01863                width = info.Width;
 01864                height = info.Height;
 1865
 01866                if (width <= 0 || height <= 0)
 1867                {
 01868                    width = null;
 01869                    height = null;
 1870                }
 1871            }
 01872        }
 01873        catch (Exception ex)
 1874        {
 01875            _logger.LogError(ex, "Error getting image information for {Item}", item.Name);
 01876        }
 1877
 1878        try
 1879        {
 01880            return new ImageInfo
 01881            {
 01882                Path = info.Path,
 01883                ImageIndex = imageIndex,
 01884                ImageType = info.Type,
 01885                ImageTag = _imageProcessor.GetImageCacheTag(item, info),
 01886                Size = length,
 01887                BlurHash = blurhash,
 01888                Width = width,
 01889                Height = height
 01890            };
 1891        }
 01892        catch (Exception ex)
 1893        {
 01894            _logger.LogError(ex, "Error getting image information for {Path}", info.Path);
 01895            return null;
 1896        }
 01897    }
 1898
 1899    private async Task<ActionResult> GetImageInternal(
 1900        Guid itemId,
 1901        ImageType imageType,
 1902        int? imageIndex,
 1903        string? tag,
 1904        ImageFormat? format,
 1905        int? maxWidth,
 1906        int? maxHeight,
 1907        double? percentPlayed,
 1908        int? unplayedCount,
 1909        int? width,
 1910        int? height,
 1911        int? quality,
 1912        int? fillWidth,
 1913        int? fillHeight,
 1914        int? blur,
 1915        string? backgroundColor,
 1916        string? foregroundLayer,
 1917        BaseItem? item,
 1918        ItemImageInfo? imageInfo = null)
 1919    {
 1920        if (percentPlayed.HasValue)
 1921        {
 1922            if (percentPlayed.Value <= 0)
 1923            {
 1924                percentPlayed = null;
 1925            }
 1926            else if (percentPlayed.Value >= 100)
 1927            {
 1928                percentPlayed = null;
 1929            }
 1930        }
 1931
 1932        if (percentPlayed.HasValue)
 1933        {
 1934            unplayedCount = null;
 1935        }
 1936
 1937        if (unplayedCount.HasValue
 1938            && unplayedCount.Value <= 0)
 1939        {
 1940            unplayedCount = null;
 1941        }
 1942
 1943        if (imageInfo is null)
 1944        {
 1945            imageInfo = item?.GetImageInfo(imageType, imageIndex ?? 0);
 1946            if (imageInfo is null)
 1947            {
 1948                return NotFound(string.Format(NumberFormatInfo.InvariantInfo, "{0} does not have an image of type {1}", 
 1949            }
 1950        }
 1951
 1952        var outputFormats = GetOutputFormats(format);
 1953
 1954        TimeSpan? cacheDuration = null;
 1955
 1956        if (!string.IsNullOrEmpty(tag))
 1957        {
 1958            cacheDuration = TimeSpan.FromDays(365);
 1959        }
 1960
 1961        var responseHeaders = new Dictionary<string, string>
 1962        {
 1963            { "transferMode.dlna.org", "Interactive" },
 1964            { "realTimeInfo.dlna.org", "DLNA.ORG_TLAG=*" }
 1965        };
 1966
 1967        if (!imageInfo.IsLocalFile && item is not null)
 1968        {
 1969            imageInfo = await _libraryManager.ConvertImageToLocal(item, imageInfo, imageIndex ?? 0).ConfigureAwait(false
 1970        }
 1971
 1972        var options = new ImageProcessingOptions
 1973        {
 1974            Height = height,
 1975            ImageIndex = imageIndex ?? 0,
 1976            Image = imageInfo,
 1977            Item = item,
 1978            ItemId = itemId,
 1979            MaxHeight = maxHeight,
 1980            MaxWidth = maxWidth,
 1981            FillHeight = fillHeight,
 1982            FillWidth = fillWidth,
 1983            Quality = quality ?? 100,
 1984            Width = width,
 1985            PercentPlayed = percentPlayed ?? 0,
 1986            UnplayedCount = unplayedCount,
 1987            Blur = blur,
 1988            BackgroundColor = backgroundColor,
 1989            ForegroundLayer = foregroundLayer,
 1990            SupportedOutputFormats = outputFormats
 1991        };
 1992
 1993        return await GetImageResult(
 1994            options,
 1995            cacheDuration,
 1996            responseHeaders).ConfigureAwait(false);
 1997    }
 1998
 1999    private ImageFormat[] GetOutputFormats(ImageFormat? format)
 2000    {
 02001        if (format.HasValue)
 2002        {
 02003            return [format.Value];
 2004        }
 2005
 02006        return GetClientSupportedFormats();
 2007    }
 2008
 2009    private ImageFormat[] GetClientSupportedFormats()
 2010    {
 02011        var supportedFormats = Request.Headers.GetCommaSeparatedValues(HeaderNames.Accept);
 02012        for (var i = 0; i < supportedFormats.Length; i++)
 2013        {
 2014            // Remove charsets etc. (anything after semi-colon)
 02015            var type = supportedFormats[i];
 02016            int index = type.IndexOf(';', StringComparison.Ordinal);
 02017            if (index != -1)
 2018            {
 02019                supportedFormats[i] = type.Substring(0, index);
 2020            }
 2021        }
 2022
 02023        var acceptParam = Request.Query[HeaderNames.Accept];
 2024
 02025        var supportsWebP = SupportsFormat(supportedFormats, acceptParam, ImageFormat.Webp, false);
 2026
 02027        if (!supportsWebP)
 2028        {
 02029            var userAgent = Request.Headers[HeaderNames.UserAgent].ToString();
 02030            if (userAgent.Contains("crosswalk", StringComparison.OrdinalIgnoreCase)
 02031                && userAgent.Contains("android", StringComparison.OrdinalIgnoreCase))
 2032            {
 02033                supportsWebP = true;
 2034            }
 2035        }
 2036
 02037        var formats = new List<ImageFormat>(4);
 2038
 02039        if (supportsWebP)
 2040        {
 02041            formats.Add(ImageFormat.Webp);
 2042        }
 2043
 02044        formats.Add(ImageFormat.Jpg);
 02045        formats.Add(ImageFormat.Png);
 2046
 02047        if (SupportsFormat(supportedFormats, acceptParam, ImageFormat.Gif, true))
 2048        {
 02049            formats.Add(ImageFormat.Gif);
 2050        }
 2051
 02052        return formats.ToArray();
 2053    }
 2054
 2055    private bool SupportsFormat(IReadOnlyCollection<string> requestAcceptTypes, string? acceptParam, ImageFormat format,
 2056    {
 02057        if (requestAcceptTypes.Contains(format.GetMimeType()))
 2058        {
 02059            return true;
 2060        }
 2061
 02062        if (acceptAll && requestAcceptTypes.Contains("*/*"))
 2063        {
 02064            return true;
 2065        }
 2066
 2067        // Review if this should be jpeg, jpg or both for ImageFormat.Jpg
 02068        var normalized = format.ToString().ToLowerInvariant();
 02069        return string.Equals(acceptParam, normalized, StringComparison.OrdinalIgnoreCase);
 2070    }
 2071
 2072    private async Task<ActionResult> GetImageResult(
 2073        ImageProcessingOptions imageProcessingOptions,
 2074        TimeSpan? cacheDuration,
 2075        IDictionary<string, string> headers)
 2076    {
 2077        var (imagePath, imageContentType, dateImageModified) = await _imageProcessor.ProcessImage(imageProcessingOptions
 2078
 2079        var disableCaching = Request.Headers[HeaderNames.CacheControl].Contains("no-cache");
 2080        var parsingSuccessful = DateTime.TryParse(Request.Headers[HeaderNames.IfModifiedSince], out var ifModifiedSinceH
 2081
 2082        // if the parsing of the IfModifiedSince header was not successful, disable caching
 2083        if (!parsingSuccessful)
 2084        {
 2085            // disableCaching = true;
 2086        }
 2087
 2088        foreach (var (key, value) in headers)
 2089        {
 2090            Response.Headers.Append(key, value);
 2091        }
 2092
 2093        Response.ContentType = imageContentType ?? MediaTypeNames.Text.Plain;
 2094        Response.Headers.Append(HeaderNames.Age, Convert.ToInt64((DateTime.UtcNow - dateImageModified).TotalSeconds).ToS
 2095        Response.Headers.Append(HeaderNames.Vary, HeaderNames.Accept);
 2096
 2097        Response.Headers.ContentDisposition = "attachment";
 2098
 2099        if (disableCaching)
 2100        {
 2101            Response.Headers.Append(HeaderNames.CacheControl, "no-cache, no-store, must-revalidate");
 2102            Response.Headers.Append(HeaderNames.Pragma, "no-cache, no-store, must-revalidate");
 2103        }
 2104        else
 2105        {
 2106            if (cacheDuration.HasValue)
 2107            {
 2108                Response.Headers.Append(HeaderNames.CacheControl, "public, max-age=" + cacheDuration.Value.TotalSeconds)
 2109            }
 2110            else
 2111            {
 2112                Response.Headers.Append(HeaderNames.CacheControl, "public");
 2113            }
 2114
 2115            Response.Headers.Append(HeaderNames.LastModified, dateImageModified.ToUniversalTime().ToString("ddd, dd MMM 
 2116
 2117            // if the image was not modified since "ifModifiedSinceHeader"-header, return a HTTP status code 304 not mod
 2118            if (!(dateImageModified > ifModifiedSinceHeader) && cacheDuration.HasValue)
 2119            {
 2120                if (ifModifiedSinceHeader.Add(cacheDuration.Value) < DateTime.UtcNow)
 2121                {
 2122                    Response.StatusCode = StatusCodes.Status304NotModified;
 2123                    return new ContentResult();
 2124                }
 2125            }
 2126        }
 2127
 2128        return PhysicalFile(imagePath, imageContentType ?? MediaTypeNames.Text.Plain);
 2129    }
 2130
 2131    internal static bool TryGetImageExtensionFromContentType(string? contentType, [NotNullWhen(true)] out string? extens
 2132    {
 142133        extension = null;
 142134        if (string.IsNullOrEmpty(contentType))
 2135        {
 22136            return false;
 2137        }
 2138
 122139        if (MediaTypeHeaderValue.TryParse(contentType, out var parsedValue)
 122140            && parsedValue.MediaType.HasValue
 122141            && MimeTypes.IsImage(parsedValue.MediaType.Value))
 2142        {
 112143            extension = MimeTypes.ToExtension(parsedValue.MediaType.Value);
 112144            return extension is not null;
 2145        }
 2146
 12147        return false;
 2148    }
 2149}