| 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061 |
- using System.Xml.Linq;
- using Application.Abstractions.YouTube;
- using Microsoft.Extensions.Logging;
- namespace Infrastructure.YouTube;
- /// <summary>
- /// YouTube 공개 Atom Feed 폴링 (할당량 0 unit).
- /// 비용 없는 라이브 감지 — PubSub의 라이브 푸시 누락을 보완.
- /// </summary>
- internal sealed class YouTubeFeedPoller(
- IHttpClientFactory httpClientFactory,
- ILogger<YouTubeFeedPoller> logger
- ) : IYouTubeFeedPoller
- {
- private const string FeedBase = "https://www.youtube.com/feeds/videos.xml?channel_id=";
- public async Task<string?> 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;
- }
- }
- }
|