Files
LlamaCasty/Models/YppProgress.cs
T
gramps a1d861608f feat(ypp): the three eligibility bars, with dated thresholds (TASK 39·2)
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.
2026-11-05 16:41:09 -08:00

57 lines
2.9 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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),
};
}