using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Jellyfin.Api.Constants;
using Jellyfin.Api.Helpers;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Drawing;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Net;
using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.Drawing;
using MediaBrowser.Model.Dto;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.IO;
using MediaBrowser.Model.Net;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
using Microsoft.Net.Http.Headers;
namespace Jellyfin.Api.Controllers
{
///
/// Image controller.
///
public class ImageController : BaseJellyfinApiController
{
private readonly IUserManager _userManager;
private readonly ILibraryManager _libraryManager;
private readonly IProviderManager _providerManager;
private readonly IImageProcessor _imageProcessor;
private readonly IFileSystem _fileSystem;
private readonly IAuthorizationContext _authContext;
private readonly ILogger _logger;
private readonly IServerConfigurationManager _serverConfigurationManager;
///
/// Initializes a new instance of the class.
///
/// Instance of the interface.
/// Instance of the interface.
/// Instance of the interface.
/// Instance of the interface.
/// Instance of the interface.
/// Instance of the interface.
/// Instance of the interface.
/// Instance of the interface.
public ImageController(
IUserManager userManager,
ILibraryManager libraryManager,
IProviderManager providerManager,
IImageProcessor imageProcessor,
IFileSystem fileSystem,
IAuthorizationContext authContext,
ILogger logger,
IServerConfigurationManager serverConfigurationManager)
{
_userManager = userManager;
_libraryManager = libraryManager;
_providerManager = providerManager;
_imageProcessor = imageProcessor;
_fileSystem = fileSystem;
_authContext = authContext;
_logger = logger;
_serverConfigurationManager = serverConfigurationManager;
}
///
/// Sets the user image.
///
/// User Id.
/// (Unused) Image type.
/// (Unused) Image index.
/// Image updated.
/// A .
[HttpPost("/Users/{userId}/Images/{imageType}")]
[HttpPost("/Users/{userId}/Images/{imageType}/{index?}")]
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "imageType", Justification = "Imported from ServiceStack")]
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "index", Justification = "Imported from ServiceStack")]
public async Task PostUserImage(
[FromRoute] Guid userId,
[FromRoute] ImageType imageType,
[FromRoute] int? index = null)
{
if (!RequestHelpers.AssertCanUpdateUser(_authContext, HttpContext.Request, userId, true))
{
return Forbid("User is not allowed to update the image.");
}
var user = _userManager.GetUserById(userId);
await using var memoryStream = await GetMemoryStream(Request.Body).ConfigureAwait(false);
// Handle image/png; charset=utf-8
var mimeType = Request.ContentType.Split(';').FirstOrDefault();
var userDataPath = Path.Combine(_serverConfigurationManager.ApplicationPaths.UserConfigurationDirectoryPath, user.Username);
user.ProfileImage = new Data.Entities.ImageInfo(Path.Combine(userDataPath, "profile" + MimeTypes.ToExtension(mimeType)));
await _providerManager
.SaveImage(user, memoryStream, mimeType, user.ProfileImage.Path)
.ConfigureAwait(false);
await _userManager.UpdateUserAsync(user).ConfigureAwait(false);
return NoContent();
}
///
/// Delete the user's image.
///
/// User Id.
/// (Unused) Image type.
/// (Unused) Image index.
/// Image deleted.
/// A .
[HttpDelete("/Users/{userId}/Images/{itemType}")]
[HttpDelete("/Users/{userId}/Images/{itemType}/{index?}")]
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "imageType", Justification = "Imported from ServiceStack")]
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "index", Justification = "Imported from ServiceStack")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
public ActionResult DeleteUserImage(
[FromRoute] Guid userId,
[FromRoute] ImageType imageType,
[FromRoute] int? index = null)
{
if (!RequestHelpers.AssertCanUpdateUser(_authContext, HttpContext.Request, userId, true))
{
return Forbid("User is not allowed to delete the image.");
}
var user = _userManager.GetUserById(userId);
try
{
System.IO.File.Delete(user.ProfileImage.Path);
}
catch (IOException e)
{
_logger.LogError(e, "Error deleting user profile image:");
}
_userManager.ClearProfileImage(user);
return NoContent();
}
///
/// Delete an item's image.
///
/// Item id.
/// Image type.
/// The image index.
/// Image deleted.
/// Item not found.
/// A on success, or a if item not found.
[HttpDelete("/Items/{itemId}/Images/{imageType}")]
[HttpDelete("/Items/{itemId}/Images/{imageType}/{imageIndex?}")]
[Authorize(Policy = Policies.RequiresElevation)]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public ActionResult DeleteItemImage(
[FromRoute] Guid itemId,
[FromRoute] ImageType imageType,
[FromRoute] int? imageIndex = null)
{
var item = _libraryManager.GetItemById(itemId);
if (item == null)
{
return NotFound();
}
item.DeleteImage(imageType, imageIndex ?? 0);
return NoContent();
}
///
/// Set item image.
///
/// Item id.
/// Image type.
/// (Unused) Image index.
/// Image saved.
/// Item not found.
/// A on success, or a if item not found.
[HttpPost("/Items/{itemId}/Images/{imageType}")]
[HttpPost("/Items/{itemId}/Images/{imageType}/{imageIndex?}")]
[Authorize(Policy = Policies.RequiresElevation)]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "index", Justification = "Imported from ServiceStack")]
public async Task SetItemImage(
[FromRoute] Guid itemId,
[FromRoute] ImageType imageType,
[FromRoute] int? imageIndex = null)
{
var item = _libraryManager.GetItemById(itemId);
if (item == null)
{
return NotFound();
}
// Handle image/png; charset=utf-8
var mimeType = Request.ContentType.Split(';').FirstOrDefault();
await _providerManager.SaveImage(item, Request.Body, mimeType, imageType, null, CancellationToken.None).ConfigureAwait(false);
item.UpdateToRepository(ItemUpdateType.ImageUpdate, CancellationToken.None);
return NoContent();
}
///
/// Updates the index for an item image.
///
/// Item id.
/// Image type.
/// Old image index.
/// New image index.
/// Image index updated.
/// Item not found.
/// A on success, or a if item not found.
[HttpPost("/Items/{itemId}/Images/{imageType}/{imageIndex}/Index")]
[Authorize(Policy = Policies.RequiresElevation)]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public ActionResult UpdateItemImageIndex(
[FromRoute] Guid itemId,
[FromRoute] ImageType imageType,
[FromRoute] int imageIndex,
[FromQuery] int newIndex)
{
var item = _libraryManager.GetItemById(itemId);
if (item == null)
{
return NotFound();
}
item.SwapImages(imageType, imageIndex, newIndex);
return NoContent();
}
///
/// Get item image infos.
///
/// Item id.
/// Item images returned.
/// Item not found.
/// The list of image infos on success, or if item not found.
[HttpGet("/Items/{itemId}/Images")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public ActionResult> GetItemImageInfos([FromRoute] Guid itemId)
{
var item = _libraryManager.GetItemById(itemId);
if (item == null)
{
return NotFound();
}
var list = new List();
var itemImages = item.ImageInfos;
if (itemImages.Length == 0)
{
// short-circuit
return list;
}
_libraryManager.UpdateImages(item); // this makes sure dimensions and hashes are correct
foreach (var image in itemImages)
{
if (!item.AllowsMultipleImages(image.Type))
{
var info = GetImageInfo(item, image, null);
if (info != null)
{
list.Add(info);
}
}
}
foreach (var imageType in itemImages.Select(i => i.Type).Distinct().Where(item.AllowsMultipleImages))
{
var index = 0;
// Prevent implicitly captured closure
var currentImageType = imageType;
foreach (var image in itemImages.Where(i => i.Type == currentImageType))
{
var info = GetImageInfo(item, image, index);
if (info != null)
{
list.Add(info);
}
index++;
}
}
return list;
}
///
/// Gets the item's image.
///
/// Item id.
/// Image type.
/// The maximum image width to return.
/// The maximum image height to return.
/// The fixed image width to return.
/// The fixed image height to return.
/// Optional. Quality setting, from 0-100. Defaults to 90 and should suffice in most cases.
/// Optional. Supply the cache tag from the item object to receive strong caching headers.
/// Optional. Specify if whitespace should be cropped out of the image. True/False. If unspecified, whitespace will be cropped from logos and clear art.
/// Determines the output format of the image - original,gif,jpg,png.
/// Optional. Add a played indicator.
/// Optional. Percent to render for the percent played overlay.
/// Optional. Unplayed count overlay to render.
/// Optional. Blur image.
/// Optional. Apply a background color for transparent images.
/// Optional. Apply a foreground layer on top of the image.
/// Image index.
/// Enable or disable image enhancers such as cover art.
/// Image stream returned.
/// Item not found.
///
/// A containing the file stream on success,
/// or a if item not found.
///
[HttpGet("/Items/{itemId}/Images/{imageType}")]
[HttpHead("/Items/{itemId}/Images/{imageType}")]
[HttpGet("/Items/{itemId}/Images/{imageType}/{imageIndex?}")]
[HttpHead("/Items/{itemId}/Images/{imageType}/{imageIndex?}")]
public async Task GetItemImage(
[FromRoute] Guid itemId,
[FromRoute] ImageType imageType,
[FromRoute] int? maxWidth,
[FromRoute] int? maxHeight,
[FromQuery] int? width,
[FromQuery] int? height,
[FromQuery] int? quality,
[FromQuery] string tag,
[FromQuery] bool? cropWhitespace,
[FromQuery] string format,
[FromQuery] bool addPlayedIndicator,
[FromQuery] double? percentPlayed,
[FromQuery] int? unplayedCount,
[FromQuery] int? blur,
[FromQuery] string backgroundColor,
[FromQuery] string foregroundLayer,
[FromRoute] int? imageIndex = null,
[FromQuery] bool enableImageEnhancers = true)
{
var item = _libraryManager.GetItemById(itemId);
if (item == null)
{
return NotFound();
}
return await GetImageInternal(
itemId,
imageType,
imageIndex,
tag,
format,
maxWidth,
maxHeight,
percentPlayed,
unplayedCount,
width,
height,
quality,
cropWhitespace,
addPlayedIndicator,
blur,
backgroundColor,
foregroundLayer,
enableImageEnhancers,
item,
Request.Method.Equals(HttpMethods.Head, StringComparison.OrdinalIgnoreCase))
.ConfigureAwait(false);
}
///
/// Gets the item's image.
///
/// Item id.
/// Image type.
/// Image index.
/// Optional. Supply the cache tag from the item object to receive strong caching headers.
/// Determines the output format of the image - original,gif,jpg,png.
/// The maximum image width to return.
/// The maximum image height to return.
/// Optional. Percent to render for the percent played overlay.
/// Optional. Unplayed count overlay to render.
/// The fixed image width to return.
/// The fixed image height to return.
/// Optional. Quality setting, from 0-100. Defaults to 90 and should suffice in most cases.
/// Optional. Specify if whitespace should be cropped out of the image. True/False. If unspecified, whitespace will be cropped from logos and clear art.
/// Optional. Add a played indicator.
/// Optional. Blur image.
/// Optional. Apply a background color for transparent images.
/// Optional. Apply a foreground layer on top of the image.
/// Enable or disable image enhancers such as cover art.
/// Image stream returned.
/// Item not found.
///
/// A containing the file stream on success,
/// or a if item not found.
///
[HttpGet("/Items/{itemId}/Images/{imageType}/{imageIndex}/{tag}/{format}/{maxWidth}/{maxHeight}/{percentPlayed}/{unplayedCount}")]
[HttpHead("/Items/{itemId}/Images/{imageType}/{imageIndex}/{tag}/{format}/{maxWidth}/{maxHeight}/{percentPlayed}/{unplayedCount}")]
public ActionResult