From e269bc5145bb89dd22fb48d9de1ac784ffab756d Mon Sep 17 00:00:00 2001 From: Ravi Prasad Date: Wed, 30 Apr 2025 21:19:31 -0500 Subject: [PATCH] backend functioning. Player models init --- .gitignore | 6 ++++ config/database.go | 19 +++++++++++++ controllers/player_controller.go | 35 +++++++++++++++++++++++ go.mod | 24 ++++++++++++++++ go.sum | 41 +++++++++++++++++++++++++++ main.go | 26 +++++++++++++++++ models/player_stat.go | 36 ++++++++++++++++++++++++ routes/player_routes.go | 14 ++++++++++ services/player_service.go | 48 ++++++++++++++++++++++++++++++++ utils/http_client.go | 29 +++++++++++++++++++ 10 files changed, 278 insertions(+) create mode 100644 config/database.go create mode 100644 controllers/player_controller.go create mode 100644 go.mod create mode 100644 go.sum create mode 100644 main.go create mode 100644 models/player_stat.go create mode 100644 routes/player_routes.go create mode 100644 services/player_service.go create mode 100644 utils/http_client.go diff --git a/.gitignore b/.gitignore index 6f72f89..fb763e3 100644 --- a/.gitignore +++ b/.gitignore @@ -23,3 +23,9 @@ go.work.sum # env file .env + + +data/nba.db +/data/ +/data +/data/* \ No newline at end of file diff --git a/config/database.go b/config/database.go new file mode 100644 index 0000000..24874f9 --- /dev/null +++ b/config/database.go @@ -0,0 +1,19 @@ +package config + +import ( + "log" + "gorm.io/driver/sqlite" // Or Postgres + "gorm.io/gorm" + "github.com/nprasad2077/NBA_Go/models" +) + +func InitDB() *gorm.DB { + db, err := gorm.Open(sqlite.Open("/Users/ravi/code/projects/NBA_Go/data/nba.db"), &gorm.Config{}) + if err != nil { + log.Fatal("Failed to connect database") + } + + db.AutoMigrate(&models.PlayerStat{}) + + return db +} \ No newline at end of file diff --git a/controllers/player_controller.go b/controllers/player_controller.go new file mode 100644 index 0000000..8862b86 --- /dev/null +++ b/controllers/player_controller.go @@ -0,0 +1,35 @@ +package controllers + +import ( + "github.com/gofiber/fiber/v2" + "gorm.io/gorm" + "github.com/nprasad2077/NBA_Go/services" + "github.com/nprasad2077/NBA_Go/models" +) + +func FetchPlayerStats(db *gorm.DB) fiber.Handler { + return func(c *fiber.Ctx) error { + season := c.QueryInt("season", 2025) + + // Call service and assign err here + err := services.FetchAndStorePlayerStats(db, season) + if err != nil { + return c.Status(500).JSON(fiber.Map{"error": err.Error()}) + } + + return c.JSON(fiber.Map{"message": "Player stats fetched and saved."}) + } +} + + +func GetAllPlayerStats(db *gorm.DB) fiber.Handler { + return func(c *fiber.Ctx) error { + var stats []models.PlayerStat + + if err := db.Find(&stats).Error; err != nil { + return c.Status(500).JSON(fiber.Map{"error": err.Error()}) + } + + return c.JSON(stats) + } +} \ No newline at end of file diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..d529792 --- /dev/null +++ b/go.mod @@ -0,0 +1,24 @@ +module github.com/nprasad2077/NBA_Go + +go 1.24.2 + +require ( + github.com/andybalholm/brotli v1.1.0 // indirect + github.com/gofiber/fiber/v2 v2.52.6 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/jinzhu/inflection v1.0.0 // indirect + github.com/jinzhu/now v1.1.5 // indirect + github.com/klauspost/compress v1.17.9 // indirect + github.com/mattn/go-colorable v0.1.13 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/mattn/go-runewidth v0.0.16 // indirect + github.com/mattn/go-sqlite3 v1.14.22 // indirect + github.com/rivo/uniseg v0.2.0 // indirect + github.com/valyala/bytebufferpool v1.0.0 // indirect + github.com/valyala/fasthttp v1.51.0 // indirect + github.com/valyala/tcplisten v1.0.0 // indirect + golang.org/x/sys v0.28.0 // indirect + golang.org/x/text v0.20.0 // indirect + gorm.io/driver/sqlite v1.5.7 // indirect + gorm.io/gorm v1.26.0 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..8042874 --- /dev/null +++ b/go.sum @@ -0,0 +1,41 @@ +github.com/andybalholm/brotli v1.1.0 h1:eLKJA0d02Lf0mVpIDgYnqXcUn0GqVmEFny3VuID1U3M= +github.com/andybalholm/brotli v1.1.0/go.mod h1:sms7XGricyQI9K10gOSf56VKKWS4oLer58Q+mhRPtnY= +github.com/gofiber/fiber/v2 v2.52.6 h1:Rfp+ILPiYSvvVuIPvxrBns+HJp8qGLDnLJawAu27XVI= +github.com/gofiber/fiber/v2 v2.52.6/go.mod h1:YEcBbO/FB+5M1IZNBP9FO3J9281zgPAreiI1oqg8nDw= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= +github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= +github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ= +github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= +github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA= +github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw= +github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= +github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= +github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= +github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU= +github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= +github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= +github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= +github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= +github.com/valyala/fasthttp v1.51.0 h1:8b30A5JlZ6C7AS81RsWjYMQmrZG6feChmgAolCl1SqA= +github.com/valyala/fasthttp v1.51.0/go.mod h1:oI2XroL+lI7vdXyYoQk03bXBThfFl2cVdIA3Xl7cH8g= +github.com/valyala/tcplisten v1.0.0 h1:rBHj/Xf+E1tRGZyWIWwJDiRY0zc1Js+CV5DqwacVSA8= +github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc= +golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA= +golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/text v0.20.0 h1:gK/Kv2otX8gz+wn7Rmb3vT96ZwuoxnQlY+HlJVj7Qug= +golang.org/x/text v0.20.0/go.mod h1:D4IsuqiFMhST5bX19pQ9ikHC2GsaKyk/oF+pn3ducp4= +gorm.io/driver/sqlite v1.5.7 h1:8NvsrhP0ifM7LX9G4zPB97NwovUakUxc+2V2uuf3Z1I= +gorm.io/driver/sqlite v1.5.7/go.mod h1:U+J8craQU6Fzkcvu8oLeAQmi50TkwPEhHDEjQZXDah4= +gorm.io/gorm v1.25.7-0.20240204074919-46816ad31dde h1:9DShaph9qhkIYw7QF91I/ynrr4cOO2PZra2PFD7Mfeg= +gorm.io/gorm v1.25.7-0.20240204074919-46816ad31dde/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8= +gorm.io/gorm v1.26.0 h1:9lqQVPG5aNNS6AyHdRiwScAVnXHg/L/Srzx55G5fOgs= +gorm.io/gorm v1.26.0/go.mod h1:8Z33v652h4//uMA76KjeDH8mJXPm1QNCYrMeatR0DOE= diff --git a/main.go b/main.go new file mode 100644 index 0000000..ad325ab --- /dev/null +++ b/main.go @@ -0,0 +1,26 @@ +package main + +import ( + "log" + "github.com/gofiber/fiber/v2" + "github.com/nprasad2077/NBA_Go/config" + "github.com/nprasad2077/NBA_Go/routes" + "github.com/nprasad2077/NBA_Go/services" +) + +func main() { + app := fiber.New() + db := config.InitDB() + + // Automatically fetch player stats on startup + go func() { + if err := services.FetchAndStorePlayerStats(db, 2025); err != nil { + log.Println("Initial fetch failed:", err) + } else { + log.Println("Initial player stats fetch successful.") + } + }() + + routes.RegisterPlayerRoutes(app, db) + app.Listen(":3001") +} \ No newline at end of file diff --git a/models/player_stat.go b/models/player_stat.go new file mode 100644 index 0000000..bbbb2c0 --- /dev/null +++ b/models/player_stat.go @@ -0,0 +1,36 @@ +package models + +import "gorm.io/gorm" + +type PlayerStat struct { + gorm.Model + ExternalID int `json:"id"` + PlayerID string `gorm:"not null;uniqueIndex:idx_player_season_team" json:"playerId"` + PlayerName string `json:"playerName"` + Position string `json:"position"` + Age int `json:"age"` + Games int `json:"games"` + MinutesPlayed int `json:"minutesPlayed"` + PER float64 `json:"per"` + TSPercent float64 `json:"tsPercent"` + ThreePAR float64 `json:"threePAR"` + FTR float64 `json:"ftr"` + OffensiveRBPercent float64 `json:"offensiveRBPercent"` + DefensiveRBPercent float64 `json:"defensiveRBPercent"` + TotalRBPercent float64 `json:"totalRBPercent"` + AssistPercent float64 `json:"assistPercent"` + StealPercent float64 `json:"stealPercent"` + BlockPercent float64 `json:"blockPercent"` + TurnoverPercent float64 `json:"turnoverPercent"` + UsagePercent float64 `json:"usagePercent"` + OffensiveWS float64 `json:"offensiveWS"` + DefensiveWS float64 `json:"defensiveWS"` + WinShares float64 `json:"winShares"` + WinSharesPer float64 `json:"winSharesPer"` + OffensiveBox float64 `json:"offensiveBox"` + DefensiveBox float64 `json:"defensiveBox"` + Box float64 `json:"box"` + VORP float64 `json:"vorp"` + Team string `gorm:"not null;uniqueIndex:idx_player_season_team" json:"team"` + Season int `gorm:"not null;uniqueIndex:idx_player_season_team" json:"season"` +} \ No newline at end of file diff --git a/routes/player_routes.go b/routes/player_routes.go new file mode 100644 index 0000000..71f6f3f --- /dev/null +++ b/routes/player_routes.go @@ -0,0 +1,14 @@ +package routes + +import ( + "github.com/gofiber/fiber/v2" + "gorm.io/gorm" + "github.com/nprasad2077/NBA_Go/controllers" +) + +func RegisterPlayerRoutes(app *fiber.App, db *gorm.DB) { + api := app.Group("/api/playerstats") + + api.Get("/fetch", controllers.FetchPlayerStats(db)) + api.Get("/", controllers.GetAllPlayerStats(db)) // ✅ Add this line +} \ No newline at end of file diff --git a/services/player_service.go b/services/player_service.go new file mode 100644 index 0000000..6d93877 --- /dev/null +++ b/services/player_service.go @@ -0,0 +1,48 @@ +package services + +import ( + "encoding/json" + "fmt" + "log" + "github.com/nprasad2077/NBA_Go/models" + "github.com/nprasad2077/NBA_Go/utils" + "gorm.io/gorm" + "gorm.io/gorm/clause" +) + +func FetchAndStorePlayerStats(db *gorm.DB, season int) error { + url := fmt.Sprintf("http://rest.nbaapi.com/api/PlayerDataAdvanced/query?season=%d&sortBy=Points&ascending=false&pageNumber=1&pageSize=20", season) + + body, err := utils.GetJSON(url) + if err != nil { + return err + } + + var stats []models.PlayerStat + if err := json.Unmarshal(body, &stats); err != nil { + return err + } + + for _, stat := range stats { + // Ensure the season is included from the query param + stat.Season = season + + err := db.Clauses(clause.OnConflict{ + Columns: []clause.Column{{Name: "player_id"}, {Name: "season"}, {Name: "team"}}, + DoUpdates: clause.AssignmentColumns([]string{ + "external_id", "player_name", "position", "age", "games", + "minutes_played", "per", "ts_percent", "three_par", "ftr", + "offensive_rb_percent", "defensive_rb_percent", "total_rb_percent", + "assist_percent", "steal_percent", "block_percent", "turnover_percent", + "usage_percent", "offensive_ws", "defensive_ws", "win_shares", + "win_shares_per", "offensive_box", "defensive_box", "box", "vorp", + }), + }).Create(&stat).Error + + if err != nil { + log.Printf("Failed to upsert stat for playerId %s (%s): %v", stat.PlayerID, stat.Team, err) + } + } + + return nil +} \ No newline at end of file diff --git a/utils/http_client.go b/utils/http_client.go new file mode 100644 index 0000000..05d4d37 --- /dev/null +++ b/utils/http_client.go @@ -0,0 +1,29 @@ +package utils + +import ( + "errors" + "io/ioutil" + "net/http" + "time" +) + +func GetJSON(url string) ([]byte, error) { + client := &http.Client{Timeout: 10 * time.Second} + + resp, err := client.Get(url) + if err != nil { + return nil, err + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return nil, errors.New("received non-200 response: " + resp.Status) + } + + body, err := ioutil.ReadAll(resp.Body) + if err != nil { + return nil, err + } + + return body, nil +} \ No newline at end of file