< Summary - Jellyfin

Information
Class: Jellyfin.Api.Controllers.ImageController
Assembly: Jellyfin.Api
File(s): /srv/git/jellyfin/Jellyfin.Api/Controllers/ImageController.cs
Line coverage
1%
Covered lines: 9
Uncovered lines: 645
Coverable lines: 654
Total lines: 2084
Line coverage: 1.3%
Branch coverage
4%
Covered branches: 8
Total branches: 186
Branch coverage: 4.3%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Coverage history

Coverage history 0 25 50 75 100 1/23/2026 - 12:11:06 AM Line coverage: 9.2% (9/97) Branch coverage: 20% (8/40) Total lines: 20694/19/2026 - 12:14:27 AM Line coverage: 1.3% (9/645) Branch coverage: 4.4% (8/180) Total lines: 20695/4/2026 - 12:15:16 AM Line coverage: 1.3% (9/654) Branch coverage: 4.3% (8/186) Total lines: 2084 1/23/2026 - 12:11:06 AM Line coverage: 9.2% (9/97) Branch coverage: 20% (8/40) Total lines: 20694/19/2026 - 12:14:27 AM Line coverage: 1.3% (9/645) Branch coverage: 4.4% (8/180) Total lines: 20695/4/2026 - 12:15:16 AM Line coverage: 1.3% (9/654) Branch coverage: 4.3% (8/186) Total lines: 2084

Coverage delta

Coverage delta 16 -16

