fceb3707b5
The NBA/WNBA espnId was captured only from espnStatsAdapter (the offline-
Python fallback), unreliable in prod. Add espnAthleteIndex — a pure,
defensive harvester that builds { nameKey -> {espnId, headshotHref} } from
the ESPN schedule->summary/boxscore/leaders/injuries/roster feeds the
pipeline already calls (free, bounded mapLimit, cached, MLB->{}).
snapshotService now fills any player the primary stats-resolve left without
an espnId from this index, and stores a DIRECT headshotHref as headshotUrl
on the enriched grade (the exact URL, never 404s on a constructed path).
Threaded headshotUrl through slateAdapter.buildPlayerStripsFromProps ->
GameCard -> StatStrip -> PlayerAvatar/getHeadshotUrl (direct href wins over
the constructed one). MLB's MLBAM path is untouched. Soccer resolves only
via a direct href; absent -> honest monogram (API_FOOTBALL_KEY remains the
reliable soccer path, unwired).
getGameSummary now also passes through ESPN `rosters` (pre-game lineups
carry id + headshot). Everything graceful: any miss -> absent -> monogram.
Tests: tests/unit/espnHeadshotIndex.test.js (11) — fixture->index, snapshot
merge fallback, direct-href-wins, soccer honest monogram, malformed/cyclic
parse never throws. Full suite 3080 green; web next build exit 0.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
74 lines
2.8 KiB
TypeScript
74 lines
2.8 KiB
TypeScript
/**
|
|
* Player headshot URL construction (Session 19; Wave 2A refactor).
|
|
*
|
|
* The PURE URL logic lives in `playerHeadshotUrl.js` (CommonJS) so it's
|
|
* requireable by the plain-JS Jest suite AND importable here (allowJs) — same
|
|
* single-source-of-truth doctrine as `vyndrTokens.js` / `playerName.js`. This
|
|
* file adds the TypeScript surface (types + the `headshotFromPlayer` helper).
|
|
*
|
|
* Each league hosts its own CDN; we don't proxy through ESPN as the primary
|
|
* because (a) ESPN rate-limits image hotlinking and (b) the league CDNs are the
|
|
* same sources PrizePicks and Sleeper use, so coverage is closer to 100%.
|
|
*
|
|
* Fallback chain inside the resolver:
|
|
* 1. `cachedPhotoUrl` — a backend-stored URL (soccer). We DO NOT construct
|
|
* soccer URLs (no central CDN for player headshots).
|
|
* 2. League CDN with `playerId` — official source.
|
|
* 3. ESPN CDN with `espnId` — NBA/WNBA (and dormant NFL/NHL) headshots.
|
|
* 4. `/images/player-silhouette.svg` — the sentinel the consuming component
|
|
* swaps for a team-colored MONOGRAM (never a gray blob, never a broken
|
|
* image). `<img onError>` also degrades a 404 to the monogram.
|
|
*/
|
|
|
|
import {
|
|
getHeadshotUrl as coreGetHeadshotUrl,
|
|
PLAYER_SILHOUETTE as CORE_SILHOUETTE,
|
|
} from '@/lib/playerHeadshotUrl';
|
|
|
|
export type HeadshotSport = 'nba' | 'wnba' | 'mlb' | 'soccer' | 'soccer_wc' | string;
|
|
|
|
export interface HeadshotInput {
|
|
sport: HeadshotSport;
|
|
/** League-specific ID (NBA stats.com ID, WNBA player ID, MLB people ID). */
|
|
playerId?: string | number | null;
|
|
/** ESPN athlete ID — the NBA/WNBA (and dormant NFL/NHL) headshot source. */
|
|
espnId?: string | number | null;
|
|
/** Wave 2B — a RESOLVED absolute headshot URL (ESPN's exact href); wins over a constructed one. */
|
|
headshotUrl?: string | null;
|
|
/** Pre-cached photo URL (used by soccer where each league has no central CDN). */
|
|
cachedPhotoUrl?: string | null;
|
|
}
|
|
|
|
export const PLAYER_SILHOUETTE: string = CORE_SILHOUETTE;
|
|
|
|
export function getHeadshotUrl(input: HeadshotInput): string {
|
|
return coreGetHeadshotUrl(input);
|
|
}
|
|
|
|
/**
|
|
* Convenience wrapper for the common case where the caller has a player object
|
|
* with mixed ID fields. Pulls the first non-empty ID out of the union before
|
|
* delegating to `getHeadshotUrl`.
|
|
*/
|
|
export function headshotFromPlayer(player: {
|
|
sport?: string;
|
|
nba_id?: string | number | null;
|
|
wnba_id?: string | number | null;
|
|
mlb_id?: string | number | null;
|
|
espn_id?: string | number | null;
|
|
photo_url?: string | null;
|
|
}): string {
|
|
const sport = String(player.sport || '').toLowerCase();
|
|
const leagueId =
|
|
sport === 'nba' ? player.nba_id :
|
|
sport === 'wnba' ? player.wnba_id :
|
|
sport === 'mlb' ? player.mlb_id :
|
|
null;
|
|
return getHeadshotUrl({
|
|
sport,
|
|
playerId: leagueId,
|
|
espnId: player.espn_id,
|
|
cachedPhotoUrl: player.photo_url,
|
|
});
|
|
}
|