mirror of
https://github.com/nprasad2077/NBA_Go.git
synced 2026-09-22 14:05:13 +00:00
shot chart create
This commit is contained in:
@@ -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
@@ -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": {
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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"`
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
Reference in New Issue
Block a user