using System.ComponentModel; using ErsatzTV.Application.Artworks; using ErsatzTV.Core; using ErsatzTV.Core.Api.Artwork; using ErsatzTV.Core.Domain; using ErsatzTV.Extensions; using MediatR; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; namespace ErsatzTV.Controllers.Api; [ApiController] public class ArtworkUploadController(IMediator mediator) : ControllerBase { [HttpPost("/api/artwork/uploads", Name = "UploadArtwork")] [Consumes("multipart/form-data")] [Tags("Artwork")] [EndpointSummary("Upload channel logo or watermark artwork")] [EndpointGroupName("general")] [ProducesResponseType(typeof(ArtworkUploadResponseModel), StatusCodes.Status201Created)] [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status422UnprocessableEntity)] public async Task Upload( IFormFile file, [FromForm] [Description("Artwork target: 'logo' (default) or 'watermark'")] string target, CancellationToken cancellationToken) { if (file is null || file.Length == 0) { return BaseError.New("A non-empty image file is required").ToErrorResult(); } long maxBytes = (long)SystemEnvironment.MaximumUploadMb * 1024 * 1024; if (file.Length > maxBytes) { return BaseError.New($"Image exceeds the maximum allowed size of {SystemEnvironment.MaximumUploadMb} MB") .ToErrorResult(); } if (!TryParseTarget(target, out ArtworkKind artworkKind)) { return BaseError.New($"Unknown upload target '{target}'; expected 'logo' or 'watermark'").ToErrorResult(); } await using Stream stream = file.OpenReadStream(); Either result = await mediator.Send( new UploadArtwork(stream, file.ContentType, artworkKind), cancellationToken); return result.ToCreatedResult( value => LocationFor(artworkKind, value.Path, value.ContentType), value => value); } // "logo" (default) and "watermark" are the two channel-artwork surfaces the API exposes today. private static bool TryParseTarget(string target, out ArtworkKind artworkKind) { switch ((target ?? string.Empty).Trim().ToLowerInvariant()) { case "": case "logo": artworkKind = ArtworkKind.Logo; return true; case "watermark": artworkKind = ArtworkKind.Watermark; return true; default: artworkKind = ArtworkKind.Logo; return false; } } // Both GetImage (IptvController) and GetWatermark (ArtworkController) require a contentType // query param to serve the cached file, so the Location header must carry it too. private static string LocationFor(ArtworkKind artworkKind, string path, string contentType) { string encodedContentType = Uri.EscapeDataString(contentType); return artworkKind switch { // logo paths already carry the servable prefix ("iptv/logos/{file}") ArtworkKind.Logo => $"/{path}?contentType={encodedContentType}", _ => $"/artwork/watermarks/{path}?contentType={encodedContentType}" }; } }