use cobra

This commit is contained in:
filifa
2024-07-14 16:43:00 -05:00
parent d064d7bd82
commit 8aff575b4b
11 changed files with 856 additions and 107 deletions

32
cmd/game.go Normal file
View 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
}

View 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
}

View 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,
}

View 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
}

View 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
}

View 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
View 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")
}