using System.Text.Json; using Application.Abstractions.YouTube; using StackExchange.Redis; namespace Infrastructure.YouTube; /// /// Redis Hash 기반 라이브 상태 저장소 /// Key: "youtube:live:channels" (Hash — channelId → LiveStreamInfo JSON) /// PubSub 알림이 없으면 데이터가 없음 = 라이브 아님 /// internal sealed class YouTubeLiveStateStore(IConnectionMultiplexer redis) : IYouTubeLiveStateStore { private const string HashKey = "youtube:live:channels"; private static readonly TimeSpan MaxLiveDuration = TimeSpan.FromHours(12); // 안전장치: 12시간 후 자동 만료 public async Task SetLiveAsync(string channelId, YouTubeLiveStreamInfo liveInfo) { var db = redis.GetDatabase(); var json = JsonSerializer.Serialize(liveInfo); await db.HashSetAsync(HashKey, channelId, json); // 전체 Hash에 만료 설정 (최소 12시간은 유지) var ttl = await db.KeyTimeToLiveAsync(HashKey); if (ttl is null || ttl < MaxLiveDuration) { await db.KeyExpireAsync(HashKey, MaxLiveDuration); } } public async Task ClearLiveAsync(string channelId) { var db = redis.GetDatabase(); await db.HashDeleteAsync(HashKey, channelId); } public async Task GetLiveAsync(string channelId) { var db = redis.GetDatabase(); var json = await db.HashGetAsync(HashKey, channelId); if (json.IsNullOrEmpty) { return null; } return JsonSerializer.Deserialize(json.ToString()); } public async Task> GetAllLiveAsync() { var db = redis.GetDatabase(); var entries = await db.HashGetAllAsync(HashKey); var result = new List(); foreach (var entry in entries) { var info = JsonSerializer.Deserialize(entry.Value.ToString()); if (info is not null) { result.Add(info); } } return result.AsReadOnly(); } }