commit cc44f961275be0902576b79f8736e2345c259aaa Author: amantahiliani Date: Tue Mar 3 00:20:09 2026 -0500 Open F1 API calls diff --git a/cmd/main.go b/cmd/main.go new file mode 100644 index 0000000..1d619dd --- /dev/null +++ b/cmd/main.go @@ -0,0 +1 @@ +package cmd diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..3d30f2a --- /dev/null +++ b/go.mod @@ -0,0 +1,3 @@ +module github.com/AmanTahiliani/box-box + +go 1.25.6 diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..e69de29 diff --git a/internal/api/client.go b/internal/api/client.go new file mode 100644 index 0000000..96ab0ff --- /dev/null +++ b/internal/api/client.go @@ -0,0 +1,18 @@ +package api + +import ( + "net/http" + "time" +) + +type OpenF1Client struct { + url string + httpClient *http.Client +} + +func NewOpenF1Client(url string, timeout time.Duration) *OpenF1Client { + return &OpenF1Client{ + url: url, + httpClient: &http.Client{Timeout: timeout}, + } +} diff --git a/internal/api/openf1.go b/internal/api/openf1.go new file mode 100644 index 0000000..dfb4ea9 --- /dev/null +++ b/internal/api/openf1.go @@ -0,0 +1,343 @@ +package api + +import ( + "encoding/json" + "errors" + "fmt" + "io" + "net/http" + "strconv" + + "github.com/AmanTahiliani/box-box/internal/models" +) + +// get performs a GET request and returns the response body, or an error if the +// status code is not 200 OK. +func (c *OpenF1Client) get(url string) (io.ReadCloser, error) { + resp, err := c.httpClient.Get(url) + if err != nil { + return nil, err + } + if resp.StatusCode != http.StatusOK { + resp.Body.Close() + return nil, fmt.Errorf("openf1 API returned status %d for %s", resp.StatusCode, url) + } + return resp.Body, nil +} + +func (c *OpenF1Client) GetMeetingsForYear(year int) ([]models.Meeting, error) { + if year < 2023 { + return nil, errors.New("Invalid year: " + strconv.Itoa(year) + ". Year must be 2023 or later.") + } + body, err := c.get(fmt.Sprintf("%s/v1/meetings?year=%d", c.url, year)) + if err != nil { + return nil, err + } + defer body.Close() + + var result []models.Meeting + if err := json.NewDecoder(body).Decode(&result); err != nil { + return nil, err + } + return result, nil +} + +func (c *OpenF1Client) GetSessionsForMeeting(meetingKey int) ([]models.Session, error) { + body, err := c.get(fmt.Sprintf("%s/v1/sessions?meeting_key=%d", c.url, meetingKey)) + if err != nil { + return nil, err + } + defer body.Close() + + var result []models.Session + if err := json.NewDecoder(body).Decode(&result); err != nil { + return nil, err + } + return result, nil +} + +func (c *OpenF1Client) GetDriversForSession(sessionKey int) ([]models.Driver, error) { + body, err := c.get(fmt.Sprintf("%s/v1/drivers?session_key=%d", c.url, sessionKey)) + if err != nil { + return nil, err + } + defer body.Close() + + var result []models.Driver + if err := json.NewDecoder(body).Decode(&result); err != nil { + return nil, err + } + return result, nil +} + +func (c *OpenF1Client) GetDriver(sessionKey, driverNumber int) (*models.Driver, error) { + body, err := c.get(fmt.Sprintf("%s/v1/drivers?session_key=%d&driver_number=%d", c.url, sessionKey, driverNumber)) + if err != nil { + return nil, err + } + defer body.Close() + + var result []models.Driver + if err := json.NewDecoder(body).Decode(&result); err != nil { + return nil, err + } + if len(result) == 0 { + return nil, nil + } + return &result[0], nil +} + +func (c *OpenF1Client) GetDriverChampionship(sessionKey int) ([]models.ChampionshipDriver, error) { + body, err := c.get(fmt.Sprintf("%s/v1/championship_drivers?session_key=%d", c.url, sessionKey)) + if err != nil { + return nil, err + } + defer body.Close() + + var result []models.ChampionshipDriver + if err := json.NewDecoder(body).Decode(&result); err != nil { + return nil, err + } + return result, nil +} + +func (c *OpenF1Client) GetTeamChampionship(sessionKey int) ([]models.ChampionshipTeam, error) { + body, err := c.get(fmt.Sprintf("%s/v1/championship_teams?session_key=%d", c.url, sessionKey)) + if err != nil { + return nil, err + } + defer body.Close() + + var result []models.ChampionshipTeam + if err := json.NewDecoder(body).Decode(&result); err != nil { + return nil, err + } + return result, nil +} + +// GetLatestDriverChampionship returns championship standings for the most recent +// race session. Note: requires the caller to supply a recent race session_key; +// the OpenF1 API does not support a "latest" shortcut for this endpoint. +func (c *OpenF1Client) GetLatestDriverChampionship() ([]models.ChampionshipDriver, error) { + body, err := c.get(fmt.Sprintf("%s/v1/championship_drivers?session_key=latest", c.url)) + if err != nil { + return nil, err + } + defer body.Close() + + var result []models.ChampionshipDriver + if err := json.NewDecoder(body).Decode(&result); err != nil { + return nil, err + } + return result, nil +} + +func (c *OpenF1Client) GetLatestTeamChampionship() ([]models.ChampionshipTeam, error) { + body, err := c.get(fmt.Sprintf("%s/v1/championship_teams?session_key=latest", c.url)) + if err != nil { + return nil, err + } + defer body.Close() + + var result []models.ChampionshipTeam + if err := json.NewDecoder(body).Decode(&result); err != nil { + return nil, err + } + return result, nil +} + +func (c *OpenF1Client) GetSessionResult(sessionKey int) ([]models.SessionResult, error) { + body, err := c.get(fmt.Sprintf("%s/v1/session_result?session_key=%d", c.url, sessionKey)) + if err != nil { + return nil, err + } + defer body.Close() + + var result []models.SessionResult + if err := json.NewDecoder(body).Decode(&result); err != nil { + return nil, err + } + return result, nil +} + +func (c *OpenF1Client) GetStartingGrid(sessionKey int) ([]models.StartingGrid, error) { + body, err := c.get(fmt.Sprintf("%s/v1/starting_grid?session_key=%d", c.url, sessionKey)) + if err != nil { + return nil, err + } + defer body.Close() + + var result []models.StartingGrid + if err := json.NewDecoder(body).Decode(&result); err != nil { + return nil, err + } + return result, nil +} + +func (c *OpenF1Client) GetLapsForSession(sessionKey int) ([]models.Lap, error) { + body, err := c.get(fmt.Sprintf("%s/v1/laps?session_key=%d", c.url, sessionKey)) + if err != nil { + return nil, err + } + defer body.Close() + + var result []models.Lap + if err := json.NewDecoder(body).Decode(&result); err != nil { + return nil, err + } + return result, nil +} + +func (c *OpenF1Client) GetLapsForDriver(sessionKey, driverNumber int) ([]models.Lap, error) { + body, err := c.get(fmt.Sprintf("%s/v1/laps?session_key=%d&driver_number=%d", c.url, sessionKey, driverNumber)) + if err != nil { + return nil, err + } + defer body.Close() + + var result []models.Lap + if err := json.NewDecoder(body).Decode(&result); err != nil { + return nil, err + } + return result, nil +} + +func (c *OpenF1Client) GetStintsForSession(sessionKey int) ([]models.Stint, error) { + body, err := c.get(fmt.Sprintf("%s/v1/stints?session_key=%d", c.url, sessionKey)) + if err != nil { + return nil, err + } + defer body.Close() + + var result []models.Stint + if err := json.NewDecoder(body).Decode(&result); err != nil { + return nil, err + } + return result, nil +} + +func (c *OpenF1Client) GetPitStopsForSession(sessionKey int) ([]models.Pit, error) { + body, err := c.get(fmt.Sprintf("%s/v1/pit?session_key=%d", c.url, sessionKey)) + if err != nil { + return nil, err + } + defer body.Close() + + var result []models.Pit + if err := json.NewDecoder(body).Decode(&result); err != nil { + return nil, err + } + return result, nil +} + +func (c *OpenF1Client) GetOvertakesForSession(sessionKey int) ([]models.Overtake, error) { + body, err := c.get(fmt.Sprintf("%s/v1/overtakes?session_key=%d", c.url, sessionKey)) + if err != nil { + return nil, err + } + defer body.Close() + + var result []models.Overtake + if err := json.NewDecoder(body).Decode(&result); err != nil { + return nil, err + } + return result, nil +} + +func (c *OpenF1Client) GetPositions(sessionKey, driverNumber int) ([]models.Position, error) { + body, err := c.get(fmt.Sprintf("%s/v1/position?session_key=%d&driver_number=%d", c.url, sessionKey, driverNumber)) + if err != nil { + return nil, err + } + defer body.Close() + + var result []models.Position + if err := json.NewDecoder(body).Decode(&result); err != nil { + return nil, err + } + return result, nil +} + +func (c *OpenF1Client) GetIntervals(sessionKey, driverNumber int) ([]models.Interval, error) { + body, err := c.get(fmt.Sprintf("%s/v1/intervals?session_key=%d&driver_number=%d", c.url, sessionKey, driverNumber)) + if err != nil { + return nil, err + } + defer body.Close() + + var result []models.Interval + if err := json.NewDecoder(body).Decode(&result); err != nil { + return nil, err + } + return result, nil +} + +func (c *OpenF1Client) GetRaceControl(sessionKey int) ([]models.RaceControl, error) { + body, err := c.get(fmt.Sprintf("%s/v1/race_control?session_key=%d", c.url, sessionKey)) + if err != nil { + return nil, err + } + defer body.Close() + + var result []models.RaceControl + if err := json.NewDecoder(body).Decode(&result); err != nil { + return nil, err + } + return result, nil +} + +func (c *OpenF1Client) GetWeather(sessionKey int) ([]models.Weather, error) { + body, err := c.get(fmt.Sprintf("%s/v1/weather?session_key=%d", c.url, sessionKey)) + if err != nil { + return nil, err + } + defer body.Close() + + var result []models.Weather + if err := json.NewDecoder(body).Decode(&result); err != nil { + return nil, err + } + return result, nil +} + +func (c *OpenF1Client) GetCarData(sessionKey, driverNumber int) ([]models.CarData, error) { + body, err := c.get(fmt.Sprintf("%s/v1/car_data?session_key=%d&driver_number=%d", c.url, sessionKey, driverNumber)) + if err != nil { + return nil, err + } + defer body.Close() + + var result []models.CarData + if err := json.NewDecoder(body).Decode(&result); err != nil { + return nil, err + } + return result, nil +} + +func (c *OpenF1Client) GetLocation(sessionKey, driverNumber int) ([]models.Location, error) { + body, err := c.get(fmt.Sprintf("%s/v1/location?session_key=%d&driver_number=%d", c.url, sessionKey, driverNumber)) + if err != nil { + return nil, err + } + defer body.Close() + + var result []models.Location + if err := json.NewDecoder(body).Decode(&result); err != nil { + return nil, err + } + return result, nil +} + +func (c *OpenF1Client) GetTeamRadio(sessionKey, driverNumber int) ([]models.TeamRadio, error) { + body, err := c.get(fmt.Sprintf("%s/v1/team_radio?session_key=%d&driver_number=%d", c.url, sessionKey, driverNumber)) + if err != nil { + return nil, err + } + defer body.Close() + + var result []models.TeamRadio + if err := json.NewDecoder(body).Decode(&result); err != nil { + return nil, err + } + return result, nil +} diff --git a/internal/api/openf1_test.go b/internal/api/openf1_test.go new file mode 100644 index 0000000..e3ff630 --- /dev/null +++ b/internal/api/openf1_test.go @@ -0,0 +1,211 @@ +package api + +import ( + "strings" + "testing" + "time" +) + +const ( + testBaseURL = "https://api.openf1.org" + testTimeout = 10 * time.Second + abuDhabiRace = 9662 // 2024 Abu Dhabi Grand Prix Race session +) + +func newTestClient() *OpenF1Client { + return NewOpenF1Client(testBaseURL, testTimeout) +} + +// skipOnRateLimit skips the test if err indicates the API rate-limited us (HTTP 429). +func skipOnRateLimit(t *testing.T, err error) { + t.Helper() + if err != nil && strings.Contains(err.Error(), "status 429") { + t.Skip("skipping: OpenF1 API rate-limited this test run") + } +} + +func TestGetMeetingsForYear(t *testing.T) { + client := newTestClient() + + meetings, err := client.GetMeetingsForYear(2024) + skipOnRateLimit(t, err) + if err != nil { + t.Fatal(err) + } + + if len(meetings) == 0 { + t.Errorf("Expected meetings for 2024, got none") + } + + for _, meeting := range meetings { + if meeting.Year != 2024 { + t.Errorf("Expected meeting year to be 2024, got %d", meeting.Year) + } + } +} + +func TestGetMeetingsForYearInvalid(t *testing.T) { + client := newTestClient() + + _, err := client.GetMeetingsForYear(-1) + if err == nil { + t.Errorf("Expected error for invalid year, got none") + } +} + +func TestGetSessionsForMeeting(t *testing.T) { + client := newTestClient() + + sessions, err := client.GetSessionsForMeeting(1304) + skipOnRateLimit(t, err) + if err != nil { + t.Fatal(err) + } + + if len(sessions) == 0 { + t.Errorf("Expected sessions for meeting key 1304, got none") + } + + for _, session := range sessions { + if session.MeetingKey != 1304 { + t.Errorf("Expected session meeting key to be 1304, got %d", session.MeetingKey) + } + } +} + +func TestGetDriversForSession(t *testing.T) { + client := newTestClient() + + drivers, err := client.GetDriversForSession(abuDhabiRace) + skipOnRateLimit(t, err) + if err != nil { + t.Fatal(err) + } + + if len(drivers) == 0 { + t.Errorf("Expected drivers for session %d, got none", abuDhabiRace) + } + + for _, d := range drivers { + if d.SessionKey != abuDhabiRace { + t.Errorf("Expected session_key %d, got %d", abuDhabiRace, d.SessionKey) + } + if d.DriverNumber == 0 { + t.Errorf("Expected non-zero driver_number") + } + } +} + +func TestGetSessionResult(t *testing.T) { + client := newTestClient() + + results, err := client.GetSessionResult(abuDhabiRace) + skipOnRateLimit(t, err) + if err != nil { + t.Fatal(err) + } + + if len(results) == 0 { + t.Errorf("Expected results for session %d, got none", abuDhabiRace) + } + + foundFinisher := false + for _, r := range results { + if r.SessionKey != abuDhabiRace { + t.Errorf("Expected session_key %d, got %d", abuDhabiRace, r.SessionKey) + } + if r.Position == 1 { + foundFinisher = true + // Winner should not be DNF/DNS/DSQ + if r.DNF || r.DNS || r.DSQ { + t.Errorf("Race winner (position 1) should not have DNF/DNS/DSQ set") + } + // Winner should have points + if r.Points <= 0 { + t.Errorf("Race winner should have positive points, got %f", r.Points) + } + } + } + if !foundFinisher { + t.Errorf("Expected at least one driver in position 1") + } +} + +func TestGetStintsForSession(t *testing.T) { + client := newTestClient() + + stints, err := client.GetStintsForSession(abuDhabiRace) + skipOnRateLimit(t, err) + if err != nil { + t.Fatal(err) + } + + if len(stints) == 0 { + t.Errorf("Expected stints for session %d, got none", abuDhabiRace) + } + + validCompounds := map[string]bool{ + "SOFT": true, "MEDIUM": true, "HARD": true, + "INTERMEDIATE": true, "WET": true, "UNKNOWN": true, + } + + for _, s := range stints { + if s.SessionKey != abuDhabiRace { + t.Errorf("Expected session_key %d, got %d", abuDhabiRace, s.SessionKey) + } + if !validCompounds[string(s.Compound)] { + t.Errorf("Unexpected tyre compound: %q", s.Compound) + } + } +} + +func TestGetDriverChampionship(t *testing.T) { + client := newTestClient() + + standings, err := client.GetDriverChampionship(abuDhabiRace) + skipOnRateLimit(t, err) + if err != nil { + t.Fatal(err) + } + + if len(standings) == 0 { + t.Errorf("Expected driver championship standings for session %d, got none", abuDhabiRace) + } + + for _, s := range standings { + if s.DriverNumber == 0 { + t.Errorf("Expected non-zero driver_number in championship standings") + } + if s.PositionCurrent == 0 { + t.Errorf("Expected non-zero position_current in championship standings") + } + if s.PointsCurrent < 0 { + t.Errorf("Expected non-negative points_current, got %f", s.PointsCurrent) + } + } +} + +func TestGetRaceControl(t *testing.T) { + client := newTestClient() + + messages, err := client.GetRaceControl(abuDhabiRace) + skipOnRateLimit(t, err) + if err != nil { + t.Fatal(err) + } + + if len(messages) == 0 { + t.Errorf("Expected race control messages for session %d, got none", abuDhabiRace) + } + + for _, m := range messages { + if m.SessionKey != abuDhabiRace { + t.Errorf("Expected session_key %d, got %d", abuDhabiRace, m.SessionKey) + } + if m.Message == "" { + t.Errorf("Expected non-empty message in race control entry") + } + // DriverNumber is a pointer — just verify it doesn't panic (nil is valid) + _ = m.DriverNumber + } +} diff --git a/internal/models/types.go b/internal/models/types.go new file mode 100644 index 0000000..ad90d5b --- /dev/null +++ b/internal/models/types.go @@ -0,0 +1,259 @@ +package models + +type Circuit struct { + CircuitKey int `json:"circuit_key"` + CircuitShortName string `json:"circuit_short_name"` + CircuitType string `json:"circuit_type"` + CircuitInfoURL string `json:"circuit_info_url"` + CircuitImage string `json:"circuit_image"` +} + +type Meeting struct { + MeetingKey int32 `json:"meeting_key"` + MeetingName string `json:"meeting_name"` + MeetingOfficialName string `json:"meeting_official_name"` + Location string `json:"location"` + CountryKey int `json:"country_key"` + CountryCode string `json:"country_code"` + CountryName string `json:"country_name"` + CountryFlag string `json:"country_flag"` + + Circuit + + GMTOffset string `json:"gmt_offset"` + DateStart string `json:"date_start"` + DateEnd string `json:"date_end"` + Year int `json:"year"` +} + +type Session struct { + SessionKey int `json:"session_key"` + SessionName string `json:"session_name"` + SessionType string `json:"session_type"` + + CircuitKey int `json:"circuit_key"` + + MeetingKey int `json:"meeting_key"` + + DateStart string `json:"date_start"` + DateEnd string `json:"date_end"` + GMTOffset string `json:"gmt_offset"` +} + +// TyreCompound represents the type of tyre compound used. +type TyreCompound string + +const ( + CompoundSoft TyreCompound = "SOFT" + CompoundMedium TyreCompound = "MEDIUM" + CompoundHard TyreCompound = "HARD" + CompoundIntermediate TyreCompound = "INTERMEDIATE" + CompoundWet TyreCompound = "WET" + CompoundUnknown TyreCompound = "UNKNOWN" +) + +// Flag represents race control flag colours/types. +type Flag string + +const ( + FlagGreen Flag = "GREEN" + FlagYellow Flag = "YELLOW" + FlagDoubleYellow Flag = "DOUBLE YELLOW" + FlagRed Flag = "RED" + FlagBlue Flag = "BLUE" + FlagChequered Flag = "CHEQUERED" + FlagBlack Flag = "BLACK" + FlagBlackOrange Flag = "BLACK AND ORANGE" + FlagBlackWhite Flag = "BLACK AND WHITE" +) + +// RaceControlCategory classifies race control messages. +type RaceControlCategory string + +const ( + CategoryFlag RaceControlCategory = "Flag" + CategorySafetyCar RaceControlCategory = "SafetyCar" + CategoryDRS RaceControlCategory = "Drs" + CategoryOther RaceControlCategory = "Other" +) + +type Driver struct { + BroadcastName string `json:"broadcast_name"` + DriverNumber int `json:"driver_number"` + FirstName string `json:"first_name"` + FullName string `json:"full_name"` + HeadshotURL string `json:"headshot_url"` + LastName string `json:"last_name"` + MeetingKey int `json:"meeting_key"` + NameAcronym string `json:"name_acronym"` + SessionKey int `json:"session_key"` + TeamColour string `json:"team_colour"` // hex RRGGBB + TeamName string `json:"team_name"` +} + +type ChampionshipDriver struct { + DriverNumber int `json:"driver_number"` + MeetingKey int `json:"meeting_key"` + PointsCurrent float64 `json:"points_current"` + PointsStart float64 `json:"points_start"` + PositionCurrent int `json:"position_current"` + PositionStart int `json:"position_start"` + SessionKey int `json:"session_key"` +} + +type ChampionshipTeam struct { + MeetingKey int `json:"meeting_key"` + PointsCurrent float64 `json:"points_current"` + PointsStart float64 `json:"points_start"` + PositionCurrent int `json:"position_current"` + PositionStart int `json:"position_start"` + SessionKey int `json:"session_key"` + TeamName string `json:"team_name"` +} + +type SessionResult struct { + DNF bool `json:"dnf"` + DNS bool `json:"dns"` + DSQ bool `json:"dsq"` + DriverNumber int `json:"driver_number"` + Duration interface{} `json:"duration"` // float64 or []float64 (qualifying) + GapToLeader interface{} `json:"gap_to_leader"` // float64, string "+N LAP(S)", or []float64 + NumberOfLaps int `json:"number_of_laps"` + MeetingKey int `json:"meeting_key"` + Points float64 `json:"points"` + Position int `json:"position"` + SessionKey int `json:"session_key"` +} + +type StartingGrid struct { + DriverNumber int `json:"driver_number"` + LapDuration float64 `json:"lap_duration"` // qualifying lap time in seconds + MeetingKey int `json:"meeting_key"` + Position int `json:"position"` + SessionKey int `json:"session_key"` +} + +type Lap struct { + DateStart string `json:"date_start"` + DriverNumber int `json:"driver_number"` + DurationSector1 *float64 `json:"duration_sector_1"` + DurationSector2 *float64 `json:"duration_sector_2"` + DurationSector3 *float64 `json:"duration_sector_3"` + I1Speed int `json:"i1_speed"` + I2Speed int `json:"i2_speed"` + IsPitOutLap bool `json:"is_pit_out_lap"` + LapDuration *float64 `json:"lap_duration"` + LapNumber int `json:"lap_number"` + MeetingKey int `json:"meeting_key"` + SegmentsSector1 []int `json:"segments_sector_1"` + SegmentsSector2 []int `json:"segments_sector_2"` + SegmentsSector3 []int `json:"segments_sector_3"` + SessionKey int `json:"session_key"` + StSpeed int `json:"st_speed"` // speed trap +} + +type Stint struct { + Compound TyreCompound `json:"compound"` + DriverNumber int `json:"driver_number"` + LapEnd int `json:"lap_end"` + LapStart int `json:"lap_start"` + MeetingKey int `json:"meeting_key"` + SessionKey int `json:"session_key"` + StintNumber int `json:"stint_number"` + TyreAgeAtStart int `json:"tyre_age_at_start"` +} + +type Pit struct { + Date string `json:"date"` + DriverNumber int `json:"driver_number"` + LaneDuration float64 `json:"lane_duration"` // pit lane time (entry to exit) + LapNumber int `json:"lap_number"` + MeetingKey int `json:"meeting_key"` + PitDuration float64 `json:"pit_duration"` // deprecated, use StopDuration + SessionKey int `json:"session_key"` + StopDuration float64 `json:"stop_duration"` // stationary time only +} + +type Position struct { + Date string `json:"date"` + DriverNumber int `json:"driver_number"` + MeetingKey int `json:"meeting_key"` + Position int `json:"position"` + SessionKey int `json:"session_key"` +} + +type Interval struct { + Date string `json:"date"` + DriverNumber int `json:"driver_number"` + GapToLeader *float64 `json:"gap_to_leader"` // null when leading + Interval *float64 `json:"interval"` // null when leading + MeetingKey int `json:"meeting_key"` + SessionKey int `json:"session_key"` +} + +type Overtake struct { + Date string `json:"date"` + MeetingKey int `json:"meeting_key"` + OvertakenDriverNumber int `json:"overtaken_driver_number"` + OvertakingDriverNumber int `json:"overtaking_driver_number"` + Position int `json:"position"` + SessionKey int `json:"session_key"` +} + +type RaceControl struct { + Category RaceControlCategory `json:"category"` + Date string `json:"date"` + DriverNumber *int `json:"driver_number"` // nil = all drivers + Flag Flag `json:"flag"` + LapNumber *int `json:"lap_number"` + MeetingKey int `json:"meeting_key"` + Message string `json:"message"` + QualifyingPhase *int `json:"qualifying_phase"` // 1-3, nil if N/A + Scope string `json:"scope"` // "Track", "Driver", "Sector" + Sector *int `json:"sector"` // nil if track-wide + SessionKey int `json:"session_key"` +} + +type Weather struct { + AirTemperature float64 `json:"air_temperature"` + Date string `json:"date"` + Humidity float64 `json:"humidity"` + MeetingKey int `json:"meeting_key"` + Pressure float64 `json:"pressure"` + Rainfall int `json:"rainfall"` // 0 = dry, 1 = rain + SessionKey int `json:"session_key"` + TrackTemperature float64 `json:"track_temperature"` + WindDirection int `json:"wind_direction"` // 0-359 degrees + WindSpeed float64 `json:"wind_speed"` // m/s +} + +type CarData struct { + Brake int `json:"brake"` // 0-100 + Date string `json:"date"` + DriverNumber int `json:"driver_number"` + DRS int `json:"drs"` // 0=off, 8=eligible, 10=open + MeetingKey int `json:"meeting_key"` + NGear int `json:"n_gear"` // 0-8 + RPM int `json:"rpm"` + SessionKey int `json:"session_key"` + Speed int `json:"speed"` // km/h + Throttle int `json:"throttle"` // 0-100 +} + +type Location struct { + Date string `json:"date"` + DriverNumber int `json:"driver_number"` + MeetingKey int `json:"meeting_key"` + SessionKey int `json:"session_key"` + X float64 `json:"x"` + Y float64 `json:"y"` + Z float64 `json:"z"` +} + +type TeamRadio struct { + Date string `json:"date"` + DriverNumber int `json:"driver_number"` + MeetingKey int `json:"meeting_key"` + RecordingURL string `json:"recording_url"` + SessionKey int `json:"session_key"` +}