using System.Net.Http;
using System.Text.Json;
using ytLive.Models;
using AppLog = ytLive.Helpers.AppLog;
namespace ytLive.Services;
///
/// Pulls a connected channel's YPP-relevant numbers from the YouTube Data API
/// (TASK 39, slice 1 — the current OAuth scopes suffice, no re-consent):
/// subscriptions/views/videos + audit standing via channels.list, and the count
/// of public uploads in the trailing 90 days via playlistItems.list on the
/// uploads playlist. Long-form watch hours and Shorts-feed views need the
/// Analytics API (a later slice behind the IChannelStatsProvider seam).
///
public class ChannelStatsService
{
private readonly YouTubeAuthService _auth;
private readonly HttpClient _http;
private const string ApiBase = "https://www.googleapis.com/youtube/v3";
public ChannelStatsService(YouTubeAuthService auth, HttpClient? http = null)
{
_auth = auth;
_http = http ?? new HttpClient();
}
private async Task EnsureToken()
{
if (_auth.CurrentChannel == null) return false;
if (_auth.CurrentChannel.TokenExpiry <= DateTime.UtcNow.AddMinutes(5))
return await _auth.RefreshToken();
return true;
}
/// Captures the channel's current stats into a snapshot, or null when
/// there's no session, a request fails, or the API returns nothing. Never throws.
public virtual async Task CaptureCurrentAsync()
{
if (!await EnsureToken()) return null;
_http.DefaultRequestHeaders.Authorization = new("Bearer", _auth.CurrentChannel!.AccessToken);
var response = await _http.GetAsync(
$"{ApiBase}/channels?mine=true&part=statistics,auditDetails,contentDetails");
if (!response.IsSuccessStatusCode)
{
AppLog.Write($"YPP stats: channels.list failed ({(int)response.StatusCode})");
return null;
}
var json = JsonSerializer.Deserialize(await response.Content.ReadAsStringAsync());
if (!json.TryGetProperty("items", out var items) || items.GetArrayLength() == 0)
return null;
var channel = items[0];
var statistics = channel.GetProperty("statistics");
var uploadsPlaylistId = channel.TryGetProperty("contentDetails", out var contentDetails)
&& contentDetails.TryGetProperty("relatedPlaylists", out var related)
&& related.TryGetProperty("uploads", out var uploads)
? uploads.GetString()
: null;
var uploadsLast90Days = uploadsPlaylistId != null
? await CountUploadsLast90DaysAsync(uploadsPlaylistId)
: 0;
return new YppStatSnapshot
{
CapturedAtUtc = DateTime.UtcNow,
SubscriberCount = statistics.TryGetProperty("subscriberCount", out var subs) ? subs.GetInt64() : 0,
VideoCount = statistics.TryGetProperty("videoCount", out var videos) ? videos.GetInt64() : 0,
TotalViews = statistics.TryGetProperty("viewCount", out var views) ? views.GetInt64() : 0,
UploadsLast90Days = uploadsLast90Days,
CommunityGuidelinesGoodStanding = ReadStandingFlag(channel, "communityGuidelinesGoodStanding"),
CopyrightStrikesGoodStanding = ReadStandingFlag(channel, "copyrightStrikesGoodStanding"),
ContentIdClaimsGoodStanding = ReadStandingFlag(channel, "contentIdClaimsGoodStanding"),
OverallGoodStanding = ReadStandingFlag(channel, "overallGoodStanding"),
};
}
/// Counts uploads published within the trailing 90 days, scanning the
/// most recent 50 from the uploads playlist (generous beyond the 3/90-day YPP
/// requirement; the YPP tab labels it as the recent-50 window).
private async Task CountUploadsLast90DaysAsync(string uploadsPlaylistId)
{
var response = await _http.GetAsync(
$"{ApiBase}/playlistItems?part=snippet&playlistId={Uri.EscapeDataString(uploadsPlaylistId)}&maxResults=50");
if (!response.IsSuccessStatusCode)
{
AppLog.Write($"YPP stats: playlistItems.list failed ({(int)response.StatusCode})");
return 0;
}
var json = JsonSerializer.Deserialize(await response.Content.ReadAsStringAsync());
if (!json.TryGetProperty("items", out var items)) return 0;
var cutoff = DateTime.UtcNow.AddDays(-90);
var count = 0;
foreach (var item in items.EnumerateArray())
{
if (!item.TryGetProperty("snippet", out var snippet)
|| !snippet.TryGetProperty("publishedAt", out var published)
|| !DateTime.TryParse(published.GetString(), out var publishedAt))
{
continue;
}
if (publishedAt.ToUniversalTime() >= cutoff)
count++;
}
return count;
}
private static bool ReadStandingFlag(JsonElement channel, string property)
=> channel.TryGetProperty("auditDetails", out var audit)
&& audit.TryGetProperty(property, out var flag)
&& flag.GetBoolean();
}