1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
|
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"`
}
|