< Summary - Jellyfin

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

Feature is only available for sponsors

Upgrade to PRO version

Coverage history

Coverage history 0 25 50 75 100

Metrics

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

File(s)

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

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