Metrics

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.Drawing;
 7using System.Globalization;
 8using System.IO;
 9using System.Linq;
 10using System.Net.Mime;
 11using System.Security.Cryptography;
 12using System.Threading;
 13using System.Threading.Tasks;
 14using Jellyfin.Api.Attributes;
 15using Jellyfin.Api.Extensions;
 16using Jellyfin.Api.Helpers;
 17using Jellyfin.Extensions;
 18using MediaBrowser.Common.Api;
 19using MediaBrowser.Common.Configuration;
 20using MediaBrowser.Controller.Configuration;
 21using MediaBrowser.Controller.Drawing;
 22using MediaBrowser.Controller.Entities;
 23using MediaBrowser.Controller.Library;
 24using MediaBrowser.Controller.Providers;
 25using MediaBrowser.Model.Branding;
 26using MediaBrowser.Model.Drawing;
 27using MediaBrowser.Model.Dto;
 28using MediaBrowser.Model.Entities;
 29using MediaBrowser.Model.IO;
 30using MediaBrowser.Model.Net;
 31using Microsoft.AspNetCore.Authorization;
 32using Microsoft.AspNetCore.Http;
 33using Microsoft.AspNetCore.Mvc;
 34using Microsoft.Extensions.Logging;
 35using Microsoft.Net.Http.Headers;
 36
 37namespace Jellyfin.Api.Controllers;
 38
 39/// <summary>
 40/// Image controller.
 41/// </summary>
 42[Route("")]
 43public class ImageController : BaseJellyfinApiController
 44{
 45    private readonly IUserManager _userManager;
 46    private readonly ILibraryManager _libraryManager;
 47    private readonly IProviderManager _providerManager;
 48    private readonly IImageProcessor _imageProcessor;
 49    private readonly IFileSystem _fileSystem;
 50    private readonly ILogger<ImageController> _logger;
 51    private readonly IServerConfigurationManager _serverConfigurationManager;
 52    private readonly IApplicationPaths _appPaths;
 53
 54    /// <summary>
 55    /// Initializes a new instance of the <see cref="ImageController"/> class.
 56    /// </summary>
 57    /// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param>
 58    /// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
 59    /// <param name="providerManager">Instance of the <see cref="IProviderManager"/> interface.</param>
 60    /// <param name="imageProcessor">Instance of the <see cref="IImageProcessor"/> interface.</param>
 61    /// <param name="fileSystem">Instance of the <see cref="IFileSystem"/> interface.</param>
 62    /// <param name="logger">Instance of the <see cref="ILogger{ImageController}"/> interface.</param>
 63    /// <param name="serverConfigurationManager">Instance of the <see cref="IServerConfigurationManager"/> interface.</p
 64    /// <param name="appPaths">Instance of the <see cref="IApplicationPaths"/> interface.</param>
 065    public ImageController(
 066        IUserManager userManager,
 067        ILibraryManager libraryManager,
 068        IProviderManager providerManager,
 069        IImageProcessor imageProcessor,
 070        IFileSystem fileSystem,
 071        ILogger<ImageController> logger,
 072        IServerConfigurationManager serverConfigurationManager,
 073        IApplicationPaths appPaths)
 74    {
 075        _userManager = userManager;
 076        _libraryManager = libraryManager;
 077        _providerManager = providerManager;
 078        _imageProcessor = imageProcessor;
 079        _fileSystem = fileSystem;
 080        _logger = logger;
 081        _serverConfigurationManager = serverConfigurationManager;
 082        _appPaths = appPaths;
 083    }
 84
 85    private static CryptoStream GetFromBase64Stream(Stream inputStream)
 086        => new CryptoStream(inputStream, new FromBase64Transform(), CryptoStreamMode.Read);
 87
 88    /// <summary>
 89    /// Sets the user image.
 90    /// </summary>
 91    /// <param name="userId">User Id.</param>
 92    /// <response code="204">Image updated.</response>
 93    /// <response code="403">User does not have permission to delete the image.</response>
 94    /// <response code="404">Item not found.</response>
 95    /// <returns>A <see cref="NoContentResult"/>.</returns>
 96    [HttpPost("UserImage")]
 97    [Authorize]
 98    [AcceptsImageFile]
 99    [ProducesResponseType(StatusCodes.Status204NoContent)]
 100    [ProducesResponseType(StatusCodes.Status400BadRequest)]
 101    [ProducesResponseType(StatusCodes.Status403Forbidden)]
 102    [ProducesResponseType(StatusCodes.Status404NotFound)]
 103    public async Task<ActionResult> PostUserImage(
 104        [FromQuery] Guid? userId)
 105    {
 0106        var requestUserId = RequestHelpers.GetUserId(User, userId);
 0107        var user = _userManager.GetUserById(requestUserId);
 0108        if (user is null)
 109        {
 0110            return NotFound();
 111        }
 112
 0113        if (!RequestHelpers.AssertCanUpdateUser(HttpContext.User, user, true))
 114        {
 0115            return StatusCode(StatusCodes.Status403Forbidden, "User is not allowed to update the image.");
 116        }
 117
 0118        if (!TryGetImageExtensionFromContentType(Request.ContentType, out string? extension))
 119        {
 0120            return BadRequest("Incorrect ContentType.");
 121        }
 122
 0123        var stream = GetFromBase64Stream(Request.Body);
 0124        await using (stream.ConfigureAwait(false))
 125        {
 126            // Handle image/png; charset=utf-8
 0127            var mimeType = Request.ContentType?.Split(';').FirstOrDefault();
 0128            var userDataPath = Path.Combine(_serverConfigurationManager.ApplicationPaths.UserConfigurationDirectoryPath,
 0129            if (user.ProfileImage is not null)
 130            {
 0131                await _userManager.ClearProfileImageAsync(user).ConfigureAwait(false);
 132            }
 133
 0134            user.ProfileImage = new Database.Implementations.Entities.ImageInfo(Path.Combine(userDataPath, "profile" + e
 135
 0136            await _providerManager
 0137                .SaveImage(stream, mimeType, user.ProfileImage.Path)
 0138                .ConfigureAwait(false);
 0139            await _userManager.UpdateUserAsync(user).ConfigureAwait(false);
 140
 0141            return NoContent();
 142        }
 0143    }
 144
 145    /// <summary>
 146    /// Sets the user image.
 147    /// </summary>
 148    /// <param name="userId">User Id.</param>
 149    /// <param name="imageType">(Unused) Image type.</param>
 150    /// <response code="204">Image updated.</response>
 151    /// <response code="403">User does not have permission to delete the image.</response>
 152    /// <returns>A <see cref="NoContentResult"/>.</returns>
 153    [HttpPost("Users/{userId}/Images/{imageType}")]
 154    [Authorize]
 155    [Obsolete("Kept for backwards compatibility")]
 156    [ApiExplorerSettings(IgnoreApi = true)]
 157    [AcceptsImageFile]
 158    [ProducesResponseType(StatusCodes.Status204NoContent)]
 159    [ProducesResponseType(StatusCodes.Status400BadRequest)]
 160    [ProducesResponseType(StatusCodes.Status403Forbidden)]
 161    [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "imageType", Justification = 
 162    public Task<ActionResult> PostUserImageLegacy(
 163        [FromRoute, Required] Guid userId,
 164        [FromRoute, Required] ImageType imageType)
 165        => PostUserImage(userId);
 166
 167    /// <summary>
 168    /// Sets the user image.
 169    /// </summary>
 170    /// <param name="userId">User Id.</param>
 171    /// <param name="imageType">(Unused) Image type.</param>
 172    /// <param name="index">(Unused) Image index.</param>
 173    /// <response code="204">Image updated.</response>
 174    /// <response code="403">User does not have permission to delete the image.</response>
 175    /// <returns>A <see cref="NoContentResult"/>.</returns>
 176    [HttpPost("Users/{userId}/Images/{imageType}/{index}")]
 177    [Authorize]
 178    [Obsolete("Kept for backwards compatibility")]
 179    [ApiExplorerSettings(IgnoreApi = true)]
 180    [AcceptsImageFile]
 181    [ProducesResponseType(StatusCodes.Status204NoContent)]
 182    [ProducesResponseType(StatusCodes.Status400BadRequest)]
 183    [ProducesResponseType(StatusCodes.Status403Forbidden)]
 184    [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "imageType", Justification = 
 185    [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "index", Justification = "Imp
 186    public Task<ActionResult> PostUserImageByIndexLegacy(
 187        [FromRoute, Required] Guid userId,
 188        [FromRoute, Required] ImageType imageType,
 189        [FromRoute] int index)
 190        => PostUserImage(userId);
 191
 192    /// <summary>
 193    /// Delete the user's image.
 194    /// </summary>
 195    /// <param name="userId">User Id.</param>
 196    /// <response code="204">Image deleted.</response>
 197    /// <response code="403">User does not have permission to delete the image.</response>
 198    /// <returns>A <see cref="NoContentResult"/>.</returns>
 199    [HttpDelete("UserImage")]
 200    [Authorize]
 201    [ProducesResponseType(StatusCodes.Status204NoContent)]
 202    [ProducesResponseType(StatusCodes.Status403Forbidden)]
 203    public async Task<ActionResult> DeleteUserImage(
 204        [FromQuery] Guid? userId)
 205    {
 0206        var requestUserId = RequestHelpers.GetUserId(User, userId);
 0207        var user = _userManager.GetUserById(requestUserId);
 0208        if (user is null)
 209        {
 0210            return NotFound();
 211        }
 212
 0213        if (!RequestHelpers.AssertCanUpdateUser(HttpContext.User, user, true))
 214        {
 0215            return StatusCode(StatusCodes.Status403Forbidden, "User is not allowed to delete the image.");
 216        }
 217
 0218        if (user.ProfileImage is null)
 219        {
 0220            return NoContent();
 221        }
 222
 223        try
 224        {
 0225            System.IO.File.Delete(user.ProfileImage.Path);
 0226        }
 0227        catch (IOException e)
 228        {
 0229            _logger.LogError(e, "Error deleting user profile image:");
 0230        }
 231
 0232        await _userManager.ClearProfileImageAsync(user).ConfigureAwait(false);
 0233        return NoContent();
 0234    }
 235
 236    /// <summary>
 237    /// Delete the user's image.
 238    /// </summary>
 239    /// <param name="userId">User Id.</param>
 240    /// <param name="imageType">(Unused) Image type.</param>
 241    /// <param name="index">(Unused) Image index.</param>
 242    /// <response code="204">Image deleted.</response>
 243    /// <response code="403">User does not have permission to delete the image.</response>
 244    /// <returns>A <see cref="NoContentResult"/>.</returns>
 245    [HttpDelete("Users/{userId}/Images/{imageType}")]
 246    [Authorize]
 247    [Obsolete("Kept for backwards compatibility")]
 248    [ApiExplorerSettings(IgnoreApi = true)]
 249    [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "imageType", Justification = 
 250    [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "index", Justification = "Imp
 251    [ProducesResponseType(StatusCodes.Status204NoContent)]
 252    [ProducesResponseType(StatusCodes.Status403Forbidden)]
 253    public Task<ActionResult> DeleteUserImageLegacy(
 254        [FromRoute, Required] Guid userId,
 255        [FromRoute, Required] ImageType imageType,
 256        [FromQuery] int? index = null)
 257        => DeleteUserImage(userId);
 258
 259    /// <summary>
 260    /// Delete the user's image.
 261    /// </summary>
 262    /// <param name="userId">User Id.</param>
 263    /// <param name="imageType">(Unused) Image type.</param>
 264    /// <param name="index">(Unused) Image index.</param>
 265    /// <response code="204">Image deleted.</response>
 266    /// <response code="403">User does not have permission to delete the image.</response>
 267    /// <returns>A <see cref="NoContentResult"/>.</returns>
 268    [HttpDelete("Users/{userId}/Images/{imageType}/{index}")]
 269    [Authorize]
 270    [Obsolete("Kept for backwards compatibility")]
 271    [ApiExplorerSettings(IgnoreApi = true)]
 272    [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "imageType", Justification = 
 273    [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "index", Justification = "Imp
 274    [ProducesResponseType(StatusCodes.Status204NoContent)]
 275    [ProducesResponseType(StatusCodes.Status403Forbidden)]
 276    public Task<ActionResult> DeleteUserImageByIndexLegacy(
 277        [FromRoute, Required] Guid userId,
 278        [FromRoute, Required] ImageType imageType,
 279        [FromRoute] int index)
 280        => DeleteUserImage(userId);
 281
 282    /// <summary>
 283    /// Delete an item's image.
 284    /// </summary>
 285    /// <param name="itemId">Item id.</param>
 286    /// <param name="imageType">Image type.</param>
 287    /// <param name="imageIndex">The image index.</param>
 288    /// <response code="204">Image deleted.</response>
 289    /// <response code="404">Item not found.</response>
 290    /// <returns>A <see cref="NoContentResult"/> on success, or a <see cref="NotFoundResult"/> if item not found.</retur
 291    [HttpDelete("Items/{itemId}/Images/{imageType}")]
 292    [Authorize(Policy = Policies.RequiresElevation)]
 293    [ProducesResponseType(StatusCodes.Status204NoContent)]
 294    [ProducesResponseType(StatusCodes.Status404NotFound)]
 295    public async Task<ActionResult> DeleteItemImage(
 296        [FromRoute, Required] Guid itemId,
 297        [FromRoute, Required] ImageType imageType,
 298        [FromQuery] int? imageIndex)
 299    {
 0300        var item = _libraryManager.GetItemById<BaseItem>(itemId, User.GetUserId());
 0301        if (item is null)
 302        {
 0303            return NotFound();
 304        }
 305
 0306        await item.DeleteImageAsync(imageType, imageIndex ?? 0).ConfigureAwait(false);
 0307        return NoContent();
 0308    }
 309
 310    /// <summary>
 311    /// Delete an item's image.
 312    /// </summary>
 313    /// <param name="itemId">Item id.</param>
 314    /// <param name="imageType">Image type.</param>
 315    /// <param name="imageIndex">The image index.</param>
 316    /// <response code="204">Image deleted.</response>
 317    /// <response code="404">Item not found.</response>
 318    /// <returns>A <see cref="NoContentResult"/> on success, or a <see cref="NotFoundResult"/> if item not found.</retur
 319    [HttpDelete("Items/{itemId}/Images/{imageType}/{imageIndex}")]
 320    [Authorize(Policy = Policies.RequiresElevation)]
 321    [ProducesResponseType(StatusCodes.Status204NoContent)]
 322    [ProducesResponseType(StatusCodes.Status404NotFound)]
 323    public async Task<ActionResult> DeleteItemImageByIndex(
 324        [FromRoute, Required] Guid itemId,
 325        [FromRoute, Required] ImageType imageType,
 326        [FromRoute] int imageIndex)
 327    {
 0328        var item = _libraryManager.GetItemById<BaseItem>(itemId, User.GetUserId());
 0329        if (item is null)
 330        {
 0331            return NotFound();
 332        }
 333
 0334        await item.DeleteImageAsync(imageType, imageIndex).ConfigureAwait(false);
 0335        return NoContent();
 0336    }
 337
 338    /// <summary>
 339    /// Set item image.
 340    /// </summary>
 341    /// <param name="itemId">Item id.</param>
 342    /// <param name="imageType">Image type.</param>
 343    /// <response code="204">Image saved.</response>
 344    /// <response code="404">Item not found.</response>
 345    /// <returns>A <see cref="NoContentResult"/> on success, or a <see cref="NotFoundResult"/> if item not found.</retur
 346    [HttpPost("Items/{itemId}/Images/{imageType}")]
 347    [Authorize(Policy = Policies.RequiresElevation)]
 348    [AcceptsImageFile]
 349    [ProducesResponseType(StatusCodes.Status204NoContent)]
 350    [ProducesResponseType(StatusCodes.Status400BadRequest)]
 351    [ProducesResponseType(StatusCodes.Status404NotFound)]
 352    [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "index", Justification = "Imp
 353    public async Task<ActionResult> SetItemImage(
 354        [FromRoute, Required] Guid itemId,
 355        [FromRoute, Required] ImageType imageType)
 356    {
 0357        var item = _libraryManager.GetItemById<BaseItem>(itemId, User.GetUserId());
 0358        if (item is null)
 359        {
 0360            return NotFound();
 361        }
 362
 0363        if (!TryGetImageExtensionFromContentType(Request.ContentType, out _))
 364        {
 0365            return BadRequest("Incorrect ContentType.");
 366        }
 367
 0368        var stream = GetFromBase64Stream(Request.Body);
 0369        await using (stream.ConfigureAwait(false))
 370        {
 371            // Handle image/png; charset=utf-8
 0372            var mimeType = Request.ContentType?.Split(';').FirstOrDefault();
 0373            await _providerManager.SaveImage(item, stream, mimeType, imageType, null, CancellationToken.None).ConfigureA
 0374            await item.UpdateToRepositoryAsync(ItemUpdateType.ImageUpdate, CancellationToken.None).ConfigureAwait(false)
 375
 0376            return NoContent();
 377        }
 0378    }
 379
 380    /// <summary>
 381    /// Set item image.
 382    /// </summary>
 383    /// <param name="itemId">Item id.</param>
 384    /// <param name="imageType">Image type.</param>
 385    /// <param name="imageIndex">(Unused) Image index.</param>
 386    /// <response code="204">Image saved.</response>
 387    /// <response code="404">Item not found.</response>
 388    /// <returns>A <see cref="NoContentResult"/> on success, or a <see cref="NotFoundResult"/> if item not found.</retur
 389    [HttpPost("Items/{itemId}/Images/{imageType}/{imageIndex}")]
 390    [Authorize(Policy = Policies.RequiresElevation)]
 391    [AcceptsImageFile]
 392    [ProducesResponseType(StatusCodes.Status204NoContent)]
 393    [ProducesResponseType(StatusCodes.Status400BadRequest)]
 394    [ProducesResponseType(StatusCodes.Status404NotFound)]
 395    [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "index", Justification = "Imp
 396    public async Task<ActionResult> SetItemImageByIndex(
 397        [FromRoute, Required] Guid itemId,
 398        [FromRoute, Required] ImageType imageType,
 399        [FromRoute] int imageIndex)
 400    {
 0401        var item = _libraryManager.GetItemById<BaseItem>(itemId, User.GetUserId());
 0402        if (item is null)
 403        {
 0404            return NotFound();
 405        }
 406
 0407        if (!TryGetImageExtensionFromContentType(Request.ContentType, out _))
 408        {
 0409            return BadRequest("Incorrect ContentType.");
 410        }
 411
 0412        var stream = GetFromBase64Stream(Request.Body);
 0413        await using (stream.ConfigureAwait(false))
 414        {
 415            // Handle image/png; charset=utf-8
 0416            var mimeType = Request.ContentType?.Split(';').FirstOrDefault();
 0417            await _providerManager.SaveImage(item, stream, mimeType, imageType, null, CancellationToken.None).ConfigureA
 0418            await item.UpdateToRepositoryAsync(ItemUpdateType.ImageUpdate, CancellationToken.None).ConfigureAwait(false)
 419
 0420            return NoContent();
 421        }
 0422    }
 423
 424    /// <summary>
 425    /// Updates the index for an item image.
 426    /// </summary>
 427    /// <param name="itemId">Item id.</param>
 428    /// <param name="imageType">Image type.</param>
 429    /// <param name="imageIndex">Old image index.</param>
 430    /// <param name="newIndex">New image index.</param>
 431    /// <response code="204">Image index updated.</response>
 432    /// <response code="404">Item not found.</response>
 433    /// <returns>A <see cref="NoContentResult"/> on success, or a <see cref="NotFoundResult"/> if item not found.</retur
 434    [HttpPost("Items/{itemId}/Images/{imageType}/{imageIndex}/Index")]
 435    [Authorize(Policy = Policies.RequiresElevation)]
 436    [ProducesResponseType(StatusCodes.Status204NoContent)]
 437    [ProducesResponseType(StatusCodes.Status404NotFound)]
 438    public async Task<ActionResult> UpdateItemImageIndex(
 439        [FromRoute, Required] Guid itemId,
 440        [FromRoute, Required] ImageType imageType,
 441        [FromRoute, Required] int imageIndex,
 442        [FromQuery, Required] int newIndex)
 443    {
 0444        var item = _libraryManager.GetItemById<BaseItem>(itemId, User.GetUserId());
 0445        if (item is null)
 446        {
 0447            return NotFound();
 448        }
 449
 0450        await item.SwapImagesAsync(imageType, imageIndex, newIndex).ConfigureAwait(false);
 0451        return NoContent();
 0452    }
 453
 454    /// <summary>
 455    /// Get item image infos.
 456    /// </summary>
 457    /// <param name="itemId">Item id.</param>
 458    /// <response code="200">Item images returned.</response>
 459    /// <response code="404">Item not found.</response>
 460    /// <returns>The list of image infos on success, or <see cref="NotFoundResult"/> if item not found.</returns>
 461    [HttpGet("Items/{itemId}/Images")]
 462    [Authorize]
 463    [ProducesResponseType(StatusCodes.Status200OK)]
 464    [ProducesResponseType(StatusCodes.Status404NotFound)]
 465    public async Task<ActionResult<IEnumerable<ImageInfo>>> GetItemImageInfos([FromRoute, Required] Guid itemId)
 466    {
 0467        var item = _libraryManager.GetItemById<BaseItem>(itemId, User.GetUserId());
 0468        if (item is null)
 469        {
 0470            return NotFound();
 471        }
 472
 0473        var list = new List<ImageInfo>();
 0474        var itemImages = item.ImageInfos;
 475
 0476        if (itemImages.Length == 0)
 477        {
 478            // short-circuit
 0479            return list;
 480        }
 481
 0482        await _libraryManager.UpdateImagesAsync(item).ConfigureAwait(false); // this makes sure dimensions and hashes ar
 483
 0484        foreach (var image in itemImages)
 485        {
 0486            if (!item.AllowsMultipleImages(image.Type))
 487            {
 0488                var info = GetImageInfo(item, image, null);
 489
 0490                if (info is not null)
 491                {
 0492                    list.Add(info);
 493                }
 494            }
 495        }
 496
 0497        foreach (var imageType in itemImages.Select(i => i.Type).Distinct().Where(item.AllowsMultipleImages))
 498        {
 0499            var index = 0;
 500
 501            // Prevent implicitly captured closure
 0502            var currentImageType = imageType;
 503
 0504            foreach (var image in itemImages.Where(i => i.Type == currentImageType))
 505            {
 0506                var info = GetImageInfo(item, image, index);
 507
 0508                if (info is not null)
 509                {
 0510                    list.Add(info);
 511                }
 512
 0513                index++;
 514            }
 515        }
 516
 0517        return list;
 0518    }
 519
 520    /// <summary>
 521    /// Gets the item's image.
 522    /// </summary>
 523    /// <param name="itemId">Item id.</param>
 524    /// <param name="imageType">Image type.</param>
 525    /// <param name="maxWidth">The maximum image width to return.</param>
 526    /// <param name="maxHeight">The maximum image height to return.</param>
 527    /// <param name="width">The fixed image width to return.</param>
 528    /// <param name="height">The fixed image height to return.</param>
 529    /// <param name="quality">Optional. Quality setting, from 0-100. Defaults to 90 and should suffice in most cases.</p
 530    /// <param name="fillWidth">Width of box to fill.</param>
 531    /// <param name="fillHeight">Height of box to fill.</param>
 532    /// <param name="tag">Optional. Supply the cache tag from the item object to receive strong caching headers.</param>
 533    /// <param name="format">Optional. The <see cref="ImageFormat"/> of the returned image.</param>
 534    /// <param name="percentPlayed">Optional. Percent to render for the percent played overlay.</param>
 535    /// <param name="unplayedCount">Optional. Unplayed count overlay to render.</param>
 536    /// <param name="blur">Optional. Blur image.</param>
 537    /// <param name="backgroundColor">Optional. Apply a background color for transparent images.</param>
 538    /// <param name="foregroundLayer">Optional. Apply a foreground layer on top of the image.</param>
 539    /// <param name="imageIndex">Image index.</param>
 540    /// <response code="200">Image stream returned.</response>
 541    /// <response code="404">Item not found.</response>
 542    /// <returns>
 543    /// A <see cref="FileStreamResult"/> containing the file stream on success,
 544    /// or a <see cref="NotFoundResult"/> if item not found.
 545    /// </returns>
 546    [HttpGet("Items/{itemId}/Images/{imageType}")]
 547    [HttpHead("Items/{itemId}/Images/{imageType}", Name = "HeadItemImage")]
 548    [ProducesResponseType(StatusCodes.Status200OK)]
 549    [ProducesResponseType(StatusCodes.Status404NotFound)]
 550    [ProducesImageFile]
 551    public async Task<ActionResult> GetItemImage(
 552        [FromRoute, Required] Guid itemId,
 553        [FromRoute, Required] ImageType imageType,
 554        [FromQuery] int? maxWidth,
 555        [FromQuery] int? maxHeight,
 556        [FromQuery] int? width,
 557        [FromQuery] int? height,
 558        [FromQuery] int? quality,
 559        [FromQuery] int? fillWidth,
 560        [FromQuery] int? fillHeight,
 561        [FromQuery] string? tag,
 562        [FromQuery] ImageFormat? format,
 563        [FromQuery] double? percentPlayed,
 564        [FromQuery] int? unplayedCount,
 565        [FromQuery] int? blur,
 566        [FromQuery] string? backgroundColor,
 567        [FromQuery] string? foregroundLayer,
 568        [FromQuery] int? imageIndex)
 569    {
 0570        var item = _libraryManager.GetItemById<BaseItem>(itemId, User.GetUserId());
 0571        if (item is null)
 572        {
 0573            return NotFound();
 574        }
 575
 0576        return await GetImageInternal(
 0577                itemId,
 0578                imageType,
 0579                imageIndex,
 0580                tag,
 0581                format,
 0582                maxWidth,
 0583                maxHeight,
 0584                percentPlayed,
 0585                unplayedCount,
 0586                width,
 0587                height,
 0588                quality,
 0589                fillWidth,
 0590                fillHeight,
 0591                blur,
 0592                backgroundColor,
 0593                foregroundLayer,
 0594                item)
 0595            .ConfigureAwait(false);
 0596    }
 597
 598    /// <summary>
 599    /// Gets the item's image.
 600    /// </summary>
 601    /// <param name="itemId">Item id.</param>
 602    /// <param name="imageType">Image type.</param>
 603    /// <param name="imageIndex">Image index.</param>
 604    /// <param name="maxWidth">The maximum image width to return.</param>
 605    /// <param name="maxHeight">The maximum image height to return.</param>
 606    /// <param name="width">The fixed image width to return.</param>
 607    /// <param name="height">The fixed image height to return.</param>
 608    /// <param name="quality">Optional. Quality setting, from 0-100. Defaults to 90 and should suffice in most cases.</p
 609    /// <param name="fillWidth">Width of box to fill.</param>
 610    /// <param name="fillHeight">Height of box to fill.</param>
 611    /// <param name="tag">Optional. Supply the cache tag from the item object to receive strong caching headers.</param>
 612    /// <param name="format">Optional. The <see cref="ImageFormat"/> of the returned image.</param>
 613    /// <param name="percentPlayed">Optional. Percent to render for the percent played overlay.</param>
 614    /// <param name="unplayedCount">Optional. Unplayed count overlay to render.</param>
 615    /// <param name="blur">Optional. Blur image.</param>
 616    /// <param name="backgroundColor">Optional. Apply a background color for transparent images.</param>
 617    /// <param name="foregroundLayer">Optional. Apply a foreground layer on top of the image.</param>
 618    /// <response code="200">Image stream returned.</response>
 619    /// <response code="404">Item not found.</response>
 620    /// <returns>
 621    /// A <see cref="FileStreamResult"/> containing the file stream on success,
 622    /// or a <see cref="NotFoundResult"/> if item not found.
 623    /// </returns>
 624    [HttpGet("Items/{itemId}/Images/{imageType}/{imageIndex}")]
 625    [HttpHead("Items/{itemId}/Images/{imageType}/{imageIndex}", Name = "HeadItemImageByIndex")]
 626    [ProducesResponseType(StatusCodes.Status200OK)]
 627    [ProducesResponseType(StatusCodes.Status404NotFound)]
 628    [ProducesImageFile]
 629    public async Task<ActionResult> GetItemImageByIndex(
 630        [FromRoute, Required] Guid itemId,
 631        [FromRoute, Required] ImageType imageType,
 632        [FromRoute] int imageIndex,
 633        [FromQuery] int? maxWidth,
 634        [FromQuery] int? maxHeight,
 635        [FromQuery] int? width,
 636        [FromQuery] int? height,
 637        [FromQuery] int? quality,
 638        [FromQuery] int? fillWidth,
 639        [FromQuery] int? fillHeight,
 640        [FromQuery] string? tag,
 641        [FromQuery] ImageFormat? format,
 642        [FromQuery] double? percentPlayed,
 643        [FromQuery] int? unplayedCount,
 644        [FromQuery] int? blur,
 645        [FromQuery] string? backgroundColor,
 646        [FromQuery] string? foregroundLayer)
 647    {
 0648        var item = _libraryManager.GetItemById<BaseItem>(itemId, User.GetUserId());
 0649        if (item is null)
 650        {
 0651            return NotFound();
 652        }
 653
 0654        return await GetImageInternal(
 0655                itemId,
 0656                imageType,
 0657                imageIndex,
 0658                tag,
 0659                format,
 0660                maxWidth,
 0661                maxHeight,
 0662                percentPlayed,
 0663                unplayedCount,
 0664                width,
 0665                height,
 0666                quality,
 0667                fillWidth,
 0668                fillHeight,
 0669                blur,
 0670                backgroundColor,
 0671                foregroundLayer,
 0672                item)
 0673            .ConfigureAwait(false);
 0674    }
 675
 676    /// <summary>
 677    /// Gets the item's image.
 678    /// </summary>
 679    /// <param name="itemId">Item id.</param>
 680    /// <param name="imageType">Image type.</param>
 681    /// <param name="maxWidth">The maximum image width to return.</param>
 682    /// <param name="maxHeight">The maximum image height to return.</param>
 683    /// <param name="width">The fixed image width to return.</param>
 684    /// <param name="height">The fixed image height to return.</param>
 685    /// <param name="quality">Optional. Quality setting, from 0-100. Defaults to 90 and should suffice in most cases.</p
 686    /// <param name="fillWidth">Width of box to fill.</param>
 687    /// <param name="fillHeight">Height of box to fill.</param>
 688    /// <param name="tag">Optional. Supply the cache tag from the item object to receive strong caching headers.</param>
 689    /// <param name="format">Determines the output format of the image - original,gif,jpg,png.</param>
 690    /// <param name="percentPlayed">Optional. Percent to render for the percent played overlay.</param>
 691    /// <param name="unplayedCount">Optional. Unplayed count overlay to render.</param>
 692    /// <param name="blur">Optional. Blur image.</param>
 693    /// <param name="backgroundColor">Optional. Apply a background color for transparent images.</param>
 694    /// <param name="foregroundLayer">Optional. Apply a foreground layer on top of the image.</param>
 695    /// <param name="imageIndex">Image index.</param>
 696    /// <response code="200">Image stream returned.</response>
 697    /// <response code="404">Item not found.</response>
 698    /// <returns>
 699    /// A <see cref="FileStreamResult"/> containing the file stream on success,
 700    /// or a <see cref="NotFoundResult"/> if item not found.
 701    /// </returns>
 702    [HttpGet("Items/{itemId}/Images/{imageType}/{imageIndex}/{tag}/{format}/{maxWidth}/{maxHeight}/{percentPlayed}/{unpl
 703    [HttpHead("Items/{itemId}/Images/{imageType}/{imageIndex}/{tag}/{format}/{maxWidth}/{maxHeight}/{percentPlayed}/{unp
 704    [ProducesResponseType(StatusCodes.Status200OK)]
 705    [ProducesResponseType(StatusCodes.Status404NotFound)]
 706    [ProducesImageFile]
 707    public async Task<ActionResult> GetItemImage2(
 708        [FromRoute, Required] Guid itemId,
 709        [FromRoute, Required] ImageType imageType,
 710        [FromRoute, Required] int maxWidth,
 711        [FromRoute, Required] int maxHeight,
 712        [FromQuery] int? width,
 713        [FromQuery] int? height,
 714        [FromQuery] int? quality,
 715        [FromQuery] int? fillWidth,
 716        [FromQuery] int? fillHeight,
 717        [FromRoute, Required] string tag,
 718        [FromRoute, Required] ImageFormat format,
 719        [FromRoute, Required] double percentPlayed,
 720        [FromRoute, Required] int unplayedCount,
 721        [FromQuery] int? blur,
 722        [FromQuery] string? backgroundColor,
 723        [FromQuery] string? foregroundLayer,
 724        [FromRoute, Required] int imageIndex)
 725    {
 0726        var item = _libraryManager.GetItemById<BaseItem>(itemId, User.GetUserId());
 0727        if (item is null)
 728        {
 0729            return NotFound();
 730        }
 731
 0732        return await GetImageInternal(
 0733                itemId,
 0734                imageType,
 0735                imageIndex,
 0736                tag,
 0737                format,
 0738                maxWidth,
 0739                maxHeight,
 0740                percentPlayed,
 0741                unplayedCount,
 0742                width,
 0743                height,
 0744                quality,
 0745                fillWidth,
 0746                fillHeight,
 0747                blur,
 0748                backgroundColor,
 0749                foregroundLayer,
 0750                item)
 0751            .ConfigureAwait(false);
 0752    }
 753
 754    /// <summary>
 755    /// Get artist image by name.
 756    /// </summary>
 757    /// <param name="name">Artist name.</param>
 758    /// <param name="imageType">Image type.</param>
 759    /// <param name="tag">Optional. Supply the cache tag from the item object to receive strong caching headers.</param>
 760    /// <param name="format">Determines the output format of the image - original,gif,jpg,png.</param>
 761    /// <param name="maxWidth">The maximum image width to return.</param>
 762    /// <param name="maxHeight">The maximum image height to return.</param>
 763    /// <param name="percentPlayed">Optional. Percent to render for the percent played overlay.</param>
 764    /// <param name="unplayedCount">Optional. Unplayed count overlay to render.</param>
 765    /// <param name="width">The fixed image width to return.</param>
 766    /// <param name="height">The fixed image height to return.</param>
 767    /// <param name="quality">Optional. Quality setting, from 0-100. Defaults to 90 and should suffice in most cases.</p
 768    /// <param name="fillWidth">Width of box to fill.</param>
 769    /// <param name="fillHeight">Height of box to fill.</param>
 770    /// <param name="blur">Optional. Blur image.</param>
 771    /// <param name="backgroundColor">Optional. Apply a background color for transparent images.</param>
 772    /// <param name="foregroundLayer">Optional. Apply a foreground layer on top of the image.</param>
 773    /// <param name="imageIndex">Image index.</param>
 774    /// <response code="200">Image stream returned.</response>
 775    /// <response code="404">Item not found.</response>
 776    /// <returns>
 777    /// A <see cref="FileStreamResult"/> containing the file stream on success,
 778    /// or a <see cref="NotFoundResult"/> if item not found.
 779    /// </returns>
 780    [HttpGet("Artists/{name}/Images/{imageType}/{imageIndex}")]
 781    [HttpHead("Artists/{name}/Images/{imageType}/{imageIndex}", Name = "HeadArtistImage")]
 782    [ProducesResponseType(StatusCodes.Status200OK)]
 783    [ProducesResponseType(StatusCodes.Status404NotFound)]
 784    [ProducesImageFile]
 785    public async Task<ActionResult> GetArtistImage(
 786        [FromRoute, Required] string name,
 787        [FromRoute, Required] ImageType imageType,
 788        [FromQuery] string? tag,
 789        [FromQuery] ImageFormat? format,
 790        [FromQuery] int? maxWidth,
 791        [FromQuery] int? maxHeight,
 792        [FromQuery] double? percentPlayed,
 793        [FromQuery] int? unplayedCount,
 794        [FromQuery] int? width,
 795        [FromQuery] int? height,
 796        [FromQuery] int? quality,
 797        [FromQuery] int? fillWidth,
 798        [FromQuery] int? fillHeight,
 799        [FromQuery] int? blur,
 800        [FromQuery] string? backgroundColor,
 801        [FromQuery] string? foregroundLayer,
 802        [FromRoute, Required] int imageIndex)
 803    {
 0804        var item = _libraryManager.GetArtist(name);
 0805        if (item is null)
 806        {
 0807            return NotFound();
 808        }
 809
 0810        return await GetImageInternal(
 0811                item.Id,
 0812                imageType,
 0813                imageIndex,
 0814                tag,
 0815                format,
 0816                maxWidth,
 0817                maxHeight,
 0818                percentPlayed,
 0819                unplayedCount,
 0820                width,
 0821                height,
 0822                quality,
 0823                fillWidth,
 0824                fillHeight,
 0825                blur,
 0826                backgroundColor,
 0827                foregroundLayer,
 0828                item)
 0829            .ConfigureAwait(false);
 0830    }
 831
 832    /// <summary>
 833    /// Get genre image by name.
 834    /// </summary>
 835    /// <param name="name">Genre name.</param>
 836    /// <param name="imageType">Image type.</param>
 837    /// <param name="tag">Optional. Supply the cache tag from the item object to receive strong caching headers.</param>
 838    /// <param name="format">Determines the output format of the image - original,gif,jpg,png.</param>
 839    /// <param name="maxWidth">The maximum image width to return.</param>
 840    /// <param name="maxHeight">The maximum image height to return.</param>
 841    /// <param name="percentPlayed">Optional. Percent to render for the percent played overlay.</param>
 842    /// <param name="unplayedCount">Optional. Unplayed count overlay to render.</param>
 843    /// <param name="width">The fixed image width to return.</param>
 844    /// <param name="height">The fixed image height to return.</param>
 845    /// <param name="quality">Optional. Quality setting, from 0-100. Defaults to 90 and should suffice in most cases.</p
 846    /// <param name="fillWidth">Width of box to fill.</param>
 847    /// <param name="fillHeight">Height of box to fill.</param>
 848    /// <param name="blur">Optional. Blur image.</param>
 849    /// <param name="backgroundColor">Optional. Apply a background color for transparent images.</param>
 850    /// <param name="foregroundLayer">Optional. Apply a foreground layer on top of the image.</param>
 851    /// <param name="imageIndex">Image index.</param>
 852    /// <response code="200">Image stream returned.</response>
 853    /// <response code="404">Item not found.</response>
 854    /// <returns>
 855    /// A <see cref="FileStreamResult"/> containing the file stream on success,
 856    /// or a <see cref="NotFoundResult"/> if item not found.
 857    /// </returns>
 858    [HttpGet("Genres/{name}/Images/{imageType}")]
 859    [HttpHead("Genres/{name}/Images/{imageType}", Name = "HeadGenreImage")]
 860    [ProducesResponseType(StatusCodes.Status200OK)]
 861    [ProducesResponseType(StatusCodes.Status404NotFound)]
 862    [ProducesImageFile]
 863    public async Task<ActionResult> GetGenreImage(
 864        [FromRoute, Required] string name,
 865        [FromRoute, Required] ImageType imageType,
 866        [FromQuery] string? tag,
 867        [FromQuery] ImageFormat? format,
 868        [FromQuery] int? maxWidth,
 869        [FromQuery] int? maxHeight,
 870        [FromQuery] double? percentPlayed,
 871        [FromQuery] int? unplayedCount,
 872        [FromQuery] int? width,
 873        [FromQuery] int? height,
 874        [FromQuery] int? quality,
 875        [FromQuery] int? fillWidth,
 876        [FromQuery] int? fillHeight,
 877        [FromQuery] int? blur,
 878        [FromQuery] string? backgroundColor,
 879        [FromQuery] string? foregroundLayer,
 880        [FromQuery] int? imageIndex)
 881    {
 0882        var item = _libraryManager.GetGenre(name);
 0883        if (item is null)
 884        {
 0885            return NotFound();
 886        }
 887
 0888        return await GetImageInternal(
 0889                item.Id,
 0890                imageType,
 0891                imageIndex,
 0892                tag,
 0893                format,
 0894                maxWidth,
 0895                maxHeight,
 0896                percentPlayed,
 0897                unplayedCount,
 0898                width,
 0899                height,
 0900                quality,
 0901                fillWidth,
 0902                fillHeight,
 0903                blur,
 0904                backgroundColor,
 0905                foregroundLayer,
 0906                item)
 0907            .ConfigureAwait(false);
 0908    }
 909
 910    /// <summary>
 911    /// Get genre image by name.
 912    /// </summary>
 913    /// <param name="name">Genre name.</param>
 914    /// <param name="imageType">Image type.</param>
 915    /// <param name="imageIndex">Image index.</param>
 916    /// <param name="tag">Optional. Supply the cache tag from the item object to receive strong caching headers.</param>
 917    /// <param name="format">Determines the output format of the image - original,gif,jpg,png.</param>
 918    /// <param name="maxWidth">The maximum image width to return.</param>
 919    /// <param name="maxHeight">The maximum image height to return.</param>
 920    /// <param name="percentPlayed">Optional. Percent to render for the percent played overlay.</param>
 921    /// <param name="unplayedCount">Optional. Unplayed count overlay to render.</param>
 922    /// <param name="width">The fixed image width to return.</param>
 923    /// <param name="height">The fixed image height to return.</param>
 924    /// <param name="quality">Optional. Quality setting, from 0-100. Defaults to 90 and should suffice in most cases.</p
 925    /// <param name="fillWidth">Width of box to fill.</param>
 926    /// <param name="fillHeight">Height of box to fill.</param>
 927    /// <param name="blur">Optional. Blur image.</param>
 928    /// <param name="backgroundColor">Optional. Apply a background color for transparent images.</param>
 929    /// <param name="foregroundLayer">Optional. Apply a foreground layer on top of the image.</param>
 930    /// <response code="200">Image stream returned.</response>
 931    /// <response code="404">Item not found.</response>
 932    /// <returns>
 933    /// A <see cref="FileStreamResult"/> containing the file stream on success,
 934    /// or a <see cref="NotFoundResult"/> if item not found.
 935    /// </returns>
 936    [HttpGet("Genres/{name}/Images/{imageType}/{imageIndex}")]
 937    [HttpHead("Genres/{name}/Images/{imageType}/{imageIndex}", Name = "HeadGenreImageByIndex")]
 938    [ProducesResponseType(StatusCodes.Status200OK)]
 939    [ProducesResponseType(StatusCodes.Status404NotFound)]
 940    [ProducesImageFile]
 941    public async Task<ActionResult> GetGenreImageByIndex(
 942        [FromRoute, Required] string name,
 943        [FromRoute, Required] ImageType imageType,
 944        [FromRoute, Required] int imageIndex,
 945        [FromQuery] string? tag,
 946        [FromQuery] ImageFormat? format,
 947        [FromQuery] int? maxWidth,
 948        [FromQuery] int? maxHeight,
 949        [FromQuery] double? percentPlayed,
 950        [FromQuery] int? unplayedCount,
 951        [FromQuery] int? width,
 952        [FromQuery] int? height,
 953        [FromQuery] int? quality,
 954        [FromQuery] int? fillWidth,
 955        [FromQuery] int? fillHeight,
 956        [FromQuery] int? blur,
 957        [FromQuery] string? backgroundColor,
 958        [FromQuery] string? foregroundLayer)
 959    {
 0960        var item = _libraryManager.GetGenre(name);
 0961        if (item is null)
 962        {
 0963            return NotFound();
 964        }
 965
 0966        return await GetImageInternal(
 0967                item.Id,
 0968                imageType,
 0969                imageIndex,
 0970                tag,
 0971                format,
 0972                maxWidth,
 0973                maxHeight,
 0974                percentPlayed,
 0975                unplayedCount,
 0976                width,
 0977                height,
 0978                quality,
 0979                fillWidth,
 0980                fillHeight,
 0981                blur,
 0982                backgroundColor,
 0983                foregroundLayer,
 0984                item)
 0985            .ConfigureAwait(false);
 0986    }
 987
 988    /// <summary>
 989    /// Get music genre image by name.
 990    /// </summary>
 991    /// <param name="name">Music genre name.</param>
 992    /// <param name="imageType">Image type.</param>
 993    /// <param name="tag">Optional. Supply the cache tag from the item object to receive strong caching headers.</param>
 994    /// <param name="format">Determines the output format of the image - original,gif,jpg,png.</param>
 995    /// <param name="maxWidth">The maximum image width to return.</param>
 996    /// <param name="maxHeight">The maximum image height to return.</param>
 997    /// <param name="percentPlayed">Optional. Percent to render for the percent played overlay.</param>
 998    /// <param name="unplayedCount">Optional. Unplayed count overlay to render.</param>
 999    /// <param name="width">The fixed image width to return.</param>
 1000    /// <param name="height">The fixed image height to return.</param>
 1001    /// <param name="quality">Optional. Quality setting, from 0-100. Defaults to 90 and should suffice in most cases.</p
 1002    /// <param name="fillWidth">Width of box to fill.</param>
 1003    /// <param name="fillHeight">Height of box to fill.</param>
 1004    /// <param name="blur">Optional. Blur image.</param>
 1005    /// <param name="backgroundColor">Optional. Apply a background color for transparent images.</param>
 1006    /// <param name="foregroundLayer">Optional. Apply a foreground layer on top of the image.</param>
 1007    /// <param name="imageIndex">Image index.</param>
 1008    /// <response code="200">Image stream returned.</response>
 1009    /// <response code="404">Item not found.</response>
 1010    /// <returns>
 1011    /// A <see cref="FileStreamResult"/> containing the file stream on success,
 1012    /// or a <see cref="NotFoundResult"/> if item not found.
 1013    /// </returns>
 1014    [HttpGet("MusicGenres/{name}/Images/{imageType}")]
 1015    [HttpHead("MusicGenres/{name}/Images/{imageType}", Name = "HeadMusicGenreImage")]
 1016    [ProducesResponseType(StatusCodes.Status200OK)]
 1017    [ProducesResponseType(StatusCodes.Status404NotFound)]
 1018    [ProducesImageFile]
 1019    public async Task<ActionResult> GetMusicGenreImage(
 1020        [FromRoute, Required] string name,
 1021        [FromRoute, Required] ImageType imageType,
 1022        [FromQuery] string? tag,
 1023        [FromQuery] ImageFormat? format,
 1024        [FromQuery] int? maxWidth,
 1025        [FromQuery] int? maxHeight,
 1026        [FromQuery] double? percentPlayed,
 1027        [FromQuery] int? unplayedCount,
 1028        [FromQuery] int? width,
 1029        [FromQuery] int? height,
 1030        [FromQuery] int? quality,
 1031        [FromQuery] int? fillWidth,
 1032        [FromQuery] int? fillHeight,
 1033        [FromQuery] int? blur,
 1034        [FromQuery] string? backgroundColor,
 1035        [FromQuery] string? foregroundLayer,
 1036        [FromQuery] int? imageIndex)
 1037    {
 01038        var item = _libraryManager.GetMusicGenre(name);
 01039        if (item is null)
 1040        {
 01041            return NotFound();
 1042        }
 1043
 01044        return await GetImageInternal(
 01045                item.Id,
 01046                imageType,
 01047                imageIndex,
 01048                tag,
 01049                format,
 01050                maxWidth,
 01051                maxHeight,
 01052                percentPlayed,
 01053                unplayedCount,
 01054                width,
 01055                height,
 01056                quality,
 01057                fillWidth,
 01058                fillHeight,
 01059                blur,
 01060                backgroundColor,
 01061                foregroundLayer,
 01062                item)
 01063            .ConfigureAwait(false);
 01064    }
 1065
 1066    /// <summary>
 1067    /// Get music genre image by name.
 1068    /// </summary>
 1069    /// <param name="name">Music genre name.</param>
 1070    /// <param name="imageType">Image type.</param>
 1071    /// <param name="imageIndex">Image index.</param>
 1072    /// <param name="tag">Optional. Supply the cache tag from the item object to receive strong caching headers.</param>
 1073    /// <param name="format">Determines the output format of the image - original,gif,jpg,png.</param>
 1074    /// <param name="maxWidth">The maximum image width to return.</param>
 1075    /// <param name="maxHeight">The maximum image height to return.</param>
 1076    /// <param name="percentPlayed">Optional. Percent to render for the percent played overlay.</param>
 1077    /// <param name="unplayedCount">Optional. Unplayed count overlay to render.</param>
 1078    /// <param name="width">The fixed image width to return.</param>
 1079    /// <param name="height">The fixed image height to return.</param>
 1080    /// <param name="quality">Optional. Quality setting, from 0-100. Defaults to 90 and should suffice in most cases.</p
 1081    /// <param name="fillWidth">Width of box to fill.</param>
 1082    /// <param name="fillHeight">Height of box to fill.</param>
 1083    /// <param name="blur">Optional. Blur image.</param>
 1084    /// <param name="backgroundColor">Optional. Apply a background color for transparent images.</param>
 1085    /// <param name="foregroundLayer">Optional. Apply a foreground layer on top of the image.</param>
 1086    /// <response code="200">Image stream returned.</response>
 1087    /// <response code="404">Item not found.</response>
 1088    /// <returns>
 1089    /// A <see cref="FileStreamResult"/> containing the file stream on success,
 1090    /// or a <see cref="NotFoundResult"/> if item not found.
 1091    /// </returns>
 1092    [HttpGet("MusicGenres/{name}/Images/{imageType}/{imageIndex}")]
 1093    [HttpHead("MusicGenres/{name}/Images/{imageType}/{imageIndex}", Name = "HeadMusicGenreImageByIndex")]
 1094    [ProducesResponseType(StatusCodes.Status200OK)]
 1095    [ProducesResponseType(StatusCodes.Status404NotFound)]
 1096    [ProducesImageFile]
 1097    public async Task<ActionResult> GetMusicGenreImageByIndex(
 1098        [FromRoute, Required] string name,
 1099        [FromRoute, Required] ImageType imageType,
 1100        [FromRoute, Required] int imageIndex,
 1101        [FromQuery] string? tag,
 1102        [FromQuery] ImageFormat? format,
 1103        [FromQuery] int? maxWidth,
 1104        [FromQuery] int? maxHeight,
 1105        [FromQuery] double? percentPlayed,
 1106        [FromQuery] int? unplayedCount,
 1107        [FromQuery] int? width,
 1108        [FromQuery] int? height,
 1109        [FromQuery] int? quality,
 1110        [FromQuery] int? fillWidth,
 1111        [FromQuery] int? fillHeight,
 1112        [FromQuery] int? blur,
 1113        [FromQuery] string? backgroundColor,
 1114        [FromQuery] string? foregroundLayer)
 1115    {
 01116        var item = _libraryManager.GetMusicGenre(name);
 01117        if (item is null)
 1118        {
 01119            return NotFound();
 1120        }
 1121
 01122        return await GetImageInternal(
 01123                item.Id,
 01124                imageType,
 01125                imageIndex,
 01126                tag,
 01127                format,
 01128                maxWidth,
 01129                maxHeight,
 01130                percentPlayed,
 01131                unplayedCount,
 01132                width,
 01133                height,
 01134                quality,
 01135                fillWidth,
 01136                fillHeight,
 01137                blur,
 01138                backgroundColor,
 01139                foregroundLayer,
 01140                item)
 01141            .ConfigureAwait(false);
 01142    }
 1143
 1144    /// <summary>
 1145    /// Get person image by name.
 1146    /// </summary>
 1147    /// <param name="name">Person name.</param>
 1148    /// <param name="imageType">Image type.</param>
 1149    /// <param name="tag">Optional. Supply the cache tag from the item object to receive strong caching headers.</param>
 1150    /// <param name="format">Determines the output format of the image - original,gif,jpg,png.</param>
 1151    /// <param name="maxWidth">The maximum image width to return.</param>
 1152    /// <param name="maxHeight">The maximum image height to return.</param>
 1153    /// <param name="percentPlayed">Optional. Percent to render for the percent played overlay.</param>
 1154    /// <param name="unplayedCount">Optional. Unplayed count overlay to render.</param>
 1155    /// <param name="width">The fixed image width to return.</param>
 1156    /// <param name="height">The fixed image height to return.</param>
 1157    /// <param name="quality">Optional. Quality setting, from 0-100. Defaults to 90 and should suffice in most cases.</p
 1158    /// <param name="fillWidth">Width of box to fill.</param>
 1159    /// <param name="fillHeight">Height of box to fill.</param>
 1160    /// <param name="blur">Optional. Blur image.</param>
 1161    /// <param name="backgroundColor">Optional. Apply a background color for transparent images.</param>
 1162    /// <param name="foregroundLayer">Optional. Apply a foreground layer on top of the image.</param>
 1163    /// <param name="imageIndex">Image index.</param>
 1164    /// <response code="200">Image stream returned.</response>
 1165    /// <response code="404">Item not found.</response>
 1166    /// <returns>
 1167    /// A <see cref="FileStreamResult"/> containing the file stream on success,
 1168    /// or a <see cref="NotFoundResult"/> if item not found.
 1169    /// </returns>
 1170    [HttpGet("Persons/{name}/Images/{imageType}")]
 1171    [HttpHead("Persons/{name}/Images/{imageType}", Name = "HeadPersonImage")]
 1172    [ProducesResponseType(StatusCodes.Status200OK)]
 1173    [ProducesResponseType(StatusCodes.Status404NotFound)]
 1174    [ProducesImageFile]
 1175    public async Task<ActionResult> GetPersonImage(
 1176        [FromRoute, Required] string name,
 1177        [FromRoute, Required] ImageType imageType,
 1178        [FromQuery] string? tag,
 1179        [FromQuery] ImageFormat? format,
 1180        [FromQuery] int? maxWidth,
 1181        [FromQuery] int? maxHeight,
 1182        [FromQuery] double? percentPlayed,
 1183        [FromQuery] int? unplayedCount,
 1184        [FromQuery] int? width,
 1185        [FromQuery] int? height,
 1186        [FromQuery] int? quality,
 1187        [FromQuery] int? fillWidth,
 1188        [FromQuery] int? fillHeight,
 1189        [FromQuery] int? blur,
 1190        [FromQuery] string? backgroundColor,
 1191        [FromQuery] string? foregroundLayer,
 1192        [FromQuery] int? imageIndex)
 1193    {
 01194        var item = _libraryManager.GetPerson(name);
 01195        if (item is null)
 1196        {
 01197            return NotFound();
 1198        }
 1199
 01200        return await GetImageInternal(
 01201                item.Id,
 01202                imageType,
 01203                imageIndex,
 01204                tag,
 01205                format,
 01206                maxWidth,
 01207                maxHeight,
 01208                percentPlayed,
 01209                unplayedCount,
 01210                width,
 01211                height,
 01212                quality,
 01213                fillWidth,
 01214                fillHeight,
 01215                blur,
 01216                backgroundColor,
 01217                foregroundLayer,
 01218                item)
 01219            .ConfigureAwait(false);
 01220    }
 1221
 1222    /// <summary>
 1223    /// Get person image by name.
 1224    /// </summary>
 1225    /// <param name="name">Person name.</param>
 1226    /// <param name="imageType">Image type.</param>
 1227    /// <param name="imageIndex">Image index.</param>
 1228    /// <param name="tag">Optional. Supply the cache tag from the item object to receive strong caching headers.</param>
 1229    /// <param name="format">Determines the output format of the image - original,gif,jpg,png.</param>
 1230    /// <param name="maxWidth">The maximum image width to return.</param>
 1231    /// <param name="maxHeight">The maximum image height to return.</param>
 1232    /// <param name="percentPlayed">Optional. Percent to render for the percent played overlay.</param>
 1233    /// <param name="unplayedCount">Optional. Unplayed count overlay to render.</param>
 1234    /// <param name="width">The fixed image width to return.</param>
 1235    /// <param name="height">The fixed image height to return.</param>
 1236    /// <param name="quality">Optional. Quality setting, from 0-100. Defaults to 90 and should suffice in most cases.</p
 1237    /// <param name="fillWidth">Width of box to fill.</param>
 1238    /// <param name="fillHeight">Height of box to fill.</param>
 1239    /// <param name="blur">Optional. Blur image.</param>
 1240    /// <param name="backgroundColor">Optional. Apply a background color for transparent images.</param>
 1241    /// <param name="foregroundLayer">Optional. Apply a foreground layer on top of the image.</param>
 1242    /// <response code="200">Image stream returned.</response>
 1243    /// <response code="404">Item not found.</response>
 1244    /// <returns>
 1245    /// A <see cref="FileStreamResult"/> containing the file stream on success,
 1246    /// or a <see cref="NotFoundResult"/> if item not found.
 1247    /// </returns>
 1248    [HttpGet("Persons/{name}/Images/{imageType}/{imageIndex}")]
 1249    [HttpHead("Persons/{name}/Images/{imageType}/{imageIndex}", Name = "HeadPersonImageByIndex")]
 1250    [ProducesResponseType(StatusCodes.Status200OK)]
 1251    [ProducesResponseType(StatusCodes.Status404NotFound)]
 1252    [ProducesImageFile]
 1253    public async Task<ActionResult> GetPersonImageByIndex(
 1254        [FromRoute, Required] string name,
 1255        [FromRoute, Required] ImageType imageType,
 1256        [FromRoute, Required] int imageIndex,
 1257        [FromQuery] string? tag,
 1258        [FromQuery] ImageFormat? format,
 1259        [FromQuery] int? maxWidth,
 1260        [FromQuery] int? maxHeight,
 1261        [FromQuery] double? percentPlayed,
 1262        [FromQuery] int? unplayedCount,
 1263        [FromQuery] int? width,
 1264        [FromQuery] int? height,
 1265        [FromQuery] int? quality,
 1266        [FromQuery] int? fillWidth,
 1267        [FromQuery] int? fillHeight,
 1268        [FromQuery] int? blur,
 1269        [FromQuery] string? backgroundColor,
 1270        [FromQuery] string? foregroundLayer)
 1271    {
 01272        var item = _libraryManager.GetPerson(name);
 01273        if (item is null)
 1274        {
 01275            return NotFound();
 1276        }
 1277
 01278        return await GetImageInternal(
 01279                item.Id,
 01280                imageType,
 01281                imageIndex,
 01282                tag,
 01283                format,
 01284                maxWidth,
 01285                maxHeight,
 01286                percentPlayed,
 01287                unplayedCount,
 01288                width,
 01289                height,
 01290                quality,
 01291                fillWidth,
 01292                fillHeight,
 01293                blur,
 01294                backgroundColor,
 01295                foregroundLayer,
 01296                item)
 01297            .ConfigureAwait(false);
 01298    }
 1299
 1300    /// <summary>
 1301    /// Get studio image by name.
 1302    /// </summary>
 1303    /// <param name="name">Studio name.</param>
 1304    /// <param name="imageType">Image type.</param>
 1305    /// <param name="tag">Optional. Supply the cache tag from the item object to receive strong caching headers.</param>
 1306    /// <param name="format">Determines the output format of the image - original,gif,jpg,png.</param>
 1307    /// <param name="maxWidth">The maximum image width to return.</param>
 1308    /// <param name="maxHeight">The maximum image height to return.</param>
 1309    /// <param name="percentPlayed">Optional. Percent to render for the percent played overlay.</param>
 1310    /// <param name="unplayedCount">Optional. Unplayed count overlay to render.</param>
 1311    /// <param name="width">The fixed image width to return.</param>
 1312    /// <param name="height">The fixed image height to return.</param>
 1313    /// <param name="quality">Optional. Quality setting, from 0-100. Defaults to 90 and should suffice in most cases.</p
 1314    /// <param name="fillWidth">Width of box to fill.</param>
 1315    /// <param name="fillHeight">Height of box to fill.</param>
 1316    /// <param name="blur">Optional. Blur image.</param>
 1317    /// <param name="backgroundColor">Optional. Apply a background color for transparent images.</param>
 1318    /// <param name="foregroundLayer">Optional. Apply a foreground layer on top of the image.</param>
 1319    /// <param name="imageIndex">Image index.</param>
 1320    /// <response code="200">Image stream returned.</response>
 1321    /// <response code="404">Item not found.</response>
 1322    /// <returns>
 1323    /// A <see cref="FileStreamResult"/> containing the file stream on success,
 1324    /// or a <see cref="NotFoundResult"/> if item not found.
 1325    /// </returns>
 1326    [HttpGet("Studios/{name}/Images/{imageType}")]
 1327    [HttpHead("Studios/{name}/Images/{imageType}", Name = "HeadStudioImage")]
 1328    [ProducesResponseType(StatusCodes.Status200OK)]
 1329    [ProducesResponseType(StatusCodes.Status404NotFound)]
 1330    [ProducesImageFile]
 1331    public async Task<ActionResult> GetStudioImage(
 1332        [FromRoute, Required] string name,
 1333        [FromRoute, Required] ImageType imageType,
 1334        [FromQuery] string? tag,
 1335        [FromQuery] ImageFormat? format,
 1336        [FromQuery] int? maxWidth,
 1337        [FromQuery] int? maxHeight,
 1338        [FromQuery] double? percentPlayed,
 1339        [FromQuery] int? unplayedCount,
 1340        [FromQuery] int? width,
 1341        [FromQuery] int? height,
 1342        [FromQuery] int? quality,
 1343        [FromQuery] int? fillWidth,
 1344        [FromQuery] int? fillHeight,
 1345        [FromQuery] int? blur,
 1346        [FromQuery] string? backgroundColor,
 1347        [FromQuery] string? foregroundLayer,
 1348        [FromQuery] int? imageIndex)
 1349    {
 01350        var item = _libraryManager.GetStudio(name);
 01351        if (item is null)
 1352        {
 01353            return NotFound();
 1354        }
 1355
 01356        return await GetImageInternal(
 01357                item.Id,
 01358                imageType,
 01359                imageIndex,
 01360                tag,
 01361                format,
 01362                maxWidth,
 01363                maxHeight,
 01364                percentPlayed,
 01365                unplayedCount,
 01366                width,
 01367                height,
 01368                quality,
 01369                fillWidth,
 01370                fillHeight,
 01371                blur,
 01372                backgroundColor,
 01373                foregroundLayer,
 01374                item)
 01375            .ConfigureAwait(false);
 01376    }
 1377
 1378    /// <summary>
 1379    /// Get studio image by name.
 1380    /// </summary>
 1381    /// <param name="name">Studio name.</param>
 1382    /// <param name="imageType">Image type.</param>
 1383    /// <param name="imageIndex">Image index.</param>
 1384    /// <param name="tag">Optional. Supply the cache tag from the item object to receive strong caching headers.</param>
 1385    /// <param name="format">Determines the output format of the image - original,gif,jpg,png.</param>
 1386    /// <param name="maxWidth">The maximum image width to return.</param>
 1387    /// <param name="maxHeight">The maximum image height to return.</param>
 1388    /// <param name="percentPlayed">Optional. Percent to render for the percent played overlay.</param>
 1389    /// <param name="unplayedCount">Optional. Unplayed count overlay to render.</param>
 1390    /// <param name="width">The fixed image width to return.</param>
 1391    /// <param name="height">The fixed image height to return.</param>
 1392    /// <param name="quality">Optional. Quality setting, from 0-100. Defaults to 90 and should suffice in most cases.</p
 1393    /// <param name="fillWidth">Width of box to fill.</param>
 1394    /// <param name="fillHeight">Height of box to fill.</param>
 1395    /// <param name="blur">Optional. Blur image.</param>
 1396    /// <param name="backgroundColor">Optional. Apply a background color for transparent images.</param>
 1397    /// <param name="foregroundLayer">Optional. Apply a foreground layer on top of the image.</param>
 1398    /// <response code="200">Image stream returned.</response>
 1399    /// <response code="404">Item not found.</response>
 1400    /// <returns>
 1401    /// A <see cref="FileStreamResult"/> containing the file stream on success,
 1402    /// or a <see cref="NotFoundResult"/> if item not found.
 1403    /// </returns>
 1404    [HttpGet("Studios/{name}/Images/{imageType}/{imageIndex}")]
 1405    [HttpHead("Studios/{name}/Images/{imageType}/{imageIndex}", Name = "HeadStudioImageByIndex")]
 1406    [ProducesResponseType(StatusCodes.Status200OK)]
 1407    [ProducesResponseType(StatusCodes.Status404NotFound)]
 1408    [ProducesImageFile]
 1409    public async Task<ActionResult> GetStudioImageByIndex(
 1410        [FromRoute, Required] string name,
 1411        [FromRoute, Required] ImageType imageType,
 1412        [FromRoute, Required] int imageIndex,
 1413        [FromQuery] string? tag,
 1414        [FromQuery] ImageFormat? format,
 1415        [FromQuery] int? maxWidth,
 1416        [FromQuery] int? maxHeight,
 1417        [FromQuery] double? percentPlayed,
 1418        [FromQuery] int? unplayedCount,
 1419        [FromQuery] int? width,
 1420        [FromQuery] int? height,
 1421        [FromQuery] int? quality,
 1422        [FromQuery] int? fillWidth,
 1423        [FromQuery] int? fillHeight,
 1424        [FromQuery] int? blur,
 1425        [FromQuery] string? backgroundColor,
 1426        [FromQuery] string? foregroundLayer)
 1427    {
 01428        var item = _libraryManager.GetStudio(name);
 01429        if (item is null)
 1430        {
 01431            return NotFound();
 1432        }
 1433
 01434        return await GetImageInternal(
 01435                item.Id,
 01436                imageType,
 01437                imageIndex,
 01438                tag,
 01439                format,
 01440                maxWidth,
 01441                maxHeight,
 01442                percentPlayed,
 01443                unplayedCount,
 01444                width,
 01445                height,
 01446                quality,
 01447                fillWidth,
 01448                fillHeight,
 01449                blur,
 01450                backgroundColor,
 01451                foregroundLayer,
 01452                item)
 01453            .ConfigureAwait(false);
 01454    }
 1455
 1456    /// <summary>
 1457    /// Get user profile image.
 1458    /// </summary>
 1459    /// <param name="userId">User id.</param>
 1460    /// <param name="tag">Optional. Supply the cache tag from the item object to receive strong caching headers.</param>
 1461    /// <param name="format">Determines the output format of the image - original,gif,jpg,png.</param>
 1462    /// <response code="200">Image stream returned.</response>
 1463    /// <response code="400">User id not provided.</response>
 1464    /// <response code="404">Item not found.</response>
 1465    /// <returns>
 1466    /// A <see cref="FileStreamResult"/> containing the file stream on success,
 1467    /// or a <see cref="NotFoundResult"/> if item not found.
 1468    /// </returns>
 1469    [HttpGet("UserImage")]
 1470    [HttpHead("UserImage", Name = "HeadUserImage")]
 1471    [ProducesResponseType(StatusCodes.Status200OK)]
 1472    [ProducesResponseType(StatusCodes.Status400BadRequest)]
 1473    [ProducesResponseType(StatusCodes.Status404NotFound)]
 1474    [ProducesImageFile]
 1475    public async Task<ActionResult> GetUserImage(
 1476        [FromQuery] Guid? userId,
 1477        [FromQuery] string? tag,
 1478        [FromQuery] ImageFormat? format)
 1479    {
 01480        var requestUserId = userId ?? User.GetUserId();
 01481        if (requestUserId.IsEmpty())
 1482        {
 01483            return BadRequest("UserId is required if unauthenticated");
 1484        }
 1485
 01486        var user = _userManager.GetUserById(requestUserId);
 01487        if (user?.ProfileImage is null)
 1488        {
 01489            return NotFound();
 1490        }
 1491
 01492        var info = new ItemImageInfo
 01493        {
 01494            Path = user.ProfileImage.Path,
 01495            Type = ImageType.Profile,
 01496            DateModified = user.ProfileImage.LastModified
 01497        };
 1498
 01499        return await GetImageInternal(
 01500                user.Id,
 01501                ImageType.Profile,
 01502                null,
 01503                tag,
 01504                format,
 01505                null,
 01506                null,
 01507                null,
 01508                null,
 01509                null,
 01510                null,
 01511                90,
 01512                null,
 01513                null,
 01514                null,
 01515                null,
 01516                null,
 01517                null,
 01518                info)
 01519            .ConfigureAwait(false);
 01520    }
 1521
 1522    /// <summary>
 1523    /// Get user profile image.
 1524    /// </summary>
 1525    /// <param name="userId">User id.</param>
 1526    /// <param name="imageType">Image type.</param>
 1527    /// <param name="tag">Optional. Supply the cache tag from the item object to receive strong caching headers.</param>
 1528    /// <param name="format">Determines the output format of the image - original,gif,jpg,png.</param>
 1529    /// <param name="maxWidth">The maximum image width to return.</param>
 1530    /// <param name="maxHeight">The maximum image height to return.</param>
 1531    /// <param name="percentPlayed">Optional. Percent to render for the percent played overlay.</param>
 1532    /// <param name="unplayedCount">Optional. Unplayed count overlay to render.</param>
 1533    /// <param name="width">The fixed image width to return.</param>
 1534    /// <param name="height">The fixed image height to return.</param>
 1535    /// <param name="quality">Optional. Quality setting, from 0-100. Defaults to 90 and should suffice in most cases.</p
 1536    /// <param name="fillWidth">Width of box to fill.</param>
 1537    /// <param name="fillHeight">Height of box to fill.</param>
 1538    /// <param name="blur">Optional. Blur image.</param>
 1539    /// <param name="backgroundColor">Optional. Apply a background color for transparent images.</param>
 1540    /// <param name="foregroundLayer">Optional. Apply a foreground layer on top of the image.</param>
 1541    /// <param name="imageIndex">Image index.</param>
 1542    /// <response code="200">Image stream returned.</response>
 1543    /// <response code="404">Item not found.</response>
 1544    /// <returns>
 1545    /// A <see cref="FileStreamResult"/> containing the file stream on success,
 1546    /// or a <see cref="NotFoundResult"/> if item not found.
 1547    /// </returns>
 1548    [HttpGet("Users/{userId}/Images/{imageType}")]
 1549    [HttpHead("Users/{userId}/Images/{imageType}", Name = "HeadUserImageLegacy")]
 1550    [Obsolete("Kept for backwards compatibility")]
 1551    [ApiExplorerSettings(IgnoreApi = true)]
 1552    [ProducesResponseType(StatusCodes.Status200OK)]
 1553    [ProducesResponseType(StatusCodes.Status404NotFound)]
 1554    [ProducesImageFile]
 1555    public Task<ActionResult> GetUserImageLegacy(
 1556        [FromRoute, Required] Guid userId,
 1557        [FromRoute, Required] ImageType imageType,
 1558        [FromQuery] string? tag,
 1559        [FromQuery] ImageFormat? format,
 1560        [FromQuery] int? maxWidth,
 1561        [FromQuery] int? maxHeight,
 1562        [FromQuery] double? percentPlayed,
 1563        [FromQuery] int? unplayedCount,
 1564        [FromQuery] int? width,
 1565        [FromQuery] int? height,
 1566        [FromQuery] int? quality,
 1567        [FromQuery] int? fillWidth,
 1568        [FromQuery] int? fillHeight,
 1569        [FromQuery] int? blur,
 1570        [FromQuery] string? backgroundColor,
 1571        [FromQuery] string? foregroundLayer,
 1572        [FromQuery] int? imageIndex)
 1573        => GetUserImage(
 1574            userId,
 1575            tag,
 1576            format);
 1577
 1578    /// <summary>
 1579    /// Get user profile image.
 1580    /// </summary>
 1581    /// <param name="userId">User id.</param>
 1582    /// <param name="imageType">Image type.</param>
 1583    /// <param name="imageIndex">Image index.</param>
 1584    /// <param name="tag">Optional. Supply the cache tag from the item object to receive strong caching headers.</param>
 1585    /// <param name="format">Determines the output format of the image - original,gif,jpg,png.</param>
 1586    /// <param name="maxWidth">The maximum image width to return.</param>
 1587    /// <param name="maxHeight">The maximum image height to return.</param>
 1588    /// <param name="percentPlayed">Optional. Percent to render for the percent played overlay.</param>
 1589    /// <param name="unplayedCount">Optional. Unplayed count overlay to render.</param>
 1590    /// <param name="width">The fixed image width to return.</param>
 1591    /// <param name="height">The fixed image height to return.</param>
 1592    /// <param name="quality">Optional. Quality setting, from 0-100. Defaults to 90 and should suffice in most cases.</p
 1593    /// <param name="fillWidth">Width of box to fill.</param>
 1594    /// <param name="fillHeight">Height of box to fill.</param>
 1595    /// <param name="blur">Optional. Blur image.</param>
 1596    /// <param name="backgroundColor">Optional. Apply a background color for transparent images.</param>
 1597    /// <param name="foregroundLayer">Optional. Apply a foreground layer on top of the image.</param>
 1598    /// <response code="200">Image stream returned.</response>
 1599    /// <response code="404">Item not found.</response>
 1600    /// <returns>
 1601    /// A <see cref="FileStreamResult"/> containing the file stream on success,
 1602    /// or a <see cref="NotFoundResult"/> if item not found.
 1603    /// </returns>
 1604    [HttpGet("Users/{userId}/Images/{imageType}/{imageIndex}")]
 1605    [HttpHead("Users/{userId}/Images/{imageType}/{imageIndex}", Name = "HeadUserImageByIndexLegacy")]
 1606    [Obsolete("Kept for backwards compatibility")]
 1607    [ApiExplorerSettings(IgnoreApi = true)]
 1608    [ProducesResponseType(StatusCodes.Status200OK)]
 1609    [ProducesResponseType(StatusCodes.Status404NotFound)]
 1610    [ProducesImageFile]
 1611    public Task<ActionResult> GetUserImageByIndexLegacy(
 1612        [FromRoute, Required] Guid userId,
 1613        [FromRoute, Required] ImageType imageType,
 1614        [FromRoute, Required] int imageIndex,
 1615        [FromQuery] string? tag,
 1616        [FromQuery] ImageFormat? format,
 1617        [FromQuery] int? maxWidth,
 1618        [FromQuery] int? maxHeight,
 1619        [FromQuery] double? percentPlayed,
 1620        [FromQuery] int? unplayedCount,
 1621        [FromQuery] int? width,
 1622        [FromQuery] int? height,
 1623        [FromQuery] int? quality,
 1624        [FromQuery] int? fillWidth,
 1625        [FromQuery] int? fillHeight,
 1626        [FromQuery] int? blur,
 1627        [FromQuery] string? backgroundColor,
 1628        [FromQuery] string? foregroundLayer)
 1629        => GetUserImage(
 1630            userId,
 1631            tag,
 1632            format);
 1633
 1634    /// <summary>
 1635    /// Generates or gets the splashscreen.
 1636    /// </summary>
 1637    /// <param name="tag">Supply the cache tag from the item object to receive strong caching headers.</param>
 1638    /// <param name="format">Determines the output format of the image - original,gif,jpg,png.</param>
 1639    /// <response code="200">Splashscreen returned successfully.</response>
 1640    /// <returns>The splashscreen.</returns>
 1641    [HttpGet("Branding/Splashscreen")]
 1642    [ProducesResponseType(StatusCodes.Status200OK)]
 1643    [ProducesImageFile]
 1644    public async Task<ActionResult> GetSplashscreen(
 1645        [FromQuery] string? tag,
 1646        [FromQuery] ImageFormat? format)
 1647    {
 01648        var brandingOptions = _serverConfigurationManager.GetConfiguration<BrandingOptions>("branding");
 01649        var isAdmin = User.IsInRole(Constants.UserRoles.Administrator);
 01650        if (!brandingOptions.SplashscreenEnabled && !isAdmin)
 1651        {
 01652            return NotFound();
 1653        }
 1654
 1655        string splashscreenPath;
 1656
 01657        if (!string.IsNullOrWhiteSpace(brandingOptions.SplashscreenLocation)
 01658            && System.IO.File.Exists(brandingOptions.SplashscreenLocation))
 1659        {
 01660            splashscreenPath = brandingOptions.SplashscreenLocation;
 1661        }
 1662        else
 1663        {
 01664            splashscreenPath = Path.Combine(_appPaths.DataPath, "splashscreen.png");
 01665            if (!System.IO.File.Exists(splashscreenPath))
 1666            {
 01667                return NotFound();
 1668            }
 1669        }
 1670
 01671        var outputFormats = GetOutputFormats(format);
 1672
 01673        TimeSpan? cacheDuration = null;
 01674        if (!string.IsNullOrEmpty(tag))
 1675        {
 01676            cacheDuration = TimeSpan.FromDays(365);
 1677        }
 1678
 01679        var options = new ImageProcessingOptions
 01680        {
 01681            Image = new ItemImageInfo
 01682            {
 01683                Path = splashscreenPath
 01684            },
 01685            Height = null,
 01686            MaxHeight = null,
 01687            MaxWidth = null,
 01688            FillHeight = null,
 01689            FillWidth = null,
 01690            Quality = 90,
 01691            Width = null,
 01692            Blur = null,
 01693            BackgroundColor = null,
 01694            ForegroundLayer = null,
 01695            SupportedOutputFormats = outputFormats
 01696        };
 1697
 01698        return await GetImageResult(
 01699                options,
 01700                cacheDuration,
 01701                ImmutableDictionary<string, string>.Empty,
 01702                tag)
 01703            .ConfigureAwait(false);
 01704    }
 1705
 1706    /// <summary>
 1707    /// Uploads a custom splashscreen.
 1708    /// The body is expected to the image contents base64 encoded.
 1709    /// </summary>
 1710    /// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
 1711    /// <response code="204">Successfully uploaded new splashscreen.</response>
 1712    /// <response code="400">Error reading MimeType from uploaded image.</response>
 1713    /// <response code="403">User does not have permission to upload splashscreen..</response>
 1714    /// <exception cref="ArgumentException">Error reading the image format.</exception>
 1715    [HttpPost("Branding/Splashscreen")]
 1716    [Authorize(Policy = Policies.RequiresElevation)]
 1717    [ProducesResponseType(StatusCodes.Status204NoContent)]
 1718    [ProducesResponseType(StatusCodes.Status400BadRequest)]
 1719    [ProducesResponseType(StatusCodes.Status403Forbidden)]
 1720    [AcceptsImageFile]
 1721    public async Task<ActionResult> UploadCustomSplashscreen()
 1722    {
 01723        if (!TryGetImageExtensionFromContentType(Request.ContentType, out var extension))
 1724        {
 01725            return BadRequest("Incorrect ContentType.");
 1726        }
 1727
 01728        var stream = GetFromBase64Stream(Request.Body);
 01729        await using (stream.ConfigureAwait(false))
 1730        {
 01731            var filePath = Path.Combine(_appPaths.DataPath, "splashscreen-upload" + extension);
 01732            var brandingOptions = _serverConfigurationManager.GetConfiguration<BrandingOptions>("branding");
 01733            brandingOptions.SplashscreenLocation = filePath;
 01734            _serverConfigurationManager.SaveConfiguration("branding", brandingOptions);
 1735
 01736            var fs = new FileStream(filePath, FileMode.Create, FileAccess.Write, FileShare.None, IODefaults.FileStreamBu
 01737            await using (fs.ConfigureAwait(false))
 1738            {
 01739                await stream.CopyToAsync(fs, CancellationToken.None).ConfigureAwait(false);
 1740            }
 1741
 01742            return NoContent();
 1743        }
 01744    }
 1745
 1746    /// <summary>
 1747    /// Delete a custom splashscreen.
 1748    /// </summary>
 1749    /// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
 1750    /// <response code="204">Successfully deleted the custom splashscreen.</response>
 1751    /// <response code="403">User does not have permission to delete splashscreen..</response>
 1752    [HttpDelete("Branding/Splashscreen")]
 1753    [Authorize(Policy = Policies.RequiresElevation)]
 1754    [ProducesResponseType(StatusCodes.Status204NoContent)]
 1755    public ActionResult DeleteCustomSplashscreen()
 1756    {
 01757        var brandingOptions = _serverConfigurationManager.GetConfiguration<BrandingOptions>("branding");
 01758        if (!string.IsNullOrEmpty(brandingOptions.SplashscreenLocation)
 01759            && System.IO.File.Exists(brandingOptions.SplashscreenLocation))
 1760        {
 01761            System.IO.File.Delete(brandingOptions.SplashscreenLocation);
 01762            brandingOptions.SplashscreenLocation = null;
 01763            _serverConfigurationManager.SaveConfiguration("branding", brandingOptions);
 1764        }
 1765
 01766        return NoContent();
 1767    }
 1768
 1769    private ImageInfo? GetImageInfo(BaseItem item, ItemImageInfo info, int? imageIndex)
 1770    {
 01771        int? width = null;
 01772        int? height = null;
 01773        string? blurhash = null;
 01774        long length = 0;
 1775
 1776        try
 1777        {
 01778            if (info.IsLocalFile)
 1779            {
 01780                var fileInfo = _fileSystem.GetFileInfo(info.Path);
 01781                length = fileInfo.Length;
 1782
 01783                blurhash = info.BlurHash;
 01784                width = info.Width;
 01785                height = info.Height;
 1786
 01787                if (width <= 0 || height <= 0)
 1788                {
 01789                    width = null;
 01790                    height = null;
 1791                }
 1792            }
 01793        }
 01794        catch (Exception ex)
 1795        {
 01796            _logger.LogError(ex, "Error getting image information for {Item}", item.Name);
 01797        }
 1798
 1799        try
 1800        {
 01801            return new ImageInfo
 01802            {
 01803                Path = info.Path,
 01804                ImageIndex = imageIndex,
 01805                ImageType = info.Type,
 01806                ImageTag = _imageProcessor.GetImageCacheTag(item, info),
 01807                Size = length,
 01808                BlurHash = blurhash,
 01809                Width = width,
 01810                Height = height
 01811            };
 1812        }
 01813        catch (Exception ex)
 1814        {
 01815            _logger.LogError(ex, "Error getting image information for {Path}", info.Path);
 01816            return null;
 1817        }
 01818    }
 1819
 1820    private async Task<ActionResult> GetImageInternal(
 1821        Guid itemId,
 1822        ImageType imageType,
 1823        int? imageIndex,
 1824        string? tag,
 1825        ImageFormat? format,
 1826        int? maxWidth,
 1827        int? maxHeight,
 1828        double? percentPlayed,
 1829        int? unplayedCount,
 1830        int? width,
 1831        int? height,
 1832        int? quality,
 1833        int? fillWidth,
 1834        int? fillHeight,
 1835        int? blur,
 1836        string? backgroundColor,
 1837        string? foregroundLayer,
 1838        BaseItem? item,
 1839        ItemImageInfo? imageInfo = null)
 1840    {
 01841        if (percentPlayed.HasValue)
 1842        {
 01843            if (percentPlayed.Value <= 0)
 1844            {
 01845                percentPlayed = null;
 1846            }
 01847            else if (percentPlayed.Value >= 100)
 1848            {
 01849                percentPlayed = null;
 1850            }
 1851        }
 1852
 01853        if (percentPlayed.HasValue)
 1854        {
 01855            unplayedCount = null;
 1856        }
 1857
 01858        if (unplayedCount.HasValue
 01859            && unplayedCount.Value <= 0)
 1860        {
 01861            unplayedCount = null;
 1862        }
 1863
 01864        if (imageInfo is null)
 1865        {
 01866            imageInfo = item?.GetImageInfo(imageType, imageIndex ?? 0);
 01867            if (imageInfo is null)
 1868            {
 01869                return NotFound(string.Format(NumberFormatInfo.InvariantInfo, "{0} does not have an image of type {1}", 
 1870            }
 1871        }
 1872
 01873        var outputFormats = GetOutputFormats(format);
 1874
 01875        TimeSpan? cacheDuration = null;
 1876
 01877        if (!string.IsNullOrEmpty(tag))
 1878        {
 01879            cacheDuration = TimeSpan.FromDays(365);
 1880        }
 1881
 01882        var responseHeaders = new Dictionary<string, string>
 01883        {
 01884            { "transferMode.dlna.org", "Interactive" },
 01885            { "realTimeInfo.dlna.org", "DLNA.ORG_TLAG=*" }
 01886        };
 1887
 01888        if (!imageInfo.IsLocalFile && item is not null)
 1889        {
 01890            imageInfo = await _libraryManager.ConvertImageToLocal(item, imageInfo, imageIndex ?? 0).ConfigureAwait(false
 1891        }
 1892
 01893        var options = new ImageProcessingOptions
 01894        {
 01895            Height = height,
 01896            ImageIndex = imageIndex ?? 0,
 01897            Image = imageInfo,
 01898            Item = item,
 01899            ItemId = itemId,
 01900            MaxHeight = maxHeight,
 01901            MaxWidth = maxWidth,
 01902            FillHeight = fillHeight,
 01903            FillWidth = fillWidth,
 01904            Quality = quality ?? 100,
 01905            Width = width,
 01906            PercentPlayed = percentPlayed ?? 0,
 01907            UnplayedCount = unplayedCount,
 01908            Blur = blur,
 01909            BackgroundColor = backgroundColor,
 01910            ForegroundLayer = foregroundLayer,
 01911            SupportedOutputFormats = outputFormats
 01912        };
 1913
 01914        return await GetImageResult(
 01915            options,
 01916            cacheDuration,
 01917            responseHeaders,
 01918            tag).ConfigureAwait(false);
 01919    }
 1920
 1921    private ImageFormat[] GetOutputFormats(ImageFormat? format)
 1922    {
 01923        if (format.HasValue)
 1924        {
 01925            return [format.Value];
 1926        }
 1927
 01928        return GetClientSupportedFormats();
 1929    }
 1930
 1931    private ImageFormat[] GetClientSupportedFormats()
 1932    {
 01933        var supportedFormats = Request.Headers.GetCommaSeparatedValues(HeaderNames.Accept);
 01934        for (var i = 0; i < supportedFormats.Length; i++)
 1935        {
 1936            // Remove charsets etc. (anything after semi-colon)
 01937            var type = supportedFormats[i];
 01938            int index = type.IndexOf(';', StringComparison.Ordinal);
 01939            if (index != -1)
 1940            {
 01941                supportedFormats[i] = type.Substring(0, index);
 1942            }
 1943        }
 1944
 01945        var acceptParam = Request.Query[HeaderNames.Accept];
 1946
 01947        var supportsWebP = SupportsFormat(supportedFormats, acceptParam, ImageFormat.Webp, false);
 1948
 01949        if (!supportsWebP)
 1950        {
 01951            var userAgent = Request.Headers[HeaderNames.UserAgent].ToString();
 01952            if (userAgent.Contains("crosswalk", StringComparison.OrdinalIgnoreCase)
 01953                && userAgent.Contains("android", StringComparison.OrdinalIgnoreCase))
 1954            {
 01955                supportsWebP = true;
 1956            }
 1957        }
 1958
 01959        var formats = new List<ImageFormat>(4);
 1960
 01961        if (supportsWebP)
 1962        {
 01963            formats.Add(ImageFormat.Webp);
 1964        }
 1965
 01966        formats.Add(ImageFormat.Jpg);
 01967        formats.Add(ImageFormat.Png);
 1968
 01969        if (SupportsFormat(supportedFormats, acceptParam, ImageFormat.Gif, true))
 1970        {
 01971            formats.Add(ImageFormat.Gif);
 1972        }
 1973
 01974        return formats.ToArray();
 1975    }
 1976
 1977    private bool SupportsFormat(IReadOnlyCollection<string> requestAcceptTypes, string? acceptParam, ImageFormat format,
 1978    {
 01979        if (requestAcceptTypes.Contains(format.GetMimeType()))
 1980        {
 01981            return true;
 1982        }
 1983
 01984        if (acceptAll && requestAcceptTypes.Contains("*/*"))
 1985        {
 01986            return true;
 1987        }
 1988
 1989        // Review if this should be jpeg, jpg or both for ImageFormat.Jpg
 01990        var normalized = format.ToString().ToLowerInvariant();
 01991        return string.Equals(acceptParam, normalized, StringComparison.OrdinalIgnoreCase);
 1992    }
 1993
 1994    private async Task<ActionResult> GetImageResult(
 1995        ImageProcessingOptions imageProcessingOptions,
 1996        TimeSpan? cacheDuration,
 1997        IDictionary<string, string> headers,
 1998        string? tag)
 1999    {
 02000        var (imagePath, imageContentType, dateImageModified) = await _imageProcessor.ProcessImage(imageProcessingOptions
 2001
 02002        var disableCaching = Request.Headers[HeaderNames.CacheControl].Contains("no-cache");
 02003        var hasTag = !string.IsNullOrEmpty(tag);
 2004
 02005        foreach (var (key, value) in headers)
 2006        {
 02007            Response.Headers.Append(key, value);
 2008        }
 2009
 02010        Response.ContentType = imageContentType ?? MediaTypeNames.Text.Plain;
 02011        Response.Headers.Append(HeaderNames.Age, Convert.ToInt64((DateTime.UtcNow - dateImageModified).TotalSeconds).ToS
 02012        Response.Headers.Append(HeaderNames.Vary, HeaderNames.Accept);
 2013
 02014        Response.Headers.ContentDisposition = "attachment";
 2015
 02016        if (disableCaching)
 2017        {
 02018            Response.Headers.Append(HeaderNames.CacheControl, "no-cache, no-store, must-revalidate");
 02019            Response.Headers.Append(HeaderNames.Pragma, "no-cache, no-store, must-revalidate");
 2020        }
 2021        else
 2022        {
 02023            if (cacheDuration.HasValue)
 2024            {
 2025                // When tag is provided, the URL is effectively immutable - the tag changes when the image changes
 02026                Response.Headers.Append(HeaderNames.CacheControl, "public, max-age=" + cacheDuration.Value.TotalSeconds 
 2027            }
 2028            else
 2029            {
 02030                Response.Headers.Append(HeaderNames.CacheControl, "public");
 2031            }
 2032
 02033            Response.Headers.Append(HeaderNames.LastModified, dateImageModified.ToUniversalTime().ToString("ddd, dd MMM 
 2034
 2035            // Add ETag header for stronger cache validation when tag is provided
 02036            if (hasTag)
 2037            {
 02038                Response.Headers.Append(HeaderNames.ETag, $"\"{tag}\"");
 2039
 2040                // Check If-None-Match header for ETag-based validation (preferred over If-Modified-Since)
 02041                var ifNoneMatch = Request.Headers[HeaderNames.IfNoneMatch].ToString();
 02042                if (!string.IsNullOrEmpty(ifNoneMatch)
 02043                    && (string.Equals(ifNoneMatch, $"\"{tag}\"", StringComparison.Ordinal)
 02044                        || string.Equals(ifNoneMatch, tag, StringComparison.Ordinal)))
 2045                {
 02046                    Response.StatusCode = StatusCodes.Status304NotModified;
 02047                    return new ContentResult();
 2048                }
 2049            }
 2050
 2051            // Check If-Modified-Since header for time-based validation
 02052            if (DateTime.TryParse(Request.Headers[HeaderNames.IfModifiedSince], out var ifModifiedSinceHeader))
 2053            {
 2054                // Return 304 if the image has not been modified since the client's cached version
 02055                if (dateImageModified <= ifModifiedSinceHeader)
 2056                {
 02057                    Response.StatusCode = StatusCodes.Status304NotModified;
 02058                    return new ContentResult();
 2059                }
 2060            }
 2061        }
 2062
 02063        return PhysicalFile(imagePath, imageContentType ?? MediaTypeNames.Text.Plain);
 02064    }
 2065
 2066    internal static bool TryGetImageExtensionFromContentType(string? contentType, [NotNullWhen(true)] out string? extens
 2067    {
 142068        extension = null;
 142069        if (string.IsNullOrEmpty(contentType))
 2070        {
 22071            return false;
 2072        }
 2073
 122074        if (MediaTypeHeaderValue.TryParse(contentType, out var parsedValue)
 122075            && parsedValue.MediaType.HasValue
 122076            && MimeTypes.IsImage(parsedValue.MediaType.Value))
 2077        {
 112078            extension = MimeTypes.ToExtension(parsedValue.MediaType.Value);
 112079            return extension is not null;
 2080        }
 2081
 12082        return false;
 2083    }
 2084}