using System.Xml.Linq; using Application.Abstractions.YouTube; using Microsoft.Extensions.Logging; namespace Infrastructure.YouTube; /// /// YouTube 공개 Atom Feed 폴링 (할당량 0 unit). /// 비용 없는 라이브 감지 — PubSub의 라이브 푸시 누락을 보완. /// internal sealed class YouTubeFeedPoller( IHttpClientFactory httpClientFactory, ILogger logger ) : IYouTubeFeedPoller { private const string FeedBase = "https://www.youtube.com/feeds/videos.xml?channel_id="; public async Task GetLatestVideoIdAsync(string youtubeChannelID, CancellationToken ct) { if (string.IsNullOrWhiteSpace(youtubeChannelID)) { return null; } try { var client = httpClientFactory.CreateClient("YouTubeFeed"); var url = FeedBase + Uri.EscapeDataString(youtubeChannelID); var response = await client.GetAsync(url, ct); if (!response.IsSuccessStatusCode) { logger.LogWarning("[FeedPoller] GET failed: channelId={ChannelId}, status={Status}", youtubeChannelID, response.StatusCode); return null; } var xml = await response.Content.ReadAsStringAsync(ct); var doc = XDocument.Parse(xml); XNamespace atom = "http://www.w3.org/2005/Atom"; XNamespace yt = "http://www.youtube.com/xml/schemas/2015"; var entry = doc.Descendants(atom + "entry").FirstOrDefault(); if (entry is null) { return null; } return entry.Element(yt + "videoId")?.Value; } catch (OperationCanceledException) { throw; } catch (Exception ex) { logger.LogError(ex, "[FeedPoller] Unexpected error: channelId={ChannelId}", youtubeChannelID); return null; } } }