package espn import ( "context" "encoding/json" "fmt" "net/http" "sort" "strings" "github.com/humdrum-tiv/sportsball/internal/model" ) // TeamSchedule fetches a team's full season schedule (completed results and // upcoming fixtures) from ESPN's team-schedule endpoint — the data behind the // schedule view, reaching beyond the polled scoreboard window. func (c *Client) TeamSchedule(ctx context.Context, l model.League, teamID string) (model.TeamSchedule, error) { url := fmt.Sprintf("%s/%s/%s/teams/%s/schedule", baseURL, l.Sport, l.Path, teamID) req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) if err != nil { return model.TeamSchedule{}, err } resp, err := c.HTTP.Do(req) if err != nil { return model.TeamSchedule{}, fmt.Errorf("schedule %s/%s: %w", l.ID, teamID, err) } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { return model.TeamSchedule{}, fmt.Errorf("schedule %s/%s: status %d", l.ID, teamID, resp.StatusCode) } var r scheduleResp if err := json.NewDecoder(resp.Body).Decode(&r); err != nil { return model.TeamSchedule{}, fmt.Errorf("decode schedule %s/%s: %w", l.ID, teamID, err) } out := model.TeamSchedule{ Team: teamFromJSON(r.Team), Season: strings.TrimSpace(r.Season.DisplayName + " " + r.Season.Name), } for _, ev := range r.Events { if g, ok := mapScheduleGame(l.ID, ev); ok { out.Games = append(out.Games, g) } } sort.SliceStable(out.Games, func(i, j int) bool { return out.Games[i].Start.Before(out.Games[j].Start) }) return out, nil } func mapScheduleGame(id model.LeagueID, ev schedEvent) (model.Game, bool) { if len(ev.Competitions) == 0 { return model.Game{}, false } comp := ev.Competitions[0] g := model.Game{ ID: ev.ID, League: id, Start: parseTime(ev.Date), State: mapState(comp.Status.Type.State), Detail: strings.TrimSpace(comp.Status.Type.Detail), Venue: comp.Venue.FullName, } for _, cmp := range comp.Competitors { t := teamFromJSON(cmp.Team) t.Score = int(cmp.Score.Value) t.Winner = cmp.Winner if cmp.HomeAway == "home" { g.Home = t } else { g.Away = t } } return g, true } // --- team-schedule JSON ----------------------------------------------------- type scheduleResp struct { Team teamJSON `json:"team"` Season seasonJSON `json:"season"` Events []schedEvent `json:"events"` } type seasonJSON struct { Name string `json:"name"` // "Regular Season" DisplayName string `json:"displayName"` // "2026" } type schedEvent struct { ID string `json:"id"` Date string `json:"date"` Competitions []schedComp `json:"competitions"` } type schedComp struct { Venue venue `json:"venue"` Status status `json:"status"` Competitors []schedCompetitor `json:"competitors"` } // schedCompetitor mirrors a scoreboard competitor but the score arrives as an // object ({value, displayValue}) here rather than the scoreboard's bare string. type schedCompetitor struct { HomeAway string `json:"homeAway"` Winner bool `json:"winner"` Score scoreObj `json:"score"` Team teamJSON `json:"team"` } type scoreObj struct { Value float64 `json:"value"` DisplayValue string `json:"displayValue"` }