Add game center, per-model shuffle, audio focus fixes, README, tests

- README.md with build/architecture overview
- Game Center screen with at-bat timeline, pitch sequence, spray chart,
  and strike zone component views
- VideoShuffle service: per-model bucketed random selection with
  no-back-to-back guarantee; replaces flat shuffle-bag approach
- Refresh JWT token for authenticated NSFW feed; add josie-hamming-2
  and dani-speegle-2 to the user list
- MultiStreamView audio focus: remove redundant isMuted writes during
  startStream and playNextWerkoutClip so audio stops ducking during
  clip transitions; gate AVAudioSession.setCategory(.playback) behind
  a one-shot flag
- GamesViewModel.attachPlayer: skip mute recalculation when the same
  player is re-attached (prevents toggle flicker on item replace)
- mlbTVOSTests target wired through project.yml with
  GENERATE_INFOPLIST_FILE; VideoShuffleTests covers groupByModel,
  pickRandomFromBuckets, real-distribution no-back-to-back invariant,
  and uniform model distribution over 6000 picks

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Trey t
2026-04-11 11:02:46 -05:00
parent 58e4c36963
commit 88308b46f5
17 changed files with 2069 additions and 313 deletions

View File

@@ -42,6 +42,10 @@ actor MLBStatsAPI {
try await fetchJSON("\(baseURL).1/game/\(gamePk)/feed/live")
}
func fetchWinProbability(gamePk: String) async throws -> [WinProbabilityEntry] {
try await fetchJSON("\(baseURL).1/game/\(gamePk)/winProbability")
}
func fetchLeagueTeams(season: String) async throws -> [LeagueTeamSummary] {
let response: TeamsResponse = try await fetchJSON(
"\(baseURL)/teams?sportId=1&season=\(season)&hydrate=league,division,venue,record"
@@ -828,6 +832,19 @@ struct LiveGameFeed: Codable, Sendable {
var homeLineup: [LiveFeedBoxscorePlayer] {
liveData.boxscore?.teams?.home.lineupPlayers ?? []
}
var currentAtBatPitches: [LiveFeedPlayEvent] {
currentPlay?.pitches ?? []
}
var allGameHits: [(play: LiveFeedPlay, hitData: LiveFeedHitData)] {
liveData.plays.allPlays.compactMap { play in
guard let lastEvent = play.playEvents?.last,
let hitData = lastEvent.hitData,
hitData.coordinates?.coordX != nil else { return nil }
return (play: play, hitData: hitData)
}
}
}
struct LiveFeedMetaData: Codable, Sendable {
@@ -890,7 +907,7 @@ struct LiveFeedVenue: Codable, Sendable {
struct LiveFeedWeather: Codable, Sendable {
let condition: String?
let temp: Int?
let temp: String?
let wind: String?
}
@@ -911,6 +928,11 @@ struct LiveFeedPlay: Codable, Sendable, Identifiable {
let about: LiveFeedPlayAbout?
let count: LiveFeedPlayCount?
let matchup: LiveFeedPlayMatchup?
let playEvents: [LiveFeedPlayEvent]?
var pitches: [LiveFeedPlayEvent] {
playEvents?.filter { $0.isPitch == true } ?? []
}
var id: String {
let inning = about?.inning ?? -1
@@ -958,6 +980,11 @@ struct LiveFeedPlayerReference: Codable, Sendable, Identifiable {
var displayName: String {
fullName ?? "TBD"
}
var headshotURL: URL? {
guard let id else { return nil }
return URL(string: "https://img.mlbstatic.com/mlb-photos/image/upload/w_213,q_auto:best/v1/people/\(id)/headshot/67/current")
}
}
struct LiveFeedLinescore: Codable, Sendable {
@@ -1054,3 +1081,87 @@ struct LiveFeedDecisions: Codable, Sendable {
let loser: LiveFeedPlayerReference?
let save: LiveFeedPlayerReference?
}
// MARK: - Play Event / Pitch Data Models
struct LiveFeedPlayEvent: Codable, Sendable, Identifiable {
let details: LiveFeedPlayEventDetails?
let count: LiveFeedPlayCount?
let pitchData: LiveFeedPitchData?
let hitData: LiveFeedHitData?
let isPitch: Bool?
let pitchNumber: Int?
let index: Int?
var id: Int { index ?? pitchNumber ?? 0 }
var callCode: String {
details?.call?.code ?? ""
}
var callDescription: String {
details?.call?.description ?? details?.description ?? ""
}
var pitchTypeDescription: String {
details?.type?.description ?? "Unknown"
}
var pitchTypeCode: String {
details?.type?.code ?? ""
}
var speedMPH: Double? {
pitchData?.startSpeed
}
}
struct LiveFeedPlayEventDetails: Codable, Sendable {
let call: LiveFeedCallInfo?
let description: String?
let type: LiveFeedPitchType?
}
struct LiveFeedCallInfo: Codable, Sendable {
let code: String?
let description: String?
}
struct LiveFeedPitchType: Codable, Sendable {
let code: String?
let description: String?
}
struct LiveFeedPitchData: Codable, Sendable {
let startSpeed: Double?
let endSpeed: Double?
let strikeZoneTop: Double?
let strikeZoneBottom: Double?
let coordinates: LiveFeedPitchCoordinates?
let zone: Int?
}
struct LiveFeedPitchCoordinates: Codable, Sendable {
let pX: Double?
let pZ: Double?
}
struct LiveFeedHitData: Codable, Sendable {
let launchSpeed: Double?
let launchAngle: Double?
let totalDistance: Double?
let coordinates: LiveFeedHitCoordinates?
}
struct LiveFeedHitCoordinates: Codable, Sendable {
let coordX: Double?
let coordY: Double?
}
// MARK: - Win Probability
struct WinProbabilityEntry: Codable, Sendable {
let atBatIndex: Int?
let homeTeamWinProbability: Double?
let awayTeamWinProbability: Double?
}