use cobra
This commit is contained in:
32
cmd/game.go
Normal file
32
cmd/game.go
Normal file
@@ -0,0 +1,32 @@
|
||||
package cmd
|
||||
|
||||
import "scm.dairydemon.net/filifa/mlblive/cmd/internal/statsapi"
|
||||
|
||||
type Game struct {
|
||||
CurrPlayDesc string
|
||||
HomeScore uint
|
||||
AwayScore uint
|
||||
}
|
||||
|
||||
func (g *Game) Update(feed *statsapi.Feed) error {
|
||||
homeScore := feed.LiveData.Linescore.Teams.Home.Runs
|
||||
awayScore := feed.LiveData.Linescore.Teams.Away.Runs
|
||||
// FIXME: currentPlay/result doesn't always have description
|
||||
desc := feed.LiveData.Plays.CurrentPlay.Result.Description
|
||||
|
||||
score, err := homeScore.Int64()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
g.HomeScore = uint(score)
|
||||
|
||||
score, err = awayScore.Int64()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
g.AwayScore = uint(score)
|
||||
|
||||
g.CurrPlayDesc = desc
|
||||
|
||||
return nil
|
||||
}
|
||||
111
cmd/internal/statsapi/client.go
Normal file
111
cmd/internal/statsapi/client.go
Normal file
@@ -0,0 +1,111 @@
|
||||
package statsapi
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
|
||||
"github.com/evanphx/json-patch/v5"
|
||||
)
|
||||
|
||||
var DefaultClient = NewClient(http.DefaultClient)
|
||||
|
||||
func RequestSchedule(sportId, teamId string) ([]byte, error) {
|
||||
return DefaultClient.RequestSchedule(sportId, teamId)
|
||||
}
|
||||
|
||||
func RequestFeed(gamePk string) ([]byte, error) {
|
||||
return DefaultClient.RequestFeed(gamePk)
|
||||
}
|
||||
|
||||
func RequestDiffPatch(gamePk, startTimecode, pushUpdateId string) (DiffPatchResponse, error) {
|
||||
return DefaultClient.RequestDiffPatch(gamePk, startTimecode, pushUpdateId)
|
||||
}
|
||||
|
||||
type DiffPatchResponse []byte
|
||||
|
||||
type Client struct {
|
||||
baseURL url.URL
|
||||
httpClient *http.Client
|
||||
}
|
||||
|
||||
func NewClient(c *http.Client) *Client {
|
||||
return &Client{
|
||||
baseURL: url.URL{
|
||||
Scheme: "https",
|
||||
Host: "statsapi.mlb.com",
|
||||
},
|
||||
httpClient: c,
|
||||
}
|
||||
}
|
||||
|
||||
func (resp *DiffPatchResponse) ExtractPatches() ([]jsonpatch.Patch, error) {
|
||||
var patches []jsonpatch.Patch
|
||||
|
||||
var objs []map[string]any
|
||||
err := json.Unmarshal([]byte(*resp), &objs)
|
||||
if err != nil {
|
||||
return patches, err
|
||||
}
|
||||
|
||||
for _, obj := range objs {
|
||||
diff := obj["diff"]
|
||||
patch, err := json.Marshal(diff)
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
|
||||
p, err := jsonpatch.DecodePatch(patch)
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
patches = append(patches, p)
|
||||
}
|
||||
|
||||
return patches, err
|
||||
}
|
||||
|
||||
func (c *Client) RequestSchedule(sportId, teamId string) ([]byte, error) {
|
||||
endpoint := url.URL{Path: "api/v1/schedule"}
|
||||
query := endpoint.Query()
|
||||
query.Add("sportId", sportId)
|
||||
query.Add("teamId", teamId)
|
||||
endpoint.RawQuery = query.Encode()
|
||||
|
||||
return c.get(&endpoint)
|
||||
}
|
||||
|
||||
func (c *Client) RequestFeed(gamePk string) ([]byte, error) {
|
||||
endpoint := url.URL{Path: "api/v1.1/game/" + gamePk + "/feed/live"}
|
||||
return c.get(&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)
|
||||
}
|
||||
|
||||
func (c *Client) get(endpoint *url.URL) ([]byte, error) {
|
||||
url := c.baseURL.ResolveReference(endpoint)
|
||||
|
||||
req, err := http.NewRequest("GET", url.String(), nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
resp, err := c.httpClient.Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
return body, err
|
||||
}
|
||||
34
cmd/internal/statsapi/common.go
Normal file
34
cmd/internal/statsapi/common.go
Normal file
@@ -0,0 +1,34 @@
|
||||
package statsapi
|
||||
|
||||
var TeamIds = map[string]int{
|
||||
"laa": 108,
|
||||
"az": 109,
|
||||
"bal": 110,
|
||||
"bos": 111,
|
||||
"chc": 112,
|
||||
"cin": 113,
|
||||
"cle": 114,
|
||||
"col": 115,
|
||||
"det": 116,
|
||||
"hou": 117,
|
||||
"kc": 118,
|
||||
"lad": 119,
|
||||
"wsh": 120,
|
||||
"nym": 121,
|
||||
"oak": 133,
|
||||
"pit": 134,
|
||||
"sd": 135,
|
||||
"sea": 136,
|
||||
"sf": 137,
|
||||
"stl": 138,
|
||||
"tb": 139,
|
||||
"tex": 140,
|
||||
"tor": 141,
|
||||
"min": 142,
|
||||
"phi": 143,
|
||||
"atl": 144,
|
||||
"cws": 145,
|
||||
"mia": 146,
|
||||
"nyy": 147,
|
||||
"mil": 158,
|
||||
}
|
||||
59
cmd/internal/statsapi/feed.go
Normal file
59
cmd/internal/statsapi/feed.go
Normal file
@@ -0,0 +1,59 @@
|
||||
package statsapi
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
)
|
||||
|
||||
type Feed struct {
|
||||
MetaData metadata
|
||||
LiveData livedata
|
||||
}
|
||||
|
||||
type metadata struct {
|
||||
TimeStamp string
|
||||
}
|
||||
|
||||
type livedata struct {
|
||||
Plays plays
|
||||
Linescore linescore
|
||||
}
|
||||
|
||||
type linescore struct {
|
||||
Teams teams
|
||||
}
|
||||
|
||||
type teams struct {
|
||||
Home team
|
||||
Away team
|
||||
}
|
||||
|
||||
type team struct {
|
||||
Runs json.Number
|
||||
}
|
||||
|
||||
type plays struct {
|
||||
AllPlays []Play
|
||||
CurrentPlay Play
|
||||
}
|
||||
|
||||
type Play struct {
|
||||
Result result
|
||||
About about
|
||||
AtBatIndex int
|
||||
}
|
||||
|
||||
type result struct {
|
||||
Event string
|
||||
Description string
|
||||
RBI int
|
||||
AwayScore int
|
||||
HomeScore int
|
||||
}
|
||||
|
||||
type about struct {
|
||||
AtBatIndex json.Number
|
||||
IsTopInning bool
|
||||
Inning json.Number
|
||||
IsScoringPlay bool
|
||||
CaptivatingIndex json.Number
|
||||
}
|
||||
31
cmd/internal/statsapi/schedule.go
Normal file
31
cmd/internal/statsapi/schedule.go
Normal file
@@ -0,0 +1,31 @@
|
||||
package statsapi
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
)
|
||||
|
||||
type ScheduleParams struct {
|
||||
SportId string
|
||||
TeamId string
|
||||
}
|
||||
|
||||
type Schedule struct {
|
||||
TotalGames json.Number
|
||||
TotalGamesInProgress json.Number
|
||||
Dates []date
|
||||
}
|
||||
|
||||
type date struct {
|
||||
TotalGames json.Number
|
||||
TotalGamesInProgress json.Number
|
||||
Games []game
|
||||
}
|
||||
|
||||
type game struct {
|
||||
GamePk json.Number
|
||||
Content content
|
||||
}
|
||||
|
||||
type content struct {
|
||||
Link string
|
||||
}
|
||||
59
cmd/internal/statsapi/websocket.go
Normal file
59
cmd/internal/statsapi/websocket.go
Normal file
@@ -0,0 +1,59 @@
|
||||
package statsapi
|
||||
|
||||
import (
|
||||
"net/url"
|
||||
"time"
|
||||
|
||||
"github.com/gorilla/websocket"
|
||||
)
|
||||
|
||||
type Push struct {
|
||||
UpdateId string
|
||||
}
|
||||
|
||||
type GamedayWebsocket struct {
|
||||
baseURL url.URL
|
||||
*websocket.Conn
|
||||
}
|
||||
|
||||
func NewGamedayWebsocket(gamePk string) (GamedayWebsocket, error) {
|
||||
ws := GamedayWebsocket{
|
||||
baseURL: url.URL{
|
||||
Scheme: "wss",
|
||||
Host: "ws.statsapi.mlb.com",
|
||||
},
|
||||
}
|
||||
|
||||
err := ws.init(gamePk)
|
||||
return ws, err
|
||||
}
|
||||
|
||||
func (g *GamedayWebsocket) init(gamePk string) error {
|
||||
endpoint := url.URL{
|
||||
Path: "api/v1/game/push/subscribe/gameday/" + gamePk,
|
||||
}
|
||||
|
||||
url := g.baseURL.ResolveReference(&endpoint)
|
||||
conn, _, err := websocket.DefaultDialer.Dial(url.String(), nil)
|
||||
|
||||
g.Conn = conn
|
||||
return err
|
||||
}
|
||||
|
||||
func (g *GamedayWebsocket) SendKeepAlive() error {
|
||||
msg := []byte("Gameday5")
|
||||
err := g.Conn.WriteMessage(websocket.TextMessage, msg)
|
||||
return err
|
||||
}
|
||||
|
||||
func (g *GamedayWebsocket) KeepAlive(interval time.Duration, ch chan<- error) {
|
||||
ticker := time.NewTicker(interval)
|
||||
defer ticker.Stop()
|
||||
|
||||
for range ticker.C {
|
||||
err := g.SendKeepAlive()
|
||||
if err != nil {
|
||||
ch <- err
|
||||
}
|
||||
}
|
||||
}
|
||||
149
cmd/root.go
Normal file
149
cmd/root.go
Normal file
@@ -0,0 +1,149 @@
|
||||
/*
|
||||
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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"scm.dairydemon.net/filifa/mlblive/cmd/internal/statsapi"
|
||||
)
|
||||
|
||||
var Team string
|
||||
|
||||
func getGamePk() (string, error) {
|
||||
if Team == "" {
|
||||
return "", errors.New("need team ID")
|
||||
}
|
||||
|
||||
id, ok := statsapi.TeamIds[Team]
|
||||
if !ok {
|
||||
return "", errors.New("invalid team ID")
|
||||
}
|
||||
|
||||
sched, err := statsapi.RequestSchedule("1", strconv.Itoa(id))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
var s statsapi.Schedule
|
||||
err = json.Unmarshal(sched, &s)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
gamePk := s.Dates[0].Games[0].GamePk.String()
|
||||
return gamePk, nil
|
||||
}
|
||||
|
||||
func updateFeed(feedResp []byte, gamePk, ts, updateId string) ([]byte, error) {
|
||||
diffPatchResp, err := statsapi.RequestDiffPatch(gamePk, ts, updateId)
|
||||
if err != nil {
|
||||
return statsapi.RequestFeed(gamePk)
|
||||
}
|
||||
|
||||
patches, err := diffPatchResp.ExtractPatches()
|
||||
if err != nil {
|
||||
return statsapi.RequestFeed(gamePk)
|
||||
}
|
||||
|
||||
for _, patch := range patches {
|
||||
feedResp, err = patch.Apply(feedResp)
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return feedResp, err
|
||||
}
|
||||
|
||||
func mlblive(cmd *cobra.Command, args []string) {
|
||||
gamePk, err := getGamePk()
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
ws, err := statsapi.NewGamedayWebsocket(gamePk)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
defer ws.Close()
|
||||
|
||||
ch := make(chan error)
|
||||
go ws.KeepAlive(10*time.Second, ch)
|
||||
|
||||
feedResp, err := statsapi.RequestFeed(gamePk)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
var feed statsapi.Feed
|
||||
for {
|
||||
fmt.Println(string(feedResp))
|
||||
|
||||
err = json.Unmarshal(feedResp, &feed)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
ts := feed.MetaData.TimeStamp
|
||||
|
||||
var p statsapi.Push
|
||||
err = ws.ReadJSON(&p)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
feedResp, err = updateFeed(feedResp, gamePk, ts, p.UpdateId)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// rootCmd represents the base command when called without any subcommands
|
||||
var rootCmd = &cobra.Command{
|
||||
Use: "mlblive",
|
||||
Short: "Output data from Major League Baseball's Stats API",
|
||||
Long: `Output data from Major League Baseball's Stats API`,
|
||||
Run: mlblive,
|
||||
}
|
||||
|
||||
// Execute adds all child commands to the root command and sets flags appropriately.
|
||||
// This is called by main.main(). It only needs to happen once to the rootCmd.
|
||||
func Execute() {
|
||||
err := rootCmd.Execute()
|
||||
if err != nil {
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
func init() {
|
||||
// Here you will define your flags and configuration settings.
|
||||
// Cobra supports persistent flags, which, if defined here,
|
||||
// will be global for your application.
|
||||
|
||||
// Cobra also supports local flags, which will only run
|
||||
// when this action is called directly.
|
||||
rootCmd.Flags().StringVarP(&Team, "team", "t", "", "team to get updates for")
|
||||
rootCmd.MarkFlagRequired("team")
|
||||
}
|
||||
Reference in New Issue
Block a user