Files
windows/Media/ArtworkCache.cs
ros d5e827112a Add MusicBridge protocol documentation and implement system volume control
- Created PROTOCOL.md to outline the MusicBridge protocol v1, detailing setup, TLS, pairing, state management, and command structure.
- Implemented SystemVolume class for managing system audio levels, including methods for setting, reading, changing, and muting volume.
- Added unit tests for the MusicBridge agent, covering pairing, identity storage, command validation, and media state management.
- Included tests for real-time media reading from Windows and ensured proper handling of artwork caching and retrieval.
2026-09-09 01:05:52 +03:00

44 lines
2.0 KiB
C#

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<Artwork> 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<byte> 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;
}
}