mirror of
https://github.com/AmanTahiliani/box-box.git
synced 2026-08-07 19:56:18 -04:00
Upgrades to Live and API Caching
This commit is contained in:
@@ -8,10 +8,23 @@ import (
|
||||
"io"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/AmanTahiliani/box-box/internal/models"
|
||||
)
|
||||
|
||||
// ErrLiveSessionLocked is returned when the OpenF1 API blocks free-tier access
|
||||
// during a live F1 session. All endpoints (including historical data) return 401
|
||||
// from ~30 min before a session starts until ~30 min after it ends.
|
||||
var ErrLiveSessionLocked = errors.New("live F1 session in progress — API access is restricted to authenticated users until the session ends")
|
||||
|
||||
// IsLiveSessionError reports whether err (or any error in its chain) is the
|
||||
// live-session lockout error from the OpenF1 API.
|
||||
func IsLiveSessionError(err error) bool {
|
||||
return errors.Is(err, ErrLiveSessionLocked)
|
||||
}
|
||||
|
||||
// get performs a GET request and returns the response body, or an error if the
|
||||
// status code is not 200 OK. It checks a local file cache before making the request.
|
||||
func (c *OpenF1Client) get(url string) (io.ReadCloser, error) {
|
||||
@@ -19,13 +32,35 @@ func (c *OpenF1Client) get(url string) (io.ReadCloser, error) {
|
||||
return io.NopCloser(bytes.NewReader(cachedData)), nil
|
||||
}
|
||||
|
||||
resp, err := c.httpClient.Get(url)
|
||||
req, err := http.NewRequest("GET", url, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if c.apiKey != "" {
|
||||
req.Header.Set("Authorization", "Bearer "+c.apiKey)
|
||||
}
|
||||
|
||||
resp, err := c.httpClient.Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
// Try to parse the JSON error body for a better message.
|
||||
if resp.StatusCode == http.StatusUnauthorized {
|
||||
body, _ := io.ReadAll(resp.Body)
|
||||
var apiErr struct {
|
||||
Detail string `json:"detail"`
|
||||
}
|
||||
if json.Unmarshal(body, &apiErr) == nil && apiErr.Detail != "" {
|
||||
detail := strings.ToLower(apiErr.Detail)
|
||||
if strings.Contains(detail, "live") && strings.Contains(detail, "session") {
|
||||
return nil, fmt.Errorf("%w", ErrLiveSessionLocked)
|
||||
}
|
||||
return nil, fmt.Errorf("openf1 API: %s", apiErr.Detail)
|
||||
}
|
||||
}
|
||||
return nil, fmt.Errorf("openf1 API returned status %d for %s", resp.StatusCode, url)
|
||||
}
|
||||
|
||||
@@ -130,8 +165,8 @@ func (c *OpenF1Client) GetTeamChampionship(sessionKey int) ([]models.Championshi
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// getLatestRaceSessionKey returns the session_key of the most recent Race session
|
||||
// by fetching sessions filtered to session_name=Race and returning the last one.
|
||||
// getLatestRaceSessionKey returns the session_key of the most recent completed
|
||||
// Race session across all years.
|
||||
func (c *OpenF1Client) getLatestRaceSessionKey() (int, error) {
|
||||
body, err := c.get(fmt.Sprintf("%s/v1/sessions?session_name=Race", c.url))
|
||||
if err != nil {
|
||||
@@ -146,16 +181,31 @@ func (c *OpenF1Client) getLatestRaceSessionKey() (int, error) {
|
||||
if len(sessions) == 0 {
|
||||
return 0, errors.New("no Race sessions found")
|
||||
}
|
||||
return sessions[len(sessions)-1].SessionKey, nil
|
||||
|
||||
// Walk backwards to find the most recent completed race.
|
||||
now := time.Now()
|
||||
for i := len(sessions) - 1; i >= 0; i-- {
|
||||
s := sessions[i]
|
||||
if s.DateEnd != "" {
|
||||
endTime, err := time.Parse(time.RFC3339, s.DateEnd)
|
||||
if err == nil && endTime.Before(now) {
|
||||
return s.SessionKey, nil
|
||||
}
|
||||
} else if s.DateStart != "" {
|
||||
startTime, err := time.Parse(time.RFC3339, s.DateStart)
|
||||
if err == nil && startTime.Add(3*time.Hour).Before(now) {
|
||||
return s.SessionKey, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return 0, errors.New("no completed Race sessions found")
|
||||
}
|
||||
|
||||
// getLatestRaceSessionKeyForYear returns the session_key of the most recent Race session
|
||||
// for a specific year.
|
||||
// getLatestRaceSessionKeyForYear returns the session_key of the most recent
|
||||
// completed Race session for a specific year. It walks backwards through the
|
||||
// year's races to find one whose date_end is in the past (i.e. has results).
|
||||
func (c *OpenF1Client) getLatestRaceSessionKeyForYear(year int) (int, error) {
|
||||
// The OpenF1 API doesn't support a direct year filter on sessions yet (verified by docs/common patterns)
|
||||
// so we'll fetch meetings for that year first, then find the latest session.
|
||||
// Actually, session endpoint does support year filter according to some versions of docs.
|
||||
// Let's try year filter first as it's more efficient.
|
||||
body, err := c.get(fmt.Sprintf("%s/v1/sessions?session_name=Race&year=%d", c.url, year))
|
||||
if err != nil {
|
||||
return 0, err
|
||||
@@ -169,7 +219,26 @@ func (c *OpenF1Client) getLatestRaceSessionKeyForYear(year int) (int, error) {
|
||||
if len(sessions) == 0 {
|
||||
return 0, fmt.Errorf("no Race sessions found for year %d", year)
|
||||
}
|
||||
return sessions[len(sessions)-1].SessionKey, nil
|
||||
|
||||
// Walk backwards to find the most recent completed race.
|
||||
now := time.Now()
|
||||
for i := len(sessions) - 1; i >= 0; i-- {
|
||||
s := sessions[i]
|
||||
if s.DateEnd != "" {
|
||||
endTime, err := time.Parse(time.RFC3339, s.DateEnd)
|
||||
if err == nil && endTime.Before(now) {
|
||||
return s.SessionKey, nil
|
||||
}
|
||||
} else if s.DateStart != "" {
|
||||
// Fallback: if no DateEnd, check DateStart + 3 hours as a rough estimate.
|
||||
startTime, err := time.Parse(time.RFC3339, s.DateStart)
|
||||
if err == nil && startTime.Add(3*time.Hour).Before(now) {
|
||||
return s.SessionKey, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return 0, fmt.Errorf("no completed Race sessions found for year %d", year)
|
||||
}
|
||||
|
||||
// GetLatestDriverChampionship returns championship standings for the most recent
|
||||
|
||||
Reference in New Issue
Block a user