@page @model Admin.Pages.Channel.YouTubePubSub.IndexModel @{ ViewData["Title"] = "YouTube PubSub 구독"; // 채널별 건강 정보 lookup (YouTubeChannelID → ChannelHealthItem) var healthByChannelId = Model.Health.Channels .GroupBy(h => h.YouTubeChannelID) .ToDictionary(g => g.Key, g => g.First()); string FormatAgo(DateTime? utc) { if (utc is null) return "-"; var diff = DateTime.UtcNow - utc.Value; if (diff < TimeSpan.Zero) return utc.Value.ToString("yyyy-MM-dd HH:mm:ss") + " UTC"; if (diff.TotalMinutes < 1) return $"{(int)diff.TotalSeconds}초 전"; if (diff.TotalHours < 1) return $"{(int)diff.TotalMinutes}분 전"; if (diff.TotalDays < 1) return $"{(int)diff.TotalHours}시간 전"; return $"{(int)diff.TotalDays}일 전"; } string FormatUntil(DateTime? utc) { if (utc is null) return "-"; var diff = utc.Value - DateTime.UtcNow; if (diff < TimeSpan.Zero) return "만료됨"; if (diff.TotalMinutes < 60) return $"{(int)diff.TotalMinutes}분 후"; if (diff.TotalHours < 24) return $"{(int)diff.TotalHours}시간 후"; return $"{(int)diff.TotalDays}일 후"; } }

@ViewData["Title"]



연결 상태
API Key
@if (Model.Health.ApiKeyConfigured) {
설정됨 @Model.Health.ApiKeyMaskedTail
} else {
없음
}
HMAC Secret
@if (Model.Health.HmacSecretConfigured) {
설정됨 @Model.Health.HmacSecretMaskedTail
} else {
없음
}
Feed 폴링 주기
@Model.Health.FeedPollingIntervalMinutes
라이브 채팅 수집기
@Model.Health.ActiveLiveChatCollectors 개 활성
활성 내부 채널
@Model.Health.ActiveInternalChannelsCount
PubSub 구독 채널
@Model.Health.SubscribedChannelsCount
현재 라이브
@Model.Health.ActiveLiveVideosCount
수동 API 키 테스트
30초 쿨다운

현재 감지된 라이브 @Model.LiveStatus.Items.Count @if (Model.LiveStatus.Items.Count == 0) {
현재 감지된 라이브 방송이 없습니다.
} else {
@foreach (var live in Model.LiveStatus.Items) { }
채널명 Video ID 제목 시작 시각(UTC)
@(live.ChannelName ?? "(미확인)") @live.VideoID @live.Title @(live.ActualStartTime?.ToString("yyyy-MM-dd HH:mm:ss") ?? "-")
}

수동 방송 감지 (긴급 복구용 - quota 사용)
현재 라이브 중인데 PubSub/Feed 폴링으로 감지되지 않았을 때 수동 트리거합니다. PubSubNotify.Command 파이프라인 그대로 실행 → videos.list 1 unit → actualStartTime 있으면 BroadcastSession 생성 + LiveChat 시작.
YouTube 라이브 URL의 watch?v= 뒤 문자열
UC로 시작하는 YouTube 채널 ID

강제 방송 감지 (quota 초과 시 긴급 복구용)
quota 소진 등으로 YouTube API 호출이 불가능할 때 사용. YouTube API 호출 없이 Redis 라이브 상태 + DB BroadcastSession + SignalR ReceiveChannelStatus 만 처리합니다. 제목 등 메타데이터는 검증 안 되므로 운영자 책임 하에 정확히 입력해야 합니다.
라이브 종료 시 강제 종료 마킹도 같이 사용해 주세요 (자동 정리 안 됨).
YouTube 라이브 URL의 watch?v= 뒤 문자열

라이브 강제 종료 마킹

Callback URL
@Model.Data.CallbackUrl
YouTube PubSub 허브가 알림을 전송하는 엔드포인트. 공개 HTTPS 여야 하며 appsettingsYouTube:CallbackUrl로 설정합니다.

YouTube 연동 채널 구독 @Model.Data.SubscribedCount 전체 @Model.Data.TotalCount @if (Model.Data.TotalCount == 0) {
표시할 채널이 없습니다.
} else {
@foreach (var item in Model.Data.Items) { @{ healthByChannelId.TryGetValue(item.YouTubeChannelID, out var h); } }
썸네일 채널명 YouTube Channel ID 채널 상태 구독 마지막 폴링 구독 만료 마지막 비디오 작업
@if (!string.IsNullOrEmpty(item.ThumbnailUrl)) { } else { } @if (item.HasInternalChannel) {
@item.ChannelName @if (!string.IsNullOrEmpty(item.Handle)) { (@@@item.Handle) }
} else { (내부 채널 없음 · 고아 구독) }
@item.YouTubeChannelID @if (!item.HasInternalChannel) { 고아 } else if (item.IsActive) { 활성 } else { 비활성 } @if (item.IsSubscribed) { 구독됨 } else { 미구독 } @FormatAgo(h?.LastPolledAtUtc) @{ var leaseLabel = FormatUntil(h?.LeaseExpiresAtUtc); var leaseClass = h?.LeaseExpiresAtUtc is null ? "text-muted" : (h.LeaseExpiresAtUtc.Value - DateTime.UtcNow).TotalDays < 2 ? "text-danger" : ""; } @leaseLabel @if (!string.IsNullOrEmpty(h?.LastProcessedVideoId)) { @h.LastProcessedVideoId } else { - }
@if (!item.IsSubscribed) {
} else {
}
}