YouTube moves the goalposts, so the app stops modelling them as its own truth and shows progress against a threshold table that carries the date it was read. - `Models/YppProgress.cs` — pure: three metrics, per-criterion met, and `MeetsPublishedRule`, which encodes the real shape of the rule (subscribers AND either view route). The drawer says "x of 3 criteria met — YouTube reviews eligibility" and spells out the either/or, so "2 of 3" can't be misread. - `Services/YouTubeAnalyticsService.cs` — the two numbers only Analytics publishes: long-form watch hours (trailing 12 months, `creatorContentType==video`) and Shorts-feed views (90 days, `==shorts`). No rows means NO DATA, not zero. - **Thresholds are data with a publication date** (`YppThresholds.AsOfUtc`, shown in the drawer) and a test fails once that reading is more than 180 days old. The 2027-02-01 doubling (4,000h → 8,000h, 10M → 20M Shorts views) resolves from the date, not from an edit. - **No Analytics scope → no invented numbers:** the drawer explains the missing consent instead of drawing two empty bars as zero. Money stays behind 3·20's "Show monetary metrics" toggle. - Fan-funding tier (500 subs) demoted to a single muted line, since the three bars are the ad-revenue path. Tests: `YppProgressTests` — either/or eligibility, subscribers alone insufficient, Shorts as a qualifying route, bars clamp at 100%, the doubling date, the staleness guard, Analytics parsing (no rows = no data), and compact value formatting. FAST gate: 567 passed, 0 failed, 0 warnings.
35 lines
2.0 KiB
C#
35 lines
2.0 KiB
C#
namespace ytLive.Models;
|
|
|
|
/// <summary>YPP eligibility thresholds as versioned, date-aware DATA — never
|
|
/// constants (ai.md "Journey tracker"). Tier 1 (fan funding) is time-invariant;
|
|
/// Tier 2 (ad revenue) doubles its long-form-hours and Shorts-view bars for NEW
|
|
/// applicants after 2027-02-01. Hardcoding the pre-2027 numbers would show the
|
|
/// wrong ETA once the doubling lands, so callers resolve the active Tier 2
|
|
/// threshold through <see cref="Tier2For"/>.</summary>
|
|
public static class YppThresholds
|
|
{
|
|
public sealed record TierThreshold(string Name, long Subscribers, long WatchHours, long ShortsViews);
|
|
|
|
/// <summary>The date the Tier-2 ad-revenue bars double for new applicants.</summary>
|
|
public static readonly DateTime Tier2DoublingDate = new(2027, 2, 1);
|
|
|
|
/// <summary>When these thresholds were last read from YouTube's published requirements. Shown in the
|
|
/// drawer and asserted by a test, because YouTube moves the bar and a stale bar must not look current.</summary>
|
|
public static readonly DateTime AsOfUtc = new(2026, 11, 5);
|
|
|
|
/// <summary>Fan-funding tier (before the full ad/YouTube Premium revenue tier).</summary>
|
|
public static TierThreshold Tier1 { get; } = new("Fan funding", 500, 3_000, 3_000_000);
|
|
|
|
/// <summary>The Tier-2 bar for a given point in time — 4,000h / 10M Shorts
|
|
/// before the doubling date, 8,000h / 20M Shorts on or after it.</summary>
|
|
public static TierThreshold Tier2For(DateTime when) => when >= Tier2DoublingDate
|
|
? new TierThreshold("Ad revenue", 1_000, 8_000, 20_000_000)
|
|
: new TierThreshold("Ad revenue", 1_000, 4_000, 10_000_000);
|
|
|
|
/// <summary>The currently-active Tier-2 threshold (resolved against today).</summary>
|
|
public static TierThreshold Tier2Now => Tier2For(DateTime.UtcNow);
|
|
|
|
/// <summary>Valid public long-form uploads required in the trailing 90 days
|
|
/// to keep each tier route eligible.</summary>
|
|
public const int UploadsRequiredPer90Days = 3;
|
|
} |