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