using ErsatzTV.Core; using ErsatzTV.Core.Api.Artwork; using ErsatzTV.Core.Domain; using ErsatzTV.Core.Interfaces.Images; namespace ErsatzTV.Application.Artworks; public class UploadArtworkHandler : IRequestHandler> { // png/jpeg/gif/webp are all decoded by SkiaSharp and read by FFmpeg, matching the // formats the Blazor logo/watermark upload already accepts. Format expansion is ersatztv#66. private static readonly System.Collections.Generic.HashSet AcceptedContentTypes = new(StringComparer.OrdinalIgnoreCase) { "image/png", "image/jpeg", "image/gif", "image/webp" }; private readonly IImageCache _imageCache; public UploadArtworkHandler(IImageCache imageCache) => _imageCache = imageCache; public async Task> Handle( UploadArtwork request, CancellationToken cancellationToken) { string contentType = (request.ContentType ?? string.Empty).Trim(); if (!AcceptedContentTypes.Contains(contentType)) { return BaseError.New( $"Unsupported image content type '{contentType}'; supported types are: {string.Join(", ", AcceptedContentTypes)}"); } Either maybeFileName = await _imageCache.SaveArtworkToCache( request.Stream, request.ArtworkKind); return maybeFileName.Map(fileName => new ArtworkUploadResponseModel( BuildPath(request.ArtworkKind, fileName), contentType)); } // Mirror the on-disk conventions the Blazor editors use so the returned path is a drop-in // for ArtworkContentTypeModel.Path: channel logos are addressed as "iptv/logos/{file}" // (see ChannelEditor.UploadLogo), watermarks by the bare cache file name (see WatermarkEditor). private static string BuildPath(ArtworkKind artworkKind, string fileName) => artworkKind switch { ArtworkKind.Logo => $"iptv/logos/{fileName}", _ => fileName }; }