a9d5f29084
Current-OAuth-scope data, no re-consent: subscriber bars for both tiers (500 fan-funding / 1,000 ad-revenue; the 2027-02-01 doubling is versioned date-aware data in YppThresholds), 3-uploads/90d, live standing from channels.list auditDetails, self-reported 2FA/AdSense checkboxes + deep links (Google 2-Step / youtube.com/earn), and the "what counts" education. Every refresh appends a YppSnapshot (SQLite v10) so the analytics slice has history from day one. - ChannelStatsService (channels.list statistics,auditDetails,contentDetails + playlistItems.list, virtual CaptureCurrentAsync = test seam); YppTrackerViewModel mirrors LiveBroadcastFormViewModel. - MainViewModel.Ypp.cs: Ypp property, ChannelStatsFactoryOverride seam, one-open-at-a-time drawer exclusivity (both directions); account sign-in/out/restore hooks in Account.cs. - PreviewPane.xaml: TextPullOutHost is now a drawer bank [broadcast][ypp][stacked white tabs]; outside-click collapse covers both drawers. MainWindow.xaml.cs updated. - Wait - watchers: the whole feature already reviewed against YouTube's own docs (YPP Earn tab + API availability) before building — see TASKS/task-39-ypp-journey-tracker.md. - ONE integration test: YppPullOutTests (snapshot capture/persist + checklist round-trip + drawer exclusivity). 313/313 pass, 0 warnings. - Memory in same commit: TASKS.md row, ai.md Journey-tracker bullet (roadmap→reality), HANDOFF.md. Slice 2 (Analytics yt-analytics.readonly re-consent → watch-hours/Shorts + velocity/ETA + sparkline) is queued behind this, not merged.
90 lines
4.2 KiB
C#
90 lines
4.2 KiB
C#
using Microsoft.Data.Sqlite;
|
|
using ytLive.Models;
|
|
|
|
namespace ytLive.Services;
|
|
|
|
public partial class LayoutStore : IDisposable
|
|
{
|
|
// ─── YPP journey-tracker snapshots (TASK 39) ───
|
|
|
|
/// <summary>Appends one YPP stats snapshot (append-only history; velocity/ETA
|
|
/// read it in a later analytics slice).</summary>
|
|
public void SaveYppSnapshot(YppStatSnapshot snapshot)
|
|
{
|
|
using var cmd = _connection.CreateCommand();
|
|
cmd.CommandText = """
|
|
INSERT INTO YppSnapshot (CapturedAtUtc, SubscriberCount, VideoCount, TotalViews,
|
|
UploadsLast90Days, CommunityGuidelinesGoodStanding,
|
|
CopyrightStrikesGoodStanding, ContentIdClaimsGoodStanding,
|
|
OverallGoodStanding)
|
|
VALUES ($captured, $subs, $videos, $views, $uploads, $cg, $cs, $cic, $overall);
|
|
""";
|
|
cmd.Parameters.AddWithValue("$captured", snapshot.CapturedAtUtc.ToString("o"));
|
|
cmd.Parameters.AddWithValue("$subs", snapshot.SubscriberCount);
|
|
cmd.Parameters.AddWithValue("$videos", snapshot.VideoCount);
|
|
cmd.Parameters.AddWithValue("$views", snapshot.TotalViews);
|
|
cmd.Parameters.AddWithValue("$uploads", snapshot.UploadsLast90Days);
|
|
cmd.Parameters.AddWithValue("$cg", snapshot.CommunityGuidelinesGoodStanding ? 1 : 0);
|
|
cmd.Parameters.AddWithValue("$cs", snapshot.CopyrightStrikesGoodStanding ? 1 : 0);
|
|
cmd.Parameters.AddWithValue("$cic", snapshot.ContentIdClaimsGoodStanding ? 1 : 0);
|
|
cmd.Parameters.AddWithValue("$overall", snapshot.OverallGoodStanding ? 1 : 0);
|
|
cmd.ExecuteNonQuery();
|
|
}
|
|
|
|
/// <summary>Most recent snapshots, newest first (defaults to the full history).</summary>
|
|
public List<YppStatSnapshot> LoadYppSnapshots(int limit = 365)
|
|
{
|
|
var result = new List<YppStatSnapshot>();
|
|
using var cmd = _connection.CreateCommand();
|
|
cmd.CommandText = """
|
|
SELECT CapturedAtUtc, SubscriberCount, VideoCount, TotalViews, UploadsLast90Days,
|
|
CommunityGuidelinesGoodStanding, CopyrightStrikesGoodStanding,
|
|
ContentIdClaimsGoodStanding, OverallGoodStanding
|
|
FROM YppSnapshot ORDER BY CapturedAtUtc DESC LIMIT $limit;
|
|
""";
|
|
cmd.Parameters.AddWithValue("$limit", limit);
|
|
using var reader = cmd.ExecuteReader();
|
|
while (reader.Read())
|
|
{
|
|
result.Add(new YppStatSnapshot
|
|
{
|
|
CapturedAtUtc = DateTime.TryParse(reader.GetString(0), out var captured) ? captured : DateTime.MinValue,
|
|
SubscriberCount = reader.GetInt64(1),
|
|
VideoCount = reader.GetInt64(2),
|
|
TotalViews = reader.GetInt64(3),
|
|
UploadsLast90Days = reader.GetInt32(4),
|
|
CommunityGuidelinesGoodStanding = reader.GetInt32(5) != 0,
|
|
CopyrightStrikesGoodStanding = reader.GetInt32(6) != 0,
|
|
ContentIdClaimsGoodStanding = reader.GetInt32(7) != 0,
|
|
OverallGoodStanding = reader.GetInt32(8) != 0,
|
|
});
|
|
}
|
|
return result;
|
|
}
|
|
|
|
/// <summary>The manually self-reported YPP checklist state (2FA / AdSense are
|
|
/// not readable from any YouTube API — the creator ticks them in-app).</summary>
|
|
public (bool TwoFactorEnabled, bool AdSenseLinked) LoadYppChecklist()
|
|
{
|
|
return (GetSetting("Ypp.TwoFactorEnabled") == "1", GetSetting("Ypp.AdSenseLinked") == "1");
|
|
}
|
|
|
|
public void SaveYppChecklist(bool twoFactorEnabled, bool adsenseLinked)
|
|
{
|
|
UpsertSetting("Ypp.TwoFactorEnabled", twoFactorEnabled ? "1" : "0");
|
|
UpsertSetting("Ypp.AdSenseLinked", adsenseLinked ? "1" : "0");
|
|
}
|
|
|
|
/// <summary>UTC timestamp of the last successful stats refresh (used to gate
|
|
/// the once-daily auto-refresh against the analytics quota-thin API).</summary>
|
|
public DateTime? LoadYppLastRefreshedAtUtc()
|
|
{
|
|
var value = GetSetting("Ypp.LastRefreshedAtUtc");
|
|
return DateTime.TryParse(value, out var dt) ? dt : null;
|
|
}
|
|
|
|
public void SaveYppLastRefreshedAtUtc(DateTime utc)
|
|
{
|
|
UpsertSetting("Ypp.LastRefreshedAtUtc", utc.ToString("o"));
|
|
}
|
|
} |