< 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: 648
Coverable lines: 657
Total lines: 2090
Line coverage: 1.3%
Branch coverage
4%
Covered branches: 8
Total branches: 188
Branch coverage: 4.2%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Coverage history

Coverage history 0 25 50 75 100 5/1/2026 - 12:13:05 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: 20847/22/2026 - 12:16:22 AM Line coverage: 1.3% (9/657) Branch coverage: 4.2% (8/188) Total lines: 2090 5/1/2026 - 12:13:05 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: 20847/22/2026 - 12:16:22 AM Line coverage: 1.3% (9/657) Branch coverage: 4.2% (8/188) Total lines: 2090

Coverage delta

Coverage delta 1 -1

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 userConfigurationDirectoryPath = _serverConfigurationManager.ApplicationPaths.UserConfigurationDirectory
 0129            var userDataPath = Path.Combine(userConfigurationDirectoryPath, user.Username);
 0130            if (!PathHelper.IsContainedIn(userConfigurationDirectoryPath, userDataPath))
 131            {
 0132                return BadRequest("Invalid user.");
 133            }
 134
 0135            if (user.ProfileImage is not null)
 136            {
 0137                await _userManager.ClearProfileImageAsync(user).ConfigureAwait(false);
 138            }
 139
 0140            user.ProfileImage = new Database.Implementations.Entities.ImageInfo(Path.Combine(userDataPath, "profile" + e
 141
 0142            await _providerManager
 0143                .SaveImage(stream, mimeType, user.ProfileImage.Path)
 0144                .ConfigureAwait(false);
 0145            await _userManager.UpdateUserAsync(user).ConfigureAwait(false);
 146
 0147            return NoContent();
 148        }
 0149    }
 150
 151    /// <summary>
 152    /// Sets the user image.
 153    /// </summary>
 154    /// <param name="userId">User Id.</param>
 155    /// <param name="imageType">(Unused) Image type.</param>
 156    /// <response code="204">Image updated.</response>
 157    /// <response code="403">User does not have permission to delete the image.</response>
 158    /// <returns>A <see cref="NoContentResult"/>.</returns>
 159    [HttpPost("Users/{userId}/Images/{imageType}")]
 160    [Authorize]
 161    [Obsolete("Kept for backwards compatibility")]
 162    [ApiExplorerSettings(IgnoreApi = true)]
 163    [AcceptsImageFile]
 164    [ProducesResponseType(StatusCodes.Status204NoContent)]
 165    [ProducesResponseType(StatusCodes.Status400BadRequest)]
 166    [ProducesResponseType(StatusCodes.Status403Forbidden)]
 167    [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "imageType", Justification = 
 168    public Task<ActionResult> PostUserImageLegacy(
 169        [FromRoute, Required] Guid userId,
 170        [FromRoute, Required] ImageType imageType)
 171        => PostUserImage(userId);
 172
 173    /// <summary>
 174    /// Sets the user image.
 175    /// </summary>
 176    /// <param name="userId">User Id.</param>
 177    /// <param name="imageType">(Unused) Image type.</param>
 178    /// <param name="index">(Unused) Image index.</param>
 179    /// <response code="204">Image updated.</response>
 180    /// <response code="403">User does not have permission to delete the image.</response>
 181    /// <returns>A <see cref="NoContentResult"/>.</returns>
 182    [HttpPost("Users/{userId}/Images/{imageType}/{index}")]
 183    [Authorize]
 184    [Obsolete("Kept for backwards compatibility")]
 185    [ApiExplorerSettings(IgnoreApi = true)]
 186    [AcceptsImageFile]
 187    [ProducesResponseType(StatusCodes.Status204NoContent)]
 188    [ProducesResponseType(StatusCodes.Status400BadRequest)]
 189    [ProducesResponseType(StatusCodes.Status403Forbidden)]
 190    [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "imageType", Justification = 
 191    [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "index", Justification = "Imp
 192    public Task<ActionResult> PostUserImageByIndexLegacy(
 193        [FromRoute, Required] Guid userId,
 194        [FromRoute, Required] ImageType imageType,
 195        [FromRoute] int index)
 196        => PostUserImage(userId);
 197
 198    /// <summary>
 199    /// Delete the user's image.
 200    /// </summary>
 201    /// <param name="userId">User Id.</param>
 202    /// <response code="204">Image deleted.</response>
 203    /// <response code="403">User does not have permission to delete the image.</response>
 204    /// <returns>A <see cref="NoContentResult"/>.</returns>
 205    [HttpDelete("UserImage")]
 206    [Authorize]
 207    [ProducesResponseType(StatusCodes.Status204NoContent)]
 208    [ProducesResponseType(StatusCodes.Status403Forbidden)]
 209    public async Task<ActionResult> DeleteUserImage(
 210        [FromQuery] Guid? userId)
 211    {
 0212        var requestUserId = RequestHelpers.GetUserId(User, userId);
 0213        var user = _userManager.GetUserById(requestUserId);
 0214        if (user is null)
 215        {
 0216            return NotFound();
 217        }
 218
 0219        if (!RequestHelpers.AssertCanUpdateUser(HttpContext.User, user, true))
 220        {
 0221            return StatusCode(StatusCodes.Status403Forbidden, "User is not allowed to delete the image.");
 222        }
 223
 0224        if (user.ProfileImage is null)
 225        {
 0226            return NoContent();
 227        }
 228
 229        try
 230        {
 0231            System.IO.File.Delete(user.ProfileImage.Path);
 0232        }
 0233        catch (IOException e)
 234        {
 0235            _logger.LogError(e, "Error deleting user profile image:");
 0236        }
 237
 0238        await _userManager.ClearProfileImageAsync(user).ConfigureAwait(false);
 0239        return NoContent();
 0240    }
 241
 242    /// <summary>
 243    /// Delete the user's image.
 244    /// </summary>
 245    /// <param name="userId">User Id.</param>
 246    /// <param name="imageType">(Unused) Image type.</param>
 247    /// <param name="index">(Unused) Image index.</param>
 248    /// <response code="204">Image deleted.</response>
 249    /// <response code="403">User does not have permission to delete the image.</response>
 250    /// <returns>A <see cref="NoContentResult"/>.</returns>
 251    [HttpDelete("Users/{userId}/Images/{imageType}")]
 252    [Authorize]
 253    [Obsolete("Kept for backwards compatibility")]
 254    [ApiExplorerSettings(IgnoreApi = true)]
 255    [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "imageType", Justification = 
 256    [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "index", Justification = "Imp
 257    [ProducesResponseType(StatusCodes.Status204NoContent)]
 258    [ProducesResponseType(StatusCodes.Status403Forbidden)]
 259    public Task<ActionResult> DeleteUserImageLegacy(
 260        [FromRoute, Required] Guid userId,
 261        [FromRoute, Required] ImageType imageType,
 262        [FromQuery] int? index = null)
 263        => DeleteUserImage(userId);
 264
 265    /// <summary>
 266    /// Delete the user's image.
 267    /// </summary>
 268    /// <param name="userId">User Id.</param>
 269    /// <param name="imageType">(Unused) Image type.</param>
 270    /// <param name="index">(Unused) Image index.</param>
 271    /// <response code="204">Image deleted.</response>
 272    /// <response code="403">User does not have permission to delete the image.</response>
 273    /// <returns>A <see cref="NoContentResult"/>.</returns>
 274    [HttpDelete("Users/{userId}/Images/{imageType}/{index}")]
 275    [Authorize]
 276    [Obsolete("Kept for backwards compatibility")]
 277    [ApiExplorerSettings(IgnoreApi = true)]
 278    [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "imageType", Justification = 
 279    [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "index", Justification = "Imp
 280    [ProducesResponseType(StatusCodes.Status204NoContent)]
 281    [ProducesResponseType(StatusCodes.Status403Forbidden)]
 282    public Task<ActionResult> DeleteUserImageByIndexLegacy(
 283        [FromRoute, Required] Guid userId,
 284        [FromRoute, Required] ImageType imageType,
 285        [FromRoute] int index)
 286        => DeleteUserImage(userId);
 287
 288    /// <summary>
 289    /// Delete an item's image.
 290    /// </summary>
 291    /// <param name="itemId">Item id.</param>
 292    /// <param name="imageType">Image type.</param>
 293    /// <param name="imageIndex">The image index.</param>
 294    /// <response code="204">Image deleted.</response>
 295    /// <response code="404">Item not found.</response>
 296    /// <returns>A <see cref="NoContentResult"/> on success, or a <see cref="NotFoundResult"/> if item not found.</retur
 297    [HttpDelete("Items/{itemId}/Images/{imageType}")]
 298    [Authorize(Policy = Policies.RequiresElevation)]
 299    [ProducesResponseType(StatusCodes.Status204NoContent)]
 300    [ProducesResponseType(StatusCodes.Status404NotFound)]
 301    public async Task<ActionResult> DeleteItemImage(
 302        [FromRoute, Required] Guid itemId,
 303        [FromRoute, Required] ImageType imageType,
 304        [FromQuery] int? imageIndex)
 305    {
 0306        var item = _libraryManager.GetItemById<BaseItem>(itemId, User.GetUserId());
 0307        if (item is null)
 308        {
 0309            return NotFound();
 310        }
 311
 0312        await item.DeleteImageAsync(imageType, imageIndex ?? 0).ConfigureAwait(false);
 0313        return NoContent();
 0314    }
 315
 316    /// <summary>
 317    /// Delete an item's image.
 318    /// </summary>
 319    /// <param name="itemId">Item id.</param>
 320    /// <param name="imageType">Image type.</param>
 321    /// <param name="imageIndex">The image index.</param>
 322    /// <response code="204">Image deleted.</response>
 323    /// <response code="404">Item not found.</response>
 324    /// <returns>A <see cref="NoContentResult"/> on success, or a <see cref="NotFoundResult"/> if item not found.</retur
 325    [HttpDelete("Items/{itemId}/Images/{imageType}/{imageIndex}")]
 326    [Authorize(Policy = Policies.RequiresElevation)]
 327    [ProducesResponseType(StatusCodes.Status204NoContent)]
 328    [ProducesResponseType(StatusCodes.Status404NotFound)]
 329    public async Task<ActionResult> DeleteItemImageByIndex(
 330        [FromRoute, Required] Guid itemId,
 331        [FromRoute, Required] ImageType imageType,
 332        [FromRoute] int imageIndex)
 333    {
 0334        var item = _libraryManager.GetItemById<BaseItem>(itemId, User.GetUserId());
 0335        if (item is null)
 336        {
 0337            return NotFound();
 338        }
 339
 0340        await item.DeleteImageAsync(imageType, imageIndex).ConfigureAwait(false);
 0341        return NoContent();
 0342    }
 343
 344    /// <summary>
 345    /// Set item image.
 346    /// </summary>
 347    /// <param name="itemId">Item id.</param>
 348    /// <param name="imageType">Image type.</param>
 349    /// <response code="204">Image saved.</response>
 350    /// <response code="404">Item not found.</response>
 351    /// <returns>A <see cref="NoContentResult"/> on success, or a <see cref="NotFoundResult"/> if item not found.</retur
 352    [HttpPost("Items/{itemId}/Images/{imageType}")]
 353    [Authorize(Policy = Policies.RequiresElevation)]
 354    [AcceptsImageFile]
 355    [ProducesResponseType(StatusCodes.Status204NoContent)]
 356    [ProducesResponseType(StatusCodes.Status400BadRequest)]
 357    [ProducesResponseType(StatusCodes.Status404NotFound)]
 358    [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "index", Justification = "Imp
 359    public async Task<ActionResult> SetItemImage(
 360        [FromRoute, Required] Guid itemId,
 361        [FromRoute, Required] ImageType imageType)
 362    {
 0363        var item = _libraryManager.GetItemById<BaseItem>(itemId, User.GetUserId());
 0364        if (item is null)
 365        {
 0366            return NotFound();
 367        }
 368
 0369        if (!TryGetImageExtensionFromContentType(Request.ContentType, out _))
 370        {
 0371            return BadRequest("Incorrect ContentType.");
 372        }
 373
 0374        var stream = GetFromBase64Stream(Request.Body);
 0375        await using (stream.ConfigureAwait(false))
 376        {
 377            // Handle image/png; charset=utf-8
 0378            var mimeType = Request.ContentType?.Split(';').FirstOrDefault();
 0379            await _providerManager.SaveImage(item, stream, mimeType, imageType, null, CancellationToken.None).ConfigureA
 0380            await item.UpdateToRepositoryAsync(ItemUpdateType.ImageUpdate, CancellationToken.None).ConfigureAwait(false)
 381
 0382            return NoContent();
 383        }
 0384    }
 385
 386    /// <summary>
 387    /// Set item image.
 388    /// </summary>
 389    /// <param name="itemId">Item id.</param>
 390    /// <param name="imageType">Image type.</param>
 391    /// <param name="imageIndex">(Unused) Image index.</param>
 392    /// <response code="204">Image saved.</response>
 393    /// <response code="404">Item not found.</response>
 394    /// <returns>A <see cref="NoContentResult"/> on success, or a <see cref="NotFoundResult"/> if item not found.</retur
 395    [HttpPost("Items/{itemId}/Images/{imageType}/{imageIndex}")]
 396    [Authorize(Policy = Policies.RequiresElevation)]
 397    [AcceptsImageFile]
 398    [ProducesResponseType(StatusCodes.Status204NoContent)]
 399    [ProducesResponseType(StatusCodes.Status400BadRequest)]
 400    [ProducesResponseType(StatusCodes.Status404NotFound)]
 401    [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "index", Justification = "Imp
 402    public async Task<ActionResult> SetItemImageByIndex(
 403        [FromRoute, Required] Guid itemId,
 404        [FromRoute, Required] ImageType imageType,
 405        [FromRoute] int imageIndex)
 406    {
 0407        var item = _libraryManager.GetItemById<BaseItem>(itemId, User.GetUserId());
 0408        if (item is null)
 409        {
 0410            return NotFound();
 411        }
 412
 0413        if (!TryGetImageExtensionFromContentType(Request.ContentType, out _))
 414        {
 0415            return BadRequest("Incorrect ContentType.");
 416        }
 417
 0418        var stream = GetFromBase64Stream(Request.Body);
 0419        await using (stream.ConfigureAwait(false))
 420        {
 421            // Handle image/png; charset=utf-8
 0422            var mimeType = Request.ContentType?.Split(';').FirstOrDefault();
 0423            await _providerManager.SaveImage(item, stream, mimeType, imageType, null, CancellationToken.None).ConfigureA
 0424            await item.UpdateToRepositoryAsync(ItemUpdateType.ImageUpdate, CancellationToken.None).ConfigureAwait(false)
 425
 0426            return NoContent();
 427        }
 0428    }
 429
 430    /// <summary>
 431    /// Updates the index for an item image.
 432    /// </summary>
 433    /// <param name="itemId">Item id.</param>
 434    /// <param name="imageType">Image type.</param>
 435    /// <param name="imageIndex">Old image index.</param>
 436    /// <param name="newIndex">New image index.</param>
 437    /// <response code="204">Image index updated.</response>
 438    /// <response code="404">Item not found.</response>
 439    /// <returns>A <see cref="NoContentResult"/> on success, or a <see cref="NotFoundResult"/> if item not found.</retur
 440    [HttpPost("Items/{itemId}/Images/{imageType}/{imageIndex}/Index")]
 441    [Authorize(Policy = Policies.RequiresElevation)]
 442    [ProducesResponseType(StatusCodes.Status204NoContent)]
 443    [ProducesResponseType(StatusCodes.Status404NotFound)]
 444    public async Task<ActionResult> UpdateItemImageIndex(
 445        [FromRoute, Required] Guid itemId,
 446        [FromRoute, Required] ImageType imageType,
 447        [FromRoute, Required] int imageIndex,
 448        [FromQuery, Required] int newIndex)
 449    {
 0450        var item = _libraryManager.GetItemById<BaseItem>(itemId, User.GetUserId());
 0451        if (item is null)
 452        {
 0453            return NotFound();
 454        }
 455
 0456        await item.SwapImagesAsync(imageType, imageIndex, newIndex).ConfigureAwait(false);
 0457        return NoContent();
 0458    }
 459
 460    /// <summary>
 461    /// Get item image infos.
 462    /// </summary>
 463    /// <param name="itemId">Item id.</param>
 464    /// <response code="200">Item images returned.</response>
 465    /// <response code="404">Item not found.</response>
 466    /// <returns>The list of image infos on success, or <see cref="NotFoundResult"/> if item not found.</returns>
 467    [HttpGet("Items/{itemId}/Images")]
 468    [Authorize]
 469    [ProducesResponseType(StatusCodes.Status200OK)]
 470    [ProducesResponseType(StatusCodes.Status404NotFound)]
 471    public async Task<ActionResult<IEnumerable<ImageInfo>>> GetItemImageInfos([FromRoute, Required] Guid itemId)
 472    {
 0473        var item = _libraryManager.GetItemById<BaseItem>(itemId, User.GetUserId());
 0474        if (item is null)
 475        {
 0476            return NotFound();
 477        }
 478
 0479        var list = new List<ImageInfo>();
 0480        var itemImages = item.ImageInfos;
 481
 0482        if (itemImages.Length == 0)
 483        {
 484            // short-circuit
 0485            return list;
 486        }
 487
 0488        await _libraryManager.UpdateImagesAsync(item).ConfigureAwait(false); // this makes sure dimensions and hashes ar
 489
 0490        foreach (var image in itemImages)
 491        {
 0492            if (!item.AllowsMultipleImages(image.Type))
 493            {
 0494                var info = GetImageInfo(item, image, null);
 495
 0496                if (info is not null)
 497                {
 0498                    list.Add(info);
 499                }
 500            }
 501        }
 502
 0503        foreach (var imageType in itemImages.Select(i => i.Type).Distinct().Where(item.AllowsMultipleImages))
 504        {
 0505            var index = 0;
 506
 507            // Prevent implicitly captured closure
 0508            var currentImageType = imageType;
 509
 0510            foreach (var image in itemImages.Where(i => i.Type == currentImageType))
 511            {
 0512                var info = GetImageInfo(item, image, index);
 513
 0514                if (info is not null)
 515                {
 0516                    list.Add(info);
 517                }
 518
 0519                index++;
 520            }
 521        }
 522
 0523        return list;
 0524    }
 525
 526    /// <summary>
 527    /// Gets the item's image.
 528    /// </summary>
 529    /// <param name="itemId">Item id.</param>
 530    /// <param name="imageType">Image type.</param>
 531    /// <param name="maxWidth">The maximum image width to return.</param>
 532    /// <param name="maxHeight">The maximum image height to return.</param>
 533    /// <param name="width">The fixed image width to return.</param>
 534    /// <param name="height">The fixed image height to return.</param>
 535    /// <param name="quality">Optional. Quality setting, from 0-100. Defaults to 90 and should suffice in most cases.</p
 536    /// <param name="fillWidth">Width of box to fill.</param>
 537    /// <param name="fillHeight">Height of box to fill.</param>
 538    /// <param name="tag">Optional. Supply the cache tag from the item object to receive strong caching headers.</param>
 539    /// <param name="format">Optional. The <see cref="ImageFormat"/> of the returned image.</param>
 540    /// <param name="percentPlayed">Optional. Percent to render for the percent played overlay.</param>
 541    /// <param name="unplayedCount">Optional. Unplayed count overlay to render.</param>
 542    /// <param name="blur">Optional. Blur image.</param>
 543    /// <param name="backgroundColor">Optional. Apply a background color for transparent images.</param>
 544    /// <param name="foregroundLayer">Optional. Apply a foreground layer on top of the image.</param>
 545    /// <param name="imageIndex">Image index.</param>
 546    /// <response code="200">Image stream returned.</response>
 547    /// <response code="404">Item not found.</response>
 548    /// <returns>
 549    /// A <see cref="FileStreamResult"/> containing the file stream on success,
 550    /// or a <see cref="NotFoundResult"/> if item not found.
 551    /// </returns>
 552    [HttpGet("Items/{itemId}/Images/{imageType}")]
 553    [HttpHead("Items/{itemId}/Images/{imageType}", Name = "HeadItemImage")]
 554    [ProducesResponseType(StatusCodes.Status200OK)]
 555    [ProducesResponseType(StatusCodes.Status404NotFound)]
 556    [ProducesImageFile]
 557    public async Task<ActionResult> GetItemImage(
 558        [FromRoute, Required] Guid itemId,
 559        [FromRoute, Required] ImageType imageType,
 560        [FromQuery] int? maxWidth,
 561        [FromQuery] int? maxHeight,
 562        [FromQuery] int? width,
 563        [FromQuery] int? height,
 564        [FromQuery] int? quality,
 565        [FromQuery] int? fillWidth,
 566        [FromQuery] int? fillHeight,
 567        [FromQuery] string? tag,
 568        [FromQuery] ImageFormat? format,
 569        [FromQuery] double? percentPlayed,
 570        [FromQuery] int? unplayedCount,
 571        [FromQuery] int? blur,
 572        [FromQuery] string? backgroundColor,
 573        [FromQuery] string? foregroundLayer,
 574        [FromQuery] int? imageIndex)
 575    {
 0576        var item = _libraryManager.GetItemById<BaseItem>(itemId, User.GetUserId());
 0577        if (item is null)
 578        {
 0579            return NotFound();
 580        }
 581
 0582        return await GetImageInternal(
 0583                itemId,
 0584                imageType,
 0585                imageIndex,
 0586                tag,
 0587                format,
 0588                maxWidth,
 0589                maxHeight,
 0590                percentPlayed,
 0591                unplayedCount,
 0592                width,
 0593                height,
 0594                quality,
 0595                fillWidth,
 0596                fillHeight,
 0597                blur,
 0598                backgroundColor,
 0599                foregroundLayer,
 0600                item)
 0601            .ConfigureAwait(false);
 0602    }
 603
 604    /// <summary>
 605    /// Gets the item's image.
 606    /// </summary>
 607    /// <param name="itemId">Item id.</param>
 608    /// <param name="imageType">Image type.</param>
 609    /// <param name="imageIndex">Image index.</param>
 610    /// <param name="maxWidth">The maximum image width to return.</param>
 611    /// <param name="maxHeight">The maximum image height to return.</param>
 612    /// <param name="width">The fixed image width to return.</param>
 613    /// <param name="height">The fixed image height to return.</param>
 614    /// <param name="quality">Optional. Quality setting, from 0-100. Defaults to 90 and should suffice in most cases.</p
 615    /// <param name="fillWidth">Width of box to fill.</param>
 616    /// <param name="fillHeight">Height of box to fill.</param>
 617    /// <param name="tag">Optional. Supply the cache tag from the item object to receive strong caching headers.</param>
 618    /// <param name="format">Optional. The <see cref="ImageFormat"/> of the returned image.</param>
 619    /// <param name="percentPlayed">Optional. Percent to render for the percent played overlay.</param>
 620    /// <param name="unplayedCount">Optional. Unplayed count overlay to render.</param>
 621    /// <param name="blur">Optional. Blur image.</param>
 622    /// <param name="backgroundColor">Optional. Apply a background color for transparent images.</param>
 623    /// <param name="foregroundLayer">Optional. Apply a foreground layer on top of the image.</param>
 624    /// <response code="200">Image stream returned.</response>
 625    /// <response code="404">Item not found.</response>
 626    /// <returns>
 627    /// A <see cref="FileStreamResult"/> containing the file stream on success,
 628    /// or a <see cref="NotFoundResult"/> if item not found.
 629    /// </returns>
 630    [HttpGet("Items/{itemId}/Images/{imageType}/{imageIndex}")]
 631    [HttpHead("Items/{itemId}/Images/{imageType}/{imageIndex}", Name = "HeadItemImageByIndex")]
 632    [ProducesResponseType(StatusCodes.Status200OK)]
 633    [ProducesResponseType(StatusCodes.Status404NotFound)]
 634    [ProducesImageFile]
 635    public async Task<ActionResult> GetItemImageByIndex(
 636        [FromRoute, Required] Guid itemId,
 637        [FromRoute, Required] ImageType imageType,
 638        [FromRoute] int imageIndex,
 639        [FromQuery] int? maxWidth,
 640        [FromQuery] int? maxHeight,
 641        [FromQuery] int? width,
 642        [FromQuery] int? height,
 643        [FromQuery] int? quality,
 644        [FromQuery] int? fillWidth,
 645        [FromQuery] int? fillHeight,
 646        [FromQuery] string? tag,
 647        [FromQuery] ImageFormat? format,
 648        [FromQuery] double? percentPlayed,
 649        [FromQuery] int? unplayedCount,
 650        [FromQuery] int? blur,
 651        [FromQuery] string? backgroundColor,
 652        [FromQuery] string? foregroundLayer)
 653    {
 0654        var item = _libraryManager.GetItemById<BaseItem>(itemId, User.GetUserId());
 0655        if (item is null)
 656        {
 0657            return NotFound();
 658        }
 659
 0660        return await GetImageInternal(
 0661                itemId,
 0662                imageType,
 0663                imageIndex,
 0664                tag,
 0665                format,
 0666                maxWidth,
 0667                maxHeight,
 0668                percentPlayed,
 0669                unplayedCount,
 0670                width,
 0671                height,
 0672                quality,
 0673                fillWidth,
 0674                fillHeight,
 0675                blur,
 0676                backgroundColor,
 0677                foregroundLayer,
 0678                item)
 0679            .ConfigureAwait(false);
 0680    }
 681
 682    /// <summary>
 683    /// Gets the item's image.
 684    /// </summary>
 685    /// <param name="itemId">Item id.</param>
 686    /// <param name="imageType">Image type.</param>
 687    /// <param name="maxWidth">The maximum image width to return.</param>
 688    /// <param name="maxHeight">The maximum image height to return.</param>
 689    /// <param name="width">The fixed image width to return.</param>
 690    /// <param name="height">The fixed image height to return.</param>
 691    /// <param name="quality">Optional. Quality setting, from 0-100. Defaults to 90 and should suffice in most cases.</p
 692    /// <param name="fillWidth">Width of box to fill.</param>
 693    /// <param name="fillHeight">Height of box to fill.</param>
 694    /// <param name="tag">Optional. Supply the cache tag from the item object to receive strong caching headers.</param>
 695    /// <param name="format">Determines the output format of the image - original,gif,jpg,png.</param>
 696    /// <param name="percentPlayed">Optional. Percent to render for the percent played overlay.</param>
 697    /// <param name="unplayedCount">Optional. Unplayed count overlay to render.</param>
 698    /// <param name="blur">Optional. Blur image.</param>
 699    /// <param name="backgroundColor">Optional. Apply a background color for transparent images.</param>
 700    /// <param name="foregroundLayer">Optional. Apply a foreground layer on top of the image.</param>
 701    /// <param name="imageIndex">Image index.</param>
 702    /// <response code="200">Image stream returned.</response>
 703    /// <response code="404">Item not found.</response>
 704    /// <returns>
 705    /// A <see cref="FileStreamResult"/> containing the file stream on success,
 706    /// or a <see cref="NotFoundResult"/> if item not found.
 707    /// </returns>
 708    [HttpGet("Items/{itemId}/Images/{imageType}/{imageIndex}/{tag}/{format}/{maxWidth}/{maxHeight}/{percentPlayed}/{unpl
 709    [HttpHead("Items/{itemId}/Images/{imageType}/{imageIndex}/{tag}/{format}/{maxWidth}/{maxHeight}/{percentPlayed}/{unp
 710    [ProducesResponseType(StatusCodes.Status200OK)]
 711    [ProducesResponseType(StatusCodes.Status404NotFound)]
 712    [ProducesImageFile]
 713    public async Task<ActionResult> GetItemImage2(
 714        [FromRoute, Required] Guid itemId,
 715        [FromRoute, Required] ImageType imageType,
 716        [FromRoute, Required] int maxWidth,
 717        [FromRoute, Required] int maxHeight,
 718        [FromQuery] int? width,
 719        [FromQuery] int? height,
 720        [FromQuery] int? quality,
 721        [FromQuery] int? fillWidth,
 722        [FromQuery] int? fillHeight,
 723        [FromRoute, Required] string tag,
 724        [FromRoute, Required] ImageFormat format,
 725        [FromRoute, Required] double percentPlayed,
 726        [FromRoute, Required] int unplayedCount,
 727        [FromQuery] int? blur,
 728        [FromQuery] string? backgroundColor,
 729        [FromQuery] string? foregroundLayer,
 730        [FromRoute, Required] int imageIndex)
 731    {
 0732        var item = _libraryManager.GetItemById<BaseItem>(itemId, User.GetUserId());
 0733        if (item is null)
 734        {
 0735            return NotFound();
 736        }
 737
 0738        return await GetImageInternal(
 0739                itemId,
 0740                imageType,
 0741                imageIndex,
 0742                tag,
 0743                format,
 0744                maxWidth,
 0745                maxHeight,
 0746                percentPlayed,
 0747                unplayedCount,
 0748                width,
 0749                height,
 0750                quality,
 0751                fillWidth,
 0752                fillHeight,
 0753                blur,
 0754                backgroundColor,
 0755                foregroundLayer,
 0756                item)
 0757            .ConfigureAwait(false);
 0758    }
 759
 760    /// <summary>
 761    /// Get artist image by name.
 762    /// </summary>
 763    /// <param name="name">Artist name.</param>
 764    /// <param name="imageType">Image type.</param>
 765    /// <param name="tag">Optional. Supply the cache tag from the item object to receive strong caching headers.</param>
 766    /// <param name="format">Determines the output format of the image - original,gif,jpg,png.</param>
 767    /// <param name="maxWidth">The maximum image width to return.</param>
 768    /// <param name="maxHeight">The maximum image height to return.</param>
 769    /// <param name="percentPlayed">Optional. Percent to render for the percent played overlay.</param>
 770    /// <param name="unplayedCount">Optional. Unplayed count overlay to render.</param>
 771    /// <param name="width">The fixed image width to return.</param>
 772    /// <param name="height">The fixed image height to return.</param>
 773    /// <param name="quality">Optional. Quality setting, from 0-100. Defaults to 90 and should suffice in most cases.</p
 774    /// <param name="fillWidth">Width of box to fill.</param>
 775    /// <param name="fillHeight">Height of box to fill.</param>
 776    /// <param name="blur">Optional. Blur image.</param>
 777    /// <param name="backgroundColor">Optional. Apply a background color for transparent images.</param>
 778    /// <param name="foregroundLayer">Optional. Apply a foreground layer on top of the image.</param>
 779    /// <param name="imageIndex">Image index.</param>
 780    /// <response code="200">Image stream returned.</response>
 781    /// <response code="404">Item not found.</response>
 782    /// <returns>
 783    /// A <see cref="FileStreamResult"/> containing the file stream on success,
 784    /// or a <see cref="NotFoundResult"/> if item not found.
 785    /// </returns>
 786    [HttpGet("Artists/{name}/Images/{imageType}/{imageIndex}")]
 787    [HttpHead("Artists/{name}/Images/{imageType}/{imageIndex}", Name = "HeadArtistImage")]
 788    [ProducesResponseType(StatusCodes.Status200OK)]
 789    [ProducesResponseType(StatusCodes.Status404NotFound)]
 790    [ProducesImageFile]
 791    public async Task<ActionResult> GetArtistImage(
 792        [FromRoute, Required] string name,
 793        [FromRoute, Required] ImageType imageType,
 794        [FromQuery] string? tag,
 795        [FromQuery] ImageFormat? format,
 796        [FromQuery] int? maxWidth,
 797        [FromQuery] int? maxHeight,
 798        [FromQuery] double? percentPlayed,
 799        [FromQuery] int? unplayedCount,
 800        [FromQuery] int? width,
 801        [FromQuery] int? height,
 802        [FromQuery] int? quality,
 803        [FromQuery] int? fillWidth,
 804        [FromQuery] int? fillHeight,
 805        [FromQuery] int? blur,
 806        [FromQuery] string? backgroundColor,
 807        [FromQuery] string? foregroundLayer,
 808        [FromRoute, Required] int imageIndex)
 809    {
 0810        var item = _libraryManager.GetArtist(name);
 0811        if (item is null)
 812        {
 0813            return NotFound();
 814        }
 815
 0816        return await GetImageInternal(
 0817                item.Id,
 0818                imageType,
 0819                imageIndex,
 0820                tag,
 0821                format,
 0822                maxWidth,
 0823                maxHeight,
 0824                percentPlayed,
 0825                unplayedCount,
 0826                width,
 0827                height,
 0828                quality,
 0829                fillWidth,
 0830                fillHeight,
 0831                blur,
 0832                backgroundColor,
 0833                foregroundLayer,
 0834                item)
 0835            .ConfigureAwait(false);
 0836    }
 837
 838    /// <summary>
 839    /// Get genre image by name.
 840    /// </summary>
 841    /// <param name="name">Genre name.</param>
 842    /// <param name="imageType">Image type.</param>
 843    /// <param name="tag">Optional. Supply the cache tag from the item object to receive strong caching headers.</param>
 844    /// <param name="format">Determines the output format of the image - original,gif,jpg,png.</param>
 845    /// <param name="maxWidth">The maximum image width to return.</param>
 846    /// <param name="maxHeight">The maximum image height to return.</param>
 847    /// <param name="percentPlayed">Optional. Percent to render for the percent played overlay.</param>
 848    /// <param name="unplayedCount">Optional. Unplayed count overlay to render.</param>
 849    /// <param name="width">The fixed image width to return.</param>
 850    /// <param name="height">The fixed image height to return.</param>
 851    /// <param name="quality">Optional. Quality setting, from 0-100. Defaults to 90 and should suffice in most cases.</p
 852    /// <param name="fillWidth">Width of box to fill.</param>
 853    /// <param name="fillHeight">Height of box to fill.</param>
 854    /// <param name="blur">Optional. Blur image.</param>
 855    /// <param name="backgroundColor">Optional. Apply a background color for transparent images.</param>
 856    /// <param name="foregroundLayer">Optional. Apply a foreground layer on top of the image.</param>
 857    /// <param name="imageIndex">Image index.</param>
 858    /// <response code="200">Image stream returned.</response>
 859    /// <response code="404">Item not found.</response>
 860    /// <returns>
 861    /// A <see cref="FileStreamResult"/> containing the file stream on success,
 862    /// or a <see cref="NotFoundResult"/> if item not found.
 863    /// </returns>
 864    [HttpGet("Genres/{name}/Images/{imageType}")]
 865    [HttpHead("Genres/{name}/Images/{imageType}", Name = "HeadGenreImage")]
 866    [ProducesResponseType(StatusCodes.Status200OK)]
 867    [ProducesResponseType(StatusCodes.Status404NotFound)]
 868    [ProducesImageFile]
 869    public async Task<ActionResult> GetGenreImage(
 870        [FromRoute, Required] string name,
 871        [FromRoute, Required] ImageType imageType,
 872        [FromQuery] string? tag,
 873        [FromQuery] ImageFormat? format,
 874        [FromQuery] int? maxWidth,
 875        [FromQuery] int? maxHeight,
 876        [FromQuery] double? percentPlayed,
 877        [FromQuery] int? unplayedCount,
 878        [FromQuery] int? width,
 879        [FromQuery] int? height,
 880        [FromQuery] int? quality,
 881        [FromQuery] int? fillWidth,
 882        [FromQuery] int? fillHeight,
 883        [FromQuery] int? blur,
 884        [FromQuery] string? backgroundColor,
 885        [FromQuery] string? foregroundLayer,
 886        [FromQuery] int? imageIndex)
 887    {
 0888        var item = _libraryManager.GetGenre(name);
 0889        if (item is null)
 890        {
 0891            return NotFound();
 892        }
 893
 0894        return await GetImageInternal(
 0895                item.Id,
 0896                imageType,
 0897                imageIndex,
 0898                tag,
 0899                format,
 0900                maxWidth,
 0901                maxHeight,
 0902                percentPlayed,
 0903                unplayedCount,
 0904                width,
 0905                height,
 0906                quality,
 0907                fillWidth,
 0908                fillHeight,
 0909                blur,
 0910                backgroundColor,
 0911                foregroundLayer,
 0912                item)
 0913            .ConfigureAwait(false);
 0914    }
 915
 916    /// <summary>
 917    /// Get genre image by name.
 918    /// </summary>
 919    /// <param name="name">Genre name.</param>
 920    /// <param name="imageType">Image type.</param>
 921    /// <param name="imageIndex">Image index.</param>
 922    /// <param name="tag">Optional. Supply the cache tag from the item object to receive strong caching headers.</param>
 923    /// <param name="format">Determines the output format of the image - original,gif,jpg,png.</param>
 924    /// <param name="maxWidth">The maximum image width to return.</param>
 925    /// <param name="maxHeight">The maximum image height to return.</param>
 926    /// <param name="percentPlayed">Optional. Percent to render for the percent played overlay.</param>
 927    /// <param name="unplayedCount">Optional. Unplayed count overlay to render.</param>
 928    /// <param name="width">The fixed image width to return.</param>
 929    /// <param name="height">The fixed image height to return.</param>
 930    /// <param name="quality">Optional. Quality setting, from 0-100. Defaults to 90 and should suffice in most cases.</p
 931    /// <param name="fillWidth">Width of box to fill.</param>
 932    /// <param name="fillHeight">Height of box to fill.</param>
 933    /// <param name="blur">Optional. Blur image.</param>
 934    /// <param name="backgroundColor">Optional. Apply a background color for transparent images.</param>
 935    /// <param name="foregroundLayer">Optional. Apply a foreground layer on top of the image.</param>
 936    /// <response code="200">Image stream returned.</response>
 937    /// <response code="404">Item not found.</response>
 938    /// <returns>
 939    /// A <see cref="FileStreamResult"/> containing the file stream on success,
 940    /// or a <see cref="NotFoundResult"/> if item not found.
 941    /// </returns>
 942    [HttpGet("Genres/{name}/Images/{imageType}/{imageIndex}")]
 943    [HttpHead("Genres/{name}/Images/{imageType}/{imageIndex}", Name = "HeadGenreImageByIndex")]
 944    [ProducesResponseType(StatusCodes.Status200OK)]
 945    [ProducesResponseType(StatusCodes.Status404NotFound)]
 946    [ProducesImageFile]
 947    public async Task<ActionResult> GetGenreImageByIndex(
 948        [FromRoute, Required] string name,
 949        [FromRoute, Required] ImageType imageType,
 950        [FromRoute, Required] int imageIndex,
 951        [FromQuery] string? tag,
 952        [FromQuery] ImageFormat? format,
 953        [FromQuery] int? maxWidth,
 954        [FromQuery] int? maxHeight,
 955        [FromQuery] double? percentPlayed,
 956        [FromQuery] int? unplayedCount,
 957        [FromQuery] int? width,
 958        [FromQuery] int? height,
 959        [FromQuery] int? quality,
 960        [FromQuery] int? fillWidth,
 961        [FromQuery] int? fillHeight,
 962        [FromQuery] int? blur,
 963        [FromQuery] string? backgroundColor,
 964        [FromQuery] string? foregroundLayer)
 965    {
 0966        var item = _libraryManager.GetGenre(name);
 0967        if (item is null)
 968        {
 0969            return NotFound();
 970        }
 971
 0972        return await GetImageInternal(
 0973                item.Id,
 0974                imageType,
 0975                imageIndex,
 0976                tag,
 0977                format,
 0978                maxWidth,
 0979                maxHeight,
 0980                percentPlayed,
 0981                unplayedCount,
 0982                width,
 0983                height,
 0984                quality,
 0985                fillWidth,
 0986                fillHeight,
 0987                blur,
 0988                backgroundColor,
 0989                foregroundLayer,
 0990                item)
 0991            .ConfigureAwait(false);
 0992    }
 993
 994    /// <summary>
 995    /// Get music genre image by name.
 996    /// </summary>
 997    /// <param name="name">Music genre name.</param>
 998    /// <param name="imageType">Image type.</param>
 999    /// <param name="tag">Optional. Supply the cache tag from the item object to receive strong caching headers.</param>
 1000    /// <param name="format">Determines the output format of the image - original,gif,jpg,png.</param>
 1001    /// <param name="maxWidth">The maximum image width to return.</param>
 1002    /// <param name="maxHeight">The maximum image height to return.</param>
 1003    /// <param name="percentPlayed">Optional. Percent to render for the percent played overlay.</param>
 1004    /// <param name="unplayedCount">Optional. Unplayed count overlay to render.</param>
 1005    /// <param name="width">The fixed image width to return.</param>
 1006    /// <param name="height">The fixed image height to return.</param>
 1007    /// <param name="quality">Optional. Quality setting, from 0-100. Defaults to 90 and should suffice in most cases.</p
 1008    /// <param name="fillWidth">Width of box to fill.</param>
 1009    /// <param name="fillHeight">Height of box to fill.</param>
 1010    /// <param name="blur">Optional. Blur image.</param>
 1011    /// <param name="backgroundColor">Optional. Apply a background color for transparent images.</param>
 1012    /// <param name="foregroundLayer">Optional. Apply a foreground layer on top of the image.</param>
 1013    /// <param name="imageIndex">Image index.</param>
 1014    /// <response code="200">Image stream returned.</response>
 1015    /// <response code="404">Item not found.</response>
 1016    /// <returns>
 1017    /// A <see cref="FileStreamResult"/> containing the file stream on success,
 1018    /// or a <see cref="NotFoundResult"/> if item not found.
 1019    /// </returns>
 1020    [HttpGet("MusicGenres/{name}/Images/{imageType}")]
 1021    [HttpHead("MusicGenres/{name}/Images/{imageType}", Name = "HeadMusicGenreImage")]
 1022    [ProducesResponseType(StatusCodes.Status200OK)]
 1023    [ProducesResponseType(StatusCodes.Status404NotFound)]
 1024    [ProducesImageFile]
 1025    public async Task<ActionResult> GetMusicGenreImage(
 1026        [FromRoute, Required] string name,
 1027        [FromRoute, Required] ImageType imageType,
 1028        [FromQuery] string? tag,
 1029        [FromQuery] ImageFormat? format,
 1030        [FromQuery] int? maxWidth,
 1031        [FromQuery] int? maxHeight,
 1032        [FromQuery] double? percentPlayed,
 1033        [FromQuery] int? unplayedCount,
 1034        [FromQuery] int? width,
 1035        [FromQuery] int? height,
 1036        [FromQuery] int? quality,
 1037        [FromQuery] int? fillWidth,
 1038        [FromQuery] int? fillHeight,
 1039        [FromQuery] int? blur,
 1040        [FromQuery] string? backgroundColor,
 1041        [FromQuery] string? foregroundLayer,
 1042        [FromQuery] int? imageIndex)
 1043    {
 01044        var item = _libraryManager.GetMusicGenre(name);
 01045        if (item is null)
 1046        {
 01047            return NotFound();
 1048        }
 1049
 01050        return await GetImageInternal(
 01051                item.Id,
 01052                imageType,
 01053                imageIndex,
 01054                tag,
 01055                format,
 01056                maxWidth,
 01057                maxHeight,
 01058                percentPlayed,
 01059                unplayedCount,
 01060                width,
 01061                height,
 01062                quality,
 01063                fillWidth,
 01064                fillHeight,
 01065                blur,
 01066                backgroundColor,
 01067                foregroundLayer,
 01068                item)
 01069            .ConfigureAwait(false);
 01070    }
 1071
 1072    /// <summary>
 1073    /// Get music genre image by name.
 1074    /// </summary>
 1075    /// <param name="name">Music genre name.</param>
 1076    /// <param name="imageType">Image type.</param>
 1077    /// <param name="imageIndex">Image index.</param>
 1078    /// <param name="tag">Optional. Supply the cache tag from the item object to receive strong caching headers.</param>
 1079    /// <param name="format">Determines the output format of the image - original,gif,jpg,png.</param>
 1080    /// <param name="maxWidth">The maximum image width to return.</param>
 1081    /// <param name="maxHeight">The maximum image height to return.</param>
 1082    /// <param name="percentPlayed">Optional. Percent to render for the percent played overlay.</param>
 1083    /// <param name="unplayedCount">Optional. Unplayed count overlay to render.</param>
 1084    /// <param name="width">The fixed image width to return.</param>
 1085    /// <param name="height">The fixed image height to return.</param>
 1086    /// <param name="quality">Optional. Quality setting, from 0-100. Defaults to 90 and should suffice in most cases.</p
 1087    /// <param name="fillWidth">Width of box to fill.</param>
 1088    /// <param name="fillHeight">Height of box to fill.</param>
 1089    /// <param name="blur">Optional. Blur image.</param>
 1090    /// <param name="backgroundColor">Optional. Apply a background color for transparent images.</param>
 1091    /// <param name="foregroundLayer">Optional. Apply a foreground layer on top of the image.</param>
 1092    /// <response code="200">Image stream returned.</response>
 1093    /// <response code="404">Item not found.</response>
 1094    /// <returns>
 1095    /// A <see cref="FileStreamResult"/> containing the file stream on success,
 1096    /// or a <see cref="NotFoundResult"/> if item not found.
 1097    /// </returns>
 1098    [HttpGet("MusicGenres/{name}/Images/{imageType}/{imageIndex}")]
 1099    [HttpHead("MusicGenres/{name}/Images/{imageType}/{imageIndex}", Name = "HeadMusicGenreImageByIndex")]
 1100    [ProducesResponseType(StatusCodes.Status200OK)]
 1101    [ProducesResponseType(StatusCodes.Status404NotFound)]
 1102    [ProducesImageFile]
 1103    public async Task<ActionResult> GetMusicGenreImageByIndex(
 1104        [FromRoute, Required] string name,
 1105        [FromRoute, Required] ImageType imageType,
 1106        [FromRoute, Required] int imageIndex,
 1107        [FromQuery] string? tag,
 1108        [FromQuery] ImageFormat? format,
 1109        [FromQuery] int? maxWidth,
 1110        [FromQuery] int? maxHeight,
 1111        [FromQuery] double? percentPlayed,
 1112        [FromQuery] int? unplayedCount,
 1113        [FromQuery] int? width,
 1114        [FromQuery] int? height,
 1115        [FromQuery] int? quality,
 1116        [FromQuery] int? fillWidth,
 1117        [FromQuery] int? fillHeight,
 1118        [FromQuery] int? blur,
 1119        [FromQuery] string? backgroundColor,
 1120        [FromQuery] string? foregroundLayer)
 1121    {
 01122        var item = _libraryManager.GetMusicGenre(name);
 01123        if (item is null)
 1124        {
 01125            return NotFound();
 1126        }
 1127
 01128        return await GetImageInternal(
 01129                item.Id,
 01130                imageType,
 01131                imageIndex,
 01132                tag,
 01133                format,
 01134                maxWidth,
 01135                maxHeight,
 01136                percentPlayed,
 01137                unplayedCount,
 01138                width,
 01139                height,
 01140                quality,
 01141                fillWidth,
 01142                fillHeight,
 01143                blur,
 01144                backgroundColor,
 01145                foregroundLayer,
 01146                item)
 01147            .ConfigureAwait(false);
 01148    }
 1149
 1150    /// <summary>
 1151    /// Get person image by name.
 1152    /// </summary>
 1153    /// <param name="name">Person name.</param>
 1154    /// <param name="imageType">Image type.</param>
 1155    /// <param name="tag">Optional. Supply the cache tag from the item object to receive strong caching headers.</param>
 1156    /// <param name="format">Determines the output format of the image - original,gif,jpg,png.</param>
 1157    /// <param name="maxWidth">The maximum image width to return.</param>
 1158    /// <param name="maxHeight">The maximum image height to return.</param>
 1159    /// <param name="percentPlayed">Optional. Percent to render for the percent played overlay.</param>
 1160    /// <param name="unplayedCount">Optional. Unplayed count overlay to render.</param>
 1161    /// <param name="width">The fixed image width to return.</param>
 1162    /// <param name="height">The fixed image height to return.</param>
 1163    /// <param name="quality">Optional. Quality setting, from 0-100. Defaults to 90 and should suffice in most cases.</p
 1164    /// <param name="fillWidth">Width of box to fill.</param>
 1165    /// <param name="fillHeight">Height of box to fill.</param>
 1166    /// <param name="blur">Optional. Blur image.</param>
 1167    /// <param name="backgroundColor">Optional. Apply a background color for transparent images.</param>
 1168    /// <param name="foregroundLayer">Optional. Apply a foreground layer on top of the image.</param>
 1169    /// <param name="imageIndex">Image index.</param>
 1170    /// <response code="200">Image stream returned.</response>
 1171    /// <response code="404">Item not found.</response>
 1172    /// <returns>
 1173    /// A <see cref="FileStreamResult"/> containing the file stream on success,
 1174    /// or a <see cref="NotFoundResult"/> if item not found.
 1175    /// </returns>
 1176    [HttpGet("Persons/{name}/Images/{imageType}")]
 1177    [HttpHead("Persons/{name}/Images/{imageType}", Name = "HeadPersonImage")]
 1178    [ProducesResponseType(StatusCodes.Status200OK)]
 1179    [ProducesResponseType(StatusCodes.Status404NotFound)]
 1180    [ProducesImageFile]
 1181    public async Task<ActionResult> GetPersonImage(
 1182        [FromRoute, Required] string name,
 1183        [FromRoute, Required] ImageType imageType,
 1184        [FromQuery] string? tag,
 1185        [FromQuery] ImageFormat? format,
 1186        [FromQuery] int? maxWidth,
 1187        [FromQuery] int? maxHeight,
 1188        [FromQuery] double? percentPlayed,
 1189        [FromQuery] int? unplayedCount,
 1190        [FromQuery] int? width,
 1191        [FromQuery] int? height,
 1192        [FromQuery] int? quality,
 1193        [FromQuery] int? fillWidth,
 1194        [FromQuery] int? fillHeight,
 1195        [FromQuery] int? blur,
 1196        [FromQuery] string? backgroundColor,
 1197        [FromQuery] string? foregroundLayer,
 1198        [FromQuery] int? imageIndex)
 1199    {
 01200        var item = _libraryManager.GetPerson(name);
 01201        if (item is null)
 1202        {
 01203            return NotFound();
 1204        }
 1205
 01206        return await GetImageInternal(
 01207                item.Id,
 01208                imageType,
 01209                imageIndex,
 01210                tag,
 01211                format,
 01212                maxWidth,
 01213                maxHeight,
 01214                percentPlayed,
 01215                unplayedCount,
 01216                width,
 01217                height,
 01218                quality,
 01219                fillWidth,
 01220                fillHeight,
 01221                blur,
 01222                backgroundColor,
 01223                foregroundLayer,
 01224                item)
 01225            .ConfigureAwait(false);
 01226    }
 1227
 1228    /// <summary>
 1229    /// Get person image by name.
 1230    /// </summary>
 1231    /// <param name="name">Person name.</param>
 1232    /// <param name="imageType">Image type.</param>
 1233    /// <param name="imageIndex">Image index.</param>
 1234    /// <param name="tag">Optional. Supply the cache tag from the item object to receive strong caching headers.</param>
 1235    /// <param name="format">Determines the output format of the image - original,gif,jpg,png.</param>
 1236    /// <param name="maxWidth">The maximum image width to return.</param>
 1237    /// <param name="maxHeight">The maximum image height to return.</param>
 1238    /// <param name="percentPlayed">Optional. Percent to render for the percent played overlay.</param>
 1239    /// <param name="unplayedCount">Optional. Unplayed count overlay to render.</param>
 1240    /// <param name="width">The fixed image width to return.</param>
 1241    /// <param name="height">The fixed image height to return.</param>
 1242    /// <param name="quality">Optional. Quality setting, from 0-100. Defaults to 90 and should suffice in most cases.</p
 1243    /// <param name="fillWidth">Width of box to fill.</param>
 1244    /// <param name="fillHeight">Height of box to fill.</param>
 1245    /// <param name="blur">Optional. Blur image.</param>
 1246    /// <param name="backgroundColor">Optional. Apply a background color for transparent images.</param>
 1247    /// <param name="foregroundLayer">Optional. Apply a foreground layer on top of the image.</param>
 1248    /// <response code="200">Image stream returned.</response>
 1249    /// <response code="404">Item not found.</response>
 1250    /// <returns>
 1251    /// A <see cref="FileStreamResult"/> containing the file stream on success,
 1252    /// or a <see cref="NotFoundResult"/> if item not found.
 1253    /// </returns>
 1254    [HttpGet("Persons/{name}/Images/{imageType}/{imageIndex}")]
 1255    [HttpHead("Persons/{name}/Images/{imageType}/{imageIndex}", Name = "HeadPersonImageByIndex")]
 1256    [ProducesResponseType(StatusCodes.Status200OK)]
 1257    [ProducesResponseType(StatusCodes.Status404NotFound)]
 1258    [ProducesImageFile]
 1259    public async Task<ActionResult> GetPersonImageByIndex(
 1260        [FromRoute, Required] string name,
 1261        [FromRoute, Required] ImageType imageType,
 1262        [FromRoute, Required] int imageIndex,
 1263        [FromQuery] string? tag,
 1264        [FromQuery] ImageFormat? format,
 1265        [FromQuery] int? maxWidth,
 1266        [FromQuery] int? maxHeight,
 1267        [FromQuery] double? percentPlayed,
 1268        [FromQuery] int? unplayedCount,
 1269        [FromQuery] int? width,
 1270        [FromQuery] int? height,
 1271        [FromQuery] int? quality,
 1272        [FromQuery] int? fillWidth,
 1273        [FromQuery] int? fillHeight,
 1274        [FromQuery] int? blur,
 1275        [FromQuery] string? backgroundColor,
 1276        [FromQuery] string? foregroundLayer)
 1277    {
 01278        var item = _libraryManager.GetPerson(name);
 01279        if (item is null)
 1280        {
 01281            return NotFound();
 1282        }
 1283
 01284        return await GetImageInternal(
 01285                item.Id,
 01286                imageType,
 01287                imageIndex,
 01288                tag,
 01289                format,
 01290                maxWidth,
 01291                maxHeight,
 01292                percentPlayed,
 01293                unplayedCount,
 01294                width,
 01295                height,
 01296                quality,
 01297                fillWidth,
 01298                fillHeight,
 01299                blur,
 01300                backgroundColor,
 01301                foregroundLayer,
 01302                item)
 01303            .ConfigureAwait(false);
 01304    }
 1305
 1306    /// <summary>
 1307    /// Get studio image by name.
 1308    /// </summary>
 1309    /// <param name="name">Studio name.</param>
 1310    /// <param name="imageType">Image type.</param>
 1311    /// <param name="tag">Optional. Supply the cache tag from the item object to receive strong caching headers.</param>
 1312    /// <param name="format">Determines the output format of the image - original,gif,jpg,png.</param>
 1313    /// <param name="maxWidth">The maximum image width to return.</param>
 1314    /// <param name="maxHeight">The maximum image height to return.</param>
 1315    /// <param name="percentPlayed">Optional. Percent to render for the percent played overlay.</param>
 1316    /// <param name="unplayedCount">Optional. Unplayed count overlay to render.</param>
 1317    /// <param name="width">The fixed image width to return.</param>
 1318    /// <param name="height">The fixed image height to return.</param>
 1319    /// <param name="quality">Optional. Quality setting, from 0-100. Defaults to 90 and should suffice in most cases.</p
 1320    /// <param name="fillWidth">Width of box to fill.</param>
 1321    /// <param name="fillHeight">Height of box to fill.</param>
 1322    /// <param name="blur">Optional. Blur image.</param>
 1323    /// <param name="backgroundColor">Optional. Apply a background color for transparent images.</param>
 1324    /// <param name="foregroundLayer">Optional. Apply a foreground layer on top of the image.</param>
 1325    /// <param name="imageIndex">Image index.</param>
 1326    /// <response code="200">Image stream returned.</response>
 1327    /// <response code="404">Item not found.</response>
 1328    /// <returns>
 1329    /// A <see cref="FileStreamResult"/> containing the file stream on success,
 1330    /// or a <see cref="NotFoundResult"/> if item not found.
 1331    /// </returns>
 1332    [HttpGet("Studios/{name}/Images/{imageType}")]
 1333    [HttpHead("Studios/{name}/Images/{imageType}", Name = "HeadStudioImage")]
 1334    [ProducesResponseType(StatusCodes.Status200OK)]
 1335    [ProducesResponseType(StatusCodes.Status404NotFound)]
 1336    [ProducesImageFile]
 1337    public async Task<ActionResult> GetStudioImage(
 1338        [FromRoute, Required] string name,
 1339        [FromRoute, Required] ImageType imageType,
 1340        [FromQuery] string? tag,
 1341        [FromQuery] ImageFormat? format,
 1342        [FromQuery] int? maxWidth,
 1343        [FromQuery] int? maxHeight,
 1344        [FromQuery] double? percentPlayed,
 1345        [FromQuery] int? unplayedCount,
 1346        [FromQuery] int? width,
 1347        [FromQuery] int? height,
 1348        [FromQuery] int? quality,
 1349        [FromQuery] int? fillWidth,
 1350        [FromQuery] int? fillHeight,
 1351        [FromQuery] int? blur,
 1352        [FromQuery] string? backgroundColor,
 1353        [FromQuery] string? foregroundLayer,
 1354        [FromQuery] int? imageIndex)
 1355    {
 01356        var item = _libraryManager.GetStudio(name);
 01357        if (item is null)
 1358        {
 01359            return NotFound();
 1360        }
 1361
 01362        return await GetImageInternal(
 01363                item.Id,
 01364                imageType,
 01365                imageIndex,
 01366                tag,
 01367                format,
 01368                maxWidth,
 01369                maxHeight,
 01370                percentPlayed,
 01371                unplayedCount,
 01372                width,
 01373                height,
 01374                quality,
 01375                fillWidth,
 01376                fillHeight,
 01377                blur,
 01378                backgroundColor,
 01379                foregroundLayer,
 01380                item)
 01381            .ConfigureAwait(false);
 01382    }
 1383
 1384    /// <summary>
 1385    /// Get studio image by name.
 1386    /// </summary>
 1387    /// <param name="name">Studio name.</param>
 1388    /// <param name="imageType">Image type.</param>
 1389    /// <param name="imageIndex">Image index.</param>
 1390    /// <param name="tag">Optional. Supply the cache tag from the item object to receive strong caching headers.</param>
 1391    /// <param name="format">Determines the output format of the image - original,gif,jpg,png.</param>
 1392    /// <param name="maxWidth">The maximum image width to return.</param>
 1393    /// <param name="maxHeight">The maximum image height to return.</param>
 1394    /// <param name="percentPlayed">Optional. Percent to render for the percent played overlay.</param>
 1395    /// <param name="unplayedCount">Optional. Unplayed count overlay to render.</param>
 1396    /// <param name="width">The fixed image width to return.</param>
 1397    /// <param name="height">The fixed image height to return.</param>
 1398    /// <param name="quality">Optional. Quality setting, from 0-100. Defaults to 90 and should suffice in most cases.</p
 1399    /// <param name="fillWidth">Width of box to fill.</param>
 1400    /// <param name="fillHeight">Height of box to fill.</param>
 1401    /// <param name="blur">Optional. Blur image.</param>
 1402    /// <param name="backgroundColor">Optional. Apply a background color for transparent images.</param>
 1403    /// <param name="foregroundLayer">Optional. Apply a foreground layer on top of the image.</param>
 1404    /// <response code="200">Image stream returned.</response>
 1405    /// <response code="404">Item not found.</response>
 1406    /// <returns>
 1407    /// A <see cref="FileStreamResult"/> containing the file stream on success,
 1408    /// or a <see cref="NotFoundResult"/> if item not found.
 1409    /// </returns>
 1410    [HttpGet("Studios/{name}/Images/{imageType}/{imageIndex}")]
 1411    [HttpHead("Studios/{name}/Images/{imageType}/{imageIndex}", Name = "HeadStudioImageByIndex")]
 1412    [ProducesResponseType(StatusCodes.Status200OK)]
 1413    [ProducesResponseType(StatusCodes.Status404NotFound)]
 1414    [ProducesImageFile]
 1415    public async Task<ActionResult> GetStudioImageByIndex(
 1416        [FromRoute, Required] string name,
 1417        [FromRoute, Required] ImageType imageType,
 1418        [FromRoute, Required] int imageIndex,
 1419        [FromQuery] string? tag,
 1420        [FromQuery] ImageFormat? format,
 1421        [FromQuery] int? maxWidth,
 1422        [FromQuery] int? maxHeight,
 1423        [FromQuery] double? percentPlayed,
 1424        [FromQuery] int? unplayedCount,
 1425        [FromQuery] int? width,
 1426        [FromQuery] int? height,
 1427        [FromQuery] int? quality,
 1428        [FromQuery] int? fillWidth,
 1429        [FromQuery] int? fillHeight,
 1430        [FromQuery] int? blur,
 1431        [FromQuery] string? backgroundColor,
 1432        [FromQuery] string? foregroundLayer)
 1433    {
 01434        var item = _libraryManager.GetStudio(name);
 01435        if (item is null)
 1436        {
 01437            return NotFound();
 1438        }
 1439
 01440        return await GetImageInternal(
 01441                item.Id,
 01442                imageType,
 01443                imageIndex,
 01444                tag,
 01445                format,
 01446                maxWidth,
 01447                maxHeight,
 01448                percentPlayed,
 01449                unplayedCount,
 01450                width,
 01451                height,
 01452                quality,
 01453                fillWidth,
 01454                fillHeight,
 01455                blur,
 01456                backgroundColor,
 01457                foregroundLayer,
 01458                item)
 01459            .ConfigureAwait(false);
 01460    }
 1461
 1462    /// <summary>
 1463    /// Get user profile image.
 1464    /// </summary>
 1465    /// <param name="userId">User id.</param>
 1466    /// <param name="tag">Optional. Supply the cache tag from the item object to receive strong caching headers.</param>
 1467    /// <param name="format">Determines the output format of the image - original,gif,jpg,png.</param>
 1468    /// <response code="200">Image stream returned.</response>
 1469    /// <response code="400">User id not provided.</response>
 1470    /// <response code="404">Item not found.</response>
 1471    /// <returns>
 1472    /// A <see cref="FileStreamResult"/> containing the file stream on success,
 1473    /// or a <see cref="NotFoundResult"/> if item not found.
 1474    /// </returns>
 1475    [HttpGet("UserImage")]
 1476    [HttpHead("UserImage", Name = "HeadUserImage")]
 1477    [ProducesResponseType(StatusCodes.Status200OK)]
 1478    [ProducesResponseType(StatusCodes.Status400BadRequest)]
 1479    [ProducesResponseType(StatusCodes.Status404NotFound)]
 1480    [ProducesImageFile]
 1481    public async Task<ActionResult> GetUserImage(
 1482        [FromQuery] Guid? userId,
 1483        [FromQuery] string? tag,
 1484        [FromQuery] ImageFormat? format)
 1485    {
 01486        var requestUserId = userId ?? User.GetUserId();
 01487        if (requestUserId.IsEmpty())
 1488        {
 01489            return BadRequest("UserId is required if unauthenticated");
 1490        }
 1491
 01492        var user = _userManager.GetUserById(requestUserId);
 01493        if (user?.ProfileImage is null)
 1494        {
 01495            return NotFound();
 1496        }
 1497
 01498        var info = new ItemImageInfo
 01499        {
 01500            Path = user.ProfileImage.Path,
 01501            Type = ImageType.Profile,
 01502            DateModified = user.ProfileImage.LastModified
 01503        };
 1504
 01505        return await GetImageInternal(
 01506                user.Id,
 01507                ImageType.Profile,
 01508                null,
 01509                tag,
 01510                format,
 01511                null,
 01512                null,
 01513                null,
 01514                null,
 01515                null,
 01516                null,
 01517                90,
 01518                null,
 01519                null,
 01520                null,
 01521                null,
 01522                null,
 01523                null,
 01524                info)
 01525            .ConfigureAwait(false);
 01526    }
 1527
 1528    /// <summary>
 1529    /// Get user profile image.
 1530    /// </summary>
 1531    /// <param name="userId">User id.</param>
 1532    /// <param name="imageType">Image type.</param>
 1533    /// <param name="tag">Optional. Supply the cache tag from the item object to receive strong caching headers.</param>
 1534    /// <param name="format">Determines the output format of the image - original,gif,jpg,png.</param>
 1535    /// <param name="maxWidth">The maximum image width to return.</param>
 1536    /// <param name="maxHeight">The maximum image height to return.</param>
 1537    /// <param name="percentPlayed">Optional. Percent to render for the percent played overlay.</param>
 1538    /// <param name="unplayedCount">Optional. Unplayed count overlay to render.</param>
 1539    /// <param name="width">The fixed image width to return.</param>
 1540    /// <param name="height">The fixed image height to return.</param>
 1541    /// <param name="quality">Optional. Quality setting, from 0-100. Defaults to 90 and should suffice in most cases.</p
 1542    /// <param name="fillWidth">Width of box to fill.</param>
 1543    /// <param name="fillHeight">Height of box to fill.</param>
 1544    /// <param name="blur">Optional. Blur image.</param>
 1545    /// <param name="backgroundColor">Optional. Apply a background color for transparent images.</param>
 1546    /// <param name="foregroundLayer">Optional. Apply a foreground layer on top of the image.</param>
 1547    /// <param name="imageIndex">Image index.</param>
 1548    /// <response code="200">Image stream returned.</response>
 1549    /// <response code="404">Item not found.</response>
 1550    /// <returns>
 1551    /// A <see cref="FileStreamResult"/> containing the file stream on success,
 1552    /// or a <see cref="NotFoundResult"/> if item not found.
 1553    /// </returns>
 1554    [HttpGet("Users/{userId}/Images/{imageType}")]
 1555    [HttpHead("Users/{userId}/Images/{imageType}", Name = "HeadUserImageLegacy")]
 1556    [Obsolete("Kept for backwards compatibility")]
 1557    [ApiExplorerSettings(IgnoreApi = true)]
 1558    [ProducesResponseType(StatusCodes.Status200OK)]
 1559    [ProducesResponseType(StatusCodes.Status404NotFound)]
 1560    [ProducesImageFile]
 1561    public Task<ActionResult> GetUserImageLegacy(
 1562        [FromRoute, Required] Guid userId,
 1563        [FromRoute, Required] ImageType imageType,
 1564        [FromQuery] string? tag,
 1565        [FromQuery] ImageFormat? format,
 1566        [FromQuery] int? maxWidth,
 1567        [FromQuery] int? maxHeight,
 1568        [FromQuery] double? percentPlayed,
 1569        [FromQuery] int? unplayedCount,
 1570        [FromQuery] int? width,
 1571        [FromQuery] int? height,
 1572        [FromQuery] int? quality,
 1573        [FromQuery] int? fillWidth,
 1574        [FromQuery] int? fillHeight,
 1575        [FromQuery] int? blur,
 1576        [FromQuery] string? backgroundColor,
 1577        [FromQuery] string? foregroundLayer,
 1578        [FromQuery] int? imageIndex)
 1579        => GetUserImage(
 1580            userId,
 1581            tag,
 1582            format);
 1583
 1584    /// <summary>
 1585    /// Get user profile image.
 1586    /// </summary>
 1587    /// <param name="userId">User id.</param>
 1588    /// <param name="imageType">Image type.</param>
 1589    /// <param name="imageIndex">Image index.</param>
 1590    /// <param name="tag">Optional. Supply the cache tag from the item object to receive strong caching headers.</param>
 1591    /// <param name="format">Determines the output format of the image - original,gif,jpg,png.</param>
 1592    /// <param name="maxWidth">The maximum image width to return.</param>
 1593    /// <param name="maxHeight">The maximum image height to return.</param>
 1594    /// <param name="percentPlayed">Optional. Percent to render for the percent played overlay.</param>
 1595    /// <param name="unplayedCount">Optional. Unplayed count overlay to render.</param>
 1596    /// <param name="width">The fixed image width to return.</param>
 1597    /// <param name="height">The fixed image height to return.</param>
 1598    /// <param name="quality">Optional. Quality setting, from 0-100. Defaults to 90 and should suffice in most cases.</p
 1599    /// <param name="fillWidth">Width of box to fill.</param>
 1600    /// <param name="fillHeight">Height of box to fill.</param>
 1601    /// <param name="blur">Optional. Blur image.</param>
 1602    /// <param name="backgroundColor">Optional. Apply a background color for transparent images.</param>
 1603    /// <param name="foregroundLayer">Optional. Apply a foreground layer on top of the image.</param>
 1604    /// <response code="200">Image stream returned.</response>
 1605    /// <response code="404">Item not found.</response>
 1606    /// <returns>
 1607    /// A <see cref="FileStreamResult"/> containing the file stream on success,
 1608    /// or a <see cref="NotFoundResult"/> if item not found.
 1609    /// </returns>
 1610    [HttpGet("Users/{userId}/Images/{imageType}/{imageIndex}")]
 1611    [HttpHead("Users/{userId}/Images/{imageType}/{imageIndex}", Name = "HeadUserImageByIndexLegacy")]
 1612    [Obsolete("Kept for backwards compatibility")]
 1613    [ApiExplorerSettings(IgnoreApi = true)]
 1614    [ProducesResponseType(StatusCodes.Status200OK)]
 1615    [ProducesResponseType(StatusCodes.Status404NotFound)]
 1616    [ProducesImageFile]
 1617    public Task<ActionResult> GetUserImageByIndexLegacy(
 1618        [FromRoute, Required] Guid userId,
 1619        [FromRoute, Required] ImageType imageType,
 1620        [FromRoute, Required] int imageIndex,
 1621        [FromQuery] string? tag,
 1622        [FromQuery] ImageFormat? format,
 1623        [FromQuery] int? maxWidth,
 1624        [FromQuery] int? maxHeight,
 1625        [FromQuery] double? percentPlayed,
 1626        [FromQuery] int? unplayedCount,
 1627        [FromQuery] int? width,
 1628        [FromQuery] int? height,
 1629        [FromQuery] int? quality,
 1630        [FromQuery] int? fillWidth,
 1631        [FromQuery] int? fillHeight,
 1632        [FromQuery] int? blur,
 1633        [FromQuery] string? backgroundColor,
 1634        [FromQuery] string? foregroundLayer)
 1635        => GetUserImage(
 1636            userId,
 1637            tag,
 1638            format);
 1639
 1640    /// <summary>
 1641    /// Generates or gets the splashscreen.
 1642    /// </summary>
 1643    /// <param name="tag">Supply the cache tag from the item object to receive strong caching headers.</param>
 1644    /// <param name="format">Determines the output format of the image - original,gif,jpg,png.</param>
 1645    /// <response code="200">Splashscreen returned successfully.</response>
 1646    /// <returns>The splashscreen.</returns>
 1647    [HttpGet("Branding/Splashscreen")]
 1648    [ProducesResponseType(StatusCodes.Status200OK)]
 1649    [ProducesImageFile]
 1650    public async Task<ActionResult> GetSplashscreen(
 1651        [FromQuery] string? tag,
 1652        [FromQuery] ImageFormat? format)
 1653    {
 01654        var brandingOptions = _serverConfigurationManager.GetConfiguration<BrandingOptions>("branding");
 01655        var isAdmin = User.IsInRole(Constants.UserRoles.Administrator);
 01656        if (!brandingOptions.SplashscreenEnabled && !isAdmin)
 1657        {
 01658            return NotFound();
 1659        }
 1660
 1661        string splashscreenPath;
 1662
 01663        if (!string.IsNullOrWhiteSpace(brandingOptions.SplashscreenLocation)
 01664            && System.IO.File.Exists(brandingOptions.SplashscreenLocation))
 1665        {
 01666            splashscreenPath = brandingOptions.SplashscreenLocation;
 1667        }
 1668        else
 1669        {
 01670            splashscreenPath = Path.Combine(_appPaths.DataPath, "splashscreen.png");
 01671            if (!System.IO.File.Exists(splashscreenPath))
 1672            {
 01673                return NotFound();
 1674            }
 1675        }
 1676
 01677        var outputFormats = GetOutputFormats(format);
 1678
 01679        TimeSpan? cacheDuration = null;
 01680        if (!string.IsNullOrEmpty(tag))
 1681        {
 01682            cacheDuration = TimeSpan.FromDays(365);
 1683        }
 1684
 01685        var options = new ImageProcessingOptions
 01686        {
 01687            Image = new ItemImageInfo
 01688            {
 01689                Path = splashscreenPath
 01690            },
 01691            Height = null,
 01692            MaxHeight = null,
 01693            MaxWidth = null,
 01694            FillHeight = null,
 01695            FillWidth = null,
 01696            Quality = 90,
 01697            Width = null,
 01698            Blur = null,
 01699            BackgroundColor = null,
 01700            ForegroundLayer = null,
 01701            SupportedOutputFormats = outputFormats
 01702        };
 1703
 01704        return await GetImageResult(
 01705                options,
 01706                cacheDuration,
 01707                ImmutableDictionary<string, string>.Empty,
 01708                tag)
 01709            .ConfigureAwait(false);
 01710    }
 1711
 1712    /// <summary>
 1713    /// Uploads a custom splashscreen.
 1714    /// The body is expected to the image contents base64 encoded.
 1715    /// </summary>
 1716    /// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
 1717    /// <response code="204">Successfully uploaded new splashscreen.</response>
 1718    /// <response code="400">Error reading MimeType from uploaded image.</response>
 1719    /// <response code="403">User does not have permission to upload splashscreen..</response>
 1720    /// <exception cref="ArgumentException">Error reading the image format.</exception>
 1721    [HttpPost("Branding/Splashscreen")]
 1722    [Authorize(Policy = Policies.RequiresElevation)]
 1723    [ProducesResponseType(StatusCodes.Status204NoContent)]
 1724    [ProducesResponseType(StatusCodes.Status400BadRequest)]
 1725    [ProducesResponseType(StatusCodes.Status403Forbidden)]
 1726    [AcceptsImageFile]
 1727    public async Task<ActionResult> UploadCustomSplashscreen()
 1728    {
 01729        if (!TryGetImageExtensionFromContentType(Request.ContentType, out var extension))
 1730        {
 01731            return BadRequest("Incorrect ContentType.");
 1732        }
 1733
 01734        var stream = GetFromBase64Stream(Request.Body);
 01735        await using (stream.ConfigureAwait(false))
 1736        {
 01737            var filePath = Path.Combine(_appPaths.DataPath, "splashscreen-upload" + extension);
 01738            var brandingOptions = _serverConfigurationManager.GetConfiguration<BrandingOptions>("branding");
 01739            brandingOptions.SplashscreenLocation = filePath;
 01740            _serverConfigurationManager.SaveConfiguration("branding", brandingOptions);
 1741
 01742            var fs = new FileStream(filePath, FileMode.Create, FileAccess.Write, FileShare.None, IODefaults.FileStreamBu
 01743            await using (fs.ConfigureAwait(false))
 1744            {
 01745                await stream.CopyToAsync(fs, CancellationToken.None).ConfigureAwait(false);
 1746            }
 1747
 01748            return NoContent();
 1749        }
 01750    }
 1751
 1752    /// <summary>
 1753    /// Delete a custom splashscreen.
 1754    /// </summary>
 1755    /// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
 1756    /// <response code="204">Successfully deleted the custom splashscreen.</response>
 1757    /// <response code="403">User does not have permission to delete splashscreen..</response>
 1758    [HttpDelete("Branding/Splashscreen")]
 1759    [Authorize(Policy = Policies.RequiresElevation)]
 1760    [ProducesResponseType(StatusCodes.Status204NoContent)]
 1761    public ActionResult DeleteCustomSplashscreen()
 1762    {
 01763        var brandingOptions = _serverConfigurationManager.GetConfiguration<BrandingOptions>("branding");
 01764        if (!string.IsNullOrEmpty(brandingOptions.SplashscreenLocation)
 01765            && System.IO.File.Exists(brandingOptions.SplashscreenLocation))
 1766        {
 01767            System.IO.File.Delete(brandingOptions.SplashscreenLocation);
 01768            brandingOptions.SplashscreenLocation = null;
 01769            _serverConfigurationManager.SaveConfiguration("branding", brandingOptions);
 1770        }
 1771
 01772        return NoContent();
 1773    }
 1774
 1775    private ImageInfo? GetImageInfo(BaseItem item, ItemImageInfo info, int? imageIndex)
 1776    {
 01777        int? width = null;
 01778        int? height = null;
 01779        string? blurhash = null;
 01780        long length = 0;
 1781
 1782        try
 1783        {
 01784            if (info.IsLocalFile)
 1785            {
 01786                var fileInfo = _fileSystem.GetFileInfo(info.Path);
 01787                length = fileInfo.Length;
 1788
 01789                blurhash = info.BlurHash;
 01790                width = info.Width;
 01791                height = info.Height;
 1792
 01793                if (width <= 0 || height <= 0)
 1794                {
 01795                    width = null;
 01796                    height = null;
 1797                }
 1798            }
 01799        }
 01800        catch (Exception ex)
 1801        {
 01802            _logger.LogError(ex, "Error getting image information for {Item}", item.Name);
 01803        }
 1804
 1805        try
 1806        {
 01807            return new ImageInfo
 01808            {
 01809                Path = info.Path,
 01810                ImageIndex = imageIndex,
 01811                ImageType = info.Type,
 01812                ImageTag = _imageProcessor.GetImageCacheTag(item, info),
 01813                Size = length,
 01814                BlurHash = blurhash,
 01815                Width = width,
 01816                Height = height
 01817            };
 1818        }
 01819        catch (Exception ex)
 1820        {
 01821            _logger.LogError(ex, "Error getting image information for {Path}", info.Path);
 01822            return null;
 1823        }
 01824    }
 1825
 1826    private async Task<ActionResult> GetImageInternal(
 1827        Guid itemId,
 1828        ImageType imageType,
 1829        int? imageIndex,
 1830        string? tag,
 1831        ImageFormat? format,
 1832        int? maxWidth,
 1833        int? maxHeight,
 1834        double? percentPlayed,
 1835        int? unplayedCount,
 1836        int? width,
 1837        int? height,
 1838        int? quality,
 1839        int? fillWidth,
 1840        int? fillHeight,
 1841        int? blur,
 1842        string? backgroundColor,
 1843        string? foregroundLayer,
 1844        BaseItem? item,
 1845        ItemImageInfo? imageInfo = null)
 1846    {
 01847        if (percentPlayed.HasValue)
 1848        {
 01849            if (percentPlayed.Value <= 0)
 1850            {
 01851                percentPlayed = null;
 1852            }
 01853            else if (percentPlayed.Value >= 100)
 1854            {
 01855                percentPlayed = null;
 1856            }
 1857        }
 1858
 01859        if (percentPlayed.HasValue)
 1860        {
 01861            unplayedCount = null;
 1862        }
 1863
 01864        if (unplayedCount.HasValue
 01865            && unplayedCount.Value <= 0)
 1866        {
 01867            unplayedCount = null;
 1868        }
 1869
 01870        if (imageInfo is null)
 1871        {
 01872            imageInfo = item?.GetImageInfo(imageType, imageIndex ?? 0);
 01873            if (imageInfo is null)
 1874            {
 01875                return NotFound(string.Format(NumberFormatInfo.InvariantInfo, "{0} does not have an image of type {1}", 
 1876            }
 1877        }
 1878
 01879        var outputFormats = GetOutputFormats(format);
 1880
 01881        TimeSpan? cacheDuration = null;
 1882
 01883        if (!string.IsNullOrEmpty(tag))
 1884        {
 01885            cacheDuration = TimeSpan.FromDays(365);
 1886        }
 1887
 01888        var responseHeaders = new Dictionary<string, string>
 01889        {
 01890            { "transferMode.dlna.org", "Interactive" },
 01891            { "realTimeInfo.dlna.org", "DLNA.ORG_TLAG=*" }
 01892        };
 1893
 01894        if (!imageInfo.IsLocalFile && item is not null)
 1895        {
 01896            imageInfo = await _libraryManager.ConvertImageToLocal(item, imageInfo, imageIndex ?? 0).ConfigureAwait(false
 1897        }
 1898
 01899        var options = new ImageProcessingOptions
 01900        {
 01901            Height = height,
 01902            ImageIndex = imageIndex ?? 0,
 01903            Image = imageInfo,
 01904            Item = item,
 01905            ItemId = itemId,
 01906            MaxHeight = maxHeight,
 01907            MaxWidth = maxWidth,
 01908            FillHeight = fillHeight,
 01909            FillWidth = fillWidth,
 01910            Quality = quality ?? 100,
 01911            Width = width,
 01912            PercentPlayed = percentPlayed ?? 0,
 01913            UnplayedCount = unplayedCount,
 01914            Blur = blur,
 01915            BackgroundColor = backgroundColor,
 01916            ForegroundLayer = foregroundLayer,
 01917            SupportedOutputFormats = outputFormats
 01918        };
 1919
 01920        return await GetImageResult(
 01921            options,
 01922            cacheDuration,
 01923            responseHeaders,
 01924            tag).ConfigureAwait(false);
 01925    }
 1926
 1927    private ImageFormat[] GetOutputFormats(ImageFormat? format)
 1928    {
 01929        if (format.HasValue)
 1930        {
 01931            return [format.Value];
 1932        }
 1933
 01934        return GetClientSupportedFormats();
 1935    }
 1936
 1937    private ImageFormat[] GetClientSupportedFormats()
 1938    {
 01939        var supportedFormats = Request.Headers.GetCommaSeparatedValues(HeaderNames.Accept);
 01940        for (var i = 0; i < supportedFormats.Length; i++)
 1941        {
 1942            // Remove charsets etc. (anything after semi-colon)
 01943            var type = supportedFormats[i];
 01944            int index = type.IndexOf(';', StringComparison.Ordinal);
 01945            if (index != -1)
 1946            {
 01947                supportedFormats[i] = type.Substring(0, index);
 1948            }
 1949        }
 1950
 01951        var acceptParam = Request.Query[HeaderNames.Accept];
 1952
 01953        var supportsWebP = SupportsFormat(supportedFormats, acceptParam, ImageFormat.Webp, false);
 1954
 01955        if (!supportsWebP)
 1956        {
 01957            var userAgent = Request.Headers[HeaderNames.UserAgent].ToString();
 01958            if (userAgent.Contains("crosswalk", StringComparison.OrdinalIgnoreCase)
 01959                && userAgent.Contains("android", StringComparison.OrdinalIgnoreCase))
 1960            {
 01961                supportsWebP = true;
 1962            }
 1963        }
 1964
 01965        var formats = new List<ImageFormat>(4);
 1966
 01967        if (supportsWebP)
 1968        {
 01969            formats.Add(ImageFormat.Webp);
 1970        }
 1971
 01972        formats.Add(ImageFormat.Jpg);
 01973        formats.Add(ImageFormat.Png);
 1974
 01975        if (SupportsFormat(supportedFormats, acceptParam, ImageFormat.Gif, true))
 1976        {
 01977            formats.Add(ImageFormat.Gif);
 1978        }
 1979
 01980        return formats.ToArray();
 1981    }
 1982
 1983    private bool SupportsFormat(IReadOnlyCollection<string> requestAcceptTypes, string? acceptParam, ImageFormat format,
 1984    {
 01985        if (requestAcceptTypes.Contains(format.GetMimeType()))
 1986        {
 01987            return true;
 1988        }
 1989
 01990        if (acceptAll && requestAcceptTypes.Contains("*/*"))
 1991        {
 01992            return true;
 1993        }
 1994
 1995        // Review if this should be jpeg, jpg or both for ImageFormat.Jpg
 01996        var normalized = format.ToString().ToLowerInvariant();
 01997        return string.Equals(acceptParam, normalized, StringComparison.OrdinalIgnoreCase);
 1998    }
 1999
 2000    private async Task<ActionResult> GetImageResult(
 2001        ImageProcessingOptions imageProcessingOptions,
 2002        TimeSpan? cacheDuration,
 2003        IDictionary<string, string> headers,
 2004        string? tag)
 2005    {
 02006        var (imagePath, imageContentType, dateImageModified) = await _imageProcessor.ProcessImage(imageProcessingOptions
 2007
 02008        var disableCaching = Request.Headers[HeaderNames.CacheControl].Contains("no-cache");
 02009        var hasTag = !string.IsNullOrEmpty(tag);
 2010
 02011        foreach (var (key, value) in headers)
 2012        {
 02013            Response.Headers.Append(key, value);
 2014        }
 2015
 02016        Response.ContentType = imageContentType ?? MediaTypeNames.Text.Plain;
 02017        Response.Headers.Append(HeaderNames.Age, Convert.ToInt64((DateTime.UtcNow - dateImageModified).TotalSeconds).ToS
 02018        Response.Headers.Append(HeaderNames.Vary, HeaderNames.Accept);
 2019
 02020        Response.Headers.ContentDisposition = "attachment";
 2021
 02022        if (disableCaching)
 2023        {
 02024            Response.Headers.Append(HeaderNames.CacheControl, "no-cache, no-store, must-revalidate");
 02025            Response.Headers.Append(HeaderNames.Pragma, "no-cache, no-store, must-revalidate");
 2026        }
 2027        else
 2028        {
 02029            if (cacheDuration.HasValue)
 2030            {
 2031                // When tag is provided, the URL is effectively immutable - the tag changes when the image changes
 02032                Response.Headers.Append(HeaderNames.CacheControl, "public, max-age=" + cacheDuration.Value.TotalSeconds 
 2033            }
 2034            else
 2035            {
 02036                Response.Headers.Append(HeaderNames.CacheControl, "public");
 2037            }
 2038
 02039            Response.Headers.Append(HeaderNames.LastModified, dateImageModified.ToUniversalTime().ToString("ddd, dd MMM 
 2040
 2041            // Add ETag header for stronger cache validation when tag is provided
 02042            if (hasTag)
 2043            {
 02044                Response.Headers.Append(HeaderNames.ETag, $"\"{tag}\"");
 2045
 2046                // Check If-None-Match header for ETag-based validation (preferred over If-Modified-Since)
 02047                var ifNoneMatch = Request.Headers[HeaderNames.IfNoneMatch].ToString();
 02048                if (!string.IsNullOrEmpty(ifNoneMatch)
 02049                    && (string.Equals(ifNoneMatch, $"\"{tag}\"", StringComparison.Ordinal)
 02050                        || string.Equals(ifNoneMatch, tag, StringComparison.Ordinal)))
 2051                {
 02052                    Response.StatusCode = StatusCodes.Status304NotModified;
 02053                    return new ContentResult();
 2054                }
 2055            }
 2056
 2057            // Check If-Modified-Since header for time-based validation
 02058            if (DateTime.TryParse(Request.Headers[HeaderNames.IfModifiedSince], CultureInfo.InvariantCulture, out var if
 2059            {
 2060                // Return 304 if the image has not been modified since the client's cached version
 02061                if (dateImageModified <= ifModifiedSinceHeader)
 2062                {
 02063                    Response.StatusCode = StatusCodes.Status304NotModified;
 02064                    return new ContentResult();
 2065                }
 2066            }
 2067        }
 2068
 02069        return PhysicalFile(imagePath, imageContentType ?? MediaTypeNames.Text.Plain);
 02070    }
 2071
 2072    internal static bool TryGetImageExtensionFromContentType(string? contentType, [NotNullWhen(true)] out string? extens
 2073    {
 142074        extension = null;
 142075        if (string.IsNullOrEmpty(contentType))
 2076        {
 22077            return false;
 2078        }
 2079
 122080        if (MediaTypeHeaderValue.TryParse(contentType, out var parsedValue)
 122081            && parsedValue.MediaType.HasValue
 122082            && MimeTypes.IsImage(parsedValue.MediaType.Value))
 2083        {
 112084            extension = MimeTypes.ToExtension(parsedValue.MediaType.Value);
 112085            return extension is not null;
 2086        }
 2087
 12088        return false;
 2089    }
 2090}