using System.Security.Cryptography; namespace MusicBridge.Agent.Media; internal sealed record Artwork(string Id, string ContentType, byte[] Bytes); // Only the current cover is retained. IDs are content hashes, never file paths or URLs. internal sealed class ArtworkCache { public const int MaxBytes = 2 * 1024 * 1024; private Artwork? current; public Artwork? Current => Volatile.Read(ref current); public Artwork? Get(string id) => Current is { } artwork && artwork.Id == id ? artwork : null; public void Clear() => Volatile.Write(ref current, null); public async Task LoadAsync(Stream stream, CancellationToken cancellationToken) { Clear(); // A failed/new cover must not leave the previous track's image attached. using var output = new MemoryStream(); var buffer = new byte[16 * 1024]; while (true) { var count = await stream.ReadAsync(buffer, cancellationToken); if (count == 0) break; if (output.Length + count > MaxBytes) throw new InvalidDataException("Обложка больше 2 МиБ."); output.Write(buffer, 0, count); } var bytes = output.ToArray(); var type = DetectType(bytes) ?? throw new InvalidDataException("Неподдерживаемый формат обложки."); var artwork = new Artwork(Convert.ToHexString(SHA256.HashData(bytes)), type, bytes); Volatile.Write(ref current, artwork); return artwork; } private static string? DetectType(ReadOnlySpan data) { if (data.StartsWith(new byte[] { 137, 80, 78, 71, 13, 10, 26, 10 })) return "image/png"; if (data.StartsWith(new byte[] { 255, 216, 255 })) return "image/jpeg"; if (data.StartsWith("GIF87a"u8) || data.StartsWith("GIF89a"u8)) return "image/gif"; if (data.Length >= 12 && data[..4].SequenceEqual("RIFF"u8) && data.Slice(8, 4).SequenceEqual("WEBP"u8)) return "image/webp"; return null; } }