From e3c8c76f356d7d48f4efc3fe31a140eb9bcabab0 Mon Sep 17 00:00:00 2001 From: Ravi Prasad Date: Wed, 30 Apr 2025 22:49:38 -0500 Subject: [PATCH] populate both tables in loop --- config/database.go | 3 +- ...oller.go => player_advanced_controller.go} | 12 +-- controllers/player_total_controller.go | 98 +++++++++++++++++++ docs/docs.go | 81 ++++++++++++++- docs/swagger.json | 81 ++++++++++++++- docs/swagger.yaml | 55 ++++++++++- main.go | 34 ++++++- models/player_advanced_stat.go | 36 +++++++ .../{player_stat.go => player_stat copy.go} | 0 models/player_total_stat.go | 39 ++++++++ routes/player_advanced_routes.go | 14 +++ routes/player_routes.go | 14 --- routes/player_total_routes.go | 14 +++ ..._service.go => player_advanced_service.go} | 6 +- services/player_total_service.go | 51 ++++++++++ 15 files changed, 506 insertions(+), 32 deletions(-) rename controllers/{player_controller.go => player_advanced_controller.go} (88%) create mode 100644 controllers/player_total_controller.go create mode 100644 models/player_advanced_stat.go rename models/{player_stat.go => player_stat copy.go} (100%) create mode 100644 models/player_total_stat.go create mode 100644 routes/player_advanced_routes.go delete mode 100644 routes/player_routes.go create mode 100644 routes/player_total_routes.go rename services/{player_service.go => player_advanced_service.go} (91%) create mode 100644 services/player_total_service.go diff --git a/config/database.go b/config/database.go index 24874f9..583e099 100644 --- a/config/database.go +++ b/config/database.go @@ -13,7 +13,8 @@ func InitDB() *gorm.DB { log.Fatal("Failed to connect database") } - db.AutoMigrate(&models.PlayerStat{}) + db.AutoMigrate(&models.PlayerAdvancedStat{}) + db.AutoMigrate(&models.PlayerTotalStat{}) return db } \ No newline at end of file diff --git a/controllers/player_controller.go b/controllers/player_advanced_controller.go similarity index 88% rename from controllers/player_controller.go rename to controllers/player_advanced_controller.go index e7e8539..e0e1477 100644 --- a/controllers/player_controller.go +++ b/controllers/player_advanced_controller.go @@ -7,12 +7,12 @@ import ( "github.com/nprasad2077/NBA_Go/models" ) -func FetchPlayerStats(db *gorm.DB) fiber.Handler { +func FetchPlayerAdvancedStats(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) + err := services.FetchAndStorePlayerAdvancedStats(db, season) if err != nil { return c.Status(500).JSON(fiber.Map{"error": err.Error()}) } @@ -37,10 +37,10 @@ func FetchPlayerStats(db *gorm.DB) fiber.Handler { // @Param sortBy query string false "Field to sort by (e.g., per, games, winShares)" // @Param ascending query bool false "Sort ascending (default false)" // @Success 200 {object} map[string]interface{} -// @Router /api/playerstats [get] -func GetAllPlayerStats(db *gorm.DB) fiber.Handler { +// @Router /api/playeradvancedstats [get] +func GetAllAdvancedPlayerStats(db *gorm.DB) fiber.Handler { return func(c *fiber.Ctx) error { - var stats []models.PlayerStat + var stats []models.PlayerAdvancedStat // Filters season := c.QueryInt("season", 0) @@ -61,7 +61,7 @@ func GetAllPlayerStats(db *gorm.DB) fiber.Handler { } // Build query - query := db.Model(&models.PlayerStat{}) + query := db.Model(&models.PlayerAdvancedStat{}) if season != 0 { query = query.Where("season = ?", season) diff --git a/controllers/player_total_controller.go b/controllers/player_total_controller.go new file mode 100644 index 0000000..3424234 --- /dev/null +++ b/controllers/player_total_controller.go @@ -0,0 +1,98 @@ +// FetchPlayerTotalStats godoc +// @Summary Fetch player total stats from external API +// @Description Imports totals data and stores or updates in DB +// @Tags PlayerTotals +// @Accept json +// @Produce json +// @Param season query int false "Season (e.g. 2000)" +// @Success 200 {object} map[string]string +// @Failure 500 {object} map[string]string +// @Router /api/playertotals/fetch [get] + +package controllers + +import ( + "github.com/gofiber/fiber/v2" + "github.com/nprasad2077/NBA_Go/services" + "gorm.io/gorm" + "github.com/nprasad2077/NBA_Go/models" +) + +func FetchPlayerTotalStats(db *gorm.DB) fiber.Handler { + return func(c *fiber.Ctx) error { + season := c.QueryInt("season", 2025) + + err := services.FetchAndStorePlayerTotalStats(db, season) + if err != nil { + return c.Status(500).JSON(fiber.Map{"error": err.Error()}) + } + + return c.JSON(fiber.Map{"message": "Player total stats fetched and saved."}) + } +} + + +// GetPlayerTotalStats godoc +// @Summary Get player total stats +// @Description Filter and paginate player totals +// @Tags PlayerTotals +// @Accept json +// @Produce json +// @Param season query int false "Season (e.g. 2000)" +// @Param team query string false "Team abbreviation (e.g. LAL)" +// @Param playerId query string false "Player ID (e.g. greenac01)" +// @Param page query int false "Page number" default(1) +// @Param pageSize query int false "Page size" default(20) +// @Param sortBy query string false "Field to sort by (e.g. points, assists)" +// @Param ascending query bool false "Sort ascending (default false)" +// @Success 200 {object} map[string]interface{} +// @Failure 500 {object} map[string]string +// @Router /api/playertotals [get] +func GetPlayerTotalStats(db *gorm.DB) fiber.Handler { + return func(c *fiber.Ctx) error { + var stats []models.PlayerTotalStat + + season := c.QueryInt("season", 0) + team := c.Query("team") + playerId := c.Query("playerId") + page := c.QueryInt("page", 1) + pageSize := c.QueryInt("pageSize", 20) + sortBy := c.Query("sortBy", "points") + ascending := c.QueryBool("ascending", false) + offset := (page - 1) * pageSize + order := sortBy + " DESC" + if ascending { + order = sortBy + " ASC" + } + + query := db.Model(&models.PlayerTotalStat{}) + + if season != 0 { + query = query.Where("season = ?", season) + } + if team != "" { + query = query.Where("team = ?", team) + } + if playerId != "" { + query = query.Where("player_id = ?", playerId) + } + + var total int64 + query.Count(&total) + + err := query.Order(order).Limit(pageSize).Offset(offset).Find(&stats).Error + if err != nil { + return c.Status(500).JSON(fiber.Map{"error": err.Error()}) + } + + return c.JSON(fiber.Map{ + "data": stats, + "pagination": fiber.Map{ + "total": total, + "page": page, + "pageSize": pageSize, + "pages": (total + int64(pageSize) - 1) / int64(pageSize), + }, + }) + } +} \ No newline at end of file diff --git a/docs/docs.go b/docs/docs.go index c67ec92..9383a90 100644 --- a/docs/docs.go +++ b/docs/docs.go @@ -15,7 +15,7 @@ const docTemplate = `{ "host": "{{.Host}}", "basePath": "{{.BasePath}}", "paths": { - "/api/playerstats": { + "/api/playeradvancedstats": { "get": { "description": "Returns filtered and paginated player stats", "consumes": [ @@ -84,6 +84,85 @@ const docTemplate = `{ } } } + }, + "/api/playertotals": { + "get": { + "description": "Filter and paginate player totals", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "PlayerTotals" + ], + "summary": "Get player total stats", + "parameters": [ + { + "type": "integer", + "description": "Season (e.g. 2000)", + "name": "season", + "in": "query" + }, + { + "type": "string", + "description": "Team abbreviation (e.g. LAL)", + "name": "team", + "in": "query" + }, + { + "type": "string", + "description": "Player ID (e.g. greenac01)", + "name": "playerId", + "in": "query" + }, + { + "type": "integer", + "default": 1, + "description": "Page number", + "name": "page", + "in": "query" + }, + { + "type": "integer", + "default": 20, + "description": "Page size", + "name": "pageSize", + "in": "query" + }, + { + "type": "string", + "description": "Field to sort by (e.g. points, assists)", + "name": "sortBy", + "in": "query" + }, + { + "type": "boolean", + "description": "Sort ascending (default false)", + "name": "ascending", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "object", + "additionalProperties": true + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + } } } }` diff --git a/docs/swagger.json b/docs/swagger.json index d4effd7..24002ea 100644 --- a/docs/swagger.json +++ b/docs/swagger.json @@ -4,7 +4,7 @@ "contact": {} }, "paths": { - "/api/playerstats": { + "/api/playeradvancedstats": { "get": { "description": "Returns filtered and paginated player stats", "consumes": [ @@ -73,6 +73,85 @@ } } } + }, + "/api/playertotals": { + "get": { + "description": "Filter and paginate player totals", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "PlayerTotals" + ], + "summary": "Get player total stats", + "parameters": [ + { + "type": "integer", + "description": "Season (e.g. 2000)", + "name": "season", + "in": "query" + }, + { + "type": "string", + "description": "Team abbreviation (e.g. LAL)", + "name": "team", + "in": "query" + }, + { + "type": "string", + "description": "Player ID (e.g. greenac01)", + "name": "playerId", + "in": "query" + }, + { + "type": "integer", + "default": 1, + "description": "Page number", + "name": "page", + "in": "query" + }, + { + "type": "integer", + "default": 20, + "description": "Page size", + "name": "pageSize", + "in": "query" + }, + { + "type": "string", + "description": "Field to sort by (e.g. points, assists)", + "name": "sortBy", + "in": "query" + }, + { + "type": "boolean", + "description": "Sort ascending (default false)", + "name": "ascending", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "object", + "additionalProperties": true + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + } } } } \ No newline at end of file diff --git a/docs/swagger.yaml b/docs/swagger.yaml index 83f5783..be018ff 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -1,7 +1,7 @@ info: contact: {} paths: - /api/playerstats: + /api/playeradvancedstats: get: consumes: - application/json @@ -48,4 +48,57 @@ paths: summary: Get player stats tags: - PlayerStats + /api/playertotals: + get: + consumes: + - application/json + description: Filter and paginate player totals + parameters: + - description: Season (e.g. 2000) + in: query + name: season + type: integer + - description: Team abbreviation (e.g. LAL) + in: query + name: team + type: string + - description: Player ID (e.g. greenac01) + in: query + name: playerId + type: string + - default: 1 + description: Page number + in: query + name: page + type: integer + - default: 20 + description: Page size + in: query + name: pageSize + type: integer + - description: Field to sort by (e.g. points, assists) + in: query + name: sortBy + type: string + - description: Sort ascending (default false) + in: query + name: ascending + type: boolean + produces: + - application/json + responses: + "200": + description: OK + schema: + additionalProperties: true + type: object + "500": + description: Internal Server Error + schema: + additionalProperties: + type: string + type: object + summary: Get player total stats + tags: + - PlayerTotals swagger: "2.0" diff --git a/main.go b/main.go index 22c6aa5..e8c5aa2 100644 --- a/main.go +++ b/main.go @@ -2,6 +2,8 @@ package main import ( "log" + "time" + "github.com/gofiber/fiber/v2/middleware/logger" "github.com/gofiber/fiber/v2" "github.com/nprasad2077/NBA_Go/config" "github.com/nprasad2077/NBA_Go/routes" @@ -12,22 +14,44 @@ import ( 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.") + for season := 2020; season <= 2025; season++ { + if err := services.FetchAndStorePlayerAdvancedStats(db, season); err != nil { + log.Printf("Fetch failed for player advanced season %d: %v\n", season, err) + } else { + log.Printf("Fetch successful for player advanced season %d\n", season) + } + time.Sleep(2 * time.Second) // optional delay } + log.Printf("player advanced Import Success") }() + go func() { + for season := 2020; season <= 2025; season++ { + if err := services.FetchAndStorePlayerTotalStats(db, season); err != nil { + log.Printf("Fetch failed for player totals season %d: %v\n", season, err) + } else { + log.Printf("Fetch successful for player totals season %d\n", season) + } + time.Sleep(1 * time.Second) // optional delay + } + log.Printf("player totals Import Success") + }() + + - routes.RegisterPlayerRoutes(app, db) + + routes.RegisterPlayerAdvancedRoutes(app, db) + routes.RegisterPlayerTotalRoutes(app, db) app.Get("/swagger/*", fiberswagger.WrapHandler) + app.Use(logger.New()) + app.Listen(":3001") } \ No newline at end of file diff --git a/models/player_advanced_stat.go b/models/player_advanced_stat.go new file mode 100644 index 0000000..8297014 --- /dev/null +++ b/models/player_advanced_stat.go @@ -0,0 +1,36 @@ +package models + +import "gorm.io/gorm" + +type PlayerAdvancedStat 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/models/player_stat.go b/models/player_stat copy.go similarity index 100% rename from models/player_stat.go rename to models/player_stat copy.go diff --git a/models/player_total_stat.go b/models/player_total_stat.go new file mode 100644 index 0000000..b129a9b --- /dev/null +++ b/models/player_total_stat.go @@ -0,0 +1,39 @@ +package models + +import "gorm.io/gorm" + +type PlayerTotalStat struct { + gorm.Model + ExternalID int `json:"id"` + PlayerID string `gorm:"not null;uniqueIndex:idx_total_player_season_team" json:"playerId"` + PlayerName string `json:"playerName"` + Position string `json:"position"` + Age int `json:"age"` + Games int `json:"games"` + GamesStarted int `json:"gamesStarted"` + MinutesPG float64 `json:"minutesPg"` + FieldGoals int `json:"fieldGoals"` + FieldAttempts int `json:"fieldAttempts"` + FieldPercent float64 `json:"fieldPercent"` + ThreeFG int `json:"threeFg"` + ThreeAttempts int `json:"threeAttempts"` + ThreePercent float64 `json:"threePercent"` + TwoFG int `json:"twoFg"` + TwoAttempts int `json:"twoAttempts"` + TwoPercent float64 `json:"twoPercent"` + EffectFGPercent float64 `json:"effectFgPercent"` + FT int `json:"ft"` + FTAttempts int `json:"ftAttempts"` + FTPercent float64 `json:"ftPercent"` + OffensiveRB int `json:"offensiveRb"` + DefensiveRB int `json:"defensiveRb"` + TotalRB int `json:"totalRb"` + Assists int `json:"assists"` + Steals int `json:"steals"` + Blocks int `json:"blocks"` + Turnovers int `json:"turnovers"` + PersonalFouls int `json:"personalFouls"` + Points int `json:"points"` + Team string `gorm:"not null;uniqueIndex:idx_total_player_season_team" json:"team"` + Season int `gorm:"not null;uniqueIndex:idx_total_player_season_team" json:"season"` +} \ No newline at end of file diff --git a/routes/player_advanced_routes.go b/routes/player_advanced_routes.go new file mode 100644 index 0000000..18681a6 --- /dev/null +++ b/routes/player_advanced_routes.go @@ -0,0 +1,14 @@ +package routes + +import ( + "github.com/gofiber/fiber/v2" + "gorm.io/gorm" + "github.com/nprasad2077/NBA_Go/controllers" +) + +func RegisterPlayerAdvancedRoutes(app *fiber.App, db *gorm.DB) { + api := app.Group("/api/playeradvancedstats") + + api.Get("/fetch", controllers.FetchPlayerAdvancedStats(db)) + api.Get("/", controllers.GetAllAdvancedPlayerStats(db)) // ✅ Add this line +} \ No newline at end of file diff --git a/routes/player_routes.go b/routes/player_routes.go deleted file mode 100644 index 71f6f3f..0000000 --- a/routes/player_routes.go +++ /dev/null @@ -1,14 +0,0 @@ -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/routes/player_total_routes.go b/routes/player_total_routes.go new file mode 100644 index 0000000..49d2ea9 --- /dev/null +++ b/routes/player_total_routes.go @@ -0,0 +1,14 @@ +package routes + +import ( + "github.com/gofiber/fiber/v2" + "github.com/nprasad2077/NBA_Go/controllers" + "gorm.io/gorm" +) + +func RegisterPlayerTotalRoutes(app *fiber.App, db *gorm.DB) { + api := app.Group("/api/playertotals") + + api.Get("/fetch", controllers.FetchPlayerTotalStats(db)) + api.Get("/", controllers.GetPlayerTotalStats(db)) +} \ No newline at end of file diff --git a/services/player_service.go b/services/player_advanced_service.go similarity index 91% rename from services/player_service.go rename to services/player_advanced_service.go index 6d93877..868b317 100644 --- a/services/player_service.go +++ b/services/player_advanced_service.go @@ -10,15 +10,15 @@ import ( "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) +func FetchAndStorePlayerAdvancedStats(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=1000", season) body, err := utils.GetJSON(url) if err != nil { return err } - var stats []models.PlayerStat + var stats []models.PlayerAdvancedStat if err := json.Unmarshal(body, &stats); err != nil { return err } diff --git a/services/player_total_service.go b/services/player_total_service.go new file mode 100644 index 0000000..4ef608a --- /dev/null +++ b/services/player_total_service.go @@ -0,0 +1,51 @@ +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 FetchAndStorePlayerTotalStats(db *gorm.DB, season int) error { + url := fmt.Sprintf( + "http://rest.nbaapi.com/api/PlayerDataTotals/query?season=%d&sortBy=PlayerName&ascending=true&pageNumber=1&pageSize=1000", + season, + ) + + body, err := utils.GetJSON(url) + if err != nil { + return err + } + + var stats []models.PlayerTotalStat + if err := json.Unmarshal(body, &stats); err != nil { + return err + } + + for _, stat := range stats { + stat.Season = season + + err := db.Clauses(clause.OnConflict{ + Columns: []clause.Column{{Name: "player_id"}, {Name: "season"}, {Name: "team"}}, + DoUpdates: clause.AssignmentColumns([]string{ + "player_name", "position", "age", "games", "games_started", + "minutes_pg", "field_goals", "field_attempts", "field_percent", + "three_fg", "three_attempts", "three_percent", "two_fg", "two_attempts", + "two_percent", "effect_fg_percent", "ft", "ft_attempts", "ft_percent", + "offensive_rb", "defensive_rb", "total_rb", "assists", "steals", + "blocks", "turnovers", "personal_fouls", "points", + }), + }).Create(&stat).Error + + if err != nil { + log.Printf("Failed to upsert PlayerTotalStat for playerId %s: %v", stat.PlayerID, err) + } + } + + return nil +} \ No newline at end of file