shot chart create

This commit is contained in:
Ravi Prasad
2025-05-12 21:06:03 -05:00
parent a1809d3808
commit f0447c2141
10 changed files with 612 additions and 2 deletions
+1
View File
@@ -21,6 +21,7 @@ func InitDB() *gorm.DB {
// Auto migrate models
db.AutoMigrate(&models.PlayerAdvancedStat{})
db.AutoMigrate(&models.PlayerTotalStat{})
db.AutoMigrate(&models.PlayerShotChart{})
db.AutoMigrate(&models.APIKey{})
@@ -0,0 +1,73 @@
// NBA_Go/controllers/player_shot_chart_controller.go
package controllers
import (
"github.com/gofiber/fiber/v2"
"github.com/nprasad2077/NBA_Go/models"
"github.com/nprasad2077/NBA_Go/services"
"gorm.io/gorm"
)
// FetchPlayerShotChart godoc
// @Summary Fetch a single player's shot-chart data from external API
// @Description Imports shot-chart data for the given playerId and stores/updates in DB
// @Tags PlayerShotChart
// @Accept json
// @Produce json
// @Param playerId query string true "Player ID (e.g., hardeja01)"
// @Success 200 {object} map[string]string
// @Failure 400 {object} map[string]string
// @Failure 500 {object} map[string]string
// @Router /api/playershotchart/fetch [get]
func FetchPlayerShotChart(db *gorm.DB) fiber.Handler {
return func(c *fiber.Ctx) error {
pid := c.Query("playerId")
if pid == "" {
return c.Status(400).JSON(fiber.Map{
"error": "playerId query parameter is required",
})
}
if err := services.FetchAndStoreShotChartForPlayer(db, pid); err != nil {
return c.Status(500).JSON(fiber.Map{"error": err.Error()})
}
return c.JSON(fiber.Map{"message": "Shot chart for " + pid + " fetched and saved."})
}
}
// GetPlayerShotChart godoc
// @Security ApiKeyAuth
// @Summary Get shot-chart data
// @Description Returns shot-chart points, optionally filtered by playerId and/or season
// @Tags PlayerShotChart
// @Accept json
// @Produce json
// @Param playerId query string false "Player ID (e.g., hardeja01)"
// @Param season query int false "Season (e.g., 2023)"
// @Success 200 {array} models.PlayerShotChart
// @Failure 500 {object} map[string]string
// @Router /api/playershotchart [get]
func GetPlayerShotChart(db *gorm.DB) fiber.Handler {
return func(c *fiber.Ctx) error {
var shots []models.PlayerShotChart
// Start with the base model
query := db.Model(&models.PlayerShotChart{})
// Optional filter: playerId
if pid := c.Query("playerId"); pid != "" {
query = query.Where("player_id = ?", pid)
}
// Optional filter: season
if s := c.QueryInt("season", 0); s != 0 {
query = query.Where("season = ?", s)
}
// Execute
if err := query.Find(&shots).Error; err != nil {
return c.Status(500).JSON(fiber.Map{"error": err.Error()})
}
return c.JSON(shots)
}
}
+163
View File
@@ -106,6 +106,113 @@ const docTemplate = `{
}
}
},
"/api/playershotchart": {
"get": {
"security": [
{
"ApiKeyAuth": []
}
],
"description": "Returns shot-chart points, optionally filtered by playerId and/or season",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"PlayerShotChart"
],
"summary": "Get shot-chart data",
"parameters": [
{
"type": "string",
"description": "Player ID (e.g., hardeja01)",
"name": "playerId",
"in": "query"
},
{
"type": "integer",
"description": "Season (e.g., 2023)",
"name": "season",
"in": "query"
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"type": "array",
"items": {
"$ref": "#/definitions/models.PlayerShotChart"
}
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"type": "object",
"additionalProperties": {
"type": "string"
}
}
}
}
}
},
"/api/playershotchart/fetch": {
"get": {
"description": "Imports shot-chart data for the given playerId and stores/updates in DB",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"PlayerShotChart"
],
"summary": "Fetch a single player's shot-chart data from external API",
"parameters": [
{
"type": "string",
"description": "Player ID (e.g., hardeja01)",
"name": "playerId",
"in": "query",
"required": true
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"type": "object",
"additionalProperties": {
"type": "string"
}
}
},
"400": {
"description": "Bad Request",
"schema": {
"type": "object",
"additionalProperties": {
"type": "string"
}
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"type": "object",
"additionalProperties": {
"type": "string"
}
}
}
}
}
},
"/api/playertotals": {
"get": {
"security": [
@@ -320,6 +427,62 @@ const docTemplate = `{
"type": "number"
}
}
},
"models.PlayerShotChart": {
"type": "object",
"properties": {
"date": {
"type": "string"
},
"distanceFt": {
"type": "integer"
},
"id": {
"type": "integer"
},
"lead": {
"type": "boolean"
},
"left": {
"type": "integer"
},
"opponent": {
"type": "string"
},
"opponentTeamScore": {
"type": "integer"
},
"playerId": {
"type": "string"
},
"playerName": {
"type": "string"
},
"qtr": {
"type": "string"
},
"result": {
"type": "boolean"
},
"season": {
"type": "integer"
},
"shotType": {
"type": "string"
},
"team": {
"type": "string"
},
"teamScore": {
"type": "integer"
},
"timeRemaining": {
"type": "string"
},
"top": {
"type": "integer"
}
}
}
},
"securityDefinitions": {
+163
View File
@@ -103,6 +103,113 @@
}
}
},
"/api/playershotchart": {
"get": {
"security": [
{
"ApiKeyAuth": []
}
],
"description": "Returns shot-chart points, optionally filtered by playerId and/or season",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"PlayerShotChart"
],
"summary": "Get shot-chart data",
"parameters": [
{
"type": "string",
"description": "Player ID (e.g., hardeja01)",
"name": "playerId",
"in": "query"
},
{
"type": "integer",
"description": "Season (e.g., 2023)",
"name": "season",
"in": "query"
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"type": "array",
"items": {
"$ref": "#/definitions/models.PlayerShotChart"
}
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"type": "object",
"additionalProperties": {
"type": "string"
}
}
}
}
}
},
"/api/playershotchart/fetch": {
"get": {
"description": "Imports shot-chart data for the given playerId and stores/updates in DB",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"PlayerShotChart"
],
"summary": "Fetch a single player's shot-chart data from external API",
"parameters": [
{
"type": "string",
"description": "Player ID (e.g., hardeja01)",
"name": "playerId",
"in": "query",
"required": true
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"type": "object",
"additionalProperties": {
"type": "string"
}
}
},
"400": {
"description": "Bad Request",
"schema": {
"type": "object",
"additionalProperties": {
"type": "string"
}
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"type": "object",
"additionalProperties": {
"type": "string"
}
}
}
}
}
},
"/api/playertotals": {
"get": {
"security": [
@@ -317,6 +424,62 @@
"type": "number"
}
}
},
"models.PlayerShotChart": {
"type": "object",
"properties": {
"date": {
"type": "string"
},
"distanceFt": {
"type": "integer"
},
"id": {
"type": "integer"
},
"lead": {
"type": "boolean"
},
"left": {
"type": "integer"
},
"opponent": {
"type": "string"
},
"opponentTeamScore": {
"type": "integer"
},
"playerId": {
"type": "string"
},
"playerName": {
"type": "string"
},
"qtr": {
"type": "string"
},
"result": {
"type": "boolean"
},
"season": {
"type": "integer"
},
"shotType": {
"type": "string"
},
"team": {
"type": "string"
},
"teamScore": {
"type": "integer"
},
"timeRemaining": {
"type": "string"
},
"top": {
"type": "integer"
}
}
}
},
"securityDefinitions": {
+108
View File
@@ -81,6 +81,43 @@ definitions:
winSharesPer:
type: number
type: object
models.PlayerShotChart:
properties:
date:
type: string
distanceFt:
type: integer
id:
type: integer
lead:
type: boolean
left:
type: integer
opponent:
type: string
opponentTeamScore:
type: integer
playerId:
type: string
playerName:
type: string
qtr:
type: string
result:
type: boolean
season:
type: integer
shotType:
type: string
team:
type: string
teamScore:
type: integer
timeRemaining:
type: string
top:
type: integer
type: object
info:
contact: {}
description: Stats service with API-key auth
@@ -147,6 +184,77 @@ paths:
summary: Get player advanced stats
tags:
- PlayerStats
/api/playershotchart:
get:
consumes:
- application/json
description: Returns shot-chart points, optionally filtered by playerId and/or
season
parameters:
- description: Player ID (e.g., hardeja01)
in: query
name: playerId
type: string
- description: Season (e.g., 2023)
in: query
name: season
type: integer
produces:
- application/json
responses:
"200":
description: OK
schema:
items:
$ref: '#/definitions/models.PlayerShotChart'
type: array
"500":
description: Internal Server Error
schema:
additionalProperties:
type: string
type: object
security:
- ApiKeyAuth: []
summary: Get shot-chart data
tags:
- PlayerShotChart
/api/playershotchart/fetch:
get:
consumes:
- application/json
description: Imports shot-chart data for the given playerId and stores/updates
in DB
parameters:
- description: Player ID (e.g., hardeja01)
in: query
name: playerId
required: true
type: string
produces:
- application/json
responses:
"200":
description: OK
schema:
additionalProperties:
type: string
type: object
"400":
description: Bad Request
schema:
additionalProperties:
type: string
type: object
"500":
description: Internal Server Error
schema:
additionalProperties:
type: string
type: object
summary: Fetch a single player's shot-chart data from external API
tags:
- PlayerShotChart
/api/playertotals:
get:
consumes:
+10
View File
@@ -48,3 +48,13 @@ func importPlayerPlayoffs(db *gorm.DB) {
time.Sleep(1500 * time.Millisecond)
}
}
// importPlayerShotChart fetches shot-charts for every known player
func importPlayerShotChart(db *gorm.DB) {
const firstID = "hardeja01"
log.Printf("▶️ importing shot chart for player %s…", firstID)
if err := services.FetchAndStoreShotChartForPlayer(db, firstID); err != nil {
log.Printf("shot chart import failed for %s: %v", firstID, err)
}
// you can add more IDs here or just rely on the API endpoint after
}
+3
View File
@@ -44,6 +44,8 @@ func main() {
log.Println("🎉 Player Totals Import completed successfully")
importPlayerPlayoffs(db)
log.Println("🎉 Player Totals Playoffs Import completed successfully")
importPlayerShotChart(db)
log.Println("🎉 Player Shot Chart Import Completed Successfully ")
log.Println("🎉 Import completed successfully")
return
}
@@ -80,6 +82,7 @@ func main() {
app.Use(middleware.APIKeyAuth(db))
routes.RegisterPlayerAdvancedRoutes(app, db)
routes.RegisterPlayerTotalRoutes(app, db)
routes.RegisterPlayerShotChartRoutes(app, db)
/* ---------- START & SHUTDOWN ---------- */
go func() {
+2 -2
View File
@@ -5,8 +5,8 @@ import "gorm.io/gorm"
type PlayerShotChart struct {
gorm.Model `swaggerignore:"true"`
ExternalID int `json:"id"`
PlayerID string `gorm:"not null;index:idx_shotchart_player_external,unique" json:"playerId"`
ExternalID int `gorm:"not null;uniqueIndex:idx_shotchart_player_external" json:"id"`
PlayerID string `gorm:"not null;uniqueIndex:idx_shotchart_player_external" json:"playerId"`
PlayerName string `json:"playerName"`
Top int `json:"top"`
Left int `json:"left"`
+13
View File
@@ -0,0 +1,13 @@
package routes
import (
"github.com/gofiber/fiber/v2"
"github.com/nprasad2077/NBA_Go/controllers"
"gorm.io/gorm"
)
func RegisterPlayerShotChartRoutes(app *fiber.App, db *gorm.DB) {
api := app.Group("/api/playershotchart")
api.Get("/fetch", controllers.FetchPlayerShotChart(db))
api.Get("/", controllers.GetPlayerShotChart(db))
}
+76
View File
@@ -0,0 +1,76 @@
package services
import (
"encoding/json"
"fmt"
"log"
"time"
"github.com/nprasad2077/NBA_Go/models"
"github.com/nprasad2077/NBA_Go/utils"
"github.com/nprasad2077/NBA_Go/utils/metrics"
"gorm.io/gorm"
"gorm.io/gorm/clause"
)
// FetchAndStoreShotChartForPlayer fetches a single player's shot chart, parses
// each shot's date into a season, and upserts into DB.
func FetchAndStoreShotChartForPlayer(db *gorm.DB, playerId string) error {
metrics.DBOperationsTotal.WithLabelValues("fetch", "player_shot_chart").Inc()
url := fmt.Sprintf("http://rest.nbaapi.com/api/ShotChartData/playerid/%s", playerId)
body, err := utils.GetJSON(url)
if err != nil {
return err
}
var shots []models.PlayerShotChart
if err := json.Unmarshal(body, &shots); err != nil {
return err
}
for _, shot := range shots {
// parse date "Feb 11, 2023" → time.Time
if t, err := time.Parse("Jan 2, 2006", shot.Date); err == nil {
shot.Season = t.Year()
}
err := db.Clauses(clause.OnConflict{
Columns: []clause.Column{{Name: "player_id"}, {Name: "external_id"}},
DoUpdates: clause.AssignmentColumns([]string{
"player_name", "top", "left", "date", "qtr",
"time_remaining", "result", "shot_type", "distance_ft",
"lead", "team_score", "opponent_team_score", "opponent",
"team", "season",
}),
}).Create(&shot).Error
metrics.DBOperationsTotal.WithLabelValues("store", "player_shot_chart").Inc()
if err != nil {
log.Printf("Failed to upsert shot chart for %s id=%d: %v",
shot.PlayerID, shot.ExternalID, err)
}
}
return nil
}
// FetchAndStoreAllPlayerShotCharts loads every distinct playerId from your stats
// tables and invokes the per-player fetch.
func FetchAndStoreAllPlayerShotCharts(db *gorm.DB) error {
var playerIds []string
// gather from total stats (could also include advanced stats)
db.Model(&models.PlayerTotalStat{}).
Distinct("player_id").
Pluck("player_id", &playerIds)
for _, pid := range playerIds {
if err := FetchAndStoreShotChartForPlayer(db, pid); err != nil {
log.Printf("Error importing shot chart for %s: %v", pid, err)
}
// throttle
time.Sleep(1100 * time.Millisecond)
}
return nil
}