diff --git a/cmd/content.go b/cmd/content.go index 379b68c..a5ec907 100644 --- a/cmd/content.go +++ b/cmd/content.go @@ -17,23 +17,28 @@ along with this program. If not, see . package cmd import ( + "context" + "encoding/json" "fmt" "log" - "strconv" "github.com/spf13/cobra" - "scm.dairydemon.net/filifa/mlbstats/cmd/internal/statsapi" + "scm.dairydemon.net/filifa/mlbstats/api" ) func content(cmd *cobra.Command, args []string) { - pkStr := strconv.Itoa(gamePk) - - contentResp, err := statsapi.RequestContent(pkStr) + client := api.NewAPIClient(api.NewConfiguration()) + standing, _, err := client.GameApi.Content(context.Background(), gamePk, nil) if err != nil { log.Fatal(err) } - fmt.Println(string(contentResp)) + json, err := json.Marshal(standing) + if err != nil { + log.Fatal(err) + } + + fmt.Println(string(json)) } // contentCmd represents the content command @@ -56,6 +61,6 @@ func init() { // Cobra supports local flags which will only run when this command // is called directly, e.g.: - contentCmd.Flags().IntVarP(&gamePk, "gamePk", "g", 0, "game PK") + contentCmd.Flags().Int32VarP(&gamePk, "gamePk", "g", 0, "game PK") contentCmd.MarkFlagRequired("gamePk") } diff --git a/cmd/feed.go b/cmd/feed.go index 38b7495..c0405b2 100644 --- a/cmd/feed.go +++ b/cmd/feed.go @@ -17,23 +17,28 @@ along with this program. If not, see . package cmd import ( + "context" + "encoding/json" "fmt" "log" - "strconv" "github.com/spf13/cobra" - "scm.dairydemon.net/filifa/mlbstats/cmd/internal/statsapi" + "scm.dairydemon.net/filifa/mlbstats/api" ) func feed(cmd *cobra.Command, args []string) { - pkStr := strconv.Itoa(gamePk) - - feedResp, err := statsapi.RequestFeed(pkStr) + client := api.NewAPIClient(api.NewConfiguration()) + standing, _, err := client.GameApi.LiveGameV1(context.Background(), gamePk, nil) if err != nil { log.Fatal(err) } - fmt.Println(string(feedResp)) + json, err := json.Marshal(standing) + if err != nil { + log.Fatal(err) + } + + fmt.Println(string(json)) } // feedCmd represents the feed command @@ -56,6 +61,6 @@ func init() { // Cobra supports local flags which will only run when this command // is called directly, e.g.: - feedCmd.Flags().IntVarP(&gamePk, "gamePk", "g", 0, "game PK") + feedCmd.Flags().Int32VarP(&gamePk, "gamePk", "g", 0, "game PK") feedCmd.MarkFlagRequired("gamePk") } diff --git a/cmd/internal/statsapi/client.go b/cmd/internal/statsapi/client.go deleted file mode 100644 index 4d13caa..0000000 --- a/cmd/internal/statsapi/client.go +++ /dev/null @@ -1,153 +0,0 @@ -/* -Copyright © 2024 filifa - -This program is free software: you can redistribute it and/or modify it under -the terms of the GNU General Public License as published by the Free Software -Foundation, either version 3 of the License, or (at your option) any later -version. - -This program is distributed in the hope that it will be useful, but WITHOUT ANY -WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A -PARTICULAR PURPOSE. See the GNU General Public License for more details. - -You should have received a copy of the GNU General Public License along with -this program. If not, see . -*/ -package statsapi - -import ( - "encoding/json" - "io" - "net/http" - "net/url" - - "github.com/evanphx/json-patch/v5" -) - -// DefaultClient is a statsapi.Client instantiated using http.DefaultClient. -var DefaultClient = NewClient(http.DefaultClient) - -// RequestSchedule uses DefaultClient to access the schedule endpoint. -func RequestSchedule(sportId, teamId, date string) ([]byte, error) { - return DefaultClient.RequestSchedule(sportId, teamId, date) -} - -// RequestFeed uses DefaultClient to access the feed endpoint. -func RequestFeed(gamePk string) ([]byte, error) { - return DefaultClient.RequestFeed(gamePk) -} - -// RequestDiffPatch uses DefaultClient to access the diffpatch endpoint. -func RequestDiffPatch(gamePk, startTimecode, pushUpdateId string) (DiffPatchResponse, error) { - return DefaultClient.RequestDiffPatch(gamePk, startTimecode, pushUpdateId) -} - -// RequestContent uses DefaultClient to access the content endpoint. -func RequestContent(gamePk string) ([]byte, error) { - return DefaultClient.RequestContent(gamePk) -} - -// RequestStandings uses DefaultClient to access the standings endpoint. -func RequestStandings(leagueId string) ([]byte, error) { - return DefaultClient.RequestStandings(leagueId) -} - -type DiffPatchResponse []byte - -// Client is a struct used for making Stats API requests. -type Client struct { - baseURL url.URL - httpClient *http.Client -} - -// NewClient returns a statsapi.Client with statsapi.mlb.com as the base URL. -func NewClient(c *http.Client) *Client { - return &Client{ - baseURL: url.URL{ - Scheme: "https", - Host: "statsapi.mlb.com", - }, - httpClient: c, - } -} - -// ExtractPatches extracts a list of jsonpatch.Patch structs from the response -// given by the diffpatch endpoint. These patches can then be applied more -// easily than the raw response. -func (resp *DiffPatchResponse) ExtractPatches() ([]jsonpatch.Patch, error) { - var patches []jsonpatch.Patch - - var objs []map[string]jsonpatch.Patch - err := json.Unmarshal([]byte(*resp), &objs) - if err != nil { - return patches, err - } - - for _, obj := range objs { - patch := obj["diff"] - patches = append(patches, patch) - } - - return patches, err -} - -// RequestSchedule accesses the schedule endpoint. -func (c *Client) RequestSchedule(sportId, teamId, date string) ([]byte, error) { - endpoint := url.URL{Path: "api/v1/schedule"} - query := endpoint.Query() - query.Add("sportId", sportId) - query.Add("teamId", teamId) - query.Add("date", date) - endpoint.RawQuery = query.Encode() - - return c.get(&endpoint) -} - -// RequestFeed accesses the feed endpoint. -func (c *Client) RequestFeed(gamePk string) ([]byte, error) { - endpoint := url.URL{Path: "api/v1.1/game/" + gamePk + "/feed/live"} - return c.get(&endpoint) -} - -// RequestDiffPatch accesses the diffpatch endpoint. -func (c *Client) RequestDiffPatch(gamePk, startTimecode, pushUpdateId string) (DiffPatchResponse, error) { - endpoint := url.URL{Path: "api/v1.1/game/" + gamePk + "/feed/live/diffPatch"} - query := endpoint.Query() - query.Add("language", "en") - query.Add("startTimecode", startTimecode) - query.Add("pushUpdateId", pushUpdateId) - endpoint.RawQuery = query.Encode() - - return c.get(&endpoint) -} - -// RequestContent accesses the content endpoint. -func (c *Client) RequestContent(gamePk string) ([]byte, error) { - endpoint := url.URL{Path: "api/v1/game/" + gamePk + "/content"} - return c.get(&endpoint) -} - -// RequestStandings accesses the standings endpoint. -func (c *Client) RequestStandings(leagueId string) ([]byte, error) { - endpoint := url.URL{Path: "api/v1/standings"} - query := endpoint.Query() - query.Add("leagueId", leagueId) - endpoint.RawQuery = query.Encode() - - return c.get(&endpoint) -} - -// get makes a GET request to the given endpoint and returns the bytes from the -// body of the response. -func (c *Client) get(endpoint *url.URL) ([]byte, error) { - url := c.baseURL.ResolveReference(endpoint) - - resp, err := c.httpClient.Get(url.String()) - if err != nil { - return nil, err - } - defer resp.Body.Close() - - body, err := io.ReadAll(resp.Body) - return body, err -} diff --git a/cmd/internal/statsapi/websocket.go b/cmd/internal/statsapi/websocket.go index fba6c01..0c309b3 100644 --- a/cmd/internal/statsapi/websocket.go +++ b/cmd/internal/statsapi/websocket.go @@ -17,6 +17,7 @@ package statsapi import ( "net/url" + "strconv" "time" "github.com/gorilla/websocket" @@ -42,7 +43,7 @@ type GamedayWebsocket struct { // NewGamedayWebsocket creates a statsapi.GamedayWebsocket using the Stats API // websocket URL and establishes a connection. -func NewGamedayWebsocket(gamePk string) (*GamedayWebsocket, error) { +func NewGamedayWebsocket(gamePk int32) (*GamedayWebsocket, error) { ws := GamedayWebsocket{ baseURL: url.URL{ Scheme: "wss", @@ -54,9 +55,9 @@ func NewGamedayWebsocket(gamePk string) (*GamedayWebsocket, error) { return &ws, err } -func (g *GamedayWebsocket) init(gamePk string) error { +func (g *GamedayWebsocket) init(gamePk int32) error { endpoint := url.URL{ - Path: "api/v1/game/push/subscribe/gameday/" + gamePk, + Path: "api/v1/game/push/subscribe/gameday/" + strconv.Itoa(int(gamePk)), } url := g.baseURL.ResolveReference(&endpoint) diff --git a/cmd/schedule.go b/cmd/schedule.go index 2e0c015..22443fb 100644 --- a/cmd/schedule.go +++ b/cmd/schedule.go @@ -18,12 +18,12 @@ package cmd import ( "context" - "fmt" "encoding/json" + "fmt" "log" - "github.com/spf13/cobra" "github.com/antihax/optional" + "github.com/spf13/cobra" "scm.dairydemon.net/filifa/mlbstats/api" ) @@ -40,7 +40,7 @@ func schedule(cmd *cobra.Command, args []string) { sportId := int32(sportIDs[string(sport)]) opts := api.ScheduleApiScheduleOpts{ - TeamId: optional.NewInterface(teamIds), + TeamId: optional.NewInterface(teamIds), SportId: optional.NewInterface([]int32{sportId}), } diff --git a/cmd/standings.go b/cmd/standings.go index 6429a0e..01cea33 100644 --- a/cmd/standings.go +++ b/cmd/standings.go @@ -22,8 +22,8 @@ import ( "fmt" "log" - "github.com/spf13/cobra" "github.com/antihax/optional" + "github.com/spf13/cobra" "scm.dairydemon.net/filifa/mlbstats/api" "scm.dairydemon.net/filifa/mlbstats/api/models" ) @@ -39,7 +39,7 @@ func standings(cmd *cobra.Command, args []string) { } client := api.NewAPIClient(api.NewConfiguration()) - standing, _, err := client.StandingsApi.Standings1(context.Background(),string(models.REGULAR_SEASON_StandingsType), &opts) + standing, _, err := client.StandingsApi.Standings1(context.Background(), string(models.REGULAR_SEASON_StandingsType), &opts) if err != nil { log.Fatal(err) } diff --git a/cmd/subscribe.go b/cmd/subscribe.go index 68335a0..6f62e75 100644 --- a/cmd/subscribe.go +++ b/cmd/subscribe.go @@ -17,49 +17,72 @@ along with this program. If not, see . package cmd import ( + "context" "encoding/json" "fmt" "log" - "strconv" "time" + "github.com/antihax/optional" + "github.com/evanphx/json-patch/v5" "github.com/gorilla/websocket" "github.com/spf13/cobra" + "scm.dairydemon.net/filifa/mlbstats/api" + "scm.dairydemon.net/filifa/mlbstats/api/models" "scm.dairydemon.net/filifa/mlbstats/cmd/internal/statsapi" ) -var gamePk int +var gamePk int32 -type Feed struct { - MetaData metadata -} +func extractPatches(resp string) ([]jsonpatch.Patch, error) { + var patches []jsonpatch.Patch -type metadata struct { - TimeStamp string -} - -func patch(feedResp []byte, gamePk, ts, updateId string) ([]byte, error) { - diffPatchResp, err := statsapi.RequestDiffPatch(gamePk, ts, updateId) + var objs []map[string]jsonpatch.Patch + err := json.Unmarshal([]byte(resp), &objs) if err != nil { - return feedResp, err + return patches, err } - patches, err := diffPatchResp.ExtractPatches() + for _, obj := range objs { + patch := obj["diff"] + patches = append(patches, patch) + } + + return patches, err +} + +func patch(feed *models.BaseballGameRestObject, client *api.APIClient) error { + opts := api.GameApiLiveGameDiffPatchV1Opts{ + StartTimecode: optional.NewString(feed.MetaData.TimeStamp), + } + + diffPatch, _, err := client.GameApi.LiveGameDiffPatchV1(context.Background(), feed.GamePk, &opts) if err != nil { - return feedResp, err + return err + } + + patches, err := extractPatches(diffPatch) + if err != nil { + return err + } + + enc, err := json.Marshal(feed) + if err != nil { + return err } for _, patch := range patches { - feedResp, err = patch.Apply(feedResp) + enc, err = patch.Apply(enc) if err != nil { - return feedResp, err + return err } } - return feedResp, err + err = json.Unmarshal(enc, &feed) + return err } -func newWebsocket(gamePk string) (*statsapi.GamedayWebsocket, <-chan error, error) { +func newWebsocket(gamePk int32) (*statsapi.GamedayWebsocket, <-chan error, error) { ws, err := statsapi.NewGamedayWebsocket(gamePk) if err != nil { return nil, nil, err @@ -71,63 +94,69 @@ func newWebsocket(gamePk string) (*statsapi.GamedayWebsocket, <-chan error, erro return ws, ch, err } -func handleUnexpectedClose(gamePk string) (*statsapi.GamedayWebsocket, []byte, error) { - ws, _, err := newWebsocket(gamePk) - if err != nil { - return nil, nil, err - } - - feedResp, err := statsapi.RequestFeed(gamePk) - return ws, feedResp, err -} - -func updateFeed(ws *statsapi.GamedayWebsocket, feedResp []byte, gamePk string) ([]byte, error) { - var feed Feed - err := json.Unmarshal(feedResp, &feed) +func handleUnexpectedClose(feed *models.BaseballGameRestObject, client *api.APIClient) (*statsapi.GamedayWebsocket, error) { + ws, _, err := newWebsocket(feed.GamePk) if err != nil { return nil, err } - ts := feed.MetaData.TimeStamp + newFeed, _, err := client.GameApi.LiveGameV1(context.Background(), feed.GamePk, nil) + *feed = newFeed + return ws, err +} + +func updateFeed(feed *models.BaseballGameRestObject, ws *statsapi.GamedayWebsocket, client *api.APIClient) error { var p statsapi.Push - err = ws.ReadJSON(&p) + err := ws.ReadJSON(&p) if websocket.IsUnexpectedCloseError(err, statsapi.GameFinalCode, statsapi.GameUnavailableCode) { log.Println(err) - newWs, feedResp, err := handleUnexpectedClose(gamePk) + newWs, err := handleUnexpectedClose(feed, client) if err != nil { - return feedResp, err + return err } + *ws = *newWs - return feedResp, err + return err } else if err != nil { - return nil, err + return err } - feedResp, err = patch(feedResp, gamePk, ts, p.UpdateId) + err = patch(feed, client) if err != nil { - feedResp, err = statsapi.RequestFeed(gamePk) + newFeed, _, err := client.GameApi.LiveGameV1(context.Background(), feed.GamePk, nil) + if err != nil { + return err + } + + *feed = newFeed } - return feedResp, err + + return err } func subscribe(cmd *cobra.Command, args []string) { - pkStr := strconv.Itoa(gamePk) - ws, _, err := newWebsocket(pkStr) + ws, _, err := newWebsocket(gamePk) if err != nil { log.Fatal(err) } defer ws.Close() - feedResp, err := statsapi.RequestFeed(pkStr) + client := api.NewAPIClient(api.NewConfiguration()) + feed, _, err := client.GameApi.LiveGameV1(context.Background(), gamePk, nil) if err != nil { log.Fatal(err) } for { - fmt.Println(string(feedResp)) + json, err := json.Marshal(feed) + if err != nil { + log.Fatal(err) + } - feedResp, err = updateFeed(ws, feedResp, pkStr) + fmt.Println(string(json)) + + err = updateFeed(&feed, ws, client) if err != nil { log.Fatal(err) } @@ -157,6 +186,6 @@ func init() { // Cobra supports local flags which will only run when this command // is called directly, e.g.: - subscribeCmd.Flags().IntVarP(&gamePk, "gamePk", "g", 0, "game PK") + subscribeCmd.Flags().Int32VarP(&gamePk, "gamePk", "g", 0, "game PK") subscribeCmd.MarkFlagRequired("gamePk") }