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.
57 lines
2.9 KiB
C#
57 lines
2.9 KiB
C#
using System;
|
||
using System.Globalization;
|
||
|
||
namespace ytLive.Models;
|
||
|
||
/// <summary>
|
||
/// TASK 39·2: the three metrics that count towards YouTube Partner Program eligibility, as progress
|
||
/// towards the ad-revenue thresholds.
|
||
/// <para><b>YouTube decides.</b> This only reports what the published numbers say; it never claims
|
||
/// eligibility, and it cannot see strikes, AdSense state, 2FA or the review decision.</para>
|
||
/// <para><b>The view criteria are either/or</b> — 4,000 public watch hours <i>or</i> 10M Shorts views —
|
||
/// so "2 of 3" is reported with that qualifier rather than implying all three must pass.</para>
|
||
/// </summary>
|
||
public sealed record YppProgress(
|
||
long Subscribers,
|
||
long SubscriberTarget,
|
||
long WatchHours,
|
||
long WatchHoursTarget,
|
||
long ShortsViews,
|
||
long ShortsViewsTarget)
|
||
{
|
||
public bool SubscribersMet => Subscribers >= SubscriberTarget;
|
||
public bool WatchHoursMet => WatchHours >= WatchHoursTarget;
|
||
public bool ShortsViewsMet => ShortsViews >= ShortsViewsTarget;
|
||
|
||
/// <summary>Criteria currently met (0–3). Two of three can mean eligible: subs plus EITHER view route.</summary>
|
||
public int CriteriaMet => (SubscribersMet ? 1 : 0) + (WatchHoursMet ? 1 : 0) + (ShortsViewsMet ? 1 : 0);
|
||
|
||
/// <summary>True when the subscriber bar AND at least one view route are satisfied — the shape of the
|
||
/// published rule. Still not a qualification claim: YouTube reviews it.</summary>
|
||
public bool MeetsPublishedRule => SubscribersMet && (WatchHoursMet || ShortsViewsMet);
|
||
|
||
public double SubscribersFraction => Fraction(Subscribers, SubscriberTarget);
|
||
public double WatchHoursFraction => Fraction(WatchHours, WatchHoursTarget);
|
||
public double ShortsViewsFraction => Fraction(ShortsViews, ShortsViewsTarget);
|
||
|
||
public string SubscribersText => $"{Compact(Subscribers)} / {Compact(SubscriberTarget)}";
|
||
public string WatchHoursText => $"{Compact(WatchHours)} / {Compact(WatchHoursTarget)} h";
|
||
public string ShortsViewsText => $"{Compact(ShortsViews)} / {Compact(ShortsViewsTarget)}";
|
||
|
||
public string CriteriaMetText => CriteriaMet == 1
|
||
? "1 of 3 criteria met — YouTube reviews eligibility."
|
||
: $"{CriteriaMet} of 3 criteria met — YouTube reviews eligibility.";
|
||
|
||
private static double Fraction(long value, long target)
|
||
=> target <= 0 ? 0 : Math.Clamp((double)value / target, 0, 1);
|
||
|
||
/// <summary>1,234 → "1,234"; 12,345 → "12.3k"; 1,234,567 → "1.23M". Mono-face friendly and stable
|
||
/// in width, so the three bars do not jitter as values move.</summary>
|
||
internal static string Compact(long value) => value switch
|
||
{
|
||
>= 1_000_000 => (value / 1_000_000d).ToString("0.##", CultureInfo.InvariantCulture) + "M",
|
||
>= 10_000 => (value / 1_000d).ToString("0.#", CultureInfo.InvariantCulture) + "k",
|
||
_ => value.ToString("N0", CultureInfo.InvariantCulture),
|
||
};
|
||
}
|